Skip to content

Commit

Permalink
Refactoring: Move ISO9660 filesystem code to a separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
kichikuou committed Aug 17, 2024
1 parent 912dfab commit 7575f70
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 171 deletions.
3 changes: 2 additions & 1 deletion shell/cddaloader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) 2019 Kichikuou <[email protected]>
// This source code is governed by the MIT License, see the LICENSE file.
import type { MainModule as XSystem35Module } from './xsystem35.js';
import { ald_getdata, isMobileSafari, createBlob, loadScript, createWaveFile } from './util.js';
import { ald_getdata, isMobileSafari, createBlob, loadScript } from './util.js';
import { createWaveFile } from './cdimage.js';

export interface CDDALoaderSource {
extractTrack(track: number): Promise<Blob>;
Expand Down
167 changes: 21 additions & 146 deletions shell/cdimage.ts
Original file line number Diff line number Diff line change
@@ -1,150 +1,5 @@
// Copyright (c) 2017 Kichikuou <[email protected]>
// This source code is governed by the MIT License, see the LICENSE file.
import { createWaveFile } from './util.js';

export class ISO9660FileSystem {
private decoder: TextDecoder;

static async create(sectorReader: Reader): Promise<ISO9660FileSystem> {
let best_vd: VolumeDescriptor | null = null;
for (let sector = 0x10;; sector++) {
let vd = new VolumeDescriptor(await sectorReader.readSector(sector));
switch (vd.type) {
case VDType.Primary:
if (!best_vd)
best_vd = vd;
break;
case VDType.Supplementary:
if (vd.encoding())
best_vd = vd;
break;
case VDType.Terminator:
if (!best_vd)
throw new Error('PVD not found');
return new ISO9660FileSystem(sectorReader, best_vd);
}
}
}

private constructor(private sectorReader: Reader, private vd: VolumeDescriptor) {
this.decoder = new TextDecoder(vd.encoding());
}

volumeLabel(): string {
return this.vd.volumeLabel(this.decoder);
}

rootDir(): DirEnt {
return this.vd.rootDirEnt(this.decoder);
}

async getDirEnt(name: string, parent: DirEnt): Promise<DirEnt | null> {
name = name.toLowerCase();
for (let e of await this.readDir(parent)) {
if (e.name.toLowerCase() === name)
return e;
}
return null;
}

async readDir(dirent: DirEnt): Promise<DirEnt[]> {
let sector = dirent.sector;
let position = 0;
let length = dirent.size;
let entries: DirEnt[] = [];
let buf: ArrayBuffer;
while (position < length) {
if (position === 0)
buf = await this.sectorReader.readSector(sector);
let child = new DirEnt(buf!, position, this.decoder);
if (child.length === 0) {
// Padded end of sector
position = 2048;
} else {
entries.push(child);
position += child.length;
}
if (position > 2048)
throw new Error('dirent across sector boundary');
if (position === 2048) {
sector++;
position = 0;
length -= 2048;
}
}
return entries;
}

readFile(dirent: DirEnt): Promise<Uint8Array[]> {
return this.sectorReader.readSequentialSectors(dirent.sector, dirent.size);
}
}

enum VDType {
Primary = 1,
Supplementary = 2,
Terminator = 255
}

class VolumeDescriptor {
private view: DataView;
constructor(private buf: ArrayBuffer) {
this.view = new DataView(buf);
if (ASCIIArrayToString(new Uint8Array(this.buf, 1, 5)) !== 'CD001')
throw new Error('Not a valid CD image');
}
get type(): number {
return this.view.getUint8(0);
}
volumeLabel(decoder: TextDecoder): string {
return decoder.decode(new DataView(this.buf, 40, 32)).trim();
}
encoding(): string | undefined {
if (this.type === VDType.Primary)
return 'shift_jis';
if (this.escapeSequence().match(/%\/[@CE]/))
return 'utf-16be'; // Joliet
return undefined;
}
escapeSequence(): string {
return ASCIIArrayToString(new Uint8Array(this.buf, 88, 32)).trim();
}
rootDirEnt(decoder: TextDecoder): DirEnt {
return new DirEnt(this.buf, 156, decoder);
}
}

export class DirEnt {
private view: DataView;
constructor(private buf: ArrayBuffer, private offset: number, private decoder: TextDecoder) {
this.view = new DataView(buf, offset);
}
get length(): number {
return this.view.getUint8(0);
}
get sector(): number {
return this.view.getUint32(2, true);
}
get size(): number {
return this.view.getUint32(10, true);
}
get isDirectory(): boolean {
return (this.view.getUint8(25) & 2) !== 0;
}
get name(): string {
let len = this.view.getUint8(32);
let name = new DataView(this.buf, this.offset + 33, len)
if (len === 1) {
switch (name.getUint8(0)) {
case 0:
return '.';
case 1:
return '..';
}
}
return this.decoder.decode(name).split(';')[0];
}
}

export interface Reader {
readSector(sector: number): Promise<ArrayBuffer>;
Expand All @@ -153,7 +8,7 @@ export interface Reader {
extractTrack(track: number): Promise<Blob>;
}

export async function createReader(img: File, metadata?: File) {
export async function createReader(img: File, metadata?: File): Promise<Reader> {
if (img.name.toLowerCase().endsWith('.iso')) {
return new IsoReader(img);
} else if (!metadata) {
Expand Down Expand Up @@ -369,3 +224,23 @@ class MdfMdsReader implements Reader {
function ASCIIArrayToString(buffer: Uint8Array): string {
return String.fromCharCode.apply(null, buffer as any);
}

export function createWaveFile(sampleRate: number, channels: number, dataSize: number, chunks: BlobPart[]): Blob {
let headerBuf = new ArrayBuffer(44);
let header = new DataView(headerBuf);
header.setUint32(0, 0x52494646, false); // 'RIFF'
header.setUint32(4, dataSize + 36, true); // filesize - 8
header.setUint32(8, 0x57415645, false); // 'WAVE'
header.setUint32(12, 0x666D7420, false); // 'fmt '
header.setUint32(16, 16, true); // size of fmt chunk
header.setUint16(20, 1, true); // PCM format
header.setUint16(22, channels, true); // stereo
header.setUint32(24, sampleRate, true); // sampling rate
header.setUint32(28, sampleRate * channels * 2, true); // bytes/sec
header.setUint16(32, channels * 2, true); // block size
header.setUint16(34, 16, true); // bit/sample
header.setUint32(36, 0x64617461, false); // 'data'
header.setUint32(40, dataSize, true); // data size
chunks.unshift(headerBuf);
return new Blob(chunks, { type: 'audio/wav' });
}
151 changes: 151 additions & 0 deletions shell/iso9660.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) 2024 Kichikuou <[email protected]>
// This source code is governed by the MIT License, see the LICENSE file.
import { Reader } from './cdimage.js';

export class FileSystem {
private decoder: TextDecoder;

static async create(sectorReader: Reader): Promise<FileSystem> {
let best_vd: VolumeDescriptor | null = null;
for (let sector = 0x10;; sector++) {
let vd = new VolumeDescriptor(await sectorReader.readSector(sector));
switch (vd.type) {
case VDType.Primary:
if (!best_vd)
best_vd = vd;
break;
case VDType.Supplementary:
if (vd.encoding())
best_vd = vd;
break;
case VDType.Terminator:
if (!best_vd)
throw new Error('PVD not found');
return new FileSystem(sectorReader, best_vd);
}
}
}

private constructor(private sectorReader: Reader, private vd: VolumeDescriptor) {
this.decoder = new TextDecoder(vd.encoding());
}

volumeLabel(): string {
return this.vd.volumeLabel(this.decoder);
}

rootDir(): DirEnt {
return this.vd.rootDirEnt(this.decoder);
}

async getDirEnt(name: string, parent: DirEnt): Promise<DirEnt | null> {
name = name.toLowerCase();
for (let e of await this.readDir(parent)) {
if (e.name.toLowerCase() === name)
return e;
}
return null;
}

async readDir(dirent: DirEnt): Promise<DirEnt[]> {
let sector = dirent.sector;
let position = 0;
let length = dirent.size;
let entries: DirEnt[] = [];
let buf: ArrayBuffer;
while (position < length) {
if (position === 0)
buf = await this.sectorReader.readSector(sector);
let child = new DirEnt(buf!, position, this.decoder);
if (child.length === 0) {
// Padded end of sector
position = 2048;
} else {
entries.push(child);
position += child.length;
}
if (position > 2048)
throw new Error('dirent across sector boundary');
if (position === 2048) {
sector++;
position = 0;
length -= 2048;
}
}
return entries;
}

readFile(dirent: DirEnt): Promise<Uint8Array[]> {
return this.sectorReader.readSequentialSectors(dirent.sector, dirent.size);
}
}

enum VDType {
Primary = 1,
Supplementary = 2,
Terminator = 255
}

class VolumeDescriptor {
private view: DataView;
constructor(private buf: ArrayBuffer) {
this.view = new DataView(buf);
if (ASCIIArrayToString(new Uint8Array(this.buf, 1, 5)) !== 'CD001')
throw new Error('Not a valid CD image');
}
get type(): number {
return this.view.getUint8(0);
}
volumeLabel(decoder: TextDecoder): string {
return decoder.decode(new DataView(this.buf, 40, 32)).trim();
}
encoding(): string | undefined {
if (this.type === VDType.Primary)
return 'shift_jis';
if (this.escapeSequence().match(/%\/[@CE]/))
return 'utf-16be'; // Joliet
return undefined;
}
escapeSequence(): string {
return ASCIIArrayToString(new Uint8Array(this.buf, 88, 32)).trim();
}
rootDirEnt(decoder: TextDecoder): DirEnt {
return new DirEnt(this.buf, 156, decoder);
}
}

export class DirEnt {
private view: DataView;
constructor(private buf: ArrayBuffer, private offset: number, private decoder: TextDecoder) {
this.view = new DataView(buf, offset);
}
get length(): number {
return this.view.getUint8(0);
}
get sector(): number {
return this.view.getUint32(2, true);
}
get size(): number {
return this.view.getUint32(10, true);
}
get isDirectory(): boolean {
return (this.view.getUint8(25) & 2) !== 0;
}
get name(): string {
let len = this.view.getUint8(32);
let name = new DataView(this.buf, this.offset + 33, len)
if (len === 1) {
switch (name.getUint8(0)) {
case 0:
return '.';
case 1:
return '..';
}
}
return this.decoder.decode(name).split(';')[0];
}
}

function ASCIIArrayToString(buffer: Uint8Array): string {
return String.fromCharCode.apply(null, buffer as any);
}
9 changes: 5 additions & 4 deletions shell/loadersource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {$, startMeasure, loadScript, JSZIP_SCRIPT, JSZipOptions, createBlob, DRI
import * as cdimage from './cdimage.js';
import {CDDALoader, BGMLoader} from './cddaloader.js';
import {registerDataFile} from './datafile.js';
import * as iso9660 from './iso9660.js';
import {loadModule, saveDirReady} from './moduleloader.js';
import {message} from './strings.js';

Expand Down Expand Up @@ -64,7 +65,7 @@ export class CDImageSource extends LoaderSource {

protected async doLoad() {
this.imageReader = await cdimage.createReader(this.imageFile, this.metadataFile);
let isofs = await cdimage.ISO9660FileSystem.create(this.imageReader);
let isofs = await iso9660.FileSystem.create(this.imageReader);
// this.walk(isofs, isofs.rootDir(), '/');
let gamedata = await this.findGameDir(isofs);
if (!gamedata)
Expand Down Expand Up @@ -104,7 +105,7 @@ export class CDImageSource extends LoaderSource {
return new CDDALoader(this.imageReader);
}

private async findGameDir(isofs: cdimage.ISO9660FileSystem): Promise<cdimage.DirEnt | null> {
private async findGameDir(isofs: iso9660.FileSystem): Promise<iso9660.DirEnt | null> {
for (let e of await isofs.readDir(isofs.rootDir())) {
if (e.isDirectory) {
if (e.name.toLowerCase() === 'gamedata' || await isofs.getDirEnt('adisk.dat', e))
Expand All @@ -116,7 +117,7 @@ export class CDImageSource extends LoaderSource {
return null;
}

private async saveDir(isofs: cdimage.ISO9660FileSystem): Promise<string> {
private async saveDir(isofs: iso9660.FileSystem): Promise<string> {
let dirname = isofs.volumeLabel();
if (!dirname) {
if (await isofs.getDirEnt('prog.bat', isofs.rootDir())) {
Expand All @@ -132,7 +133,7 @@ export class CDImageSource extends LoaderSource {
}

// For debug
private async walk(isofs: cdimage.ISO9660FileSystem, dir: cdimage.DirEnt, dirname: string) {
private async walk(isofs: iso9660.FileSystem, dir: iso9660.DirEnt, dirname: string) {
for (let e of await isofs.readDir(dir)) {
if (e.name !== '.' && e.name !== '..') {
console.log(dirname + e.name);
Expand Down
Loading

0 comments on commit 7575f70

Please sign in to comment.