From b5d5a012e77c2137edf58341bdadf43acdb20c4e Mon Sep 17 00:00:00 2001 From: James Prevett Date: Mon, 3 Feb 2025 10:56:39 -0600 Subject: [PATCH] Zip contents are now cached (resolves #12) --- src/iso/DirectoryRecord.ts | 71 +++++++++++++------------------------- src/zip/fs.ts | 5 +-- src/zip/zip.ts | 27 +++++++++------ 3 files changed, 42 insertions(+), 61 deletions(-) diff --git a/src/iso/DirectoryRecord.ts b/src/iso/DirectoryRecord.ts index b624f7f..01bc5f5 100644 --- a/src/iso/DirectoryRecord.ts +++ b/src/iso/DirectoryRecord.ts @@ -1,4 +1,4 @@ -import { Errno, ErrnoError } from '@zenfs/core'; +import { Errno, ErrnoError, log } from '@zenfs/core'; import { deserialize, member, struct, types as t } from 'utilium'; import { Directory } from './Directory.js'; import { SLComponentFlags } from './SLComponentRecord.js'; @@ -102,27 +102,23 @@ export class DirectoryRecord { public fileName(isoData: Uint8Array): string { if (this.hasRockRidge) { const fn = this._rockRidgeFilename(isoData); - if (fn != null) { - return fn; - } + if (fn != null) return fn; } const ident = this.identifier; - if (this.isDirectory(isoData)) { - return ident; - } + if (this.isDirectory(isoData)) return ident; + // Files: // - MUST have 0x2E (.) separating the name from the extension // - MUST have 0x3B (;) separating the file name and extension from the version // Gets expanded to two-byte char in Unicode directory records. const versionSeparator = ident.indexOf(';'); - if (versionSeparator === -1) { - // Some Joliet filenames lack the version separator, despite the standard specifying that it should be there. - return ident; - } - if (ident[versionSeparator - 1] === '.') { - // Empty extension. Do not include '.' in the filename. - return ident.slice(0, versionSeparator - 1); - } + + // Some Joliet filenames lack the version separator, despite the standard specifying that it should be there. + if (versionSeparator === -1) return ident; + + // Empty extension. Do not include '.' in the filename. + if (ident[versionSeparator - 1] === '.') return ident.slice(0, versionSeparator - 1); + // Include up to version separator. return ident.slice(0, versionSeparator); } @@ -175,26 +171,24 @@ export class DirectoryRecord { } public getFile(isoData: Uint8Array): Uint8Array { - if (this.isDirectory(isoData)) { - throw new Error('Tried to get a File from a directory.'); - } - this._file ||= isoData.slice(this.lba, this.lba + this.dataLength); + if (this.isDirectory(isoData)) throw log.err(ErrnoError.With('EISDIR', undefined, 'read')); + this._file ??= isoData.slice(this.lba, this.lba + this.dataLength); return this._file; } public getDirectory(isoData: Uint8Array): Directory { - if (!this.isDirectory(isoData)) { - throw new Error('Tried to get a Directory from a file.'); - } - this._dir ||= this._constructDirectory(isoData); + if (!this.isDirectory(isoData)) throw log.err(ErrnoError.With('ENOTDIR', undefined, 'read')); + this._dir ??= new Directory(this, isoData); return this._dir; } public getSUEntries(isoData: Uint8Array): SystemUseEntry[] { - if (!this._suEntries) { - this._constructSUEntries(isoData); - } - return this._suEntries!; + if (this._suEntries) return this._suEntries; + let i = 33 + this.data[32]; + if (i % 2 === 1) i++; // Skip padding fields. + i += this._rockRidgeOffset; + this._suEntries = constructSystemUseEntries(this.data, i, BigInt(this.length), isoData); + return this._suEntries; } protected getString(): string { @@ -208,35 +202,18 @@ export class DirectoryRecord { return (data: Uint8Array) => this._decoder!.decode(data).toLowerCase(); } - protected _constructDirectory(isoData: Uint8Array): Directory { - return new Directory(this, isoData); - } - protected _rockRidgeFilename(isoData: Uint8Array): string | null { const nmEntries = this.getSUEntries(isoData).filter(e => e instanceof NMEntry); - if (nmEntries.length === 0 || nmEntries[0].flags & (NMFlags.CURRENT | NMFlags.PARENT)) { - return null; - } + if (!nmEntries.length || nmEntries[0].flags & (NMFlags.CURRENT | NMFlags.PARENT)) return null; + let str = ''; for (const e of nmEntries) { str += e.name(this._decode); - if (!(e.flags & NMFlags.CONTINUE)) { - break; - } + if (!(e.flags & NMFlags.CONTINUE)) break; } return str; } - private _constructSUEntries(isoData: Uint8Array): void { - let i = 33 + this.data[32]; - if (i % 2 === 1) { - // Skip padding field. - i++; - } - i += this._rockRidgeOffset; - this._suEntries = constructSystemUseEntries(this.data, i, BigInt(this.length), isoData); - } - /** * !!ONLY VALID ON FIRST ENTRY OF ROOT DIRECTORY!! * Returns -1 if rock ridge is not enabled. Otherwise, returns the offset diff --git a/src/zip/fs.ts b/src/zip/fs.ts index caf9f3f..f97f3ae 100644 --- a/src/zip/fs.ts +++ b/src/zip/fs.ts @@ -186,10 +186,7 @@ export class ZipFS extends Readonly(Sync(FileSystem)) { public openFileSync(path: string, flag: string): File { if (isWriteable(flag)) throw new ErrnoError(Errno.EPERM, path); - - const stats = this.statSync(path); - - return new LazyFile(this, path, flag, stats); + return new LazyFile(this, path, flag, this.statSync(path)); } public readdirSync(path: string): string[] { diff --git a/src/zip/zip.ts b/src/zip/zip.ts index 36e9ffa..a365c42 100644 --- a/src/zip/zip.ts +++ b/src/zip/zip.ts @@ -177,16 +177,16 @@ export const sizeof_FileEntry = 46; export class FileEntry { public constructor( protected zipData: ArrayBufferLike, - protected _data: T + protected _buffer: T ) { - deserialize(this, _data); + deserialize(this, _buffer); // Sanity check. if (this.signature != 0x02014b50) { throw new ErrnoError(Errno.EINVAL, 'Invalid Zip file: Central directory record has invalid signature: ' + this.signature); } - this.name = safeDecode(this._data, this.useUTF8, sizeof_FileEntry, this.nameLength).replace(/\\/g, '/'); - this.comment = safeDecode(this._data, this.useUTF8, sizeof_FileEntry + this.nameLength + this.extraLength, this.commentLength); + this.name = safeDecode(this._buffer, this.useUTF8, sizeof_FileEntry, this.nameLength).replace(/\\/g, '/'); + this.comment = safeDecode(this._buffer, this.useUTF8, sizeof_FileEntry + this.nameLength + this.extraLength, this.commentLength); } @t.uint32 public signature!: number; @@ -329,7 +329,7 @@ export class FileEntry { */ public get extra(): T { const offset = 44 + this.nameLength; - return this._data.slice(offset, offset + this.extraLength) as T; + return this._buffer.slice(offset, offset + this.extraLength) as T; } /** @@ -366,11 +366,9 @@ export class FileEntry { return !this.isDirectory; } - /** - * Gets the file data, and decompresses it if needed. - * @see http://pkware.com/documents/casestudies/APPNOTE.TXT#:~:text=4.3.8 - */ - public get data(): Uint8Array { + protected _data?: Uint8Array; + + protected _decompress(): Uint8Array { // Get the local header before we can figure out where the actual compressed data starts. const { compressionMethod, size, name } = new LocalFileHeader(this.zipData.slice(this.headerRelativeOffset)); const data = this.zipData.slice(this.headerRelativeOffset + size); @@ -383,6 +381,15 @@ export class FileEntry { return decompress(data, this.compressedSize, this.uncompressedSize, this.flag); } + /** + * Gets the file data, and decompresses it if needed. + * @see http://pkware.com/documents/casestudies/APPNOTE.TXT#:~:text=4.3.8 + */ + public get data(): Uint8Array { + this._data ??= this._decompress(); + return this._data; + } + public get stats(): Stats { return new Stats({ mode: 0o555 | (this.isDirectory ? S_IFDIR : S_IFREG),