From e267bc3b55a5708ff9dc12a692241666aeab5011 Mon Sep 17 00:00:00 2001 From: Kristopher Cieplak Date: Wed, 7 May 2025 14:02:46 -0400 Subject: [PATCH 1/2] Resolves #505 - Fix handling for Windows long paths PR#369 - https://github.com/swiftlang/swift-tools-support-core/pull/369 Caused the majority of tests on Windows to fail, as normalization of RelativePath was removed. This change also removed the call to 'PathAllocCanonicalize' for AbsolutePath which had the long file flag 'PATHCCH_ALLOW_LONG_PATHS'. Reintroducing canonicalization of AbsolutePath path representation to handle long paths. * Update tests dealing with RelativePath to match implementation. * Canonicalize the path representation for AbsolutePath which also allows for long path '\\?\' prefix addition when path > 260 in length. * Strip trailing backslash on string representation of AbsolutePath to match definition. Only strips for non root paths. * Add helper functions: - removeTrailingBackslash - stripPrefix - canonicalPathRepresentation * Add Windows API Error helpers * Add long path tests into each test case * Fix up .suffix. '.' has no suffix * Update copyright dates --- Sources/TSCBasic/Path.swift | 149 ++++++++- Sources/TSCBasic/Win32Error.swift | 37 +++ Tests/TSCBasicTests/PathTests.swift | 481 +++++++++++++++++++++++++--- 3 files changed, 601 insertions(+), 66 deletions(-) create mode 100644 Sources/TSCBasic/Win32Error.swift diff --git a/Sources/TSCBasic/Path.swift b/Sources/TSCBasic/Path.swift index 208f9819..c9caf01c 100644 --- a/Sources/TSCBasic/Path.swift +++ b/Sources/TSCBasic/Path.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2025 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 @@ -32,6 +32,7 @@ import var Foundation.NSLocalizedDescriptionKey /// - Removing `.` path components /// - Removing any trailing path separator /// - Removing any redundant path separators +/// - Converting the disk designator to uppercase (Windows) i.e. c:\ to C:\ /// /// This string manipulation may change the meaning of a path if any of the /// path components are symbolic links on disk. However, the file system is @@ -457,6 +458,79 @@ private struct WindowsPath: Path, Sendable { return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) } + /// When this function returns successfully, the same path string will have had the prefix removed, + /// if the prefix was present. If no prefix was present, the string will be unchanged. + static func stripPrefix(_ path: String) -> String { + return path.withCString(encodedAs: UTF16.self) { cStringPtr in + let mutableCStringPtr = UnsafeMutablePointer(mutating: cStringPtr) + let result = PathCchStripPrefix(mutableCStringPtr, path.utf16.count + 1) + if result == S_OK { + return String(decodingCString: mutableCStringPtr, as: UTF16.self) + } + return path + } + } + + /// Remove a trailing backslash from a path if the following conditions + /// are true: + /// * Path is not a root path + /// * Pash has a trailing backslash + /// If conditions are not met then the string is returned unchanged. + static func removeTrailingBackslash(_ path: String) -> String { + return path.withCString(encodedAs: UTF16.self) { cStringPtr in + let mutableCStringPtr = UnsafeMutablePointer(mutating: cStringPtr) + let result = PathCchRemoveBackslash(mutableCStringPtr, path.utf16.count + 1) + + if result == S_OK { + return String(decodingCString: mutableCStringPtr, as: UTF16.self) + } + return path + } + } + + /// Create a canonicalized path representation for Windows. + /// Returns a potentially `\\?\`-prefixed version of the path, + /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. + /// + /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + static func canonicalPathRepresentation(_ path: String) throws -> String { + return try path.withCString(encodedAs: UTF16.self) { pwszPlatformPath in + // 1. Normalize the path first. + // Contrary to the documentation, this works on long paths independently + // of the registry or process setting to enable long paths (but it will also + // not add the \\?\ prefix required by other functions under these conditions). + let dwLength: DWORD = GetFullPathNameW(pwszPlatformPath, 0, nil, nil) + + return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in + guard (1 ..< dwLength).contains(GetFullPathNameW(pwszPlatformPath, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else { + throw Win32Error(GetLastError()) + } + // 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these. + if let base = pwszFullPath.baseAddress, + base[0] == UInt8(ascii: "\\"), + base[1] == UInt8(ascii: "\\"), + base[2] == UInt8(ascii: "."), + base[3] == UInt8(ascii: "\\") + { + return String(decodingCString: base, as: UTF16.self) + } + // 2. Canonicalize the path. + // This will add the \\?\ prefix if needed based on the path's length. + var pwszCanonicalPath: LPWSTR? + let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue) | numericCast(PATHCCH_CANONICALIZE_SLASHES.rawValue) + let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath) + if let pwszCanonicalPath { + defer { LocalFree(pwszCanonicalPath) } + if result == S_OK { + // 3. Perform the operation on the normalized path. + return String(decodingCString: pwszCanonicalPath, as: UTF16.self) + } + } + throw Win32Error(WIN32_FROM_HRESULT(result)) + } + } + } + var dirname: String { let fsr: UnsafePointer = self.string.fileSystemRepresentation defer { fsr.deallocate() } @@ -506,8 +580,8 @@ private struct WindowsPath: Path, Sendable { var components: [String] { let normalized: UnsafePointer = string.fileSystemRepresentation defer { normalized.deallocate() } - - return String(cString: normalized).components(separatedBy: "\\").filter { !$0.isEmpty } + // Remove prefix from the components, allowing for comparison across normalized paths. + return Self.stripPrefix(String(cString: normalized)).components(separatedBy: #"\"#).filter { !$0.isEmpty } } var parentDirectory: Self { @@ -515,12 +589,16 @@ private struct WindowsPath: Path, Sendable { } init(string: String) { - if string.first?.isASCII ?? false, string.first?.isLetter ?? false, string.first?.isLowercase ?? false, - string.count > 1, string[string.index(string.startIndex, offsetBy: 1)] == ":" + let noPrefixPath = Self.stripPrefix(string) + let prefix = string.replacingOccurrences(of: noPrefixPath, with: "") // Just the prefix or empty + + // Perform drive designator normalization i.e. 'c:\' to 'C:\' on string. + if noPrefixPath.first?.isASCII ?? false, noPrefixPath.first?.isLetter ?? false, noPrefixPath.first?.isLowercase ?? false, + noPrefixPath.count > 1, noPrefixPath[noPrefixPath.index(noPrefixPath.startIndex, offsetBy: 1)] == ":" { - self.string = "\(string.first!.uppercased())\(string.dropFirst(1))" + self.string = prefix + "\(noPrefixPath.first!.uppercased())\(noPrefixPath.dropFirst(1))" } else { - self.string = string + self.string = prefix + noPrefixPath } } @@ -536,7 +614,13 @@ private struct WindowsPath: Path, Sendable { if !Self.isAbsolutePath(realpath) { throw PathValidationError.invalidAbsolutePath(path) } - self.init(string: realpath) + do { + let canonicalizedPath = try Self.canonicalPathRepresentation(realpath) + let normalizedPath = Self.removeTrailingBackslash(canonicalizedPath) // AbsolutePath states paths have no trailing separator. + self.init(string: normalizedPath) + } catch { + throw PathValidationError.invalidAbsolutePath("\(path): \(error)") + } } init(validatingRelativePath path: String) throws { @@ -554,12 +638,20 @@ private struct WindowsPath: Path, Sendable { func suffix(withDot: Bool) -> String? { return self.string.withCString(encodedAs: UTF16.self) { - if let pointer = PathFindExtensionW($0) { - let substring = String(decodingCString: pointer, as: UTF16.self) - guard substring.length > 0 else { return nil } - return withDot ? substring : String(substring.dropFirst(1)) - } - return nil + if let dotPointer = PathFindExtensionW($0) { + // If the dotPointer is the same as the full path, there are no components before + // the suffix and therefore there is no suffix. + if dotPointer == $0 { + return nil + } + let substring = String(decodingCString: dotPointer, as: UTF16.self) + // Substring must have a dot and one more character to be considered a suffix + guard substring.length > 1 else { + return nil + } + return withDot ? substring : String(substring.dropFirst(1)) + } + return nil } } @@ -585,6 +677,32 @@ private struct WindowsPath: Path, Sendable { return Self(string: String(decodingCString: result!, as: UTF16.self)) } } + +fileprivate func HRESULT_CODE(_ hr: HRESULT) -> DWORD { + DWORD(hr) & 0xFFFF +} + +@inline(__always) +fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD { + DWORD(hr << 16) & 0x1FFF +} + +@inline(__always) +fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool { + hr >= 0 +} + +// This is a non-standard extension to the Windows SDK that allows us to convert +// an HRESULT to a Win32 error code. +@inline(__always) +fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD { + if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) } + if HRESULT_FACILITY(hr) == FACILITY_WIN32 { + return HRESULT_CODE(hr) + } + return DWORD(hr) +} + #else private struct UNIXPath: Path, Sendable { let string: String @@ -966,7 +1084,8 @@ extension AbsolutePath { } } - assert(AbsolutePath(base, result) == self) + assert(AbsolutePath(base, result) == self, "base:\(base) result:\(result) self: \(self)") + return result } diff --git a/Sources/TSCBasic/Win32Error.swift b/Sources/TSCBasic/Win32Error.swift new file mode 100644 index 00000000..ac091f54 --- /dev/null +++ b/Sources/TSCBasic/Win32Error.swift @@ -0,0 +1,37 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2025 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 Swift project authors + */ + +#if os(Windows) +public import WinSDK +import Foundation + +public struct Win32Error: Error, CustomStringConvertible { + public let error: DWORD + + public init(_ error: DWORD) { + self.error = error + } + + public var description: String { + let flags: DWORD = DWORD(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS) + var buffer: UnsafeMutablePointer? + let length: DWORD = withUnsafeMutablePointer(to: &buffer) { + $0.withMemoryRebound(to: WCHAR.self, capacity: 2) { + FormatMessageW(flags, nil, error, 0, $0, 0, nil) + } + } + guard let buffer, length > 0 else { + return "Win32 Error Code \(error)" + } + defer { LocalFree(buffer) } + return String(decodingCString: buffer, as: UTF16.self).trimmingCharacters(in: .whitespacesAndNewlines) + } +} +#endif \ No newline at end of file diff --git a/Tests/TSCBasicTests/PathTests.swift b/Tests/TSCBasicTests/PathTests.swift index d18ce814..1c3ba631 100644 --- a/Tests/TSCBasicTests/PathTests.swift +++ b/Tests/TSCBasicTests/PathTests.swift @@ -1,31 +1,75 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2025 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 Swift project authors -*/ + */ import Foundation import TSCBasic import TSCTestSupport import XCTest +func generatePath(_ length: Int, absolute: Bool = true, duplicateSeparators: Bool = false) -> String { + #if !os(Windows) + var path = absolute ? "/" : "" + let separator = duplicateSeparators ? "//" : "/" + #else + var path = absolute ? #"C:"# : "" + let separator = duplicateSeparators ? #"\\"# : #"\"# + #endif + var currentPathLength = path.count + var dirNameCount = 0 + while currentPathLength < length { + let dirName = String(dirNameCount) + path.append("\(path.count != 0 ? separator : "")\(dirName)") + dirNameCount += 1 + currentPathLength += separator.count + dirName.count + } + return path +} + class PathTests: XCTestCase { + // The implementation of RelativePath on Windows does not do any path canonicalization/normalization". + // Canonicalization is only done on AbsolutePaths, so all tests need to handle this difference. func testBasics() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/").pathString, "/") XCTAssertEqual(AbsolutePath("/a").pathString, "/a") XCTAssertEqual(AbsolutePath("/a/b/c").pathString, "/a/b/c") XCTAssertEqual(RelativePath(".").pathString, ".") XCTAssertEqual(RelativePath("a").pathString, "a") XCTAssertEqual(RelativePath("a/b/c").pathString, "a/b/c") - XCTAssertEqual(RelativePath("~").pathString, "~") // `~` is not special + XCTAssertEqual(RelativePath("~").pathString, "~") // `~` is not special + #else + // Backslash is considered an absolute path by 'PathIsRelativeW', however after canonicalization the drive designator + // of current working drive will be added to the path. + XCTAssert(try #/[A-Z]:\\/#.wholeMatch(in: AbsolutePath(#"\"#).pathString) != nil) + XCTAssert(try #/[A-Z]:\\foo/#.wholeMatch(in: AbsolutePath(#"\foo"#).pathString) != nil) + XCTAssertEqual(AbsolutePath(#"C:\"#).pathString, #"C:\"#) + XCTAssertEqual(RelativePath(".").pathString, ".") + XCTAssertEqual(RelativePath("a").pathString, "a") + XCTAssertEqual(RelativePath(#"a\b\c"#).pathString, #"a\b\c"#) + let longAbsolutePathUnderPathMax = generatePath(200) + XCTAssertEqual(AbsolutePath(longAbsolutePathUnderPathMax).pathString, longAbsolutePathUnderPathMax) + let longAbsolutePathOverPathMax = generatePath(260) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax).pathString, #"\\?\"# + longAbsolutePathOverPathMax) + #endif + } + + func testMixedSeperators() { + #if os(Windows) + XCTAssertEqual(AbsolutePath(#"C:\foo/bar"#).pathString, #"C:\foo\bar"#) + XCTAssertEqual(AbsolutePath(#"C:\\foo//bar"#).pathString, #"C:\foo\bar"#) + #endif } func testStringInitialization() throws { + #if !os(Windows) let abs1 = AbsolutePath("/") let abs2 = AbsolutePath(abs1, ".") XCTAssertEqual(abs1, abs2) @@ -37,41 +81,94 @@ class PathTests: XCTestCase { XCTAssertEqual(abs4, AbsolutePath("/a/b/c")) let abs5 = AbsolutePath("./a/b/c", relativeTo: base) XCTAssertEqual(abs5, AbsolutePath("/base/path/a/b/c")) - let abs6 = AbsolutePath("~/bla", relativeTo: base) // `~` isn't special + let abs6 = AbsolutePath("~/bla", relativeTo: base) // `~` isn't special XCTAssertEqual(abs6, AbsolutePath("/base/path/~/bla")) + #else + let abs1 = AbsolutePath(#"C:\"#) + let abs2 = AbsolutePath(abs1, ".") + XCTAssertEqual(abs1, abs2) + let rel3 = "." + let abs3 = try AbsolutePath(abs2, validating: rel3) + XCTAssertEqual(abs2, abs3) + let base = AbsolutePath(#"C:\base\path"#) + let abs4 = AbsolutePath(#"\a\b\c"#, relativeTo: base) + XCTAssertEqual(abs4, AbsolutePath(#"C:\a\b\c"#)) + let abs5 = AbsolutePath(#".\a\b\c"#, relativeTo: base) + XCTAssertEqual(abs5, AbsolutePath(#"C:\base\path\a\b\c"#)) + #endif } func testStringLiteralInitialization() { + #if !os(Windows) let abs = AbsolutePath("/") XCTAssertEqual(abs.pathString, "/") let rel1 = RelativePath(".") XCTAssertEqual(rel1.pathString, ".") let rel2 = RelativePath("~") - XCTAssertEqual(rel2.pathString, "~") // `~` is not special + XCTAssertEqual(rel2.pathString, "~") // `~` is not special + #else + let abs = AbsolutePath(#"C:\"#) + XCTAssertEqual(abs.pathString, #"C:\"#) + let rel1 = RelativePath(".") + XCTAssertEqual(rel1.pathString, ".") + #endif } func testRepeatedPathSeparators() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/ab//cd//ef").pathString, "/ab/cd/ef") XCTAssertEqual(AbsolutePath("/ab///cd//ef").pathString, "/ab/cd/ef") XCTAssertEqual(RelativePath("ab//cd//ef").pathString, "ab/cd/ef") XCTAssertEqual(RelativePath("ab//cd///ef").pathString, "ab/cd/ef") + #else + XCTAssertEqual(AbsolutePath(#"C:\ab\\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"C:\ab\\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(RelativePath(#"ab\\cd\\ef"#).pathString, #"ab\\cd\\ef"#) + XCTAssertEqual(RelativePath(#"ab\\cd\\\ef"#).pathString, #"ab\\cd\\\ef"#) + // Duplicate backslashes will be squashed, so needs to be more that PATH_MAX + let longAbsolutePathOverPathMax = generatePath(2 * 260, duplicateSeparators: true) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax).pathString, + #"\\?\"# + longAbsolutePathOverPathMax.replacingOccurrences(of: #"\\"#, with: #"\"#)) + #endif } func testTrailingPathSeparators() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/ab/cd/ef/").pathString, "/ab/cd/ef") XCTAssertEqual(AbsolutePath("/ab/cd/ef//").pathString, "/ab/cd/ef") XCTAssertEqual(RelativePath("ab/cd/ef/").pathString, "ab/cd/ef") XCTAssertEqual(RelativePath("ab/cd/ef//").pathString, "ab/cd/ef") + #else + XCTAssertEqual(AbsolutePath(#"C:\"#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\ab\cd\ef\"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"C:\ab\cd\ef\\"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(RelativePath(#"ab\cd\ef\"#).pathString, #"ab\cd\ef\"#) + XCTAssertEqual(RelativePath(#"ab\cd\ef\\"#).pathString, #"ab\cd\ef\\"#) + let longAbsolutePathOverPathMax = generatePath(280) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\"#).pathString, #"\\?\"# + longAbsolutePathOverPathMax) + #endif } func testDotPathComponents() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/ab/././cd//ef").pathString, "/ab/cd/ef") XCTAssertEqual(AbsolutePath("/ab/./cd//ef/.").pathString, "/ab/cd/ef") XCTAssertEqual(RelativePath("ab/./cd/././ef").pathString, "ab/cd/ef") XCTAssertEqual(RelativePath("ab/./cd/ef/.").pathString, "ab/cd/ef") + #else + XCTAssertEqual(AbsolutePath(#"C:\ab\.\.\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"C:\ab\.\cd\\ef\."#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(RelativePath(#"ab\.\cd\.\.\ef"#).pathString, #"ab\.\cd\.\.\ef"#) + XCTAssertEqual(RelativePath(#"ab\.\cd\ef\."#).pathString, #"ab\.\cd\ef\."#) + let longAbsolutePathOverPathMax = generatePath(260) + let longAbsolutePathOverPathMaxWithDotComponents = longAbsolutePathOverPathMax + #"\.\foo\.\bar\"# + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMaxWithDotComponents).pathString, + #"\\?\"# + longAbsolutePathOverPathMax + #"\foo\bar"#) + #endif } func testDotDotPathComponents() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/..").pathString, "/") XCTAssertEqual(AbsolutePath("/../../../../..").pathString, "/") XCTAssertEqual(AbsolutePath("/abc/..").pathString, "/") @@ -85,9 +182,27 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("../abc/..").pathString, "..") XCTAssertEqual(RelativePath("../abc/.././").pathString, "..") XCTAssertEqual(RelativePath("abc/..").pathString, ".") + #else + XCTAssertEqual(AbsolutePath(#"C:\.."#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\..\..\..\..\.."#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\abc\.."#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\abc\..\.."#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\..\abc"#).pathString, #"C:\abc"#) + XCTAssertEqual(AbsolutePath(#"C:\..\abc\.."#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\..\abc\..\def"#).pathString, #"C:\def"#) + XCTAssertEqual(RelativePath(#".."#).pathString, #".."#) + XCTAssertEqual(RelativePath(#"..\.."#).pathString, #"..\.."#) + XCTAssertEqual(RelativePath(#"..\.\.."#).pathString, #"..\.\.."#) + XCTAssertEqual(RelativePath(#"..\abc\.."#).pathString, #"..\abc\.."#) + XCTAssertEqual(RelativePath(#"..\abc\..\.\"#).pathString, #"..\abc\..\.\"#) + XCTAssertEqual(RelativePath(#"abc\.."#).pathString, #"abc\.."#) + let longAbsolutePathOverPathMax = generatePath(280) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\abc\..\"#).pathString, #"\\?\"# + longAbsolutePathOverPathMax) + #endif } func testCombinationsAndEdgeCases() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("///").pathString, "/") XCTAssertEqual(AbsolutePath("/./").pathString, "/") XCTAssertEqual(RelativePath("").pathString, ".") @@ -114,9 +229,38 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("../a/..").pathString, "..") XCTAssertEqual(RelativePath("a/..").pathString, ".") XCTAssertEqual(RelativePath("a/../////../////./////").pathString, "..") + #else + XCTAssertEqual(AbsolutePath(#"C:\\\"#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\.\"#).pathString, #"C:\"#) + XCTAssertEqual(RelativePath("").pathString, ".") + XCTAssertEqual(RelativePath(".").pathString, ".") + XCTAssertEqual(RelativePath(#".\abc"#).pathString, #".\abc"#) + XCTAssertEqual(RelativePath(#".\abc\"#).pathString, #".\abc\"#) + XCTAssertEqual(RelativePath(#".\abc\..\bar"#).pathString, #".\abc\..\bar"#) + XCTAssertEqual(RelativePath(#"foo\..\bar"#).pathString, #"foo\..\bar"#) + XCTAssertEqual(RelativePath(#"foo\\\..\\\bar\\\baz"#).pathString, #"foo\\\..\\\bar\\\baz"#) + XCTAssertEqual(RelativePath(#"foo\..\bar\.\"#).pathString, #"foo\..\bar\.\"#) + XCTAssertEqual(RelativePath(#"..\abc\def\"#).pathString, #"..\abc\def\"#) + XCTAssertEqual(RelativePath(#".\.\.\.\."#).pathString, #".\.\.\.\."#) + XCTAssertEqual(RelativePath(#".\.\.\..\."#).pathString, #".\.\.\..\."#) + XCTAssertEqual(RelativePath(#".\"#).pathString, #".\"#) + XCTAssertEqual(RelativePath(#".\\"#).pathString, #".\\"#) + XCTAssertEqual(RelativePath(#".\."#).pathString, #".\."#) + XCTAssertEqual(RelativePath(#".\.\"#).pathString, #".\.\"#) + XCTAssertEqual(RelativePath(#"..\"#).pathString, #"..\"#) + XCTAssertEqual(RelativePath(#"..\."#).pathString, #"..\."#) + XCTAssertEqual(RelativePath(#".\.."#).pathString, #".\.."#) + XCTAssertEqual(RelativePath(#".\..\."#).pathString, #".\..\."#) + XCTAssertEqual(RelativePath(#".\\\\\..\\\\\.\\\\\"#).pathString, #".\\\\\..\\\\\.\\\\\"#) + XCTAssertEqual(RelativePath(#"..\a"#).pathString, #"..\a"#) + XCTAssertEqual(RelativePath(#"..\a\.."#).pathString, #"..\a\.."#) + XCTAssertEqual(RelativePath(#"a\.."#).pathString, #"a\.."#) + XCTAssertEqual(RelativePath(#"a\..\\\\\..\\\\\.\\\\\"#).pathString, #"a\..\\\\\..\\\\\.\\\\\"#) + #endif } func testDirectoryNameExtraction() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/").dirname, "/") XCTAssertEqual(AbsolutePath("/a").dirname, "/") XCTAssertEqual(AbsolutePath("/./a").dirname, "/") @@ -131,18 +275,21 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("abc").dirname, ".") XCTAssertEqual(RelativePath("").dirname, ".") XCTAssertEqual(RelativePath(".").dirname, ".") -#if os(Windows) - XCTAssertEqual(AbsolutePath("C:\\a\\b").dirname, "C:\\a") - XCTAssertEqual(AbsolutePath("C:\\").dirname, "C:\\") - XCTAssertEqual(AbsolutePath("C:\\\\").dirname, "C:\\") - XCTAssertEqual(AbsolutePath("C:\\\\\\").dirname, "C:\\") - XCTAssertEqual(AbsolutePath("C:\\a\\b\\").dirname, "C:\\a") - XCTAssertEqual(AbsolutePath("C:\\a\\b\\\\").dirname, "C:\\a") - XCTAssertEqual(AbsolutePath("C:\\a\\").dirname, "C:\\") -#endif + #else + XCTAssertEqual(AbsolutePath(#"C:\a\b"#).dirname, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).dirname, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\\"#).dirname, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\\\"#).dirname, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\a\b\"#).dirname, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"C:\a\b\\"#).dirname, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"C:\a\"#).dirname, #"C:\"#) + let longAbsolutePathOverPathMax = generatePath(280) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\a.txt"#).dirname, #"\\?\"# + longAbsolutePathOverPathMax) + #endif } func testBaseNameExtraction() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/").basename, "/") XCTAssertEqual(AbsolutePath("/a").basename, "a") XCTAssertEqual(AbsolutePath("/./a").basename, "a") @@ -156,9 +303,27 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("abc").basename, "abc") XCTAssertEqual(RelativePath("").basename, ".") XCTAssertEqual(RelativePath(".").basename, ".") + #else + XCTAssertEqual(AbsolutePath(#"C:\"#).basename, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\a"#).basename, #"a"#) + XCTAssertEqual(AbsolutePath(#"C:\.\a"#).basename, #"a"#) + XCTAssertEqual(AbsolutePath(#"C:\..\.."#).basename, #"C:\"#) + XCTAssertEqual(RelativePath(#"..\.."#).basename, #".."#) + XCTAssertEqual(RelativePath(#"..\a"#).basename, #"a"#) + XCTAssertEqual(RelativePath(#"..\a\.."#).basename, #".."#) + XCTAssertEqual(RelativePath(#"a\.."#).basename, #".."#) + XCTAssertEqual(RelativePath(#".\.."#).basename, #".."#) + XCTAssertEqual(RelativePath(#"a\..\\\\..\\\\.\\\\"#).basename, #".\\\\"#) + XCTAssertEqual(RelativePath(#"abc"#).basename, #"abc"#) + XCTAssertEqual(RelativePath(#""#).basename, #"."#) + XCTAssertEqual(RelativePath(#"."#).basename, #"."#) + let longAbsolutePathOverPathMax = generatePath(280) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\a.txt"#).basename, #"a.txt"#) + #endif } func testBaseNameWithoutExt() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/").basenameWithoutExt, "/") XCTAssertEqual(AbsolutePath("/a").basenameWithoutExt, "a") XCTAssertEqual(AbsolutePath("/./a").basenameWithoutExt, "a") @@ -179,6 +344,31 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("abc.swift").basenameWithoutExt, "abc") XCTAssertEqual(RelativePath("../a.b.c").basenameWithoutExt, "a.b") XCTAssertEqual(RelativePath("abc.xyz.123").basenameWithoutExt, "abc.xyz") + #else + XCTAssertEqual(AbsolutePath(#"C:\"#).basenameWithoutExt, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\a"#).basenameWithoutExt, #"a"#) + XCTAssertEqual(AbsolutePath(#"C:\.\a"#).basenameWithoutExt, #"a"#) + XCTAssertEqual(AbsolutePath(#"C:\..\.."#).basenameWithoutExt, #"C:\"#) + XCTAssertEqual(RelativePath(#"..\.."#).basenameWithoutExt, #".."#) + XCTAssertEqual(RelativePath(#"..\a"#).basenameWithoutExt, #"a"#) + XCTAssertEqual(RelativePath(#"..\a\.."#).basenameWithoutExt, #".."#) + XCTAssertEqual(RelativePath(#"a\.."#).basenameWithoutExt, #".."#) + XCTAssertEqual(RelativePath(#".\.."#).basenameWithoutExt, #".."#) + XCTAssertEqual(RelativePath(#"a\..\\\\..\\\\.\\\\"#).basenameWithoutExt, #".\\\\"#) + XCTAssertEqual(RelativePath(#"abc"#).basenameWithoutExt, #"abc"#) + XCTAssertEqual(RelativePath(#""#).basenameWithoutExt, #"."#) + XCTAssertEqual(RelativePath(#"."#).basenameWithoutExt, #"."#) + + XCTAssertEqual(AbsolutePath(#"C:\a.txt"#).basenameWithoutExt, #"a"#) + XCTAssertEqual(AbsolutePath(#"C:\.\a.txt"#).basenameWithoutExt, #"a"#) + XCTAssertEqual(RelativePath(#"..\a.bc"#).basenameWithoutExt, #"a"#) + XCTAssertEqual(RelativePath(#"abc.swift"#).basenameWithoutExt, #"abc"#) + XCTAssertEqual(RelativePath(#"..\a.b.c"#).basenameWithoutExt, #"a.b"#) + XCTAssertEqual(RelativePath(#"abc.xyz.123"#).basenameWithoutExt, #"abc.xyz"#) + + let longAbsolutePathOverPathMax = generatePath(280) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\a.txt"#).basenameWithoutExt, #"a"#) + #endif } func testSuffixExtraction() { @@ -207,15 +397,26 @@ class PathTests: XCTestCase { } func testParentDirectory() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/").parentDirectory, AbsolutePath("/")) XCTAssertEqual(AbsolutePath("/").parentDirectory.parentDirectory, AbsolutePath("/")) XCTAssertEqual(AbsolutePath("/bar").parentDirectory, AbsolutePath("/")) XCTAssertEqual(AbsolutePath("/bar/../foo/..//").parentDirectory.parentDirectory, AbsolutePath("/")) XCTAssertEqual(AbsolutePath("/bar/../foo/..//yabba/a/b").parentDirectory.parentDirectory, AbsolutePath("/yabba")) + #else + XCTAssertEqual(AbsolutePath(#"C:\"#).parentDirectory, AbsolutePath(#"C:\"#)) + XCTAssertEqual(AbsolutePath(#"C:\"#).parentDirectory.parentDirectory, AbsolutePath(#"C:\"#)) + XCTAssertEqual(AbsolutePath(#"C:\bar"#).parentDirectory, AbsolutePath(#"C:\"#)) + XCTAssertEqual(AbsolutePath(#"C:\bar\..\foo\..\\"#).parentDirectory.parentDirectory, AbsolutePath(#"C:\"#)) + XCTAssertEqual(AbsolutePath(#"C:\bar\..\foo\..\\yabba\a\b"#).parentDirectory.parentDirectory, AbsolutePath(#"C:\yabba"#)) + let longAbsolutePathOverPathMax = generatePath(280) + XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax).parentDirectory, AbsolutePath(longAbsolutePathOverPathMax.replacingOccurrences(of: #"\95"#, with: ""))) + #endif } @available(*, deprecated) func testConcatenation() { + #if !os(Windows) XCTAssertEqual(AbsolutePath(AbsolutePath("/"), RelativePath("")).pathString, "/") XCTAssertEqual(AbsolutePath(AbsolutePath("/"), RelativePath(".")).pathString, "/") XCTAssertEqual(AbsolutePath(AbsolutePath("/"), RelativePath("..")).pathString, "/") @@ -250,9 +451,53 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("hello").appending(components: "a", "b", "c", "..").pathString, "hello/a/b") XCTAssertEqual(RelativePath("hello").appending(RelativePath("a/b/../c/d")).pathString, "hello/a/c/d") + #else + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\"#), RelativePath("")).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\"#), RelativePath(".")).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\"#), RelativePath("..")).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\"#), RelativePath("bar")).pathString, #"C:\bar"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\foo\bar"#), RelativePath("..")).pathString, #"C:\foo"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\bar"#), RelativePath(#"..\foo"#)).pathString, #"C:\foo"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\bar"#), RelativePath(#"..\foo\..\\"#)).pathString, #"C:\\"#) + XCTAssertEqual(AbsolutePath(AbsolutePath(#"C:\bar\..\foo\..\\yabba\"#), RelativePath("a/b")).pathString, #"C:\yabba\a\b"#) + + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(RelativePath("")).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(RelativePath(".")).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(RelativePath("..")).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(RelativePath("bar")).pathString, #"C:\bar"#) + XCTAssertEqual(AbsolutePath(#"C:\foo\bar"#).appending(RelativePath("..")).pathString, #"C:\foo"#) + XCTAssertEqual(AbsolutePath(#"C:\bar"#).appending(RelativePath(#"..\foo"#)).pathString, #"C:\foo"#) + XCTAssertEqual(AbsolutePath(#"C:\bar"#).appending(RelativePath(#"..\foo\..\\"#)).pathString, #"C:\\"#) + XCTAssertEqual(AbsolutePath(#"C:\bar\..\foo\..\\yabba\"#).appending(RelativePath(#"a\b"#)).pathString, #"C:\yabba\a\b"#) + + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(component: "a").pathString, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"C:\a"#).appending(component: "b").pathString, #"C:\a\b"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(components: "a", "b").pathString, #"C:\a\b"#) + XCTAssertEqual(AbsolutePath(#"C:\a"#).appending(components: "b", "c").pathString, #"C:\a\b\c"#) + + XCTAssertEqual(AbsolutePath(#"C:\a\b\c"#).appending(components: "", "c").pathString, #"C:\a\b\c\c"#) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c"#).appending(components: "").pathString, #"C:\a\b\c"#) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c"#).appending(components: ".").pathString, #"C:\a\b\c"#) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c"#).appending(components: "..").pathString, #"C:\a\b"#) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c"#).appending(components: "..", "d").pathString, #"C:\a\b\d"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(components: "..").pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(components: ".").pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"C:\"#).appending(components: "..", "a").pathString, #"C:\a"#) + + XCTAssertEqual(RelativePath("hello").appending(components: "a", "b", "c", "..").pathString, #"hello\a\b"#) + XCTAssertEqual(RelativePath("hello").appending(RelativePath(#"a\b\..\c\d"#)).pathString, #"hello\a\c\d"#) + + var longAbsolutePathUnderPathMax = generatePath(255) + XCTAssertEqual(AbsolutePath(longAbsolutePathUnderPathMax).appending(components: "a", "b", "c", "d", "e").pathString, + #"\\?\"# + longAbsolutePathUnderPathMax + #"\a\b\c\d\e"#) + longAbsolutePathUnderPathMax = generatePath(255) + XCTAssertEqual(AbsolutePath(longAbsolutePathUnderPathMax).appending(RelativePath(#"a\b\..\c\d"#)).pathString, + #"\\?\"# + longAbsolutePathUnderPathMax + #"\a\c\d"#) + #endif } func testPathComponents() { + #if !os(Windows) XCTAssertEqual(AbsolutePath("/").components, ["/"]) XCTAssertEqual(AbsolutePath("/.").components, ["/"]) XCTAssertEqual(AbsolutePath("/..").components, ["/"]) @@ -278,28 +523,89 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath("./..").components, [".."]) XCTAssertEqual(RelativePath("a/../////../////./////").components, [".."]) XCTAssertEqual(RelativePath("abc").components, ["abc"]) + #else + XCTAssertEqual(AbsolutePath(#"C:\"#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"C:\."#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"C:\.."#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"C:\bar"#).components, ["C:", "bar"]) + XCTAssertEqual(AbsolutePath(#"C:\foo/bar/.."#).components, ["C:", "foo"]) + XCTAssertEqual(AbsolutePath(#"C:\bar/../foo"#).components, ["C:", "foo"]) + XCTAssertEqual(AbsolutePath(#"C:\bar/../foo/..//"#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"C:\bar/../foo/..//yabba/a/b/"#).components, ["C:", "yabba", "a", "b"]) + + XCTAssertEqual(RelativePath(#""#).components, ["."]) + XCTAssertEqual(RelativePath(#"."#).components, ["."]) + XCTAssertEqual(RelativePath(#".."#).components, [".."]) + XCTAssertEqual(RelativePath(#"bar"#).components, ["bar"]) + XCTAssertEqual(RelativePath(#"foo/bar/.."#).components, ["foo", "bar", ".."]) + XCTAssertEqual(RelativePath(#"bar/../foo"#).components, ["bar", "..", "foo"]) + XCTAssertEqual(RelativePath(#"bar/../foo/..//"#).components, ["bar", "..", "foo", ".."]) + XCTAssertEqual(RelativePath(#"bar/../foo/..//yabba/a/b/"#).components, ["bar", "..", "foo", "..", "yabba", "a", "b"]) + XCTAssertEqual(RelativePath(#"../.."#).components, ["..", ".."]) + XCTAssertEqual(RelativePath(#".././/.."#).components, ["..", ".", ".."]) + XCTAssertEqual(RelativePath(#"../a"#).components, ["..", "a"]) + XCTAssertEqual(RelativePath(#"../a/.."#).components, ["..", "a", ".."]) + XCTAssertEqual(RelativePath(#"a/.."#).components, ["a", ".."]) + XCTAssertEqual(RelativePath(#"./.."#).components, [".", ".."]) + XCTAssertEqual(RelativePath(#"a/../////../////./////"#).components, ["a", "..", "..", "."]) + XCTAssertEqual(RelativePath(#"abc"#).components, ["abc"]) + #endif } func testRelativePathFromAbsolutePaths() { - XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/")), RelativePath(".")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/")), RelativePath("a/b/c/d")); - XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/a/b/c")), RelativePath("../../..")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b")), RelativePath("c/d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b/c")), RelativePath("d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/c/d")), RelativePath("../../b/c/d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/b/c/d")), RelativePath("../../../a/b/c/d")); + #if !os(Windows) + XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/")), RelativePath(".")) + XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/")), RelativePath("a/b/c/d")) + XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/a/b/c")), RelativePath("../../..")) + XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b")), RelativePath("c/d")) + XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b/c")), RelativePath("d")) + XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/c/d")), RelativePath("../../b/c/d")) + XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/b/c/d")), RelativePath("../../../a/b/c/d")) + #else + XCTAssertEqual(AbsolutePath(#"C:\"#).relative(to: AbsolutePath(#"C:\"#)), RelativePath(".")) + XCTAssertEqual(AbsolutePath(#"C:\a/b/c/d"#).relative(to: AbsolutePath(#"C:\"#)), RelativePath(#"a\b\c\d"#)) + XCTAssertEqual(AbsolutePath(#"C:\"#).relative(to: AbsolutePath(#"C:\a\b\c"#)), RelativePath(#"..\..\.."#)) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\b"#)), RelativePath(#"c\d"#)) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\b\c"#)), RelativePath(#"d"#)) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\c\d"#)), RelativePath(#"..\..\b\c\d"#)) + XCTAssertEqual(AbsolutePath(#"C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\b\c\d"#)), RelativePath(#"..\..\..\a\b\c\d"#)) + + var longAbsolutePathOverPathMax = generatePath(264) + XCTAssertEqual( + AbsolutePath(longAbsolutePathOverPathMax).relative(to: AbsolutePath(longAbsolutePathOverPathMax.replacingOccurrences(of: #"\85\86\87\88\89\90"#, with: ""))), + RelativePath(#"85\86\87\88\89\90"#) + ) + + #endif } func testComparison() { - XCTAssertTrue(AbsolutePath("/") <= AbsolutePath("/")); - XCTAssertTrue(AbsolutePath("/abc") < AbsolutePath("/def")); - XCTAssertTrue(AbsolutePath("/2") <= AbsolutePath("/2.1")); - XCTAssertTrue(AbsolutePath("/3.1") > AbsolutePath("/2")); - XCTAssertTrue(AbsolutePath("/2") >= AbsolutePath("/2")); - XCTAssertTrue(AbsolutePath("/2.1") >= AbsolutePath("/2")); + #if !os(Windows) + XCTAssertTrue(AbsolutePath("/") <= AbsolutePath("/")) + XCTAssertTrue(AbsolutePath("/abc") < AbsolutePath("/def")) + XCTAssertTrue(AbsolutePath("/2") <= AbsolutePath("/2.1")) + XCTAssertTrue(AbsolutePath("/3.1") > AbsolutePath("/2")) + XCTAssertTrue(AbsolutePath("/2") >= AbsolutePath("/2")) + XCTAssertTrue(AbsolutePath("/2.1") >= AbsolutePath("/2")) + #else + XCTAssertTrue(AbsolutePath(#"C:\"#) <= AbsolutePath(#"C:\"#)) + XCTAssertTrue(AbsolutePath(#"C:\abc"#) < AbsolutePath(#"C:\def"#)) + XCTAssertTrue(AbsolutePath(#"C:\2"#) <= AbsolutePath(#"C:\2.1"#)) + XCTAssertTrue(AbsolutePath(#"C:\3.1"#) > AbsolutePath(#"C:\2"#)) + XCTAssertTrue(AbsolutePath(#"C:\2"#) >= AbsolutePath(#"C:\2"#)) + XCTAssertTrue(AbsolutePath(#"C:\2.1"#) >= AbsolutePath(#"C:\2"#)) + + var longAbsolutePathOverPathMax = generatePath(260) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\abc"#) < AbsolutePath(longAbsolutePathOverPathMax + #"\def"#)) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\2"#) <= AbsolutePath(longAbsolutePathOverPathMax + #"\2.1"#)) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\3.1"#) > AbsolutePath(longAbsolutePathOverPathMax + #"\2"#)) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\2"#) >= AbsolutePath(longAbsolutePathOverPathMax + #"\2"#)) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\2.1"#) >= AbsolutePath(longAbsolutePathOverPathMax + #"\2"#)) + #endif } func testAncestry() { + #if !os(Windows) XCTAssertTrue(AbsolutePath("/a/b/c/d/e/f").isDescendantOfOrEqual(to: AbsolutePath("/a/b/c/d"))) XCTAssertTrue(AbsolutePath("/a/b/c/d/e/f.swift").isDescendantOfOrEqual(to: AbsolutePath("/a/b/c"))) XCTAssertTrue(AbsolutePath("/").isDescendantOfOrEqual(to: AbsolutePath("/"))) @@ -319,9 +625,50 @@ class PathTests: XCTestCase { XCTAssertFalse(AbsolutePath("/foo/bar").isAncestor(of: AbsolutePath("/foo/bar"))) XCTAssertTrue(AbsolutePath("/foo").isAncestor(of: AbsolutePath("/foo/bar"))) + #else + XCTAssertTrue(AbsolutePath(#"C:\a\b\c\d\e\f"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d"#))) + XCTAssertTrue(AbsolutePath(#"C:\a\b\c\d\e\f.swift"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\a\b\c"#))) + XCTAssertTrue(AbsolutePath(#"C:\"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\"#))) + XCTAssertTrue(AbsolutePath(#"C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\"#))) + XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\foo\bar\baz"#))) + XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\bar"#))) + + XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isDescendant(of: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertTrue(AbsolutePath(#"C:\foo\bar"#).isDescendant(of: AbsolutePath(#"C:\foo"#))) + + XCTAssertTrue(AbsolutePath(#"C:\a\b\c\d"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d\e\f"#))) + XCTAssertTrue(AbsolutePath(#"C:\a\b\c"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d\e\f.swift"#))) + XCTAssertTrue(AbsolutePath(#"C:\"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\"#))) + XCTAssertTrue(AbsolutePath(#"C:\"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertFalse(AbsolutePath(#"C:\foo\bar\baz"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertFalse(AbsolutePath(#"C:\bar"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + + XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isAncestor(of: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertTrue(AbsolutePath(#"C:\foo"#).isAncestor(of: AbsolutePath(#"C:\foo\bar"#))) + + // Long/Long Ancestry + let longAbsolutePathOverPathMax = generatePath(265) + let longerAbsolutePathOverPathMax = generatePath(300) + XCTAssertTrue(AbsolutePath(longerAbsolutePathOverPathMax).isDescendant(of: AbsolutePath(longAbsolutePathOverPathMax))) + XCTAssertTrue(AbsolutePath(longerAbsolutePathOverPathMax).isDescendantOfOrEqual(to: AbsolutePath(longAbsolutePathOverPathMax))) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax).isAncestorOfOrEqual(to: AbsolutePath(longerAbsolutePathOverPathMax))) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax).isAncestor(of: AbsolutePath(longerAbsolutePathOverPathMax))) + + XCTAssertFalse(AbsolutePath(longerAbsolutePathOverPathMax).isAncestor(of: AbsolutePath(longAbsolutePathOverPathMax))) + XCTAssertFalse(AbsolutePath(longAbsolutePathOverPathMax).isAncestor(of: AbsolutePath(longAbsolutePathOverPathMax))) + XCTAssertFalse(AbsolutePath(longAbsolutePathOverPathMax + #"\baz"#).isAncestorOfOrEqual(to: AbsolutePath(longAbsolutePathOverPathMax))) + + // Long/Short to Long Ancestry + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax).isDescendant(of: AbsolutePath(#"C:\0\1\2"#))) + XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax).isDescendantOfOrEqual(to: AbsolutePath(#"C:\0\1\2"#))) + XCTAssertTrue(AbsolutePath(#"C:\0\1\2"#).isAncestorOfOrEqual(to: AbsolutePath(longAbsolutePathOverPathMax))) + XCTAssertTrue(AbsolutePath(#"C:\0\1\2"#).isAncestor(of: AbsolutePath(longAbsolutePathOverPathMax))) + + #endif } func testAbsolutePathValidation() { + #if !os(Windows) XCTAssertNoThrow(try AbsolutePath(validating: "/a/b/c/d")) XCTAssertThrowsError(try AbsolutePath(validating: "~/a/b/d")) { error in @@ -331,19 +678,43 @@ class PathTests: XCTestCase { XCTAssertThrowsError(try AbsolutePath(validating: "a/b/d")) { error in XCTAssertEqual("\(error)", "invalid absolute path 'a/b/d'") } + #else + XCTAssertNoThrow(try AbsolutePath(validating: #"C:\a\b\c\d"#)) + + XCTAssertThrowsError(try AbsolutePath(validating: #"~\a\b\d"#)) { error in + XCTAssertEqual("\(error)", #"invalid absolute path '~\a\b\d'"#) + } + + XCTAssertThrowsError(try AbsolutePath(validating: #"a\b\d"#)) { error in + XCTAssertEqual("\(error)", #"invalid absolute path 'a\b\d'"#) + } + + let relativeLongPath = generatePath(265, absolute: false) + XCTAssertThrowsError(try AbsolutePath(validating: relativeLongPath)) { error in + XCTAssertEqual("\(error)", "invalid absolute path '\(relativeLongPath)'") + } + #endif } func testRelativePathValidation() { + #if !os(Windows) XCTAssertNoThrow(try RelativePath(validating: "a/b/c/d")) XCTAssertThrowsError(try RelativePath(validating: "/a/b/d")) { error in XCTAssertEqual("\(error)", "invalid relative path '/a/b/d'; relative path should not begin with '/'") - //XCTAssertEqual("\(error)", "invalid relative path '/a/b/d'; relative path should not begin with '/' or '~'") } + #else + XCTAssertNoThrow(try RelativePath(validating: #"a\b\c\d"#)) - /*XCTAssertThrowsError(try RelativePath(validating: "~/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid relative path '~/a/b/d'; relative path should not begin with '/' or '~'") - }*/ + XCTAssertThrowsError(try RelativePath(validating: #"\a\b\d"#)) { error in + XCTAssertEqual("\(error)", #"invalid relative path '\a\b\d'; relative path should not begin with '\'"#) + } + + let absoluteLongPath = generatePath(265, absolute: true) + XCTAssertThrowsError(try RelativePath(validating: absoluteLongPath)) { error in + XCTAssertEqual("\(error)", "invalid relative path '\(absoluteLongPath)'; relative path should not begin with '\\'") + } + #endif } func testCodable() throws { @@ -359,36 +730,42 @@ class PathTests: XCTestCase { var path: String } + #if os(Windows) + let isWindowsOS = true + #else + let isWindowsOS = false + #endif + do { - let foo = Foo(path: "/path/to/foo") + let foo = Foo(path: !isWindowsOS ? "/path/to/foo" : #"\path\to\foo"#) let data = try JSONEncoder().encode(foo) let decodedFoo = try JSONDecoder().decode(Foo.self, from: data) XCTAssertEqual(foo, decodedFoo) } do { - let foo = Foo(path: "/path/to/../to/foo") + let foo = Foo(path: !isWindowsOS ? "/path/to/../to/foo" : #"C:\path\to\..\to\foo"#) let data = try JSONEncoder().encode(foo) let decodedFoo = try JSONDecoder().decode(Foo.self, from: data) XCTAssertEqual(foo, decodedFoo) - XCTAssertEqual(foo.path.pathString, "/path/to/foo") - XCTAssertEqual(decodedFoo.path.pathString, "/path/to/foo") + XCTAssertEqual(foo.path.pathString, !isWindowsOS ? "/path/to/foo" : #"C:\path\to\foo"#) + XCTAssertEqual(decodedFoo.path.pathString, !isWindowsOS ? "/path/to/foo" : #"C:\path\to\foo"#) } do { - let bar = Bar(path: "path/to/bar") + let bar = Bar(path: !isWindowsOS ? "path/to/bar" : #"path/to/bar"#) let data = try JSONEncoder().encode(bar) let decodedBar = try JSONDecoder().decode(Bar.self, from: data) XCTAssertEqual(bar, decodedBar) } do { - let bar = Bar(path: "path/to/../to/bar") + let bar = Bar(path: !isWindowsOS ? "path/to/../to/bar" : #"path\to\..\to\bar"#) let data = try JSONEncoder().encode(bar) let decodedBar = try JSONDecoder().decode(Bar.self, from: data) XCTAssertEqual(bar, decodedBar) - XCTAssertEqual(bar.path.pathString, "path/to/bar") - XCTAssertEqual(decodedBar.path.pathString, "path/to/bar") + XCTAssertEqual(bar.path.pathString, !isWindowsOS ? "path/to/bar" : #"path\to\..\to\bar"#) + XCTAssertEqual(decodedBar.path.pathString, !isWindowsOS ? "path/to/bar" : #"path\to\..\to\bar"#) } do { @@ -403,29 +780,31 @@ class PathTests: XCTestCase { } do { - let data = try JSONEncoder().encode(Baz(path: "/foo")) + let data = try JSONEncoder().encode(Baz(path: !isWindowsOS ? "/foo" : #"C:\foo"#)) XCTAssertThrowsError(try JSONDecoder().decode(Bar.self, from: data)) } } - // FIXME: We also need tests for join() operations. - - // FIXME: We also need tests for dirname, basename, suffix, etc. - - // FIXME: We also need test for stat() operations. - #if os(Windows) - func testNormalization() { + func testDiskDesignatorNormalization() { XCTAssertEqual( - AbsolutePath(#"C:\Users\compnerd\AppData\Local\Programs\Swift\Toolchains\0.0.0+Asserts\usr\bin\swiftc.exe"#) - .pathString, + AbsolutePath(#"C:\Users\compnerd\AppData\Local\Programs\Swift\Toolchains\0.0.0+Asserts\usr\bin\swiftc.exe"#).pathString, #"C:\Users\compnerd\AppData\Local\Programs\Swift\Toolchains\0.0.0+Asserts\usr\bin\swiftc.exe"# ) XCTAssertEqual( - AbsolutePath(#"c:\Users\compnerd\AppData\Local\Programs\Swift\Toolchains\0.0.0+Asserts\usr\bin\swiftc.exe"#) - .pathString, + AbsolutePath(#"c:\Users\compnerd\AppData\Local\Programs\Swift\Toolchains\0.0.0+Asserts\usr\bin\swiftc.exe"#).pathString, #"C:\Users\compnerd\AppData\Local\Programs\Swift\Toolchains\0.0.0+Asserts\usr\bin\swiftc.exe"# ) + + let absoluteLongPath = generatePath(265) + XCTAssertEqual( + AbsolutePath(absoluteLongPath).pathString, + #"\\?\"# + absoluteLongPath + ) + XCTAssertEqual( + AbsolutePath(absoluteLongPath.replacingOccurrences(of: "C:", with: "c:")).pathString, + #"\\?\"# + absoluteLongPath + ) } #endif } From 7de5288ec792573db02de92ecc960c9c5848befd Mon Sep 17 00:00:00 2001 From: Kristopher Cieplak Date: Tue, 13 May 2025 13:39:38 -0400 Subject: [PATCH 2/2] Make PathCchStripPrefix and PathCchRemoveBackslash use temporary buffer. - Move PathCchStripPrefix and PathCchRemoveBackslash to use a mutable temporary buffer. - Could not use buffer.withMemoryRebound and getCstring() as on Windows this seem to produce corrupt data. - Add more tests for unParsed '\\?\' and device '\\.\' paths - Remove the PATHCCH_CANONICALIZE_SLASHES flag as it is not needed. - Add Win32Error.swift to CMakeLists --- Sources/TSCBasic/CMakeLists.txt | 3 +- Sources/TSCBasic/Path.swift | 167 +++++++++++++------------ Tests/TSCBasicTests/PathTests.swift | 185 +++++++++++++++++++++++++++- 3 files changed, 273 insertions(+), 82 deletions(-) diff --git a/Sources/TSCBasic/CMakeLists.txt b/Sources/TSCBasic/CMakeLists.txt index d8b85eaa..2fb5a8a3 100644 --- a/Sources/TSCBasic/CMakeLists.txt +++ b/Sources/TSCBasic/CMakeLists.txt @@ -49,7 +49,8 @@ add_library(TSCBasic TerminalController.swift Thread.swift Tuple.swift - misc.swift) + misc.swift + Win32Error.swift) target_compile_options(TSCBasic PUBLIC # Ignore secure function warnings on Windows. diff --git a/Sources/TSCBasic/Path.swift b/Sources/TSCBasic/Path.swift index c9caf01c..70020951 100644 --- a/Sources/TSCBasic/Path.swift +++ b/Sources/TSCBasic/Path.swift @@ -458,79 +458,6 @@ private struct WindowsPath: Path, Sendable { return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) } - /// When this function returns successfully, the same path string will have had the prefix removed, - /// if the prefix was present. If no prefix was present, the string will be unchanged. - static func stripPrefix(_ path: String) -> String { - return path.withCString(encodedAs: UTF16.self) { cStringPtr in - let mutableCStringPtr = UnsafeMutablePointer(mutating: cStringPtr) - let result = PathCchStripPrefix(mutableCStringPtr, path.utf16.count + 1) - if result == S_OK { - return String(decodingCString: mutableCStringPtr, as: UTF16.self) - } - return path - } - } - - /// Remove a trailing backslash from a path if the following conditions - /// are true: - /// * Path is not a root path - /// * Pash has a trailing backslash - /// If conditions are not met then the string is returned unchanged. - static func removeTrailingBackslash(_ path: String) -> String { - return path.withCString(encodedAs: UTF16.self) { cStringPtr in - let mutableCStringPtr = UnsafeMutablePointer(mutating: cStringPtr) - let result = PathCchRemoveBackslash(mutableCStringPtr, path.utf16.count + 1) - - if result == S_OK { - return String(decodingCString: mutableCStringPtr, as: UTF16.self) - } - return path - } - } - - /// Create a canonicalized path representation for Windows. - /// Returns a potentially `\\?\`-prefixed version of the path, - /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. - /// - /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation - static func canonicalPathRepresentation(_ path: String) throws -> String { - return try path.withCString(encodedAs: UTF16.self) { pwszPlatformPath in - // 1. Normalize the path first. - // Contrary to the documentation, this works on long paths independently - // of the registry or process setting to enable long paths (but it will also - // not add the \\?\ prefix required by other functions under these conditions). - let dwLength: DWORD = GetFullPathNameW(pwszPlatformPath, 0, nil, nil) - - return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in - guard (1 ..< dwLength).contains(GetFullPathNameW(pwszPlatformPath, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else { - throw Win32Error(GetLastError()) - } - // 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these. - if let base = pwszFullPath.baseAddress, - base[0] == UInt8(ascii: "\\"), - base[1] == UInt8(ascii: "\\"), - base[2] == UInt8(ascii: "."), - base[3] == UInt8(ascii: "\\") - { - return String(decodingCString: base, as: UTF16.self) - } - // 2. Canonicalize the path. - // This will add the \\?\ prefix if needed based on the path's length. - var pwszCanonicalPath: LPWSTR? - let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue) | numericCast(PATHCCH_CANONICALIZE_SLASHES.rawValue) - let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath) - if let pwszCanonicalPath { - defer { LocalFree(pwszCanonicalPath) } - if result == S_OK { - // 3. Perform the operation on the normalized path. - return String(decodingCString: pwszCanonicalPath, as: UTF16.self) - } - } - throw Win32Error(WIN32_FROM_HRESULT(result)) - } - } - } - var dirname: String { let fsr: UnsafePointer = self.string.fileSystemRepresentation defer { fsr.deallocate() } @@ -581,7 +508,12 @@ private struct WindowsPath: Path, Sendable { let normalized: UnsafePointer = string.fileSystemRepresentation defer { normalized.deallocate() } // Remove prefix from the components, allowing for comparison across normalized paths. - return Self.stripPrefix(String(cString: normalized)).components(separatedBy: #"\"#).filter { !$0.isEmpty } + var prefixStrippedPath = PathCchStripPrefix(String(cString: normalized)) + // The '\\.\'' prefix is not removed by PathCchStripPrefix do this manually. + if prefixStrippedPath.starts(with: #"\\.\"#) { + prefixStrippedPath = String(prefixStrippedPath.dropFirst(4)) + } + return prefixStrippedPath.components(separatedBy: #"\"#).filter { !$0.isEmpty } } var parentDirectory: Self { @@ -589,14 +521,14 @@ private struct WindowsPath: Path, Sendable { } init(string: String) { - let noPrefixPath = Self.stripPrefix(string) + let noPrefixPath = PathCchStripPrefix(string) let prefix = string.replacingOccurrences(of: noPrefixPath, with: "") // Just the prefix or empty // Perform drive designator normalization i.e. 'c:\' to 'C:\' on string. if noPrefixPath.first?.isASCII ?? false, noPrefixPath.first?.isLetter ?? false, noPrefixPath.first?.isLowercase ?? false, noPrefixPath.count > 1, noPrefixPath[noPrefixPath.index(noPrefixPath.startIndex, offsetBy: 1)] == ":" { - self.string = prefix + "\(noPrefixPath.first!.uppercased())\(noPrefixPath.dropFirst(1))" + self.string = "\(prefix)\(noPrefixPath.first!.uppercased())\(noPrefixPath.dropFirst(1))" } else { self.string = prefix + noPrefixPath } @@ -615,8 +547,8 @@ private struct WindowsPath: Path, Sendable { throw PathValidationError.invalidAbsolutePath(path) } do { - let canonicalizedPath = try Self.canonicalPathRepresentation(realpath) - let normalizedPath = Self.removeTrailingBackslash(canonicalizedPath) // AbsolutePath states paths have no trailing separator. + let canonicalizedPath = try canonicalPathRepresentation(realpath) + let normalizedPath = PathCchRemoveBackslash(canonicalizedPath) // AbsolutePath states paths have no trailing separator. self.init(string: normalizedPath) } catch { throw PathValidationError.invalidAbsolutePath("\(path): \(error)") @@ -703,6 +635,85 @@ fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD { return DWORD(hr) } +/// Create a canonicalized path representation for Windows. +/// Returns a potentially `\\?\`-prefixed version of the path, +/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. +/// +/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +fileprivate func canonicalPathRepresentation(_ path: String) throws -> String { + return try path.withCString(encodedAs: UTF16.self) { pwszPlatformPath in + // 1. Normalize the path first. + // Contrary to the documentation, this works on long paths independently + // of the registry or process setting to enable long paths (but it will also + // not add the \\?\ prefix required by other functions under these conditions). + let dwLength: DWORD = GetFullPathNameW(pwszPlatformPath, 0, nil, nil) + + return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in + guard (1 ..< dwLength).contains(GetFullPathNameW(pwszPlatformPath, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else { + throw Win32Error(GetLastError()) + } + // 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these. + if pwszFullPath.count >= 4 { + if let base = pwszFullPath.baseAddress, + base[0] == UInt8(ascii: "\\"), + base[1] == UInt8(ascii: "\\"), + base[2] == UInt8(ascii: "."), + base[3] == UInt8(ascii: "\\") + { + return String(decodingCString: base, as: UTF16.self) + } + } + // 2. Canonicalize the path. + // This will add the \\?\ prefix if needed based on the path's length. + var pwszCanonicalPath: LPWSTR? + let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue) + let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath) + if let pwszCanonicalPath { + defer { LocalFree(pwszCanonicalPath) } + if result == S_OK { + // 3. Perform the operation on the normalized path. + return String(decodingCString: pwszCanonicalPath, as: UTF16.self) + } + } + throw Win32Error(WIN32_FROM_HRESULT(result)) + } + } +} + +/// Removes the "\\?\" prefix, if present, from a file path. When this function returns successfully, +/// the same path string will have the prefix removed,if the prefix was present. +/// If no prefix was present,the string will be unchanged. +fileprivate func PathCchStripPrefix(_ path: String) -> String { + return path.withCString(encodedAs: UTF16.self) { cStringPtr in + withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: path.utf16.count + 1) { buffer in + buffer.initialize(from: UnsafeBufferPointer(start: cStringPtr, count: path.utf16.count + 1)) + let result = PathCchStripPrefix(buffer.baseAddress!, buffer.count) + if result == S_OK { + return String(decodingCString: buffer.baseAddress!, as: UTF16.self) + } + return path + } + } +} + +/// Remove a trailing backslash from a path if the following conditions +/// are true: +/// * Path is not a root path +/// * Pash has a trailing backslash +/// If conditions are not met then the string is returned unchanged. +fileprivate func PathCchRemoveBackslash(_ path: String) -> String { + return path.withCString(encodedAs: UTF16.self) { cStringPtr in + return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: path.utf16.count + 1) { buffer in + buffer.initialize(from: UnsafeBufferPointer(start: cStringPtr, count: path.utf16.count + 1)) + let result = PathCchRemoveBackslash(buffer.baseAddress!, path.utf16.count + 1) + if result == S_OK { + return String(decodingCString: buffer.baseAddress!, as: UTF16.self) + } + return path + } + return path + } +} #else private struct UNIXPath: Path, Sendable { let string: String diff --git a/Tests/TSCBasicTests/PathTests.swift b/Tests/TSCBasicTests/PathTests.swift index 1c3ba631..25c5e73c 100644 --- a/Tests/TSCBasicTests/PathTests.swift +++ b/Tests/TSCBasicTests/PathTests.swift @@ -13,18 +13,25 @@ import TSCBasic import TSCTestSupport import XCTest -func generatePath(_ length: Int, absolute: Bool = true, duplicateSeparators: Bool = false) -> String { +func generatePath(_ length: Int, absolute: Bool = true, duplicateSeparators: Bool = false, useUnparsedPrefix: Bool=false, useDevicePrefix: Bool=false) -> String { #if !os(Windows) var path = absolute ? "/" : "" let separator = duplicateSeparators ? "//" : "/" #else var path = absolute ? #"C:"# : "" + if useUnparsedPrefix && !useDevicePrefix { + path = #"\\?\"# + path + } + if useDevicePrefix && !useUnparsedPrefix { + path = #"\\.\"# + path + } let separator = duplicateSeparators ? #"\\"# : #"\"# #endif var currentPathLength = path.count var dirNameCount = 0 while currentPathLength < length { let dirName = String(dirNameCount) + assert(!(dirName.count > 255), "Path component of \(dirName) exceeds 255 characters") // Windows has path component limits of 255 path.append("\(path.count != 0 ? separator : "")\(dirName)") dirNameCount += 1 currentPathLength += separator.count + dirName.count @@ -54,17 +61,35 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath(".").pathString, ".") XCTAssertEqual(RelativePath("a").pathString, "a") XCTAssertEqual(RelativePath(#"a\b\c"#).pathString, #"a\b\c"#) + + // Unparsed prefix '\\?\'' < PATH_MAX + XCTAssertEqual(AbsolutePath(#"\\?\C:\"#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:"#).pathString, #"C:\"#) + XCTAssert(try #/[A-Z]:foo/#.wholeMatch(in: AbsolutePath(#"\\?\C:foo"#).pathString) != nil, "Got: \(AbsolutePath(#"\\?\C:foo"#).pathString)") + XCTAssert(try #/[A-Z]:\\foo/#.wholeMatch(in: AbsolutePath(#"\\?\C:\foo"#).pathString) != nil, "Got: \(AbsolutePath(#"\\?\C:\foo"#).pathString)") + + // Unparsed prefix > PATH_MAX let longAbsolutePathUnderPathMax = generatePath(200) XCTAssertEqual(AbsolutePath(longAbsolutePathUnderPathMax).pathString, longAbsolutePathUnderPathMax) let longAbsolutePathOverPathMax = generatePath(260) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax).pathString, #"\\?\"# + longAbsolutePathOverPathMax) + let unParsedLongAbsolutePathUnderPathMax = generatePath(265, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathUnderPathMax).pathString, unParsedLongAbsolutePathUnderPathMax) + + // Device prefix < PATH_MAX + XCTAssertEqual(AbsolutePath(#"\\.\C:\"#).pathString, #"\\.\C:"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\foo"#).pathString, #"\\.\C:\foo"#) + let deviceLongAbsolutePathUnderPathMax = generatePath(265, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathUnderPathMax).pathString, deviceLongAbsolutePathUnderPathMax) #endif } func testMixedSeperators() { #if os(Windows) XCTAssertEqual(AbsolutePath(#"C:\foo/bar"#).pathString, #"C:\foo\bar"#) - XCTAssertEqual(AbsolutePath(#"C:\\foo//bar"#).pathString, #"C:\foo\bar"#) + XCTAssertEqual(AbsolutePath(#"C:\foo/bar"#).pathString, #"C:\foo\bar"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\foo/bar"#).pathString, #"C:\foo\bar"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\foo/bar"#).pathString, #"\\.\C:\foo\bar"#) #endif } @@ -123,12 +148,25 @@ class PathTests: XCTestCase { #else XCTAssertEqual(AbsolutePath(#"C:\ab\\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) XCTAssertEqual(AbsolutePath(#"C:\ab\\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\ab\\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\ab\\cd\\ef"#).pathString, #"\\.\C:\ab\cd\ef"#) XCTAssertEqual(RelativePath(#"ab\\cd\\ef"#).pathString, #"ab\\cd\\ef"#) XCTAssertEqual(RelativePath(#"ab\\cd\\\ef"#).pathString, #"ab\\cd\\\ef"#) + // Duplicate backslashes will be squashed, so needs to be more that PATH_MAX let longAbsolutePathOverPathMax = generatePath(2 * 260, duplicateSeparators: true) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax).pathString, #"\\?\"# + longAbsolutePathOverPathMax.replacingOccurrences(of: #"\\"#, with: #"\"#)) + + // Note: .replacingOccurrences() will squash the leading double backslash, add one extra to the start of comparision string for the \\? or \\. + let unParsedLongAbsolutePathOverPathMax = generatePath(2 * 260, duplicateSeparators: true, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax).pathString, + #"\"# + unParsedLongAbsolutePathOverPathMax.replacingOccurrences(of: #"\\"#, with: #"\"#)) + let deviceLongAbsolutePathOverPathMax = generatePath(2 * 260, duplicateSeparators: true, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax).pathString, + #"\"# + deviceLongAbsolutePathOverPathMax.replacingOccurrences(of: #"\\"#, with: #"\"#)) + + #endif } @@ -142,10 +180,25 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath(#"C:\"#).pathString, #"C:\"#) XCTAssertEqual(AbsolutePath(#"C:\ab\cd\ef\"#).pathString, #"C:\ab\cd\ef"#) XCTAssertEqual(AbsolutePath(#"C:\ab\cd\ef\\"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\"#).pathString, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\ab\cd\ef\"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\ab\cd\ef\\"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\"#).pathString, #"\\.\C:"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\ab\cd\ef\"#).pathString, #"\\.\C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\ab\cd\ef\\"#).pathString, #"\\.\C:\ab\cd\ef"#) + XCTAssertEqual(RelativePath(#"ab\cd\ef\"#).pathString, #"ab\cd\ef\"#) XCTAssertEqual(RelativePath(#"ab\cd\ef\\"#).pathString, #"ab\cd\ef\\"#) + let longAbsolutePathOverPathMax = generatePath(280) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\"#).pathString, #"\\?\"# + longAbsolutePathOverPathMax) + + let unParsedLongAbsolutePathOverPathMax = generatePath(265, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\"# ).pathString, + unParsedLongAbsolutePathOverPathMax) + let deviceLongAbsolutePathOverPathMax = generatePath(265, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\"#).pathString, + deviceLongAbsolutePathOverPathMax) #endif } @@ -158,12 +211,25 @@ class PathTests: XCTestCase { #else XCTAssertEqual(AbsolutePath(#"C:\ab\.\.\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) XCTAssertEqual(AbsolutePath(#"C:\ab\.\cd\\ef\."#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\ab\.\.\cd\\ef"#).pathString, #"C:\ab\cd\ef"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\ab\.\cd\\ef\."#).pathString, #"\\.\C:\ab\cd\ef"#) XCTAssertEqual(RelativePath(#"ab\.\cd\.\.\ef"#).pathString, #"ab\.\cd\.\.\ef"#) XCTAssertEqual(RelativePath(#"ab\.\cd\ef\."#).pathString, #"ab\.\cd\ef\."#) + let longAbsolutePathOverPathMax = generatePath(260) let longAbsolutePathOverPathMaxWithDotComponents = longAbsolutePathOverPathMax + #"\.\foo\.\bar\"# XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMaxWithDotComponents).pathString, #"\\?\"# + longAbsolutePathOverPathMax + #"\foo\bar"#) + + let unParsedLongAbsolutePathOverPathMax = generatePath(265, useUnparsedPrefix: true) + let unParsedLongAbsolutePathOverPathMaxWithDotComponents = unParsedLongAbsolutePathOverPathMax + #"\.\foo\.\bar\"# + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMaxWithDotComponents).pathString, + unParsedLongAbsolutePathOverPathMax + #"\foo\bar"#) + + let deviceLongAbsolutePathOverPathMax = generatePath(265, useDevicePrefix: true) + let deviceLongAbsolutePathOverPathMaxWithDotComponents = deviceLongAbsolutePathOverPathMax + #"\.\foo\.\bar\"# + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMaxWithDotComponents).pathString, + deviceLongAbsolutePathOverPathMax + #"\foo\bar"#) #endif } @@ -190,6 +256,7 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath(#"C:\..\abc"#).pathString, #"C:\abc"#) XCTAssertEqual(AbsolutePath(#"C:\..\abc\.."#).pathString, #"C:\"#) XCTAssertEqual(AbsolutePath(#"C:\..\abc\..\def"#).pathString, #"C:\def"#) + XCTAssertEqual(RelativePath(#".."#).pathString, #".."#) XCTAssertEqual(RelativePath(#"..\.."#).pathString, #"..\.."#) XCTAssertEqual(RelativePath(#"..\.\.."#).pathString, #"..\.\.."#) @@ -198,6 +265,11 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath(#"abc\.."#).pathString, #"abc\.."#) let longAbsolutePathOverPathMax = generatePath(280) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\abc\..\"#).pathString, #"\\?\"# + longAbsolutePathOverPathMax) + let unParsedLongAbsolutePathOverPathMax = generatePath(280, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\abc\..\"#).pathString, unParsedLongAbsolutePathOverPathMax) + let deviceLongAbsolutePathOverPathMax = generatePath(280, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\abc\..\"#).pathString, deviceLongAbsolutePathOverPathMax) + #endif } @@ -283,8 +355,29 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath(#"C:\a\b\"#).dirname, #"C:\a"#) XCTAssertEqual(AbsolutePath(#"C:\a\b\\"#).dirname, #"C:\a"#) XCTAssertEqual(AbsolutePath(#"C:\a\"#).dirname, #"C:\"#) + + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b"#).dirname, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\"#).dirname, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\\"#).dirname, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\\\"#).dirname, #"C:\"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b\"#).dirname, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b\\"#).dirname, #"C:\a"#) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\"#).dirname, #"C:\"#) + + XCTAssertEqual(AbsolutePath(#"\\.\C:\a\b"#).dirname, #"\\.\C:\a"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\"#).dirname, #"\\.\C:"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\\"#).dirname, #"\\.\C:"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\\\"#).dirname, #"\\.\C:"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\a\b\"#).dirname, #"\\.\C:\a"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\a\b\\"#).dirname, #"\\.\C:\a"#) + XCTAssertEqual(AbsolutePath(#"\\.\C:\a\"#).dirname, #"\\.\C:"#) + let longAbsolutePathOverPathMax = generatePath(280) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\a.txt"#).dirname, #"\\?\"# + longAbsolutePathOverPathMax) + let unParsedLongAbsolutePathOverPathMax = generatePath(280, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\a.txt"#).dirname, unParsedLongAbsolutePathOverPathMax) + let deviceLongAbsolutePathOverPathMax = generatePath(280, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\a.txt"#).dirname, deviceLongAbsolutePathOverPathMax) #endif } @@ -319,6 +412,10 @@ class PathTests: XCTestCase { XCTAssertEqual(RelativePath(#"."#).basename, #"."#) let longAbsolutePathOverPathMax = generatePath(280) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\a.txt"#).basename, #"a.txt"#) + let unParsedLongAbsolutePathOverPathMax = generatePath(280, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\a.txt"#).basename, #"a.txt"#) + let deviceLongAbsolutePathOverPathMax = generatePath(280, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\a.txt"#).basename, #"a.txt"#) #endif } @@ -368,6 +465,10 @@ class PathTests: XCTestCase { let longAbsolutePathOverPathMax = generatePath(280) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax + #"\a.txt"#).basenameWithoutExt, #"a"#) + let unParsedLongAbsolutePathOverPathMax = generatePath(280, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\a.txt"#).basenameWithoutExt, #"a"#) + let deviceLongAbsolutePathOverPathMax = generatePath(280, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\a.txt"#).basenameWithoutExt, #"a"#) #endif } @@ -411,6 +512,10 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath(#"C:\bar\..\foo\..\\yabba\a\b"#).parentDirectory.parentDirectory, AbsolutePath(#"C:\yabba"#)) let longAbsolutePathOverPathMax = generatePath(280) XCTAssertEqual(AbsolutePath(longAbsolutePathOverPathMax).parentDirectory, AbsolutePath(longAbsolutePathOverPathMax.replacingOccurrences(of: #"\95"#, with: ""))) + let unParsedLongAbsolutePathOverPathMax = generatePath(280, useUnparsedPrefix: true) + XCTAssertEqual(AbsolutePath(unParsedLongAbsolutePathOverPathMax).parentDirectory, AbsolutePath(unParsedLongAbsolutePathOverPathMax.replacingOccurrences(of: #"\94"#, with: ""))) + let deviceLongAbsolutePathOverPathMax = generatePath(280, useDevicePrefix: true) + XCTAssertEqual(AbsolutePath(deviceLongAbsolutePathOverPathMax).parentDirectory, AbsolutePath(deviceLongAbsolutePathOverPathMax.replacingOccurrences(of: #"\94"#, with: ""))) #endif } @@ -533,6 +638,22 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath(#"C:\bar/../foo/..//"#).components, ["C:"]) XCTAssertEqual(AbsolutePath(#"C:\bar/../foo/..//yabba/a/b/"#).components, ["C:", "yabba", "a", "b"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\"#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\."#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\bar"#).components, ["C:", "bar"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\foo/bar/.."#).components, ["C:", "foo"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\bar/../foo"#).components, ["C:", "foo"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\bar/../foo/..//"#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"\\?\C:\bar/../foo/..//yabba/a/b/"#).components, ["C:", "yabba", "a", "b"]) + + XCTAssertEqual(AbsolutePath(#"\\.\C:\"#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"\\.\C:\."#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"\\.\C:\bar"#).components, ["C:", "bar"]) + XCTAssertEqual(AbsolutePath(#"\\.\C:\foo/bar/.."#).components, ["C:", "foo"]) + XCTAssertEqual(AbsolutePath(#"\\.\C:\bar/../foo"#).components, ["C:", "foo"]) + XCTAssertEqual(AbsolutePath(#"\\.\C:\bar/../foo/..//"#).components, ["C:"]) + XCTAssertEqual(AbsolutePath(#"\\.\C:\bar/../foo/..//yabba/a/b/"#).components, ["C:", "yabba", "a", "b"]) + XCTAssertEqual(RelativePath(#""#).components, ["."]) XCTAssertEqual(RelativePath(#"."#).components, ["."]) XCTAssertEqual(RelativePath(#".."#).components, [".."]) @@ -570,11 +691,29 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath(#"C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\c\d"#)), RelativePath(#"..\..\b\c\d"#)) XCTAssertEqual(AbsolutePath(#"C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\b\c\d"#)), RelativePath(#"..\..\..\a\b\c\d"#)) + XCTAssertEqual(AbsolutePath(#"\\?\C:\"#).relative(to: AbsolutePath(#"C:\"#)), RelativePath(".")) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a/b/c/d"#).relative(to: AbsolutePath(#"C:\"#)), RelativePath(#"a\b\c\d"#)) + XCTAssertEqual(AbsolutePath(#"\\?\C:\"#).relative(to: AbsolutePath(#"C:\a\b\c"#)), RelativePath(#"..\..\.."#)) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\b"#)), RelativePath(#"c\d"#)) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\b\c"#)), RelativePath(#"d"#)) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\a\c\d"#)), RelativePath(#"..\..\b\c\d"#)) + XCTAssertEqual(AbsolutePath(#"\\?\C:\a\b\c\d"#).relative(to: AbsolutePath(#"C:\b\c\d"#)), RelativePath(#"..\..\..\a\b\c\d"#)) + var longAbsolutePathOverPathMax = generatePath(264) XCTAssertEqual( AbsolutePath(longAbsolutePathOverPathMax).relative(to: AbsolutePath(longAbsolutePathOverPathMax.replacingOccurrences(of: #"\85\86\87\88\89\90"#, with: ""))), RelativePath(#"85\86\87\88\89\90"#) ) + var unParsedLongAbsolutePathOverPathMax = generatePath(264, useUnparsedPrefix: true) + XCTAssertEqual( + AbsolutePath(unParsedLongAbsolutePathOverPathMax).relative(to: AbsolutePath(unParsedLongAbsolutePathOverPathMax.replacingOccurrences(of: #"\85\86\87\88\89"#, with: ""))), + RelativePath(#"85\86\87\88\89"#) + ) + var deviceLongAbsolutePathOverPathMax = generatePath(264, useDevicePrefix: true) + XCTAssertEqual( + AbsolutePath(unParsedLongAbsolutePathOverPathMax).relative(to: AbsolutePath(unParsedLongAbsolutePathOverPathMax.replacingOccurrences(of: #"\85\86\87\88\89"#, with: ""))), + RelativePath(#"85\86\87\88\89"#) + ) #endif } @@ -595,12 +734,28 @@ class PathTests: XCTestCase { XCTAssertTrue(AbsolutePath(#"C:\2"#) >= AbsolutePath(#"C:\2"#)) XCTAssertTrue(AbsolutePath(#"C:\2.1"#) >= AbsolutePath(#"C:\2"#)) - var longAbsolutePathOverPathMax = generatePath(260) + let longAbsolutePathOverPathMax = generatePath(260) XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\abc"#) < AbsolutePath(longAbsolutePathOverPathMax + #"\def"#)) XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\2"#) <= AbsolutePath(longAbsolutePathOverPathMax + #"\2.1"#)) XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\3.1"#) > AbsolutePath(longAbsolutePathOverPathMax + #"\2"#)) XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\2"#) >= AbsolutePath(longAbsolutePathOverPathMax + #"\2"#)) XCTAssertTrue(AbsolutePath(longAbsolutePathOverPathMax + #"\2.1"#) >= AbsolutePath(longAbsolutePathOverPathMax + #"\2"#)) + + let unParsedLongAbsolutePathOverPathMax = generatePath(260, useUnparsedPrefix: true) + XCTAssertTrue(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\abc"#) < AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\def"#)) + XCTAssertTrue(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2"#) <= AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2.1"#)) + XCTAssertTrue(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\3.1"#) > AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2"#)) + XCTAssertTrue(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2"#) >= AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2"#)) + XCTAssertTrue(AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2.1"#) >= AbsolutePath(unParsedLongAbsolutePathOverPathMax + #"\2"#)) + + let deviceLongAbsolutePathOverPathMax = generatePath(260, useDevicePrefix: true) + XCTAssertTrue(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\abc"#) < AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\def"#)) + XCTAssertTrue(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2"#) <= AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2.1"#)) + XCTAssertTrue(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\3.1"#) > AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2"#)) + XCTAssertTrue(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2"#) >= AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2"#)) + XCTAssertTrue(AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2.1"#) >= AbsolutePath(deviceLongAbsolutePathOverPathMax + #"\2"#)) + + #endif } @@ -633,9 +788,19 @@ class PathTests: XCTestCase { XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\foo\bar\baz"#))) XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\bar"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\a\b\c\d\e\f"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\a\b\c\d\e\f.swift"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\a\b\c"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\"#))) + XCTAssertFalse(AbsolutePath(#"\\?\C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\foo\bar\baz"#))) + XCTAssertFalse(AbsolutePath(#"\\?\C:\foo\bar"#).isDescendantOfOrEqual(to: AbsolutePath(#"C:\bar"#))) + XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isDescendant(of: AbsolutePath(#"C:\foo\bar"#))) XCTAssertTrue(AbsolutePath(#"C:\foo\bar"#).isDescendant(of: AbsolutePath(#"C:\foo"#))) + XCTAssertFalse(AbsolutePath(#"\\?\C:\foo\bar"#).isDescendant(of: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\foo\bar"#).isDescendant(of: AbsolutePath(#"C:\foo"#))) + XCTAssertTrue(AbsolutePath(#"C:\a\b\c\d"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d\e\f"#))) XCTAssertTrue(AbsolutePath(#"C:\a\b\c"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d\e\f.swift"#))) XCTAssertTrue(AbsolutePath(#"C:\"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\"#))) @@ -643,9 +808,19 @@ class PathTests: XCTestCase { XCTAssertFalse(AbsolutePath(#"C:\foo\bar\baz"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) XCTAssertFalse(AbsolutePath(#"C:\bar"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\a\b\c\d"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d\e\f"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\a\b\c"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\a\b\c\d\e\f.swift"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertFalse(AbsolutePath(#"\\?\C:\foo\bar\baz"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertFalse(AbsolutePath(#"\\?\C:\bar"#).isAncestorOfOrEqual(to: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertFalse(AbsolutePath(#"C:\foo\bar"#).isAncestor(of: AbsolutePath(#"C:\foo\bar"#))) XCTAssertTrue(AbsolutePath(#"C:\foo"#).isAncestor(of: AbsolutePath(#"C:\foo\bar"#))) + XCTAssertFalse(AbsolutePath(#"\\?\C:\foo\bar"#).isAncestor(of: AbsolutePath(#"\\?\C:\foo\bar"#))) + XCTAssertTrue(AbsolutePath(#"\\?\C:\foo"#).isAncestor(of: AbsolutePath(#"\\?\C:\foo\bar"#))) + // Long/Long Ancestry let longAbsolutePathOverPathMax = generatePath(265) let longerAbsolutePathOverPathMax = generatePath(300) @@ -689,10 +864,14 @@ class PathTests: XCTestCase { XCTAssertEqual("\(error)", #"invalid absolute path 'a\b\d'"#) } + XCTAssertNoThrow(try AbsolutePath(validating: #"\\?\C:\a\b\c\d"#)) + XCTAssertNoThrow(try AbsolutePath(validating: #"\\.\C:\a\b\c\d"#)) + let relativeLongPath = generatePath(265, absolute: false) XCTAssertThrowsError(try AbsolutePath(validating: relativeLongPath)) { error in XCTAssertEqual("\(error)", "invalid absolute path '\(relativeLongPath)'") } + #endif }