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