import Cocoa class ViewController: NSViewController { @IBOutlet weak var label: NSTextField! @IBOutlet var textView: NSTextView! var task: Task < Void, Error > ! @IBAction func cancel(_ sender: Any) { task.cancel() } override func viewDidAppear() { let openPanel = NSOpenPanel() openPanel.canChooseDirectories = true openPanel.canChooseFiles = false openPanel.runModal() let url = openPanel.urls[0] task = Task { let scan = Scan() scan.label = label scan.textView = textView await scan.scanDirectory(at: url.path, level: 0) } } } class Scan { static var attributeKeys = { var attributeKeys = attrlist() attributeKeys.bitmapcount = u_short(ATTR_BIT_MAP_COUNT) attributeKeys.commonattr = attrgroup_t(ATTR_CMN_RETURNED_ATTRS) | attrgroup_t(bitPattern: ATTR_CMN_ERROR | ATTR_CMN_NAME | ATTR_CMN_OBJTYPE | ATTR_CMN_CRTIME | ATTR_CMN_MODTIME | ATTR_CMN_CHGTIME | ATTR_CMN_FLAGS | ATTR_CMN_FILEID) attributeKeys.dirattr = attrgroup_t(bitPattern: ATTR_DIR_MOUNTSTATUS) attributeKeys.fileattr = attrgroup_t(bitPattern: ATTR_FILE_DATALENGTH) return attributeKeys }() var label: NSTextField! var textView: NSTextView! var total = 0 let bufferWithAlignment16 = UnsafeMutableRawBufferPointer.allocate(byteCount: 32 * 1024, alignment: 16) func scanDirectory(at path: String, level: Int) async { do { let fileManagerCount = try FileManager.default.contentsOfDirectory(atPath: path).count await addLog("start scan \(path) level \(level) fileManagerCount \(fileManagerCount)", level: level) } catch { await addLog("start scan \(path) level \(level) fileManagerCount error: \(error)", level: level) } let fileDescriptor = open(path, O_RDONLY) if fileDescriptor < 0 { await addScanError(NSError(domain: NSPOSIXErrorDomain, code: Int(errno))) return } defer { close(fileDescriptor) } let attributeBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 32 * 1024, alignment: 16) defer { attributeBuffer.deallocate() } var sum = 0 while true { let itemCount = Int(getattrlistbulk(fileDescriptor, &Scan.attributeKeys, attributeBuffer.baseAddress!, attributeBuffer.count, 0)) sum += itemCount total += itemCount await setLabel("\(total)") await addLog("continue scan \(path) new items \(itemCount), sum \(sum), errno \(errno)", level: level) if itemCount == 0 { return } else if itemCount > 0 { var entryOffset = attributeBuffer.baseAddress! for _ in 0 ..< itemCount { let length = Int(entryOffset.load(as: UInt32.self)) let result = unpackResources(at: entryOffset + MemoryLayout < UInt32 > .size, parentDirectory: path) entryOffset += length guard let (error, file, isDirectory, /*isMountPoint*/_) = result else { continue } if Task.isCancelled { return } await addLog("\(file.path)", level: level) if let error = error { await addScanError(error) continue } else if isDirectory /*&& !isMountPoint*/ { await scanDirectory(at: file.path, level: level + 1) } } } else if errno != EINTR { await addScanError(NSError(domain: NSPOSIXErrorDomain, code: Int(errno))) return } } } private func unpackResources(at attributeOffset: UnsafeMutableRawPointer, parentDirectory: String) -> (error: Error?, file: File, isDirectory: Bool, isMountPoint: Bool)? { var attributeOffset = attributeOffset let returned = attributeOffset.load(as: attribute_set_t.self) attributeOffset += MemoryLayout < attribute_set_t > .size var _error: Error? if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_ERROR)) != 0 { _error = NSError(domain: NSPOSIXErrorDomain, code: Int(attributeOffset.load(as: UInt32.self))) attributeOffset += MemoryLayout < UInt32 > .size } let name: String if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_NAME)) != 0 { let nameInfo = attributeOffset.load(as: attrreference_t.self) let originalName = String(cString: (attributeOffset + Int(nameInfo.attr_dataoffset)).assumingMemoryBound(to: CChar.self)) name = originalName attributeOffset += MemoryLayout < attrreference_t > .size } else { name = "" } let fullPath = (parentDirectory as NSString).appendingPathComponent(name) let fileType: fsobj_type_t if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_OBJTYPE)) != 0 { fileType = attributeOffset.load(as: fsobj_type_t.self) attributeOffset += MemoryLayout < fsobj_type_t > .size } else { fileType = VNON.rawValue } let creationDate: Date if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_CRTIME)) != 0 { creationDate = Date(timeIntervalSince1970: TimeInterval(readUnaligned(pointer: &attributeOffset, as: timespec.self).tv_sec)) } else { creationDate = .distantPast } let modificationDate: Date if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_MODTIME)) != 0 { modificationDate = Date(timeIntervalSince1970: TimeInterval(readUnaligned(pointer: &attributeOffset, as: timespec.self).tv_sec)) // time modified is reset to midnight for some files on FTP server (FB13671336) } else { modificationDate = .distantPast } let changeDate: Date if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_CHGTIME)) != 0 { changeDate = Date(timeIntervalSince1970: TimeInterval(readUnaligned(pointer: &attributeOffset, as: timespec.self).tv_sec)) } else { changeDate = .distantPast } let flags: UInt32 if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_FLAGS)) != 0 { flags = readUnaligned(pointer: &attributeOffset, as: UInt32.self) } else { flags = 0 } let fileId: UInt64 if (returned.commonattr & attrgroup_t(bitPattern: ATTR_CMN_FILEID)) != 0 { fileId = readUnaligned(pointer: &attributeOffset, as: UInt64.self) } else { fileId = 0 } let mountStatus: Int if (returned.dirattr & attrgroup_t(bitPattern: ATTR_DIR_MOUNTSTATUS)) != 0 { mountStatus = Int(readUnaligned(pointer: &attributeOffset, as: off_t.self)) } else { mountStatus = 0 } let size: Int64 if (returned.fileattr & attrgroup_t(bitPattern: ATTR_FILE_DATALENGTH)) != 0 { size = Int64(readUnaligned(pointer: &attributeOffset, as: off_t.self)) } else { size = 0 } let isDirectory = fileType == VDIR.rawValue let isSymbolicLink = fileType == VLNK.rawValue let isHidden = (Int32(flags) & UF_HIDDEN == UF_HIDDEN || name.hasPrefix(".")) && name != "Icon\r" let isLocked = Int32(flags) & UF_IMMUTABLE == UF_IMMUTABLE let file = File(path: fullPath, size: size, modificationDate: modificationDate, fileId: fileId, isSymbolicLink: isSymbolicLink, isHidden: isHidden, isLocked: isLocked, changeDate: changeDate, creationDate: creationDate) let isMountPoint = mountStatus == DIR_MNTSTATUS_MNTPOINT return (_error, file, isDirectory, isMountPoint) } private func readUnaligned < Result > (pointer: inout UnsafeMutableRawPointer, as: Result.Type) -> Result { let count = MemoryLayout < Result > .size bufferWithAlignment16.copyMemory(from: UnsafeRawBufferPointer(start: pointer, count: count)) pointer += count return bufferWithAlignment16.baseAddress!.load(as: Result.self) } @MainActor func addScanError(_ error: Error) async { textView.textStorage!.append(NSAttributedString(string: "\(error)\n", attributes: [.foregroundColor: NSColor.labelColor])) textView.scrollToEndOfDocument(nil) } @MainActor func addLog(_ log: String, level: Int) async { textView.textStorage!.append(NSAttributedString(string: "\(String(repeating: "\t", count: level))\(log)\n", attributes: [.foregroundColor: NSColor.labelColor])) textView.scrollToEndOfDocument(nil) } @MainActor func setLabel(_ label: String) async { self.label.stringValue = label } } struct File { let path: String let size: Int64 let modificationDate: Date let fileId: UInt64 let isSymbolicLink: Bool let isHidden: Bool let isLocked: Bool let changeDate: Date let creationDate: Date }