Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
65 changes: 49 additions & 16 deletions src/zip/fs.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,6 +35,13 @@
* 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;
}

/**
Expand Down Expand Up @@ -66,6 +74,7 @@
export class ZipFS<TBuffer extends ArrayBufferLike = ArrayBuffer> extends Readonly(FileSystem) {
protected files: Map<string, FileEntry<TBuffer>> = new Map();
protected directories: Map<string, Set<string>> = new Map();
protected folded: Map<string, string> = new Map();

protected _time = Date.now();
private _ready: boolean = false;
Expand Down Expand Up @@ -101,15 +110,17 @@
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);

Check warning on line 121 in src/zip/fs.ts

View workflow job for this annotation

GitHub Actions / CI

'base' is never reassigned. Use 'const' instead

dir = this._caseFold(dir, true);
if (!this.directories.has(dir)) {
this.directories.set(dir, new Set());
}
Expand All @@ -119,8 +130,10 @@

// 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);

Check warning on line 134 in src/zip/fs.ts

View workflow job for this annotation

GitHub Actions / CI

'base' is never reassigned. Use 'const' instead

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

if (!this.directories.has(dir)) {
Expand All @@ -134,7 +147,8 @@
public constructor(
public label: string,
protected data: ZipDataSource<TBuffer>,
public readonly lazy: boolean = false
public readonly lazy: boolean = false,
public readonly caseFold?: CaseFold
) {
super(0x207a6970, 'zipfs');
}
Expand All @@ -146,13 +160,14 @@
};
}

public async stat(path: string): Promise<Inode> {

Check warning on line 163 in src/zip/fs.ts

View workflow job for this annotation

GitHub Actions / CI

Async method 'stat' has no 'await' expression
return this.statSync(path);
}

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 +178,7 @@
});
}

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

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

Expand All @@ -178,36 +193,42 @@
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<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 +237,17 @@

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';
Expand All @@ -230,7 +262,7 @@
length: number;
}>();

const allDone = (async function __read() {

Check warning on line 265 in src/zip/fs.ts

View workflow job for this annotation

GitHub Actions / CI

'allDone' is assigned a value but never used
for await (const chunk of stream) {
data.set(chunk, bytesRead);
bytesRead += chunk.byteLength;
Expand Down Expand Up @@ -305,14 +337,15 @@
},
name: { type: 'string', required: false },
lazy: { type: 'boolean', required: false },
caseFold: { type: 'string', required: false },
},

isAvailable(): boolean {
return true;
},

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.lazy, opt.caseFold);
},
} satisfies Backend<ZipFS, ZipOptions>;
type _Zip = typeof _Zip;
Expand Down
Loading