From 7a99929602e7f3df246712f3ec75376c6c6fdd2b Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Mon, 29 Jan 2018 10:11:38 -0800 Subject: [PATCH 1/8] =?UTF-8?q?Implement=20FileManager.urls(for:=E2=80=A6)?= =?UTF-8?q?,=20.url(for:=E2=80=A6)=20and=20NSSearchPathForDirectoriesInDom?= =?UTF-8?q?ains.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement urls(for:…) as the primitive directory method: - In Darwin, match paths with the Objective-C implementation wherever possible. - On platforms that use FHS/XDG, implement reading user-dirs.* files to determine XDG directories; Implement url(for:in:…) and NSSearchPathForDirectoriesInDomains() in terms of urls(for:…). --- Foundation.xcodeproj/project.pbxproj | 6 + Foundation/FileManager.swift | 306 ++++++++++++++++++++++++++- Foundation/FileManager_XDG.swift | 109 ++++++++++ Foundation/NSPathUtilities.swift | 24 ++- TestFoundation/TestFileManager.swift | 13 ++ build.py | 7 +- lib/phases.py | 7 +- 7 files changed, 467 insertions(+), 5 deletions(-) create mode 100644 Foundation/FileManager_XDG.swift diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index daa407b6b6..2ca9288e00 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 */, @@ -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 */, @@ -2708,6 +2712,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 = ( @@ -2780,6 +2785,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 e9bb49ca53..233a8b76ba 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -120,10 +120,284 @@ 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 + } + if domainMask == .localDomainMask { + self = .local + } + if domainMask == .networkDomainMask { + self = .network + } + if domainMask == .userDomainMask { + self = .user + } + + return nil + } + } + + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + 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)) ] + } + } + #endif + + #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. + + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + // For Darwin: + 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 [] + } + #elseif !os(Windows) + // 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 [] + + 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 [] + } + #endif + + } + #endif + + 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 +405,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..05b72d4ad7 --- /dev/null +++ b/Foundation/FileManager_XDG.swift @@ -0,0 +1,109 @@ +// +// FileManager_XDG.swift +// SwiftFoundation +// +// Created by Lily Vulcano on 2/26/18. +// Copyright © 2018 Apple. All rights reserved. +// + +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 { + if let url = _XDGUserDirectory.configuredDirectoryURLs[self] { + return url + } else if let url = _XDGUserDirectory.osDefaultDirectoryURLs[self] { + return url + } else { + return _XDGUserDirectory.stopgapDefaultDirectoryURLs[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) { + var entries: [_XDGUserDirectory: URL] = [:] + + // 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) + } + } + } + + return entries + } else { + return nil + } + } + + 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..6de65d1fd7 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.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..9fcf70afe3 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -30,6 +30,8 @@ class TestFileManager : XCTestCase { ("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath), ("test_mountedVolumeURLs", test_mountedVolumeURLs), ("test_contentsEqual", test_contentsEqual) + ("test_XDGStopgapsCoverAllConstants", test_XDGStopgapsCoverAllConstants), + ("test_parseXDGConfiguration", test_parseXDGConfiguration), ] } @@ -947,4 +949,15 @@ class TestFileManager : XCTestCase { XCTAssertFalse(fm.contentsEqual(atPath: dataFile1.path, andPath: dataFile2.path)) XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path)) } + + func test_XDGStopgapsCoverAllConstants() { + let stopgaps = _XDGUserDirectory.stopgapDefaultDirectoryURLs + for directory in _XDGUserDirectory.allDirectories { + XCTAssertNotNil(stopgaps[directory]) + } + } + + func test_parseXDGConfiguration() { + + } } 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 From e731323d79f3049accf8e2a77da7b8c6b745db6e Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Tue, 27 Feb 2018 15:14:26 -0800 Subject: [PATCH 2/8] Added tests. --- Foundation/FileManager_XDG.swift | 79 ++++++++++++-------- TestFoundation/TestFileManager.swift | 106 +++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 32 deletions(-) diff --git a/Foundation/FileManager_XDG.swift b/Foundation/FileManager_XDG.swift index 05b72d4ad7..6f539708bc 100644 --- a/Foundation/FileManager_XDG.swift +++ b/Foundation/FileManager_XDG.swift @@ -25,15 +25,23 @@ enum _XDGUserDirectory: String { .music, .pictures, .videos, - ] + ] var url: URL { - if let url = _XDGUserDirectory.configuredDirectoryURLs[self] { + 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 = _XDGUserDirectory.osDefaultDirectoryURLs[self] { + } else if let url = osDefaultConfiguration[self] { return url } else { - return _XDGUserDirectory.stopgapDefaultDirectoryURLs[self]! + return stopgaps[self]! } } @@ -52,38 +60,45 @@ enum _XDGUserDirectory: String { static func userDirectories(fromConfigurationFileAt url: URL) -> [_XDGUserDirectory: URL]? { if let configuration = try? String(contentsOf: url, encoding: .utf8) { - var entries: [_XDGUserDirectory: URL] = [:] - - // 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 - } + 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) - let path = String(line[range.upperBound ..< line.endIndex]).trimmingCharacters(in: .whitespaces) - if path.isEmpty { - entries[directory] = URL(fileURLWithPath: path, isDirectory: true) - } + 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 - } else { - return nil } + + return entries } static let configuredDirectoryURLs: [_XDGUserDirectory: URL] = { diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index 9fcf70afe3..8529e46016 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -32,6 +32,7 @@ class TestFileManager : XCTestCase { ("test_contentsEqual", test_contentsEqual) ("test_XDGStopgapsCoverAllConstants", test_XDGStopgapsCoverAllConstants), ("test_parseXDGConfiguration", test_parseXDGConfiguration), + ("test_xdgURLSelection", test_xdgURLSelection), ] } @@ -958,6 +959,111 @@ class TestFileManager : XCTestCase { } 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")) + } + + } From 939cc365350cb620f7d7b895b573fe24be0de2b4 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Tue, 3 Apr 2018 15:47:07 -0700 Subject: [PATCH 3/8] =?UTF-8?q?Ensure=20.moviesDirectory=20maps=20to=20XDG?= =?UTF-8?q?=E2=80=99s=20VIDEOS=20directory.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Foundation/FileManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Foundation/FileManager.swift b/Foundation/FileManager.swift index 233a8b76ba..6e701c2e33 100644 --- a/Foundation/FileManager.swift +++ b/Foundation/FileManager.swift @@ -337,7 +337,7 @@ open class FileManager : NSObject { return [ URL(fileURLWithPath: "/home", isDirectory: true) ] case .moviesDirectory: - return [] + return [ _XDGUserDirectory.videos.url ] case .musicDirectory: guard domain == .user else { return [] } From 243b353d3974fc46a28d726ea5ffed828a9df70d Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Mon, 28 May 2018 07:18:37 -0700 Subject: [PATCH 4/8] Fix licensing comment and test syntax from @spevans feedback. --- Foundation/FileManager_XDG.swift | 9 +++++---- TestFoundation/TestFileManager.swift | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Foundation/FileManager_XDG.swift b/Foundation/FileManager_XDG.swift index 6f539708bc..e44e9b0be7 100644 --- a/Foundation/FileManager_XDG.swift +++ b/Foundation/FileManager_XDG.swift @@ -1,9 +1,10 @@ +// This source file is part of the Swift.org open source project // -// FileManager_XDG.swift -// SwiftFoundation +// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// Created by Lily Vulcano on 2/26/18. -// Copyright © 2018 Apple. All rights reserved. +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // import CoreFoundation diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index 8529e46016..c9fa7b0a79 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -29,7 +29,6 @@ class TestFileManager : XCTestCase { ("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser), ("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath), ("test_mountedVolumeURLs", test_mountedVolumeURLs), - ("test_contentsEqual", test_contentsEqual) ("test_XDGStopgapsCoverAllConstants", test_XDGStopgapsCoverAllConstants), ("test_parseXDGConfiguration", test_parseXDGConfiguration), ("test_xdgURLSelection", test_xdgURLSelection), From cd7060e55a6cda95255a7b5311cb809a3c13f341 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Fri, 8 Jun 2018 10:03:25 -0700 Subject: [PATCH 5/8] Merge fixes. --- TestFoundation/TestFileManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index c9fa7b0a79..f6453b5c8c 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -7,6 +7,10 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // +#if !(DEPLOYMENT_RUNTIME_OBJC || os(Linux) || os(Android)) +@testable import SwiftFoundation +#endif + class TestFileManager : XCTestCase { static var allTests: [(String, (TestFileManager) -> () throws -> Void)] { From b935c06ef850366f1b5a944aaf1b5ca37b9cece1 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Fri, 8 Jun 2018 13:40:55 -0700 Subject: [PATCH 6/8] Make this work, and testable under Darwin and Linux. --- Foundation/FileManager.swift | 247 +++++++++++++----------- Foundation/NSPathUtilities.swift | 2 +- TestFoundation/TestFileManager.swift | 77 +++++++- TestFoundation/TestProcess.swift | 2 +- TestFoundation/xdgTestHelper/main.swift | 98 +++++++++- 5 files changed, 294 insertions(+), 132 deletions(-) diff --git a/Foundation/FileManager.swift b/Foundation/FileManager.swift index 6e701c2e33..b9fdfd04d6 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 @@ -128,23 +130,22 @@ open class FileManager : NSObject { init?(_ domainMask: SearchPathDomainMask) { if domainMask == .systemDomainMask { - self = .system + self = .system; return } if domainMask == .localDomainMask { - self = .local + self = .local; return } if domainMask == .networkDomainMask { - self = .network + self = .network; return } if domainMask == .userDomainMask { - self = .user + self = .user; return } return nil } } - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) private func darwinPathURLs(for domain: _SearchPathDomain, system: String?, local: String?, network: String?, userHomeSubpath: String?) -> [URL] { switch domain { case .system: @@ -174,7 +175,6 @@ open class FileManager : NSObject { return [ URL(fileURLWithPath: all, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] } } - #endif #if os(Windows) // Non-Apple OSes that do not implement FHS/XDG are not currently supported. @available(*, unavailable, message: "Not implemented for this OS") @@ -182,27 +182,133 @@ open class FileManager : NSObject { 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] { - 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. - - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - // For Darwin: + + 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) @@ -229,7 +335,7 @@ open class FileManager : NSObject { 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 [] @@ -251,7 +357,7 @@ open class FileManager : NSObject { 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)) ] @@ -259,19 +365,19 @@ open class FileManager : NSObject { 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) @@ -282,15 +388,15 @@ open class FileManager : NSObject { 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 { @@ -303,99 +409,8 @@ open class FileManager : NSObject { // This directory is only returned by url(for:in:appropriateFor:create:) return [] } - #elseif !os(Windows) - // 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 [] - } - #endif - } - #endif - + private enum URLForDirectoryError: Error { case directoryUnknown } diff --git a/Foundation/NSPathUtilities.swift b/Foundation/NSPathUtilities.swift index 6de65d1fd7..659611b7f7 100755 --- a/Foundation/NSPathUtilities.swift +++ b/Foundation/NSPathUtilities.swift @@ -578,7 +578,7 @@ public func NSSearchPathForDirectoriesInDomains(_ directory: FileManager.SearchP } return result.map { (url) in - var path = url.path + var path = url.absoluteURL.path if expandTilde { path = NSString(string: path).expandingTildeInPath } diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index f6453b5c8c..b934bc66b0 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -7,14 +7,14 @@ // 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 @testable import SwiftFoundation #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), @@ -34,9 +34,17 @@ class TestFileManager : XCTestCase { ("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath), ("test_mountedVolumeURLs", test_mountedVolumeURLs), ("test_XDGStopgapsCoverAllConstants", test_XDGStopgapsCoverAllConstants), + ] + +#if !DEPLOYMENT_RUNTIME_OBJC + tests.append(contentsOf: [ ("test_parseXDGConfiguration", test_parseXDGConfiguration), ("test_xdgURLSelection", test_xdgURLSelection), - ] + ("test_fetchXDGPathsFromHelper", test_fetchXDGPathsFromHelper), + ]) +#endif + + return tests } func ignoreError(_ block: () throws -> Void) { @@ -954,6 +962,7 @@ class TestFileManager : XCTestCase { XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path)) } +#if !DEPLOYMENT_RUNTIME_OBJC // XDG tests require swift-corelibs-foundation func test_XDGStopgapsCoverAllConstants() { let stopgaps = _XDGUserDirectory.stopgapDefaultDirectoryURLs for directory in _XDGUserDirectory.allDirectories { @@ -1062,11 +1071,69 @@ VIDEOS=StopgapVideos } 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")) } + 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) + } + + let helper = xdgTestHelperURL() + let (stdout, _) = try runTask([ helper.path, "--nspathfor", method, identifier ], + environment: [ "XDG_CONFIG_HOME": path.path, + "_NSFileManagerUseXDGPathsForDirectoryDomains": "YES" ]) + + 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.") +} From cf348b88653cbb30cd2b0547e86e00661085e328 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Fri, 8 Jun 2018 14:44:30 -0700 Subject: [PATCH 7/8] Fix Linux build. --- TestFoundation/TestFileManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index b934bc66b0..2de4b727e3 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -7,7 +7,9 @@ // 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)) +@testable import Foundation +#else @testable import SwiftFoundation #endif From 37b8b44797a6cc9db0c1c07b19b0991a0b6dc7b3 Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Fri, 8 Jun 2018 15:32:21 -0700 Subject: [PATCH 8/8] Pass through LD_*/DYLD_* environment variables. --- TestFoundation/TestFileManager.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/TestFoundation/TestFileManager.swift b/TestFoundation/TestFileManager.swift index 2de4b727e3..9d7ab2963b 100644 --- a/TestFoundation/TestFileManager.swift +++ b/TestFoundation/TestFileManager.swift @@ -1096,10 +1096,17 @@ VIDEOS=StopgapVideos 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: [ "XDG_CONFIG_HOME": path.path, - "_NSFileManagerUseXDGPathsForDirectoryDomains": "YES" ]) + environment: environment) return stdout.trimmingCharacters(in: CharacterSet.newlines) }