Skip to content

Revert "Resolves #505 - Fix handling for Windows long paths" #507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Sources/TSCBasic/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ add_library(TSCBasic
TerminalController.swift
Thread.swift
Tuple.swift
misc.swift
Win32Error.swift)
misc.swift)

target_compile_options(TSCBasic PUBLIC
# Ignore secure function warnings on Windows.
Expand Down
160 changes: 15 additions & 145 deletions Sources/TSCBasic/Path.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
Copyright (c) 2014 - 2018 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
Expand Down Expand Up @@ -32,7 +32,6 @@ 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
Expand Down Expand Up @@ -507,30 +506,21 @@ private struct WindowsPath: Path, Sendable {
var components: [String] {
let normalized: UnsafePointer<Int8> = string.fileSystemRepresentation
defer { normalized.deallocate() }
// Remove prefix from the components, allowing for comparison across normalized paths.
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 }

return String(cString: normalized).components(separatedBy: "\\").filter { !$0.isEmpty }
}

var parentDirectory: Self {
return self == .root ? self : Self(string: dirname)
}

init(string: 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)] == ":"
if string.first?.isASCII ?? false, string.first?.isLetter ?? false, string.first?.isLowercase ?? false,
string.count > 1, string[string.index(string.startIndex, offsetBy: 1)] == ":"
{
self.string = "\(prefix)\(noPrefixPath.first!.uppercased())\(noPrefixPath.dropFirst(1))"
self.string = "\(string.first!.uppercased())\(string.dropFirst(1))"
} else {
self.string = prefix + noPrefixPath
self.string = string
}
}

Expand All @@ -546,13 +536,7 @@ private struct WindowsPath: Path, Sendable {
if !Self.isAbsolutePath(realpath) {
throw PathValidationError.invalidAbsolutePath(path)
}
do {
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)")
}
self.init(string: realpath)
}

init(validatingRelativePath path: String) throws {
Expand All @@ -570,20 +554,12 @@ private struct WindowsPath: Path, Sendable {

func suffix(withDot: Bool) -> String? {
return self.string.withCString(encodedAs: UTF16.self) {
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
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
}
}

Expand All @@ -609,111 +585,6 @@ 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)
}

/// 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
Expand Down Expand Up @@ -1095,8 +966,7 @@ extension AbsolutePath {
}
}

assert(AbsolutePath(base, result) == self, "base:\(base) result:\(result) self: \(self)")

assert(AbsolutePath(base, result) == self)
return result
}

Expand Down
37 changes: 0 additions & 37 deletions Sources/TSCBasic/Win32Error.swift

This file was deleted.

Loading