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:
+5
-1
@@ -22,6 +22,10 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/runtime/speed.ts` | Engine, `TopologyNetworkRuntimeSpeed` | übertragen |
|
| `src/runtime/speed.ts` | Engine, `TopologyNetworkRuntimeSpeed` | übertragen |
|
||||||
| `src/runtime/trace.ts` | Engine, Paketspur-Modell | übertragen |
|
| `src/runtime/trace.ts` | Engine, Paketspur-Modell | übertragen |
|
||||||
| `src/util/equal.ts` | — | neu (Ersatz für Swifts `Equatable`) |
|
| `src/util/equal.ts` | — | neu (Ersatz für Swifts `Equatable`) |
|
||||||
|
| `src/util/swiftString.ts` | — | neu: Swift-String-Verhalten, das der übertragene Code stillschweigend voraussetzt (Trimmen, Kleinschreibung je Skalar, kanonische Gleichheit, Sortierung nach Skalarwerten, Base64 wie Foundation); am Swift-Orakel gemessen, Leerraum und Steuerzeichen über ganz Unicode |
|
||||||
|
| `src/model/virtualFileSystem.ts`, `src/model/defaultImages.ts` | `State/TopologyVirtualFileSystem.swift` | übertragen; Schlüssel in NFC statt Swifts kanonischer Wörterbuch-Gleichheit; Steuerzeichen nach Unicode Cc+Cf, ohne Foundations Tabellenfehler in Ebene 14 |
|
||||||
|
| `test/virtualFileSystem.test.ts`, `test/vfs.*.json` | `TopologyProjectPersistenceTests.swift`, `TopologyEditorReducerTests.swift` (drei Dateisystem-Tests, als Fälle) | übertragen und erweitert: 11 Fälle, Ergebnis aus dem Swift-Kern |
|
||||||
|
| `test/strings.test.ts`, `test/strings.*.json` | — | neu: Fälle und ganze Zeichenmengen, Ergebnis aus dem Swift-Kern |
|
||||||
| `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`-Referenzwerte an OpenJDK 21 gemessen |
|
| `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`-Referenzwerte an OpenJDK 21 gemessen |
|
||||||
| `test/oracle.test.ts`, `test/scenarios/` | — | neu: 5 Szenarien, Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`) |
|
| `test/oracle.test.ts`, `test/scenarios/` | — | neu: 5 Szenarien, Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`) |
|
||||||
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
|
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
|
||||||
@@ -33,7 +37,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen |
|
| `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen |
|
||||||
| `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene |
|
| `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene |
|
||||||
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
|
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
|
||||||
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
|
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`); gegen Swifts importiertes Modell auf 13 Dateien geprüft |
|
||||||
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) |
|
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) |
|
||||||
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem, Dateisystem von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
|
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem, Dateisystem von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
|
||||||
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from './model/documentation.js';
|
|||||||
export * from './model/graph.js';
|
export * from './model/graph.js';
|
||||||
export * from './model/link.js';
|
export * from './model/link.js';
|
||||||
export * from './model/node.js';
|
export * from './model/node.js';
|
||||||
|
export * from './model/virtualFileSystem.js';
|
||||||
export * from './runtime/config.js';
|
export * from './runtime/config.js';
|
||||||
export * from './runtime/engine.js';
|
export * from './runtime/engine.js';
|
||||||
export * from './runtime/ipv4.js';
|
export * from './runtime/ipv4.js';
|
||||||
@@ -14,3 +15,4 @@ export * from './runtime/snapshot.js';
|
|||||||
export * from './runtime/speed.js';
|
export * from './runtime/speed.js';
|
||||||
export * from './runtime/trace.js';
|
export * from './runtime/trace.js';
|
||||||
export * from './util/equal.js';
|
export * from './util/equal.js';
|
||||||
|
export * from './util/swiftString.js';
|
||||||
|
|||||||
@@ -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:
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAUAAAAC0CAMAAADSOgUjAAAAwFBMVEWaWzVWblUtXWIuJVQcboZxVDNcMEqpSEaOi0ZuNzVs' +
|
||||||
|
'hFrBPUOCLjc5Qzmxmzweg6PJy86GOWFePIQbMjzCQ0ugo6pNhoV2fIW+wcW7vsIQGzAUIzIWNU4jM00rJi0sJUwpHS3+/v6y' +
|
||||||
|
'ljQzNi1OJTG0O0RORy2NeDELSGeoiTNtKjQLZ40NVnVzZjGRNDmPNk5qWC4RdZmqOD1JLWuYhTRNKk4MLENLOC5XVDJTMnRt' +
|
||||||
|
'NGsMeqOsRTyMaDFzMk0RQ1pPNnn/AAAAQHRSTlP/////////////////////////////////////////////////////////' +
|
||||||
|
'////////////////////////////73leyQAAFuJJREFUeNrt3QlbGkm3AGAWhZhEY5JJvrlA07gAakRQJGoS9f//q1tdXcvZ' +
|
||||||
|
'qhdoxDGc5wlBBqO8c2rvqq5FudGrPqIKI/nXlvthOd9V7NesRR0e0mu9il+LKn5tlZ+7yve+acDoLQP2toBvBzDaAm4Bt4Bb' +
|
||||||
|
'wL8EMN4CbgG3gP8VwHhTgJuKqkcn8QqvvWnA3hZwC/jmAaMtIIEZpBGrP1vAcoADGLF7Vk3L/KoAe9UDDmjE6Ks3loGVAw4G' +
|
||||||
|
'OYCp4WoZCMK/XD88PDo8rKs/h3X11VG300me/88+6nd86ydvPTIP6X/R7zXfXUIubzGjwHpHXFlk/NyoV2hNpGNh1J/osNP5' +
|
||||||
|
'9q1uoNxjXb1c/xcBpt+m3+u+Wj0De0uOOsRsC722ljrQAHaU1L/RNwr4f40ESwDU7904YAaW/Nr6AOvfOvX/JYoY8BC9zwOm' +
|
||||||
|
'790wYA6W/Np6AFW91u180zVh8jyyjxBQvXJo6sCGee9GAbOxFmHU9WTg9+8JlspDkoGBIpy+d4OAWdnWV5EBOFhmeJcL2Pim' +
|
||||||
|
'QQ5pI/KvkcKA5r2bA5SLa9/Hwvy9TOd6yUZEN6uHEQZMyuq3Lu3GfDPvXTNgcNQhZZYBOzWxSB4IYjzggvFbGYmUAeT1HbLz' +
|
||||||
|
'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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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"trim": [
|
||||||
|
" a ",
|
||||||
|
"\t\na\r",
|
||||||
|
"\u0085a\u0085",
|
||||||
|
"\ufeffa",
|
||||||
|
"\u00a0a\u00a0",
|
||||||
|
"\u2028a\u2029",
|
||||||
|
"\u3000a",
|
||||||
|
"\u200ba",
|
||||||
|
"\u001ca",
|
||||||
|
"\u000ba\f",
|
||||||
|
" \u0301a",
|
||||||
|
"a \u0301",
|
||||||
|
"\u180ea",
|
||||||
|
"\u1680a"
|
||||||
|
],
|
||||||
|
"lowercased": [
|
||||||
|
"ABC",
|
||||||
|
"\u03a3\u0391\u03a3",
|
||||||
|
"\u0391\u03a3 x",
|
||||||
|
"\u0130",
|
||||||
|
"\u1e9e",
|
||||||
|
"\u01c5",
|
||||||
|
"\u00c4\u00d6\u00dc",
|
||||||
|
"\u212a"
|
||||||
|
],
|
||||||
|
"equal": [
|
||||||
|
[
|
||||||
|
"\u00e9",
|
||||||
|
"e\u0301"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"\u00c5",
|
||||||
|
"A\u030a"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"\ufb01",
|
||||||
|
"fi"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"K",
|
||||||
|
"\u212a"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"\u03a9",
|
||||||
|
"\u2126"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"A"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"\u00e9",
|
||||||
|
"\u00e9"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"sort": [
|
||||||
|
[
|
||||||
|
"b",
|
||||||
|
"a",
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"\uff5e",
|
||||||
|
"\ud83d\ude00"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"e\u0301x",
|
||||||
|
"\u00e9a"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"A",
|
||||||
|
"\u00e1",
|
||||||
|
"Z"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"file10",
|
||||||
|
"file2",
|
||||||
|
"File1"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"base64": [
|
||||||
|
"aGVsbG8=",
|
||||||
|
"aGVsbG8",
|
||||||
|
"aGVs bG8=",
|
||||||
|
"aGVsbG8=\n",
|
||||||
|
"aGVs=bG8=",
|
||||||
|
"====",
|
||||||
|
"",
|
||||||
|
"aGVsbG8==",
|
||||||
|
"YQ==",
|
||||||
|
"YQ",
|
||||||
|
"Y",
|
||||||
|
"!!!!YQ==",
|
||||||
|
"YQ==YQ==",
|
||||||
|
"YWJj",
|
||||||
|
"Y\u00e9Q==",
|
||||||
|
"YQ=\n=",
|
||||||
|
"Y===",
|
||||||
|
"YQ=",
|
||||||
|
"YWI=",
|
||||||
|
"YW==",
|
||||||
|
"=YQ=",
|
||||||
|
"A===",
|
||||||
|
"AA==",
|
||||||
|
"AAA=",
|
||||||
|
"AB==",
|
||||||
|
"ABC=",
|
||||||
|
"YQ===",
|
||||||
|
"YQ==\u0000",
|
||||||
|
"Y=Q=",
|
||||||
|
"YWJjZA",
|
||||||
|
"YWJjZA=",
|
||||||
|
"=",
|
||||||
|
"==",
|
||||||
|
"==="
|
||||||
|
],
|
||||||
|
"controlCharacters": [
|
||||||
|
"a",
|
||||||
|
"a\u200b",
|
||||||
|
"a\u007f",
|
||||||
|
"a\u00ad",
|
||||||
|
"a\u0085",
|
||||||
|
"\ufeff",
|
||||||
|
"a\ud83d\ude00",
|
||||||
|
"a\ue000"
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
|||||||
|
// Swift String behaviour, measured in the Swift core (strings.swift.json,
|
||||||
|
// tools/swift-oracle --strings): every case must come out the same here.
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
base64Encode,
|
||||||
|
containsSwiftControlCharacter,
|
||||||
|
swiftBase64Decode,
|
||||||
|
swiftCompare,
|
||||||
|
swiftEqual,
|
||||||
|
swiftLowercased,
|
||||||
|
swiftTrim,
|
||||||
|
} from '../src/index.js';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const cases = JSON.parse(readFileSync(join(here, 'strings.cases.json'), 'utf8')) as {
|
||||||
|
trim: string[];
|
||||||
|
lowercased: string[];
|
||||||
|
equal: [string, string][];
|
||||||
|
sort: string[][];
|
||||||
|
base64: string[];
|
||||||
|
controlCharacters: string[];
|
||||||
|
};
|
||||||
|
const swift = JSON.parse(readFileSync(join(here, 'strings.swift.json'), 'utf8')) as {
|
||||||
|
trim: string[];
|
||||||
|
lowercased: string[];
|
||||||
|
equal: boolean[];
|
||||||
|
sort: string[][];
|
||||||
|
base64: (number[] | null)[];
|
||||||
|
controlCharacters: boolean[];
|
||||||
|
whitespacesAndNewlinesSet: [number, number][];
|
||||||
|
controlCharactersSet: [number, number][];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The scalars for which `test` holds, as ranges [first, last] — the oracle's shape. */
|
||||||
|
function scalarRanges(test: (scalar: string) => boolean): [number, number][] {
|
||||||
|
const ranges: [number, number][] = [];
|
||||||
|
for (let value = 0; value <= 0x10ffff; value++) {
|
||||||
|
if (value >= 0xd800 && value <= 0xdfff) continue;
|
||||||
|
if (!test(String.fromCodePoint(value))) continue;
|
||||||
|
const last = ranges.at(-1);
|
||||||
|
if (last !== undefined && last[1] === value - 1) last[1] = value;
|
||||||
|
else ranges.push([value, value]);
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Swift string semantics', () => {
|
||||||
|
it('trims like trimmingCharacters(in: .whitespacesAndNewlines)', () => {
|
||||||
|
expect(cases.trim.map(swiftTrim)).toEqual(swift.trim);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lower-cases like lowercased()', () => {
|
||||||
|
expect(cases.lowercased.map(swiftLowercased)).toEqual(swift.lowercased);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('compares like ==', () => {
|
||||||
|
expect(cases.equal.map(([lhs, rhs]) => swiftEqual(lhs, rhs))).toEqual(swift.equal);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts like sorted()', () => {
|
||||||
|
expect(cases.sort.map((list) => [...list].sort(swiftCompare))).toEqual(swift.sort);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes Base64 like Data(base64Encoded:options: .ignoreUnknownCharacters)', () => {
|
||||||
|
expect(cases.base64.map((text) => {
|
||||||
|
const bytes = swiftBase64Decode(text);
|
||||||
|
return bytes === undefined ? null : [...bytes];
|
||||||
|
})).toEqual(swift.base64);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds control characters like CharacterSet.controlCharacters', () => {
|
||||||
|
expect(cases.controlCharacters.map(containsSwiftControlCharacter)).toEqual(swift.controlCharacters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims exactly the scalars Swift trims, all of Unicode', () => {
|
||||||
|
expect(scalarRanges((scalar) => swiftTrim(`${scalar}a`) === 'a')).toEqual(swift.whitespacesAndNewlinesSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts the same scalars as control characters, except Foundation\'s plane-14 artefact', () => {
|
||||||
|
const below = (ranges: [number, number][]) => ranges.filter(([first]) => first < 0xe0100);
|
||||||
|
expect(scalarRanges(containsSwiftControlCharacter).filter(([first]) => first < 0xe0100)).toEqual(
|
||||||
|
below(swift.controlCharactersSet).map(([first, last]) => [first, Math.min(last, 0xe00ff)]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes Base64 so that it decodes again', () => {
|
||||||
|
for (const length of [0, 1, 2, 3, 4, 5, 255]) {
|
||||||
|
const bytes = Uint8Array.from({ length }, (_, index) => (index * 37) & 0xff);
|
||||||
|
expect(swiftBase64Decode(base64Encode(bytes))).toEqual(bytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "whitespace components and case-insensitive siblings (Swift test)",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/docs/Readme.txt",
|
||||||
|
"text": "one"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/docs/ note.txt",
|
||||||
|
"text": "two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/DOCS/readme.TXT",
|
||||||
|
"text": "two",
|
||||||
|
"overwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "text",
|
||||||
|
"path": "/docs/Readme.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entry",
|
||||||
|
"path": "/DOCS/README.TXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/docs/README.txt",
|
||||||
|
"text": "three",
|
||||||
|
"overwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "fromEntries",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/docs",
|
||||||
|
"kind": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/docs/Readme.txt",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "one"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/docs/readme.TXT",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "two"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quotas are explicit and atomic (Swift test)",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/oversized.bin",
|
||||||
|
"size": 8388609,
|
||||||
|
"byte": 65
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "contains",
|
||||||
|
"path": "/oversized.bin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/file-0.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 66
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/file-1.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 66
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/file-2.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 66
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/file-3.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 66
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/file-4.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 66
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "contains",
|
||||||
|
"path": "/file-4.bin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "overwrite uses the final state's quota, move is atomic (Swift test)",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/source.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 65,
|
||||||
|
"mediaType": "application/octet-stream"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/destination.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 65,
|
||||||
|
"mediaType": "application/octet-stream"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/third.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 65,
|
||||||
|
"mediaType": "application/octet-stream"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeBinary",
|
||||||
|
"path": "/fourth.bin",
|
||||||
|
"size": 8388608,
|
||||||
|
"byte": 65,
|
||||||
|
"mediaType": "application/octet-stream"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/SOURCE.BIN",
|
||||||
|
"to": "/DESTINATION.BIN",
|
||||||
|
"overwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/source.bin",
|
||||||
|
"to": "/new.bin",
|
||||||
|
"overwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "move",
|
||||||
|
"path": "/SOURCE.BIN",
|
||||||
|
"to": "/DESTINATION.BIN",
|
||||||
|
"overwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "contains",
|
||||||
|
"path": "/source.bin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "contains",
|
||||||
|
"path": "/destination.bin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "path normalization",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/./b/../c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/.."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/../.."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "\\a\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/ b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/b "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/b\u0007"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/b\u200b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "//a//b//"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/\u0085b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/\ufeffb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/a/\u00a0b\u00a0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "normalize",
|
||||||
|
"path": "/\u00e9/e\u0301"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "canonical equivalence and scalar-wise lower case",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/\u00e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entry",
|
||||||
|
"path": "/e\u0301"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/E\u0301/x.txt",
|
||||||
|
"text": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/e\u0301"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/\u039f\u0394\u039f\u03a3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entry",
|
||||||
|
"path": "/\u03bf\u03b4\u03bf\u03c3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entry",
|
||||||
|
"path": "/\u03bf\u03b4\u03bf\u03c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/Stra\u00dfe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/STRASSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/\u212aelvin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entry",
|
||||||
|
"path": "/kelvin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "listing order",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/b.txt",
|
||||||
|
"text": "b.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/a.txt",
|
||||||
|
"text": "a.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/Zeta",
|
||||||
|
"text": "Zeta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/\u00e9mile",
|
||||||
|
"text": "\u00e9mile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/file10",
|
||||||
|
"text": "file10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/file2",
|
||||||
|
"text": "file2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/\uff5e",
|
||||||
|
"text": "\uff5e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/\ud83d\ude00",
|
||||||
|
"text": "\ud83d\ude00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/zdir"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/Adir"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entries",
|
||||||
|
"path": "/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "copy, move, rename and delete trees",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/a/b/c",
|
||||||
|
"recursive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/a/b/c/f.txt",
|
||||||
|
"text": "f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/a/g.txt",
|
||||||
|
"text": "g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/a",
|
||||||
|
"to": "/a/b/x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/a/b",
|
||||||
|
"to": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/A/B",
|
||||||
|
"to": "/copy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entries",
|
||||||
|
"path": "/copy/c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "move",
|
||||||
|
"path": "/copy",
|
||||||
|
"to": "/a/moved"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "move",
|
||||||
|
"path": "/a",
|
||||||
|
"to": "/a/b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "rename",
|
||||||
|
"path": "/a/moved",
|
||||||
|
"name": "Renamed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "rename",
|
||||||
|
"path": "/a/g.txt",
|
||||||
|
"name": "x/y"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "delete",
|
||||||
|
"path": "/a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "delete",
|
||||||
|
"path": "/",
|
||||||
|
"recursive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/a/g.txt",
|
||||||
|
"to": "/a/b",
|
||||||
|
"overwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "copy",
|
||||||
|
"path": "/a/b",
|
||||||
|
"to": "/a/g.txt",
|
||||||
|
"overwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "delete",
|
||||||
|
"path": "/A/b",
|
||||||
|
"recursive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/a",
|
||||||
|
"text": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "text",
|
||||||
|
"path": "/a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/a/g.txt/x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "createDirectory",
|
||||||
|
"path": "/q/r"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file kinds",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "writeImage",
|
||||||
|
"path": "/i.png",
|
||||||
|
"size": 10,
|
||||||
|
"byte": 1,
|
||||||
|
"mediaType": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "text",
|
||||||
|
"path": "/i.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/",
|
||||||
|
"text": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "writeText",
|
||||||
|
"path": "/missing/x.txt",
|
||||||
|
"text": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entry",
|
||||||
|
"path": "/i.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "defaults of a new device",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entries",
|
||||||
|
"path": "/images"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entries need their parents",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "fromEntries",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/a/b",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "x"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "fromEntries",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"kind": "directory"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "fromEntries",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/b",
|
||||||
|
"kind": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/b/c",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/a",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "a"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "canonically equal entries are one path",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "fromEntries",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/\u00e9",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "composed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/e\u0301",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "decomposed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "fromEntries",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/\u00e9",
|
||||||
|
"kind": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/e\u0301/x",
|
||||||
|
"kind": "text",
|
||||||
|
"text": "x"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "entries",
|
||||||
|
"path": "/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,863 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/docs\/Readme.txt",
|
||||||
|
"text" : "two"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "whitespace components and case-insensitive siblings (Swift test)",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
" note.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : {
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/docs\/Readme.txt",
|
||||||
|
"text" : "two"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "caseInsensitiveSiblingCollision",
|
||||||
|
"values" : [
|
||||||
|
"\/docs\/Readme.txt",
|
||||||
|
"\/docs\/README.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "caseInsensitiveSiblingCollision",
|
||||||
|
"values" : [
|
||||||
|
"\/docs\/Readme.txt",
|
||||||
|
"\/docs\/readme.TXT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : null,
|
||||||
|
"path" : "\/file-0.bin",
|
||||||
|
"sha256" : "001224bdbc0a675a104bc57050e10365bce70ab7ca449685f8142460b0dd5ba5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : null,
|
||||||
|
"path" : "\/file-1.bin",
|
||||||
|
"sha256" : "001224bdbc0a675a104bc57050e10365bce70ab7ca449685f8142460b0dd5ba5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : null,
|
||||||
|
"path" : "\/file-2.bin",
|
||||||
|
"sha256" : "001224bdbc0a675a104bc57050e10365bce70ab7ca449685f8142460b0dd5ba5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : null,
|
||||||
|
"path" : "\/file-3.bin",
|
||||||
|
"sha256" : "001224bdbc0a675a104bc57050e10365bce70ab7ca449685f8142460b0dd5ba5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "quotas are explicit and atomic (Swift test)",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"error" : "fileSizeQuotaExceeded",
|
||||||
|
"values" : [
|
||||||
|
"\/oversized.bin",
|
||||||
|
8388609,
|
||||||
|
8388608
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "deviceSizeQuotaExceeded",
|
||||||
|
"values" : [
|
||||||
|
41943040,
|
||||||
|
33554432
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : "application\/octet-stream",
|
||||||
|
"path" : "\/destination.bin",
|
||||||
|
"sha256" : "b16bd32b101132fd0102461bc75ea65442c37293ac881ae953486c8ac26a7388"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : "application\/octet-stream",
|
||||||
|
"path" : "\/fourth.bin",
|
||||||
|
"sha256" : "b16bd32b101132fd0102461bc75ea65442c37293ac881ae953486c8ac26a7388"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 8388608,
|
||||||
|
"kind" : "binary",
|
||||||
|
"mediaType" : "application\/octet-stream",
|
||||||
|
"path" : "\/third.bin",
|
||||||
|
"sha256" : "b16bd32b101132fd0102461bc75ea65442c37293ac881ae953486c8ac26a7388"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "overwrite uses the final state's quota, move is atomic (Swift test)",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "deviceSizeQuotaExceeded",
|
||||||
|
"values" : [
|
||||||
|
41943040,
|
||||||
|
33554432
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "path normalization",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"error" : "pathMustBeAbsolute",
|
||||||
|
"values" : [
|
||||||
|
"a"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/a\/c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "pathEscapesRoot",
|
||||||
|
"values" : [
|
||||||
|
"\/.."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "pathEscapesRoot",
|
||||||
|
"values" : [
|
||||||
|
"\/a\/..\/.."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/a\/b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
" b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
"b "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
"b\u0007"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
"b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/a\/b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/a\/..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
"
b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
"b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
" b "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/é\/é"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/Kelvin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/STRASSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/Straße"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/é"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/é\/x.txt",
|
||||||
|
"text" : "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/ΟΔΟΣ"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "canonical equivalence and scalar-wise lower case",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : {
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/é"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "itemAlreadyExists",
|
||||||
|
"values" : [
|
||||||
|
"\/é"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : {
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/ΟΔΟΣ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "itemNotFound",
|
||||||
|
"values" : [
|
||||||
|
"\/οδος"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : {
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/Kelvin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/Adir"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/Zeta",
|
||||||
|
"text" : "Zeta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/a.txt",
|
||||||
|
"text" : "a.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/b.txt",
|
||||||
|
"text" : "b.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/file10",
|
||||||
|
"text" : "file10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/file2",
|
||||||
|
"text" : "file2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/zdir"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/émile",
|
||||||
|
"text" : "émile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/~",
|
||||||
|
"text" : "~"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/😀",
|
||||||
|
"text" : "😀"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "listing order",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/Adir"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/zdir"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/Zeta",
|
||||||
|
"text" : "Zeta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/a.txt",
|
||||||
|
"text" : "a.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/b.txt",
|
||||||
|
"text" : "b.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/file10",
|
||||||
|
"text" : "file10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/file2",
|
||||||
|
"text" : "file2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/émile",
|
||||||
|
"text" : "émile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/~",
|
||||||
|
"text" : "~"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/😀",
|
||||||
|
"text" : "😀"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/a\/Renamed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/a\/Renamed\/c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/a\/Renamed\/c\/f.txt",
|
||||||
|
"text" : "f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/a\/g.txt",
|
||||||
|
"text" : "g"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "copy, move, rename and delete trees",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "cannotMoveDirectoryIntoItself",
|
||||||
|
"values" : [
|
||||||
|
"\/a",
|
||||||
|
"\/a\/b\/x"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "itemAlreadyExists",
|
||||||
|
"values" : [
|
||||||
|
"\/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : [
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/copy\/c\/f.txt",
|
||||||
|
"text" : "f"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "cannotMoveDirectoryIntoItself",
|
||||||
|
"values" : [
|
||||||
|
"\/a",
|
||||||
|
"\/a\/b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : "\/a\/Renamed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "invalidPathComponent",
|
||||||
|
"values" : [
|
||||||
|
"x\/y"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "directoryNotEmpty",
|
||||||
|
"values" : [
|
||||||
|
"\/a"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "cannotMutateRoot",
|
||||||
|
"values" : [
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "expectedFile",
|
||||||
|
"values" : [
|
||||||
|
"\/a\/b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "expectedDirectory",
|
||||||
|
"values" : [
|
||||||
|
"\/a\/g.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "expectedFile",
|
||||||
|
"values" : [
|
||||||
|
"\/a"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "expectedTextFile",
|
||||||
|
"values" : [
|
||||||
|
"\/a"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "parentDirectoryNotFound",
|
||||||
|
"values" : [
|
||||||
|
"\/a\/g.txt\/x"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "parentDirectoryNotFound",
|
||||||
|
"values" : [
|
||||||
|
"\/q\/r"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 10,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/i.png",
|
||||||
|
"sha256" : "ffadf8d89d37b3b55fe1847b513cf92e3be87e4c168708c7851845df96fb36be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "file kinds",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "expectedTextFile",
|
||||||
|
"values" : [
|
||||||
|
"\/i.png"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "expectedFile",
|
||||||
|
"values" : [
|
||||||
|
"\/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "itemNotFound",
|
||||||
|
"values" : [
|
||||||
|
"\/missing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : {
|
||||||
|
"bytes" : 10,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/i.png",
|
||||||
|
"sha256" : "ffadf8d89d37b3b55fe1847b513cf92e3be87e4c168708c7851845df96fb36be"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/home\/lab-notes.txt",
|
||||||
|
"text" : "Document deterministic runtime notes here."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/home\/topology-exports.csv",
|
||||||
|
"text" : "timestamp,event,detail\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/images"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 2352,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/images\/network-map.png",
|
||||||
|
"sha256" : "4105bdb0131a539791f3b0f00e43341de6a5e04ab21b0cda19ac49548cd0cdd1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 1478,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/images\/switch-backplane.png",
|
||||||
|
"sha256" : "8ae2492515a66cd69d729d70d449ece4e6c23ee151f98da44c6b7862058c463f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 6195,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/images\/traffic-heatmap.png",
|
||||||
|
"sha256" : "6b541170a55a139281170fcf1176ce0b962f1ab58b1f26b9d2cff7558711825b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/var"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/var\/log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/var\/log\/runtime-events.log",
|
||||||
|
"text" : "Virtual runtime initialized.\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "defaults of a new device",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : [
|
||||||
|
{
|
||||||
|
"bytes" : 2352,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/images\/network-map.png",
|
||||||
|
"sha256" : "4105bdb0131a539791f3b0f00e43341de6a5e04ab21b0cda19ac49548cd0cdd1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 1478,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/images\/switch-backplane.png",
|
||||||
|
"sha256" : "8ae2492515a66cd69d729d70d449ece4e6c23ee151f98da44c6b7862058c463f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bytes" : 6195,
|
||||||
|
"kind" : "image",
|
||||||
|
"mediaType" : "image\/png",
|
||||||
|
"path" : "\/images\/traffic-heatmap.png",
|
||||||
|
"sha256" : "6b541170a55a139281170fcf1176ce0b962f1ab58b1f26b9d2cff7558711825b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/a",
|
||||||
|
"text" : "a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/b\/c",
|
||||||
|
"text" : "c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "entries need their parents",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"error" : "parentDirectoryNotFound",
|
||||||
|
"values" : [
|
||||||
|
"\/a\/b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error" : "itemAlreadyExists",
|
||||||
|
"values" : [
|
||||||
|
"\/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entries" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/é"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind" : "text",
|
||||||
|
"path" : "\/é\/x",
|
||||||
|
"text" : "x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "canonically equal entries are one path",
|
||||||
|
"results" : [
|
||||||
|
{
|
||||||
|
"error" : "itemAlreadyExists",
|
||||||
|
"values" : [
|
||||||
|
"\/é"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ok" : [
|
||||||
|
{
|
||||||
|
"kind" : "directory",
|
||||||
|
"path" : "\/é"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
inDeterministicRenderOrder,
|
inDeterministicRenderOrder,
|
||||||
interfaceKey,
|
interfaceKey,
|
||||||
isSafeFrame,
|
isSafeFrame,
|
||||||
|
swiftTrim,
|
||||||
topologyNode,
|
topologyNode,
|
||||||
DOCUMENTATION_BLACK,
|
DOCUMENTATION_BLACK,
|
||||||
DOCUMENTATION_PALE_YELLOW,
|
DOCUMENTATION_PALE_YELLOW,
|
||||||
@@ -235,9 +236,14 @@ function importNode(source: FlsNode, newId: () => Id, warnings: string[]): Topol
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Swift's `value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""`. */
|
||||||
|
function trimmedOrEmpty(value: string | null | undefined): string {
|
||||||
|
return value == null ? '' : swiftTrim(value);
|
||||||
|
}
|
||||||
|
|
||||||
/** Swift's normalizedOrDefault; Java's null (a rejected address) counts as empty. */
|
/** Swift's normalizedOrDefault; Java's null (a rejected address) counts as empty. */
|
||||||
function orDefault(value: string | null, fallback: string): string {
|
function orDefault(value: string | null, fallback: string): string {
|
||||||
const trimmed = value?.trim() ?? '';
|
const trimmed = trimmedOrEmpty(value);
|
||||||
return trimmed === '' ? fallback : trimmed;
|
return trimmed === '' ? fallback : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,8 +259,8 @@ function registerConfigurations(
|
|||||||
deviceConfigurations.set(node.id, {
|
deviceConfigurations.set(node.id, {
|
||||||
ipAddress: orDefault(nic?.ip ?? null, '192.168.0.10'),
|
ipAddress: orDefault(nic?.ip ?? null, '192.168.0.10'),
|
||||||
subnetMask: orDefault(nic?.mask ?? null, '255.255.255.0'),
|
subnetMask: orDefault(nic?.mask ?? null, '255.255.255.0'),
|
||||||
defaultGateway: nic?.gateway?.trim() ?? '',
|
defaultGateway: trimmedOrEmpty(nic?.gateway),
|
||||||
dnsServer: nic?.dns?.trim() ?? '',
|
dnsServer: trimmedOrEmpty(nic?.dns),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -272,14 +278,14 @@ function registerConfigurations(
|
|||||||
// Swift's model has no router gateway at all; the core reads it from the
|
// Swift's model has no router gateway at all; the core reads it from the
|
||||||
// router's device configuration, which is set only when there is one.
|
// router's device configuration, which is set only when there is one.
|
||||||
const first = source.interfaces[0];
|
const first = source.interfaces[0];
|
||||||
const routerGateway = first?.gateway?.trim() ?? '';
|
const routerGateway = trimmedOrEmpty(first?.gateway);
|
||||||
if (node.kind === 'router' && first !== undefined && routerGateway !== '') {
|
if (node.kind === 'router' && first !== undefined && routerGateway !== '') {
|
||||||
const firstConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id));
|
const firstConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id));
|
||||||
deviceConfigurations.set(node.id, {
|
deviceConfigurations.set(node.id, {
|
||||||
ipAddress: firstConfiguration?.ipAddress ?? '',
|
ipAddress: firstConfiguration?.ipAddress ?? '',
|
||||||
subnetMask: firstConfiguration?.subnetMask ?? '',
|
subnetMask: firstConfiguration?.subnetMask ?? '',
|
||||||
defaultGateway: routerGateway,
|
defaultGateway: routerGateway,
|
||||||
dnsServer: first.dns?.trim() ?? '',
|
dnsServer: trimmedOrEmpty(first.dns),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const wan = source.interfaces[0];
|
const wan = source.interfaces[0];
|
||||||
@@ -288,8 +294,8 @@ function registerConfigurations(
|
|||||||
deviceConfigurations.set(node.id, {
|
deviceConfigurations.set(node.id, {
|
||||||
ipAddress: wanConfiguration?.ipAddress ?? '',
|
ipAddress: wanConfiguration?.ipAddress ?? '',
|
||||||
subnetMask: wanConfiguration?.subnetMask ?? '',
|
subnetMask: wanConfiguration?.subnetMask ?? '',
|
||||||
defaultGateway: wan.gateway?.trim() ?? '',
|
defaultGateway: trimmedOrEmpty(wan.gateway),
|
||||||
dnsServer: wan.dns?.trim() ?? '',
|
dnsServer: trimmedOrEmpty(wan.dns),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +316,7 @@ type SystemConfigurations = Pick<
|
|||||||
/** A row of text cells, trimmed, or undefined unless it has exactly four. */
|
/** A row of text cells, trimmed, or undefined unless it has exactly four. */
|
||||||
function fourCells(row: (string | null)[]): [string, string, string, string] | undefined {
|
function fourCells(row: (string | null)[]): [string, string, string, string] | undefined {
|
||||||
if (row.length !== 4 || row.some((cell) => cell === null)) return undefined;
|
if (row.length !== 4 || row.some((cell) => cell === null)) return undefined;
|
||||||
return row.map((cell) => cell!.trim()) as [string, string, string, string];
|
return row.map((cell) => swiftTrim(cell!)) as [string, string, string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -334,10 +340,10 @@ function registerServiceConfigurations(
|
|||||||
if (server !== undefined) {
|
if (server !== undefined) {
|
||||||
const configuration = dhcpServerConfiguration({
|
const configuration = dhcpServerConfiguration({
|
||||||
isActive: server.isActive,
|
isActive: server.isActive,
|
||||||
lowerBoundIPAddress: server.lowerBound?.trim() ?? '',
|
lowerBoundIPAddress: trimmedOrEmpty(server.lowerBound),
|
||||||
upperBoundIPAddress: server.upperBound?.trim() ?? '',
|
upperBoundIPAddress: trimmedOrEmpty(server.upperBound),
|
||||||
gatewayIPAddress: server.gateway?.trim() ?? '',
|
gatewayIPAddress: trimmedOrEmpty(server.gateway),
|
||||||
dnsServerIPAddress: server.dnsServer?.trim() ?? '',
|
dnsServerIPAddress: trimmedOrEmpty(server.dnsServer),
|
||||||
useOwnSettings: server.useOwnSettings,
|
useOwnSettings: server.useOwnSettings,
|
||||||
staticAssignments: server.staticAssignedAddresses.map((entry) => ({
|
staticAssignments: server.staticAssignedAddresses.map((entry) => ({
|
||||||
id: newId(),
|
id: newId(),
|
||||||
@@ -393,7 +399,7 @@ function registerSystemConfigurations(
|
|||||||
warnings: string[],
|
warnings: string[],
|
||||||
): void {
|
): void {
|
||||||
const where = `node '${node.displayName}'`;
|
const where = `node '${node.displayName}'`;
|
||||||
const ssid = source.system.ssid?.trim() ?? '';
|
const ssid = trimmedOrEmpty(source.system.ssid);
|
||||||
if (node.kind === 'networkSwitch') {
|
if (node.kind === 'networkSwitch') {
|
||||||
// Without an SSID in the file, Java rolls one; Swift derives it from the node id.
|
// Without an SSID in the file, Java rolls one; Swift derives it from the node id.
|
||||||
system.switchConfigurations.set(node.id, {
|
system.switchConfigurations.set(node.id, {
|
||||||
@@ -435,10 +441,10 @@ function firewallConfigurationOf(firewall: FlsFirewall, where: string, warnings:
|
|||||||
const protocol = PROTOCOLS[rule.protocol];
|
const protocol = PROTOCOLS[rule.protocol];
|
||||||
if (protocol === undefined) warnings.push(`${where}: firewall protocol ${rule.protocol} unknown, used TCP`);
|
if (protocol === undefined) warnings.push(`${where}: firewall protocol ${rule.protocol} unknown, used TCP`);
|
||||||
return {
|
return {
|
||||||
sourceIPAddress: rule.srcIP?.trim() ?? '',
|
sourceIPAddress: trimmedOrEmpty(rule.srcIP),
|
||||||
sourceSubnetMask: rule.srcMask?.trim() ?? '',
|
sourceSubnetMask: trimmedOrEmpty(rule.srcMask),
|
||||||
destinationIPAddress: rule.destIP?.trim() ?? '',
|
destinationIPAddress: trimmedOrEmpty(rule.destIP),
|
||||||
destinationSubnetMask: rule.destMask?.trim() ?? '',
|
destinationSubnetMask: trimmedOrEmpty(rule.destMask),
|
||||||
port: rule.port,
|
port: rule.port,
|
||||||
protocol: protocol ?? 'tcp',
|
protocol: protocol ?? 'tcp',
|
||||||
action: action(rule.action, 'action'),
|
action: action(rule.action, 'action'),
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ und unter `state` Swifts ganzes importiertes Modell ohne die zufälligen UUIDs
|
|||||||
ihren abgeleiteten IDs). Daran misst `packages/fls/test/importProject.test.ts`
|
ihren abgeleiteten IDs). Daran misst `packages/fls/test/importProject.test.ts`
|
||||||
den Umbau — auf den 13 Dateien, die Swift vollständig liest.
|
den Umbau — auf den 13 Dateien, die Swift vollständig liest.
|
||||||
|
|
||||||
|
## Dateisystem und Strings
|
||||||
|
|
||||||
|
`--vfs` spielt die Fälle aus `packages/core/test/vfs.cases.json` im
|
||||||
|
virtuellen Dateisystem ab (jedes Ergebnis, jeder Fehler samt Werten, der
|
||||||
|
Endzustand mit SHA-256 der Binärdaten). `--strings` misst, was Swift aus
|
||||||
|
Strings macht, wo JavaScript abweicht: Trimmen, Kleinschreibung, Gleichheit,
|
||||||
|
Sortierung, Base64 — und die ganzen Mengen `whitespacesAndNewlines` und
|
||||||
|
`controlCharacters` über alle Unicode-Skalare. Grenze: Das ist Foundation
|
||||||
|
unter Linux; ihre Steuerzeichen-Tabelle hat in Ebene 14 einen Fehler, den
|
||||||
|
der Port nicht übernimmt.
|
||||||
|
|
||||||
## Wie es gebaut wird
|
## Wie es gebaut wird
|
||||||
|
|
||||||
`fetch-upstream.sh` holt `filius-ipad` am festen Stand `3b5ded0` und kopiert
|
`fetch-upstream.sh` holt `filius-ipad` am festen Stand `3b5ded0` und kopiert
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// Usage: Oracle <directory> — reads <name>.scenario.json, writes <name>.swift.json
|
// Usage: Oracle <directory> — reads <name>.scenario.json, writes <name>.swift.json
|
||||||
|
|
||||||
@testable import FiliusCore
|
@testable import FiliusCore
|
||||||
|
import CryptoKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// MARK: - Scenario format (shared with packages/core/test/oracle/scenario.ts)
|
// MARK: - Scenario format (shared with packages/core/test/oracle/scenario.ts)
|
||||||
@@ -405,6 +406,161 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON
|
|||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Virtual file system
|
||||||
|
|
||||||
|
/// One step of a file system case (packages/core/test/vfs.cases.json).
|
||||||
|
struct FileSystemStep: Decodable {
|
||||||
|
let op: String
|
||||||
|
let path: String?
|
||||||
|
let to: String?
|
||||||
|
let name: String?
|
||||||
|
let text: String?
|
||||||
|
let size: Int?
|
||||||
|
let byte: UInt8?
|
||||||
|
let mediaType: String?
|
||||||
|
let recursive: Bool?
|
||||||
|
let overwrite: Bool?
|
||||||
|
let entries: [FileSystemCaseEntry]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FileSystemCaseEntry: Decodable { let path: String; let kind: String; let text: String? }
|
||||||
|
struct FileSystemCase: Decodable { let name: String; let steps: [FileSystemStep] }
|
||||||
|
|
||||||
|
func describeFileSystemError(_ error: Error) -> JSON {
|
||||||
|
guard let error = error as? TopologyVirtualFileSystemError else {
|
||||||
|
return ["error": "other", "values": [String(describing: error)]]
|
||||||
|
}
|
||||||
|
func result(_ code: String, _ values: [Any] = []) -> JSON { ["error": code, "values": values] }
|
||||||
|
switch error {
|
||||||
|
case let .pathMustBeAbsolute(path): return result("pathMustBeAbsolute", [path])
|
||||||
|
case let .pathEscapesRoot(path): return result("pathEscapesRoot", [path])
|
||||||
|
case let .invalidPathComponent(component): return result("invalidPathComponent", [component])
|
||||||
|
case let .itemAlreadyExists(path): return result("itemAlreadyExists", [path])
|
||||||
|
case let .caseInsensitiveSiblingCollision(existing, attempted):
|
||||||
|
return result("caseInsensitiveSiblingCollision", [existing, attempted])
|
||||||
|
case let .itemNotFound(path): return result("itemNotFound", [path])
|
||||||
|
case let .parentDirectoryNotFound(path): return result("parentDirectoryNotFound", [path])
|
||||||
|
case let .expectedDirectory(path): return result("expectedDirectory", [path])
|
||||||
|
case let .expectedFile(path): return result("expectedFile", [path])
|
||||||
|
case let .expectedTextFile(path): return result("expectedTextFile", [path])
|
||||||
|
case let .expectedImageFile(path): return result("expectedImageFile", [path])
|
||||||
|
case let .directoryNotEmpty(path): return result("directoryNotEmpty", [path])
|
||||||
|
case .cannotMutateRoot: return result("cannotMutateRoot")
|
||||||
|
case let .cannotMoveDirectoryIntoItself(source, destination):
|
||||||
|
return result("cannotMoveDirectoryIntoItself", [source, destination])
|
||||||
|
case let .fileSizeQuotaExceeded(path, actual, limit): return result("fileSizeQuotaExceeded", [path, actual, limit])
|
||||||
|
case let .deviceSizeQuotaExceeded(actual, limit): return result("deviceSizeQuotaExceeded", [actual, limit])
|
||||||
|
case let .deviceEntryQuotaExceeded(actual, limit): return result("deviceEntryQuotaExceeded", [actual, limit])
|
||||||
|
case let .totalSizeQuotaExceeded(actual, limit): return result("totalSizeQuotaExceeded", [actual, limit])
|
||||||
|
case let .totalEntryQuotaExceeded(actual, limit): return result("totalEntryQuotaExceeded", [actual, limit])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func describeFileEntries(_ entries: [TopologyVirtualFileEntry]) -> [JSON] {
|
||||||
|
entries.map { entry in
|
||||||
|
var json: JSON = ["path": entry.path]
|
||||||
|
switch entry.content {
|
||||||
|
case .directory: json["kind"] = "directory"
|
||||||
|
case let .text(value): json["kind"] = "text"; json["text"] = value
|
||||||
|
case let .binary(data, mediaType):
|
||||||
|
json["kind"] = "binary"; json["bytes"] = data.count; json["mediaType"] = mediaType ?? NSNull()
|
||||||
|
json["sha256"] = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
||||||
|
case let .image(data, mediaType):
|
||||||
|
json["kind"] = "image"; json["bytes"] = data.count; json["mediaType"] = mediaType
|
||||||
|
json["sha256"] = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runFileSystemCases(_ cases: [FileSystemCase]) -> [JSON] {
|
||||||
|
cases.map { item in
|
||||||
|
var fileSystem = TopologyVirtualFileSystem()
|
||||||
|
var results: [JSON] = []
|
||||||
|
for step in item.steps {
|
||||||
|
do {
|
||||||
|
let path = step.path ?? "/"
|
||||||
|
switch step.op {
|
||||||
|
case "createDirectory": try fileSystem.createDirectory(at: path, recursive: step.recursive ?? false)
|
||||||
|
case "writeText": try fileSystem.writeTextFile(at: path, text: step.text ?? "", overwrite: step.overwrite ?? true)
|
||||||
|
case "writeBinary":
|
||||||
|
try fileSystem.writeBinaryFile(
|
||||||
|
at: path, data: Data(repeating: step.byte ?? 0, count: step.size ?? 0),
|
||||||
|
mediaType: step.mediaType, overwrite: step.overwrite ?? true)
|
||||||
|
case "writeImage":
|
||||||
|
try fileSystem.writeImageFile(
|
||||||
|
at: path, data: Data(repeating: step.byte ?? 0, count: step.size ?? 0),
|
||||||
|
mediaType: step.mediaType ?? "image/png", overwrite: step.overwrite ?? true)
|
||||||
|
case "copy": try fileSystem.copyItem(at: path, to: step.to ?? "/", overwrite: step.overwrite ?? false)
|
||||||
|
case "move": try fileSystem.moveItem(at: path, to: step.to ?? "/", overwrite: step.overwrite ?? false)
|
||||||
|
case "rename": results.append(["ok": try fileSystem.renameItem(at: path, to: step.name ?? "")]); continue
|
||||||
|
case "delete": try fileSystem.deleteItem(at: path, recursive: step.recursive ?? false)
|
||||||
|
case "entry": results.append(["ok": describeFileEntries([try fileSystem.entry(at: path)])[0]]); continue
|
||||||
|
case "entries": results.append(["ok": describeFileEntries(try fileSystem.entries(in: path))]); continue
|
||||||
|
case "text": results.append(["ok": try fileSystem.textFile(at: path)]); continue
|
||||||
|
case "contains": results.append(["ok": fileSystem.contains(path)]); continue
|
||||||
|
case "normalize": results.append(["ok": try TopologyVirtualFileSystem.normalizedAbsolutePath(path)]); continue
|
||||||
|
case "default": fileSystem = .defaultForDevice()
|
||||||
|
case "fromEntries":
|
||||||
|
fileSystem = try TopologyVirtualFileSystem(entries: (step.entries ?? []).map { entry in
|
||||||
|
TopologyVirtualFileEntry(
|
||||||
|
path: entry.path,
|
||||||
|
content: entry.kind == "directory" ? .directory : .text(entry.text ?? ""))
|
||||||
|
})
|
||||||
|
default: results.append(["error": "unknown step \(step.op)"]); continue
|
||||||
|
}
|
||||||
|
results.append(["ok": true])
|
||||||
|
} catch {
|
||||||
|
results.append(describeFileSystemError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ["name": item.name, "results": results, "entries": describeFileEntries(fileSystem.allEntries())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String semantics
|
||||||
|
|
||||||
|
struct StringCases: Decodable {
|
||||||
|
let trim: [String]
|
||||||
|
let lowercased: [String]
|
||||||
|
let equal: [[String]]
|
||||||
|
let sort: [[String]]
|
||||||
|
let base64: [String]
|
||||||
|
let controlCharacters: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What Swift makes of strings where JavaScript differs (packages/core/test/strings.cases.json).
|
||||||
|
func measureStrings(_ cases: StringCases) -> JSON {
|
||||||
|
[
|
||||||
|
"trim": cases.trim.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) },
|
||||||
|
"lowercased": cases.lowercased.map { $0.lowercased() },
|
||||||
|
"equal": cases.equal.map { $0[0] == $0[1] },
|
||||||
|
"sort": cases.sort.map { $0.sorted() },
|
||||||
|
"base64": cases.base64.map { text -> Any in
|
||||||
|
Data(base64Encoded: text, options: .ignoreUnknownCharacters).map { Array($0) } ?? NSNull()
|
||||||
|
},
|
||||||
|
"controlCharacters": cases.controlCharacters.map { text in
|
||||||
|
text.unicodeScalars.contains(where: { CharacterSet.controlCharacters.contains($0) })
|
||||||
|
},
|
||||||
|
// The whole sets, scalar by scalar, as ranges [first, last].
|
||||||
|
"whitespacesAndNewlinesSet": scalarRanges(in: .whitespacesAndNewlines),
|
||||||
|
"controlCharactersSet": scalarRanges(in: .controlCharacters),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func scalarRanges(in set: CharacterSet) -> [[Int]] {
|
||||||
|
var ranges: [[Int]] = []
|
||||||
|
for value in 0...0x10FFFF {
|
||||||
|
guard let scalar = Unicode.Scalar(value), set.contains(scalar) else { continue }
|
||||||
|
if let last = ranges.last, last[1] == value - 1 {
|
||||||
|
ranges[ranges.count - 1][1] = value
|
||||||
|
} else {
|
||||||
|
ranges.append([value, value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Archive path folding
|
// MARK: - Archive path folding
|
||||||
|
|
||||||
struct PathCase: Decodable { let name: String; let first: String; let second: String }
|
struct PathCase: Decodable { let name: String; let first: String; let second: String }
|
||||||
@@ -446,6 +602,23 @@ if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--path-folding
|
|||||||
exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--vfs" {
|
||||||
|
let cases = try JSONDecoder().decode([FileSystemCase].self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2])))
|
||||||
|
let output = runFileSystemCases(cases)
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])
|
||||||
|
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
|
||||||
|
print("vfs: \(output.count) cases")
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--strings" {
|
||||||
|
let cases = try JSONDecoder().decode(StringCases.self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2])))
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: measureStrings(cases), options: [.prettyPrinted, .sortedKeys])
|
||||||
|
try (data + Data("\n".utf8)).write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
|
||||||
|
print("strings: measured")
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--corpus" {
|
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--corpus" {
|
||||||
let summary = try readCorpus(root: URL(fileURLWithPath: CommandLine.arguments[2]))
|
let summary = try readCorpus(root: URL(fileURLWithPath: CommandLine.arguments[2]))
|
||||||
let data = try JSONSerialization.data(withJSONObject: summary, options: [.prettyPrinted, .sortedKeys])
|
let data = try JSONSerialization.data(withJSONObject: summary, options: [.prettyPrinted, .sortedKeys])
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ docker run --rm -v "$root:/work" -w /work/tools/swift-oracle swift:6.1-jammy sh
|
|||||||
swift build -q
|
swift build -q
|
||||||
.build/debug/Oracle /work/packages/core/test/scenarios
|
.build/debug/Oracle /work/packages/core/test/scenarios
|
||||||
.build/debug/Oracle --corpus /work/corpus /work/corpus/corpus.swift.json
|
.build/debug/Oracle --corpus /work/corpus /work/corpus/corpus.swift.json
|
||||||
|
.build/debug/Oracle --vfs /work/packages/core/test/vfs.cases.json /work/packages/core/test/vfs.swift.json
|
||||||
|
.build/debug/Oracle --strings /work/packages/core/test/strings.cases.json /work/packages/core/test/strings.swift.json
|
||||||
.build/debug/Oracle --path-folding /work/packages/fls/test/path-folding.cases.json /work/packages/fls/test/path-folding.swift.json'
|
.build/debug/Oracle --path-folding /work/packages/fls/test/path-folding.cases.json /work/packages/fls/test/path-folding.swift.json'
|
||||||
|
|||||||
Reference in New Issue
Block a user