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..0491ba40dc 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,318 @@ open class FileManager : NSObject { return result } + private enum _SearchPathDomain { + case system + case local + 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 let value = _SearchPathDomain.correspondingValues[domainMask.rawValue] { + self = value + } else { + return nil + } + } + + 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 domains + } + } + + 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() + 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. + + 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 + } + + 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 + + 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 +441,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..5b18ea5660 --- /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..e9607183c8 100644 --- a/TestFoundation/xdgTestHelper/main.swift +++ b/TestFoundation/xdgTestHelper/main.swift @@ -55,6 +55,8 @@ class XDGCheck { } } +// ----- + // Used by TestProcess: test_interrupt(), test_suspend_resume() func signalTest() { @@ -102,6 +104,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 +170,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