Skip to content

Commit 1d31458

Browse files
authored
Merge pull request #1548 from spevans/pr_fm_enumerator
2 parents 0e91936 + 0027a3a commit 1d31458

File tree

2 files changed

+164
-83
lines changed

2 files changed

+164
-83
lines changed

Foundation/FileManager.swift

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -945,9 +945,6 @@ open class FileManager : NSObject {
945945
*/
946946
// Note: Because the error handler is an optional block, the compiler treats it as @escaping by default. If that behavior changes, the @escaping will need to be added back.
947947
open func enumerator(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: DirectoryEnumerationOptions = [], errorHandler handler: (/* @escaping */ (URL, Error) -> Bool)? = nil) -> DirectoryEnumerator? {
948-
if mask.contains(.skipsPackageDescendants) || mask.contains(.skipsHiddenFiles) {
949-
NSUnimplemented("Enumeration options not yet implemented")
950-
}
951948
return NSURLDirectoryEnumerator(url: url, options: mask, errorHandler: handler)
952949
}
953950

@@ -1309,19 +1306,25 @@ extension FileManager {
13091306
internal class NSPathDirectoryEnumerator: DirectoryEnumerator {
13101307
let baseURL: URL
13111308
let innerEnumerator : DirectoryEnumerator
1309+
private var _currentItemPath: String?
1310+
13121311
override var fileAttributes: [FileAttributeKey : Any]? {
1313-
NSUnimplemented()
1312+
guard let currentItemPath = _currentItemPath else {
1313+
return nil
1314+
}
1315+
return try? FileManager.default.attributesOfItem(atPath: baseURL.appendingPathComponent(currentItemPath).path)
13141316
}
1317+
13151318
override var directoryAttributes: [FileAttributeKey : Any]? {
1316-
NSUnimplemented()
1319+
return try? FileManager.default.attributesOfItem(atPath: baseURL.path)
13171320
}
13181321

13191322
override var level: Int {
1320-
NSUnimplemented()
1323+
return innerEnumerator.level
13211324
}
13221325

13231326
override func skipDescendants() {
1324-
NSUnimplemented()
1327+
innerEnumerator.skipDescendants()
13251328
}
13261329

13271330
init?(path: String) {
@@ -1339,9 +1342,9 @@ extension FileManager {
13391342
return nil
13401343
}
13411344
let path = url.path.replacingOccurrences(of: baseURL.path+"/", with: "")
1345+
_currentItemPath = path
13421346
return path
13431347
}
1344-
13451348
}
13461349

13471350
internal class NSURLDirectoryEnumerator : DirectoryEnumerator {
@@ -1380,32 +1383,58 @@ extension FileManager {
13801383
}
13811384

13821385
override func nextObject() -> Any? {
1386+
1387+
func match(filename: String, to options: DirectoryEnumerationOptions, isDir: Bool) -> (Bool, Bool) {
1388+
var showFile = true
1389+
var skipDescendants = false
1390+
1391+
if isDir {
1392+
if options.contains(.skipsSubdirectoryDescendants) {
1393+
skipDescendants = true
1394+
}
1395+
// Ignore .skipsPackageDescendants
1396+
}
1397+
if options.contains(.skipsHiddenFiles) && (filename[filename._startOfLastPathComponent] == ".") {
1398+
showFile = false
1399+
skipDescendants = true
1400+
}
1401+
1402+
return (showFile, skipDescendants)
1403+
}
1404+
1405+
13831406
if let stream = _stream {
13841407

13851408
if !_gotRoot {
13861409
_gotRoot = true
13871410

13881411
// Skip the root.
13891412
_current = fts_read(stream)
1390-
13911413
}
13921414

13931415
_current = fts_read(stream)
13941416
while let current = _current {
1417+
let filename = NSString(bytes: current.pointee.fts_path, length: Int(strlen(current.pointee.fts_path)), encoding: String.Encoding.utf8.rawValue)!._swiftObject
1418+
13951419
switch Int32(current.pointee.fts_info) {
13961420
case FTS_D:
1397-
if _options.contains(.skipsSubdirectoryDescendants) {
1421+
let (showFile, skipDescendants) = match(filename: filename, to: _options, isDir: true)
1422+
if skipDescendants {
13981423
fts_set(_stream, _current, FTS_SKIP)
13991424
}
1400-
fallthrough
1425+
if showFile {
1426+
return URL(fileURLWithPath: filename)
1427+
}
1428+
14011429
case FTS_DEFAULT, FTS_F, FTS_NSOK, FTS_SL, FTS_SLNONE:
1402-
let str = NSString(bytes: current.pointee.fts_path, length: Int(strlen(current.pointee.fts_path)), encoding: String.Encoding.utf8.rawValue)!._swiftObject
1403-
return URL(fileURLWithPath: str)
1430+
let (showFile, _) = match(filename: filename, to: _options, isDir: false)
1431+
if showFile {
1432+
return URL(fileURLWithPath: filename)
1433+
}
14041434
case FTS_DNR, FTS_ERR, FTS_NS:
1405-
let keepGoing : Bool
1435+
let keepGoing: Bool
14061436
if let handler = _errorHandler {
1407-
let str = NSString(bytes: current.pointee.fts_path, length: Int(strlen(current.pointee.fts_path)), encoding: String.Encoding.utf8.rawValue)!._swiftObject
1408-
keepGoing = handler(URL(fileURLWithPath: str), _NSErrorWithErrno(current.pointee.fts_errno, reading: true))
1437+
keepGoing = handler(URL(fileURLWithPath: filename), _NSErrorWithErrno(current.pointee.fts_errno, reading: true))
14091438
} else {
14101439
keepGoing = true
14111440
}

TestFoundation/TestFileManager.swift

Lines changed: 119 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -343,93 +343,153 @@ class TestFileManager : XCTestCase {
343343
try fm.createDirectory(atPath: basePath, withIntermediateDirectories: false, attributes: nil)
344344
try fm.createDirectory(atPath: basePath2, withIntermediateDirectories: false, attributes: nil)
345345

346-
let _ = fm.createFile(atPath: itemPath, contents: Data(), attributes: nil)
347-
let _ = fm.createFile(atPath: itemPath2, contents: Data(), attributes: nil)
346+
let _ = fm.createFile(atPath: itemPath, contents: Data(count: 123), attributes: nil)
347+
let _ = fm.createFile(atPath: itemPath2, contents: Data(count: 456), attributes: nil)
348348

349349
} catch _ {
350350
XCTFail()
351351
}
352-
352+
353+
var item1FileAttributes: [FileAttributeKey: Any]!
354+
var item2FileAttributes: [FileAttributeKey: Any]!
353355
if let e = FileManager.default.enumerator(atPath: basePath) {
356+
let attrs = e.directoryAttributes
357+
XCTAssertNotNil(attrs)
358+
XCTAssertEqual(attrs?[.type] as? FileAttributeType, .typeDirectory)
359+
354360
var foundItems = Set<String>()
355361
while let item = e.nextObject() as? String {
356362
foundItems.insert(item)
363+
if item == "item" {
364+
item1FileAttributes = e.fileAttributes
365+
} else if item == "path2/item" {
366+
item2FileAttributes = e.fileAttributes
367+
}
357368
}
358369
XCTAssertEqual(foundItems, Set(["item", "path2", "path2/item"]))
359370
} else {
360371
XCTFail()
361372
}
362373

374+
XCTAssertNotNil(item1FileAttributes)
375+
if let size = item1FileAttributes[.size] as? NSNumber {
376+
XCTAssertEqual(size.int64Value, 123)
377+
} else {
378+
XCTFail("Cant get file size for 'item'")
379+
}
380+
381+
XCTAssertNotNil(item2FileAttributes)
382+
if let size = item2FileAttributes[.size] as? NSNumber {
383+
XCTAssertEqual(size.int64Value, 456)
384+
} else {
385+
XCTFail("Cant get file size for 'path2/item'")
386+
}
387+
388+
if let e2 = FileManager.default.enumerator(atPath: basePath) {
389+
var foundItems = Set<String>()
390+
while let item = e2.nextObject() as? String {
391+
foundItems.insert(item)
392+
if item == "path2" {
393+
e2.skipDescendants()
394+
XCTAssertEqual(e2.level, 1)
395+
XCTAssertNotNil(e2.fileAttributes)
396+
}
397+
}
398+
XCTAssertEqual(foundItems, Set(["item", "path2"]))
399+
} else {
400+
XCTFail()
401+
}
363402
}
364403

365404
func test_directoryEnumerator() {
366405
let fm = FileManager.default
367-
let testDirName = "testdir\(NSUUID().uuidString)"
368-
let path = NSTemporaryDirectory() + "\(testDirName)"
369-
let itemPath = NSTemporaryDirectory() + "\(testDirName)/item"
370-
371-
ignoreError { try fm.removeItem(atPath: path) }
372-
373-
do {
374-
try fm.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
375-
let _ = fm.createFile(atPath: itemPath, contents: Data(), attributes: nil)
376-
} catch _ {
377-
XCTFail()
406+
let basePath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)/"
407+
let subDirs1 = basePath + "subdir1/subdir2/.hiddenDir/subdir3/"
408+
let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5./.subdir6.ext/subdir7.ext./"
409+
let itemPath1 = basePath + "itemFile1"
410+
let itemPath2 = subDirs1 + "itemFile2."
411+
let itemPath3 = subDirs1 + "itemFile3.ext."
412+
let hiddenItem1 = basePath + ".hiddenFile1"
413+
let hiddenItem2 = subDirs1 + ".hiddenFile2"
414+
let hiddenItem3 = subDirs2 + ".hiddenFile3"
415+
let hiddenItem4 = subDirs2 + ".hiddenFile4.ext"
416+
417+
var fileLevels: [String: Int] = [
418+
"itemFile1": 1,
419+
".hiddenFile1": 1,
420+
"subdir1": 1,
421+
"subdir2": 2,
422+
"subdir4.app": 3,
423+
"subdir5.": 4,
424+
".subdir6.ext": 5,
425+
"subdir7.ext.": 6,
426+
".hiddenFile4.ext": 7,
427+
".hiddenFile3": 7,
428+
".hiddenDir": 3,
429+
"subdir3": 4,
430+
"itemFile3.ext.": 5,
431+
"itemFile2.": 5,
432+
".hiddenFile2": 5
433+
]
434+
435+
func directoryItems(options: FileManager.DirectoryEnumerationOptions) -> [String: Int]? {
436+
if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath), includingPropertiesForKeys: nil, options: options, errorHandler: nil) {
437+
var foundItems = [String:Int]()
438+
while let item = e.nextObject() as? URL {
439+
foundItems[item.lastPathComponent] = e.level
440+
}
441+
return foundItems
442+
} else {
443+
return nil
444+
}
378445
}
379446

380-
if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [], errorHandler: nil) {
381-
var foundItems = [String:Int]()
382-
while let item = e.nextObject() as? URL {
383-
foundItems[item.path] = e.level
447+
ignoreError { try fm.removeItem(atPath: basePath) }
448+
defer { ignoreError { try fm.removeItem(atPath: basePath) } }
449+
450+
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs1, withIntermediateDirectories: true, attributes: nil))
451+
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs2, withIntermediateDirectories: true, attributes: nil))
452+
for filename in [itemPath1, itemPath2, itemPath3, hiddenItem1, hiddenItem2, hiddenItem3, hiddenItem4] {
453+
XCTAssertTrue(fm.createFile(atPath: filename, contents: Data(), attributes: nil), "Cant create file '\(filename)'")
454+
}
455+
456+
if let foundItems = directoryItems(options: []) {
457+
XCTAssertEqual(foundItems.count, fileLevels.count)
458+
for (name, level) in foundItems {
459+
XCTAssertEqual(fileLevels[name], level, "File level for \(name) is wrong")
384460
}
385-
XCTAssertEqual(foundItems[itemPath], 1)
386461
} else {
387-
XCTFail()
462+
XCTFail("Cant enumerate directory at \(basePath) with options: []")
388463
}
389-
390-
let subDirPath = NSTemporaryDirectory() + "\(testDirName)/testdir2"
391-
let subDirItemPath = NSTemporaryDirectory() + "\(testDirName)/testdir2/item"
392-
do {
393-
try fm.createDirectory(atPath: subDirPath, withIntermediateDirectories: false, attributes: nil)
394-
let _ = fm.createFile(atPath: subDirItemPath, contents: Data(), attributes: nil)
395-
} catch _ {
396-
XCTFail()
464+
465+
if let foundItems = directoryItems(options: [.skipsHiddenFiles]) {
466+
XCTAssertEqual(foundItems.count, 5)
467+
} else {
468+
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsHiddenFiles]")
397469
}
398-
399-
if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [], errorHandler: nil) {
400-
var foundItems = [String:Int]()
401-
while let item = e.nextObject() as? URL {
402-
foundItems[item.path] = e.level
403-
}
404-
XCTAssertEqual(foundItems[itemPath], 1)
405-
XCTAssertEqual(foundItems[subDirPath], 1)
406-
XCTAssertEqual(foundItems[subDirItemPath], 2)
470+
471+
if let foundItems = directoryItems(options: [.skipsSubdirectoryDescendants]) {
472+
XCTAssertEqual(foundItems.count, 3)
407473
} else {
408-
XCTFail()
474+
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsSubdirectoryDescendants]")
409475
}
410-
411-
if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants], errorHandler: nil) {
412-
var foundItems = [String:Int]()
413-
while let item = e.nextObject() as? URL {
414-
foundItems[item.path] = e.level
415-
}
416-
XCTAssertEqual(foundItems[itemPath], 1)
417-
XCTAssertEqual(foundItems[subDirPath], 1)
476+
477+
if let foundItems = directoryItems(options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) {
478+
XCTAssertEqual(foundItems.count, 2)
418479
} else {
419-
XCTFail()
480+
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]")
420481
}
421-
422-
if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [], errorHandler: nil) {
423-
var foundItems = [String:Int]()
424-
while let item = e.nextObject() as? URL {
425-
foundItems[item.path] = e.level
426-
}
427-
XCTAssertEqual(foundItems[itemPath], 1)
428-
XCTAssertEqual(foundItems[subDirPath], 1)
482+
483+
if let foundItems = directoryItems(options: [.skipsPackageDescendants]) {
484+
#if DARWIN_COMPATIBILITY_TESTS
485+
XCTAssertEqual(foundItems.count, 10) // Only native Foundation does not gnore .skipsPackageDescendants
486+
#else
487+
XCTAssertEqual(foundItems.count, 15)
488+
#endif
429489
} else {
430-
XCTFail()
490+
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]")
431491
}
432-
492+
433493
var didGetError = false
434494
let handler : (URL, Error) -> Bool = { (URL, Error) in
435495
didGetError = true
@@ -443,21 +503,13 @@ class TestFileManager : XCTestCase {
443503
XCTAssertTrue(didGetError)
444504

445505
do {
446-
let contents = try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: []).map {
506+
let contents = try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: basePath), includingPropertiesForKeys: nil, options: []).map {
447507
return $0.path
448508
}
449-
XCTAssertEqual(contents.count, 2)
450-
XCTAssertTrue(contents.contains(itemPath))
451-
XCTAssertTrue(contents.contains(subDirPath))
509+
XCTAssertEqual(contents.count, 3)
452510
} catch {
453511
XCTFail()
454512
}
455-
456-
do {
457-
try fm.removeItem(atPath: path)
458-
} catch {
459-
XCTFail("Failed to clean up files")
460-
}
461513
}
462514

463515
func test_contentsOfDirectoryAtPath() {

0 commit comments

Comments
 (0)