1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
/**
* A name that can be written on every platform a member saves to
* (docs/MESHBAY_DESIGN.md §10).
*
* A node serves a file under the name its own disk gave it, and a name that is
* fine on ext4 can be impossible on Windows or on an exFAT drive: reserved
* characters, a trailing dot or space, `CON` or `aux.txt`. The node never
* rewrites a name — it is the string that opens the file — so the client does,
* at the moment it saves, and says so.
*
* The same rules as `meshbay_common.paths.sanitize_for_download`, and held to
* them by `test_portable_name_parity.py`: two copies of a rule that differ
* decide differently which files get renamed.
*/
const WINDOWS_RESERVED = new Set([
'CON', 'PRN', 'AUX', 'NUL',
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => `COM${i}`),
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => `LPT${i}`),
]);
const RESERVED_CHARS = new Set('<>:"/\\|?*');
const reserved = (c) => RESERVED_CHARS.has(c) || c.charCodeAt(0) < 32;
function isPortable(name) {
if (!name || name === '.' || name === '..') return false;
for (const c of name) if (reserved(c)) return false;
if (name.endsWith(' ') || name.endsWith('.')) return false;
return !WINDOWS_RESERVED.has(name.split('.', 1)[0].toUpperCase());
}
/** `name` if it can be written everywhere, otherwise the nearest name that can. */
export function portableName(name, replacement = '_') {
const text = String(name ?? '');
if (isPortable(text)) return text;
let out = Array.from(text).map((c) => (reserved(c) ? replacement : c)).join('');
out = out.replace(/[ .]+$/, '');
const dot = out.indexOf('.');
let stem = dot === -1 ? out : out.slice(0, dot);
const rest = dot === -1 ? '' : out.slice(dot);
if (WINDOWS_RESERVED.has(stem.toUpperCase())) stem += replacement;
out = stem + rest;
return out || 'unnamed';
}
/** Every segment of a relative path made portable, keeping `/` between them. */
export function portablePath(path) {
return String(path ?? '').split('/')
.map((segment) => (segment ? portableName(segment) : segment)).join('/');
}
|