50 lines
2.7 KiB
JavaScript
50 lines
2.7 KiB
JavaScript
// C2-01d (M12): the paths an agent may write. A path passes only under a supporting prefix of the
|
|
// named universe, and only if nothing in the tree could turn the write into something else: a
|
|
// symlink or gitlink on the way, or a name that differs from a tree name only in case or Unicode
|
|
// form (APFS folds both, so her checkout would put the file somewhere else). Pure; reads no git.
|
|
import { LibraryError } from './problems.mjs';
|
|
import { classify } from './scope.mjs';
|
|
|
|
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
const MAX_BYTES = 1024;
|
|
const LINKS = new Set(['120000', '160000']);
|
|
const fold = name => name.normalize('NFC').toLowerCase();
|
|
|
|
// `tree` is the array listTree returns ({ mode, type, oid, size, path }). Returns `path` or throws
|
|
// library-write-story-path naming the class. Checks run in a fixed order, so a path with two faults
|
|
// always names the same one.
|
|
export function supportingPath(mapping, universe, path, tree) {
|
|
if (!Array.isArray(tree)) throw new TypeError('supportingPath needs the listTree array');
|
|
const refuse = message => { throw new LibraryError('library-write-story-path', message); };
|
|
if (typeof path !== 'string') refuse('the path must be a string');
|
|
const at = JSON.stringify(path);
|
|
if (Buffer.byteLength(path, 'utf8') > MAX_BYTES) refuse(`${at} is longer than ${MAX_BYTES} bytes`);
|
|
if (CONTROL.test(path)) refuse('the path holds a control character');
|
|
if (path.includes('\\')) refuse(`${at} holds a backslash`);
|
|
if (path.startsWith('/')) refuse(`${at} is absolute`);
|
|
const parts = path.split('/');
|
|
if (parts.some(part => part === '' || part === '.' || part === '..')) refuse(`${at} has an empty, . or .. segment`);
|
|
const place = classify(mapping, path);
|
|
if (place.scope !== 'supporting' || place.universe !== universe)
|
|
refuse(`${at} is not under a supporting folder of universe ${JSON.stringify(universe)}`);
|
|
const own = parts.map((_, i) => parts.slice(0, i + 1).join('/'));
|
|
const modes = new Map(tree.map(entry => [entry.path, entry.mode]));
|
|
for (const p of own) if (LINKS.has(modes.get(p))) refuse(`${at} goes through the symlink or gitlink ${JSON.stringify(p)}`);
|
|
const names = new Map();
|
|
for (const entry of tree) {
|
|
const segs = entry.path.split('/');
|
|
for (let i = 1; i <= segs.length; i++) {
|
|
const name = segs.slice(0, i).join('/');
|
|
const key = fold(name);
|
|
if (!names.has(key)) names.set(key, new Set());
|
|
names.get(key).add(name);
|
|
}
|
|
}
|
|
for (const p of own) {
|
|
const seen = names.get(fold(p));
|
|
if (seen && [...seen].some(name => name !== p))
|
|
refuse(`${at} differs only in case or Unicode form from ${JSON.stringify([...seen].find(name => name !== p))}`);
|
|
}
|
|
return path;
|
|
}
|