Skip to content

Commit 2bf476e

Browse files
authored
Merge pull request swiftlang#2366 from gmittert/WinFSR
Implemented _getFileSystemRepresentation for Windows
2 parents 392d900 + 366d983 commit 2bf476e

File tree

6 files changed

+173
-46
lines changed

6 files changed

+173
-46
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: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ extension FileManager {
637637
var szDirectory: [WCHAR] = Array<WCHAR>(repeating: 0, count: Int(dwLength + 1))
638638

639639
GetCurrentDirectoryW(dwLength, &szDirectory)
640-
return String(decodingCString: &szDirectory, as: UTF16.self)
640+
return String(decodingCString: &szDirectory, as: UTF16.self).standardizingPath
641641
}
642642

643643
@discardableResult
@@ -701,8 +701,8 @@ extension FileManager {
701701
return true
702702
}
703703

704-
internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
705-
let _fsRep: UnsafePointer<Int8>
704+
internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<NativeFSRCharType>? = nil) throws -> stat {
705+
let _fsRep: UnsafePointer<NativeFSRCharType>
706706
if fsRep == nil {
707707
_fsRep = try __fileSystemRepresentation(withPath: path)
708708
} else {
@@ -714,15 +714,13 @@ extension FileManager {
714714
}
715715

716716
var statInfo = stat()
717-
let h = path.withCString(encodedAs: UTF16.self) {
718-
CreateFileW(/*lpFileName=*/$0,
719-
/*dwDesiredAccess=*/DWORD(0),
720-
/*dwShareMode=*/DWORD(FILE_SHARE_READ),
721-
/*lpSecurityAttributes=*/nil,
722-
/*dwCreationDisposition=*/DWORD(OPEN_EXISTING),
723-
/*dwFlagsAndAttributes=*/DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS),
724-
/*hTemplateFile=*/nil)
725-
}
717+
let h = CreateFileW(_fsRep,
718+
/*dwDesiredAccess=*/DWORD(0),
719+
DWORD(FILE_SHARE_READ),
720+
/*lpSecurityAttributes=*/nil,
721+
DWORD(OPEN_EXISTING),
722+
DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS),
723+
/*hTemplateFile=*/nil)
726724
if h == INVALID_HANDLE_VALUE {
727725
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
728726
}
@@ -858,7 +856,7 @@ extension FileManager {
858856
}
859857

