Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encode FilePath as a string or an array of code units #181

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion Sources/System/FilePath/FilePath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,41 @@ extension FilePath {
}

@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *)
extension FilePath: Hashable, Codable {}
extension FilePath: Hashable {}

@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *)
extension FilePath: Codable {
public init(from decoder: any Decoder) throws {
// Try to decode as a string first for the common case where the path can be
// losslessly represented as a UTF-8 string.
let singleValueContainer = try decoder.singleValueContainer()
if let string = try? singleValueContainer.decode(String.self) {
self.init(string)
return
}
// Try to decode as an array of UTF-8 code unit on Unix and UTF-16 code unit on Windows.
if let chars = try? singleValueContainer.decode([CInterop.PlatformChar].self) {
// Decode code units in a fault-tolerant way instead of fatalError on non-null-terminated input
// unlike the `init(platformString: [CInterop.PlatformChar])` initializer.
guard let _ = chars.firstIndex(of: 0) else {
throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Expected null-terminated array of \(CInterop.PlatformChar.self)")
}
self = chars.withUnsafeBufferPointer {
FilePath(platformString: $0.baseAddress!)
}
return
}

// Otherwise, data is corrupted.
throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Expected String or Array of \(CInterop.PlatformChar.self)")
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
if let string = String(validating: self) {
try container.encode(string)
} else {
try container.encode(_storage.nullTerminatedStorage)
}
}
}
92 changes: 92 additions & 0 deletions Tests/SystemTests/FilePathTests/FilePathCodableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2024 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

import XCTest

#if SYSTEM_PACKAGE
@testable import SystemPackage
#else
@testable import System
#endif

private struct EncodingTestCase: TestCase {
// The JSON source to decode
let source: String

// The expected FilePath value. nil if the decoding is expected to fail.
let expected: FilePath?

var file: StaticString
var line: UInt
}

extension EncodingTestCase {
static func valid(
_ source: String, _ expected: FilePath,
file: StaticString = #file, line: UInt = #line
) -> EncodingTestCase {
EncodingTestCase(source: source, expected: expected, file: file, line: line)
}

static func invalid(
_ source: String,
file: StaticString = #file, line: UInt = #line
) -> EncodingTestCase {
EncodingTestCase(source: source, expected: nil, file: file, line: line)
}
}

extension EncodingTestCase {
private struct Content: Codable {
var path: FilePath
}

func runAllTests() {
let data = Data(source.utf8)
do {
let decoded = try JSONDecoder().decode(Content.self, from: data)
guard let expected else {
self.fail("expected error, but successfully decoded: \(decoded.path)")
return
}
expectEqual(expected, decoded.path)

// Encoding should round-trip
let reencoded = try JSONEncoder().encode(decoded)
let redecoded = try JSONDecoder().decode(Content.self, from: reencoded)
expectEqual(expected, redecoded.path)
} catch {
if expected != nil {
self.fail("unexpected error: \(error)")
}
}
}
}

@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *)
final class FilePathCodableTest: XCTestCase {
func testEncoding() {
let testCases: [EncodingTestCase] = [
.valid(#"{ "path": "/" }"#, "/"),
.valid(#"{ "path": "\/" }"#, "/"),
.valid(#"{ "path": "\/foo" }"#, "/foo"),
.valid(#"{ "path": [47, 102, 111, 111, 0] }"#, "/foo"),
// Decode up to null terminator
.valid(#"{ "path": [47, 102, 111, 111, 0, 47] }"#, "/foo/"),
// Non-null-terminated input
.invalid(#"{ "path": [47, 102, 111, 111] }"#),
// Coding format used in older versions of swift-system, synthesized by the compiler
.invalid(#"{ "path": { "_storage": { "nullTerminatedStorage": [47, 0] } } }"#),
]

for testCase in testCases {
testCase.runAllTests()
}
}
}