From 264ceae43b317c1289154d082c605f0c868f72ac Mon Sep 17 00:00:00 2001 From: Gwen Mittertreiner Date: Fri, 3 May 2019 10:22:12 -0700 Subject: [PATCH] Fix Up FileManager Tests on Windows - Windows directory enumerator now doesn't look in a directory it returns until nextObject is called again - Fixed some path handling platform issues - Properly calculate level --- CoreFoundation/URL.subproj/CFURL.c | 2 +- Foundation/FileManager+Win32.swift | 106 +++++++++++++-------------- Foundation/NSPathUtilities.swift | 6 +- Foundation/NSURL.swift | 4 +- TestFoundation/TestFileManager.swift | 35 +++++++-- 5 files changed, 87 insertions(+), 66 deletions(-) diff --git a/CoreFoundation/URL.subproj/CFURL.c b/CoreFoundation/URL.subproj/CFURL.c index 2229a92069..49d07d9d90 100644 --- a/CoreFoundation/URL.subproj/CFURL.c +++ b/CoreFoundation/URL.subproj/CFURL.c @@ -4292,7 +4292,7 @@ static CFStringRef URLPathToWindowsPath(CFStringRef path, CFAllocatorRef allocat static CFStringRef _resolveFileSystemPaths(CFStringRef relativePath, CFStringRef basePath, Boolean baseIsDir, CFURLPathStyle fsType, CFAllocatorRef alloc) CF_RETURNS_RETAINED { CFIndex baseLen = CFStringGetLength(basePath); CFIndex relLen = CFStringGetLength(relativePath); - UniChar pathDelimiter = '/'; + UniChar pathDelimiter = _CFGetSlash(); UniChar *buf = (UniChar *)CFAllocatorAllocate(alloc, sizeof(UniChar)*(relLen + baseLen + 2), 0); CFStringGetCharacters(basePath, CFRangeMake(0, baseLen), buf); if (baseIsDir) { diff --git a/Foundation/FileManager+Win32.swift b/Foundation/FileManager+Win32.swift index 784df30cca..9d2a7ff219 100644 --- a/Foundation/FileManager+Win32.swift +++ b/Foundation/FileManager+Win32.swift @@ -652,64 +652,69 @@ extension FileManager { var _options : FileManager.DirectoryEnumerationOptions var _errorHandler : ((URL, Error) -> Bool)? var _stack: [URL] - var _current: URL? + var _lastReturned: URL var _rootDepth : Int init(url: URL, options: FileManager.DirectoryEnumerationOptions, errorHandler: (/* @escaping */ (URL, Error) -> Bool)?) { _options = options _errorHandler = errorHandler - _stack = [url] + _stack = [] _rootDepth = url.pathComponents.count + _lastReturned = url } override func nextObject() -> Any? { - func contentsOfDir(directory: URL) -> [URL]? { - var ffd: WIN32_FIND_DATAW = WIN32_FIND_DATAW() - guard let dirFSR = directory.withUnsafeFileSystemRepresentation({ $0.flatMap { fsr in String(utf8String: fsr) } }) - else { return nil } - let dirPath = joinPath(prefix: dirFSR, suffix: "*") - let h: HANDLE = dirPath.withCString(encodedAs: UTF16.self) { + func firstValidItem() -> URL? { + while let url = _stack.popLast() { + if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) { + guard let handler = _errorHandler, + handler(url, _NSErrorWithWindowsError(GetLastError(), reading: true)) + else { return nil } + continue + } + _lastReturned = url + return _lastReturned + } + return nil + } + + // If we most recently returned a directory, decend into it + var isDir: ObjCBool = false + guard FileManager.default.fileExists(atPath: _lastReturned.path, isDirectory: &isDir) else { + guard let handler = _errorHandler, + handler(_lastReturned, _NSErrorWithWindowsError(GetLastError(), reading: true)) + else { return nil } + return firstValidItem() + } + + if isDir.boolValue && (level == 0 || !_options.contains(.skipsSubdirectoryDescendants)) { + var ffd = WIN32_FIND_DATAW() + let dirPath = joinPath(prefix: _lastReturned.path, suffix: "*") + let handle = dirPath.withCString(encodedAs: UTF16.self) { FindFirstFileW($0, &ffd) } - guard h != INVALID_HANDLE_VALUE else { return nil } - defer { FindClose(h) } + guard handle != INVALID_HANDLE_VALUE else { return firstValidItem() } + defer { FindClose(handle) } - var files: [URL] = [] repeat { let fileArr = Array( UnsafeBufferPointer(start: &ffd.cFileName.0, count: MemoryLayout.size(ofValue: ffd.cFileName))) let file = String(decodingCString: fileArr, as: UTF16.self) - if file != "." - && file != ".." + if file != "." && file != ".." && (!_options.contains(.skipsHiddenFiles) || (ffd.dwFileAttributes & DWORD(FILE_ATTRIBUTE_HIDDEN) == 0)) { - files.append(URL(fileURLWithPath: joinPath(prefix: dirFSR, suffix: file))) - } - } while FindNextFileW(h, &ffd) - return files - } - while let url = _stack.popLast() { - if url.hasDirectoryPath && !_options.contains(.skipsSubdirectoryDescendants) { - guard let dirContents = contentsOfDir(directory: url)?.reversed() else { - if let handler = _errorHandler { - let dirFSR = url.withUnsafeFileSystemRepresentation { $0.flatMap { fsr in String(utf8String: fsr) } } - let keepGoing = handler(URL(fileURLWithPath: dirFSR ?? ""), - _NSErrorWithWindowsError(GetLastError(), reading: true)) - if !keepGoing { return nil } - } - continue + let relative = URL(fileURLWithPath: file, relativeTo: _lastReturned) + _stack.append(relative) } - _stack.append(contentsOf: dirContents) - } - _current = url - return url + } while FindNextFileW(handle, &ffd) } - return nil + + return firstValidItem() } override var level: Int { - return _rootDepth - (_current?.pathComponents.count ?? _rootDepth) + return _lastReturned.pathComponents.count - _rootDepth } override func skipDescendants() { @@ -728,31 +733,24 @@ extension FileManager { extension FileManager.NSPathDirectoryEnumerator { internal func _nextObject() -> Any? { - let o = innerEnumerator.nextObject() - guard let url = o as? URL else { - return nil - } + guard let url = innerEnumerator.nextObject() as? URL else { return nil } - var relativePath = UnsafeMutableBufferPointer.allocate(capacity: Int(MAX_PATH)) - defer { relativePath.deallocate() } - func withURLCString(url: URL, _ f: (UnsafePointer) -> Result?) -> Result? { - return url.withUnsafeFileSystemRepresentation { fsr in - (fsr.flatMap { String(utf8String: $0) })?.withCString(encodedAs: UTF16.self) { f($0) } - } - } - guard withURLCString(url: baseURL, { pszFrom -> Bool? in - withURLCString(url: url) { pszTo in - let fromAttrs = GetFileAttributesW(pszFrom) - let toAttrs = GetFileAttributesW(pszTo) + var relativePath: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH)) + + guard baseURL._withUnsafeWideFileSystemRepresentation({ baseUrlFsr in + url._withUnsafeWideFileSystemRepresentation { urlFsr in + let fromAttrs = GetFileAttributesW(baseUrlFsr) + let toAttrs = GetFileAttributesW(urlFsr) guard fromAttrs != INVALID_FILE_ATTRIBUTES, toAttrs != INVALID_FILE_ATTRIBUTES else { return false } - return PathRelativePathToW(relativePath.baseAddress, pszFrom, fromAttrs, pszTo, toAttrs) + return PathRelativePathToW(&relativePath, baseUrlFsr, fromAttrs, urlFsr, toAttrs) } - }) == true, let (path, _) = String.decodeCString(relativePath.baseAddress, as: UTF16.self) else { - return nil - } - _currentItemPath = path + }) else { return nil } + + let path = String(decodingCString: &relativePath, as: UTF16.self) + // Drop the leading ".\" from the path + _currentItemPath = String(path.dropFirst(2)) return _currentItemPath } diff --git a/Foundation/NSPathUtilities.swift b/Foundation/NSPathUtilities.swift index 9f51cd3b1e..005203334f 100755 --- a/Foundation/NSPathUtilities.swift +++ b/Foundation/NSPathUtilities.swift @@ -117,8 +117,12 @@ extension String { return nil } - internal var absolutePath: Bool { + internal var isAbsolutePath: Bool { +#if os(Windows) + return !withCString(encodedAs: UTF16.self, PathIsRelativeW) +#else return hasPrefix("~") || hasPrefix("/") +#endif } internal func _stringByAppendingPathComponent(_ str: String, doneAppending : Bool = true) -> String { diff --git a/Foundation/NSURL.swift b/Foundation/NSURL.swift index a015e0dcfa..6abbe0059a 100644 --- a/Foundation/NSURL.swift +++ b/Foundation/NSURL.swift @@ -29,7 +29,7 @@ internal let kCFURLPlatformPathStyle = kCFURLPOSIXPathStyle #endif private func _standardizedPath(_ path: String) -> String { - if !path.absolutePath { + if !path.isAbsolutePath { return path._nsObject.standardizingPath } return path @@ -1007,7 +1007,7 @@ extension NSURL { } let absolutePath: String - if selfPath.hasPrefix("/") { + if selfPath.isAbsolutePath { absolutePath = selfPath } else { let workingDir = FileManager.default.currentDirectoryPath diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index 9ef912422a..8895283acf 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -16,6 +16,11 @@ #endif class TestFileManager : XCTestCase { +#if os(Windows) + let pathSep = "\\" +#else + let pathSep = "/" +#endif func test_createDirectory() { let fm = FileManager.default @@ -449,11 +454,11 @@ class TestFileManager : XCTestCase { foundItems.insert(item) if item == "item" { item1FileAttributes = e.fileAttributes - } else if item == "path2/item" { + } else if item == "path2\(pathSep)item" { item2FileAttributes = e.fileAttributes } } - XCTAssertEqual(foundItems, Set(["item", "path2", "path2/item"])) + XCTAssertEqual(foundItems, Set(["item", "path2", "path2\(pathSep)item"])) } else { XCTFail() } @@ -492,10 +497,17 @@ class TestFileManager : XCTestCase { let fm = FileManager.default let basePath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)/" let subDirs1 = basePath + "subdir1/subdir2/.hiddenDir/subdir3/" - let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5./.subdir6.ext/subdir7.ext./" let itemPath1 = basePath + "itemFile1" +#if os(Windows) + // Filenames ending with '.' are not valid on Windows, so don't bother testing them + let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5/.subdir6.ext/subdir7.ext/" + let itemPath2 = subDirs1 + "itemFile2" + let itemPath3 = subDirs1 + "itemFile3.ext" +#else + let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5./.subdir6.ext/subdir7.ext./" let itemPath2 = subDirs1 + "itemFile2." let itemPath3 = subDirs1 + "itemFile3.ext." +#endif let hiddenItem1 = basePath + ".hiddenFile1" let hiddenItem2 = subDirs1 + ".hiddenFile2" let hiddenItem3 = subDirs2 + ".hiddenFile3" @@ -507,17 +519,24 @@ class TestFileManager : XCTestCase { "subdir1": 1, "subdir2": 2, "subdir4.app": 3, - "subdir5.": 4, ".subdir6.ext": 5, - "subdir7.ext.": 6, ".hiddenFile4.ext": 7, ".hiddenFile3": 7, ".hiddenDir": 3, "subdir3": 4, - "itemFile3.ext.": 5, - "itemFile2.": 5, - ".hiddenFile2": 5 + ".hiddenFile2": 5, ] +#if os(Windows) + fileLevels["itemFile2"] = 5 + fileLevels["subdir5"] = 4 + fileLevels["subdir7.ext"] = 6 + fileLevels["itemFile3.ext"] = 5 +#else + fileLevels["itemFile2."] = 5 + fileLevels["subdir5."] = 4 + fileLevels["subdir7.ext."] = 6 + fileLevels["itemFile3.ext."] = 5 +#endif func directoryItems(options: FileManager.DirectoryEnumerationOptions) -> [String: Int]? { if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath), includingPropertiesForKeys: nil, options: options, errorHandler: nil) {