import { fail } from './errors.js'; import type { OnePkgLimits } from './types.js'; const STRICT_UTF8 = new TextDecoder('utf-8', { fatal: true }); export function decodeCabFileName( bytes: Uint8Array, utf8: boolean, offset: number ): string { try { // OneNote/Joplin exports are observed to store UTF-8 names without setting // ATTR_NAME_IS_UTF. Strict UTF-8 is the only deterministic compatibility // fallback: malformed input is rejected and no legacy code page is guessed. return STRICT_UTF8.decode(bytes); } catch (error) { fail( utf8 ? 'cab-name-invalid-utf8' : 'cab-name-encoding-unsupported', utf8 ? `CAB filename is not valid UTF-8${error instanceof Error ? `: ${error.message}` : '.'}` : 'A non-ASCII CAB filename is not valid UTF-8; no legacy code-page guess was made.', { offset, structure: 'CFFILE.szName' } ); } } export interface NormalizedCabPath { path: string; normalizedPath: string; duplicateKey: string; } export function normalizeCabPath( path: string, limits: OnePkgLimits, offset: number ): NormalizedCabPath { if (path.length === 0) { fail('cab-path-empty', 'CAB entry has an empty path.', { offset, structure: 'CFFILE.szName', }); } if (path.length > limits.maxPathCharacters) { fail( 'cab-path-too-long', `CAB path exceeds the configured ${limits.maxPathCharacters}-character limit.`, { offset, structure: 'CFFILE.szName' } ); } if (/^[\\/]/u.test(path) || /^[A-Za-z]:/u.test(path)) { fail('cab-path-absolute', `Absolute CAB path is not allowed: ${path}`, { offset, structure: 'CFFILE.szName', }); } const slashPath = path.replaceAll('\\', '/'); const rawSegments = slashPath.split('/'); if (rawSegments.length > limits.maxPathDepth) { fail( 'cab-path-too-deep', `CAB path exceeds the configured ${limits.maxPathDepth}-segment limit.`, { offset, structure: 'CFFILE.szName' } ); } const segments = rawSegments.map((segment) => { if (segment.length === 0 || segment === '.' || segment === '..') { fail( 'cab-path-invalid-segment', `CAB path contains an unsafe or empty segment: ${path}`, { offset, structure: 'CFFILE.szName' } ); } if ( [...segment].some((character) => { const codePoint = character.codePointAt(0)!; return codePoint <= 0x1f || codePoint === 0x7f; }) ) { fail( 'cab-path-control-character', `CAB path contains a control character: ${path}`, { offset, structure: 'CFFILE.szName' } ); } return segment.normalize('NFC'); }); const normalizedPath = segments.join('/'); return { path, normalizedPath, // CAB paths have Windows semantics. NFC + lower-case catches the common // duplicate aliases without writing any archive path to the filesystem. duplicateKey: normalizedPath.toLowerCase(), }; }