emmercm/igir

View on GitHub
src/types/files/archives/chd/chdGdiParser.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import fs from 'node:fs';
import path from 'node:path';
import util from 'node:util';

import async, { AsyncResultCallback } from 'async';
import chdman from 'chdman';
import fg from 'fast-glob';

import Defaults from '../../../../globals/defaults.js';
import Temp from '../../../../globals/temp.js';
import FsPoly from '../../../../polyfill/fsPoly.js';
import ExpectedError from '../../../expectedError.js';
import FileChecksums from '../../fileChecksums.js';
import Archive from '../archive.js';
import ArchiveEntry from '../archiveEntry.js';

/**
 * https://dreamcast.wiki/GDI_format
 */
export default class ChdGdiParser {
  public static async getArchiveEntriesGdRom<T extends Archive>(
    archive: T,
    checksumBitmask: number,
  ): Promise<ArchiveEntry<T>[]> {
    const tempDir = await FsPoly.mkdtemp(path.join(Temp.getTempDir(), 'chd-gdi'));
    const gdiFilePath = path.join(tempDir, `${path.parse(archive.getFilePath()).name}.gdi`);
    let binRawFilePaths: string[] = [];

    try {
      await chdman.extractCd({
        inputFilename: archive.getFilePath(),
        outputFilename: gdiFilePath,
      });
      binRawFilePaths = await fg(`${fg.convertPathToPattern(tempDir)}/*.{bin,raw}`);
      if (binRawFilePaths.length === 0) {
        throw new ExpectedError(
          `failed to find bin/raw files for GD-ROM: ${archive.getFilePath()}`,
        );
      }
      return await this.parseGdi(archive, gdiFilePath, binRawFilePaths, checksumBitmask);
    } finally {
      await FsPoly.rm(gdiFilePath, { force: true });
      await Promise.all(binRawFilePaths.map(async (file) => FsPoly.rm(file, { force: true })));
    }
  }

  private static async parseGdi<T extends Archive>(
    archive: T,
    gdiFilePath: string,
    binRawFilePaths: string[],
    checksumBitmask: number,
  ): Promise<ArchiveEntry<T>[]> {
    const gdiExtractedContents = await util.promisify(fs.readFile)(gdiFilePath);

    const { name: filePrefix } = path.parse(gdiFilePath);
    const gdiContents = `${gdiExtractedContents
      .toString()
      .split(/\r?\n/)
      .filter((line) => line)
      // Replace the chdman-generated track files with TOSEC-style track filenames
      .map((line) => line.replace(filePrefix, 'track').replace(/"/g, ''))
      .join('\r\n')}\r\n`;

    const gdiFile = await ArchiveEntry.entryOf({
      archive,
      entryPath: path.basename(gdiFilePath),
      size: gdiContents.length,
      ...(await FileChecksums.hashData(gdiContents, checksumBitmask)),
    });

    const binRawFiles = await async.mapLimit(
      binRawFilePaths,
      Defaults.ARCHIVE_ENTRY_SCANNER_THREADS_PER_ARCHIVE,
      async (binRawFilePath, callback: AsyncResultCallback<ArchiveEntry<T>, Error>) => {
        try {
          const binRawFile = await ArchiveEntry.entryOf({
            archive,
            entryPath: path.basename(binRawFilePath).replace(filePrefix, 'track'),
            size: await FsPoly.size(binRawFilePath),
            ...(await FileChecksums.hashFile(binRawFilePath, checksumBitmask)),
          });
          callback(undefined, binRawFile);
        } catch (error) {
          if (error instanceof Error) {
            callback(error);
          } else if (typeof error === 'string') {
            callback(new Error(error));
          } else {
            callback(
              new Error(`unknown error when parsing GD-ROM bin/raw file: ${binRawFilePath}`),
            );
          }
        }
      },
    );

    return [gdiFile, ...binRawFiles];
  }
}