Skip to content

Commit b2527eb

Browse files
authored
refactor RelativePath to allow late stage canonicalization in support of windows (#369)
motivation: canonicalizing RelativePath on construction does not work on Windows changes: * drop RelativePath canonicalizing constructors in favor of validating ones * add utilities for constructing RelativePath and AbsolutePath in test * update call sites and tests
1 parent c4e0757 commit b2527eb

12 files changed

+307
-272
lines changed

Sources/TSCBasic/Path.swift

+48-65
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public struct AbsolutePath: Hashable, Sendable {
9090

9191
self.init(String(decodingCString: pwszResult, as: UTF16.self))
9292
#else
93-
self.init(basePath, RelativePath(str))
93+
try self.init(basePath, RelativePath(validating: str))
9494
#endif
9595
}
9696
}
@@ -102,8 +102,8 @@ public struct AbsolutePath: Hashable, Sendable {
102102
}
103103

104104
/// Convenience initializer that appends a string to a relative path.
105-
public init(_ absPath: AbsolutePath, _ relStr: String) {
106-
self.init(absPath, RelativePath(relStr))
105+
public init(_ absPath: AbsolutePath, validating relStr: String) throws {
106+
try self.init(absPath, RelativePath(validating: relStr))
107107
}
108108

109109
/// Initializes the AbsolutePath from `absStr`, which must be an absolute
@@ -240,23 +240,6 @@ public struct RelativePath: Hashable, Sendable {
240240
_impl = impl
241241
}
242242

243-
/// Private initializer for constructing a relative path without performing
244-
/// normalization or canonicalization. This will construct a path without
245-
/// an anchor and thus may be invalid.
246-
fileprivate init(unsafeUncheckedPath string: String) {
247-
self.init(PathImpl(string: string))
248-
}
249-
250-
/// Initializes the RelativePath from `str`, which must be a relative path
251-
/// (which means that it must not begin with a path separator or a tilde).
252-
/// An empty input path is allowed, but will be normalized to a single `.`
253-
/// character. The input string will be normalized if needed, as described
254-
/// in the documentation for RelativePath.
255-
public init(_ string: String) {
256-
// Normalize the relative string and store it as our Path.
257-
self.init(PathImpl(normalizingRelativePath: string))
258-
}
259-
260243
/// Convenience initializer that verifies that the path is relative.
261244
public init(validating path: String) throws {
262245
try self.init(PathImpl(validatingRelativePath: path))
@@ -429,12 +412,6 @@ protocol Path: Hashable {
429412
/// Creates a path from its normalized string representation.
430413
init(string: String)
431414

432-
/// Creates a path from an absolute string representation and normalizes it.
433-
init(normalizingAbsolutePath: String)
434-
435-
/// Creates a path from an relative string representation and normalizes it.
436-
init(normalizingRelativePath: String)
437-
438415
/// Creates a path from a string representation, validates that it is a valid absolute path and normalizes it.
439416
init(validatingAbsolutePath: String) throws
440417

@@ -534,34 +511,12 @@ private struct WindowsPath: Path, Sendable {
534511
return String(cString: representation)
535512
}
536513

537-
init(normalizingAbsolutePath path: String) {
538-
self.init(string: Self.repr(path).withCString(encodedAs: UTF16.self) { pwszPath in
539-
var canonical: PWSTR!
540-
_ = PathAllocCanonicalize(pwszPath,
541-
ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue),
542-
&canonical)
543-
return String(decodingCString: canonical, as: UTF16.self)
544-
})
545-
}
546-
547514
init(validatingAbsolutePath path: String) throws {
548515
let realpath = Self.repr(path)
549516
if !Self.isAbsolutePath(realpath) {
550517
throw PathValidationError.invalidAbsolutePath(path)
551518
}
552-
self.init(normalizingAbsolutePath: path)
553-
}
554-
555-
init(normalizingRelativePath path: String) {
556-
if path.isEmpty || path == "." {
557-
self.init(string: ".")
558-
} else {
559-
var buffer: [WCHAR] = Array<WCHAR>(repeating: 0, count: Int(MAX_PATH + 1))
560-
_ = path.replacingOccurrences(of: "/", with: "\\").withCString(encodedAs: UTF16.self) {
561-
PathCanonicalizeW(&buffer, $0)
562-
}
563-
self.init(string: String(decodingCString: buffer, as: UTF16.self))
564-
}
519+
self.init(string: realpath)
565520
}
566521

567522
init(validatingRelativePath path: String) throws {
@@ -570,11 +525,10 @@ private struct WindowsPath: Path, Sendable {
570525
} else {
571526
let realpath: String = Self.repr(path)
572527
// Treat a relative path as an invalid relative path...
573-
if Self.isAbsolutePath(realpath) ||
574-
realpath.first == "~" || realpath.first == "\\" {
528+
if Self.isAbsolutePath(realpath) || realpath.first == "\\" {
575529
throw PathValidationError.invalidRelativePath(path)
576530
}
577-
self.init(normalizingRelativePath: path)
531+
self.init(string: realpath)
578532
}
579533
}
580534

@@ -597,7 +551,7 @@ private struct WindowsPath: Path, Sendable {
597551
}
598552
}
599553
defer { LocalFree(result) }
600-
return PathImpl(string: String(decodingCString: result!, as: UTF16.self))
554+
return Self(string: String(decodingCString: result!, as: UTF16.self))
601555
}
602556

603557
func appending(relativePath: Self) -> Self {
@@ -608,7 +562,7 @@ private struct WindowsPath: Path, Sendable {
608562
}
609563
}
610564
defer { LocalFree(result) }
611-
return PathImpl(string: String(decodingCString: result!, as: UTF16.self))
565+
return Self(string: String(decodingCString: result!, as: UTF16.self))
612566
}
613567
}
614568
#else
@@ -830,7 +784,7 @@ private struct UNIXPath: Path, Sendable {
830784

831785
init(validatingRelativePath path: String) throws {
832786
switch path.first {
833-
case "/", "~":
787+
case "/":
834788
throw PathValidationError.invalidRelativePath(path)
835789
default:
836790
self.init(normalizingRelativePath: path)
@@ -875,9 +829,9 @@ private struct UNIXPath: Path, Sendable {
875829
}
876830

877831
if self == Self.root {
878-
return PathImpl(string: "/" + name)
832+
return Self(string: "/" + name)
879833
} else {
880-
return PathImpl(string: string + "/" + name)
834+
return Self(string: string + "/" + name)
881835
}
882836
}
883837

@@ -900,12 +854,12 @@ private struct UNIXPath: Path, Sendable {
900854
// the beginning of the path only.
901855
if relativePathString.hasPrefix(".") {
902856
if newPathString.hasPrefix("/") {
903-
return PathImpl(normalizingAbsolutePath: newPathString)
857+
return Self(normalizingAbsolutePath: newPathString)
904858
} else {
905-
return PathImpl(normalizingRelativePath: newPathString)
859+
return Self(normalizingRelativePath: newPathString)
906860
}
907861
} else {
908-
return PathImpl(string: newPathString)
862+
return Self(string: newPathString)
909863
}
910864
}
911865
}
@@ -926,7 +880,7 @@ extension PathValidationError: CustomStringConvertible {
926880
case .invalidAbsolutePath(let path):
927881
return "invalid absolute path '\(path)'"
928882
case .invalidRelativePath(let path):
929-
return "invalid relative path '\(path)'; relative path should not begin with '\(AbsolutePath.root.pathString)' or '~'"
883+
return "invalid relative path '\(path)'; relative path should not begin with '\(AbsolutePath.root.pathString)'"
930884
}
931885
}
932886
}
@@ -956,10 +910,16 @@ extension AbsolutePath {
956910
// might be an empty path (when self and the base are equal).
957911
let relComps = pathComps.dropFirst(baseComps.count)
958912
#if os(Windows)
959-
result = RelativePath(unsafeUncheckedPath: relComps.joined(separator: "\\"))
913+
let pathString = relComps.joined(separator: "\\")
960914
#else
961-
result = RelativePath(relComps.joined(separator: "/"))
915+
let pathString = relComps.joined(separator: "/")
962916
#endif
917+
do {
918+
result = try RelativePath(validating: pathString)
919+
} catch {
920+
preconditionFailure("invalid relative path computed from \(pathString)")
921+
}
922+
963923
} else {
964924
// General case, in which we might well need `..` components to go
965925
// "up" before we can go "down" the directory tree.
@@ -975,10 +935,15 @@ extension AbsolutePath {
975935
var relComps = Array(repeating: "..", count: newBaseComps.count)
976936
relComps.append(contentsOf: newPathComps)
977937
#if os(Windows)
978-
result = RelativePath(unsafeUncheckedPath: relComps.joined(separator: "\\"))
938+
let pathString = relComps.joined(separator: "\\")
979939
#else
980-
result = RelativePath(relComps.joined(separator: "/"))
940+
let pathString = relComps.joined(separator: "/")
981941
#endif
942+
do {
943+
result = try RelativePath(validating: pathString)
944+
} catch {
945+
preconditionFailure("invalid relative path computed from \(pathString)")
946+
}
982947
}
983948

984949
assert(AbsolutePath(base, result) == self)
@@ -1065,13 +1030,31 @@ private func mayNeedNormalization(absolute string: String) -> Bool {
10651030
// MARK: - `AbsolutePath` backwards compatibility, delete after deprecation period.
10661031

10671032
extension AbsolutePath {
1033+
@_disfavoredOverload
10681034
@available(*, deprecated, message: "use throwing `init(validating:)` variant instead")
10691035
public init(_ absStr: String) {
10701036
try! self.init(validating: absStr)
10711037
}
10721038

1039+
@_disfavoredOverload
10731040
@available(*, deprecated, message: "use throwing `init(validating:relativeTo:)` variant instead")
10741041
public init(_ str: String, relativeTo basePath: AbsolutePath) {
10751042
try! self.init(validating: str, relativeTo: basePath)
10761043
}
1044+
1045+
@_disfavoredOverload
1046+
@available(*, deprecated, message: "use throwing variant instead")
1047+
public init(_ absPath: AbsolutePath, _ relStr: String) {
1048+
try! self.init(absPath, validating: relStr)
1049+
}
1050+
}
1051+
1052+
// MARK: - `AbsolutePath` backwards compatibility, delete after deprecation period.
1053+
1054+
extension RelativePath {
1055+
@_disfavoredOverload
1056+
@available(*, deprecated, message: "use throwing variant instead")
1057+
public init(_ string: String) {
1058+
try! self.init(validating: string)
1059+
}
10771060
}

Sources/TSCTestSupport/FileSystemExtensions.swift

+54-3
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,69 @@ extension FileSystem {
7373
}
7474
}
7575

76-
public extension AbsolutePath {
77-
init(path: StaticString) {
76+
extension AbsolutePath {
77+
@available(*, deprecated, message: "use direct string instead")
78+
public init(path: StaticString) {
7879
let pathString = path.withUTF8Buffer {
7980
String(decoding: $0, as: UTF8.self)
8081
}
8182
try! self.init(validating: pathString)
8283
}
8384

84-
init(path: StaticString, relativeTo basePath: AbsolutePath) {
85+
@available(*, deprecated, message: "use init(: relativeTo:) instead")
86+
public init(path: StaticString, relativeTo basePath: AbsolutePath) {
8587
let pathString = path.withUTF8Buffer {
8688
String(decoding: $0, as: UTF8.self)
8789
}
8890
try! self.init(validating: pathString, relativeTo: basePath)
8991
}
92+
93+
94+
@available(*, deprecated, message: "use direct string instead")
95+
public init(base: AbsolutePath, _ relStr: StaticString) {
96+
let pathString = relStr.withUTF8Buffer {
97+
String(decoding: $0, as: UTF8.self)
98+
}
99+
self.init(base, RelativePath(stringLiteral: pathString))
100+
}
101+
}
102+
103+
extension AbsolutePath: ExpressibleByStringLiteral {
104+
public init(_ value: StringLiteralType) {
105+
try! self.init(validating: value)
106+
}
107+
}
108+
109+
extension AbsolutePath: ExpressibleByStringInterpolation {
110+
public init(stringLiteral value: String) {
111+
try! self.init(validating: value)
112+
}
113+
}
114+
115+
extension AbsolutePath {
116+
public init(_ path: StringLiteralType, relativeTo basePath: AbsolutePath) {
117+
try! self.init(validating: path, relativeTo: basePath)
118+
}
119+
}
120+
121+
extension RelativePath {
122+
@available(*, deprecated, message: "use direct string instead")
123+
public init(static path: StaticString) {
124+
let pathString = path.withUTF8Buffer {
125+
String(decoding: $0, as: UTF8.self)
126+
}
127+
try! self.init(validating: pathString)
128+
}
129+
}
130+
131+
extension RelativePath: ExpressibleByStringLiteral {
132+
public init(_ value: StringLiteralType) {
133+
try! self.init(validating: value)
134+
}
135+
}
136+
137+
extension RelativePath: ExpressibleByStringInterpolation {
138+
public init(stringLiteral value: String) {
139+
try! self.init(validating: value)
140+
}
90141
}

Tests/TSCBasicPerformanceTests/PathPerfTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class PathPerfTests: XCTestCasePerf {
1919
@available(*, deprecated)
2020
func testJoinPerf_X100000() {
2121
#if canImport(Darwin)
22-
let absPath = AbsolutePath(path: "/hello/little")
22+
let absPath = AbsolutePath("/hello/little")
2323
let relPath = RelativePath("world")
2424
let N = 100000
2525
self.measure {

0 commit comments

Comments
 (0)