Skip to content

Commit 56a2c19

Browse files
committed
Implemented _getFileSystemRepresentation for Windows
Modified the helper _getFileSystemRepresentation functions to return a UTF16 Windows style path.
1 parent fff2489 commit 56a2c19

File tree

6 files changed

+151
-37
lines changed

6 files changed

+151
-37
lines changed

CoreFoundation/URL.subproj/CFURL.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3965,10 +3965,13 @@ CF_EXPORT void __CFURLSetResourceInfoPtr(CFURLRef url, void *ptr) {
39653965
/* HFSPath<->URLPath functions at the bottom of the file */
39663966
static CFArrayRef WindowsPathToURLComponents(CFStringRef path, CFAllocatorRef alloc, Boolean isDir, Boolean isAbsolute) CF_RETURNS_RETAINED {
39673967
CFArrayRef tmp;
3968+
CFMutableStringRef mutablePath = CFStringCreateMutableCopy(alloc, 0, path);
39683969
CFMutableArrayRef urlComponents = NULL;
39693970
CFIndex i=0;
3970-
3971-
tmp = CFStringCreateArrayBySeparatingStrings(alloc, path, CFSTR("\\"));
3971+
// Since '/' is a valid Windows path separator, we convert / to \ before splitting
3972+
CFStringFindAndReplace(mutablePath, CFSTR("/"), CFSTR("\\"), CFRangeMake(0, CFStringGetLength(mutablePath)), 0);
3973+
tmp = CFStringCreateArrayBySeparatingStrings(alloc, mutablePath, CFSTR("\\"));
3974+
CFRelease(mutablePath);
39723975
urlComponents = CFArrayCreateMutableCopy(alloc, 0, tmp);
39733976
CFRelease(tmp);
39743977

Foundation/FileManager+Win32.swift

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -684,8 +684,8 @@ extension FileManager {
684684
return true
685685
}
686686

687-
internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
688-
let _fsRep: UnsafePointer<Int8>
687+
internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<NativeFSRCharType>? = nil) throws -> stat {
688+
let _fsRep: UnsafePointer<NativeFSRCharType>
689689
if fsRep == nil {
690690
_fsRep = try __fileSystemRepresentation(withPath: path)
691691
} else {
@@ -697,15 +697,13 @@ extension FileManager {
697697
}
698698

699699
var statInfo = stat()
700-
let h = path.withCString(encodedAs: UTF16.self) {
701-
CreateFileW(/*lpFileName=*/$0,
702-
/*dwDesiredAccess=*/DWORD(0),
703-
/*dwShareMode=*/DWORD(FILE_SHARE_READ),
704-
/*lpSecurityAttributes=*/nil,
705-
/*dwCreationDisposition=*/DWORD(OPEN_EXISTING),
706-
/*dwFlagsAndAttributes=*/DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS),
707-
/*hTemplateFile=*/nil)
708-
}
700+
let h = CreateFileW(_fsRep,
701+
/*dwDesiredAccess=*/DWORD(0),
702+
DWORD(FILE_SHARE_READ),
703+
/*lpSecurityAttributes=*/nil,
704+
DWORD(OPEN_EXISTING),
705+
DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS),
706+
/*hTemplateFile=*/nil)
709707
if h == INVALID_HANDLE_VALUE {
710708
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
711709
}
@@ -841,7 +839,7 @@ extension FileManager {
841839
}
842840

843841
internal func _updateTimes(atPath path: String,
844-
withFileSystemRepresentation fsr: UnsafePointer<Int8>,
842+
withFileSystemRepresentation fsr: UnsafePointer<NativeFSRCharType>,
845843
creationTime: Date? = nil,
846844
accessTime: Date? = nil,
847845
modificationTime: Date? = nil) throws {
@@ -852,10 +850,7 @@ extension FileManager {
852850
var mtime: FILETIME =
853851
FILETIME(from: time_t((modificationTime ?? stat.lastModificationDate).timeIntervalSince1970))
854852

855-
let hFile: HANDLE = String(utf8String: fsr)!.withCString(encodedAs: UTF16.self) {
856-
CreateFileW($0, DWORD(GENERIC_WRITE), DWORD(FILE_SHARE_WRITE),
857-
nil, DWORD(OPEN_EXISTING), 0, nil)
858-
}
853+
let hFile = CreateFileW(fsr, DWORD(GENERIC_WRITE), DWORD(FILE_SHARE_WRITE), nil, DWORD(OPEN_EXISTING), 0, nil)
859854
if hFile == INVALID_HANDLE_VALUE {
860855
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path])
861856
}

Foundation/FileManager.swift

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import CoreFoundation
2020
import MSVCRT
2121
#endif
2222

23+
#if os(Windows)
24+
internal typealias NativeFSRCharType = WCHAR
25+
let NativeFSREncoding = String.Encoding.utf16LittleEndian.rawValue
26+
#else
27+
internal typealias NativeFSRCharType = CChar
28+
let NativeFSREncoding = String.Encoding.utf8.rawValue
29+
#endif
30+
2331
open class FileManager : NSObject {
2432

2533
/* Returns the default singleton instance.
@@ -383,7 +391,12 @@ open class FileManager : NSObject {
383391
#elseif os(Linux) || os(Android) || os(Windows)
384392
let modeT = number.uint32Value
385393
#endif
386-
guard chmod(fsRep, mode_t(modeT)) == 0 else {
394+
#if os(Windows)
395+
let result = _wchmod(fsRep, mode_t(modeT))
396+
#else
397+
let result = chmod(fsRep, mode_t(modeT))
398+
#endif
399+
guard result == 0 else {
387400
throw _NSErrorWithErrno(errno, reading: false, path: path)
388401
}
389402

@@ -1021,15 +1034,29 @@ open class FileManager : NSObject {
10211034
*/
10221035
open func fileSystemRepresentation(withPath path: String) -> UnsafePointer<Int8> {
10231036
precondition(path != "", "Empty path argument")
1037+
#if os(Windows)
1038+
// On Windows, the internal _fileSystemRepresentation returns
1039+
// UTF16 encoded data, so we need to re-encode the result as
1040+
// UTF8 before returning.
1041+
return try! _fileSystemRepresentation(withPath: path) {
1042+
String(decodingCString: $0, as: UTF16.self).withCString() {
1043+
let sz = strnlen($0, Int(MAX_PATH))
1044+
let buf = UnsafeMutablePointer<Int8>.allocate(capacity: sz + 1)
1045+
buf.initialize(from: $0, count: sz + 1)
1046+
return UnsafePointer(buf)
1047+
}
1048+
}
1049+
#else
10241050
return try! __fileSystemRepresentation(withPath: path)
1051+
#endif
10251052
}
10261053

1027-
internal func __fileSystemRepresentation(withPath path: String) throws -> UnsafePointer<Int8> {
1054+
internal func __fileSystemRepresentation(withPath path: String) throws -> UnsafePointer<NativeFSRCharType> {
10281055
let len = CFStringGetMaximumSizeOfFileSystemRepresentation(path._cfObject)
10291056
if len != kCFNotFound {
1030-
let buf = UnsafeMutablePointer<Int8>.allocate(capacity: len)
1057+
let buf = UnsafeMutablePointer<NativeFSRCharType>.allocate(capacity: len)
10311058
buf.initialize(repeating: 0, count: len)
1032-
if path._nsObject.getFileSystemRepresentation(buf, maxLength: len) {
1059+
if path._nsObject._getFileSystemRepresentation(buf, maxLength: len) {
10331060
return UnsafePointer(buf)
10341061
}
10351062
buf.deinitialize(count: len)
@@ -1038,13 +1065,13 @@ open class FileManager : NSObject {
10381065
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInvalidFileName.rawValue, userInfo: [NSFilePathErrorKey: path])
10391066
}
10401067

1041-
internal func _fileSystemRepresentation<ResultType>(withPath path: String, _ body: (UnsafePointer<Int8>) throws -> ResultType) throws -> ResultType {
1068+
internal func _fileSystemRepresentation<ResultType>(withPath path: String, _ body: (UnsafePointer<NativeFSRCharType>) throws -> ResultType) throws -> ResultType {
10421069
let fsRep = try __fileSystemRepresentation(withPath: path)
10431070
defer { fsRep.deallocate() }
10441071
return try body(fsRep)
10451072
}
10461073

1047-
internal func _fileSystemRepresentation<ResultType>(withPath path1: String, andPath path2: String, _ body: (UnsafePointer<Int8>, UnsafePointer<Int8>) throws -> ResultType) throws -> ResultType {
1074+
internal func _fileSystemRepresentation<ResultType>(withPath path1: String, andPath path2: String, _ body: (UnsafePointer<NativeFSRCharType>, UnsafePointer<NativeFSRCharType>) throws -> ResultType) throws -> ResultType {
10481075
let fsRep1 = try __fileSystemRepresentation(withPath: path1)
10491076
defer { fsRep1.deallocate() }
10501077
let fsRep2 = try __fileSystemRepresentation(withPath: path2)
@@ -1058,7 +1085,7 @@ open class FileManager : NSObject {
10581085
open func string(withFileSystemRepresentation str: UnsafePointer<Int8>, length len: Int) -> String {
10591086
return NSString(bytes: str, length: len, encoding: String.Encoding.utf8.rawValue)!._swiftObject
10601087
}
1061-
1088+
10621089
/* -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.
10631090

10641091
The `originalItemURL` is the item being replaced.

Foundation/NSPathUtilities.swift

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public func NSTemporaryDirectory() -> String {
1616
guard GetTempPathW(DWORD(wszPath.count), &wszPath) <= cchLength else {
1717
preconditionFailure("GetTempPathW mutation race")
1818
}
19-
return String(decodingCString: wszPath, as: UTF16.self)
19+
return String(decodingCString: wszPath, as: UTF16.self).standardizingPath
2020
#else
2121
#if canImport(Darwin)
2222
let safe_confstr = { (name: Int32, buf: UnsafeMutablePointer<Int8>?, len: Int) -> Int in
@@ -344,11 +344,28 @@ extension NSString {
344344
}
345345

346346
public var standardizingPath: String {
347+
let hasDirPath = validPathSeps.contains(where: { self.hasSuffix(String($0)) })
348+
#if os(Windows)
349+
// Convert to a posix style '/' separated path
350+
var converted = self as String
351+
if converted.isAbsolutePath {
352+
// If there is anything before the drive letter, e.g. "\\?\, \\host\, \??\", remove it
353+
if let idx = converted.firstIndex(of: ":") {
354+
converted.removeSubrange(..<converted.index(before: idx))
355+
}
356+
}
357+
converted = String(converted.map({ $0 == "\\" ? "/" : $0 }))
358+
let expanded = converted.expandingTildeInPath
359+
#else
347360
let expanded = expandingTildeInPath
361+
#endif
348362
var resolved = expanded._bridgeToObjectiveC().resolvingSymlinksInPath
349363

350364
let automount = "/var/automount"
351365
resolved = resolved._tryToRemovePathPrefix(automount) ?? resolved
366+
if hasDirPath, let last = resolved.last, last != "/" {
367+
resolved += "/"
368+
}
352369
return resolved
353370
}
354371

@@ -550,11 +567,61 @@ extension NSString {
550567
}
551568

552569
public func getFileSystemRepresentation(_ cname: UnsafeMutablePointer<Int8>, maxLength max: Int) -> Bool {
570+
#if os(Windows)
571+
let fsr = UnsafeMutablePointer<WCHAR>.allocate(capacity: max)
572+
defer { fsr.deallocate() }
573+
guard _getFileSystemRepresentation(fsr, maxLength: max) else { return false }
574+
return String(decodingCString: fsr, as: UTF16.self).withCString() {
575+
let chars = strnlen_s($0, max)
576+
guard chars < max else { return false }
577+
cname.assign(from: $0, count: chars + 1)
578+
return true
579+
}
580+
#else
581+
return _getFileSystemRepresentation(cname, maxLength: max)
582+
#endif
583+
}
584+
585+
internal func _getFileSystemRepresentation(_ cname: UnsafeMutablePointer<NativeFSRCharType>, maxLength max: Int) -> Bool {
553586
guard self.length > 0 else {
554587
return false
555588
}
556-
589+
#if os(Windows)
590+
var fsr = self._swiftObject
591+
let idx = fsr.startIndex
592+
593+
// If we have an RFC 8089 style path e.g. `/[drive-letter]:/...`, drop the
594+
// leading /, otherwise, a leading slash indicates a rooted path on the
595+
// drive for the current working directory
596+
if fsr.count >= 3 && fsr[idx] == "/" && fsr[fsr.index(after: idx)].isLetter && fsr[fsr.index(idx, offsetBy: 2)] == ":" {
597+
fsr.removeFirst()
598+
}
599+
600+
// Windows APIS that go through the path parser can handle
601+
// forward slashes in paths. However, symlinks created with
602+
// forward slashes do not resolve properly, so we normalize
603+
// the path separators anyways.
604+
fsr = fsr.replacingOccurrences(of: "/", with: "\\")
605+
606+
// Drop trailing slashes unless it follows a drive letter. On
607+
// Windows the path `C:\` indicates the root directory of the
608+
// `C:` drive. The path `C:` indicates the current working
609+
// directory on the `C:` drive.
610+
while fsr.count > 1
611+
&& (fsr[fsr.index(before: fsr.endIndex)] == "\\")
612+
&& !(fsr.count == 3 && fsr[fsr.index(fsr.endIndex, offsetBy: -2)] == ":") {
613+
fsr.removeLast()
614+
}
615+
616+
return fsr.withCString(encodedAs: UTF16.self) {
617+
let wchars = wcsnlen_s($0, max)
618+
guard wchars < max else { return false }
619+
cname.assign(from: $0, count: wchars + 1)
620+
return true
621+
}
622+
#else
557623
return CFStringGetFileSystemRepresentation(self._cfObject, cname, max)
624+
#endif
558625
}
559626

560627
}

Foundation/NSURL.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ internal let kCFURLPlatformPathStyle = kCFURLPOSIXPathStyle
2929
#endif
3030

3131
private func _standardizedPath(_ path: String) -> String {
32+
#if os(Windows)
33+
// Since Windows needs to support various types of paths, always attempt to
34+
// standardize what ever we're passed
35+
return path._nsObject.standardizingPath
36+
#else
3237
if !path.isAbsolutePath {
3338
return path._nsObject.standardizingPath
3439
}
3540
return path
41+
#endif
3642
}
3743

3844
internal func _pathComponents(_ path: String?) -> [String]? {
@@ -337,7 +343,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
337343
let thePath = _standardizedPath(path)
338344

339345
var isDir: ObjCBool = false
340-
if thePath.hasSuffix("/") {
346+
if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) {
341347
isDir = true
342348
} else {
343349
let absolutePath: String
@@ -360,14 +366,17 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
360366
public init(fileURLWithPath path: String) {
361367
let thePath: String
362368
let pathString = NSString(string: path)
363-
if !pathString.isAbsolutePath {
364-
thePath = pathString.standardizingPath
365-
} else {
369+
#if os(Windows)
370+
thePath = pathString.standardizingPath
371+
#else
372+
if pathString.isAbsolutePath {
366373
thePath = path
374+
} else {
375+
thePath = pathString.standardizingPath
367376
}
368-
377+
#endif
369378
var isDir: ObjCBool = false
370-
if thePath.hasSuffix("/") {
379+
if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) {
371380
isDir = true
372381
} else {
373382
if !FileManager.default.fileExists(atPath: path, isDirectory: &isDir) {
@@ -548,7 +557,19 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
548557

549558
open var path: String? {
550559
let absURL = CFURLCopyAbsoluteURL(_cfObject)
551-
return CFURLCopyFileSystemPath(absURL, kCFURLPlatformPathStyle)?._swiftObject
560+
guard var url = CFURLCopyFileSystemPath(absURL, kCFURLPOSIXPathStyle)?._swiftObject else {
561+
return nil
562+
}
563+
#if os(Windows)
564+
// Per RFC 8089:E.2, if we have an absolute Windows/DOS path
565+
// we can begin the url with a drive letter rather than a '/'
566+
let scalars = Array(url.unicodeScalars)
567+
if isFileURL, url.isAbsolutePath,
568+
scalars.count >= 3, scalars[0] == "/", scalars[2] == ":" {
569+
url.removeFirst()
570+
}
571+
#endif
572+
return url
552573
}
553574

554575
open var fragment: String? {
@@ -565,7 +586,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
565586

566587
// The same as path if baseURL is nil
567588
open var relativePath: String? {
568-
return CFURLCopyFileSystemPath(_cfObject, kCFURLPlatformPathStyle)?._swiftObject
589+
return CFURLCopyFileSystemPath(_cfObject, kCFURLPOSIXPathStyle)?._swiftObject
569590
}
570591

571592
/* 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.

TestFoundation/TestURL.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ class TestURL : XCTestCase {
6262
// ensure that the trailing slashes are compressed even when mixed
6363
// e.g. NOT file:///S:/b/u3%2F%/%2F%2/
6464
let u3 = URL(fileURLWithPath: "S:\\b\\u3//\\//")
65-
// XCTAssertEqual(u3.absoluteString, "file:///S:/b/u3/%2F/")
66-
XCTAssertEqual(u3.path, "S:\\b\\u3\\")
65+
// URL.path is defined to strip trailing slashes
66+
XCTAssertEqual(u3.absoluteString, "file:///S:/b/u3/")
67+
XCTAssertEqual(u3.path, "S:/b/u3")
6768

6869
// ensure that the regular conversion works
6970
let u4 = URL(fileURLWithPath: "S:\\b\\u4")

0 commit comments

Comments
 (0)