feat(core): virtual file system and Swift string semantics

The core gains Swift's virtual file system (paths, case-insensitive
lookup that keeps the spelling, quotas, atomic changes, the defaults of
a new device) and the string behaviour the ported code relies on without
saying so. Both are measured in the Swift oracle: 11 file system cases
(the three Swift tests among them) step by step, and trimming, lower
case, equality, ordering and Base64 case by case — whitespace and control
characters over all of Unicode.

Found by measuring:
- Swift trims NEL and ZERO WIDTH SPACE, not the BOM; JS trim() does the
  opposite. The .fls import now trims with swiftTrim.
- lowercased() maps scalar by scalar: a word-final sigma stays σ.
- Foundation decodes "====" and "Y===" as empty data.
- Foundation on Linux counts 24,735 plane-14 scalars as control
  characters, a table artefact; the port keeps Unicode Cc+Cf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:23:45 +00:00
co-authored by Claude Opus 5.5
parent 51a83ea68d
commit a198d10b4a
15 changed files with 5247 additions and 18 deletions
+121
View File
@@ -0,0 +1,121 @@
// Swift String behaviour the ported code relies on without saying so.
// JavaScript strings compare and trim differently; each function here says
// which Swift expression it stands for. Measured against the Swift core
// (test/strings.cases.json, tools/swift-oracle --strings).
/**
* `value.trimmingCharacters(in: .whitespacesAndNewlines)`, scalar by scalar:
* Unicode spaces (Zs), ZERO WIDTH SPACE (U+200B, a space in old Unicode and
* still in Foundation's table), TAB, and the newlines LF, VT, FF, CR, NEL,
* LS, PS. Unlike JS trim(), NEL and U+200B are trimmed and the BOM is not.
* The whole set is measured against Swift (strings.swift.json).
*/
export function swiftTrim(value: string): string {
const codePoints = Array.from(value);
let start = 0;
let end = codePoints.length;
while (start < end && isSwiftWhitespaceOrNewline(codePoints[start]!)) start += 1;
while (end > start && isSwiftWhitespaceOrNewline(codePoints[end - 1]!)) end -= 1;
return codePoints.slice(start, end).join('');
}
const WHITESPACE_OR_NEWLINE = /^[\p{Zs}\u200B\t\n\u000B\f\r\u0085\u2028\u2029]$/u;
function isSwiftWhitespaceOrNewline(codePoint: string): boolean {
return WHITESPACE_OR_NEWLINE.test(codePoint);
}
/**
* `CharacterSet.controlCharacters.contains(scalar)` for some scalar: Cc or
* Cf. Measured: identical below plane 14; Foundation on Linux also counts
* 24,735 variation selectors and unassigned scalars from U+E0101 up, in a
* pattern that repeats every 256 — a table artefact this port does not copy.
*/
export function containsSwiftControlCharacter(value: string): boolean {
return /[\p{Cc}\p{Cf}]/u.test(value);
}
/**
* `value.lowercased()`: the lower-case mapping scalar by scalar, without
* context — JS toLowerCase() turns a word-final Σ into ς, Swift into σ.
*/
export function swiftLowercased(value: string): string {
return Array.from(value, (codePoint) => codePoint.toLowerCase()).join('');
}
/** `lhs == rhs`: Swift strings are equal when they are canonically equivalent. */
export function swiftEqual(lhs: string, rhs: string): boolean {
return lhs === rhs || lhs.normalize('NFC') === rhs.normalize('NFC');
}
/**
* `lhs < rhs` as a comparator: canonically equivalent strings are equal, the
* rest ordered by Unicode scalar values (JS `<` orders UTF-16 units, which
* puts U+FF5E after U+1F600).
*/
export function swiftCompare(lhs: string, rhs: string): number {
if (lhs === rhs) return 0;
const left = Array.from(lhs.normalize('NFC'), (codePoint) => codePoint.codePointAt(0)!);
const right = Array.from(rhs.normalize('NFC'), (codePoint) => codePoint.codePointAt(0)!);
const length = Math.min(left.length, right.length);
for (let index = 0; index < length; index++) {
if (left[index] !== right[index]) return left[index]! - right[index]!;
}
return left.length - right.length;
}
/** `lhs.hasPrefix(prefix)` for the path prefixes the port uses (a parent path and "/"). */
export function swiftHasPrefix(value: string, prefix: string): boolean {
return value.startsWith(prefix) || value.normalize('NFC').startsWith(prefix.normalize('NFC'));
}
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
/**
* `Data(base64Encoded: text, options: .ignoreUnknownCharacters)` as
* Foundation does it (measured): characters outside the alphabet and `=` are
* skipped; after the first `=` only more `=` may follow; symbols and padding
* together must be a multiple of four — "====" and "Y===" are empty data.
* Else nil (undefined).
*/
export function swiftBase64Decode(text: string): Uint8Array | undefined {
const values: number[] = [];
let padding = 0;
for (const character of text) {
if (character === '=') {
padding += 1;
continue;
}
const value = BASE64_ALPHABET.indexOf(character);
if (value < 0) continue;
if (padding > 0) return undefined;
values.push(value);
}
if ((values.length + padding) % 4 !== 0) return undefined;
const bytes = new Uint8Array(Math.floor((values.length * 6) / 8));
let length = 0;
let buffer = 0;
let bits = 0;
for (const value of values) {
buffer = ((buffer << 6) | value) & 0xffffff;
bits += 6;
if (bits >= 8) {
bits -= 8;
bytes[length++] = (buffer >> bits) & 0xff;
}
}
return bytes;
}
/** Base64 with padding, no line breaks — `data.base64EncodedString()`. */
export function base64Encode(bytes: Uint8Array): string {
let text = '';
for (let index = 0; index < bytes.length; index += 3) {
const chunk = (bytes[index]! << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
const remaining = bytes.length - index;
text += BASE64_ALPHABET[(chunk >> 18) & 63]! + BASE64_ALPHABET[(chunk >> 12) & 63]!;
text += remaining > 1 ? BASE64_ALPHABET[(chunk >> 6) & 63]! : '=';
text += remaining > 2 ? BASE64_ALPHABET[chunk & 63]! : '=';
}
return text;
}