Skip to content

Commit

Permalink
Feature: DVD creation and extraction (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmercm authored Mar 13, 2024
1 parent 7bbc73c commit 4d1d588
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 57 deletions.
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
```
45 changes: 3 additions & 42 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string> {
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
Expand Down
63 changes: 63 additions & 0 deletions src/chdman/chdmanDvd.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
await ChdmanBin.run([
'extractdvd',
'--output', options.outputFilename,
...(options.outputBinFilename ? ['--outputbin', options.outputBinFilename] : []),
...(options.force === true ? ['--force'] : []),
'--input', options.inputFilename,
...(options.inputParentFilename ? ['--inputparent', options.inputParentFilename] : []),
]);
},
};
73 changes: 73 additions & 0 deletions test/chdman/chdmanDvd.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
});
5 changes: 3 additions & 2 deletions test/chdman/chdmanHd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
File renamed without changes.
File renamed without changes.
Binary file added test/fixtures/iso/16384.chd.iso
Binary file not shown.
Binary file added test/fixtures/iso/16384.iso
Binary file not shown.
Binary file added test/fixtures/iso/2048.iso
Binary file not shown.

0 comments on commit 4d1d588

Please sign in to comment.