Files
Sidhartha Mani 0885cdd6a9 Native Builder: DiffKey and Differ Procol (#482)
This PR introduces the `Differ` with methods:

```swift
// Differ protocol
func diff(base: Snapshot?, target: Snapshot) async throws -> Descriptor
func apply(descriptor: Descriptor, to base: Snapshot?) async throws -> Snapshot
```

It also introduces `DiffKey`, which is a MerkeTree based key for fast
diff computations between two dirs
2025-08-12 11:23:55 -07:00

533 lines
20 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerBuildSnapshotter
@Suite struct BinaryPathTests {
// MARK: - Initialization Tests
@Test func InitFromString() {
let path = BinaryPath(string: "/usr/local/bin")
#expect(path.stringValue == "/usr/local/bin")
#expect(path.isValidUTF8)
#expect(path.rawBytes == Data("/usr/local/bin".utf8))
}
@Test func InitFromBytes() {
let bytes = Data([0x2F, 0x75, 0x73, 0x72]) // "/usr" in bytes
let path = BinaryPath(bytes: bytes)
#expect(path.stringValue == "/usr")
#expect(path.rawBytes == bytes)
}
@Test func InitFromNonUTF8Bytes() {
// Invalid UTF-8 sequence
let bytes = Data([0x2F, 0xFF, 0xFE, 0x00])
let path = BinaryPath(bytes: bytes)
#expect(path.stringValue == nil)
#expect(!path.isValidUTF8)
#expect(path.rawBytes == bytes)
// requireString should provide a fallback with replacement character
let requiredString = path.requireString
#expect(requiredString.contains("\u{FFFD}"))
}
@Test func InitFromCString() {
let cString = "/tmp/test".cString(using: .utf8)!
cString.withUnsafeBufferPointer { buffer in
let path = BinaryPath(cString: buffer.baseAddress!)
#expect(path.stringValue == "/tmp/test")
}
}
@Test func InitFromURL() {
let url = URL(fileURLWithPath: "/var/log/system.log")
let path = BinaryPath(url: url)
#expect(path.stringValue == "/var/log/system.log")
#expect(path.isValidUTF8)
}
@Test func InitFromStringLiteral() {
let path: BinaryPath = "/home/user/documents"
#expect(path.stringValue == "/home/user/documents")
}
// MARK: - Empty Path Tests
@Test func emptyPath() {
let empty = BinaryPath(bytes: Data())
#expect(empty.isEmpty)
#expect(empty.stringValue == "")
#expect(empty.components == [])
}
@Test func emptyStringPath() {
let empty = BinaryPath(string: "")
#expect(empty.isEmpty)
#expect(empty.stringValue == "")
}
// MARK: - Path Component Tests
@Test func LastPathComponent() {
let path = BinaryPath(string: "/usr/local/bin")
#expect(path.lastPathComponent.stringValue == "bin")
let rootPath = BinaryPath(string: "/")
#expect(rootPath.lastPathComponent.stringValue == "")
let noSlash = BinaryPath(string: "filename")
#expect(noSlash.lastPathComponent.stringValue == "filename")
}
@Test func DeletingLastPathComponent() {
let path = BinaryPath(string: "/usr/local/bin")
let parent = path.deletingLastPathComponent()
#expect(parent.stringValue == "/usr/local")
let rootPath = BinaryPath(string: "/usr")
let rootParent = rootPath.deletingLastPathComponent()
#expect(rootParent.stringValue == "/")
let justRoot = BinaryPath(string: "/")
let justRootParent = justRoot.deletingLastPathComponent()
#expect(justRootParent.stringValue == "/")
let noSlash = BinaryPath(string: "filename")
let noSlashParent = noSlash.deletingLastPathComponent()
#expect(noSlashParent.isEmpty)
}
@Test func AppendingPathComponent() {
let base = BinaryPath(string: "/usr/local")
let appended = base.appending(BinaryPath(string: "bin"))
#expect(appended.stringValue == "/usr/local/bin")
let baseWithSlash = BinaryPath(string: "/usr/local/")
let appendedToSlash = baseWithSlash.appending(BinaryPath(string: "bin"))
#expect(appendedToSlash.stringValue == "/usr/local/bin")
let empty = BinaryPath(string: "")
let appendedToEmpty = empty.appending(BinaryPath(string: "test"))
#expect(appendedToEmpty.stringValue == "test")
let appendEmpty = base.appending(BinaryPath(string: ""))
#expect(appendEmpty.stringValue == "/usr/local")
}
@Test func PathComponents() {
let path = BinaryPath(string: "/usr/local/bin")
let components = path.components
#expect(components.count == 3)
#expect(components[0].stringValue == "usr")
#expect(components[1].stringValue == "local")
#expect(components[2].stringValue == "bin")
let multiSlash = BinaryPath(string: "//usr//local//")
let multiComponents = multiSlash.components
#expect(multiComponents.count == 2)
#expect(multiComponents[0].stringValue == "usr")
#expect(multiComponents[1].stringValue == "local")
let noSlash = BinaryPath(string: "filename")
let noSlashComponents = noSlash.components
#expect(noSlashComponents.count == 1)
#expect(noSlashComponents[0].stringValue == "filename")
}
// MARK: - Relative Path Tests
@Test func RelativePath() {
let base = BinaryPath(string: "/usr/local")
let full = BinaryPath(string: "/usr/local/bin/test")
let relative = full.relativePath(from: base)
#expect(relative?.stringValue == "bin/test")
let sameBase = BinaryPath(string: "/usr/local")
let sameFull = BinaryPath(string: "/usr/local")
let sameRelative = sameFull.relativePath(from: sameBase)
#expect(sameRelative?.stringValue == "")
let differentBase = BinaryPath(string: "/var")
let differentFull = BinaryPath(string: "/usr/local")
let differentRelative = differentFull.relativePath(from: differentBase)
#expect(differentRelative == nil)
}
@Test func HasPrefix() {
let path = BinaryPath(string: "/usr/local/bin")
#expect(path.hasPrefix(BinaryPath(string: "/usr")))
#expect(path.hasPrefix(BinaryPath(string: "/usr/local")))
#expect(!path.hasPrefix(BinaryPath(string: "/var")))
#expect(path.hasPrefix(path))
}
@Test func HasSuffix() {
let path = BinaryPath(string: "/usr/local/bin")
#expect(path.hasSuffix(BinaryPath(string: "bin")))
#expect(path.hasSuffix(BinaryPath(string: "local/bin")))
#expect(!path.hasSuffix(BinaryPath(string: "usr")))
#expect(path.hasSuffix(path))
}
// MARK: - Comparison Tests
@Test func Equality() {
let path1 = BinaryPath(string: "/usr/local")
let path2 = BinaryPath(string: "/usr/local")
let path3 = BinaryPath(string: "/usr/bin")
#expect(path1 == path2)
#expect(path1 != path3)
// Test with byte initialization
let bytePath1 = BinaryPath(bytes: Data("/usr/local".utf8))
#expect(path1 == bytePath1)
}
@Test func Comparison() {
let path1 = BinaryPath(string: "/usr/bin")
let path2 = BinaryPath(string: "/usr/local")
#expect(path1 < path2)
#expect(!(path2 < path1))
#expect(!(path1 < path1))
}
@Test func Hashable() {
let path1 = BinaryPath(string: "/usr/local")
let path2 = BinaryPath(string: "/usr/local")
let path3 = BinaryPath(string: "/usr/bin")
var set = Set<BinaryPath>()
set.insert(path1)
set.insert(path2)
set.insert(path3)
#expect(set.count == 2) // path1 and path2 are equal
#expect(set.contains(path1))
#expect(set.contains(path3))
}
// MARK: - Interop Tests
@Test func WithCString() {
let path = BinaryPath(string: "/usr/local/bin")
path.withCString { cString in
let length = strlen(cString)
#expect(length == 14)
#expect(String(cString: cString) == "/usr/local/bin")
}
// Test with non-UTF8 path
let nonUTF8 = BinaryPath(bytes: Data([0x2F, 0xFF, 0xFE]))
nonUTF8.withCString { cString in
// Should still work as C string (bytes with null termination)
let length = strlen(cString)
#expect(length == 3)
}
}
@Test func URLConversion() {
let path = BinaryPath(string: "/usr/local/bin")
let url = path.url
#expect(url != nil)
#expect(url?.path == "/usr/local/bin")
// Non-UTF8 path should not convert to URL
let nonUTF8 = BinaryPath(bytes: Data([0x2F, 0xFF, 0xFE]))
#expect(nonUTF8.url == nil)
}
// MARK: - Codable Tests
@Test func CodableWithValidUTF8() throws {
let original = BinaryPath(string: "/usr/local/bin")
let encoder = JSONEncoder()
let data = try encoder.encode(original)
let decoder = JSONDecoder()
let decoded = try decoder.decode(BinaryPath.self, from: data)
#expect(original == decoded)
#expect(decoded.stringValue == "/usr/local/bin")
}
@Test func CodableWithNonUTF8() throws {
let nonUTF8Bytes = Data([0x2F, 0xFF, 0xFE, 0x00])
let original = BinaryPath(bytes: nonUTF8Bytes)
let encoder = JSONEncoder()
let data = try encoder.encode(original)
// When non-UTF8 is encoded, it becomes base64
// JSONDecoder will decode the base64 string as a String first
// This means the decoded path will contain the base64 string as UTF-8 bytes
// not the original non-UTF8 bytes
let decoder = JSONDecoder()
let decoded = try decoder.decode(BinaryPath.self, from: data)
// The base64 representation becomes the new path content
// This is a limitation of the current Codable implementation
// For true binary preservation, a different encoding strategy would be needed
#expect(decoded.stringValue != nil) // It's now a valid UTF-8 string (the base64)
// Alternative test: ensure original path with valid UTF-8 round-trips correctly
let utf8Path = BinaryPath(string: "/usr/local/bin")
let utf8Data = try encoder.encode(utf8Path)
let utf8Decoded = try decoder.decode(BinaryPath.self, from: utf8Data)
#expect(utf8Path == utf8Decoded)
}
// MARK: - Description Tests
@Test func Description() {
let utf8Path = BinaryPath(string: "/usr/local")
#expect(utf8Path.description == "/usr/local")
let nonUTF8 = BinaryPath(bytes: Data([0x2F, 0xFF, 0xFE]))
#expect(nonUTF8.description.contains("non-UTF8"))
#expect(nonUTF8.description.contains("3 bytes"))
}
// MARK: - Edge Cases
@Test func RootPath() {
let root = BinaryPath(string: "/")
#expect(root.stringValue == "/")
#expect(root.lastPathComponent.stringValue == "")
#expect(root.deletingLastPathComponent().stringValue == "/")
#expect(root.components == [])
}
@Test func PathWithTrailingSlash() {
let path = BinaryPath(string: "/usr/local/")
#expect(path.lastPathComponent.stringValue == "")
#expect(path.components.count == 2)
}
@Test func PathWithMultipleSlashes() {
let path = BinaryPath(string: "//usr///local//bin//")
let components = path.components
#expect(components.count == 3)
#expect(components[0].stringValue == "usr")
#expect(components[1].stringValue == "local")
#expect(components[2].stringValue == "bin")
}
@Test func LongPath() {
let longComponent = String(repeating: "a", count: 255)
let longPath = "/usr/local/\(longComponent)/bin"
let path = BinaryPath(string: longPath)
#expect(path.stringValue == longPath)
#expect(path.components.count == 4)
}
@Test func AppendingWithLeadingSlash() {
let base = BinaryPath(string: "/usr")
let component = BinaryPath(string: "/local")
let result = base.appending(component)
#expect(result.stringValue == "/usr/local")
}
@Test func RequireStringWithEmptyPath() {
let empty = BinaryPath(bytes: Data())
// Empty Data is valid UTF-8 (empty string), so requireString returns it directly
#expect(empty.stringValue == "")
#expect(empty.requireString == "")
}
@Test func RequireStringWithComplexNonUTF8() {
// Mix of valid and invalid UTF-8
let bytes = Data([
0x2F, // /
0x75, 0x73, 0x72, // usr
0x2F, // /
0xFF, 0xFE, // Invalid UTF-8
0x2F, // /
0x62, 0x69, 0x6E, // bin
])
let path = BinaryPath(bytes: bytes)
let required = path.requireString
// The algorithm processes bytes sequentially and may not preserve all text
// when invalid UTF-8 is encountered in the middle
#expect(required.contains("\u{FFFD}"))
// Additional test with simpler invalid UTF-8
let simpleInvalid = BinaryPath(bytes: Data([0xFF, 0xFE]))
let simpleRequired = simpleInvalid.requireString
#expect(simpleRequired.contains("\u{FFFD}"))
}
// MARK: - Special Character Path Tests
@Test func SpecialCharacterPaths() {
// Test paths with special characters, quotes, and Unicode
let specialPaths = [
"<F!chïer> (@vec) {càraçt#èrë} $épêcial",
"Char ;059090 to quote",
"DIR", // Contains replacement character
"Fichier @ <root>",
"Fichier avec non asci char Évelyne Mère.txt",
"Répertoire (@vec) {càraçt#èrë} $épêcial",
"Répertoire Existant",
"test\\test", // Backslash in filename
"이루마 YIRUMA - River Flows in You.mp3", // Korean characters
]
for pathString in specialPaths {
let path = BinaryPath(string: pathString)
#expect(path.stringValue == pathString, "Path should preserve special characters: \(pathString)")
#expect(path.isValidUTF8, "Path should be valid UTF-8: \(pathString)")
// Test round-trip through bytes
let bytes = path.rawBytes
let reconstructed = BinaryPath(bytes: bytes)
#expect(reconstructed.stringValue == pathString, "Round-trip should preserve path: \(pathString)")
}
}
@Test func SpecialCharacterPathComponents() {
// Test path component operations with special characters
let basePath = BinaryPath(string: "/tmp")
let specialComponent = BinaryPath(string: "Répertoire (@vec) {càraçt#èrë} $épêcial")
let fullPath = basePath.appending(specialComponent)
#expect(fullPath.stringValue == "/tmp/Répertoire (@vec) {càraçt#èrë} $épêcial")
#expect(fullPath.lastPathComponent.stringValue == "Répertoire (@vec) {càraçt#èrë} $épêcial")
#expect(fullPath.deletingLastPathComponent().stringValue == "/tmp")
}
@Test func UnicodePathOperations() {
// Test with Korean characters
let koreanPath = BinaryPath(string: "/music/이루마 YIRUMA - River Flows in You.mp3")
#expect(koreanPath.lastPathComponent.stringValue == "이루마 YIRUMA - River Flows in You.mp3")
let components = koreanPath.components
#expect(components.count == 2)
#expect(components[0].stringValue == "music")
#expect(components[1].stringValue == "이루마 YIRUMA - River Flows in You.mp3")
}
@Test func PathsWithQuotesAndSpecialChars() {
// Test paths that would need shell escaping
let quotePath = BinaryPath(string: "Char ;090 to quote")
#expect(quotePath.stringValue == "Char ;090 to quote")
let atSymbolPath = BinaryPath(string: "Fichier @ <root>")
#expect(atSymbolPath.stringValue == "Fichier @ <root>")
let dollarPath = BinaryPath(string: "file$with$dollars")
#expect(dollarPath.stringValue == "file$with$dollars")
}
@Test func BackslashInFilename() {
// Test backslash in filename (not as path separator)
let backslashPath = BinaryPath(string: "test\\test")
#expect(backslashPath.stringValue == "test\\test")
#expect(backslashPath.lastPathComponent.stringValue == "test\\test")
// When used as a component in a path
let fullPath = BinaryPath(string: "/tmp/test\\test/some data")
let components = fullPath.components
#expect(components.count == 3)
#expect(components[0].stringValue == "tmp")
#expect(components[1].stringValue == "test\\test")
#expect(components[2].stringValue == "some data")
}
@Test func PathsWithReplacementCharacter() {
// Test path containing Unicode replacement character ()
let replacementPath = BinaryPath(string: "DIR")
#expect(replacementPath.stringValue == "DIR")
#expect(replacementPath.isValidUTF8)
// Test that it can be used in path operations
let basePath = BinaryPath(string: "/tmp")
let fullPath = basePath.appending(replacementPath)
#expect(fullPath.stringValue == "/tmp/DIR")
}
@Test func AccentedCharacterPaths() {
// Test various accented characters
let accentedPaths = [
"Foldèr with éncodïng",
"Évelyne Mère.txt",
"càraçt#èrë",
"épêcial",
]
for pathString in accentedPaths {
let path = BinaryPath(string: pathString)
#expect(path.stringValue == pathString)
#expect(path.isValidUTF8)
// Test in a full path context
let fullPath = BinaryPath(string: "/home/user/\(pathString)")
#expect(fullPath.lastPathComponent.stringValue == pathString)
}
}
@Test func SpecialCharacterPathComparison() {
// Test that paths with special characters can be compared and used in sets
let path1 = BinaryPath(string: "Répertoire (@vec) {càraçt#èrë} $épêcial")
let path2 = BinaryPath(string: "Répertoire (@vec) {càraçt#èrë} $épêcial")
let path3 = BinaryPath(string: "이루마 YIRUMA - River Flows in You.mp3")
#expect(path1 == path2)
#expect(path1 != path3)
var pathSet = Set<BinaryPath>()
pathSet.insert(path1)
pathSet.insert(path2)
pathSet.insert(path3)
#expect(pathSet.count == 2) // path1 and path2 are equal
#expect(pathSet.contains(path1))
#expect(pathSet.contains(path3))
}
@Test func SpecialCharacterPathCoding() throws {
// Test that special character paths can be encoded/decoded
let specialPaths = [
"Répertoire (@vec) {càraçt#èrë} $épêcial",
"이루마 YIRUMA - River Flows in You.mp3",
"test\\test",
"DIR",
]
let encoder = JSONEncoder()
let decoder = JSONDecoder()
for pathString in specialPaths {
let original = BinaryPath(string: pathString)
let data = try encoder.encode(original)
let decoded = try decoder.decode(BinaryPath.self, from: data)
#expect(original == decoded, "Coding round-trip failed for: \(pathString)")
#expect(decoded.stringValue == pathString, "String value not preserved for: \(pathString)")
}
}
}