Skip to content

Commit e7660ff

Browse files
committed
Implemented _getFileSystemRepresentation for Windows
Modified the helper _getFileSystemRepresentation functions to return a UTF16 Windows style path.
1 parent 6a1ca96 commit e7660ff

File tree

4 files changed

+128
-30
lines changed

4 files changed

+128
-30
lines changed

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: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,24 @@ extension NSString {
344344
}
345345

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

350367
let automount = "/var/automount"
@@ -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: 15 additions & 6 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]? {
@@ -360,12 +366,15 @@ 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
370379
if thePath.hasSuffix("/") {
371380
isDir = true
@@ -548,7 +557,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
548557

549558
open var path: String? {
550559
let absURL = CFURLCopyAbsoluteURL(_cfObject)
551-
return CFURLCopyFileSystemPath(absURL, kCFURLPlatformPathStyle)?._swiftObject
560+
return CFURLCopyFileSystemPath(absURL, kCFURLPOSIXPathStyle)?._swiftObject
552561
}
553562

554563
open var fragment: String? {
@@ -565,7 +574,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
565574

566575
// The same as path if baseURL is nil
567576
open var relativePath: String? {
568-
return CFURLCopyFileSystemPath(_cfObject, kCFURLPlatformPathStyle)?._swiftObject
577+
return CFURLCopyFileSystemPath(_cfObject, kCFURLPOSIXPathStyle)?._swiftObject
569578
}
570579

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

0 commit comments

Comments
 (0)