Files
stack/packages/library/src/supporting.mjs
T

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;
}