diff --git a/README.md b/README.md index f20dced..9f228c7 100644 --- a/README.md +++ b/README.md @@ -30,32 +30,46 @@ npm install --save chdman ```javascript import chdman from 'chdman'; +/** + * Create and extract hard disks + */ +await chdman.createHd({ + inputFilename: 'original-image', + outputFilename: 'image.chd', +}); +console.log(await chdman.info('image.chd')); +// { inputFile: 'image.chd', fileVersion: 5, ... } +await chdman.extractHd({ + inputFilename: 'image.chd', + outputFilename: 'extracted-image', +}); + /** * Create and extract CD-ROMs */ await chdman.createCd({ inputFilename: 'Original.cue', - outputFilename: 'Disc.chd', + outputFilename: 'CD.chd', }); -console.log(await chdman.info('Disc.chd')); -// { inputFile: 'Disc.chd', fileVersion: 5, ... } +console.log(await chdman.info('CD.chd')); +// { inputFile: 'CD.chd', fileVersion: 5, ... } await chdman.extractCd({ - inputFilename: 'Disc.chd', + inputFilename: 'CD.chd', outputFilename: 'Extracted.cue', outputBinFilename: 'Extracted.bin', }); /** - * Create and extract hard disks + * Create and extract DVD-ROMs */ -await chdman.createHd({ - inputFilename: 'original-image', - outputFilename: 'image.chd', +await chdman.createDvd({ + inputFilename: 'Original.iso', + outputFilename: 'DVD.chd', }); -console.log(await chdman.info('image.chd')); -// { inputFile: 'image.chd', fileVersion: 5, ... } -await chdman.extractHd({ - inputFilename: 'image.chd', - outputFilename: 'extracted-image', +console.log(await chdman.info('DVD.chd')); +// { inputFile: 'DVD.chd', fileVersion: 5, ... } +await chdman.extractDvd({ + inputFilename: 'DVD.chd', + outputFilename: 'Extracted.iso', }); ``` diff --git a/index.ts b/index.ts index 43c069b..bd4177b 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import ChdmanBin from './src/chdman/chdmanBin.js'; 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'; export default { run: ChdmanBin.run, @@ -14,53 +15,13 @@ export default { // TODO(cemmer): createraw createHd: ChdmanHd.createHd, createCd: ChdmanCd.createCd, - // TODO(cemmer): createdvd + createDvd: ChdmanDvd.createDvd, // TODO(cemmer): createld - /** - * Automatically extract a CHD. - */ - async extract(inputChdFilename: string, outputFilename: string): Promise { - const info = await this.info({ inputFilename: inputChdFilename }); - const metadataTags = new Set(info.metadata - .map((metadata) => metadata.tag)); - - if (metadataTags.has('GDDD')) { - await this.extractHd({ - inputFilename: inputChdFilename, - outputFilename, - }); - return outputFilename; - } - - if (metadataTags.has('CHCD') || metadataTags.has('CHTR') || metadataTags.has('CHT2')) { - // CD-ROMs - const outputCue = `${outputFilename}.cue`; - const outputBin = `${outputFilename}.bin`; - await this.extractCd({ - inputFilename: inputChdFilename, - outputFilename: outputCue, - outputBinFilename: outputBin, - }); - return outputCue; - } - if (metadataTags.has('CHGT') || metadataTags.has('CHGD')) { - // Dreamcast GD-ROM - const outputGdi = `${outputFilename}.gdi`; - await this.extractCd({ - inputFilename: inputChdFilename, - outputFilename: outputGdi, - }); - return outputGdi; - } - - throw new Error('couldn\'t automatically detect CHD data type'); - }, - // TODO(cemmer): extractraw extractHd: ChdmanHd.extractHd, extractCd: ChdmanCd.extractCd, - // TODO(cemmer): extractdvd + extractDvd: ChdmanDvd.extractDvd, // TODO(cemmer): extractld // TODO(cemmer): copy diff --git a/src/chdman/chdmanDvd.ts b/src/chdman/chdmanDvd.ts new file mode 100644 index 0000000..17d8bbf --- /dev/null +++ b/src/chdman/chdmanDvd.ts @@ -0,0 +1,63 @@ +import util from 'node:util'; +import fs from 'node:fs'; +import ChdmanBin from './chdmanBin.js'; +import { CompressionAlgorithm } from './common.js'; + +export interface CreateDvdOptions { + outputFilename: string, + outputParentFilename?: string, + force?: boolean, + inputFilename: string, + compression?: 'none' | CompressionAlgorithm[], + numProcessors?: number +} + +export interface ExtractDvdOptions { + outputFilename: string, + outputBinFilename?: string, + force?: boolean, + inputFilename: string, + inputParentFilename?: string, +} + +export default { + /** + * Create a DVD CHD. + */ + async createDvd(options: CreateDvdOptions): Promise { + const existedBefore = await util.promisify(fs.exists)(options.outputFilename); + try { + await ChdmanBin.run([ + 'createdvd', + '--output', options.outputFilename, + ...(options.outputParentFilename ? ['--outputparent', String(options.outputParentFilename)] : []), + ...(options.force === true ? ['--force'] : []), + '--input', options.inputFilename, + ...(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; + } + }, + + /** + * Extract a DVD CHD. + */ + async extractDvd(options: ExtractDvdOptions): Promise { + await ChdmanBin.run([ + 'extractdvd', + '--output', options.outputFilename, + ...(options.outputBinFilename ? ['--outputbin', options.outputBinFilename] : []), + ...(options.force === true ? ['--force'] : []), + '--input', options.inputFilename, + ...(options.inputParentFilename ? ['--inputparent', options.inputParentFilename] : []), + ]); + }, +}; diff --git a/test/chdman/chdmanDvd.test.ts b/test/chdman/chdmanDvd.test.ts new file mode 100644 index 0000000..7ffa69b --- /dev/null +++ b/test/chdman/chdmanDvd.test.ts @@ -0,0 +1,73 @@ +import path from 'node:path'; +import os from 'node:os'; +import util from 'node:util'; +import fs from 'node:fs'; +import crypto from 'node:crypto'; +import ChdmanInfo from '../../src/chdman/chdmanInfo.js'; +import TestUtil from '../testUtil.js'; +import ChdmanDvd from '../../src/chdman/chdmanDvd.js'; + +// https://unix.stackexchange.com/a/33634 + +test('should fail on nonexistent file', async () => { + const temporaryChd = `${await TestUtil.mktemp(path.join(os.tmpdir(), 'dummy'))}.chd`; + const temporaryIso = `${await TestUtil.mktemp(path.join(os.tmpdir(), 'dummy'))}.iso`; + + try { + await expect(ChdmanDvd.createDvd({ + inputFilename: os.devNull, + outputFilename: temporaryChd, + })).rejects.toBeDefined(); + await expect(ChdmanInfo.info({ + inputFilename: temporaryIso, + })).rejects.toBeDefined(); + await expect(ChdmanDvd.createDvd({ + inputFilename: temporaryChd, + outputFilename: temporaryIso, + })).rejects.toBeDefined(); + } finally { + await util.promisify(fs.rm)(temporaryChd, { force: true }); + } +}); + +test.each([ + [path.join('test', 'fixtures', 'iso', '2048.iso')], + [path.join('test', 'fixtures', 'iso', '16384.iso')], +])('should create, info, and extract: %s', async (iso) => { + const temporaryChd = `${await TestUtil.mktemp(path.join(os.tmpdir(), path.basename(iso)))}.chd`; + const temporaryIso = `${await TestUtil.mktemp(path.join(os.tmpdir(), 'dummy'))}.hd`; + + try { + await ChdmanDvd.createDvd({ + inputFilename: iso, + outputFilename: temporaryChd, + }); + 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 ChdmanDvd.extractDvd({ + inputFilename: temporaryChd, + outputFilename: temporaryIso, + }); + await expect(TestUtil.exists(temporaryIso)).resolves.toEqual(true); + expect(crypto.createHash('sha1').update(await util.promisify(fs.readFile)(iso)).digest('hex')) + .toEqual(crypto.createHash('sha1').update(await util.promisify(fs.readFile)(temporaryIso)).digest('hex')); + } finally { + await util.promisify(fs.rm)(temporaryChd, { force: true }); + await util.promisify(fs.rm)(temporaryIso, { force: true }); + } +}); diff --git a/test/chdman/chdmanHd.test.ts b/test/chdman/chdmanHd.test.ts index 5b5ba60..965471d 100644 --- a/test/chdman/chdmanHd.test.ts +++ b/test/chdman/chdmanHd.test.ts @@ -30,8 +30,9 @@ test('should fail on nonexistent file', async () => { }); test.each([ - [path.join('test', 'fixtures', 'cue', 'small.hd'), Math.ceil(512 / 2048) * 2048], - [path.join('test', 'fixtures', 'cue', 'large.hd'), Math.ceil(3584 / 2048) * 2048], + [path.join('test', 'fixtures', 'hd', '512.hd'), Math.ceil(512 / 2048) * 2048], + [path.join('test', 'fixtures', 'hd', '3584.hd'), Math.ceil(3584 / 2048) * 2048], + [path.join('test', 'fixtures', 'iso', '2048.iso'), 2048], ])('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`; diff --git a/test/fixtures/cue/large.hd b/test/fixtures/hd/3584.hd similarity index 100% rename from test/fixtures/cue/large.hd rename to test/fixtures/hd/3584.hd diff --git a/test/fixtures/cue/small.hd b/test/fixtures/hd/512.hd similarity index 100% rename from test/fixtures/cue/small.hd rename to test/fixtures/hd/512.hd diff --git a/test/fixtures/iso/16384.chd.iso b/test/fixtures/iso/16384.chd.iso new file mode 100644 index 0000000..a4aba7a Binary files /dev/null and b/test/fixtures/iso/16384.chd.iso differ diff --git a/test/fixtures/iso/16384.iso b/test/fixtures/iso/16384.iso new file mode 100644 index 0000000..a4aba7a Binary files /dev/null and b/test/fixtures/iso/16384.iso differ diff --git a/test/fixtures/iso/2048.iso b/test/fixtures/iso/2048.iso new file mode 100644 index 0000000..3383e58 Binary files /dev/null and b/test/fixtures/iso/2048.iso differ