Skip to content

Commit 7a99929

Browse files
committed
Implement FileManager.urls(for:…), .url(for:…) and NSSearchPathForDirectoriesInDomains.
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:…).
1 parent c91c99d commit 7a99929

File tree

7 files changed

+467
-5
lines changed

7 files changed

+467
-5
lines changed

Foundation.xcodeproj/project.pbxproj

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
0383A1751D2E558A0052E5D1 /* TestStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0383A1741D2E558A0052E5D1 /* TestStream.swift */; };
1111
03B6F5841F15F339004F25AF /* TestURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B6F5831F15F339004F25AF /* TestURLProtocol.swift */; };
12+
1513A8432044893F00539722 /* FileManager_XDG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1513A8422044893F00539722 /* FileManager_XDG.swift */; };
1213
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */; };
1314
153E951120111DC500F250BE /* CFKnownLocations.h in Headers */ = {isa = PBXBuildFile; fileRef = 153E950F20111DC500F250BE /* CFKnownLocations.h */; settings = {ATTRIBUTES = (Private, ); }; };
1415
153E951220111DC500F250BE /* CFKnownLocations.c in Sources */ = {isa = PBXBuildFile; fileRef = 153E951020111DC500F250BE /* CFKnownLocations.c */; };
@@ -515,6 +516,7 @@
515516
/* Begin PBXFileReference section */
516517
0383A1741D2E558A0052E5D1 /* TestStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestStream.swift; sourceTree = "<group>"; };
517518
03B6F5831F15F339004F25AF /* TestURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLProtocol.swift; sourceTree = "<group>"; };
519+
1513A8422044893F00539722 /* FileManager_XDG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager_XDG.swift; sourceTree = "<group>"; };
518520
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPServer.swift; sourceTree = "<group>"; };
519521
153E950F20111DC500F250BE /* CFKnownLocations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CFKnownLocations.h; sourceTree = "<group>"; };
520522
153E951020111DC500F250BE /* CFKnownLocations.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CFKnownLocations.c; sourceTree = "<group>"; };
@@ -1822,6 +1824,7 @@
18221824
children = (
18231825
EADE0B5D1BD15DFF00C49C64 /* FileHandle.swift */,
18241826
EADE0B5E1BD15DFF00C49C64 /* FileManager.swift */,
1827+
1513A8422044893F00539722 /* FileManager_XDG.swift */,
18251828
EADE0B7A1BD15DFF00C49C64 /* Process.swift */,
18261829
5BDC3F2F1BCC5DCB00ED97BB /* Bundle.swift */,
18271830
5BDC3F411BCC5DCB00ED97BB /* ProcessInfo.swift */,
@@ -2344,6 +2347,7 @@
23442347
5BECBA3A1D1CAE9A00B39B1F /* NSMeasurement.swift in Sources */,
23452348
5BF7AEB21BCD51F9008F214A /* NSNumber.swift in Sources */,
23462349
61D2F9AF1FECFB3E0033306A /* NativeProtocol.swift in Sources */,
2350+
1513A8432044893F00539722 /* FileManager_XDG.swift in Sources */,
23472351
B9974B991EDF4A22007F15B8 /* HTTPURLProtocol.swift in Sources */,
23482352
5BCD03821D3EE35C00E3FF9B /* TimeZone.swift in Sources */,
23492353
EADE0BBC1BD15E0000C49C64 /* URLCache.swift in Sources */,
@@ -2708,6 +2712,7 @@
27082712
DYLIB_COMPATIBILITY_VERSION = 150;
27092713
DYLIB_CURRENT_VERSION = 1303;
27102714
DYLIB_INSTALL_NAME_BASE = "@rpath";
2715+
ENABLE_TESTABILITY = YES;
27112716
FRAMEWORK_VERSION = A;
27122717
GCC_PREFIX_HEADER = CoreFoundation/Base.subproj/CoreFoundation_Prefix.h;
27132718
HEADER_SEARCH_PATHS = (
@@ -2780,6 +2785,7 @@
27802785
DYLIB_COMPATIBILITY_VERSION = 150;
27812786
DYLIB_CURRENT_VERSION = 1303;
27822787
DYLIB_INSTALL_NAME_BASE = "@rpath";
2788+
ENABLE_TESTABILITY = YES;
27832789
FRAMEWORK_VERSION = A;
27842790
GCC_PREFIX_HEADER = CoreFoundation/Base.subproj/CoreFoundation_Prefix.h;
27852791
HEADER_SEARCH_PATHS = (

Foundation/FileManager.swift

+304-2
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,320 @@ open class FileManager : NSObject {
120120
return result
121121
}
122122

123+
private enum _SearchPathDomain {
124+
case system
125+
case local
126+
case network
127+
case user
128+
129+
init?(_ domainMask: SearchPathDomainMask) {
130+
if domainMask == .systemDomainMask {
131+
self = .system
132+
}
133+
if domainMask == .localDomainMask {
134+
self = .local
135+
}
136+
if domainMask == .networkDomainMask {
137+
self = .network
138+
}
139+
if domainMask == .userDomainMask {
140+
self = .user
141+
}
142+
143+
return nil
144+
}
145+
}
146+
147+
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
148+
private func darwinPathURLs(for domain: _SearchPathDomain, system: String?, local: String?, network: String?, userHomeSubpath: String?) -> [URL] {
149+
switch domain {
150+
case .system:
151+
guard let path = system else { return [] }
152+
return [ URL(fileURLWithPath: path, isDirectory: true) ]
153+
case .local:
154+
guard let path = local else { return [] }
155+
return [ URL(fileURLWithPath: path, isDirectory: true) ]
156+
case .network:
157+
guard let path = network else { return [] }
158+
return [ URL(fileURLWithPath: path, isDirectory: true) ]
159+
case .user:
160+
guard let path = userHomeSubpath else { return [] }
161+
return [ URL(fileURLWithPath: path, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
162+
}
163+
}
164+
165+
private func darwinPathURLs(for domain: _SearchPathDomain, all: String, useLocalDirectoryForSystem: Bool = false) -> [URL] {
166+
switch domain {
167+
case .system:
168+
return [ URL(fileURLWithPath: useLocalDirectoryForSystem ? "/\(all)" : "/System/\(all)", isDirectory: true) ]
169+
case .local:
170+
return [ URL(fileURLWithPath: "/\(all)", isDirectory: true) ]
171+
case .network:
172+
return [ URL(fileURLWithPath: "/Network/\(all)", isDirectory: true) ]
173+
case .user:
174+
return [ URL(fileURLWithPath: all, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
175+
}
176+
}
177+
#endif
178+
179+
#if os(Windows) // Non-Apple OSes that do not implement FHS/XDG are not currently supported.
180+
@available(*, unavailable, message: "Not implemented for this OS")
181+
open func urls(for directory: SearchPathDirectory, in domainMask: SearchPathDomainMask) -> [URL] {
182+
NSUnimplemented()
183+
}
184+
#else
185+
123186
/* -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.
124187
*/
125188
open func urls(for directory: SearchPathDirectory, in domainMask: SearchPathDomainMask) -> [URL] {
126-
NSUnimplemented()
189+
190+
guard let domain = _SearchPathDomain(domainMask) else {
191+
fatalError("Values other than .systemDomainMask, .localDomainMask, .userDomainMask, .networkDomainMask are unsupported")
192+
}
193+
194+
// We are going to return appropriate paths on Darwin, but [] on platforms that do not have comparable locations.
195+
// For example, on FHS/XDG systems, applications are not installed in a single path.
196+
197+
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
198+
// For Darwin:
199+
switch directory {
200+
case .applicationDirectory:
201+
return darwinPathURLs(for: domain, all: "Applications", useLocalDirectoryForSystem: true)
202+
203+
case .demoApplicationDirectory:
204+
return darwinPathURLs(for: domain, all: "Demos", useLocalDirectoryForSystem: true)
205+
206+
case .developerApplicationDirectory:
207+
return darwinPathURLs(for: domain, all: "Developer/Applications", useLocalDirectoryForSystem: true)
208+
209+
case .adminApplicationDirectory:
210+
return darwinPathURLs(for: domain, all: "Applications/Utilities", useLocalDirectoryForSystem: true)
211+
212+
case .libraryDirectory:
213+
return darwinPathURLs(for: domain, all: "Library")
214+
215+
case .developerDirectory:
216+
return darwinPathURLs(for: domain, all: "Developer", useLocalDirectoryForSystem: true)
217+
218+
case .documentationDirectory:
219+
return darwinPathURLs(for: domain, all: "Library/Documentation")
220+
221+
case .coreServiceDirectory:
222+
return darwinPathURLs(for: domain, system: "/System/Library/CoreServices", local: nil, network: nil, userHomeSubpath: nil)
223+
224+
case .autosavedInformationDirectory:
225+
return darwinPathURLs(for: domain, system: nil, local: nil, network: nil, userHomeSubpath: "Library/Autosave Information")
226+
227+
case .inputMethodsDirectory:
228+
return darwinPathURLs(for: domain, all: "Library/Input Methods")
229+
230+
case .preferencePanesDirectory:
231+
return darwinPathURLs(for: domain, system: "/System/Library/PreferencePanes", local: "/Library/PreferencePanes", network: nil, userHomeSubpath: "Library/PreferencePanes")
232+
233+
case .applicationScriptsDirectory:
234+
// Only the ObjC Foundation can know where this is.
235+
return []
236+
237+
case .allApplicationsDirectory:
238+
var directories: [URL] = []
239+
directories.append(contentsOf: darwinPathURLs(for: domain, all: "Applications", useLocalDirectoryForSystem: true))
240+
directories.append(contentsOf: darwinPathURLs(for: domain, all: "Demos", useLocalDirectoryForSystem: true))
241+
directories.append(contentsOf: darwinPathURLs(for: domain, all: "Developer/Applications", useLocalDirectoryForSystem: true))
242+
directories.append(contentsOf: darwinPathURLs(for: domain, all: "Applications/Utilities", useLocalDirectoryForSystem: true))
243+
return directories
244+
245+
case .allLibrariesDirectory:
246+
var directories: [URL] = []
247+
directories.append(contentsOf: darwinPathURLs(for: domain, all: "Library"))
248+
directories.append(contentsOf: darwinPathURLs(for: domain, all: "Developer"))
249+
return directories
250+
251+
case .printerDescriptionDirectory:
252+
guard domain == .system else { return [] }
253+
return [ URL(fileURLWithPath: "/System/Library/Printers/PPD", isDirectory: true) ]
254+
255+
case .desktopDirectory:
256+
guard domain == .user else { return [] }
257+
return [ URL(fileURLWithPath: "Desktop", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
258+
259+
case .documentDirectory:
260+
guard domain == .user else { return [] }
261+
return [ URL(fileURLWithPath: "Documents", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
262+
263+
case .cachesDirectory:
264+
guard domain == .user else { return [] }
265+
return [ URL(fileURLWithPath: "Library/Caches", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
266+
267+
case .applicationSupportDirectory:
268+
guard domain == .user else { return [] }
269+
return [ URL(fileURLWithPath: "Library/Application Support", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
270+
271+
case .downloadsDirectory:
272+
guard domain == .user else { return [] }
273+
return [ URL(fileURLWithPath: "Downloads", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
274+
275+
case .userDirectory:
276+
return darwinPathURLs(for: domain, system: nil, local: "/Users", network: "/Network/Users", userHomeSubpath: nil)
277+
278+
case .moviesDirectory:
279+
guard domain == .user else { return [] }
280+
return [ URL(fileURLWithPath: "Movies", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
281+
282+
case .musicDirectory:
283+
guard domain == .user else { return [] }
284+
return [ URL(fileURLWithPath: "Music", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
285+
286+
case .picturesDirectory:
287+
guard domain == .user else { return [] }
288+
return [ URL(fileURLWithPath: "Pictures", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
289+
290+
case .sharedPublicDirectory:
291+
guard domain == .user else { return [] }
292+
return [ URL(fileURLWithPath: "Public", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ]
293+
294+
case .trashDirectory:
295+
let userTrashURL = URL(fileURLWithPath: ".Trash", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true))
296+
if domain == .user || domain == .local {
297+
return [ userTrashURL ]
298+
} else {
299+
return []
300+
}
301+
302+
case .itemReplacementDirectory:
303+
// This directory is only returned by url(for:in:appropriateFor:create:)
304+
return []
305+
}
306+
#elseif !os(Windows)
307+
// FHS/XDG-compliant OSes:
308+
switch directory {
309+
case .autosavedInformationDirectory:
310+
let runtimePath = _SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String
311+
return [ URL(fileURLWithPath: "Autosave Information", isDirectory: true, relativeTo: URL(fileURLWithPath: runtimePath, isDirectory: true)) ]
312+
313+
case .desktopDirectory:
314+
guard domain == .user else { return [] }
315+
return [ _XDGUserDirectory.desktop.url ]
316+
317+
case .documentDirectory:
318+
guard domain == .user else { return [] }
319+
return [ _XDGUserDirectory.documents.url ]
320+
321+
case .cachesDirectory:
322+
guard domain == .user else { return [] }
323+
let path = _SwiftValue.fetch(nonOptional: _CFXDGCreateCacheDirectoryPath()) as! String
324+
return [ URL(fileURLWithPath: path, isDirectory: true) ]
325+
326+
case .applicationSupportDirectory:
327+
guard domain == .user else { return [] }
328+
let path = _SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String
329+
return [ URL(fileURLWithPath: path, isDirectory: true) ]
330+
331+
case .downloadsDirectory:
332+
guard domain == .user else { return [] }
333+
return [ _XDGUserDirectory.download.url ]
334+
335+
case .userDirectory:
336+
guard domain == .local else { return [] }
337+
return [ URL(fileURLWithPath: "/home", isDirectory: true) ]
338+
339+
case .moviesDirectory:
340+
return []
341+
342+
case .musicDirectory:
343+
guard domain == .user else { return [] }
344+
return [ _XDGUserDirectory.music.url ]
345+
346+
case .picturesDirectory:
347+
guard domain == .user else { return [] }
348+
return [ _XDGUserDirectory.pictures.url ]
349+
350+
case .sharedPublicDirectory:
351+
guard domain == .user else { return [] }
352+
return [ _XDGUserDirectory.publicShare.url ]
353+
354+
case .trashDirectory:
355+
let userTrashURL = URL(fileURLWithPath: ".Trash", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true))
356+
if domain == .user || domain == .local {
357+
return [ userTrashURL ]
358+
} else {
359+
return []
360+
}
361+
362+
// None of these are supported outside of Darwin:
363+
case .applicationDirectory:
364+
fallthrough
365+
case .demoApplicationDirectory:
366+
fallthrough
367+
case .developerApplicationDirectory:
368+
fallthrough
369+
case .adminApplicationDirectory:
370+
fallthrough
371+
case .libraryDirectory:
372+
fallthrough
373+
case .developerDirectory:
374+
fallthrough
375+
case .documentationDirectory:
376+
fallthrough
377+
case .coreServiceDirectory:
378+
fallthrough
379+
case .inputMethodsDirectory:
380+
fallthrough
381+
case .preferencePanesDirectory:
382+
fallthrough
383+
case .applicationScriptsDirectory:
384+
fallthrough
385+
case .allApplicationsDirectory:
386+
fallthrough
387+
case .allLibrariesDirectory:
388+
fallthrough
389+
case .printerDescriptionDirectory:
390+
fallthrough
391+
case .itemReplacementDirectory:
392+
return []
393+
}
394+
#endif
395+
396+
}
397+
#endif
398+
399+
private enum URLForDirectoryError: Error {
400+
case directoryUnknown
127401
}
128402

129403
/* -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.
130404

131405
You may pass only one of the values from the NSSearchPathDomainMask enumeration, and you may not pass NSAllDomainsMask.
132406
*/
133407
open func url(for directory: SearchPathDirectory, in domain: SearchPathDomainMask, appropriateFor url: URL?, create shouldCreate: Bool) throws -> URL {
134-
NSUnimplemented()
408+
let urls = self.urls(for: directory, in: domain)
409+
guard let url = urls.first else {
410+
// On Apple OSes, this case returns nil without filling in the error parameter; Swift then synthesizes an error rather than trap.
411+
// We simulate that behavior by throwing a private error.
412+
throw URLForDirectoryError.directoryUnknown
413+
}
414+
415+
if shouldCreate {
416+
var attributes: [FileAttributeKey : Any] = [:]
417+
418+
switch _SearchPathDomain(domain) {
419+
case .some(.user):
420+
attributes[.posixPermissions] = 0700
421+
422+
case .some(.system):
423+
attributes[.posixPermissions] = 0755
424+
attributes[.ownerAccountID] = 0 // root
425+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
426+
attributes[.ownerAccountID] = 80 // on Darwin, the admin group's fixed ID.
427+
#endif
428+
429+
default:
430+
break
431+
}
432+
433+
try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
434+
}
435+
436+
return url
135437
}
136438

137439
/* 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'.

0 commit comments

Comments
 (0)