Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ docs
*.log
tmp
build
.coverage
.coverage
.DS_Store
40 changes: 33 additions & 7 deletions src/iso/fs.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import { FileSystem, Inode, type UsageInfo } from '@zenfs/core';
import type { Backend } from '@zenfs/core/backends/backend.js';
import type { Backend, SharedConfig } from '@zenfs/core/backends/backend.js';
import { S_IFDIR, S_IFREG } from '@zenfs/core/constants';
import { Readonly, Sync } from '@zenfs/core/mixins/index.js';
import { resolve } from '@zenfs/core/path';
import { log, withErrno } from 'kerium';
import { decodeASCII } from 'utilium';
import type { Directory } from './Directory.js';
import type { DirectoryRecord } from './DirectoryRecord.js';
import { PrimaryVolumeDescriptor, VolumeDescriptorType } from './VolumeDescriptor.js';
import { PXEntry, TFEntry, TFFlag } from './entries.js';

/**
* Options for IsoFS file system instances.
*/
export interface IsoOptions {
export interface IsoOptions extends SharedConfig {
/**
* The ISO file in a buffer.
*/
Expand All @@ -33,17 +34,24 @@ export interface IsoOptions {
* * Microsoft Joliet and Rock Ridge extensions to the ISO9660 standard
*/
export class IsoFS extends Readonly(Sync(FileSystem)) {
protected data: Uint8Array;
protected readonly options: IsoOptions;
protected pvd: PrimaryVolumeDescriptor;

/**
* Constructs a read-only file system from the given ISO.
* @param data The ISO file in a buffer.
* @param name The name of the ISO (optional; used for debug messages / identification).
*/
public constructor(protected data: Uint8Array) {
public constructor(options: IsoOptions) {
super(0x2069736f, 'iso9660');

this.options = options;
this.data = options.data;
this.label = options.name;

let candidate: PrimaryVolumeDescriptor | undefined;
const data = this.data;

for (let i = 16 * 2048, terminatorFound = false; i < data.length && !terminatorFound; i += 2048) {
switch (data[i] as VolumeDescriptorType) {
Expand Down Expand Up @@ -127,13 +135,33 @@ export class IsoFS extends Readonly(Sync(FileSystem)) {

for (const part of path.split('/').slice(1)) {
if (!dir.isDirectory()) return;
dir = dir.directory.get(part);
const directory: Directory = dir.directory;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const directory: Directory = dir.directory;
const { directory } = dir;

let next: DirectoryRecord | undefined = directory.get(part);
if (!next && this.options.caseFold) {
const foldedPart = this._caseFold(part);
for (const [name, record] of directory) {
if (this._caseFold(name) === foldedPart) {
next = record;
break;
}
}
}

dir = next;
if (!dir) return;
Comment on lines +140 to 151
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of difficult to read... if there is a way to make it more readable & reduce indentation that would be great— perhaps using a gaurd clause?

}

return dir;
}

private _caseFold(original: string): string {
if (!this.options.caseFold) {
return original;
}

return this.options.caseFold === 'upper' ? original.toUpperCase() : original.toLowerCase();
}
Comment on lines +157 to +163
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated with ZipFS as well. To reduce duplcation, I'm thinking:

  • Make options public but add @internal
  • Change case fold to be @internal, and take the FS: _caseFold(fs: FileSystem, path: string) => string
  • Both FSes use the shared case fold function


private _get(path: string, record: DirectoryRecord): Inode | undefined {
if (record.isSymlink) {
const target = resolve(path, record.symlinkPath);
Expand Down Expand Up @@ -183,9 +211,7 @@ const _Iso = {
},

create(options: IsoOptions) {
const fs = new IsoFS(options.data);
fs.label = options.name;
return fs;
return new IsoFS(options);
},
} as const satisfies Backend<IsoFS, IsoOptions>;
type _Iso = typeof _Iso;
Expand Down
55 changes: 29 additions & 26 deletions src/zip/fs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import { FileSystem, Inode, type UsageInfo } from '@zenfs/core';
import type { Backend } from '@zenfs/core/backends/backend.js';
import type { Backend, SharedConfig } from '@zenfs/core/backends/backend.js';
import { S_IFDIR, S_IFREG } from '@zenfs/core/constants';
import { Readonly } from '@zenfs/core/mixins/readonly.js';
import { parse } from '@zenfs/core/path';
Expand All @@ -19,7 +19,7 @@ export interface ZipDataSource<TBuffer extends ArrayBufferLike = ArrayBuffer> {
/**
* Configuration options for a ZipFS file system.
*/
export interface ZipOptions<TBuffer extends ArrayBufferLike = ArrayBuffer> {
export interface ZipOptions<TBuffer extends ArrayBufferLike = ArrayBuffer> extends SharedConfig {
/**
* The zip file as a binary buffer.
*/
Expand Down Expand Up @@ -93,7 +93,7 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
while (ptr < cdEnd) {
const cd = await FileEntry.from<TBuffer>(this.data, ptr);

if (!this.lazy) await cd.loadContents();
if (!this.options.lazy) await cd.loadContents();
/* Paths must be absolute,
yet zip file paths are always relative to the zip root.
So we prepend '/' and call it a day. */
Expand All @@ -102,14 +102,15 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
}
// Strip the trailing '/' if it exists
const name = cd.name.endsWith('/') ? cd.name.slice(0, -1) : cd.name;
this.files.set('/' + name, cd);
this.files.set('/'+ this._caseFold(name), cd);
ptr += cd.size;
}

// Parse directory entries
for (const entry of this.files.keys()) {
const { dir, base } = parse(entry);
let { dir, base } = parse(entry);

dir = this._caseFold(dir);
if (!this.directories.has(dir)) {
this.directories.set(dir, new Set());
}
Expand All @@ -119,8 +120,9 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon

// Add subdirectories to their parent's entries
for (const entry of this.directories.keys()) {
const { dir, base } = parse(entry);
let { dir, base } = parse(entry);

dir = this._caseFold(dir);
if (base == '') continue;

if (!this.directories.has(dir)) {
Expand All @@ -134,7 +136,7 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
public constructor(
public label: string,
protected data: ZipDataSource<TBuffer>,
public readonly lazy: boolean = false
protected readonly options: ZipOptions<TBuffer>
) {
super(0x207a6970, 'zipfs');
}
Expand All @@ -151,8 +153,9 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
}

public statSync(path: string): Inode {
const folded = this._caseFold(path);
// The EOCD/Header does not track directories, so it does not exist in `entries`
if (this.directories.has(path)) {
if (this.directories.has(folded)) {
return new Inode({
mode: 0o555 | S_IFDIR,
size: 4096,
Expand All @@ -163,7 +166,7 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
});
}

const entry = this.files.get(path);
const entry = this.files.get(folded);

if (!entry) throw withErrno('ENOENT');

Expand All @@ -178,7 +181,7 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
const inode = await this.stat(path);
if (!(inode.mode & S_IFDIR)) throw withErrno('ENOTDIR');

const entries = this.directories.get(path);
const entries = this.directories.get(this._caseFold(path));
if (!entries) throw withErrno('ENODATA');

return Array.from(entries);
Expand All @@ -188,26 +191,28 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon
const inode = this.statSync(path);
if (!(inode.mode & S_IFDIR)) throw withErrno('ENOTDIR');

const entries = this.directories.get(path);
const entries = this.directories.get(this._caseFold(path));
if (!entries) throw withErrno('ENODATA');

return Array.from(entries);
}

public async read(path: string, buffer: Uint8Array, offset: number, end: number): Promise<void> {
if (this.directories.has(path)) throw withErrno('EISDIR');
const folded = this._caseFold(path);
if (this.directories.has(folded)) throw withErrno('EISDIR');

const file = this.files.get(path) ?? _throw(withErrno('ENOENT'));
const file = this.files.get(folded) ?? _throw(withErrno('ENOENT'));

if (!file.contents) await file.loadContents();

buffer.set(file.contents.subarray(offset, end));
}

public readSync(path: string, buffer: Uint8Array, offset: number, end: number): void {
if (this.directories.has(path)) throw withErrno('EISDIR');
const folded = this._caseFold(path);
if (this.directories.has(folded)) throw withErrno('EISDIR');

const file = this.files.get(path) ?? _throw(withErrno('ENOENT'));
const file = this.files.get(folded) ?? _throw(withErrno('ENOENT'));

if (!file.contents) {
void file.loadContents();
Expand All @@ -216,6 +221,13 @@ export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readon

buffer.set(file.contents.subarray(offset, end));
}

private _caseFold(original: string): string {
if (!this.options.caseFold) {
return original;
}
return this.options.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase();
}
}

const _isShared = (b: unknown): b is SharedArrayBuffer => typeof b == 'object' && b !== null && b.constructor.name === 'SharedArrayBuffer';
Expand Down Expand Up @@ -293,16 +305,7 @@ const _Zip = {
name: 'Zip',

options: {
data: {
type: [
ArrayBuffer,
Object.getPrototypeOf(Uint8Array) /* %TypedArray% */,
function ZipDataSource(v: unknown): v is ZipDataSource {
return typeof v == 'object' && v !== null && 'size' in v && typeof v.size == 'number' && 'get' in v && typeof v.get == 'function';
},
],
required: true,
},
data: { type: 'object', required: true },
name: { type: 'string', required: false },
lazy: { type: 'boolean', required: false },
},
Expand All @@ -312,7 +315,7 @@ const _Zip = {
},

create<TBuffer extends ArrayBufferLike = ArrayBuffer>(opt: ZipOptions<TBuffer>): ZipFS<TBuffer> {
return new ZipFS<TBuffer>(opt.name ?? '', getSource(opt.data), opt.lazy);
return new ZipFS<TBuffer>(opt.name ?? '', getSource(opt.data), opt);
},
} satisfies Backend<ZipFS, ZipOptions>;
type _Zip = typeof _Zip;
Expand Down
30 changes: 28 additions & 2 deletions tests/iso.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import { setupLogs } from '@zenfs/core/tests/logs.js';

setupLogs();

await suite('Basic ISO9660 operations', () => {
suite('Basic ISO9660 operations', () => {
test('Configure', async () => {
const data = readFileSync(dirname(fileURLToPath(import.meta.url)) + '/files/data.iso');
//const data = buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
await configureSingle({ backend: Iso, data });
});

Expand All @@ -38,3 +37,30 @@ await suite('Basic ISO9660 operations', () => {
assert.equal(fs.readFileSync('/nested/omg.txt', 'utf8'), 'This is a nested file!');
});
});

await suite('ISO case fold', {}, () => {
test('Configure', async () => {
const data = readFileSync(dirname(fileURLToPath(import.meta.url)) + '/files/data.iso');
await configureSingle({ backend: Iso, data, caseFold: 'upper' });
});

test('read /ONES.TXT', () => {
assert.equal(fs.readFileSync('/ONE.TXT', 'utf8'), '1');
});

test('read /NESTED/OMG.TXT', () => {
assert.equal(fs.readFileSync('/NESTED/OMG.TXT', 'utf8'), 'This is a nested file!');
});

test('readdir /NESTED', () => {
assert.equal(fs.readdirSync('/NESTED').length, 1);
});

test('read /nested/omg.txt (all lower)', () => {
assert.equal(fs.readFileSync('/nested/omg.txt', 'utf8'), 'This is a nested file!');
});

test('readdir /Nested (mixed case)', () => {
assert.equal(fs.readdirSync('/Nested').length, 1);
});
});
30 changes: 29 additions & 1 deletion tests/zip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function _runTests() {
assert.equal(fs.readdirSync('/nested').length, 1);
});

test('readdir /nested/omg.txt', () => {
test('read /nested/omg.txt', () => {
assert.equal(fs.readFileSync('/nested/omg.txt', 'utf8'), 'This is a nested file!');
});
}
Expand All @@ -41,6 +41,34 @@ suite('Basic ZIP operations', () => {
_runTests();
});

await suite('ZIP case fold', {}, () => {
test('Configure', async () => {
const buffer = readFileSync(import.meta.dirname + '/files/data.zip');
const data = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
await configureSingle({ backend: Zip, data, caseFold: 'upper' });
});

test('read /ONES.TXT', () => {
assert.equal(fs.readFileSync('/ONE.TXT', 'utf8'), '1');
});

test('read /NESTED/OMG.TXT', () => {
assert.equal(fs.readFileSync('/NESTED/OMG.TXT', 'utf8'), 'This is a nested file!');
});

test('readdir /NESTED', () => {
assert.equal(fs.readdirSync('/NESTED').length, 1);
});

test('read /nested/omg.txt (all lower)', () => {
assert.equal(fs.readFileSync('/nested/omg.txt', 'utf8'), 'This is a nested file!');
});

test('readdir /Nested (mixed case)', () => {
assert.equal(fs.readdirSync('/Nested').length, 1);
});
});

await using handle = await open(import.meta.dirname + '/files/data.zip');

await suite('ZIP Streaming', () => {
Expand Down
Loading