Skip to content

Commit 37e8245

Browse files
authored
Add SerializedJSON type as string literal escaping helper (#6730)
Currently we have to call `_nativePathString` manually in a lot of places where `AbsolutePath` is interpolated into strings. We also need to take into account whether `_nativePathString` should be called with `escaped: true` or `escaped: false` arguments, which is prone to errors. The serialization format should carry the knowledge of its correct escaping algorithm, not the path representation itself. We can make this much cleaner if string interpolations call `_nativePathString` automatically where needed, and serialization formats add proper escaping on top of that. Let's mplements this for JSON, specifically where it is used in Swift SDKs. ### Modifications: Added `SerializedJSON` type as string literal escaping helper. Also added a custom `AbsolutePath` interpolation to the new type, which removes the need to call `_nativePathString` when paths are interpolated in JSON strings. ### Result: We don't have to call `_nativePathString` manually in as many places. In future PRs `_nativePathString` should be declared `private` or at least `internal` so that it's hidden as an implementation detail of appropriate string interpolations.
1 parent bcb4b1a commit 37e8245

File tree

8 files changed

+180
-72
lines changed

8 files changed

+180
-72
lines changed

Sources/Basics/ByteString+Extensions.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ extension ByteString {
2323
public var sha256Checksum: String {
2424
SHA256().hash(self).hexadecimalRepresentation
2525
}
26+
27+
public init(json: SerializedJSON) {
28+
self.init(json.underlying.utf8)
29+
}
2630
}

Sources/Basics/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ add_library(Basics
2727
Errors.swift
2828
FileSystem/AbsolutePath.swift
2929
FileSystem/FileSystem+Extensions.swift
30+
FileSystem/NativePathExtensions.swift
3031
FileSystem/RelativePath.swift
3132
FileSystem/TemporaryFile.swift
3233
FileSystem/TSCAdapters.swift
@@ -45,12 +46,12 @@ add_library(Basics
4546
ImportScanning.swift
4647
JSON+Extensions.swift
4748
JSONDecoder+Extensions.swift
48-
NativePathExtensions.swift
4949
Netrc.swift
5050
Observability.swift
5151
SQLite.swift
5252
Sandbox.swift
5353
SendableTimeInterval.swift
54+
Serialization/SerializedJSON.swift
5455
String+Extensions.swift
5556
SwiftVersion.swift
5657
SQLiteBackedCache.swift

Sources/Basics/NativePathExtensions.swift renamed to Sources/Basics/FileSystem/NativePathExtensions.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,15 @@ extension AbsolutePath {
2424
}
2525
}
2626
}
27+
28+
extension DefaultStringInterpolation {
29+
public mutating func appendInterpolation(_ value: AbsolutePath) {
30+
self.appendInterpolation(value._nativePathString(escaped: false))
31+
}
32+
}
33+
34+
extension SerializedJSON.StringInterpolation {
35+
public mutating func appendInterpolation(_ value: AbsolutePath) {
36+
self.appendInterpolation(value._nativePathString(escaped: false))
37+
}
38+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Wrapper type representing serialized escaped JSON strings providing helpers
14+
/// for escaped string interpolations for common types such as `AbsolutePath`.
15+
public struct SerializedJSON {
16+
let underlying: String
17+
}
18+
19+
extension SerializedJSON: ExpressibleByStringLiteral {
20+
public init(stringLiteral: String) {
21+
self.underlying = stringLiteral
22+
}
23+
}
24+
25+
extension SerializedJSON: ExpressibleByStringInterpolation {
26+
public init(stringInterpolation: StringInterpolation) {
27+
self.init(underlying: stringInterpolation.value)
28+
}
29+
30+
public struct StringInterpolation: StringInterpolationProtocol {
31+
fileprivate var value: String = ""
32+
33+
private func escape(_ string: String) -> String {
34+
string.replacingOccurrences(of: #"\"#, with: #"\\"#)
35+
}
36+
37+
public init(literalCapacity: Int, interpolationCount: Int) {
38+
self.value.reserveCapacity(literalCapacity)
39+
}
40+
41+
public mutating func appendLiteral(_ literal: String) {
42+
self.value.append(self.escape(literal))
43+
}
44+
45+
public mutating func appendInterpolation(_ value: some CustomStringConvertible) {
46+
self.value.append(self.escape(value.description))
47+
}
48+
}
49+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
@testable import Basics
14+
import XCTest
15+
16+
final class SerializedJSONTests: XCTestCase {
17+
func testPathInterpolation() throws {
18+
var path = try AbsolutePath(validating: #"/test\backslashes"#)
19+
var json: SerializedJSON = "\(path)"
20+
21+
XCTAssertEqual(json.underlying, #"/test\\backslashes"#)
22+
23+
#if os(Windows)
24+
path = try AbsolutePath(validating: #"\\?\C:\Users"#)
25+
json = "\(path)"
26+
27+
XCTAssertEqual(json.underlying, #"\\\\?\\C:\\Users"#)
28+
29+
path = try AbsolutePath(validating: #"\\.\UNC\server\share\n\"#)
30+
json = "\(path)"
31+
32+
XCTAssertEqual(json.underlying, #"\\\\.\\UNC\\server\\share\\"#)
33+
34+
path = try AbsolutePath(validating: #"\??\Volumes{b79de17a-a1ed-4c58-a353-731b7c4885a6}\\"#)
35+
json = "\(path)"
36+
37+
XCTAssertEqual(json.underlying, #"\\??\\Volumes{b79de17a-a1ed-4c58-a353-731b7c4885a6}\\"#)
38+
#endif
39+
}
40+
}

Tests/PackageModelTests/SwiftSDKBundleTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import class TSCBasic.InMemoryFileSystem
2121

2222
private let testArtifactID = "test-artifact"
2323

24-
private func generateInfoJSON(artifacts: [MockArtifact]) -> String {
24+
private func generateInfoJSON(artifacts: [MockArtifact]) -> SerializedJSON {
2525
"""
2626
{
2727
"artifacts" : {
@@ -68,7 +68,7 @@ private func generateTestFileSystem(bundleArtifacts: [MockArtifact]) throws -> (
6868
(
6969
"\($0.path)/info.json",
7070
ByteString(
71-
encodingAsUTF8: generateInfoJSON(artifacts: $0.artifacts)
71+
json: generateInfoJSON(artifacts: $0.artifacts)
7272
)
7373
)
7474
})

0 commit comments

Comments
 (0)