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
+2
View File
@@ -4,6 +4,7 @@ export * from './model/documentation.js';
export * from './model/graph.js';
export * from './model/link.js';
export * from './model/node.js';
export * from './model/virtualFileSystem.js';
export * from './runtime/config.js';
export * from './runtime/engine.js';
export * from './runtime/ipv4.js';
@@ -14,3 +15,4 @@ export * from './runtime/snapshot.js';
export * from './runtime/speed.js';
export * from './runtime/trace.js';
export * from './util/equal.js';
export * from './util/swiftString.js';
+163
View File
@@ -0,0 +1,163 @@
// Ported from filius-ipad (GPLv2/GPLv3):
// State/TopologyVirtualFileSystem.swift (defaultImageFiles, legacyDefaultImageData)
// The PNG images a new PC or notebook finds in /images, as Base64. Copied
// verbatim, line breaks removed.
/** The 1×1 black placeholder earlier filius-ipad versions wrote instead. */
export const LEGACY_DEFAULT_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
export const DEFAULT_IMAGE_FILES: readonly { name: string; base64: string }[] = [
{
name: 'network-map.png',
base64:
'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',
},
{
name: 'traffic-heatmap.png',
base64:
'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' +
'QmCC',
},
{
name: 'switch-backplane.png',
base64:
'iVBORw0KGgoAAAANSUhEUgAAAUAAAAC0CAMAAADSOgUjAAAAwFBMVEXa5ecPKzAYO0IHFxojTVRfhYsxWmI8VVlm44tHZWvW' +
'qEuTqKy1xcnI1tg6YWmkt7pbfIJrh4xWdHodQUh4k5isvcHS3uBifYKbsLOBmJyEnKDAz9EAAAAAAAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAABX2cKYAAAAQHRSTlP/////////////////////////////////////AAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAnFbtRQAABHVJREFUeNrtnYtWqzgUQM84GIwJN4C0vv7/P+ckhIK0XnWorcjeyxbIgSTs' +
'hlALi4gAAAAAAAAAAAAAAAAAwJXx1tqqdiKmEWmcFPJk7Y21zhkNiC5Lfnuw1gQRd6OpY7AylQas79fJ4QfbbxQjz0NJLm0u' +
'Eh5ybrqR5mNczjqXmyq0IoGmEte6RvZGd1prPggLtn+NAvXN68uq5UmwV+fNIDCGpbAhBYtJQZUVX+p0bw+5maAFVrpuXChz' +
'OalCKxJYeJGdtxIao5UfXakYkceZQCl1/307Cca2W6RgLyuFpch2pwK9k31cbtwhtyLls7M5h/yKFVqRQFc8hiimC4/eT4QN' +
'+/5GYAjy9CxmDL40h/bap6WwzvfNqpiVpfnv2kkjL3IRcaHN5fQVWhGVM502Kbt3zy/uWGDsq+zQa91YqY0twiFYtnmVfOz3' +
'YZ3vjfYdnTT2KcZey2rsJdJGg0BdKKrhg0oVWo8+PVp0PzqnPVTbVrNDuD3uA2Pv1wzBh8oOzvN7Cqftw7wF7o1u5Ur9GIbG' +
'JnZ3OISfuz4xV2g9AvWT1zNAsHosGjtxpUZTZ/5GYGV2ut6+HINV4acC+3Cc39m5wNYd9QrTk0i2miu0HkI6yvY3une2ne5d' +
'V9p2/jXG+niO1dY5Bt3QI8Zwk8MxJR73KW3oAOPCTODwNaZI2vvEvkIAAAAAf+UfiGDvKg6RtsggxpYpzNvcQOTrBtF3UuEX' +
'BaJtbhB/FzKIv4UG8feuQQReQiD+FhpEIAJ/mkCzVc4j0GyZMwjUXDb7+8Fbg/9P4Ib19QoXCty4vzcGzyDw7u7uqIRTaWVZ' +
'firtWtt+Nm25wCN/R7U+lVaWx7U5lXatbT+bNjOIQASuXeAW+8DzCtz2aXiZwNspG0g7t8Dbf6fc/vo0BCIQgQicCSwGJgX/' +
'2jQEIhCBCEQgAhGIQAQiEIE/RCD/yiEQgQhEIAK5JnKVayJclUMgAhGIQAQiEIEIRCACEYjAlQq8xF2h5y5jSX6XuEf63Pcl' +
'n7uMJfkhEIEXyu9+4MQyfeDH+d0ffk29P17mLPwxFxH4m399nghLad8h8FdfE5kIS2kIRCACEbilm4sQiEAEIhCBCEQgAhGI' +
'QAT+XIH8K4dABH5T2uEaSE77hmsi3GSOQO7S5y597o1BIAIvLBCD530ILQJ5jvSlnyPNk8x5lv61n6XPaA6MJ8KALAhEICAQ' +
'gQwsx8iGCGRwUoZ3ZYDhDfhjiPClQ4QzSP3CQeoPBmFEvgjGFunD4GJ/OFxqDwAAAGDV/BneTd1P9a9qK53vYsAZnXVt3VQS' +
'6trUdUDZOwL/1H4QGOeywKZ7Eq9qd/W4MpwWGD0lgY2LSbvU1uqqkcYPOhH4N4Gx4cWpS66k2+tbcNKKna0M7wjUJhj7QtvI' +
'eAS3rfUI/KRAbYJxuq/jIezTcRx7vy51iQ0CPxbo6zSt7KueeeOJ2Mdm2AbVGBD4DvrNpH7JAqU2aRrafAR38URSv/ZfYxD4' +
'JXYoAAAAAAAAAAAAAAC4IP8B6NExPbtCuTQAAAAASUVORK5CYII=',
},
];
@@ -0,0 +1,583 @@
// Ported from filius-ipad (GPLv2/GPLv3):
// State/TopologyVirtualFileSystem.swift
// The files of one PC or notebook: a flat map from normalized absolute paths
// to directories and files, case-insensitive lookup that keeps the spelling
// it was created with, and quotas. Every change works on a copy and replaces
// the state only when it succeeds, as Swift's value type does.
//
// Swift compares the path keys by canonical equivalence (a dictionary of
// Strings); the map here is keyed by the NFC form and keeps the spelling of
// the first insertion. Ordering and trimming follow Swift (swiftString.ts).
import {
containsSwiftControlCharacter,
swiftBase64Decode,
swiftCompare,
swiftEqual,
swiftHasPrefix,
swiftLowercased,
swiftTrim,
} from '../util/swiftString.js';
import { DEFAULT_IMAGE_FILES, LEGACY_DEFAULT_IMAGE_BASE64 } from './defaultImages.js';
export type VirtualFileContent =
| { readonly kind: 'directory' }
| { readonly kind: 'text'; readonly text: string }
| { readonly kind: 'binary'; readonly data: Uint8Array; readonly mediaType?: string }
| { readonly kind: 'image'; readonly data: Uint8Array; readonly mediaType: string };
export interface VirtualFileEntry {
readonly path: string;
readonly content: VirtualFileContent;
}
export type VirtualFileSystemErrorCode =
| 'pathMustBeAbsolute'
| 'pathEscapesRoot'
| 'invalidPathComponent'
| 'itemAlreadyExists'
| 'caseInsensitiveSiblingCollision'
| 'itemNotFound'
| 'parentDirectoryNotFound'
| 'expectedDirectory'
| 'expectedFile'
| 'expectedTextFile'
| 'expectedImageFile'
| 'directoryNotEmpty'
| 'cannotMutateRoot'
| 'cannotMoveDirectoryIntoItself'
| 'fileSizeQuotaExceeded'
| 'deviceSizeQuotaExceeded'
| 'deviceEntryQuotaExceeded'
| 'totalSizeQuotaExceeded'
| 'totalEntryQuotaExceeded';
/** Swift's TopologyVirtualFileSystemError: the case and its associated values. */
export class VirtualFileSystemError extends Error {
constructor(
readonly code: VirtualFileSystemErrorCode,
readonly values: readonly (string | number)[] = [],
) {
super(`${code}(${values.join(', ')})`);
this.name = 'VirtualFileSystemError';
}
}
function fail(code: VirtualFileSystemErrorCode, ...values: (string | number)[]): never {
throw new VirtualFileSystemError(code, values);
}
export function isDirectoryContent(content: VirtualFileContent): boolean {
return content.kind === 'directory';
}
const encoder = new TextEncoder();
export function contentByteCount(content: VirtualFileContent): number {
switch (content.kind) {
case 'directory':
return 0;
case 'text':
return encoder.encode(content.text).length;
case 'binary':
case 'image':
return content.data.length;
}
}
export function entryName(path: string): string {
if (path === '/') return '/';
return path.split('/').filter((part) => part !== '').at(-1) ?? '';
}
export function parentPath(normalizedPath: string): string {
if (normalizedPath === '/') return '/';
const separator = normalizedPath.lastIndexOf('/');
if (separator < 0) return '/';
return separator === 0 ? '/' : normalizedPath.slice(0, separator);
}
function depth(path: string): number {
return path.split('/').filter((part) => part !== '').length;
}
/** Shallow first, then by path — the order Swift applies a set of entries in. */
function byDepthThenPath(lhs: string, rhs: string): number {
return depth(lhs) - depth(rhs) || swiftCompare(lhs, rhs);
}
/**
* An absolute path with `\` as `/`, empty and `.` components dropped and `..`
* resolved; a component with surrounding whitespace or a control character
* is invalid.
*/
export function normalizedAbsolutePath(rawPath: string): string {
const path = rawPath.replaceAll('\\', '/');
if (!path.startsWith('/')) fail('pathMustBeAbsolute', rawPath);
const components: string[] = [];
for (const component of path.split('/')) {
if (component === '' || component === '.') continue;
if (component === '..') {
if (components.length === 0) fail('pathEscapesRoot', rawPath);
components.pop();
continue;
}
if (!swiftEqual(component, swiftTrim(component)) || containsSwiftControlCharacter(component)) {
fail('invalidPathComponent', component);
}
components.push(component);
}
return components.length === 0 ? '/' : `/${components.join('/')}`;
}
export const VIRTUAL_FILE_SYSTEM_LIMITS = {
// The 64 MiB project ceiling leaves headroom below the 128 MiB
// konfiguration.xml archive-entry limit for Base64 and XML structure.
maximumFileBytes: 8 * 1024 * 1024,
maximumDeviceBytes: 32 * 1024 * 1024,
maximumProjectBytes: 64 * 1024 * 1024,
maximumDeviceEntries: 4096,
maximumProjectEntries: 16_384,
} as const;
type Store = Map<string, { path: string; content: VirtualFileContent }>;
function keyOf(path: string): string {
return path.normalize('NFC');
}
export class VirtualFileSystem {
private store: Store;
constructor() {
this.store = new Map([[keyOf('/'), { path: '/', content: { kind: 'directory' } }]]);
}
/** `init(entries:)`: shallow entries first; each parent must exist. */
static fromEntries(entries: readonly VirtualFileEntry[]): VirtualFileSystem {
const fileSystem = new VirtualFileSystem();
const sorted = [...entries].sort((lhs, rhs) => byDepthThenPath(lhs.path, rhs.path));
for (const entry of sorted) {
const path = normalizedAbsolutePath(entry.path);
if (path === '/') fail('itemAlreadyExists', '/');
if (fileSystem.has(path)) fail('itemAlreadyExists', path);
if (fileSystem.get(parentPath(path))?.kind !== 'directory') fail('parentDirectoryNotFound', path);
fileSystem.ensureNoCaseInsensitiveSiblingCollision(path);
fileSystem.set(path, entry.content);
}
fileSystem.validateQuotas();
return fileSystem;
}
/** What a new PC or notebook starts with: notes, a log and three images. */
static defaultForDevice(): VirtualFileSystem {
const fileSystem = new VirtualFileSystem();
const attempt = (change: () => void) => {
try {
change();
} catch {
// `try?` in Swift: a default that cannot be written is skipped.
}
};
attempt(() => fileSystem.createDirectory('/home'));
attempt(() => fileSystem.createDirectory('/images'));
attempt(() => fileSystem.createDirectory('/var/log', true));
attempt(() => fileSystem.writeTextFile('/home/lab-notes.txt', 'Document deterministic runtime notes here.', false));
attempt(() => fileSystem.writeTextFile('/home/topology-exports.csv', 'timestamp,event,detail\n', false));
attempt(() => fileSystem.writeTextFile('/var/log/runtime-events.log', 'Virtual runtime initialized.\n', false));
for (const image of DEFAULT_IMAGE_FILES) {
const data = swiftBase64Decode(image.base64);
if (data === undefined) continue;
attempt(() => fileSystem.writeImageFile(`/images/${image.name}`, data, 'image/png', false));
}
return fileSystem;
}
/** Replaces the 1×1 black images of older versions by the current defaults; returns how many. */
upgradeLegacyDefaultImages(): number {
const legacy = swiftBase64Decode(LEGACY_DEFAULT_IMAGE_BASE64)!;
let replaced = 0;
for (const image of DEFAULT_IMAGE_FILES) {
const path = `/images/${image.name}`;
const existing = this.get(path);
const replacement = swiftBase64Decode(image.base64);
if (existing?.kind !== 'image' || !bytesEqual(existing.data, legacy) || replacement === undefined) continue;
this.set(path, { kind: 'image', data: replacement, mediaType: 'image/png' });
replaced += 1;
}
return replaced;
}
/** `validateProjectQuotas`: every device's quotas, then the project's totals. */
static validateProjectQuotas(fileSystems: Iterable<VirtualFileSystem>): void {
let totalBytes = 0;
let totalEntries = 0;
for (const fileSystem of fileSystems) {
fileSystem.validateQuotas();
totalBytes += fileSystem.totalFileBytes();
totalEntries += fileSystem.persistedEntryCount();
}
const limits = VIRTUAL_FILE_SYSTEM_LIMITS;
if (totalBytes > limits.maximumProjectBytes) fail('totalSizeQuotaExceeded', totalBytes, limits.maximumProjectBytes);
if (totalEntries > limits.maximumProjectEntries) {
fail('totalEntryQuotaExceeded', totalEntries, limits.maximumProjectEntries);
}
}
// ── Reading ──────────────────────────────────────────────────────────────
entry(rawPath: string): VirtualFileEntry {
const path = this.existingPath(rawPath);
const content = this.get(path);
if (content === undefined) fail('itemNotFound', path);
return { path, content };
}
contains(rawPath: string): boolean {
return this.existingPathIfPresent(rawPath) !== undefined;
}
allEntries(): VirtualFileEntry[] {
return [...this.store.values()]
.map(({ path, content }) => ({ path, content }))
.sort((lhs, rhs) => swiftCompare(lhs.path, rhs.path));
}
/** A directory's entries: directories first, then by name. */
entries(rawDirectoryPath: string): VirtualFileEntry[] {
const directoryPath = this.existingPath(rawDirectoryPath);
const content = this.get(directoryPath);
if (content === undefined) fail('itemNotFound', directoryPath);
if (content.kind !== 'directory') fail('expectedDirectory', directoryPath);
return [...this.store.values()]
.filter(({ path }) => !swiftEqual(path, directoryPath) && swiftEqual(parentPath(path), directoryPath))
.map(({ path, content: entryContent }) => ({ path, content: entryContent }))
.sort((lhs, rhs) => {
const lhsDirectory = lhs.content.kind === 'directory';
const rhsDirectory = rhs.content.kind === 'directory';
if (lhsDirectory !== rhsDirectory) return lhsDirectory ? -1 : 1;
const byName = swiftCompare(entryName(lhs.path), entryName(rhs.path));
return byName !== 0 ? byName : swiftCompare(lhs.path, rhs.path);
});
}
textFile(rawPath: string): string {
const entry = this.entry(rawPath);
if (entry.content.kind !== 'text') fail('expectedTextFile', entry.path);
return entry.content.text;
}
imageFile(rawPath: string): { data: Uint8Array; mediaType: string } {
const entry = this.entry(rawPath);
if (entry.content.kind !== 'image') fail('expectedImageFile', entry.path);
return { data: entry.content.data, mediaType: entry.content.mediaType };
}
/** Swift's `==`: the same paths (canonically) with equal contents. */
equals(other: VirtualFileSystem): boolean {
if (this.store.size !== other.store.size) return false;
for (const [key, { content }] of this.store) {
const theirs = other.store.get(key);
if (theirs === undefined || !contentEqual(content, theirs.content)) return false;
}
return true;
}
clone(): VirtualFileSystem {
const copy = new VirtualFileSystem();
copy.store = new Map(this.store);
return copy;
}
// ── Changing ─────────────────────────────────────────────────────────────
createDirectory(rawPath: string, recursive = false): void {
this.transaction((candidate) => candidate.createDirectoryUnchecked(rawPath, recursive));
}
writeTextFile(rawPath: string, text: string, overwrite = true): void {
this.writeFileContent({ kind: 'text', text }, rawPath, overwrite);
}
writeBinaryFile(rawPath: string, data: Uint8Array, mediaType?: string, overwrite = true): void {
this.writeFileContent(
mediaType === undefined ? { kind: 'binary', data } : { kind: 'binary', data, mediaType },
rawPath,
overwrite,
);
}
writeImageFile(rawPath: string, data: Uint8Array, mediaType: string, overwrite = true): void {
this.writeFileContent({ kind: 'image', data, mediaType }, rawPath, overwrite);
}
copyItem(rawSourcePath: string, rawDestinationPath: string, overwrite = false): void {
this.transaction((candidate) => candidate.copyItemUnchecked(rawSourcePath, rawDestinationPath, overwrite));
}
moveItem(rawSourcePath: string, rawDestinationPath: string, overwrite = false): void {
this.transaction((candidate) => candidate.moveItemUnchecked(rawSourcePath, rawDestinationPath, overwrite));
}
/** Renames in place; returns the new path. */
renameItem(rawPath: string, newName: string): string {
if (newName.includes('/') || newName.includes('\\')) fail('invalidPathComponent', newName);
const sourcePath = this.existingPath(rawPath);
if (sourcePath === '/') fail('cannotMutateRoot');
const parent = parentPath(sourcePath);
const destination = normalizedAbsolutePath(parent + (parent === '/' ? '' : '/') + newName);
this.moveItem(sourcePath, destination);
return this.existingPath(destination);
}
deleteItem(rawPath: string, recursive = false): void {
const path = this.existingPath(rawPath);
if (path === '/') fail('cannotMutateRoot');
const content = this.get(path);
if (content === undefined) fail('itemNotFound', path);
const descendants = [...this.store.values()].filter((entry) => swiftHasPrefix(entry.path, `${path}/`));
if (content.kind === 'directory' && !recursive && descendants.length > 0) fail('directoryNotEmpty', path);
for (const descendant of descendants) this.delete(descendant.path);
this.delete(path);
}
// ── Internals ────────────────────────────────────────────────────────────
private get(path: string): VirtualFileContent | undefined {
return this.store.get(keyOf(path))?.content;
}
private has(path: string): boolean {
return this.store.has(keyOf(path));
}
/** Assigning to an existing (canonically equal) key keeps its spelling, as a Swift dictionary does. */
private set(path: string, content: VirtualFileContent): void {
const key = keyOf(path);
const existing = this.store.get(key);
this.store.set(key, { path: existing?.path ?? path, content });
}
private delete(path: string): void {
this.store.delete(keyOf(path));
}
private transaction(change: (candidate: VirtualFileSystem) => void): void {
const candidate = this.clone();
change(candidate);
candidate.validateQuotas();
this.store = candidate.store;
}
private persistedEntryCount(): number {
return Math.max(0, this.store.size - 1);
}
private totalFileBytes(): number {
let total = 0;
for (const { content } of this.store.values()) total += contentByteCount(content);
return total;
}
private validateQuotas(): void {
const limits = VIRTUAL_FILE_SYSTEM_LIMITS;
for (const { path, content } of this.allEntries()) {
if (content.kind === 'directory') continue;
const bytes = contentByteCount(content);
if (bytes > limits.maximumFileBytes) fail('fileSizeQuotaExceeded', path, bytes, limits.maximumFileBytes);
}
const bytes = this.totalFileBytes();
if (bytes > limits.maximumDeviceBytes) fail('deviceSizeQuotaExceeded', bytes, limits.maximumDeviceBytes);
const entries = this.persistedEntryCount();
if (entries > limits.maximumDeviceEntries) fail('deviceEntryQuotaExceeded', entries, limits.maximumDeviceEntries);
}
private createDirectoryUnchecked(rawPath: string, recursive: boolean): void {
const normalized = normalizedAbsolutePath(rawPath);
if (normalized === '/') return;
const existing = this.existingPathIfPresent(normalized);
if (existing !== undefined) fail('itemAlreadyExists', existing);
const rawParent = parentPath(normalized);
let parent = this.existingPathIfPresent(rawParent);
if (parent === undefined) {
if (!recursive) fail('parentDirectoryNotFound', normalized);
this.createDirectoryUnchecked(rawParent, true);
parent = this.existingPath(rawParent);
}
if (this.get(parent)?.kind !== 'directory') fail('parentDirectoryNotFound', normalized);
const destination = join(parent, entryName(normalized));
this.ensureNoCaseInsensitiveSiblingCollision(destination);
this.set(destination, { kind: 'directory' });
}
private writeFileContent(content: VirtualFileContent, rawPath: string, overwrite: boolean): void {
const candidate = this.clone();
const normalized = normalizedAbsolutePath(rawPath);
if (normalized === '/') fail('expectedFile', normalized);
let destination: string;
const existing = candidate.existingPathIfPresent(normalized);
if (existing !== undefined) {
destination = existing;
if (!overwrite) {
if (swiftEqual(existing, normalized)) fail('itemAlreadyExists', existing);
fail('caseInsensitiveSiblingCollision', existing, normalized);
}
if (candidate.get(existing)?.kind === 'directory') fail('expectedFile', existing);
} else {
const parent = candidate.existingPath(parentPath(normalized));
if (candidate.get(parent)?.kind !== 'directory') fail('parentDirectoryNotFound', normalized);
destination = join(parent, entryName(normalized));
candidate.ensureNoCaseInsensitiveSiblingCollision(destination);
}
candidate.set(destination, content);
candidate.validateQuotas();
this.store = candidate.store;
}
/** The subtree at `sourcePath`, shallow first. */
private subtree(sourcePath: string): { path: string; content: VirtualFileContent }[] {
return [...this.store.values()]
.filter(({ path }) => swiftEqual(path, sourcePath) || swiftHasPrefix(path, `${sourcePath}/`))
.sort((lhs, rhs) => byDepthThenPath(lhs.path, rhs.path));
}
/** What copy and move check, and the destination they clear, before they write. */
private prepareTransfer(
sourcePath: string,
sourceContent: VirtualFileContent,
destinationPath: string,
overwrite: boolean,
skipSource: boolean,
): void {
if (
sourceContent.kind === 'directory' &&
(swiftHasPrefix(destinationPath, `${sourcePath}/`) || swiftHasPrefix(sourcePath, `${destinationPath}/`))
) {
fail('cannotMoveDirectoryIntoItself', sourcePath, destinationPath);
}
const destinationExisting = this.existingPathIfPresent(destinationPath);
if (destinationExisting !== undefined && !(skipSource && swiftEqual(destinationExisting, sourcePath))) {
if (!overwrite) fail('itemAlreadyExists', destinationExisting);
const destinationContent = this.get(destinationExisting);
if (destinationContent === undefined) fail('itemNotFound', destinationExisting);
if ((destinationContent.kind === 'directory') !== (sourceContent.kind === 'directory')) {
fail(sourceContent.kind === 'directory' ? 'expectedDirectory' : 'expectedFile', destinationExisting);
}
this.removeItemTree(destinationExisting);
}
if (this.get(parentPath(destinationPath))?.kind !== 'directory') fail('parentDirectoryNotFound', destinationPath);
this.ensureNoCaseInsensitiveSiblingCollision(destinationPath);
}
private copyItemUnchecked(rawSourcePath: string, rawDestinationPath: string, overwrite: boolean): void {
const sourcePath = this.existingPath(rawSourcePath);
const normalizedDestination = normalizedAbsolutePath(rawDestinationPath);
if (sourcePath === '/') fail('cannotMutateRoot');
const sourceContent = this.get(sourcePath);
if (sourceContent === undefined) fail('itemNotFound', sourcePath);
const destinationPath = this.canonicalDestinationPath(normalizedDestination);
if (swiftEqual(destinationPath, sourcePath)) {
if (overwrite) return;
fail('itemAlreadyExists', destinationPath);
}
this.prepareTransfer(sourcePath, sourceContent, destinationPath, overwrite, true);
for (const { path, content } of this.subtree(sourcePath)) {
this.set(destinationPath + path.slice(sourcePath.length), content);
}
}
private moveItemUnchecked(rawSourcePath: string, rawDestinationPath: string, overwrite: boolean): void {
const sourcePath = this.existingPath(rawSourcePath);
const normalizedDestination = normalizedAbsolutePath(rawDestinationPath);
if (sourcePath === '/') fail('cannotMutateRoot');
const sourceContent = this.get(sourcePath);
if (sourceContent === undefined) fail('itemNotFound', sourcePath);
const destinationPath = this.canonicalDestinationPath(normalizedDestination);
if (swiftEqual(destinationPath, sourcePath)) return;
this.prepareTransfer(sourcePath, sourceContent, destinationPath, overwrite, false);
const entries = this.subtree(sourcePath);
for (const { path } of entries) this.delete(path);
for (const { path, content } of entries) {
this.set(destinationPath + path.slice(sourcePath.length), content);
}
}
private canonicalDestinationPath(normalizedPath: string): string {
const existing = this.existingPathIfPresent(normalizedPath);
if (existing !== undefined) return existing;
const parent = this.existingPath(parentPath(normalizedPath));
return join(parent, entryName(normalizedPath));
}
private existingPathIfPresent(rawPath: string): string | undefined {
try {
return this.existingPath(rawPath);
} catch {
return undefined;
}
}
/** The stored spelling of a path, looked up component by component, case-insensitively. */
private existingPath(rawPath: string): string {
const normalized = normalizedAbsolutePath(rawPath);
if (normalized === '/') return '/';
let current = '/';
for (const component of normalized.split('/').filter((part) => part !== '')) {
const wanted = swiftLowercased(component);
const candidates = [...this.store.values()]
.filter(
({ path }) => swiftEqual(parentPath(path), current) && swiftEqual(swiftLowercased(entryName(path)), wanted),
)
.map(({ path }) => path);
if (candidates.length !== 1) fail('itemNotFound', normalized);
current = candidates[0]!;
}
return current;
}
private removeItemTree(path: string): void {
for (const entry of [...this.store.values()]) {
if (swiftEqual(entry.path, path) || swiftHasPrefix(entry.path, `${path}/`)) this.delete(entry.path);
}
}
private ensureNoCaseInsensitiveSiblingCollision(path: string): void {
const parent = parentPath(path);
const name = swiftLowercased(entryName(path));
const existing = [...this.store.values()]
.map((entry) => entry.path)
.filter(
(candidate) =>
!swiftEqual(candidate, path) &&
swiftEqual(parentPath(candidate), parent) &&
swiftEqual(swiftLowercased(entryName(candidate)), name),
)
.sort(swiftCompare)[0];
if (existing !== undefined) fail('caseInsensitiveSiblingCollision', existing, path);
}
}
function join(parent: string, name: string): string {
return parent === '/' ? `/${name}` : `${parent}/${name}`;
}
function bytesEqual(lhs: Uint8Array, rhs: Uint8Array): boolean {
return lhs.length === rhs.length && lhs.every((byte, index) => byte === rhs[index]);
}
function contentEqual(lhs: VirtualFileContent, rhs: VirtualFileContent): boolean {
if (lhs.kind !== rhs.kind) return false;
switch (lhs.kind) {
case 'directory':
return true;
case 'text':
return swiftEqual(lhs.text, (rhs as typeof lhs).text);
case 'binary':
case 'image': {
const other = rhs as typeof lhs;
return lhs.mediaType === other.mediaType && bytesEqual(lhs.data, other.data);
}
}
}
+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;
}