swift-foundation/Sources/FoundationEssentials/FileManager/FileOperations+Enumeration.swift
Jeremy Schonfeld 33856a556d
(120741818) Port FileManager to swift-foundation
* (120741818) Port FileManager to swift-foundation

* (120741818) Fix linux test failures

* (120741818) Fix build failures
2024-02-09 15:03:43 -08:00

354 lines
13 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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<FTSENT>
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<FTSENT>.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<FTSENT>) {
self.ptr = ptr
}
}
case entry(SwiftFTSENT)
case error(errno: Int32, path: String)
}
final class Iterator: IteratorProtocol {
enum State {
case stream(UnsafeMutablePointer<FTS>)
case error(Int32, String)
case ended
}
var state: State
var path: UnsafePointer<CChar>
#if canImport(Darwin)
var lastDeviceInode: dev_t = 0
var deviceNumbers: [dev_t] = []
var deviceEntryPoints: [ino_t] = []
var shouldFilterUnderbars = false
#endif
init(_ path: UnsafePointer<CChar>, _ 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<CChar>
let opts: Int32
init(_ path: UnsafePointer<CChar>, _ 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<SubpathElement> {
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<DIR>
#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)
}
}