UNPKG

typia

Version:

Superfast runtime validators with only one line

223 lines (208 loc) 7.79 kB
import fs from "fs"; import path from "path"; export namespace FileSystemIdentity { export interface IIdentity { readonly caseSensitive: boolean; contains(file: string, directory: string): boolean; filesystemKey(file: string): string; isDeclarationFile(file: string): boolean; isSamePath(x: string, y: string): boolean; isSupportedExtension(file: string): boolean; projectFileKey(file: string): string; } export function create( caseSensitive: boolean, pathApi: typeof path.posix = path, ): IIdentity { const fold = (value: string): string => caseSensitive ? value : value.toLowerCase(); const filesystemKey = (file: string): string => fold(pathApi.normalize(file)); return { caseSensitive, contains: (file, directory) => { const fileKey: string = filesystemKey(pathApi.resolve(file)); const directoryKey: string = filesystemKey(pathApi.resolve(directory)); const prefix: string = directoryKey.endsWith(pathApi.sep) ? directoryKey : `${directoryKey}${pathApi.sep}`; return fileKey === directoryKey || fileKey.startsWith(prefix); }, filesystemKey, isDeclarationFile: (file) => DTS_PATTERN.test(fold(pathApi.basename(file))), isSamePath: (x, y) => filesystemKey(x) === filesystemKey(y), isSupportedExtension: (file) => { const filename: string = fold(pathApi.basename(file)); return ( TS_PATTERN.test(filename) && DTS_PATTERN.test(filename) === false ); }, projectFileKey: fold, }; } export class Policy { private caseSensitive_: boolean | undefined; private location_: string | undefined; public observe(caseSensitive: boolean | undefined, location: string): void { if (caseSensitive === undefined) return; if (this.caseSensitive_ === undefined) { this.caseSensitive_ = caseSensitive; this.location_ = location; } else if (this.caseSensitive_ !== caseSensitive) { throw new URIError( `Error on TypiaGenerateWizard.generate(): inconsistent filesystem case behavior between ${this.location_} and ${location}.`, ); } } public get(): IIdentity { if (this.caseSensitive_ === undefined) { throw new URIError( "Error on TypiaGenerateWizard.generate(): unable to determine filesystem case behavior for this run.", ); } return create(this.caseSensitive_); } } export async function inspectDirectory( directory: string, ): Promise<boolean | undefined> { const names: string[] = await fs.promises.readdir(directory); const entries: Set<string> = new Set(names); for (const name of names) { const alternate: string | undefined = alternateCase(name); if (alternate === undefined) continue; if (entries.has(alternate)) return true; try { await fs.promises.lstat(path.join(directory, alternate)); return false; } catch (error) { if (isMissingFileError(error)) return true; throw error; } } return undefined; } export async function probeDirectory(directory: string): Promise<boolean> { const inspected: boolean | undefined = await inspectDirectory(directory); if (inspected !== undefined) return inspected; for (let attempt = 0; attempt < 10; ++attempt) { const name: string = `.typia-case-probe-${process.pid}-${Date.now()}-${Math.random() .toString(36) .slice(2)}`.toLowerCase(); const original: string = path.join(directory, name); const alternate: string = path.join(directory, name.toUpperCase()); let handle: fs.promises.FileHandle; try { handle = await fs.promises.open(original, "wx"); } catch (error) { if (isAlreadyExistsError(error)) continue; throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to probe filesystem case behavior at ${directory}: ${formatUnknownError(error)}`, ); } let closed: boolean = false; try { await handle.close(); closed = true; try { await fs.promises.lstat(alternate); return false; } catch (error) { if (isMissingFileError(error)) return true; throw error; } } finally { let cleanupError: unknown; if (closed === false) { try { await handle.close(); } catch (error) { cleanupError = error; } } try { await fs.promises.unlink(original); } catch (error) { if (isMissingFileError(error) === false) cleanupError ??= error; } if (cleanupError !== undefined) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to clean filesystem case probe ${original}: ${formatUnknownError(cleanupError)}`, ); } } } throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to reserve a filesystem case probe at ${directory}.`, ); } /** * Builds the key that decides whether two paths are the same filesystem * object. * * The identity is read from `fs.BigIntStats` rather than `fs.Stats`, because * `fs.Stats.ino` is a JavaScript number. An NTFS file ID is `(sequenceNumber * << 48) | mftRecordIndex` and routinely exceeds `Number.MAX_SAFE_INTEGER` — * in a 4000-directory probe, 1879 of them did — where the spacing between * representable doubles is 8 or more. Two entries sharing a sequence number * with MFT record indices within that spacing round to one double. The * traversal callers would then treat a distinct directory or file as already * visited and skip it with no diagnostic, and the overwrite guard would * refuse a legitimate output (samchon/typia#2269). * * A filesystem that reports no inode at all keeps the canonical path * fallback, which is the only identity available there. * * @param stat Stats read with `{ bigint: true }`. * @param realpath Canonical path of the same object. * @returns Key that is equal for two paths only when they are one object. */ export function identityKey( stat: Pick<fs.BigIntStats, "dev" | "ino">, realpath: string, ): string { // `BigInt(0)` rather than `0n`: this package compiles at ES2016, where a // BigInt literal is a syntax error, while the constructor and the `bigint` // type are fine. return stat.ino === BigInt(0) ? `path:${path.normalize(realpath)}` : `inode:${stat.dev}:${stat.ino}`; } function alternateCase(name: string): string | undefined { let changed: boolean = false; let output: string = ""; for (const character of name) { if (character >= "a" && character <= "z") { output += character.toUpperCase(); changed = true; } else if (character >= "A" && character <= "Z") { output += character.toLowerCase(); changed = true; } else output += character; } return changed ? output : undefined; } function isAlreadyExistsError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST" ); } function isMissingFileError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT" ); } function formatUnknownError(error: unknown): string { return error instanceof Error ? error.message : String(error); } } const TS_PATTERN = /\.[cm]?tsx?$/; const DTS_PATTERN = /\.d\.[cm]?tsx?$/;