diff --git a/index.ts b/index.ts index 4f0da96..d86ca2b 100644 --- a/index.ts +++ b/index.ts @@ -4,6 +4,7 @@ import ChdmanHd from './src/chdman/chdmanHd.js'; import ChdmanCd from './src/chdman/chdmanCd.js'; import ChdmanVerify from './src/chdman/chdmanVerify.js'; import ChdmanDvd from './src/chdman/chdmanDvd.js'; +import ChdmanRaw from './src/chdman/chdmanRaw.js'; export * from './src/chdman/chdmanInfo.js'; export * from './src/chdman/chdmanBin.js'; @@ -11,6 +12,7 @@ export * from './src/chdman/chdmanHd.js'; export * from './src/chdman/chdmanCd.js'; export * from './src/chdman/chdmanVerify.js'; export * from './src/chdman/chdmanDvd.js'; +export * from './src/chdman/chdmanRaw.js'; export * from './src/chdman/common.js'; export default { @@ -20,13 +22,13 @@ export default { verify: ChdmanVerify.verify, - // TODO(cemmer): createraw + createRaw: ChdmanRaw.createRaw, createHd: ChdmanHd.createHd, createCd: ChdmanCd.createCd, createDvd: ChdmanDvd.createDvd, // TODO(cemmer): createld - // TODO(cemmer): extractraw + extractRaw: ChdmanRaw.extractRaw, extractHd: ChdmanHd.extractHd, extractCd: ChdmanCd.extractCd, extractDvd: ChdmanDvd.extractDvd, diff --git a/src/chdman/chdmanRaw.ts b/src/chdman/chdmanRaw.ts new file mode 100644 index 0000000..c3a2fb2 --- /dev/null +++ b/src/chdman/chdmanRaw.ts @@ -0,0 +1,75 @@ +import util from 'node:util'; +import fs from 'node:fs'; +import { CHDCompressionAlgorithm } from './common.js'; +import ChdmanBin from './chdmanBin.js'; + +export interface CreateRawOptions { + outputFilename: string, + outputParentFilename?: string, + force?: boolean, + inputFilename: string, + inputStartByte?: number, + inputStartHunk?: number, + inputBytes?: number, + inputHunks?: number, + hunkSize: number, + unitSize: number, + compression?: 'none' | CHDCompressionAlgorithm[], + numProcessors?: number +} + +export interface ExtractRawOptions { + outputFilename: string, + force?: boolean, + inputFilename: string, + inputParentFilename?: string, + inputStartByte?: number, + inputStartHunk?: number, + inputBytes?: number, + inputHunks?: number, +} + +export default { + async createRaw(options: CreateRawOptions): Promise { + const existedBefore = await util.promisify(fs.exists)(options.outputFilename); + try { + await ChdmanBin.run([ + 'createraw', + '--output', options.outputFilename, + ...(options.outputParentFilename ? ['--outputparent', String(options.outputParentFilename)] : []), + ...(options.force === true ? ['--force'] : []), + '--input', options.inputFilename, + ...(options.inputStartByte === undefined ? [] : ['--inputstartbyte', String(options.inputStartByte)]), + ...(options.inputStartHunk === undefined ? [] : ['--inputstarthunk', String(options.inputStartHunk)]), + ...(options.inputBytes === undefined ? [] : ['--inputbytes', String(options.inputBytes)]), + ...(options.inputHunks === undefined ? [] : ['--inputhunks', String(options.inputHunks)]), + '--hunksize', String(options.hunkSize), + '--unitsize', String(options.unitSize), + ...(options.compression === undefined + ? [] + : ['--compression', Array.isArray(options.compression) ? options.compression.join(',') : options.compression]), + ...(options.numProcessors === undefined ? [] : ['--numprocessors', String(options.numProcessors)]), + ]); + } catch (error) { + // chdman can leave cruft when it fails + if (!existedBefore) { + await util.promisify(fs.rm)(options.outputFilename, { force: true }); + } + throw error; + } + }, + + async extractRaw(options: ExtractRawOptions): Promise { + await ChdmanBin.run([ + 'extractraw', + '--output', options.outputFilename, + ...(options.force === true ? ['--force'] : []), + '--input', options.inputFilename, + ...(options.inputParentFilename ? ['--inputparent', options.inputParentFilename] : []), + ...(options.inputStartByte === undefined ? [] : ['--inputstartbyte', String(options.inputStartByte)]), + ...(options.inputStartHunk === undefined ? [] : ['--inputstarthunk', String(options.inputStartHunk)]), + ...(options.inputBytes === undefined ? [] : ['--inputbytes', String(options.inputBytes)]), + ...(options.inputHunks === undefined ? [] : ['--inputhunks', String(options.inputHunks)]), + ]); + }, +}; diff --git a/test/chdman/chdmanRaw.test.ts b/test/chdman/chdmanRaw.test.ts new file mode 100644 index 0000000..c02f0c6 --- /dev/null +++ b/test/chdman/chdmanRaw.test.ts @@ -0,0 +1,76 @@ +import path from 'node:path'; +import os from 'node:os'; +import util from 'node:util'; +import fs from 'node:fs'; +import ChdmanInfo from '../../src/chdman/chdmanInfo.js'; +import TestUtil from '../testUtil.js'; +import ChdmanRaw from '../../src/chdman/chdmanRaw.js'; + +test('should fail on nonexistent file', async () => { + const temporaryChd = `${await TestUtil.mktemp(path.join(os.tmpdir(), 'dummy'))}.chd`; + const temporaryRaw = `${await TestUtil.mktemp(path.join(os.tmpdir(), 'dummy'))}.hd`; + + try { + await expect(ChdmanRaw.createRaw({ + inputFilename: os.devNull, + outputFilename: temporaryChd, + hunkSize: 64, + unitSize: 64, + })).rejects.toBeDefined(); + await expect(ChdmanInfo.info({ + inputFilename: temporaryRaw, + })).rejects.toBeDefined(); + await expect(ChdmanRaw.extractRaw({ + inputFilename: temporaryChd, + outputFilename: temporaryRaw, + })).rejects.toBeDefined(); + } finally { + await util.promisify(fs.rm)(temporaryChd, { force: true }); + } +}); + +test.each([ + [path.join('test', 'fixtures', 'hd', '512.hd'), 512], + [path.join('test', 'fixtures', 'hd', '3584.hd'), 3584], + [path.join('test', 'fixtures', 'iso', '2048.iso'), 2048], + [path.join('test', 'fixtures', 'iso', '16384.iso'), 16_384], +])('should create, info, and extract: %s', async (hd, expectedBinSize) => { + const temporaryChd = `${await TestUtil.mktemp(path.join(os.tmpdir(), path.basename(hd)))}.chd`; + const temporaryHd = `${await TestUtil.mktemp(path.join(os.tmpdir(), 'dummy'))}.hd`; + + try { + await ChdmanRaw.createRaw({ + inputFilename: hd, + outputFilename: temporaryChd, + hunkSize: 64, + unitSize: 64, + }); + await expect(TestUtil.exists(temporaryChd)).resolves.toEqual(true); + + const info = await ChdmanInfo.info({ + inputFilename: temporaryChd, + }); + expect(info.fileVersion).toBeGreaterThan(0); + expect(info.logicalSize).toBeGreaterThan(0); + expect(info.hunkSize).toBeGreaterThan(0); + expect(info.totalHunks).toBeGreaterThan(0); + expect(info.unitSize).toBeGreaterThan(0); + expect(info.totalUnits).toBeGreaterThan(0); + expect(info.compression.length).toBeGreaterThan(0); + expect(info.chdSize).toBeGreaterThan(0); + expect(info.ratio).toBeGreaterThan(0); + expect(info.sha1).toBeTruthy(); + expect(info.dataSha1).toBeTruthy(); + + await ChdmanRaw.extractRaw({ + inputFilename: temporaryChd, + outputFilename: temporaryHd, + }); + await expect(TestUtil.exists(temporaryHd)).resolves.toEqual(true); + const temporaryHdStat = await util.promisify(fs.stat)(temporaryHd); + expect(temporaryHdStat.size).toEqual(expectedBinSize); + } finally { + await util.promisify(fs.rm)(temporaryChd, { force: true }); + await util.promisify(fs.rm)(temporaryHd, { force: true }); + } +}); diff --git a/test/fixtures/iso/16384.chd.iso b/test/fixtures/iso/16384.chd.iso deleted file mode 100644 index a4aba7a..0000000 Binary files a/test/fixtures/iso/16384.chd.iso and /dev/null differ