diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index af4835a0db..91a0cc6383 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 */; }; @@ -515,6 +516,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 = ""; }; @@ -1822,6 +1824,7 @@ children = ( EADE0B5D1BD15DFF00C49C64 /* FileHandle.swift */, EADE0B5E1BD15DFF00C49C64 /* FileManager.swift */, + 1513A8422044893F00539722 /* FileManager_XDG.swift */, EADE0B7A1BD15DFF00C49C64 /* Process.swift */, 5BDC3F2F1BCC5DCB00ED97BB /* Bundle.swift */, 5BDC3F411BCC5DCB00ED97BB /* ProcessInfo.swift */, @@ -2204,7 +2207,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 */ @@ -2344,6 +2347,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 */, @@ -2646,6 +2650,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 = ""; }; @@ -2713,6 +2718,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 = ( @@ -2786,6 +2792,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 752cb07300..116c8fa3b9 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -7,9 +7,11 @@ // See http://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 a7ea0ed446..659611b7f7 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 8c8295d7d3..25307462d6 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), @@ -29,8 +37,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 ignoreError(_ block: () throws -> Void) { @@ -947,4 +970,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 4690f07c34..475d131611 100644 --- a/TestFoundation/TestProcess.swift +++ b/TestFoundation/TestProcess.swift @@ -395,7 +395,7 @@ private enum Error: Swift.Error { } #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 86faf742b1..2aa185c67a 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,15 +57,93 @@ class XDGCheck { } } -if let arg = ProcessInfo.processInfo.arguments.last { - if arg == "--xdgcheck" { - XDGCheck.run() +// ----- + +#if !DEPLOYMENT_RUNTIME_OBJC +struct NSURLForPrintTest { + enum Method: String { + case NSSearchPath + case FileManagerDotURLFor + case FileManagerDotURLsFor } - if arg == "--getcwd" { - print(FileManager.default.currentDirectoryPath) + + enum Identifier: String { + case desktop + case download + case publicShare + case documents + case music + case pictures + case videos } - if arg == "--echo-PWD" { - print(ProcessInfo.processInfo.environment["PWD"] ?? "") + + 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 { + fatalError("The unit test must specify the correct number of flags and arguments.") +} + +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 + +default: + fatalError("These arguments are not recognized. Only run this from a unit test.") +} diff --git a/build.py b/build.py index 341f539e0c..78c6611122 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\\"', @@ -367,6 +368,7 @@ 'Foundation/NSExpression.swift', 'Foundation/FileHandle.swift', 'Foundation/FileManager.swift', + 'Foundation/FileManager_XDG.swift', 'Foundation/Formatter.swift', 'Foundation/NSGeometry.swift', 'Foundation/Host.swift', @@ -489,6 +491,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