|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift.org open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2023 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | + |
| 13 | +#if canImport(Darwin) |
| 14 | +import Darwin |
| 15 | +#elseif canImport(Glibc) |
| 16 | +import Glibc |
| 17 | +#endif |
| 18 | + |
| 19 | +internal struct _FileManagerImpl { |
| 20 | + weak var _manager: FileManager? |
| 21 | + weak var delegate: FileManagerDelegate? |
| 22 | + |
| 23 | + var fileManager: FileManager { |
| 24 | + guard let _manager else { |
| 25 | + fatalError("_FileManagerImpl called without a valid reference to a FileManager") |
| 26 | + } |
| 27 | + return _manager |
| 28 | + } |
| 29 | + |
| 30 | + var safeDelegate: FileManagerDelegate? { |
| 31 | +#if FOUNDATION_FRAMEWORK |
| 32 | + fileManager._safeDelegate() as? FileManagerDelegate |
| 33 | +#else |
| 34 | + self.delegate |
| 35 | +#endif |
| 36 | + } |
| 37 | + |
| 38 | + init() {} |
| 39 | + |
| 40 | + #if FOUNDATION_FRAMEWORK |
| 41 | + func displayName(atPath path: String) -> String { |
| 42 | + // We lie to filePath:directoryHint: to avoid the extra stat. Since this URL isn't used as a base URL for another URL, it shouldn't make any difference. |
| 43 | + let url = URL(filePath: path, directoryHint: .notDirectory) |
| 44 | + |
| 45 | + if let storedName = try? url.resourceValues(forKeys: [.localizedNameKey]).localizedName { |
| 46 | + return storedName |
| 47 | + } |
| 48 | + |
| 49 | + return path.lastPathComponent.replacing(":", with: "/") |
| 50 | + } |
| 51 | + #endif |
| 52 | + |
| 53 | + func contents(atPath path: String) -> Data? { |
| 54 | + try? Data(contentsOfFile: path) |
| 55 | + } |
| 56 | + |
| 57 | + func contentsEqual( |
| 58 | + atPath path: String, |
| 59 | + andPath other: String |
| 60 | + ) -> Bool { |
| 61 | + func _openFD(_ path: UnsafePointer<CChar>) -> Int32? { |
| 62 | + var statBuf = stat() |
| 63 | + let fd = open(path, 0, 0) |
| 64 | + guard fd >= 0 else { return nil } |
| 65 | + if fstat(fd, &statBuf) < 0 || statBuf.st_mode & S_IFMT == S_IFDIR { |
| 66 | + close(fd) |
| 67 | + return nil |
| 68 | + } |
| 69 | + return fd |
| 70 | + } |
| 71 | + |
| 72 | + // compares contents in efficient manner |
| 73 | + // note that symlinks are not traversed! |
| 74 | + guard let myInfo = fileManager._fileStat(path), let otherInfo = fileManager._fileStat(other) else { |
| 75 | + return false |
| 76 | + } |
| 77 | + |
| 78 | + /* check for being hard links */ |
| 79 | + if myInfo.st_dev == otherInfo.st_dev && myInfo.st_ino == otherInfo.st_ino { |
| 80 | + return true |
| 81 | + } |
| 82 | + |
| 83 | + /* check for being same type */ |
| 84 | + if myInfo.st_mode & S_IFMT != otherInfo.st_mode & S_IFMT { |
| 85 | + return false |
| 86 | + } |
| 87 | + |
| 88 | + if myInfo.isSpecial { |
| 89 | + return myInfo.st_rdev == otherInfo.st_rdev // different inodes aiming at same device |
| 90 | + } |
| 91 | + |
| 92 | + if myInfo.isRegular { |
| 93 | + if myInfo.st_size != otherInfo.st_size { |
| 94 | + return false |
| 95 | + } |
| 96 | + return fileManager.withFileSystemRepresentation(for: path) { pathPtr in |
| 97 | + guard let pathPtr, let fd1 = _openFD(pathPtr) else { return false } |
| 98 | + defer { close(fd1) } |
| 99 | + return fileManager.withFileSystemRepresentation(for: other) { otherPtr in |
| 100 | + guard let otherPtr, let fd2 = _openFD(otherPtr) else { return false } |
| 101 | + defer { close(fd2) } |
| 102 | + #if canImport(Darwin) |
| 103 | + _ = fcntl(fd1, F_NOCACHE, 1) |
| 104 | + _ = fcntl(fd2, F_NOCACHE, 1) |
| 105 | + #endif |
| 106 | + let quantum = 8 * 1024 |
| 107 | + return withUnsafeTemporaryAllocation(of: CChar.self, capacity: quantum) { buf1 in |
| 108 | + buf1.initialize(repeating: 0) |
| 109 | + defer { buf1.deinitialize() } |
| 110 | + return withUnsafeTemporaryAllocation(of: CChar.self, capacity: quantum) { buf2 in |
| 111 | + buf2.initialize(repeating: 0) |
| 112 | + defer { buf2.deinitialize() } |
| 113 | + var readBytes = 0 |
| 114 | + while true { |
| 115 | + readBytes = read(fd1, buf1.baseAddress!, quantum) |
| 116 | + guard readBytes > 0 else { break } |
| 117 | + if read(fd2, buf2.baseAddress!, quantum) != readBytes { |
| 118 | + return false |
| 119 | + } |
| 120 | + if !buf1.elementsEqual(buf2) { |
| 121 | + return false |
| 122 | + } |
| 123 | + } |
| 124 | + if readBytes < -1 { return false } |
| 125 | + return true |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } else if myInfo.isSymbolicLink { |
| 131 | + return (try? fileManager.destinationOfSymbolicLink(atPath: path) == fileManager.destinationOfSymbolicLink(atPath: other)) ?? false |
| 132 | + } else if myInfo.isDirectory { |
| 133 | + guard let myContents = try? fileManager.contentsOfDirectory(atPath: path), |
| 134 | + let otherContents = try? Set(fileManager.contentsOfDirectory(atPath: other)), |
| 135 | + myContents.count == otherContents.count else { return false } |
| 136 | + for item in myContents { |
| 137 | + guard otherContents.contains(item) else { return false } |
| 138 | + let myItemPath = "\(path)/\(item)" |
| 139 | + let otherItemPath = "\(other)/\(item)" |
| 140 | + // Ok to call to self here because it's the same function |
| 141 | + if !self.contentsEqual(atPath: myItemPath, andPath: otherItemPath) { |
| 142 | + return false |
| 143 | + } |
| 144 | + } |
| 145 | + return true |
| 146 | + } |
| 147 | + |
| 148 | + fatalError("Unknown file type 0x\(String(myInfo.st_mode, radix: 16)) for file \(path)") |
| 149 | + } |
| 150 | + |
| 151 | + func fileSystemRepresentation(withPath path: String) -> UnsafePointer<CChar>? { |
| 152 | + path.withFileSystemRepresentation { ptr -> UnsafePointer<CChar>? in |
| 153 | + guard let ptr else { |
| 154 | + return nil |
| 155 | + } |
| 156 | + |
| 157 | + let len = strlen(ptr) + 1 |
| 158 | + let newPtr = UnsafeMutablePointer<CChar>.allocate(capacity: len) |
| 159 | + memcpy(newPtr, ptr, len) |
| 160 | + return UnsafePointer(newPtr) |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + // SPI |
| 165 | + func getFileSystemRepresentation(_ buffer: UnsafeMutablePointer<CChar>, maxLength: UInt, with path: String) -> Bool { |
| 166 | + guard !path.isEmpty else { |
| 167 | + return false |
| 168 | + } |
| 169 | + return path.withFileSystemRepresentation { ptr in |
| 170 | + guard let ptr else { |
| 171 | + return false |
| 172 | + } |
| 173 | + let lengthOfData = strlen(ptr) + 1 |
| 174 | + guard lengthOfData <= maxLength else { |
| 175 | + return false |
| 176 | + } |
| 177 | + |
| 178 | + memcpy(buffer, ptr, lengthOfData) |
| 179 | + return true |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + func string( |
| 184 | + withFileSystemRepresentation str: UnsafePointer<CChar>, |
| 185 | + length len: Int |
| 186 | + ) -> String { |
| 187 | + UnsafeBufferPointer(start: str, count: len).withMemoryRebound(to: UInt8.self) { buffer in |
| 188 | + String(decoding: buffer, as: UTF8.self) |
| 189 | + } |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +extension FileManager { |
| 194 | + #if FOUNDATION_FRAMEWORK |
| 195 | + @nonobjc |
| 196 | + func withFileSystemRepresentation<R>(for path: String, _ body: (UnsafePointer<CChar>?) throws -> R) rethrows -> R { |
| 197 | + var selfType: Any.Type { Self.self } |
| 198 | + if selfType != FileManager.self { |
| 199 | + // Subclasses can override getFileSystemRepresentation. Continue to call into that function to preserve subclassing behavior |
| 200 | + return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in |
| 201 | + guard self.getFileSystemRepresentation(buffer.baseAddress!, maxLength: FileManager.MAX_PATH_SIZE, withPath: path) else { |
| 202 | + return try body(nil) |
| 203 | + } |
| 204 | + return try body(buffer.baseAddress) |
| 205 | + } |
| 206 | + } |
| 207 | + // We don't have a subclass, so we can call this directly to avoid the temp allocation + copy |
| 208 | + return try path.withFileSystemRepresentation(body) |
| 209 | + } |
| 210 | + #endif |
| 211 | + |
| 212 | + @nonobjc |
| 213 | + func _fileStat(_ path: String) -> stat? { |
| 214 | + let result = self.withFileSystemRepresentation(for: path) { rep -> stat? in |
| 215 | + var s = stat() |
| 216 | + guard let rep, lstat(rep, &s) == 0 else { |
| 217 | + return nil |
| 218 | + } |
| 219 | + return s |
| 220 | + } |
| 221 | + |
| 222 | + guard let result else { return nil } |
| 223 | + return result |
| 224 | + } |
| 225 | + |
| 226 | + @nonobjc |
| 227 | + static var MAX_PATH_SIZE: Int { 1026 } |
| 228 | +} |
0 commit comments