diff --git a/Sources/Basics/ByteString+Extensions.swift b/Sources/Basics/ByteString+Extensions.swift index 9d2d1226cbb..651ba2381aa 100644 --- a/Sources/Basics/ByteString+Extensions.swift +++ b/Sources/Basics/ByteString+Extensions.swift @@ -23,4 +23,8 @@ extension ByteString { public var sha256Checksum: String { SHA256().hash(self).hexadecimalRepresentation } + + public init(json: SerializedJSON) { + self.init(json.underlying.utf8) + } } diff --git a/Sources/Basics/CMakeLists.txt b/Sources/Basics/CMakeLists.txt index 5a6c948e874..4be6cb37002 100644 --- a/Sources/Basics/CMakeLists.txt +++ b/Sources/Basics/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(Basics Errors.swift FileSystem/AbsolutePath.swift FileSystem/FileSystem+Extensions.swift + FileSystem/NativePathExtensions.swift FileSystem/RelativePath.swift FileSystem/TemporaryFile.swift FileSystem/TSCAdapters.swift @@ -45,12 +46,12 @@ add_library(Basics ImportScanning.swift JSON+Extensions.swift JSONDecoder+Extensions.swift - NativePathExtensions.swift Netrc.swift Observability.swift SQLite.swift Sandbox.swift SendableTimeInterval.swift + Serialization/SerializedJSON.swift String+Extensions.swift SwiftVersion.swift SQLiteBackedCache.swift diff --git a/Sources/Basics/NativePathExtensions.swift b/Sources/Basics/FileSystem/NativePathExtensions.swift similarity index 70% rename from Sources/Basics/NativePathExtensions.swift rename to Sources/Basics/FileSystem/NativePathExtensions.swift index 914ac1c754f..3e3a50178b7 100644 --- a/Sources/Basics/NativePathExtensions.swift +++ b/Sources/Basics/FileSystem/NativePathExtensions.swift @@ -24,3 +24,15 @@ extension AbsolutePath { } } } + +extension DefaultStringInterpolation { + public mutating func appendInterpolation(_ value: AbsolutePath) { + self.appendInterpolation(value._nativePathString(escaped: false)) + } +} + +extension SerializedJSON.StringInterpolation { + public mutating func appendInterpolation(_ value: AbsolutePath) { + self.appendInterpolation(value._nativePathString(escaped: false)) + } +} diff --git a/Sources/Basics/Serialization/SerializedJSON.swift b/Sources/Basics/Serialization/SerializedJSON.swift new file mode 100644 index 00000000000..422af737418 --- /dev/null +++ b/Sources/Basics/Serialization/SerializedJSON.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Wrapper type representing serialized escaped JSON strings providing helpers +/// for escaped string interpolations for common types such as `AbsolutePath`. +public struct SerializedJSON { + let underlying: String +} + +extension SerializedJSON: ExpressibleByStringLiteral { + public init(stringLiteral: String) { + self.underlying = stringLiteral + } +} + +extension SerializedJSON: ExpressibleByStringInterpolation { + public init(stringInterpolation: StringInterpolation) { + self.init(underlying: stringInterpolation.value) + } + + public struct StringInterpolation: StringInterpolationProtocol { + fileprivate var value: String = "" + + private func escape(_ string: String) -> String { + string.replacingOccurrences(of: #"\"#, with: #"\\"#) + } + + public init(literalCapacity: Int, interpolationCount: Int) { + self.value.reserveCapacity(literalCapacity) + } + + public mutating func appendLiteral(_ literal: String) { + self.value.append(self.escape(literal)) + } + + public mutating func appendInterpolation(_ value: some CustomStringConvertible) { + self.value.append(self.escape(value.description)) + } + } +} diff --git a/Tests/BasicsTests/Serialization/SerializedJSONTests.swift b/Tests/BasicsTests/Serialization/SerializedJSONTests.swift new file mode 100644 index 00000000000..763c1d7ca56 --- /dev/null +++ b/Tests/BasicsTests/Serialization/SerializedJSONTests.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@testable import Basics +import XCTest + +final class SerializedJSONTests: XCTestCase { + func testPathInterpolation() throws { + var path = try AbsolutePath(validating: #"/test\backslashes"#) + var json: SerializedJSON = "\(path)" + + XCTAssertEqual(json.underlying, #"/test\\backslashes"#) + + #if os(Windows) + path = try AbsolutePath(validating: #"\\?\C:\Users"#) + json = "\(path)" + + XCTAssertEqual(json.underlying, #"\\\\?\\C:\\Users"#) + + path = try AbsolutePath(validating: #"\\.\UNC\server\share\n\"#) + json = "\(path)" + + XCTAssertEqual(json.underlying, #"\\\\.\\UNC\\server\\share\\"#) + + path = try AbsolutePath(validating: #"\??\Volumes{b79de17a-a1ed-4c58-a353-731b7c4885a6}\\"#) + json = "\(path)" + + XCTAssertEqual(json.underlying, #"\\??\\Volumes{b79de17a-a1ed-4c58-a353-731b7c4885a6}\\"#) + #endif + } +} diff --git a/Tests/PackageModelTests/SwiftSDKBundleTests.swift b/Tests/PackageModelTests/SwiftSDKBundleTests.swift index d290cd5cdd2..f94185fc906 100644 --- a/Tests/PackageModelTests/SwiftSDKBundleTests.swift +++ b/Tests/PackageModelTests/SwiftSDKBundleTests.swift @@ -21,7 +21,7 @@ import class TSCBasic.InMemoryFileSystem private let testArtifactID = "test-artifact" -private func generateInfoJSON(artifacts: [MockArtifact]) -> String { +private func generateInfoJSON(artifacts: [MockArtifact]) -> SerializedJSON { """ { "artifacts" : { @@ -68,7 +68,7 @@ private func generateTestFileSystem(bundleArtifacts: [MockArtifact]) throws -> ( ( "\($0.path)/info.json", ByteString( - encodingAsUTF8: generateInfoJSON(artifacts: $0.artifacts) + json: generateInfoJSON(artifacts: $0.artifacts) ) ) }) diff --git a/Tests/PackageModelTests/DestinationTests.swift b/Tests/PackageModelTests/SwiftSDKTests.swift similarity index 84% rename from Tests/PackageModelTests/DestinationTests.swift rename to Tests/PackageModelTests/SwiftSDKTests.swift index 635a5989e70..562a595ec76 100644 --- a/Tests/PackageModelTests/DestinationTests.swift +++ b/Tests/PackageModelTests/SwiftSDKTests.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -import Basics +@testable import Basics @testable import PackageModel @testable import SPMBuildCore import XCTest @@ -31,7 +31,7 @@ private let extraFlags = BuildFlags( ) private let destinationV1 = ( - path: "\(bundleRootPath)/destinationV1.json", + path: bundleRootPath.appending(component: "destinationV1.json"), json: #""" { "version": 1, @@ -42,11 +42,11 @@ private let destinationV1 = ( "extra-swiftc-flags": \#(extraFlags.swiftCompilerFlags), "extra-cpp-flags": \#(extraFlags.cxxCompilerFlags) } - """# + """# as SerializedJSON ) private let destinationV2 = ( - path: "\(bundleRootPath)/destinationV2.json", + path: bundleRootPath.appending(component: "destinationV2.json"), json: #""" { "version": 2, @@ -59,11 +59,11 @@ private let destinationV2 = ( "extraCXXFlags": \#(extraFlags.cxxCompilerFlags), "extraLinkerFlags": \#(extraFlags.linkerFlags) } - """# + """# as SerializedJSON ) private let toolsetNoRootDestinationV3 = ( - path: "\(bundleRootPath)/toolsetNoRootDestinationV3.json", + path: bundleRootPath.appending(component: "toolsetNoRootDestinationV3.json"), json: #""" { "runTimeTriples": { @@ -74,11 +74,11 @@ private let toolsetNoRootDestinationV3 = ( }, "schemaVersion": "3.0" } - """# + """# as SerializedJSON ) private let toolsetRootDestinationV3 = ( - path: "\(bundleRootPath)/toolsetRootDestinationV3.json", + path: bundleRootPath.appending(component: "toolsetRootDestinationV3.json"), json: #""" { "runTimeTriples": { @@ -89,11 +89,11 @@ private let toolsetRootDestinationV3 = ( }, "schemaVersion": "3.0" } - """# + """# as SerializedJSON ) private let missingToolsetDestinationV3 = ( - path: "\(bundleRootPath)/missingToolsetDestinationV3.json", + path: bundleRootPath.appending(component: "missingToolsetDestinationV3.json"), json: #""" { "runTimeTriples": { @@ -104,11 +104,11 @@ private let missingToolsetDestinationV3 = ( }, "schemaVersion": "3.0" } - """# + """# as SerializedJSON ) private let invalidVersionDestinationV3 = ( - path: "\(bundleRootPath)/invalidVersionDestinationV3.json", + path: bundleRootPath.appending(component: "invalidVersionDestinationV3.json"), json: #""" { "runTimeTriples": { @@ -119,11 +119,11 @@ private let invalidVersionDestinationV3 = ( }, "schemaVersion": "2.9" } - """# + """# as SerializedJSON ) private let invalidToolsetDestinationV3 = ( - path: "\(bundleRootPath)/invalidToolsetDestinationV3.json", + path: bundleRootPath.appending(component: "invalidToolsetDestinationV3.json"), json: #""" { "runTimeTriples": { @@ -134,11 +134,11 @@ private let invalidToolsetDestinationV3 = ( }, "schemaVersion": "3.0" } - """# + """# as SerializedJSON ) private let toolsetNoRootSwiftSDKv4 = ( - path: "\(bundleRootPath)/toolsetNoRootSwiftSDKv4.json", + path: bundleRootPath.appending(component: "toolsetNoRootSwiftSDKv4.json"), json: #""" { "targetTriples": { @@ -149,11 +149,11 @@ private let toolsetNoRootSwiftSDKv4 = ( }, "schemaVersion": "4.0" } - """# + """# as SerializedJSON ) private let toolsetRootSwiftSDKv4 = ( - path: "\(bundleRootPath)/toolsetRootSwiftSDKv4.json", + path: bundleRootPath.appending(component: "toolsetRootSwiftSDKv4.json"), json: #""" { "targetTriples": { @@ -164,11 +164,11 @@ private let toolsetRootSwiftSDKv4 = ( }, "schemaVersion": "4.0" } - """# + """# as SerializedJSON ) private let missingToolsetSwiftSDKv4 = ( - path: "\(bundleRootPath)/missingToolsetSwiftSDKv4.json", + path: bundleRootPath.appending(component: "missingToolsetSwiftSDKv4.json"), json: #""" { "targetTriples": { @@ -179,11 +179,11 @@ private let missingToolsetSwiftSDKv4 = ( }, "schemaVersion": "4.0" } - """# + """# as SerializedJSON ) private let invalidVersionSwiftSDKv4 = ( - path: "\(bundleRootPath)/invalidVersionSwiftSDKv4.json", + path: bundleRootPath.appending(component: "invalidVersionSwiftSDKv4.json"), json: #""" { "targetTriples": { @@ -194,11 +194,11 @@ private let invalidVersionSwiftSDKv4 = ( }, "schemaVersion": "42.9" } - """# + """# as SerializedJSON ) private let invalidToolsetSwiftSDKv4 = ( - path: "\(bundleRootPath)/invalidToolsetSwiftSDKv4.json", + path: bundleRootPath.appending(component: "invalidToolsetSwiftSDKv4.json"), json: #""" { "targetTriples": { @@ -209,7 +209,7 @@ private let invalidToolsetSwiftSDKv4 = ( }, "schemaVersion": "4.0" } - """# + """# as SerializedJSON ) private let usrBinTools = Dictionary(uniqueKeysWithValues: Toolset.KnownTool.allCases.map { @@ -217,7 +217,7 @@ private let usrBinTools = Dictionary(uniqueKeysWithValues: Toolset.KnownTool.all }) private let otherToolsNoRoot = ( - path: "/tools/otherToolsNoRoot.json", + path: try! AbsolutePath(validating: "/tools/otherToolsNoRoot.json"), json: #""" { "schemaVersion": "1.0", @@ -225,13 +225,13 @@ private let otherToolsNoRoot = ( "linker": { "path": "\#(usrBinTools[.linker]!)" }, "debugger": { "path": "\#(usrBinTools[.debugger]!)" } } - """# + """# as SerializedJSON ) private let cCompilerOptions = ["-fopenmp"] private let someToolsWithRoot = ( - path: "/tools/someToolsWithRoot.json", + path: try! AbsolutePath(validating: "/tools/someToolsWithRoot.json"), json: #""" { "schemaVersion": "1.0", @@ -241,11 +241,11 @@ private let someToolsWithRoot = ( "librarian": { "path": "llvm-ar" }, "debugger": { "path": "\#(usrBinTools[.debugger]!)" } } - """# + """# as SerializedJSON ) private let invalidToolset = ( - path: "/tools/invalidToolset.json", + path: try! AbsolutePath(validating: "/tools/invalidToolset.json"), json: #""" { "rootPath" : "swift.xctoolchain\/usr\/bin", @@ -271,7 +271,7 @@ private let invalidToolset = ( ], "schemaVersion" : "1.0" } - """# + """# as SerializedJSON ) private let sdkRootAbsolutePath = bundleRootPath.appending(sdkRootDir) @@ -326,37 +326,39 @@ private let parsedToolsetRootDestination = SwiftSDK( ) ) +private let testFiles: [(path: AbsolutePath, json: SerializedJSON)] = [ + destinationV1, + destinationV2, + toolsetNoRootDestinationV3, + toolsetRootDestinationV3, + missingToolsetDestinationV3, + invalidVersionDestinationV3, + invalidToolsetDestinationV3, + toolsetNoRootSwiftSDKv4, + toolsetRootSwiftSDKv4, + missingToolsetSwiftSDKv4, + invalidVersionSwiftSDKv4, + invalidToolsetSwiftSDKv4, + otherToolsNoRoot, + someToolsWithRoot, + invalidToolset, +] + final class DestinationTests: XCTestCase { func testDestinationCodable() throws { let fs = InMemoryFileSystem() try fs.createDirectory(AbsolutePath(validating: "/tools")) try fs.createDirectory(AbsolutePath(validating: "/tmp")) try fs.createDirectory(AbsolutePath(validating: "\(bundleRootPath)")) - for testFile in [ - destinationV1, - destinationV2, - toolsetNoRootDestinationV3, - toolsetRootDestinationV3, - missingToolsetDestinationV3, - invalidVersionDestinationV3, - invalidToolsetDestinationV3, - toolsetNoRootSwiftSDKv4, - toolsetRootSwiftSDKv4, - missingToolsetSwiftSDKv4, - invalidVersionSwiftSDKv4, - invalidToolsetSwiftSDKv4, - otherToolsNoRoot, - someToolsWithRoot, - invalidToolset, - ] { - try fs.writeFileContents(AbsolutePath(validating: testFile.path), string: testFile.json) + for testFile in testFiles { + try fs.writeFileContents(testFile.path, string: testFile.json.underlying) } let system = ObservabilitySystem.makeForTesting() let observability = system.topScope let destinationV1Decoded = try SwiftSDK.decode( - fromFile: AbsolutePath(validating: destinationV1.path), + fromFile: destinationV1.path, fileSystem: fs, observabilityScope: observability ) @@ -378,7 +380,7 @@ final class DestinationTests: XCTestCase { ) let destinationV2Decoded = try SwiftSDK.decode( - fromFile: AbsolutePath(validating: destinationV2.path), + fromFile: destinationV2.path, fileSystem: fs, observabilityScope: observability ) @@ -386,7 +388,7 @@ final class DestinationTests: XCTestCase { XCTAssertEqual(destinationV2Decoded, [parsedDestinationV2GNU]) let toolsetNoRootDestinationV3Decoded = try SwiftSDK.decode( - fromFile: AbsolutePath(validating: toolsetNoRootDestinationV3.path), + fromFile: toolsetNoRootDestinationV3.path, fileSystem: fs, observabilityScope: observability ) @@ -394,7 +396,7 @@ final class DestinationTests: XCTestCase { XCTAssertEqual(toolsetNoRootDestinationV3Decoded, [parsedToolsetNoRootDestination]) let toolsetRootDestinationV3Decoded = try SwiftSDK.decode( - fromFile: AbsolutePath(validating: toolsetRootDestinationV3.path), + fromFile: toolsetRootDestinationV3.path, fileSystem: fs, observabilityScope: observability ) @@ -402,7 +404,7 @@ final class DestinationTests: XCTestCase { XCTAssertEqual(toolsetRootDestinationV3Decoded, [parsedToolsetRootDestination]) XCTAssertThrowsError(try SwiftSDK.decode( - fromFile: AbsolutePath(validating: missingToolsetDestinationV3.path), + fromFile: missingToolsetDestinationV3.path, fileSystem: fs, observabilityScope: observability )) { @@ -417,13 +419,13 @@ final class DestinationTests: XCTestCase { ) } XCTAssertThrowsError(try SwiftSDK.decode( - fromFile: AbsolutePath(validating: invalidVersionDestinationV3.path), + fromFile: invalidVersionDestinationV3.path, fileSystem: fs, observabilityScope: observability )) XCTAssertThrowsError(try SwiftSDK.decode( - fromFile: AbsolutePath(validating: invalidToolsetDestinationV3.path), + fromFile: invalidToolsetDestinationV3.path, fileSystem: fs, observabilityScope: observability )) { @@ -434,7 +436,7 @@ final class DestinationTests: XCTestCase { } let toolsetNoRootSwiftSDKv4Decoded = try SwiftSDK.decode( - fromFile: AbsolutePath(validating: toolsetNoRootSwiftSDKv4.path), + fromFile: toolsetNoRootSwiftSDKv4.path, fileSystem: fs, observabilityScope: observability ) @@ -442,7 +444,7 @@ final class DestinationTests: XCTestCase { XCTAssertEqual(toolsetNoRootSwiftSDKv4Decoded, [parsedToolsetNoRootDestination]) let toolsetRootSwiftSDKv4Decoded = try SwiftSDK.decode( - fromFile: AbsolutePath(validating: toolsetRootSwiftSDKv4.path), + fromFile: toolsetRootSwiftSDKv4.path, fileSystem: fs, observabilityScope: observability ) @@ -450,7 +452,7 @@ final class DestinationTests: XCTestCase { XCTAssertEqual(toolsetRootSwiftSDKv4Decoded, [parsedToolsetRootDestination]) XCTAssertThrowsError(try SwiftSDK.decode( - fromFile: AbsolutePath(validating: missingToolsetSwiftSDKv4.path), + fromFile: missingToolsetSwiftSDKv4.path, fileSystem: fs, observabilityScope: observability )) { @@ -465,13 +467,13 @@ final class DestinationTests: XCTestCase { ) } XCTAssertThrowsError(try SwiftSDK.decode( - fromFile: AbsolutePath(validating: invalidVersionSwiftSDKv4.path), + fromFile: invalidVersionSwiftSDKv4.path, fileSystem: fs, observabilityScope: observability )) XCTAssertThrowsError(try SwiftSDK.decode( - fromFile: AbsolutePath(validating: invalidToolsetSwiftSDKv4.path), + fromFile: invalidToolsetSwiftSDKv4.path, fileSystem: fs, observabilityScope: observability )) { diff --git a/Tests/PackageModelTests/ToolsetTests.swift b/Tests/PackageModelTests/ToolsetTests.swift index 1ec328ed903..4755c03e3fa 100644 --- a/Tests/PackageModelTests/ToolsetTests.swift +++ b/Tests/PackageModelTests/ToolsetTests.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -import Basics +@testable import Basics @testable import PackageModel import SPMTestSupport import XCTest @@ -34,7 +34,7 @@ private let compilersNoRoot = ( "cCompiler": { "path": "\#(usrBinTools[.cCompiler]!)", "extraCLIOptions": \#(cCompilerOptions) }, "cxxCompiler": { "path": "\#(usrBinTools[.cxxCompiler]!)", "extraCLIOptions": \#(cxxCompilerOptions) }, } - """# + """# as SerializedJSON ) private let noValidToolsNoRoot = ( @@ -44,7 +44,7 @@ private let noValidToolsNoRoot = ( "schemaVersion": "1.0", "cCompiler": {} } - """# + """# as SerializedJSON ) private let unknownToolsNoRoot = ( @@ -55,7 +55,7 @@ private let unknownToolsNoRoot = ( "foo": {}, "bar": {} } - """# + """# as SerializedJSON ) private let otherToolsNoRoot = ( @@ -67,7 +67,7 @@ private let otherToolsNoRoot = ( "linker": { "path": "\#(usrBinTools[.linker]!)" }, "debugger": { "path": "\#(usrBinTools[.debugger]!)" } } - """# + """# as SerializedJSON ) private let someToolsWithRoot = ( @@ -81,7 +81,7 @@ private let someToolsWithRoot = ( "librarian": { "path": "llvm-ar" }, "debugger": { "path": "\#(usrBinTools[.debugger]!)" } } - """# + """# as SerializedJSON ) private let someToolsWithRelativeRoot = ( @@ -92,7 +92,7 @@ private let someToolsWithRelativeRoot = ( "rootPath": "relative/custom", "cCompiler": { "extraCLIOptions": \#(newCCompilerOptions) } } - """# + """# as SerializedJSON ) final class ToolsetTests: XCTestCase { @@ -100,7 +100,7 @@ final class ToolsetTests: XCTestCase { let fileSystem = InMemoryFileSystem() try fileSystem.createDirectory(AbsolutePath(validating: "/tools")) for testFile in [compilersNoRoot, noValidToolsNoRoot, unknownToolsNoRoot, otherToolsNoRoot, someToolsWithRoot, someToolsWithRelativeRoot] { - try fileSystem.writeFileContents(testFile.path, data: .init(testFile.json.utf8)) + try fileSystem.writeFileContents(testFile.path, string: testFile.json.underlying) } let observability = ObservabilitySystem.makeForTesting()