//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2023 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc package import _CShims #endif // MARK: Directory Iteration struct _FTSSequence: Sequence { enum Element { struct SwiftFTSENT { fileprivate let ptr: UnsafeMutablePointer var ftsEnt: FTSENT { ptr.pointee } var name: String { // FTSENT incorrectly represents the `fts_name` property so we must access it directly via the pointer rather than the pointee struct value let nameOffset = MemoryLayout.offset(of: \.fts_name)! let len = Int(ptr.pointee.fts_namelen) return UnsafeRawPointer(ptr).advanced(by: nameOffset).withMemoryRebound(to: UTF8.CodeUnit.self, capacity: len) { namePtr in String(decoding: UnsafeBufferPointer(start: namePtr, count: len), as: UTF8.self) } } init(_ ptr: UnsafeMutablePointer) { self.ptr = ptr } } case entry(SwiftFTSENT) case error(errno: Int32, path: String) } final class Iterator: IteratorProtocol { enum State { case stream(UnsafeMutablePointer) case error(Int32, String) case ended } var state: State var path: UnsafePointer #if canImport(Darwin) var lastDeviceInode: dev_t = 0 var deviceNumbers: [dev_t] = [] var deviceEntryPoints: [ino_t] = [] var shouldFilterUnderbars = false #endif init(_ path: UnsafePointer, _ opts: Int32) { self.path = path var statBuf = stat() if lstat(path, &statBuf) > 0 { state = .error(errno, String(cString: path)) return } state = [UnsafeMutablePointer(mutating: path), nil].withUnsafeBufferPointer{ dirList in guard let stream = fts_open(dirList.baseAddress!, opts, nil) else { return .error(errno, String(cString: path)) } return .stream(stream) } } deinit { _close() } private func _close() { if case .stream(let fts) = state { fts_close(fts) } state = .ended } #if canImport(Darwin) private func _shouldFilter(_ swiftEnt: Element.SwiftFTSENT) -> Bool { let ent = swiftEnt.ftsEnt let ftsName = swiftEnt.name // If we're being requested to iterate a directory that begins with a ._ we should do it. if lastDeviceInode == 0 && ftsName.hasPrefix("._") { return false } // Instead of asking fts to stat every file just to get the fts_statp->st_dev, we can trust the already-gathered fts_dev info, which is present for at least every FTS_D and FTS_DP entry. 8740034. // Don't worry. Even if someone uses FTS_SKIP, FTS always balances FTS_D with FTS_DP. var currentDev = deviceNumbers.last ?? 0 if ent.fts_info == FTS_D { if deviceNumbers.last != ent.fts_dev { currentDev = ent.fts_dev deviceEntryPoints.append(ent.fts_ino) deviceNumbers.append(ent.fts_dev) } } else if ent.fts_info == FTS_DP { if let lastEntry = deviceEntryPoints.last, lastEntry == ent.fts_ino { deviceEntryPoints.removeLast() deviceNumbers.removeLast() } } if currentDev != lastDeviceInode { // We've crossed a mount point (i.e. the device is different than the last time we looked). var fileSystemInfo = statfs() shouldFilterUnderbars = statfs(ent.fts_path, &fileSystemInfo) == 0 && ((fileSystemInfo.f_flags & UInt32(MNT_DOVOLFS)) == 0) lastDeviceInode = currentDev } if shouldFilterUnderbars && ftsName.hasPrefix("._") { // Don't report ._ files on filesystems that require them. return true } return false } #else private func _shouldFilter(_ swiftEnt: Element.SwiftFTSENT) -> Bool { false } #endif func next() -> Element? { switch state { case .stream(let fts): if let ent = fts_read(fts) { let swiftEnt = Element.SwiftFTSENT(ent) if _shouldFilter(swiftEnt) { return self.next() } else { return .entry(swiftEnt) } } else if errno != 0 { let errNumber = errno _close() return .error(errno: errNumber, path: String(cString: path)) } else { _close() return nil } case .error(let errNum, let path): state = .ended return .error(errno: errNum, path: path) case .ended: return nil } } func skipDescendants(of entry: Element.SwiftFTSENT, skipPostProcessing: Bool = false) { guard case .stream(let fts) = state else { return } _ = fts_set(fts, entry.ptr, FTS_SKIP) if skipPostProcessing { assert(Int32(entry.ftsEnt.fts_info) == FTS_D) _ = self.next() // Skip the FTS_DP entry for this directory } } } let path: UnsafePointer let opts: Int32 init(_ path: UnsafePointer, _ opts: Int32) { self.path = path self.opts = opts } func makeIterator() -> Iterator { Iterator(path, opts) } } enum SubpathElement { case entry(String) case error(Int32, String) } extension Sequence<_FTSSequence.Element> { var subpaths: some Sequence { self.lazy.compactMap { switch $0 { case .error(let error, let path): return .error(error, path) case .entry(let ent): switch Int32(ent.ftsEnt.fts_info) { // Do the action case FTS_D: fallthrough // Directory being visited in pre-order. case FTS_DEFAULT: fallthrough // Something not defined anywhere else. case FTS_F: fallthrough // Regular file. case FTS_NSOK: fallthrough // No stat(2) information was requested, but that's OK. case FTS_SL: fallthrough // Symlink. case FTS_SLNONE: // Symlink with no target. return .entry(String(cString: ent.ftsEnt.fts_path)) // Error returns case FTS_DNR: fallthrough // Directory cannot be read. case FTS_ERR: fallthrough // Some error occurred, but we don't know what. case FTS_NS: // No stat(2) information is available. let path = String(cString: ent.ftsEnt.fts_path) return .error(ent.ftsEnt.fts_errno, path) default: return nil } } } } } struct _POSIXDirectoryContentsSequence: Sequence { #if canImport(Darwin) typealias DirectoryEntryPtr = UnsafeMutablePointer #elseif canImport(Glibc) typealias DirectoryEntryPtr = OpaquePointer #endif final class Iterator: IteratorProtocol { func next() -> Element? { guard let dirp else { return nil } // Loop until we find a value or end repeat { guard let dent = readdir(dirp) else { closedir(dirp) self.dirp = nil return nil } #if canImport(Darwin) guard dent.pointee.d_namlen != 0 else { continue } #endif guard dent.pointee.d_ino != 0 else { continue } // Use name let fileName = withUnsafeBytes(of: &dent.pointee.d_name) { buf in let ptr = buf.baseAddress!.assumingMemoryBound(to: CChar.self) return String(cString: ptr) } if fileName == "." || fileName == ".." || fileName == "._" { continue } let fullFileName: String if appendSlash { var isDirectory = false if dent.pointee.d_type == DT_DIR { isDirectory = true } else if dent.pointee.d_type == DT_UNKNOWN { // We need to do an additional stat on this to see if it's really a directory or not. // This path should be uncommon. var statBuf: stat = stat() let statDir = directoryPath + "/" + fileName if stat(statDir, &statBuf) == 0 { // #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) if (statBuf.st_mode & S_IFMT) == S_IFDIR { isDirectory = true } } } if isDirectory { fullFileName = prefix + fileName + "/" } else { fullFileName = prefix + fileName } } else { fullFileName = prefix + fileName } return Element(fileName: fileName, fileNameWithPrefix: fullFileName, fileType: dent.pointee.d_type) } while true } struct Element { var fileName: String var fileNameWithPrefix: String var fileType: UInt8 } private var dirp: DirectoryEntryPtr? private let directoryPath: String private let prefix: String private let appendSlash: Bool var error: CocoaError? init(path: String, appendSlashForDirectory: Bool = false, prefix: [String] = []) { let dirp = path.withFileSystemRepresentation { ptr -> DirectoryEntryPtr? in guard let ptr else { return nil } return opendir(ptr) } if let dirp { directoryPath = path self.dirp = dirp self.appendSlash = appendSlashForDirectory // Ensure stuff to prefix list is all /-terminated let prefixes: [String] = prefix.compactMap { guard let last = $0.last else { // This string is empty return nil } if last == "/" { return $0 } else { return $0 + "/" } } self.prefix = prefixes.joined() } else { // It would be nice to propagate an error from here, but for now the best we can do is return nil from `next`. directoryPath = "" self.prefix = "" appendSlash = false error = CocoaError.errorWithFilePath(path, errno: errno, reading: true, variant: "Folder") } } deinit { if let dirp { closedir(dirp) } } } let path: String let appendSlashForDirectory: Bool let prefix: [String] init(path: String, appendSlashForDirectory: Bool, prefix: [String] = []) { self.path = path self.appendSlashForDirectory = appendSlashForDirectory self.prefix = prefix } func makeIterator() -> Iterator { Iterator(path: path, appendSlashForDirectory: appendSlashForDirectory, prefix: prefix) } }