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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user