diff --git a/Sources/TSCBasic/Path.swift b/Sources/TSCBasic/Path.swift index 0624ea75..ed2716cf 100644 --- a/Sources/TSCBasic/Path.swift +++ b/Sources/TSCBasic/Path.swift @@ -12,11 +12,7 @@ import Foundation import WinSDK #endif -#if os(Windows) -private typealias PathImpl = UNIXPath -#else private typealias PathImpl = UNIXPath -#endif /// Represents an absolute file system path, independently of what (or whether /// anything at all) exists at that path in the file system at any given time. @@ -435,15 +431,25 @@ extension Path { private struct UNIXPath: Path { let string: String +#if os(Windows) + static let root = UNIXPath(string: "\\") +#else static let root = UNIXPath(string: "/") +#endif static func isValidComponent(_ name: String) -> Bool { +#if os(Windows) + if name.contains("\\") { + return false + } +#endif return name != "" && name != "." && name != ".." && !name.contains("/") } #if os(Windows) static func isAbsolutePath(_ path: String) -> Bool { - return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) + return !path.standardizingPathSeparator() + .withCString(encodedAs: UTF16.self, PathIsRelativeW) } #endif @@ -453,11 +459,12 @@ private struct UNIXPath: Path { defer { fsr.deallocate() } let path: String = String(cString: fsr) - return path.withCString(encodedAs: UTF16.self) { + let result: String = path.withCString(encodedAs: UTF16.self) { let data = UnsafeMutablePointer(mutating: $0) PathCchRemoveFileSpec(data, path.count) return String(decodingCString: data, as: UTF16.self) } + return result.isEmpty ? "." : result #else // FIXME: This method seems too complicated; it should be simplified, // if possible, and certainly optimized (using UTF8View). @@ -544,11 +551,12 @@ private struct UNIXPath: Path { init(normalizingAbsolutePath path: String) { #if os(Windows) - var buffer: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) - _ = path.withCString(encodedAs: UTF16.self) { - PathCanonicalizeW(&buffer, $0) + var result: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) + + _ = path.standardizingPathSeparator().withCString(encodedAs: UTF16.self) { + PathCchCanonicalize(&result, result.count, $0) } - self.init(string: String(decodingCString: buffer, as: UTF16.self)) + self.init(string: String(decodingCString: result, as: UTF16.self)) #else precondition(path.first == "/", "Failure normalizing \(path), absolute paths should start with '/'") @@ -613,14 +621,14 @@ private struct UNIXPath: Path { } init(normalizingRelativePath path: String) { - #if os(Windows) - var buffer: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) - _ = path.replacingOccurrences(of: "/", with: "\\").withCString(encodedAs: UTF16.self) { - PathCanonicalizeW(&buffer, $0) - } - self.init(string: String(decodingCString: buffer, as: UTF16.self)) - #else - precondition(path.first != "/") + let pathSeparator: Character +#if os(Windows) + pathSeparator = "\\" + let path = path.standardizingPathSeparator() +#else + pathSeparator = "/" +#endif + precondition(path.first != pathSeparator) // FIXME: Here we should also keep track of whether anything actually has // to be changed in the string, and if not, just return the existing one. @@ -630,7 +638,7 @@ private struct UNIXPath: Path { // the normalized string representation. var parts: [String] = [] var capacity = 0 - for part in path.split(separator: "/") { + for part in path.split(separator: pathSeparator) { switch part.count { case 0: // Ignore empty path components. @@ -669,7 +677,7 @@ private struct UNIXPath: Path { if let first = iter.next() { result.append(contentsOf: first) while let next = iter.next() { - result.append("/") + result.append(pathSeparator) result.append(contentsOf: next) } } @@ -680,11 +688,15 @@ private struct UNIXPath: Path { // If the result is empty, return `.`, otherwise we return it as a string. self.init(string: result.isEmpty ? "." : result) - #endif } init(validatingAbsolutePath path: String) throws { - #if os(Windows) +#if os(Windows) + // Explicitly handle the empty path, since retrieving + // `fileSystemRepresentation` of it is illegal. + guard !path.isEmpty else { + throw PathValidationError.invalidAbsolutePath(path) + } let fsr: UnsafePointer = path.fileSystemRepresentation defer { fsr.deallocate() } @@ -693,7 +705,7 @@ private struct UNIXPath: Path { throw PathValidationError.invalidAbsolutePath(path) } self.init(normalizingAbsolutePath: path) - #else +#else switch path.first { case "/": self.init(normalizingAbsolutePath: path) @@ -702,11 +714,17 @@ private struct UNIXPath: Path { default: throw PathValidationError.invalidAbsolutePath(path) } - #endif +#endif } init(validatingRelativePath path: String) throws { - #if os(Windows) +#if os(Windows) + // Explicitly handle the empty path, since retrieving + // `fileSystemRepresentation` of it is illegal. + guard !path.isEmpty else { + self.init(normalizingRelativePath: path) + return + } let fsr: UnsafePointer = path.fileSystemRepresentation defer { fsr.deallocate() } @@ -715,14 +733,14 @@ private struct UNIXPath: Path { throw PathValidationError.invalidRelativePath(path) } self.init(normalizingRelativePath: path) - #else +#else switch path.first { case "/", "~": throw PathValidationError.invalidRelativePath(path) default: self.init(normalizingRelativePath: path) } - #endif +#endif } func suffix(withDot: Bool) -> String? { @@ -941,3 +959,11 @@ private func mayNeedNormalization(absolute string: String) -> Bool { } return false } + +#if os(Windows) +fileprivate extension String { + func standardizingPathSeparator() -> String { + return self.replacingOccurrences(of: "/", with: "\\") + } +} +#endif diff --git a/Sources/TSCBasic/PathShims.swift b/Sources/TSCBasic/PathShims.swift index bae0210d..982abd7a 100644 --- a/Sources/TSCBasic/PathShims.swift +++ b/Sources/TSCBasic/PathShims.swift @@ -26,6 +26,8 @@ public func resolveSymlinks(_ path: AbsolutePath) -> AbsolutePath { var resolved: URL = URL(fileURLWithPath: path.pathString) if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: path.pathString) { resolved = URL(fileURLWithPath: destination, relativeTo: URL(fileURLWithPath: path.pathString)) + } else { + return try! AbsolutePath(validating: path.pathString) } return resolved.standardized.withUnsafeFileSystemRepresentation {