Last active
January 25, 2023 13:43
-
-
Save aynik/8278bc9fb2157e724b2b1ee01cf76767 to your computer and use it in GitHub Desktop.
netmd-session
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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