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,166 @@
|
||||
// The virtual file system against the Swift core: every case in
|
||||
// vfs.cases.json runs in Swift (vfs.swift.json, tools/swift-oracle --vfs);
|
||||
// each step's result or error and the final entries must match. The cases
|
||||
// include the three Swift tests on the file system, ported as cases.
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
VirtualFileSystem,
|
||||
VirtualFileSystemError,
|
||||
normalizedAbsolutePath,
|
||||
type VirtualFileContent,
|
||||
type VirtualFileEntry,
|
||||
} from '../src/index.js';
|
||||
|
||||
interface Step {
|
||||
op: string;
|
||||
path?: string;
|
||||
to?: string;
|
||||
name?: string;
|
||||
text?: string;
|
||||
size?: number;
|
||||
byte?: number;
|
||||
mediaType?: string;
|
||||
recursive?: boolean;
|
||||
overwrite?: boolean;
|
||||
entries?: { path: string; kind: string; text?: string }[];
|
||||
}
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const cases = JSON.parse(readFileSync(join(here, 'vfs.cases.json'), 'utf8')) as { name: string; steps: Step[] }[];
|
||||
const swift = JSON.parse(readFileSync(join(here, 'vfs.swift.json'), 'utf8')) as {
|
||||
name: string;
|
||||
results: unknown[];
|
||||
entries: unknown[];
|
||||
}[];
|
||||
|
||||
function describeEntries(entries: VirtualFileEntry[]): unknown[] {
|
||||
return entries.map(({ path, content }) => ({ path, ...describeContent(content) }));
|
||||
}
|
||||
|
||||
function describeContent(content: VirtualFileContent): Record<string, unknown> {
|
||||
switch (content.kind) {
|
||||
case 'directory':
|
||||
return { kind: 'directory' };
|
||||
case 'text':
|
||||
return { kind: 'text', text: content.text };
|
||||
case 'binary':
|
||||
case 'image':
|
||||
return {
|
||||
kind: content.kind,
|
||||
bytes: content.data.length,
|
||||
mediaType: content.mediaType ?? null,
|
||||
sha256: createHash('sha256').update(content.data).digest('hex'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function run(steps: Step[]) {
|
||||
let fileSystem = new VirtualFileSystem();
|
||||
const results: unknown[] = [];
|
||||
for (const step of steps) {
|
||||
const path = step.path ?? '/';
|
||||
const bytes = () => new Uint8Array(step.size ?? 0).fill(step.byte ?? 0);
|
||||
try {
|
||||
switch (step.op) {
|
||||
case 'createDirectory':
|
||||
fileSystem.createDirectory(path, step.recursive ?? false);
|
||||
break;
|
||||
case 'writeText':
|
||||
fileSystem.writeTextFile(path, step.text ?? '', step.overwrite ?? true);
|
||||
break;
|
||||
case 'writeBinary':
|
||||
fileSystem.writeBinaryFile(path, bytes(), step.mediaType, step.overwrite ?? true);
|
||||
break;
|
||||
case 'writeImage':
|
||||
fileSystem.writeImageFile(path, bytes(), step.mediaType ?? 'image/png', step.overwrite ?? true);
|
||||
break;
|
||||
case 'copy':
|
||||
fileSystem.copyItem(path, step.to ?? '/', step.overwrite ?? false);
|
||||
break;
|
||||
case 'move':
|
||||
fileSystem.moveItem(path, step.to ?? '/', step.overwrite ?? false);
|
||||
break;
|
||||
case 'rename':
|
||||
results.push({ ok: fileSystem.renameItem(path, step.name ?? '') });
|
||||
continue;
|
||||
case 'delete':
|
||||
fileSystem.deleteItem(path, step.recursive ?? false);
|
||||
break;
|
||||
case 'entry':
|
||||
results.push({ ok: describeEntries([fileSystem.entry(path)])[0] });
|
||||
continue;
|
||||
case 'entries':
|
||||
results.push({ ok: describeEntries(fileSystem.entries(path)) });
|
||||
continue;
|
||||
case 'text':
|
||||
results.push({ ok: fileSystem.textFile(path) });
|
||||
continue;
|
||||
case 'contains':
|
||||
results.push({ ok: fileSystem.contains(path) });
|
||||
continue;
|
||||
case 'normalize':
|
||||
results.push({ ok: normalizedAbsolutePath(path) });
|
||||
continue;
|
||||
case 'default':
|
||||
fileSystem = VirtualFileSystem.defaultForDevice();
|
||||
break;
|
||||
case 'fromEntries':
|
||||
fileSystem = VirtualFileSystem.fromEntries(
|
||||
(step.entries ?? []).map((entry) => ({
|
||||
path: entry.path,
|
||||
content: entry.kind === 'directory' ? { kind: 'directory' } : { kind: 'text', text: entry.text ?? '' },
|
||||
})),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown step ${step.op}`);
|
||||
}
|
||||
results.push({ ok: true });
|
||||
} catch (error) {
|
||||
if (!(error instanceof VirtualFileSystemError)) throw error;
|
||||
results.push({ error: error.code, values: error.values });
|
||||
}
|
||||
}
|
||||
return { results, entries: describeEntries(fileSystem.allEntries()) };
|
||||
}
|
||||
|
||||
describe('virtual file system against Swift', () => {
|
||||
it('covers every case', () => {
|
||||
expect(swift.map((result) => result.name)).toEqual(cases.map((item) => item.name));
|
||||
});
|
||||
|
||||
cases.forEach((item, index) => {
|
||||
it(item.name, () => {
|
||||
const { results, entries } = run(item.steps);
|
||||
const expected = swift[index]!;
|
||||
results.forEach((result, step) => {
|
||||
expect(result, `step ${step + 1}: ${JSON.stringify(item.steps[step])}`).toEqual(expected.results[step]);
|
||||
});
|
||||
expect(entries).toEqual(expected.entries);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('virtual file system', () => {
|
||||
it('leaves itself unchanged when a copy breaks a quota (Swift test)', () => {
|
||||
const fileSystem = new VirtualFileSystem();
|
||||
const maximum = new Uint8Array(8 * 1024 * 1024).fill(0x41);
|
||||
for (const name of ['source', 'destination', 'third', 'fourth']) {
|
||||
fileSystem.writeBinaryFile(`/${name}.bin`, maximum, 'application/octet-stream');
|
||||
}
|
||||
const before = fileSystem.clone();
|
||||
|
||||
expect(() => fileSystem.copyItem('/source.bin', '/new.bin', true)).toThrow(VirtualFileSystemError);
|
||||
expect(fileSystem.equals(before)).toBe(true);
|
||||
});
|
||||
|
||||
it('upgrades the 1×1 black images of older versions, and only those', () => {
|
||||
const fileSystem = VirtualFileSystem.defaultForDevice();
|
||||
expect(fileSystem.upgradeLegacyDefaultImages()).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user