mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-23 22:13:35 +08:00
* (120741818) Port FileManager to swift-foundation * (120741818) Fix linux test failures * (120741818) Fix build failures
354 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|