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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 21:23:45 +00:00
co-authored by Claude Opus 5.5
parent 51a83ea68d
commit a198d10b4a
15 changed files with 5247 additions and 18 deletions
@@ -6,6 +6,7 @@
// Usage: Oracle <directory> — reads <name>.scenario.json, writes <name>.swift.json
@testable import FiliusCore
import CryptoKit
import Foundation
// MARK: - Scenario format (shared with packages/core/test/oracle/scenario.ts)
@@ -405,6 +406,161 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> 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
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)
}
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" {
let summary = try readCorpus(root: URL(fileURLWithPath: CommandLine.arguments[2]))
let data = try JSONSerialization.data(withJSONObject: summary, options: [.prettyPrinted, .sortedKeys])