From 2544436ffeca9b238cda0d8ade90188118231229 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Wed, 13 Jun 2018 14:33:37 -0600 Subject: [PATCH 1/4] Revert "Merge pull request #1598 from apple/revert-1597-xdg" This reverts commit ac8a8eb6b56d38e4a2fe124300b1c5a5faa0da96, reversing changes made to 1afa1ef49971deb743c4d3ed1f6e0b5f2669449e. --- Foundation.xcodeproj/project.pbxproj | 9 +- Foundation/FileManager.swift | 325 +++++++++++++++++++++++- Foundation/FileManager_XDG.swift | 125 +++++++++ Foundation/NSPathUtilities.swift | 24 +- TestFoundation/TestFileManager.swift | 214 +++++++++++++++- TestFoundation/TestProcess.swift | 2 +- TestFoundation/xdgTestHelper/main.swift | 83 +++++- build.py | 7 +- lib/phases.py | 7 +- lib/script.py | 2 +- 10 files changed, 782 insertions(+), 16 deletions(-) create mode 100644 Foundation/FileManager_XDG.swift diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 04d99d86ef..9166242a56 100644 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0383A1751D2E558A0052E5D1 /* TestStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0383A1741D2E558A0052E5D1 /* TestStream.swift */; }; 03B6F5841F15F339004F25AF /* TestURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B6F5831F15F339004F25AF /* TestURLProtocol.swift */; }; + 1513A8432044893F00539722 /* FileManager_XDG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1513A8422044893F00539722 /* FileManager_XDG.swift */; }; 1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */; }; 153E951120111DC500F250BE /* CFKnownLocations.h in Headers */ = {isa = PBXBuildFile; fileRef = 153E950F20111DC500F250BE /* CFKnownLocations.h */; settings = {ATTRIBUTES = (Private, ); }; }; 153E951220111DC500F250BE /* CFKnownLocations.c in Sources */ = {isa = PBXBuildFile; fileRef = 153E951020111DC500F250BE /* CFKnownLocations.c */; }; @@ -527,6 +528,7 @@ /* Begin PBXFileReference section */ 0383A1741D2E558A0052E5D1 /* TestStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestStream.swift; sourceTree = ""; }; 03B6F5831F15F339004F25AF /* TestURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLProtocol.swift; sourceTree = ""; }; + 1513A8422044893F00539722 /* FileManager_XDG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager_XDG.swift; sourceTree = ""; }; 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPServer.swift; sourceTree = ""; }; 153E950F20111DC500F250BE /* CFKnownLocations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CFKnownLocations.h; sourceTree = ""; }; 153E951020111DC500F250BE /* CFKnownLocations.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CFKnownLocations.c; sourceTree = ""; }; @@ -1858,6 +1860,7 @@ children = ( EADE0B5D1BD15DFF00C49C64 /* FileHandle.swift */, EADE0B5E1BD15DFF00C49C64 /* FileManager.swift */, + 1513A8422044893F00539722 /* FileManager_XDG.swift */, EADE0B7A1BD15DFF00C49C64 /* Process.swift */, 5BDC3F2F1BCC5DCB00ED97BB /* Bundle.swift */, 5BDC3F411BCC5DCB00ED97BB /* ProcessInfo.swift */, @@ -2248,7 +2251,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cp ${BUILD_ROOT}/Debug/xdgTestHelper.app/Contents/MacOS/xdgTestHelper ${BUILD_ROOT}/Debug/TestFoundation.app/Contents/MacOS/"; + shellScript = "cp ${BUILT_PRODUCTS_DIR}/xdgTestHelper.app/Contents/MacOS/xdgTestHelper ${BUILT_PRODUCTS_DIR}/TestFoundation.app/Contents/MacOS/\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -2388,6 +2391,7 @@ 5BECBA3A1D1CAE9A00B39B1F /* NSMeasurement.swift in Sources */, 5BF7AEB21BCD51F9008F214A /* NSNumber.swift in Sources */, 61D2F9AF1FECFB3E0033306A /* NativeProtocol.swift in Sources */, + 1513A8432044893F00539722 /* FileManager_XDG.swift in Sources */, B9974B991EDF4A22007F15B8 /* HTTPURLProtocol.swift in Sources */, 5BCD03821D3EE35C00E3FF9B /* TimeZone.swift in Sources */, EADE0BBC1BD15E0000C49C64 /* URLCache.swift in Sources */, @@ -2694,6 +2698,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT DEBUG"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -2761,6 +2766,7 @@ DYLIB_COMPATIBILITY_VERSION = 150; DYLIB_CURRENT_VERSION = 1303; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = A; GCC_PREFIX_HEADER = CoreFoundation/Base.subproj/CoreFoundation_Prefix.h; HEADER_SEARCH_PATHS = ( @@ -2834,6 +2840,7 @@ DYLIB_COMPATIBILITY_VERSION = 150; DYLIB_CURRENT_VERSION = 1303; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = A; GCC_PREFIX_HEADER = CoreFoundation/Base.subproj/CoreFoundation_Prefix.h; HEADER_SEARCH_PATHS = ( diff --git a/Foundation/FileManager.swift b/Foundation/FileManager.swift index 61c625c37f..a41ea5eac0 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -7,9 +7,11 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // -#if os(macOS) || os(iOS) +#if canImport(Darwin) import Darwin -#elseif os(Linux) || CYGWIN +#endif + +#if canImport(Glibc) import Glibc #endif @@ -120,10 +122,297 @@ open class FileManager : NSObject { return result } + private enum _SearchPathDomain { + case system + case local + case network + case user + + init?(_ domainMask: SearchPathDomainMask) { + if domainMask == .systemDomainMask { + self = .system; return + } + if domainMask == .localDomainMask { + self = .local; return + } + if domainMask == .networkDomainMask { + self = .network; return + } + if domainMask == .userDomainMask { + self = .user; return + } + + return nil + } + } + + private func darwinPathURLs(for domain: _SearchPathDomain, system: String?, local: String?, network: String?, userHomeSubpath: String?) -> [URL] { + switch domain { + case .system: + guard let path = system else { return [] } + return [ URL(fileURLWithPath: path, isDirectory: true) ] + case .local: + guard let path = local else { return [] } + return [ URL(fileURLWithPath: path, isDirectory: true) ] + case .network: + guard let path = network else { return [] } + return [ URL(fileURLWithPath: path, isDirectory: true) ] + case .user: + guard let path = userHomeSubpath else { return [] } + return [ URL(fileURLWithPath: path, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + } + } + + private func darwinPathURLs(for domain: _SearchPathDomain, all: String, useLocalDirectoryForSystem: Bool = false) -> [URL] { + switch domain { + case .system: + return [ URL(fileURLWithPath: useLocalDirectoryForSystem ? "/\(all)" : "/System/\(all)", isDirectory: true) ] + case .local: + return [ URL(fileURLWithPath: "/\(all)", isDirectory: true) ] + case .network: + return [ URL(fileURLWithPath: "/Network/\(all)", isDirectory: true) ] + case .user: + return [ URL(fileURLWithPath: all, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + } + } + + #if os(Windows) // Non-Apple OSes that do not implement FHS/XDG are not currently supported. + @available(*, unavailable, message: "Not implemented for this OS") + open func urls(for directory: SearchPathDirectory, in domainMask: SearchPathDomainMask) -> [URL] { + NSUnimplemented() + } + #else /* -URLsForDirectory:inDomains: is analogous to NSSearchPathForDirectoriesInDomains(), but returns an array of NSURL instances for use with URL-taking APIs. This API is suitable when you need to search for a file or files which may live in one of a variety of locations in the domains specified. */ open func urls(for directory: SearchPathDirectory, in domainMask: SearchPathDomainMask) -> [URL] { - NSUnimplemented() + guard let domain = _SearchPathDomain(domainMask) else { + fatalError("Values other than .systemDomainMask, .localDomainMask, .userDomainMask, .networkDomainMask are unsupported") + } + + // We are going to return appropriate paths on Darwin, but [] on platforms that do not have comparable locations. + // For example, on FHS/XDG systems, applications are not installed in a single path. + + let useDarwinPaths: Bool + if let envVar = ProcessInfo.processInfo.environment["_NSFileManagerUseXDGPathsForDirectoryDomains"] { + useDarwinPaths = !NSString(string: envVar).boolValue + } else { + #if canImport(Darwin) + useDarwinPaths = true + #else + useDarwinPaths = false + #endif + } + + if useDarwinPaths { + return darwinURLs(for: directory, in: domain) + } else { + return xdgURLs(for: directory, in: domain) + } + } + #endif + + private func xdgURLs(for directory: SearchPathDirectory, in domain: _SearchPathDomain) -> [URL] { + // FHS/XDG-compliant OSes: + switch directory { + case .autosavedInformationDirectory: + let runtimePath = _SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String + return [ URL(fileURLWithPath: "Autosave Information", isDirectory: true, relativeTo: URL(fileURLWithPath: runtimePath, isDirectory: true)) ] + + case .desktopDirectory: + guard domain == .user else { return [] } + return [ _XDGUserDirectory.desktop.url ] + + case .documentDirectory: + guard domain == .user else { return [] } + return [ _XDGUserDirectory.documents.url ] + + case .cachesDirectory: + guard domain == .user else { return [] } + let path = _SwiftValue.fetch(nonOptional: _CFXDGCreateCacheDirectoryPath()) as! String + return [ URL(fileURLWithPath: path, isDirectory: true) ] + + case .applicationSupportDirectory: + guard domain == .user else { return [] } + let path = _SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String + return [ URL(fileURLWithPath: path, isDirectory: true) ] + + case .downloadsDirectory: + guard domain == .user else { return [] } + return [ _XDGUserDirectory.download.url ] + + case .userDirectory: + guard domain == .local else { return [] } + return [ URL(fileURLWithPath: "/home", isDirectory: true) ] + + case .moviesDirectory: + return [ _XDGUserDirectory.videos.url ] + + case .musicDirectory: + guard domain == .user else { return [] } + return [ _XDGUserDirectory.music.url ] + + case .picturesDirectory: + guard domain == .user else { return [] } + return [ _XDGUserDirectory.pictures.url ] + + case .sharedPublicDirectory: + guard domain == .user else { return [] } + return [ _XDGUserDirectory.publicShare.url ] + + case .trashDirectory: + let userTrashURL = URL(fileURLWithPath: ".Trash", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) + if domain == .user || domain == .local { + return [ userTrashURL ] + } else { + return [] + } + + // None of these are supported outside of Darwin: + case .applicationDirectory: + fallthrough + case .demoApplicationDirectory: + fallthrough + case .developerApplicationDirectory: + fallthrough + case .adminApplicationDirectory: + fallthrough + case .libraryDirectory: + fallthrough + case .developerDirectory: + fallthrough + case .documentationDirectory: + fallthrough + case .coreServiceDirectory: + fallthrough + case .inputMethodsDirectory: + fallthrough + case .preferencePanesDirectory: + fallthrough + case .applicationScriptsDirectory: + fallthrough + case .allApplicationsDirectory: + fallthrough + case .allLibrariesDirectory: + fallthrough + case .printerDescriptionDirectory: + fallthrough + case .itemReplacementDirectory: + return [] + } + } + + private func darwinURLs(for directory: SearchPathDirectory, in domain: _SearchPathDomain) -> [URL] { + switch directory { + case .applicationDirectory: + return darwinPathURLs(for: domain, all: "Applications", useLocalDirectoryForSystem: true) + + case .demoApplicationDirectory: + return darwinPathURLs(for: domain, all: "Demos", useLocalDirectoryForSystem: true) + + case .developerApplicationDirectory: + return darwinPathURLs(for: domain, all: "Developer/Applications", useLocalDirectoryForSystem: true) + + case .adminApplicationDirectory: + return darwinPathURLs(for: domain, all: "Applications/Utilities", useLocalDirectoryForSystem: true) + + case .libraryDirectory: + return darwinPathURLs(for: domain, all: "Library") + + case .developerDirectory: + return darwinPathURLs(for: domain, all: "Developer", useLocalDirectoryForSystem: true) + + case .documentationDirectory: + return darwinPathURLs(for: domain, all: "Library/Documentation") + + case .coreServiceDirectory: + return darwinPathURLs(for: domain, system: "/System/Library/CoreServices", local: nil, network: nil, userHomeSubpath: nil) + + case .autosavedInformationDirectory: + return darwinPathURLs(for: domain, system: nil, local: nil, network: nil, userHomeSubpath: "Library/Autosave Information") + + case .inputMethodsDirectory: + return darwinPathURLs(for: domain, all: "Library/Input Methods") + + case .preferencePanesDirectory: + return darwinPathURLs(for: domain, system: "/System/Library/PreferencePanes", local: "/Library/PreferencePanes", network: nil, userHomeSubpath: "Library/PreferencePanes") + + case .applicationScriptsDirectory: + // Only the ObjC Foundation can know where this is. + return [] + + case .allApplicationsDirectory: + var directories: [URL] = [] + directories.append(contentsOf: darwinPathURLs(for: domain, all: "Applications", useLocalDirectoryForSystem: true)) + directories.append(contentsOf: darwinPathURLs(for: domain, all: "Demos", useLocalDirectoryForSystem: true)) + directories.append(contentsOf: darwinPathURLs(for: domain, all: "Developer/Applications", useLocalDirectoryForSystem: true)) + directories.append(contentsOf: darwinPathURLs(for: domain, all: "Applications/Utilities", useLocalDirectoryForSystem: true)) + return directories + + case .allLibrariesDirectory: + var directories: [URL] = [] + directories.append(contentsOf: darwinPathURLs(for: domain, all: "Library")) + directories.append(contentsOf: darwinPathURLs(for: domain, all: "Developer")) + return directories + + case .printerDescriptionDirectory: + guard domain == .system else { return [] } + return [ URL(fileURLWithPath: "/System/Library/Printers/PPD", isDirectory: true) ] + + case .desktopDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Desktop", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .documentDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Documents", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .cachesDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Library/Caches", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .applicationSupportDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Library/Application Support", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .downloadsDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Downloads", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .userDirectory: + return darwinPathURLs(for: domain, system: nil, local: "/Users", network: "/Network/Users", userHomeSubpath: nil) + + case .moviesDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Movies", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .musicDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Music", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .picturesDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Pictures", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .sharedPublicDirectory: + guard domain == .user else { return [] } + return [ URL(fileURLWithPath: "Public", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] + + case .trashDirectory: + let userTrashURL = URL(fileURLWithPath: ".Trash", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) + if domain == .user || domain == .local { + return [ userTrashURL ] + } else { + return [] + } + + case .itemReplacementDirectory: + // This directory is only returned by url(for:in:appropriateFor:create:) + return [] + } + } + + private enum URLForDirectoryError: Error { + case directoryUnknown } /* -URLForDirectory:inDomain:appropriateForURL:create:error: is a URL-based replacement for FSFindFolder(). It allows for the specification and (optional) creation of a specific directory for a particular purpose (e.g. the replacement of a particular item on disk, or a particular Library directory. @@ -131,7 +420,35 @@ open class FileManager : NSObject { You may pass only one of the values from the NSSearchPathDomainMask enumeration, and you may not pass NSAllDomainsMask. */ open func url(for directory: SearchPathDirectory, in domain: SearchPathDomainMask, appropriateFor url: URL?, create shouldCreate: Bool) throws -> URL { - NSUnimplemented() + let urls = self.urls(for: directory, in: domain) + guard let url = urls.first else { + // On Apple OSes, this case returns nil without filling in the error parameter; Swift then synthesizes an error rather than trap. + // We simulate that behavior by throwing a private error. + throw URLForDirectoryError.directoryUnknown + } + + if shouldCreate { + var attributes: [FileAttributeKey : Any] = [:] + + switch _SearchPathDomain(domain) { + case .some(.user): + attributes[.posixPermissions] = 0700 + + case .some(.system): + attributes[.posixPermissions] = 0755 + attributes[.ownerAccountID] = 0 // root + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + attributes[.ownerAccountID] = 80 // on Darwin, the admin group's fixed ID. + #endif + + default: + break + } + + try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes) + } + + return url } /* Sets 'outRelationship' to NSURLRelationshipContains if the directory at 'directoryURL' directly or indirectly contains the item at 'otherURL', meaning 'directoryURL' is found while enumerating parent URLs starting from 'otherURL'. Sets 'outRelationship' to NSURLRelationshipSame if 'directoryURL' and 'otherURL' locate the same item, meaning they have the same NSURLFileResourceIdentifierKey value. If 'directoryURL' is not a directory, or does not contain 'otherURL' and they do not locate the same file, then sets 'outRelationship' to NSURLRelationshipOther. If an error occurs, returns NO and sets 'error'. diff --git a/Foundation/FileManager_XDG.swift b/Foundation/FileManager_XDG.swift new file mode 100644 index 0000000000..e44e9b0be7 --- /dev/null +++ b/Foundation/FileManager_XDG.swift @@ -0,0 +1,125 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// + +import CoreFoundation + +enum _XDGUserDirectory: String { + case desktop = "DESKTOP" + case download = "DOWNLOAD" + case publicShare = "PUBLICSHARE" + case documents = "DOCUMENTS" + case music = "MUSIC" + case pictures = "PICTURES" + case videos = "VIDEOS" + + static let allDirectories: [_XDGUserDirectory] = [ + .desktop, + .download, + .publicShare, + .documents, + .music, + .pictures, + .videos, + ] + + var url: URL { + return url(userConfiguration: _XDGUserDirectory.configuredDirectoryURLs, + osDefaultConfiguration: _XDGUserDirectory.osDefaultDirectoryURLs, + stopgaps: _XDGUserDirectory.stopgapDefaultDirectoryURLs) + } + + func url(userConfiguration: [_XDGUserDirectory: URL], + osDefaultConfiguration: [_XDGUserDirectory: URL], + stopgaps: [_XDGUserDirectory: URL]) -> URL { + if let url = userConfiguration[self] { + return url + } else if let url = osDefaultConfiguration[self] { + return url + } else { + return stopgaps[self]! + } + } + + static let stopgapDefaultDirectoryURLs: [_XDGUserDirectory: URL] = { + let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + return [ + .desktop: home.appendingPathComponent("Desktop"), + .download: home.appendingPathComponent("Downloads"), + .publicShare: home.appendingPathComponent("Public"), + .documents: home.appendingPathComponent("Documents"), + .music: home.appendingPathComponent("Music"), + .pictures: home.appendingPathComponent("Pictures"), + .videos: home.appendingPathComponent("Videos"), + ] + }() + + static func userDirectories(fromConfigurationFileAt url: URL) -> [_XDGUserDirectory: URL]? { + if let configuration = try? String(contentsOf: url, encoding: .utf8) { + return userDirectories(fromConfiguration: configuration) + } else { + return nil + } + } + + static func userDirectories(fromConfiguration configuration: String) -> [_XDGUserDirectory: URL] { + var entries: [_XDGUserDirectory: URL] = [:] + let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + + // Parse it: + let lines = configuration.split(separator: "\n") + for line in lines { + if let range = line.range(of: "=") { + var variable = String(line[line.startIndex ..< range.lowerBound].trimmingCharacters(in: .whitespaces)) + + let prefix = "XDG_" + let suffix = "_DIR" + if variable.hasPrefix(prefix) && variable.hasSuffix(suffix) { + let endOfPrefix = variable.index(variable.startIndex, offsetBy: prefix.length) + let startOfSuffix = variable.index(variable.endIndex, offsetBy: -suffix.length) + + variable = String(variable[endOfPrefix ..< startOfSuffix]) + } + + guard let directory = _XDGUserDirectory(rawValue: variable) else { + continue + } + + let path = String(line[range.upperBound ..< line.endIndex]).trimmingCharacters(in: .whitespaces) + if !path.isEmpty { + entries[directory] = URL(fileURLWithPath: path, isDirectory: true, relativeTo: home) + } + } else { + return [:] // Incorrect syntax. + } + } + + return entries + } + + static let configuredDirectoryURLs: [_XDGUserDirectory: URL] = { + let configurationHome = _SwiftValue.fetch(nonOptional: _CFXDGCreateConfigHomePath()) as! String + let configurationFile = URL(fileURLWithPath: "user-dirs.dirs", isDirectory: false, relativeTo: URL(fileURLWithPath: configurationHome, isDirectory: true)) + + return userDirectories(fromConfigurationFileAt: configurationFile) ?? [:] + }() + + static let osDefaultDirectoryURLs: [_XDGUserDirectory: URL] = { + let configurationDirs = _SwiftValue.fetch(nonOptional: _CFXDGCreateConfigDirectoriesPaths()) as! [String] + + for directory in configurationDirs { + let configurationFile = URL(fileURLWithPath: directory, isDirectory: true).appendingPathComponent("user-dirs.defaults") + + if let result = userDirectories(fromConfigurationFileAt: configurationFile) { + return result + } + } + + return [:] + }() +} diff --git a/Foundation/NSPathUtilities.swift b/Foundation/NSPathUtilities.swift index 5ba99de57e..3269677288 100755 --- a/Foundation/NSPathUtilities.swift +++ b/Foundation/NSPathUtilities.swift @@ -562,7 +562,29 @@ extension FileManager { } public func NSSearchPathForDirectoriesInDomains(_ directory: FileManager.SearchPathDirectory, _ domainMask: FileManager.SearchPathDomainMask, _ expandTilde: Bool) -> [String] { - NSUnimplemented() + let knownDomains: [FileManager.SearchPathDomainMask] = [ + .userDomainMask, + .networkDomainMask, + .localDomainMask, + .systemDomainMask, + ] + + var result: [URL] = [] + + for domain in knownDomains { + if domainMask.contains(domain) { + result.append(contentsOf: FileManager.default.urls(for: directory, in: domain)) + } + } + + return result.map { (url) in + var path = url.absoluteURL.path + if expandTilde { + path = NSString(string: path).expandingTildeInPath + } + + return path + } } public func NSHomeDirectory() -> String { diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index b66de8de3c..f01d2f4a5a 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -7,10 +7,18 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // +#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT + #if (os(Linux) || os(Android)) + @testable import Foundation + #else + @testable import SwiftFoundation + #endif +#endif + class TestFileManager : XCTestCase { static var allTests: [(String, (TestFileManager) -> () throws -> Void)] { - return [ + var tests: [(String, (TestFileManager) -> () throws -> Void)] = [ ("test_createDirectory", test_createDirectory ), ("test_createFile", test_createFile ), ("test_moveFile", test_moveFile), @@ -33,8 +41,23 @@ class TestFileManager : XCTestCase { ("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser), ("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath), ("test_mountedVolumeURLs", test_mountedVolumeURLs), - ("test_contentsEqual", test_contentsEqual) ] + +#if !DEPLOYMENT_RUNTIME_OBJC && NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT + tests.append(contentsOf: [ + ("test_xdgStopgapsCoverAllConstants", test_xdgStopgapsCoverAllConstants), + ("test_parseXDGConfiguration", test_parseXDGConfiguration), + ("test_xdgURLSelection", test_xdgURLSelection), + ]) +#endif + +#if !DEPLOYMENT_RUNTIME_OBJC + tests.append(contentsOf: [ + ("test_fetchXDGPathsFromHelper", test_fetchXDGPathsFromHelper), + ]) +#endif + + return tests } func test_createDirectory() { @@ -1035,4 +1058,191 @@ class TestFileManager : XCTestCase { XCTAssertFalse(fm.contentsEqual(atPath: dataFile1.path, andPath: dataFile2.path)) XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path)) } + +#if !DEPLOYMENT_RUNTIME_OBJC // XDG tests require swift-corelibs-foundation + + #if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT // These are white box tests for the internals of XDG parsing: + func test_xdgStopgapsCoverAllConstants() { + let stopgaps = _XDGUserDirectory.stopgapDefaultDirectoryURLs + for directory in _XDGUserDirectory.allDirectories { + XCTAssertNotNil(stopgaps[directory]) + } + } + + func test_parseXDGConfiguration() { + let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + + let assertConfigurationProduces = { (configuration: String, paths: [_XDGUserDirectory: String]) in + XCTAssertEqual(_XDGUserDirectory.userDirectories(fromConfiguration: configuration).mapValues({ $0.absoluteURL.path }), + paths.mapValues({ URL(fileURLWithPath: $0, isDirectory: true, relativeTo: home).absoluteURL.path })) + } + + assertConfigurationProduces("", [:]) + + // Test partial configuration and paths relative to home. + assertConfigurationProduces( +""" +DESKTOP=/xdg_test/Desktop +MUSIC=/xdg_test/Music +PICTURES=Pictures +""", [ .desktop: "/xdg_test/Desktop", + .music: "/xdg_test/Music", + .pictures: "Pictures" ]) + + // Test full configuration with XDG_…_DIR syntax, duplicate keys and varying indentation + // 'XDG_MUSIC_DIR' is duplicated, below. + assertConfigurationProduces( +""" + XDG_MUSIC_DIR=ShouldNotBeUsedUseTheOneBelowInstead + + XDG_DESKTOP_DIR=Desktop + XDG_DOWNLOAD_DIR=Download + XDG_PUBLICSHARE_DIR=Public +XDG_DOCUMENTS_DIR=Documents + XDG_MUSIC_DIR=Music +XDG_PICTURES_DIR=Pictures + XDG_VIDEOS_DIR=Videos +""", [ .desktop: "Desktop", + .download: "Download", + .publicShare: "Public", + .documents: "Documents", + .music: "Music", + .pictures: "Pictures", + .videos: "Videos" ]) + + // Same, without XDG…DIR. + assertConfigurationProduces( +""" + MUSIC=ShouldNotBeUsedUseTheOneBelowInstead + + DESKTOP=Desktop + DOWNLOAD=Download + PUBLICSHARE=Public +DOCUMENTS=Documents + MUSIC=Music +PICTURES=Pictures + VIDEOS=Videos +""", [ .desktop: "Desktop", + .download: "Download", + .publicShare: "Public", + .documents: "Documents", + .music: "Music", + .pictures: "Pictures", + .videos: "Videos" ]) + + assertConfigurationProduces( +""" + DESKTOP=/home/Desktop +This configuration file has an invalid syntax. +""", [:]) + } + + func test_xdgURLSelection() { + let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + + let configuration = _XDGUserDirectory.userDirectories(fromConfiguration: +""" +DESKTOP=UserDesktop +""" + ) + + let osDefaults = _XDGUserDirectory.userDirectories(fromConfiguration: +""" +DESKTOP=SystemDesktop +PUBLICSHARE=SystemPublicShare +""" + ) + + let stopgaps = _XDGUserDirectory.userDirectories(fromConfiguration: +""" +DESKTOP=StopgapDesktop +DOWNLOAD=StopgapDownload +PUBLICSHARE=StopgapPublicShare +DOCUMENTS=StopgapDocuments +MUSIC=StopgapMusic +PICTURES=StopgapPictures +VIDEOS=StopgapVideos +""" + ) + + let assertSameAbsolutePath = { (lhs: URL, rhs: URL) in + XCTAssertEqual(lhs.absoluteURL.path, rhs.absoluteURL.path) + } + + assertSameAbsolutePath(_XDGUserDirectory.desktop.url(userConfiguration: configuration, osDefaultConfiguration: osDefaults, stopgaps: stopgaps), home.appendingPathComponent("UserDesktop")) + assertSameAbsolutePath(_XDGUserDirectory.publicShare.url(userConfiguration: configuration, osDefaultConfiguration: osDefaults, stopgaps: stopgaps), home.appendingPathComponent("SystemPublicShare")) + assertSameAbsolutePath(_XDGUserDirectory.music.url(userConfiguration: configuration, osDefaultConfiguration: osDefaults, stopgaps: stopgaps), home.appendingPathComponent("StopgapMusic")) + } + #endif // NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT + + // This test below is a black box test, and does not require @testable import. + + enum TestError: Error { + case notImplementedOnThisPlatform + } + + func printPathByRunningHelper(withConfiguration config: String, method: String, identifier: String) throws -> String { + #if os(Android) + throw TestError.notImplementedOnThisPlatform + #endif + + let uuid = UUID().uuidString + let path = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("org.swift.Foundation.XDGTestHelper").appendingPathComponent(uuid) + try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) + + let configFilePath = path.appendingPathComponent("user-dirs.dirs") + try config.write(to: configFilePath, atomically: true, encoding: .utf8) + defer { + try? FileManager.default.removeItem(at: path) + } + + var environment = [ "XDG_CONFIG_HOME": path.path, + "_NSFileManagerUseXDGPathsForDirectoryDomains": "YES" ] + + // Copy all LD_* and DYLD_* variables over, in case we're running with altered paths (e.g. from ninja test on Linux) + for entry in ProcessInfo.processInfo.environment.lazy.filter({ $0.key.hasPrefix("DYLD_") || $0.key.hasPrefix("LD_") }) { + environment[entry.key] = entry.value + } + + let helper = xdgTestHelperURL() + let (stdout, _) = try runTask([ helper.path, "--nspathfor", method, identifier ], + environment: environment) + + return stdout.trimmingCharacters(in: CharacterSet.newlines) + } + + func assertFetchingPath(withConfiguration config: String, identifier: String, yields path: String) { + for method in [ "NSSearchPath", "FileManagerDotURLFor", "FileManagerDotURLsFor" ] { + do { + let found = try printPathByRunningHelper(withConfiguration: config, method: method, identifier: identifier) + XCTAssertEqual(found, path) + } catch let error { + XCTFail("Failed with method \(method), configuration \(config), identifier \(identifier), equal to \(path), error \(error)") + } + } + } + + func test_fetchXDGPathsFromHelper() { + let prefix = NSHomeDirectory() + "/_Foundation_Test_" + + let configuration = """ + DESKTOP=\(prefix)/Desktop + DOWNLOAD=\(prefix)/Download + PUBLICSHARE=\(prefix)/PublicShare + DOCUMENTS=\(prefix)/Documents + MUSIC=\(prefix)/Music + PICTURES=\(prefix)/Pictures + VIDEOS=\(prefix)/Videos + """ + + assertFetchingPath(withConfiguration: configuration, identifier: "desktop", yields: "\(prefix)/Desktop") + assertFetchingPath(withConfiguration: configuration, identifier: "download", yields: "\(prefix)/Download") + assertFetchingPath(withConfiguration: configuration, identifier: "publicShare", yields: "\(prefix)/PublicShare") + assertFetchingPath(withConfiguration: configuration, identifier: "documents", yields: "\(prefix)/Documents") + assertFetchingPath(withConfiguration: configuration, identifier: "music", yields: "\(prefix)/Music") + assertFetchingPath(withConfiguration: configuration, identifier: "pictures", yields: "\(prefix)/Pictures") + assertFetchingPath(withConfiguration: configuration, identifier: "videos", yields: "\(prefix)/Videos") + } +#endif // !DEPLOYMENT_RUNTIME_OBJC + } diff --git a/TestFoundation/TestProcess.swift b/TestFoundation/TestProcess.swift index e3d7548c31..3e8f6eab75 100644 --- a/TestFoundation/TestProcess.swift +++ b/TestFoundation/TestProcess.swift @@ -611,7 +611,7 @@ class _SignalHelperRunner { } #if !os(Android) -private func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) { +internal func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) { let process = Process() var arguments = arguments diff --git a/TestFoundation/xdgTestHelper/main.swift b/TestFoundation/xdgTestHelper/main.swift index 7435c604e2..02ccaa2c34 100644 --- a/TestFoundation/xdgTestHelper/main.swift +++ b/TestFoundation/xdgTestHelper/main.swift @@ -7,10 +7,12 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // -#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) || os(Android) +#if DEPLOYMENT_RUNTIME_OBJC import Foundation +#elseif os(Linux) || os(Android) +@testable import Foundation #else -import SwiftFoundation +@testable import SwiftFoundation #endif enum HelperCheckStatus : Int32 { @@ -55,6 +57,8 @@ class XDGCheck { } } +// ----- + // Used by TestProcess: test_interrupt(), test_suspend_resume() func signalTest() { @@ -102,6 +106,63 @@ func signalTest() { } } +// ----- + +#if !DEPLOYMENT_RUNTIME_OBJC +struct NSURLForPrintTest { + enum Method: String { + case NSSearchPath + case FileManagerDotURLFor + case FileManagerDotURLsFor + } + + enum Identifier: String { + case desktop + case download + case publicShare + case documents + case music + case pictures + case videos + } + + let method: Method + let identifier: Identifier + + func run() { + let directory: FileManager.SearchPathDirectory + + switch identifier { + case .desktop: + directory = .desktopDirectory + case .download: + directory = .downloadsDirectory + case .publicShare: + directory = .sharedPublicDirectory + case .documents: + directory = .documentDirectory + case .music: + directory = .musicDirectory + case .pictures: + directory = .picturesDirectory + case .videos: + directory = .moviesDirectory + } + + switch method { + case .NSSearchPath: + print(NSSearchPathForDirectoriesInDomains(directory, .userDomainMask, true).first!) + case .FileManagerDotURLFor: + print(try! FileManager.default.url(for: directory, in: .userDomainMask, appropriateFor: nil, create: false).path) + case .FileManagerDotURLsFor: + print(FileManager.default.urls(for: directory, in: .userDomainMask).first!.path) + } + } +} +#endif + +// ----- + var arguments = ProcessInfo.processInfo.arguments.dropFirst().makeIterator() guard let arg = arguments.next() else { @@ -111,16 +172,30 @@ guard let arg = arguments.next() else { switch arg { case "--xdgcheck": XDGCheck.run() - + case "--getcwd": print(FileManager.default.currentDirectoryPath) case "--echo-PWD": print(ProcessInfo.processInfo.environment["PWD"] ?? "") + +#if !DEPLOYMENT_RUNTIME_OBJC +case "--nspathfor": + guard let methodString = arguments.next(), + let method = NSURLForPrintTest.Method(rawValue: methodString), + let identifierString = arguments.next(), + let identifier = NSURLForPrintTest.Identifier(rawValue: identifierString) else { + fatalError("Usage: --nspathfor ") + } + + let test = NSURLForPrintTest(method: method, identifier: identifier) + test.run() +#endif case "--signal-test": signalTest() - + default: fatalError("These arguments are not recognized. Only run this from a unit test.") } + diff --git a/build.py b/build.py index 811858e837..61100bdd73 100755 --- a/build.py +++ b/build.py @@ -30,7 +30,8 @@ swift_cflags += ['-DCYGWIN'] if Configuration.current.build_mode == Configuration.Debug: - foundation.LDFLAGS += ' -lswiftSwiftOnoneSupport ' + foundation.LDFLAGS += ' -lswiftSwiftOnoneSupport ' + swift_cflags += ['-enable-testing'] foundation.ASFLAGS = " ".join([ '-DCF_CHARACTERSET_BITMAP=\\"CoreFoundation/CharacterSets/CFCharacterSetBitmaps.bitmap\\"', @@ -377,6 +378,7 @@ 'Foundation/NSExpression.swift', 'Foundation/FileHandle.swift', 'Foundation/FileManager.swift', + 'Foundation/FileManager_XDG.swift', 'Foundation/Formatter.swift', 'Foundation/NSGeometry.swift', 'Foundation/Host.swift', @@ -499,6 +501,9 @@ 'Foundation/JSONEncoder.swift', ]) +if Configuration.current.build_mode == Configuration.Debug: + swift_sources.enable_testable_import = True + swift_sources.add_dependency(headers) foundation.add_phase(swift_sources) diff --git a/lib/phases.py b/lib/phases.py index ab46998670..921e8eef1a 100644 --- a/lib/phases.py +++ b/lib/phases.py @@ -348,6 +348,7 @@ def __init__(self, sources): BuildPhase.__init__(self, "CompileSwiftSources") if sources is not None: self._sources = sources + self.enable_testable_import = False @property def product(self): @@ -396,11 +397,15 @@ def generate(self): partial_modules += compiled.relative() + ".~partial.swiftmodule " partial_docs += compiled.relative() + ".~partial.swiftdoc " + testable_import_flags = "" + if self.enable_testable_import: + testable_import_flags = "-enable-testing" + generated += """ build """ + self._module.relative() + ": MergeSwiftModule " + objects + """ partials = """ + partial_modules + """ module_name = """ + self.product.name + """ - flags = -I""" + self.product.public_module_path.relative() + """ """ + TargetConditional.value(self.product.SWIFTCFLAGS) + """ -emit-module-doc-path """ + self._module.parent().path_by_appending(self.product.name).relative() + """.swiftdoc + flags = """ + testable_import_flags + " -I" + self.product.public_module_path.relative() + """ """ + TargetConditional.value(self.product.SWIFTCFLAGS) + """ -emit-module-doc-path """ + self._module.parent().path_by_appending(self.product.name).relative() + """.swiftdoc """ return generated diff --git a/lib/script.py b/lib/script.py index 2533fa8d4b..8ed08af641 100644 --- a/lib/script.py +++ b/lib/script.py @@ -124,7 +124,7 @@ def generate_products(self): swift_flags += """ TARGET_SWIFTEXE_FLAGS = -I${SDKROOT}/lib/swift/""" + Configuration.current.target.swift_sdk_name + """ -L${SDKROOT}/lib/swift/""" + Configuration.current.target.swift_sdk_name + """ """ if Configuration.current.build_mode == Configuration.Debug: - swift_flags += "-g -Onone -enable-testing " + swift_flags += "-g -Onone -enable-testing -DNS_FOUNDATION_ALLOWS_TESTABLE_IMPORT " elif Configuration.current.build_mode == Configuration.Release: swift_flags += " " swift_flags += Configuration.current.extra_swift_flags From 3dc7d99a88c9c274fb134108a49ca38f33c66da8 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Wed, 13 Jun 2018 16:55:16 -0600 Subject: [PATCH 2/4] We do not need testable imports in xdgTestHelper. --- TestFoundation/xdgTestHelper/main.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/TestFoundation/xdgTestHelper/main.swift b/TestFoundation/xdgTestHelper/main.swift index 02ccaa2c34..e9607183c8 100644 --- a/TestFoundation/xdgTestHelper/main.swift +++ b/TestFoundation/xdgTestHelper/main.swift @@ -7,12 +7,10 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // -#if DEPLOYMENT_RUNTIME_OBJC +#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) || os(Android) import Foundation -#elseif os(Linux) || os(Android) -@testable import Foundation #else -@testable import SwiftFoundation +import SwiftFoundation #endif enum HelperCheckStatus : Int32 { From 03c77d990d2fb477304c31798b3ddd13474782e3 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Wed, 10 Oct 2018 18:34:16 -0700 Subject: [PATCH 3/4] _SwiftValue -> __SwiftValue. --- Foundation/FileManager.swift | 6 +++--- Foundation/FileManager_XDG.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Foundation/FileManager.swift b/Foundation/FileManager.swift index a41ea5eac0..c5f487e4db 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -215,7 +215,7 @@ open class FileManager : NSObject { // FHS/XDG-compliant OSes: switch directory { case .autosavedInformationDirectory: - let runtimePath = _SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String + let runtimePath = __SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String return [ URL(fileURLWithPath: "Autosave Information", isDirectory: true, relativeTo: URL(fileURLWithPath: runtimePath, isDirectory: true)) ] case .desktopDirectory: @@ -228,12 +228,12 @@ open class FileManager : NSObject { case .cachesDirectory: guard domain == .user else { return [] } - let path = _SwiftValue.fetch(nonOptional: _CFXDGCreateCacheDirectoryPath()) as! String + let path = __SwiftValue.fetch(nonOptional: _CFXDGCreateCacheDirectoryPath()) as! String return [ URL(fileURLWithPath: path, isDirectory: true) ] case .applicationSupportDirectory: guard domain == .user else { return [] } - let path = _SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String + let path = __SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String return [ URL(fileURLWithPath: path, isDirectory: true) ] case .downloadsDirectory: diff --git a/Foundation/FileManager_XDG.swift b/Foundation/FileManager_XDG.swift index e44e9b0be7..5b18ea5660 100644 --- a/Foundation/FileManager_XDG.swift +++ b/Foundation/FileManager_XDG.swift @@ -103,14 +103,14 @@ enum _XDGUserDirectory: String { } static let configuredDirectoryURLs: [_XDGUserDirectory: URL] = { - let configurationHome = _SwiftValue.fetch(nonOptional: _CFXDGCreateConfigHomePath()) as! String + let configurationHome = __SwiftValue.fetch(nonOptional: _CFXDGCreateConfigHomePath()) as! String let configurationFile = URL(fileURLWithPath: "user-dirs.dirs", isDirectory: false, relativeTo: URL(fileURLWithPath: configurationHome, isDirectory: true)) return userDirectories(fromConfigurationFileAt: configurationFile) ?? [:] }() static let osDefaultDirectoryURLs: [_XDGUserDirectory: URL] = { - let configurationDirs = _SwiftValue.fetch(nonOptional: _CFXDGCreateConfigDirectoriesPaths()) as! [String] + let configurationDirs = __SwiftValue.fetch(nonOptional: _CFXDGCreateConfigDirectoriesPaths()) as! [String] for directory in configurationDirs { let configurationFile = URL(fileURLWithPath: directory, isDirectory: true).appendingPathComponent("user-dirs.defaults") From 9745d1814c841d1cbeaefade09fe82143e9abfa1 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Wed, 10 Oct 2018 19:18:58 -0700 Subject: [PATCH 4/4] =?UTF-8?q?urls(for:=E2=80=A6)=20can=20accept=20multip?= =?UTF-8?q?le=20search=20path=20domain=20bits,=20unlike=20url(for:?= =?UTF-8?q?=E2=80=A6).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Foundation/FileManager.swift | 57 ++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/Foundation/FileManager.swift b/Foundation/FileManager.swift index c5f487e4db..0491ba40dc 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -128,21 +128,38 @@ open class FileManager : NSObject { case network case user + static let correspondingValues: [UInt: _SearchPathDomain] = [ + SearchPathDomainMask.systemDomainMask.rawValue: .system, + SearchPathDomainMask.localDomainMask.rawValue: .local, + SearchPathDomainMask.networkDomainMask.rawValue: .network, + SearchPathDomainMask.userDomainMask.rawValue: .user, + ] + + static let searchOrder: [SearchPathDomainMask] = [ + .systemDomainMask, + .localDomainMask, + .networkDomainMask, + .userDomainMask, + ] + init?(_ domainMask: SearchPathDomainMask) { - if domainMask == .systemDomainMask { - self = .system; return - } - if domainMask == .localDomainMask { - self = .local; return - } - if domainMask == .networkDomainMask { - self = .network; return + if let value = _SearchPathDomain.correspondingValues[domainMask.rawValue] { + self = value + } else { + return nil } - if domainMask == .userDomainMask { - self = .user; return + } + + static func allInSearchOrder(from domainMask: SearchPathDomainMask) -> [_SearchPathDomain] { + var domains: [_SearchPathDomain] = [] + + for bit in _SearchPathDomain.searchOrder { + if domainMask.contains(bit) { + domains.append(_SearchPathDomain.correspondingValues[bit.rawValue]!) + } } - return nil + return domains } } @@ -185,9 +202,7 @@ open class FileManager : NSObject { /* -URLsForDirectory:inDomains: is analogous to NSSearchPathForDirectoriesInDomains(), but returns an array of NSURL instances for use with URL-taking APIs. This API is suitable when you need to search for a file or files which may live in one of a variety of locations in the domains specified. */ open func urls(for directory: SearchPathDirectory, in domainMask: SearchPathDomainMask) -> [URL] { - guard let domain = _SearchPathDomain(domainMask) else { - fatalError("Values other than .systemDomainMask, .localDomainMask, .userDomainMask, .networkDomainMask are unsupported") - } + let domains = _SearchPathDomain.allInSearchOrder(from: domainMask) // We are going to return appropriate paths on Darwin, but [] on platforms that do not have comparable locations. // For example, on FHS/XDG systems, applications are not installed in a single path. @@ -203,11 +218,17 @@ open class FileManager : NSObject { #endif } - if useDarwinPaths { - return darwinURLs(for: directory, in: domain) - } else { - return xdgURLs(for: directory, in: domain) + var urls: [URL] = [] + + for domain in domains { + if useDarwinPaths { + urls.append(contentsOf: darwinURLs(for: directory, in: domain)) + } else { + urls.append(contentsOf: xdgURLs(for: directory, in: domain)) + } } + + return urls } #endif