feat: add local OneNote and ONEPKG reader
This commit is contained in:
99
src/onenote/onepkg/path.ts
Normal file
99
src/onenote/onepkg/path.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user