Skip to content

Instantly share code, notes, and snippets.

@aynik
Last active January 25, 2023 13:43
Show Gist options
  • Save aynik/8278bc9fb2157e724b2b1ee01cf76767 to your computer and use it in GitHub Desktop.
Save aynik/8278bc9fb2157e724b2b1ee01cf76767 to your computer and use it in GitHub Desktop.
netmd-session
#!/usr/bin/env bash
sed "s/.*=//g" \
| tr "\n" "|" \
| sed "s/|/ - /" \
| sed "s/\// /" \
| sed "s/ō/ou/g" \
| sed "s/ū/uu/g" \
| sed "s/ī/ii/g" \
| sed "s/ā/aa/g" \
| sed "s/ē/ee/g" \
| sed "s/á/a/g" \
| sed "s/é/e/g" \
| sed "s/í/i/g" \
| sed "s/ó/o/g" \
| sed "s/ú/u/g" \
| sed "s/ñ/n/g" \
| sed "s/.$//"
#!/usr/bin/env bash
set -e
netmd-session init &
sleep 1
echo -n "Reading... "
netmd-session read last "$1"
echo -n "Close... "
netmd-session close
#!/usr/bin/env bash
set -e
netmd-session init &
sleep 1
echo -n "Wiping... "
netmd-session wipe
FIRST_TRACK="$1"
ALBUM_TITLE="$(\
exiftool -q -s3 -Album -AlbumArtist "$FIRST_TRACK" \
| concat-tags \
| cutlet \
)"
echo -n "Title: $ALBUM_TITLE... "
netmd-session discTitle "$ALBUM_TITLE"
TMP_DIR="$(mktemp -d)"
for TRACK_PATH in "$@"; do
RAW_NAME="$(\
exiftool -q -s3 -Title -Artist "$TRACK_PATH" \
| concat-tags \
| cutlet \
)"
echo -n "Track: $RAW_NAME... "
RAW_PATH="$TMP_DIR/$RAW_NAME"
ffmpeg -nostdin -hide_banner -loglevel error -i "$TRACK_PATH" -f s16be -ar 44100 -ac 2 "$RAW_PATH"
netmd-session upload "$RAW_PATH"
done
echo -n "Data... "
netmd-session write $(archive)
echo -n "Finalize... "
netmd-session finalize
echo -n "Close... "
netmd-session close
#!/usr/bin/env bash
set -e
netmd-session init &
sleep 1
echo -n "Recovering... "
netmd-session recover
echo -n "Close... "
netmd-session close
echo "Note: perform hard power cycle"
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { WebUSB } = require("usb");
const { Worker } = require("worker_threads");
const { default: ipc } = require("@achrinza/node-ipc");
const {
MDTrack,
MDSession,
DiscFormat,
Wireformat,
DevicesIds,
EKBOpenSource,
openNewDevice,
prepareDownload,
readUTOCSector,
writeUTOCSector,
} = require("netmd-js");
const {
makeGetAsyncPacketIteratorOnWorkerThread,
} = require("netmd-js/dist/node-encrypt-worker");
const { concatUint8Arrays, createAeaHeader } = require("netmd-js/dist/utils");
const {
ExploitStateManager,
CachedSectorControlDownload,
ForceTOCEdit,
} = require("netmd-exploits");
const { parseTOC, reconstructTOC } = require("netmd-tocmanip");
const HEADER_SIZE = 8;
const PACKET_SIZE = 192;
async function concatUint8ArrayAsyncIterable(iterator) {
let data;
for await (let it of iterator) {
data = data ? concatUint8Arrays(data, it) : it;
}
return data;
}
function* packetize(payload) {
const header = Buffer.alloc(HEADER_SIZE);
header.writeDoubleLE(payload.length);
yield header;
for (let i = 0; i < payload.length; i += PACKET_SIZE) {
yield payload.slice(i, i + PACKET_SIZE);
}
}
async function* unpacketize(chunks) {
let isWavHeader = true;
let isHeader = true;
let payloadLength = 0;
for await (let chunk of chunks) {
if (isWavHeader) {
isWavHeader = false;
} else {
if (isHeader) {
payloadLength = Buffer.from(chunk.slice(0, HEADER_SIZE)).readDoubleLE();
chunk = chunk.slice(HEADER_SIZE);
isHeader = false;
}
if (payloadLength == 0) break;
for (let i = 0; i < chunk.length; i += PACKET_SIZE) {
const packet = chunk.slice(i, i + PACKET_SIZE);
const takeLength =
payloadLength < PACKET_SIZE ? payloadLength : packet.length;
yield packet.slice(0, takeLength);
payloadLength -= takeLength;
}
}
}
}
function throwError(message) {
throw new Error(message);
}
class NetMD {
worker() {
return new Worker(
path.join(
__dirname,
"..",
"node_modules",
"netmd-js",
"dist",
"node-encrypt-worker.js"
)
);
}
session(device) {
return new MDSession(device, new EKBOpenSource());
}
track(worker, wireformat, data, title = "") {
return new MDTrack(
title,
wireformat,
data.buffer,
0x400,
"",
makeGetAsyncPacketIteratorOnWorkerThread(worker)
);
}
get usb() {
Object.defineProperty(this, "usb", {
value: new WebUSB({
deviceTimeout: 100000,
allowedDevices: DevicesIds,
}),
});
return this.usb;
}
get device() {
Object.defineProperty(this, "device", {
value: openNewDevice(this.usb).then(
(it) => it ?? throwError("NetMD device not found")
),
});
return this.device;
}
get factory() {
Object.defineProperty(this, "factory", {
value: this.device.then((it) => it.factory()),
});
return this.factory;
}
get exploits() {
Object.defineProperty(this, "exploits", {
value: new Promise(async (resolve) => {
resolve(
await ExploitStateManager.create(
await this.device,
await this.factory
)
);
}),
});
return this.exploits;
}
get recovery() {
Object.defineProperty(this, "recovery", {
value: this.exploits.then((it) =>
it.require(CachedSectorControlDownload)
),
});
return this.recovery;
}
get editToc() {
Object.defineProperty(this, "editToc", {
value: this.exploits.then((it) => it.require(ForceTOCEdit)),
});
return this.editToc;
}
async *write(file) {
const device = await this.device;
await prepareDownload(device);
const session = this.session(device);
await session.init();
const worker = this.worker();
const data = await concatUint8ArrayAsyncIterable(
packetize(fs.readFileSync(file))
);
const track = this.track(worker, Wireformat.lp2, data);
let status;
try {
await session.downloadTrack(track);
status = "done";
} catch ({}) {}
worker.terminate();
await session.close();
yield status ?? "rejected";
}
async *read(track, file) {
console.log = function () {};
const device = await this.device;
const recovery = await this.recovery;
const trackIndex =
(track === "last" ? await device.getTrackCount() : Number(track)) - 1;
const data = await concatUint8ArrayAsyncIterable(
unpacketize(recovery.downloadTrackGenerator(trackIndex))
);
await recovery.finishDownload();
fs.writeFileSync(file, data);
yield "done";
}
async *upload(file) {
const device = await this.device;
await prepareDownload(device);
const session = this.session(device);
await session.init();
const worker = this.worker();
const data = fs.readFileSync(file);
const title = path.basename(file);
const track = this.track(worker, Wireformat.pcm, data, title);
let status;
try {
await session.downloadTrack(track);
status = "done";
} catch ({}) {}
worker.terminate();
await session.close();
yield status ?? "rejected";
}
async *download(track, file) {
const device = await this.device;
const trackIndex =
(track === "last" ? await device.getTrackCount() : Number(track)) - 1;
const [format, frames, result] = await device.saveTrackToArray(trackIndex);
const data = concatUint8Arrays(
createAeaHeader(
await device.getTrackTitle(trackIndex),
format === DiscFormat.spStereo ? 2 : 1,
frames
),
result
);
fs.writeFileSync(file, data);
yield "done";
}
async *dump(file) {
console.log = function () {};
const factory = await this.factory;
let fullToc;
for (let i = 0; i < 6; i++) {
fullToc = !fullToc
? await readUTOCSector(factory, i)
: concatUint8Arrays(fullToc, await readUTOCSector(factory, i));
}
fs.writeFileSync(file, fullToc);
yield "done";
}
async *load(file) {
console.log = function () {};
const factory = await this.factory;
const editToc = await this.editToc;
const tocBinary = fs.readFileSync(file);
for (let i = 0; i < 6; i++) {
const tocSlice = tocBinary.subarray(2352 * i, 2352 * (i + 1));
await writeUTOCSector(factory, i, tocSlice);
}
await editToc.forceTOCEdit();
yield "done";
}
async *finalize() {
console.log = function () {};
const factory = await this.factory;
const editToc = await this.editToc;
const parsedTOC = parseTOC(
await readUTOCSector(factory, 0),
await readUTOCSector(factory, 1),
await readUTOCSector(factory, 2)
);
for (let i = 0; i < parsedTOC.trackFragmentList.length; i++) {
let track = parsedTOC.trackFragmentList[i];
if (track.mode === 166) track.mode = 230;
else if (track.mode == 162) {
Object.assign(parsedTOC.trackFragmentList[i], {
mode: 0,
end: { cluster: 0, sector: 0, group: 0 },
start: { cluster: 0, sector: 0, group: 0 },
});
Object.assign(parsedTOC.trackFragmentList[i + 1], {
mode: 0,
end: { cluster: 0, sector: 0, group: 0 },
start: { cluster: 0, sector: 0, group: 0 },
});
parsedTOC.trackMap[i] = 0;
parsedTOC.timestampMap[i] = 0;
parsedTOC.nTracks = i - 1;
parsedTOC.nextFreeTrackSlot = i;
parsedTOC.nextFreeTimestampSlot = i;
}
}
let sectorIndex = 0;
for (let sector of reconstructTOC(parsedTOC))
await writeUTOCSector(factory, sectorIndex++, sector);
await editToc.forceTOCEdit();
yield "done";
}
async *recover() {
console.log = function () {};
const device = await this.device;
const factory = await this.factory;
const editToc = await this.editToc;
const parsedTOC = parseTOC(
await readUTOCSector(factory, 0),
await readUTOCSector(factory, 1),
await readUTOCSector(factory, 2)
);
const lastTrackIndex = (await device.getTrackCount()) + 1;
parsedTOC.trackMap[lastTrackIndex] = lastTrackIndex;
parsedTOC.timestampMap[lastTrackIndex] = lastTrackIndex;
parsedTOC.nTracks = lastTrackIndex;
parsedTOC.nextFreeTrackSlot = lastTrackIndex + 1;
parsedTOC.nextFreeTimestampSlot = lastTrackIndex + 1;
Object.assign(parsedTOC.trackFragmentList[lastTrackIndex], {
link: 0,
mode: 130,
end: { cluster: 2251, sector: 31, group: 10 },
start: {
cluster:
parsedTOC.trackFragmentList[lastTrackIndex - 1].end.cluster + 2,
sector: 0,
group: 0,
},
});
let sectorIndex = 0;
for (let sector of reconstructTOC(parsedTOC))
await writeUTOCSector(factory, sectorIndex++, sector);
await editToc.forceTOCEdit();
yield "done";
}
async *erase(track) {
const device = await this.device;
const trackIndex =
(track === "last" ? await device.getTrackCount() : Number(track)) - 1;
await device.eraseTrack(trackIndex);
yield "done";
}
async *trackCount() {
const device = await this.device;
yield `${await device.getTrackCount()}`;
}
async *wipe() {
const device = await this.device;
await device.eraseDisc();
yield "done";
}
async *discTitle(title) {
const device = await this.device;
await device.setDiscTitle(title);
yield "done";
}
async *trackTitle(track, title) {
const device = await this.device;
const trackIndex =
(track === "last" ? await device.getTrackCount() : Number(track)) - 1;
await device.setTrackTitle(trackIndex, title);
yield "done";
}
async *close() {
const device = await this.device;
await device.release();
yield "done";
ipc.server.stop();
process.exit(0);
}
async *kill() {
yield "done";
ipc.server.stop();
process.exit(0);
}
}
function init() {
const netmd = new NetMD();
ipc.config.silent = true;
ipc.config.id = "netmd";
ipc.serve(() =>
ipc.server.on("message", async (data, socket) => {
const [method, ...args] = data;
if (
!netmd[method] ||
netmd[method].constructor.name !== "AsyncGeneratorFunction"
) {
ipc.server.emit(socket, "message", "unknown method");
} else {
for await (let message of netmd[method](...args)) {
ipc.server.emit(socket, "message", message);
}
}
})
);
ipc.server.start();
}
function parse(file) {
const tocBinary = fs.readFileSync(file);
const tocSlices = [];
for (let i = 0; i < 6; i++) {
tocSlices.push(tocBinary.subarray(2352 * i, 2352 * (i + 1)));
}
const parsedTOC = parseTOC(tocSlices[0], tocSlices[1], tocSlices[2]);
console.log(JSON.stringify(parsedTOC, null, 4));
}
function send(message) {
ipc.config.silent = true;
return new Promise((resolve, reject) => {
ipc.connectTo("netmd", () => {
ipc.of.netmd.on("message", resolve);
ipc.of.netmd.on("error", reject);
ipc.of.netmd.emit("message", message);
});
});
}
if (process.argv[2] === "init") {
init();
} else if (process.argv[2] === "parse") {
parse(process.argv[3]);
} else if (process.argv[2] === "help") {
console.log("netmd-session [command] [...arguments]");
const script = fs.readFileSync(__filename, "utf-8");
for (let command of script
.split(/\n/)
.filter((it) => it.match(/async \*/))
.map((it) =>
it
.split("*")[1]
.split("{")[0]
.split(/[\(\)]/)
.filter((it) => it.length && it !== " ")
)
.map((it) => `netmd-session ${it.join(" ")}`)) {
console.log(command);
}
process.exit(0);
} else {
send(process.argv.slice(2)).then((message) => {
if (typeof message === "string") {
console.log(message);
}
process.exit(0);
});
}
{
"name": "netmd-session",
"version": "1.0.0",
"description": "NetMD Session",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gist.github.com/aynik"
},
"author": "",
"license": "ISC",
"dependencies": {
"@achrinza/node-ipc": "^10.1.6",
"netmd-exploits": "^0.4.3",
"netmd-js": "^4.1.0",
"netmd-tocmanip": "^0.1.4",
"usb": "^2.4.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment