diff --git a/CoreFoundation/URL.subproj/CFURL.c b/CoreFoundation/URL.subproj/CFURL.c index 49d07d9d90..d8bb21a59f 100644 --- a/CoreFoundation/URL.subproj/CFURL.c +++ b/CoreFoundation/URL.subproj/CFURL.c @@ -3965,10 +3965,13 @@ CF_EXPORT void __CFURLSetResourceInfoPtr(CFURLRef url, void *ptr) { /* HFSPath<->URLPath functions at the bottom of the file */ static CFArrayRef WindowsPathToURLComponents(CFStringRef path, CFAllocatorRef alloc, Boolean isDir, Boolean isAbsolute) CF_RETURNS_RETAINED { CFArrayRef tmp; + CFMutableStringRef mutablePath = CFStringCreateMutableCopy(alloc, 0, path); CFMutableArrayRef urlComponents = NULL; CFIndex i=0; - - tmp = CFStringCreateArrayBySeparatingStrings(alloc, path, CFSTR("\\")); + // Since '/' is a valid Windows path separator, we convert / to \ before splitting + CFStringFindAndReplace(mutablePath, CFSTR("/"), CFSTR("\\"), CFRangeMake(0, CFStringGetLength(mutablePath)), 0); + tmp = CFStringCreateArrayBySeparatingStrings(alloc, mutablePath, CFSTR("\\")); + CFRelease(mutablePath); urlComponents = CFArrayCreateMutableCopy(alloc, 0, tmp); CFRelease(tmp); diff --git a/Foundation/FileManager+Win32.swift b/Foundation/FileManager+Win32.swift index b202c32b8f..476d681ec4 100644 --- a/Foundation/FileManager+Win32.swift +++ b/Foundation/FileManager+Win32.swift @@ -637,7 +637,7 @@ extension FileManager { var szDirectory: [WCHAR] = Array(repeating: 0, count: Int(dwLength + 1)) GetCurrentDirectoryW(dwLength, &szDirectory) - return String(decodingCString: &szDirectory, as: UTF16.self) + return String(decodingCString: &szDirectory, as: UTF16.self).standardizingPath } @discardableResult @@ -701,8 +701,8 @@ extension FileManager { return true } - internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer? = nil) throws -> stat { - let _fsRep: UnsafePointer + internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer? = nil) throws -> stat { + let _fsRep: UnsafePointer if fsRep == nil { _fsRep = try __fileSystemRepresentation(withPath: path) } else { @@ -714,15 +714,13 @@ extension FileManager { } var statInfo = stat() - let h = path.withCString(encodedAs: UTF16.self) { - CreateFileW(/*lpFileName=*/$0, - /*dwDesiredAccess=*/DWORD(0), - /*dwShareMode=*/DWORD(FILE_SHARE_READ), - /*lpSecurityAttributes=*/nil, - /*dwCreationDisposition=*/DWORD(OPEN_EXISTING), - /*dwFlagsAndAttributes=*/DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS), - /*hTemplateFile=*/nil) - } + let h = CreateFileW(_fsRep, + /*dwDesiredAccess=*/DWORD(0), + DWORD(FILE_SHARE_READ), + /*lpSecurityAttributes=*/nil, + DWORD(OPEN_EXISTING), + DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS), + /*hTemplateFile=*/nil) if h == INVALID_HANDLE_VALUE { throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path]) } @@ -858,7 +856,7 @@ extension FileManager { } internal func _updateTimes(atPath path: String, - withFileSystemRepresentation fsr: UnsafePointer, + withFileSystemRepresentation fsr: UnsafePointer, creationTime: Date? = nil, accessTime: Date? = nil, modificationTime: Date? = nil) throws { @@ -869,10 +867,7 @@ extension FileManager { var mtime: FILETIME = FILETIME(from: time_t((modificationTime ?? stat.lastModificationDate).timeIntervalSince1970)) - let hFile: HANDLE = String(utf8String: fsr)!.withCString(encodedAs: UTF16.self) { - CreateFileW($0, DWORD(GENERIC_WRITE), DWORD(FILE_SHARE_WRITE), - nil, DWORD(OPEN_EXISTING), 0, nil) - } + let hFile = CreateFileW(fsr, DWORD(GENERIC_WRITE), DWORD(FILE_SHARE_WRITE), nil, DWORD(OPEN_EXISTING), 0, nil) if hFile == INVALID_HANDLE_VALUE { throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path]) } diff --git a/Foundation/FileManager.swift b/Foundation/FileManager.swift index e2ca10ae19..f405286b4a 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -20,6 +20,14 @@ import CoreFoundation import MSVCRT #endif +#if os(Windows) +internal typealias NativeFSRCharType = WCHAR +let NativeFSREncoding = String.Encoding.utf16LittleEndian.rawValue +#else +internal typealias NativeFSRCharType = CChar +let NativeFSREncoding = String.Encoding.utf8.rawValue +#endif + open class FileManager : NSObject { /* Returns the default singleton instance. @@ -383,7 +391,12 @@ open class FileManager : NSObject { #elseif os(Linux) || os(Android) || os(Windows) let modeT = number.uint32Value #endif - guard chmod(fsRep, mode_t(modeT)) == 0 else { +#if os(Windows) + let result = _wchmod(fsRep, mode_t(modeT)) +#else + let result = chmod(fsRep, mode_t(modeT)) +#endif + guard result == 0 else { throw _NSErrorWithErrno(errno, reading: false, path: path) } @@ -1021,15 +1034,29 @@ open class FileManager : NSObject { */ open func fileSystemRepresentation(withPath path: String) -> UnsafePointer { precondition(path != "", "Empty path argument") +#if os(Windows) + // On Windows, the internal _fileSystemRepresentation returns + // UTF16 encoded data, so we need to re-encode the result as + // UTF8 before returning. + return try! _fileSystemRepresentation(withPath: path) { + String(decodingCString: $0, as: UTF16.self).withCString() { + let sz = strnlen($0, Int(MAX_PATH)) + let buf = UnsafeMutablePointer.allocate(capacity: sz + 1) + buf.initialize(from: $0, count: sz + 1) + return UnsafePointer(buf) + } + } +#else return try! __fileSystemRepresentation(withPath: path) +#endif } - internal func __fileSystemRepresentation(withPath path: String) throws -> UnsafePointer { + internal func __fileSystemRepresentation(withPath path: String) throws -> UnsafePointer { let len = CFStringGetMaximumSizeOfFileSystemRepresentation(path._cfObject) if len != kCFNotFound { - let buf = UnsafeMutablePointer.allocate(capacity: len) + let buf = UnsafeMutablePointer.allocate(capacity: len) buf.initialize(repeating: 0, count: len) - if path._nsObject.getFileSystemRepresentation(buf, maxLength: len) { + if path._nsObject._getFileSystemRepresentation(buf, maxLength: len) { return UnsafePointer(buf) } buf.deinitialize(count: len) @@ -1038,13 +1065,13 @@ open class FileManager : NSObject { throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInvalidFileName.rawValue, userInfo: [NSFilePathErrorKey: path]) } - internal func _fileSystemRepresentation(withPath path: String, _ body: (UnsafePointer) throws -> ResultType) throws -> ResultType { + internal func _fileSystemRepresentation(withPath path: String, _ body: (UnsafePointer) throws -> ResultType) throws -> ResultType { let fsRep = try __fileSystemRepresentation(withPath: path) defer { fsRep.deallocate() } return try body(fsRep) } - internal func _fileSystemRepresentation(withPath path1: String, andPath path2: String, _ body: (UnsafePointer, UnsafePointer) throws -> ResultType) throws -> ResultType { + internal func _fileSystemRepresentation(withPath path1: String, andPath path2: String, _ body: (UnsafePointer, UnsafePointer) throws -> ResultType) throws -> ResultType { let fsRep1 = try __fileSystemRepresentation(withPath: path1) defer { fsRep1.deallocate() } let fsRep2 = try __fileSystemRepresentation(withPath: path2) @@ -1058,7 +1085,7 @@ open class FileManager : NSObject { open func string(withFileSystemRepresentation str: UnsafePointer, length len: Int) -> String { return NSString(bytes: str, length: len, encoding: String.Encoding.utf8.rawValue)!._swiftObject } - + /* -replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error: is for developers who wish to perform a safe-save without using the full NSDocument machinery that is available in the AppKit. The `originalItemURL` is the item being replaced. diff --git a/Foundation/NSPathUtilities.swift b/Foundation/NSPathUtilities.swift index 8d18a5898d..9b583447ef 100644 --- a/Foundation/NSPathUtilities.swift +++ b/Foundation/NSPathUtilities.swift @@ -22,7 +22,7 @@ public func NSTemporaryDirectory() -> String { guard GetTempPathW(DWORD(wszPath.count), &wszPath) <= cchLength else { preconditionFailure("GetTempPathW mutation race") } - return String(decodingCString: wszPath, as: UTF16.self) + return String(decodingCString: wszPath, as: UTF16.self).standardizingPath #else #if canImport(Darwin) let safe_confstr = { (name: Int32, buf: UnsafeMutablePointer?, len: Int) -> Int in @@ -348,14 +348,32 @@ extension NSString { return result } - + +#if os(Windows) + // Convert to a posix style '/' separated path + internal var unixPath: String { + var droppedPrefix = self as String + // If there is anything before the drive letter, + // e.g. "\\?\, \\host\, \??\", remove it + if isAbsolutePath, let idx = droppedPrefix.firstIndex(of: ":") { + droppedPrefix.removeSubrange(.., maxLength max: Int) -> Bool { +#if os(Windows) + let fsr = UnsafeMutablePointer.allocate(capacity: max) + defer { fsr.deallocate() } + guard _getFileSystemRepresentation(fsr, maxLength: max) else { return false } + return String(decodingCString: fsr, as: UTF16.self).withCString() { + let chars = strnlen_s($0, max) + guard chars < max else { return false } + cname.assign(from: $0, count: chars + 1) + return true + } +#else + return _getFileSystemRepresentation(cname, maxLength: max) +#endif + } + + internal func _getFileSystemRepresentation(_ cname: UnsafeMutablePointer, maxLength max: Int) -> Bool { guard self.length > 0 else { return false } - +#if os(Windows) + var fsr = self._swiftObject + let idx = fsr.startIndex + + // If we have an RFC 8089 style path e.g. `/[drive-letter]:/...`, drop the + // leading /, otherwise, a leading slash indicates a rooted path on the + // drive for the current working directory + if fsr.count >= 3 && fsr[idx] == "/" && fsr[fsr.index(after: idx)].isLetter && fsr[fsr.index(idx, offsetBy: 2)] == ":" { + fsr.removeFirst() + } + + // Windows APIS that go through the path parser can handle + // forward slashes in paths. However, symlinks created with + // forward slashes do not resolve properly, so we normalize + // the path separators anyways. + fsr = fsr.replacingOccurrences(of: "/", with: "\\") + + // Drop trailing slashes unless it follows a drive letter. On + // Windows the path `C:\` indicates the root directory of the + // `C:` drive. The path `C:` indicates the current working + // directory on the `C:` drive. + while fsr.count > 1 + && (fsr[fsr.index(before: fsr.endIndex)] == "\\") + && !(fsr.count == 3 && fsr[fsr.index(fsr.endIndex, offsetBy: -2)] == ":") { + fsr.removeLast() + } + + return fsr.withCString(encodedAs: UTF16.self) { + let wchars = wcsnlen_s($0, max) + guard wchars < max else { return false } + cname.assign(from: $0, count: wchars + 1) + return true + } +#else return CFStringGetFileSystemRepresentation(self._cfObject, cname, max) +#endif } } diff --git a/Foundation/NSURL.swift b/Foundation/NSURL.swift index c5c74d3fda..8108c8c448 100644 --- a/Foundation/NSURL.swift +++ b/Foundation/NSURL.swift @@ -30,7 +30,11 @@ private func _standardizedPath(_ path: String) -> String { if !path.isAbsolutePath { return path._nsObject.standardizingPath } +#if os(Windows) + return path.unixPath +#else return path +#endif } internal func _pathComponents(_ path: String?) -> [String]? { @@ -335,7 +339,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying { let thePath = _standardizedPath(path) var isDir: ObjCBool = false - if thePath.hasSuffix("/") { + if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) { isDir = true } else { let absolutePath: String @@ -356,16 +360,9 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying { } public init(fileURLWithPath path: String) { - let thePath: String - let pathString = NSString(string: path) - if !pathString.isAbsolutePath { - thePath = pathString.standardizingPath - } else { - thePath = path - } - + let thePath = _standardizedPath(path) var isDir: ObjCBool = false - if thePath.hasSuffix("/") { + if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) { isDir = true } else { if !FileManager.default.fileExists(atPath: path, isDirectory: &isDir) { @@ -542,7 +539,19 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying { open var path: String? { let absURL = CFURLCopyAbsoluteURL(_cfObject) - return CFURLCopyFileSystemPath(absURL, kCFURLPlatformPathStyle)?._swiftObject + guard var url = CFURLCopyFileSystemPath(absURL, kCFURLPOSIXPathStyle)?._swiftObject else { + return nil + } +#if os(Windows) + // Per RFC 8089:E.2, if we have an absolute Windows/DOS path + // we can begin the url with a drive letter rather than a '/' + let scalars = Array(url.unicodeScalars) + if isFileURL, url.isAbsolutePath, + scalars.count >= 3, scalars[0] == "/", scalars[2] == ":" { + url.removeFirst() + } +#endif + return url } open var fragment: String? { @@ -559,7 +568,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying { // The same as path if baseURL is nil open var relativePath: String? { - return CFURLCopyFileSystemPath(_cfObject, kCFURLPlatformPathStyle)?._swiftObject + return CFURLCopyFileSystemPath(_cfObject, kCFURLPOSIXPathStyle)?._swiftObject } /* Determines if a given URL string's path represents a directory (i.e. the path component in the URL string ends with a '/' character). This does not check the resource the URL refers to. diff --git a/TestFoundation/TestURL.swift b/TestFoundation/TestURL.swift index c60c368c97..1b7abf21b3 100644 --- a/TestFoundation/TestURL.swift +++ b/TestFoundation/TestURL.swift @@ -62,8 +62,9 @@ class TestURL : XCTestCase { // ensure that the trailing slashes are compressed even when mixed // e.g. NOT file:///S:/b/u3%2F%/%2F%2/ let u3 = URL(fileURLWithPath: "S:\\b\\u3//\\//") - // XCTAssertEqual(u3.absoluteString, "file:///S:/b/u3/%2F/") - XCTAssertEqual(u3.path, "S:\\b\\u3\\") + // URL.path is defined to strip trailing slashes + XCTAssertEqual(u3.absoluteString, "file:///S:/b/u3/") + XCTAssertEqual(u3.path, "S:/b/u3") // ensure that the regular conversion works let u4 = URL(fileURLWithPath: "S:\\b\\u4") @@ -377,7 +378,13 @@ class TestURL : XCTestCase { // 1 for path separator let expectedLength = UInt(strlen(TestURL.gFileDoesNotExistName)) + TestURL.gRelativeOffsetFromBaseCurrentWorkingDirectory XCTAssertEqual(UInt(actualLength), expectedLength, "fileSystemRepresentation was too short") +#if os(Windows) + // On Windows, the url path should have '/' separators, and + // the fileSystemRepresentation should '\' separators. + XCTAssertTrue(strncmp(String(TestURL.gBaseCurrentWorkingDirectoryPath.map {$0 == "/" ? "\\" : $0} ), fileSystemRep, Int(strlen(TestURL.gBaseCurrentWorkingDirectoryPath))) == 0, "fileSystemRepresentation of base path is wrong") +#else XCTAssertTrue(strncmp(TestURL.gBaseCurrentWorkingDirectoryPath, fileSystemRep, Int(strlen(TestURL.gBaseCurrentWorkingDirectoryPath))) == 0, "fileSystemRepresentation of base path is wrong") +#endif let lengthOfRelativePath = Int(strlen(TestURL.gFileDoesNotExistName)) let relativePath = fileSystemRep.advanced(by: Int(TestURL.gRelativeOffsetFromBaseCurrentWorkingDirectory)) XCTAssertTrue(strncmp(TestURL.gFileDoesNotExistName, relativePath, lengthOfRelativePath) == 0, "fileSystemRepresentation of file path is wrong") @@ -432,7 +439,13 @@ class TestURL : XCTestCase { // 1 for path separator let expectedLength = UInt(strlen(TestURL.gFileDoesNotExistName)) + TestURL.gRelativeOffsetFromBaseCurrentWorkingDirectory XCTAssertEqual(actualLength, expectedLength, "fileSystemRepresentation was too short") +#if os(Windows) + // On Windows, the url path should have '/' separators, and + // the fileSystemRepresentation should '\' separators. + XCTAssertTrue(strncmp(String(TestURL.gBaseCurrentWorkingDirectoryPath.map { $0 == "/" ? "\\" : $0 }), fileSystemRep, Int(strlen(TestURL.gBaseCurrentWorkingDirectoryPath))) == 0, "fileSystemRepresentation of base path is wrong") +#else XCTAssertTrue(strncmp(TestURL.gBaseCurrentWorkingDirectoryPath, fileSystemRep, Int(strlen(TestURL.gBaseCurrentWorkingDirectoryPath))) == 0, "fileSystemRepresentation of base path is wrong") +#endif let lengthOfRelativePath = Int(strlen(TestURL.gFileDoesNotExistName)) let relativePath = fileSystemRep.advanced(by: Int(TestURL.gRelativeOffsetFromBaseCurrentWorkingDirectory)) XCTAssertTrue(strncmp(TestURL.gFileDoesNotExistName, relativePath, lengthOfRelativePath) == 0, "fileSystemRepresentation of file path is wrong") @@ -459,14 +472,26 @@ class TestURL : XCTestCase { do { let url = URL(fileURLWithPath: "~") let result = url.resolvingSymlinksInPath().absoluteString +#if os(Windows) + // On Windows, currentDirectoryPath will return something + // like C:/Users/... Which doesn't have a leading slash + let expected = "file:///" + FileManager.default.currentDirectoryPath + "/~" +#else let expected = "file://" + FileManager.default.currentDirectoryPath + "/~" +#endif XCTAssertEqual(result, expected, "URLByResolvingSymlinksInPath resolves relative paths using current working directory.") } do { let url = URL(fileURLWithPath: "anysite.com/search") let result = url.resolvingSymlinksInPath().absoluteString +#if os(Windows) + // On Windows, currentDirectoryPath will return something + // like C:/Users/... Which doesn't have a leading slash + let expected = "file:///" + FileManager.default.currentDirectoryPath + "/anysite.com/search" +#else let expected = "file://" + FileManager.default.currentDirectoryPath + "/anysite.com/search" +#endif XCTAssertEqual(result, expected) }