860858
internal func _updateTimes(atPath path: String,
861-
withFileSystemRepresentation fsr: UnsafePointer<Int8>,
859+
withFileSystemRepresentation fsr: UnsafePointer<NativeFSRCharType>,
862860
creationTime: Date? = nil,
863861
accessTime: Date? = nil,
864862
modificationTime: Date? = nil) throws {
@@ -869,10 +867,7 @@ extension FileManager {
869867
var mtime: FILETIME =
870868
FILETIME(from: time_t((modificationTime ?? stat.lastModificationDate).timeIntervalSince1970))
871869

872-
let hFile: HANDLE = String(utf8String: fsr)!.withCString(encodedAs: UTF16.self) {
873-
CreateFileW($0, DWORD(GENERIC_WRITE), DWORD(FILE_SHARE_WRITE),
874-
nil, DWORD(OPEN_EXISTING), 0, nil)
875-
}
870+
let hFile = CreateFileW(fsr, DWORD(GENERIC_WRITE), DWORD(FILE_SHARE_WRITE), nil, DWORD(OPEN_EXISTING), 0, nil)
876871
if hFile == INVALID_HANDLE_VALUE {
877872
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path])
878873
}

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: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public func NSTemporaryDirectory() -> String {
2222
guard GetTempPathW(DWORD(wszPath.count), &wszPath) <= cchLength else {
2323
preconditionFailure("GetTempPathW mutation race")
2424
}
25-
return String(decodingCString: wszPath, as: UTF16.self)
25+
return String(decodingCString: wszPath, as: UTF16.self).standardizingPath
2626
#else
2727
#if canImport(Darwin)
2828
let safe_confstr = { (name: Int32, buf: UnsafeMutablePointer<Int8>?, len: Int) -> Int in
@@ -348,14 +348,32 @@ extension NSString {
348348

349349
return result
350350
}
351-
351+
352+
#if os(Windows)
353+
// Convert to a posix style '/' separated path
354+
internal var unixPath: String {
355+
var droppedPrefix = self as String
356+
// If there is anything before the drive letter,
357+
// e.g. "\\?\, \\host\, \??\", remove it
358+
if isAbsolutePath, let idx = droppedPrefix.firstIndex(of: ":") {
359+
droppedPrefix.removeSubrange(..<droppedPrefix.index(before: idx))
360+
}
361+
let slashesConverted = String(droppedPrefix.map({ $0 == "\\" ? "/" : $0 }))
362+
let compressTrailing = slashesConverted._stringByFixingSlashes(stripTrailing: false)
363+
return compressTrailing
364+
}
365+
#endif
366+
352367
public var standardizingPath: String {
368+
#if os(Windows)
369+
let expanded = unixPath.expandingTildeInPath
370+
#else
353371
let expanded = expandingTildeInPath
354-
var resolved = expanded._bridgeToObjectiveC().resolvingSymlinksInPath
372+
#endif
373+
let resolved = expanded._bridgeToObjectiveC().resolvingSymlinksInPath
355374

356375
let automount = "/var/automount"
357-
resolved = resolved._tryToRemovePathPrefix(automount) ?? resolved
358-
return resolved
376+
return resolved._tryToRemovePathPrefix(automount) ?? resolved
359377
}
360378

361379
public var resolvingSymlinksInPath: String {
@@ -554,11 +572,61 @@ extension NSString {
554572
}
555573

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

564632
}

Foundation/NSURL.swift

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ private func _standardizedPath(_ path: String) -> String {
3030
if !path.isAbsolutePath {
3131
return path._nsObject.standardizingPath
3232
}
33+
#if os(Windows)
34+
return path.unixPath
35+
#else
3336
return path
37+
#endif
3438
}
3539

3640
internal func _pathComponents(_ path: String?) -> [String]? {
@@ -335,7 +339,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
335339
let thePath = _standardizedPath(path)
336340

337341
var isDir: ObjCBool = false
338-
if thePath.hasSuffix("/") {
342+
if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) {
339343
isDir = true
340344
} else {
341345
let absolutePath: String
@@ -356,16 +360,9 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
356360
}
357361

358362
public init(fileURLWithPath path: String) {
359-
let thePath: String
360-
let pathString = NSString(string: path)
361-
if !pathString.isAbsolutePath {
362-
thePath = pathString.standardizingPath
363-
} else {
364-
thePath = path
365-
}
366-
363+
let thePath = _standardizedPath(path)
367364
var isDir: ObjCBool = false
368-
if thePath.hasSuffix("/") {
365+
if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) {
369366
isDir = true
370367
} else {
371368
if !FileManager.default.fileExists(atPath: path, isDirectory: &isDir) {
@@ -542,7 +539,19 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
542539

543540
open var path: String? {
544541
let absURL = CFURLCopyAbsoluteURL(_cfObject)
545-
return CFURLCopyFileSystemPath(absURL, kCFURLPlatformPathStyle)?._swiftObject
542+
guard var url = CFURLCopyFileSystemPath(absURL, kCFURLPOSIXPathStyle)?._swiftObject else {
543+
return nil
544+
}
545+
#if os(Windows)
546+
// Per RFC 8089:E.2, if we have an absolute Windows/DOS path
547+
// we can begin the url with a drive letter rather than a '/'
548+
let scalars = Array(url.unicodeScalars)
549+
if isFileURL, url.isAbsolutePath,
550+
scalars.count >= 3, scalars[0] == "/", scalars[2] == ":" {
551+
url.removeFirst()
552+
}
553+
#endif
554+
return url
546555
}
547556

548557
open var fragment: String? {
@@ -559,7 +568,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
559568

560569
// The same as path if baseURL is nil
561570
open var relativePath: String? {
562-
return CFURLCopyFileSystemPath(_cfObject, kCFURLPlatformPathStyle)?._swiftObject
571+
return CFURLCopyFileSystemPath(_cfObject, kCFURLPOSIXPathStyle)?._swiftObject
563572
}
564573

565574
/* 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)