From 8c93b088dbc23d70a45793d99bfc0a3f94b01d4a Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Mon, 3 Nov 2025 21:56:05 -0700 Subject: [PATCH 1/8] Implemented support for `caseFold` parameter on Zip FileSystem --- .gitignore | 3 ++- src/zip/fs.ts | 65 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 476a964..6865e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ docs *.log tmp build -.coverage \ No newline at end of file +.coverage +.DS_Store \ No newline at end of file diff --git a/src/zip/fs.ts b/src/zip/fs.ts index 30e03c5..fc60048 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -1,6 +1,7 @@ // 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 { CaseFold } from '@zenfs/core/internal/filesystem.js'; import { S_IFDIR, S_IFREG } from '@zenfs/core/constants'; import { Readonly } from '@zenfs/core/mixins/readonly.js'; import { parse } from '@zenfs/core/path'; @@ -34,6 +35,13 @@ export interface ZipOptions { * Whether to wait to initialize entries */ lazy?: boolean; + + /** + * Case folding mode for file names (optional). + * Can be 'lower', or 'upper'. + * Default is undefined. + */ + caseFold?: CaseFold; } /** @@ -66,6 +74,7 @@ export interface ZipOptions { export class ZipFS extends Readonly(FileSystem) { protected files: Map> = new Map(); protected directories: Map> = new Map(); + protected folded: Map = new Map(); protected _time = Date.now(); private _ready: boolean = false; @@ -101,15 +110,17 @@ export class ZipFS extends Readon throw withErrno('EPERM', 'Unexpectedly encountered an absolute path in a zip file.'); } // Strip the trailing '/' if it exists - const name = cd.name.endsWith('/') ? cd.name.slice(0, -1) : cd.name; - this.files.set('/' + name, cd); + const name = cd.name.endsWith('/') ? '/' + cd.name.slice(0, -1) : '/' + cd.name; + this.files.set(this._caseFold(name, true), cd); ptr += cd.size; } // Parse directory entries for (const entry of this.files.keys()) { - const { dir, base } = parse(entry); + const name = this.folded.get(entry) ?? entry; + let { dir, base } = parse(name); + dir = this._caseFold(dir, true); if (!this.directories.has(dir)) { this.directories.set(dir, new Set()); } @@ -119,8 +130,10 @@ export class ZipFS extends Readon // Add subdirectories to their parent's entries for (const entry of this.directories.keys()) { - const { dir, base } = parse(entry); + const name = this.folded.get(entry) ?? entry; + let { dir, base } = parse(name); + dir = this._caseFold(dir, true); if (base == '') continue; if (!this.directories.has(dir)) { @@ -134,7 +147,8 @@ export class ZipFS extends Readon public constructor( public label: string, protected data: ZipDataSource, - public readonly lazy: boolean = false + public readonly lazy: boolean = false, + public readonly caseFold?: CaseFold ) { super(0x207a6970, 'zipfs'); } @@ -151,8 +165,9 @@ export class ZipFS 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, @@ -163,7 +178,7 @@ export class ZipFS extends Readon }); } - const entry = this.files.get(path); + const entry = this.files.get(folded); if (!entry) throw withErrno('ENOENT'); @@ -178,26 +193,31 @@ export class ZipFS 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); + return Array.from(entries).map(entry => { + return this.folded.get(entry) ?? entry; + }); } public readdirSync(path: string): string[] { 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); + return Array.from(entries).map(entry => { + return this.folded.get(entry) ?? entry; + }); } public async read(path: string, buffer: Uint8Array, offset: number, end: number): Promise { - 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(); @@ -205,9 +225,10 @@ export class ZipFS extends Readon } 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(); @@ -216,6 +237,17 @@ export class ZipFS extends Readon buffer.set(file.contents.subarray(offset, end)); } + + private _caseFold(original: string, update: boolean = false): string { + if (this.caseFold) { + const folded = this.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); + if (update && !this.folded.has(folded) && folded !== original) { + this.folded.set(folded, original); + } + return folded; + } + return original; + } } const _isShared = (b: unknown): b is SharedArrayBuffer => typeof b == 'object' && b !== null && b.constructor.name === 'SharedArrayBuffer'; @@ -305,6 +337,7 @@ const _Zip = { }, name: { type: 'string', required: false }, lazy: { type: 'boolean', required: false }, + caseFold: { type: 'string', required: false }, }, isAvailable(): boolean { @@ -312,7 +345,7 @@ const _Zip = { }, create(opt: ZipOptions): ZipFS { - return new ZipFS(opt.name ?? '', getSource(opt.data), opt.lazy); + return new ZipFS(opt.name ?? '', getSource(opt.data), opt.lazy, opt.caseFold); }, } satisfies Backend; type _Zip = typeof _Zip; From 6679bbbc722f3d90b7eae3074e69e59e61f40ec2 Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Wed, 5 Nov 2025 13:48:07 -0700 Subject: [PATCH 2/8] Apply suggestion from @james-pre Co-authored-by: James Prevett --- src/zip/fs.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/zip/fs.ts b/src/zip/fs.ts index fc60048..0e967f6 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -208,9 +208,7 @@ export class ZipFS extends Readon const entries = this.directories.get(this._caseFold(path)); if (!entries) throw withErrno('ENODATA'); - return Array.from(entries).map(entry => { - return this.folded.get(entry) ?? entry; - }); + return Array.from(entries).map(entry => this.folded.get(entry) ?? entry); } public async read(path: string, buffer: Uint8Array, offset: number, end: number): Promise { From 50b8d04a5afcc56669b914ecd08a60cbbc0064d2 Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Wed, 5 Nov 2025 13:50:55 -0700 Subject: [PATCH 3/8] simplified _caseFold --- src/zip/fs.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/zip/fs.ts b/src/zip/fs.ts index 0e967f6..a143aa8 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -237,14 +237,14 @@ export class ZipFS extends Readon } private _caseFold(original: string, update: boolean = false): string { - if (this.caseFold) { - const folded = this.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); - if (update && !this.folded.has(folded) && folded !== original) { - this.folded.set(folded, original); - } - return folded; + if (!this.caseFold) { + return original; + } + const folded = this.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); + if (update && !this.folded.has(folded) && folded !== original) { + this.folded.set(folded, original); } - return original; + return folded; } } From d3791b811468e7c7398be027893313dd5acab493 Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Wed, 5 Nov 2025 13:52:51 -0700 Subject: [PATCH 4/8] Apply suggestion from @james-pre Co-authored-by: James Prevett --- src/zip/fs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zip/fs.ts b/src/zip/fs.ts index a143aa8..73e4aa2 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -110,7 +110,7 @@ export class ZipFS extends Readon throw withErrno('EPERM', 'Unexpectedly encountered an absolute path in a zip file.'); } // Strip the trailing '/' if it exists - const name = cd.name.endsWith('/') ? '/' + cd.name.slice(0, -1) : '/' + cd.name; + const name = '/' + (cd.name.endsWith('/') ? + cd.name.slice(0, -1) : cd.name); this.files.set(this._caseFold(name, true), cd); ptr += cd.size; } From cf06405e3f5a9be02c17902c931a19043743ff33 Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Wed, 5 Nov 2025 14:08:11 -0700 Subject: [PATCH 5/8] Improved options handling --- src/zip/fs.ts | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/src/zip/fs.ts b/src/zip/fs.ts index 73e4aa2..72406e6 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -1,7 +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 { CaseFold } from '@zenfs/core/internal/filesystem.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'; @@ -20,7 +19,7 @@ export interface ZipDataSource { /** * Configuration options for a ZipFS file system. */ -export interface ZipOptions { +export interface ZipOptions extends SharedConfig { /** * The zip file as a binary buffer. */ @@ -35,13 +34,6 @@ export interface ZipOptions { * Whether to wait to initialize entries */ lazy?: boolean; - - /** - * Case folding mode for file names (optional). - * Can be 'lower', or 'upper'. - * Default is undefined. - */ - caseFold?: CaseFold; } /** @@ -102,7 +94,7 @@ export class ZipFS extends Readon while (ptr < cdEnd) { const cd = await FileEntry.from(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. */ @@ -110,7 +102,7 @@ export class ZipFS extends Readon throw withErrno('EPERM', 'Unexpectedly encountered an absolute path in a zip file.'); } // Strip the trailing '/' if it exists - const name = '/' + (cd.name.endsWith('/') ? + cd.name.slice(0, -1) : cd.name); + const name = '/' + (cd.name.endsWith('/') ? +cd.name.slice(0, -1) : cd.name); this.files.set(this._caseFold(name, true), cd); ptr += cd.size; } @@ -147,8 +139,7 @@ export class ZipFS extends Readon public constructor( public label: string, protected data: ZipDataSource, - public readonly lazy: boolean = false, - public readonly caseFold?: CaseFold + protected readonly options: ZipOptions ) { super(0x207a6970, 'zipfs'); } @@ -237,10 +228,10 @@ export class ZipFS extends Readon } private _caseFold(original: string, update: boolean = false): string { - if (!this.caseFold) { + if (!this.options.caseFold) { return original; } - const folded = this.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); + const folded = this.options.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); if (update && !this.folded.has(folded) && folded !== original) { this.folded.set(folded, original); } @@ -323,19 +314,9 @@ 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 }, - caseFold: { type: 'string', required: false }, }, isAvailable(): boolean { @@ -343,7 +324,7 @@ const _Zip = { }, create(opt: ZipOptions): ZipFS { - return new ZipFS(opt.name ?? '', getSource(opt.data), opt.lazy, opt.caseFold); + return new ZipFS(opt.name ?? '', getSource(opt.data), opt); }, } satisfies Backend; type _Zip = typeof _Zip; From 8910c3f0d233282a2da45317d2bcd43400d2b5d4 Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Thu, 6 Nov 2025 15:06:54 -0700 Subject: [PATCH 6/8] Removed the need of folded map --- src/zip/fs.ts | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/zip/fs.ts b/src/zip/fs.ts index 72406e6..60476bb 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -66,7 +66,6 @@ export interface ZipOptions exten export class ZipFS extends Readonly(FileSystem) { protected files: Map> = new Map(); protected directories: Map> = new Map(); - protected folded: Map = new Map(); protected _time = Date.now(); private _ready: boolean = false; @@ -103,16 +102,15 @@ export class ZipFS extends Readon } // Strip the trailing '/' if it exists const name = '/' + (cd.name.endsWith('/') ? +cd.name.slice(0, -1) : cd.name); - this.files.set(this._caseFold(name, true), cd); + this.files.set(this._caseFold(name), cd); ptr += cd.size; } // Parse directory entries for (const entry of this.files.keys()) { - const name = this.folded.get(entry) ?? entry; - let { dir, base } = parse(name); + let { dir, base } = parse(entry); - dir = this._caseFold(dir, true); + dir = this._caseFold(dir); if (!this.directories.has(dir)) { this.directories.set(dir, new Set()); } @@ -122,10 +120,9 @@ export class ZipFS extends Readon // Add subdirectories to their parent's entries for (const entry of this.directories.keys()) { - const name = this.folded.get(entry) ?? entry; - let { dir, base } = parse(name); + let { dir, base } = parse(entry); - dir = this._caseFold(dir, true); + dir = this._caseFold(dir); if (base == '') continue; if (!this.directories.has(dir)) { @@ -187,9 +184,7 @@ export class ZipFS extends Readon const entries = this.directories.get(this._caseFold(path)); if (!entries) throw withErrno('ENODATA'); - return Array.from(entries).map(entry => { - return this.folded.get(entry) ?? entry; - }); + return Array.from(entries); } public readdirSync(path: string): string[] { @@ -199,7 +194,7 @@ export class ZipFS extends Readon const entries = this.directories.get(this._caseFold(path)); if (!entries) throw withErrno('ENODATA'); - return Array.from(entries).map(entry => this.folded.get(entry) ?? entry); + return Array.from(entries); } public async read(path: string, buffer: Uint8Array, offset: number, end: number): Promise { @@ -227,15 +222,11 @@ export class ZipFS extends Readon buffer.set(file.contents.subarray(offset, end)); } - private _caseFold(original: string, update: boolean = false): string { + private _caseFold(original: string): string { if (!this.options.caseFold) { return original; } - const folded = this.options.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); - if (update && !this.folded.has(folded) && folded !== original) { - this.folded.set(folded, original); - } - return folded; + return this.options.caseFold == 'upper' ? original.toUpperCase() : original.toLowerCase(); } } From 4de1e36bdccfff2cbd21867ee19ace5c06096f3b Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Thu, 6 Nov 2025 15:55:33 -0700 Subject: [PATCH 7/8] Fixed zip parsing --- src/zip/fs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zip/fs.ts b/src/zip/fs.ts index 60476bb..bbb0358 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -101,8 +101,8 @@ export class ZipFS extends Readon throw withErrno('EPERM', 'Unexpectedly encountered an absolute path in a zip file.'); } // Strip the trailing '/' if it exists - const name = '/' + (cd.name.endsWith('/') ? +cd.name.slice(0, -1) : cd.name); - this.files.set(this._caseFold(name), cd); + const name = cd.name.endsWith('/') ? cd.name.slice(0, -1) : cd.name; + this.files.set('/'+ this._caseFold(name), cd); ptr += cd.size; } From a2e18289f2521d2c40a94c513486330ca6f02270 Mon Sep 17 00:00:00 2001 From: Marcelo Lv Cabral Date: Thu, 6 Nov 2025 16:46:26 -0700 Subject: [PATCH 8/8] Implemented ISO support for Case Fold and unit tests --- src/iso/fs.ts | 40 +++++++++++++++++++++++++++++++++------- tests/iso.test.ts | 30 ++++++++++++++++++++++++++++-- tests/zip.test.ts | 30 +++++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/iso/fs.ts b/src/iso/fs.ts index 27bcd09..6f3f046 100644 --- a/src/iso/fs.ts +++ b/src/iso/fs.ts @@ -1,11 +1,12 @@ // 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'; @@ -13,7 +14,7 @@ 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. */ @@ -33,6 +34,8 @@ 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; /** @@ -40,10 +43,15 @@ export class IsoFS extends Readonly(Sync(FileSystem)) { * @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) { @@ -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; + 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; } return dir; } + private _caseFold(original: string): string { + if (!this.options.caseFold) { + return original; + } + + return this.options.caseFold === 'upper' ? original.toUpperCase() : original.toLowerCase(); + } + private _get(path: string, record: DirectoryRecord): Inode | undefined { if (record.isSymlink) { const target = resolve(path, record.symlinkPath); @@ -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; type _Iso = typeof _Iso; diff --git a/tests/iso.test.ts b/tests/iso.test.ts index 9eb5c65..63f5aa0 100644 --- a/tests/iso.test.ts +++ b/tests/iso.test.ts @@ -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 }); }); @@ -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); + }); +}); diff --git a/tests/zip.test.ts b/tests/zip.test.ts index cca0577..5934232 100644 --- a/tests/zip.test.ts +++ b/tests/zip.test.ts @@ -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!'); }); } @@ -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', () => {