mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-24 06:20:09 +08:00
GetBinaryType will return ERROR_BAD_EXE_FORMAT when querying an arm64 executable from an x86 process running on an ARM system. This change switches the implementation to use SHGetFileInfoW, which isn't subject to this quirk. This also makes isExecutableFile behave more similarly to other platforms -- e.g. isExecutableFile already returns true for any file with the execute bit, even for an arm64 executable on an x86_64 macOS system (which it can't actually run). The spirit of the API is that the file is of an executable type, not necessarily that the running system is capable of executing it. The practical consequence of fixing this bug is that queries like: ```swift FileManager.default.isExecutableFile(atPath: "C:\\Windows\\system32\\cmd.exe") ``` will now correctly return true regardless of what architecture the binary is compiled for or what type of system it's running on. Closes #860
1005 lines
41 KiB
Swift
1005 lines
41 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 FOUNDATION_FRAMEWORK
|
|
internal import Foundation_Private.NSFileManager
|
|
internal import DarwinPrivate.sys.content_protection
|
|
#endif
|
|
|
|
#if canImport(Darwin)
|
|
import Darwin
|
|
#elseif canImport(Android)
|
|
@preconcurrency import Android
|
|
import posix_filesystem
|
|
#elseif canImport(Glibc)
|
|
@preconcurrency import Glibc
|
|
internal import _FoundationCShims
|
|
#elseif canImport(Musl)
|
|
@preconcurrency import Musl
|
|
internal import _FoundationCShims
|
|
#elseif os(Windows)
|
|
import CRT
|
|
import WinSDK
|
|
#elseif os(WASI)
|
|
internal import _FoundationCShims
|
|
@preconcurrency import WASILibc
|
|
#endif
|
|
|
|
extension Date {
|
|
fileprivate init(seconds: TimeInterval, nanoSeconds: TimeInterval) {
|
|
self.init(timeIntervalSinceReferenceDate: seconds - Self.timeIntervalBetween1970AndReferenceDate + nanoSeconds / 1_000_000_000.0 )
|
|
}
|
|
}
|
|
|
|
#if !os(Windows)
|
|
extension mode_t {
|
|
private var _fileType: FileAttributeType {
|
|
switch self & S_IFMT {
|
|
case S_IFCHR: .typeCharacterSpecial
|
|
case S_IFDIR: .typeDirectory
|
|
case S_IFBLK: .typeBlockSpecial
|
|
case S_IFREG: .typeRegular
|
|
case S_IFLNK: .typeSymbolicLink
|
|
case S_IFSOCK: .typeSocket
|
|
default: .typeUnknown
|
|
}
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
// Since FileAttributeType is an NS_TYPED_ENUM, clients rely on being able to cast values to both String and FileAttributeType
|
|
// Store NSString values in attribute dictionaries to support both of these casting behaviors
|
|
fileprivate var fileType: NSString { _fileType as NSString }
|
|
#else
|
|
// In swift-foundation, use FileAttributeType values instead since NSString doesn't exist
|
|
fileprivate var fileType: FileAttributeType { _fileType }
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
func _readFileAttributePrimitive<T: BinaryInteger>(_ value: Any?, as type: T.Type) -> T? {
|
|
guard let value else { return nil }
|
|
#if FOUNDATION_FRAMEWORK
|
|
if let nsNumber = value as? NSNumber, let result = nsNumber as? T {
|
|
return result
|
|
}
|
|
#endif
|
|
|
|
if let exact = value as? T {
|
|
return exact
|
|
} else if let binInt = value as? (any BinaryInteger), let result = T(exactly: binInt) {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func _readFileAttributePrimitive(_ value: Any?, as type: Bool.Type) -> Bool? {
|
|
guard let value else { return nil }
|
|
#if FOUNDATION_FRAMEWORK
|
|
if let nsNumber = value as? NSNumber, let result = nsNumber as? Bool {
|
|
return result
|
|
}
|
|
#endif
|
|
|
|
if let boolValue = value as? Bool {
|
|
return boolValue
|
|
} else if let binInt = value as? (any BinaryInteger), let result = Int(exactly: binInt) {
|
|
switch result {
|
|
case 0: return false
|
|
case 1: return true
|
|
default: return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
#if !FOUNDATION_FRAMEWORK
|
|
@_spi(SwiftCorelibsFoundation)
|
|
public protocol _NSNumberInitializer {
|
|
static func initialize(value: Bool) -> Any
|
|
static func initialize(value: some BinaryInteger) -> Any
|
|
}
|
|
|
|
@_spi(SwiftCorelibsFoundation)
|
|
dynamic public func _nsNumberInitializer() -> (any _NSNumberInitializer.Type)? {
|
|
// Dynamically replaced by swift-corelibs-foundation
|
|
return nil
|
|
}
|
|
#endif
|
|
|
|
func _writeFileAttributePrimitive<T: BinaryInteger, U: BinaryInteger>(_ value: T, as type: U.Type) -> Any {
|
|
#if FOUNDATION_FRAMEWORK
|
|
if let int = Int64(exactly: value) {
|
|
NSNumber(value: int)
|
|
} else {
|
|
NSNumber(value: UInt64(value))
|
|
}
|
|
#else
|
|
if let ns = _nsNumberInitializer()?.initialize(value: value) {
|
|
return ns
|
|
} else {
|
|
return U(value)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func _writeFileAttributePrimitive(_ value: Bool) -> Any {
|
|
#if FOUNDATION_FRAMEWORK
|
|
NSNumber(value: value)
|
|
#else
|
|
if let ns = _nsNumberInitializer()?.initialize(value: value) {
|
|
return ns
|
|
} else {
|
|
return value
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if !os(Windows)
|
|
extension stat {
|
|
var modificationDate: Date {
|
|
#if canImport(Darwin)
|
|
Date(seconds: TimeInterval(st_mtimespec.tv_sec), nanoSeconds: TimeInterval(st_mtimespec.tv_nsec))
|
|
#else
|
|
Date(seconds: TimeInterval(st_mtim.tv_sec), nanoSeconds: TimeInterval(st_mtim.tv_nsec))
|
|
#endif
|
|
}
|
|
|
|
var creationDate: Date {
|
|
#if canImport(Darwin)
|
|
Date(seconds: TimeInterval(st_ctimespec.tv_sec), nanoSeconds: TimeInterval(st_ctimespec.tv_nsec))
|
|
#else
|
|
Date(seconds: TimeInterval(st_ctim.tv_sec), nanoSeconds: TimeInterval(st_ctim.tv_nsec))
|
|
#endif
|
|
}
|
|
|
|
fileprivate var fileAttributes: [FileAttributeKey : Any] {
|
|
// On 32 bit Android, st_mode is UInt32.
|
|
let fileType = mode_t(st_mode).fileType
|
|
var result: [FileAttributeKey : Any] = [
|
|
.size : _writeFileAttributePrimitive(st_size, as: UInt.self),
|
|
.modificationDate : modificationDate,
|
|
.creationDate : creationDate,
|
|
.posixPermissions : _writeFileAttributePrimitive(st_mode & 0o7777, as: UInt.self),
|
|
.referenceCount : _writeFileAttributePrimitive(st_nlink, as: UInt.self),
|
|
.systemNumber : _writeFileAttributePrimitive(st_dev, as: UInt.self),
|
|
.systemFileNumber : _writeFileAttributePrimitive(st_ino, as: UInt64.self),
|
|
.type : fileType,
|
|
.ownerAccountID : _writeFileAttributePrimitive(st_uid, as: UInt.self),
|
|
.groupOwnerAccountID : _writeFileAttributePrimitive(st_gid, as: UInt.self)
|
|
]
|
|
#if !os(WASI)
|
|
if let userName = Platform.name(forUID: st_uid) {
|
|
result[.ownerAccountName] = userName
|
|
}
|
|
if let groupName = Platform.name(forGID: st_gid) {
|
|
result[.groupOwnerAccountName] = groupName
|
|
}
|
|
#endif
|
|
switch fileType as FileAttributeType {
|
|
case .typeBlockSpecial, .typeCharacterSpecial:
|
|
result[.deviceIdentifier] = _writeFileAttributePrimitive(st_rdev, as: UInt.self)
|
|
default:
|
|
// Do nothing
|
|
break
|
|
}
|
|
#if canImport(Darwin)
|
|
let immutable = (st_flags & UInt32(UF_IMMUTABLE)) != 0 || (st_flags & UInt32(SF_IMMUTABLE)) != 0
|
|
result[.immutable] = _writeFileAttributePrimitive(immutable)
|
|
let appendOnly = (st_flags & UInt32(UF_APPEND)) != 0 || (st_flags & UInt32(SF_APPEND)) != 0
|
|
result[.appendOnly] = _writeFileAttributePrimitive(appendOnly)
|
|
#endif
|
|
return result
|
|
}
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
extension FileProtectionType {
|
|
var intValue: Int32? {
|
|
switch self {
|
|
case .complete: PROTECTION_CLASS_A
|
|
case .init(rawValue: "NSFileProtectionWriteOnly"), .completeUnlessOpen: PROTECTION_CLASS_B
|
|
case .init(rawValue: "NSFileProtectionCompleteUntilUserAuthentication"), .completeUntilFirstUserAuthentication: PROTECTION_CLASS_C
|
|
case .none: PROTECTION_CLASS_D
|
|
#if !os(macOS)
|
|
case .completeWhenUserInactive: PROTECTION_CLASS_CX
|
|
#endif
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
init?(intValue value: Int32) {
|
|
switch value {
|
|
case PROTECTION_CLASS_A: self = .complete
|
|
case PROTECTION_CLASS_B: self = .completeUnlessOpen
|
|
case PROTECTION_CLASS_C: self = .completeUntilFirstUserAuthentication
|
|
case PROTECTION_CLASS_D: self = .none
|
|
#if !os(macOS)
|
|
case PROTECTION_CLASS_CX: self = .completeWhenUserInactive
|
|
#endif
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
extension FileAttributeKey {
|
|
fileprivate static var _extendedAttributes: Self { Self("NSFileExtendedAttributes") }
|
|
}
|
|
|
|
extension _FileManagerImpl {
|
|
func createFile(
|
|
atPath path: String,
|
|
contents data: Data?,
|
|
attributes attr: [FileAttributeKey : Any]? = nil
|
|
) -> Bool {
|
|
#if (os(iOS) || os(watchOS) || os(tvOS)) && FOUNDATION_FRAMEWORK
|
|
// Creating a file with a specific file protection class must have that class specified at open() time. Special-case NSFileProtectionKey here so that we can pass it as an NSDataWritingOption instead. 21998573.
|
|
var opts = Data.WritingOptions.atomic
|
|
var attr = attr
|
|
if let protection = attr?[.protectionKey] as? String {
|
|
let option: Data.WritingOptions? = switch FileProtectionType(rawValue: protection) {
|
|
case .none: .noFileProtection
|
|
case .complete: .completeFileProtection
|
|
case .completeUnlessOpen: .completeFileProtectionUnlessOpen
|
|
case .completeUntilFirstUserAuthentication: .completeFileProtectionUntilFirstUserAuthentication
|
|
case .completeWhenUserInactive: .completeFileProtectionWhenUserInactive
|
|
default: nil
|
|
}
|
|
if let option {
|
|
opts.insert(option)
|
|
}
|
|
attr?[.protectionKey] = nil
|
|
}
|
|
#elseif os(WASI)
|
|
// `.atomic` is unavailable on WASI
|
|
let opts: Data.WritingOptions = []
|
|
#else
|
|
let opts = Data.WritingOptions.atomic
|
|
#endif
|
|
|
|
do {
|
|
try (data ?? .init()).write(to: URL(fileURLWithPath: path), options: opts)
|
|
} catch {
|
|
return false
|
|
}
|
|
if let attr {
|
|
try? fileManager.setAttributes(attr, ofItemAtPath: path)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func removeItem(at url: URL) throws {
|
|
guard url.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, url)
|
|
}
|
|
|
|
let path = url.path
|
|
guard !path.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, url)
|
|
}
|
|
|
|
try removeItem(atPath: path)
|
|
}
|
|
|
|
func removeItem(atPath path: String) throws {
|
|
try _FileOperations.removeFile(path, with: fileManager)
|
|
}
|
|
|
|
func copyItem(
|
|
at srcURL: URL,
|
|
to dstURL: URL,
|
|
options: NSFileManagerCopyOptions
|
|
) throws {
|
|
guard srcURL.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, srcURL)
|
|
}
|
|
guard dstURL.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, dstURL)
|
|
}
|
|
|
|
let srcPath = srcURL.path
|
|
guard !srcPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, srcURL)
|
|
}
|
|
let dstPath = dstURL.path
|
|
guard !dstPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, dstURL)
|
|
}
|
|
|
|
try copyItem(atPath: srcPath, toPath: dstPath, options: options)
|
|
}
|
|
|
|
func copyItem(
|
|
atPath srcPath: String,
|
|
toPath dstPath: String,
|
|
options: NSFileManagerCopyOptions
|
|
) throws {
|
|
try _FileOperations.copyFile(srcPath, to: dstPath, with: fileManager, options: options)
|
|
}
|
|
|
|
func moveItem(
|
|
at srcURL: URL,
|
|
to dstURL: URL,
|
|
options: NSFileManagerMoveOptions
|
|
) throws {
|
|
guard srcURL.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, srcURL)
|
|
}
|
|
guard dstURL.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, dstURL)
|
|
}
|
|
|
|
let srcPath = srcURL.path
|
|
guard !srcPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, srcURL)
|
|
}
|
|
let dstPath = dstURL.path
|
|
guard !dstPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, dstURL)
|
|
}
|
|
|
|
try moveItem(atPath: srcPath, toPath: dstPath, options: options)
|
|
}
|
|
|
|
func moveItem(
|
|
atPath srcPath: String,
|
|
toPath dstPath: String,
|
|
options: NSFileManagerMoveOptions
|
|
) throws {
|
|
try _FileOperations.moveFile(
|
|
URL(fileURLWithPath: srcPath),
|
|
to: URL(fileURLWithPath: dstPath),
|
|
with: fileManager,
|
|
options: options
|
|
)
|
|
}
|
|
|
|
private func _fileExists(_ path: String) -> (exists: Bool, isDirectory: Bool) {
|
|
#if os(Windows)
|
|
guard !path.isEmpty else { return (false, false) }
|
|
return (try? path.withNTPathRepresentation { pwszPath in
|
|
let handle = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nil)
|
|
if handle == INVALID_HANDLE_VALUE {
|
|
return (false, false)
|
|
}
|
|
defer { CloseHandle(handle) }
|
|
|
|
var info: BY_HANDLE_FILE_INFORMATION = BY_HANDLE_FILE_INFORMATION()
|
|
guard GetFileInformationByHandle(handle, &info) else {
|
|
return (false, false)
|
|
}
|
|
|
|
return (true, info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY)
|
|
}) ?? (false, false)
|
|
#else
|
|
path.withFileSystemRepresentation { rep -> (Bool, Bool) in
|
|
guard let rep else {
|
|
return (false, false)
|
|
}
|
|
|
|
var fileInfo = stat()
|
|
guard stat(rep, &fileInfo) == 0 else {
|
|
return (false, false)
|
|
}
|
|
let isDir = (mode_t(fileInfo.st_mode) & S_IFMT) == S_IFDIR
|
|
return (true, isDir)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func fileExists(atPath path: String) -> Bool {
|
|
_fileExists(path).exists
|
|
}
|
|
|
|
func fileExists(
|
|
atPath path: String,
|
|
isDirectory: inout Bool
|
|
) -> Bool {
|
|
let result = _fileExists(path)
|
|
guard result.exists else { return false }
|
|
isDirectory = result.isDirectory
|
|
return true
|
|
}
|
|
|
|
#if !os(Windows)
|
|
private func _fileAccessibleForMode(_ path: String, _ mode: Int32) -> Bool {
|
|
path.withFileSystemRepresentation { ptr in
|
|
guard let ptr else { return false }
|
|
return access(ptr, mode) == 0
|
|
}
|
|
}
|
|
#endif
|
|
|
|
func isReadableFile(atPath path: String) -> Bool {
|
|
#if os(Windows)
|
|
return (try? path.withNTPathRepresentation {
|
|
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
|
|
return GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes)
|
|
}) ?? false
|
|
#else
|
|
_fileAccessibleForMode(path, R_OK)
|
|
#endif
|
|
}
|
|
|
|
func isWritableFile(atPath path: String) -> Bool {
|
|
#if os(Windows)
|
|
return (try? path.withNTPathRepresentation {
|
|
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
|
|
guard GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) else {
|
|
return false
|
|
}
|
|
return faAttributes.dwFileAttributes & FILE_ATTRIBUTE_READONLY != FILE_ATTRIBUTE_READONLY
|
|
}) ?? false
|
|
#else
|
|
_fileAccessibleForMode(path, W_OK)
|
|
#endif
|
|
}
|
|
|
|
func isExecutableFile(atPath path: String) -> Bool {
|
|
#if os(Windows)
|
|
return (try? path.withNTPathRepresentation {
|
|
// Use SHGetFileInfo instead of GetBinaryType because the latter returns the wrong answer for x86 binaries running under emulation on ARM systems.
|
|
return (SHGetFileInfoW($0, 0, nil, 0, SHGFI_EXETYPE) & 0xFFFF) != 0
|
|
}) ?? false
|
|
#else
|
|
_fileAccessibleForMode(path, X_OK)
|
|
#endif
|
|
}
|
|
|
|
func isDeletableFile(atPath path: String) -> Bool {
|
|
var parent = path.deletingLastPathComponent()
|
|
if parent.isEmpty {
|
|
parent = fileManager.currentDirectoryPath
|
|
}
|
|
|
|
#if os(Windows) || os(WASI)
|
|
return fileManager.isWritableFile(atPath: parent) && fileManager.isWritableFile(atPath: path)
|
|
#else
|
|
guard fileManager.isWritableFile(atPath: parent),
|
|
let dirInfo = fileManager._fileStat(parent) else {
|
|
return false
|
|
}
|
|
|
|
if ((mode_t(dirInfo.st_mode) & S_ISVTX) != 0) && fileManager.fileExists(atPath: path) {
|
|
// its sticky so verify that we own the file
|
|
// otherwise we answer YES on the principle that if
|
|
// we create files we can delete them
|
|
|
|
guard let fileInfo = fileManager._fileStat(path) else {
|
|
return false
|
|
}
|
|
return fileInfo.st_uid == getuid();
|
|
} else {
|
|
return true
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if !os(Windows) && !os(WASI) && !os(OpenBSD)
|
|
private func _extendedAttribute(_ key: UnsafePointer<CChar>, at path: UnsafePointer<CChar>, followSymlinks: Bool) throws -> Data? {
|
|
#if canImport(Darwin)
|
|
var size = getxattr(path, key, nil, 0, 0, followSymlinks ? 0 : XATTR_NOFOLLOW)
|
|
#elseif os(FreeBSD)
|
|
var size = (followSymlinks ? extattr_get_file : extattr_get_link)(path, EXTATTR_NAMESPACE_USER, key, nil, 0)
|
|
#else
|
|
var size = followSymlinks ? getxattr(path, key, nil, 0) : lgetxattr(path, key, nil, 0)
|
|
#endif
|
|
guard size != -1 else {
|
|
throw CocoaError.errorWithFilePath(String(cString: path), errno: errno, reading: true)
|
|
}
|
|
// Historically we've omitted extended attribute keys with no associated data value
|
|
guard size > 0 else { return nil }
|
|
// Deallocated below in the Data deallocator
|
|
let buffer = malloc(size)!
|
|
#if canImport(Darwin)
|
|
size = getxattr(path, key, buffer, size, 0, followSymlinks ? 0 : XATTR_NOFOLLOW)
|
|
#elseif os(FreeBSD)
|
|
size = (followSymlinks ? extattr_get_file : extattr_get_link)(path, EXTATTR_NAMESPACE_USER, key, buffer, size)
|
|
#else
|
|
size = followSymlinks ? getxattr(path, key, buffer, size) : lgetxattr(path, key, buffer, size)
|
|
#endif
|
|
guard size != -1 else {
|
|
free(buffer)
|
|
throw CocoaError.errorWithFilePath(String(cString: path), errno: errno, reading: true)
|
|
}
|
|
// Check size again in case something has changed between the two getxattr calls
|
|
guard size > 0 else {
|
|
free(buffer)
|
|
return nil
|
|
}
|
|
return Data(bytesNoCopy: buffer, count: size, deallocator: .free)
|
|
}
|
|
|
|
private func _extendedAttributes(at path: UnsafePointer<CChar>, followSymlinks: Bool) throws -> [String : Data]? {
|
|
#if canImport(Darwin)
|
|
var size = listxattr(path, nil, 0, 0)
|
|
#elseif os(FreeBSD)
|
|
var size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0)
|
|
#else
|
|
var size = listxattr(path, nil, 0)
|
|
#endif
|
|
guard size > 0 else { return nil }
|
|
let keyList = UnsafeMutableBufferPointer<CChar>.allocate(capacity: size)
|
|
defer { keyList.deallocate() }
|
|
#if canImport(Darwin)
|
|
size = listxattr(path, keyList.baseAddress!, size, 0)
|
|
#elseif os(FreeBSD)
|
|
size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0)
|
|
#else
|
|
size = listxattr(path, keyList.baseAddress!, size)
|
|
#endif
|
|
guard size > 0 else { return nil }
|
|
|
|
var extendedAttrs: [String : Data] = [:]
|
|
var current = keyList.baseAddress!
|
|
let end = keyList.baseAddress!.advanced(by: keyList.count)
|
|
while current < end {
|
|
let currentKey = String(cString: current)
|
|
defer { current = current.advanced(by: currentKey.utf8.count) + 1 /* pass null byte */ }
|
|
|
|
#if canImport(Darwin)
|
|
if currentKey == XATTR_RESOURCEFORK_NAME || currentKey == XATTR_FINDERINFO_NAME || currentKey == "system.Security" {
|
|
continue
|
|
}
|
|
#endif
|
|
|
|
if let value = try _extendedAttribute(current, at: path, followSymlinks: false) {
|
|
extendedAttrs[currentKey] = value
|
|
}
|
|
}
|
|
return extendedAttrs
|
|
}
|
|
#endif
|
|
|
|
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey : Any] {
|
|
#if os(Windows)
|
|
return try path.withNTPathRepresentation { pwszPath in
|
|
let hFile = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nil)
|
|
if hFile == INVALID_HANDLE_VALUE {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
defer { CloseHandle(hFile) }
|
|
|
|
var info: BY_HANDLE_FILE_INFORMATION = BY_HANDLE_FILE_INFORMATION()
|
|
guard GetFileInformationByHandle(hFile, &info) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
let dwFileType = GetFileType(hFile)
|
|
var fatType: FileAttributeType = switch (dwFileType) {
|
|
case FILE_TYPE_CHAR: FileAttributeType.typeCharacterSpecial
|
|
case FILE_TYPE_DISK:
|
|
info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY
|
|
? FileAttributeType.typeDirectory
|
|
: FileAttributeType.typeRegular
|
|
case FILE_TYPE_PIPE: FileAttributeType.typeSocket
|
|
case FILE_TYPE_UNKNOWN: FileAttributeType.typeUnknown
|
|
default: FileAttributeType.typeUnknown
|
|
}
|
|
|
|
if info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT {
|
|
// This could by a symlink, check if that's the case and update fatType if necessary
|
|
var tagInfo = FILE_ATTRIBUTE_TAG_INFO()
|
|
if GetFileInformationByHandleEx(hFile, FileAttributeTagInfo, &tagInfo, DWORD(MemoryLayout<FILE_ATTRIBUTE_TAG_INFO>.size)) {
|
|
if tagInfo.ReparseTag == IO_REPARSE_TAG_SYMLINK {
|
|
fatType = .typeSymbolicLink
|
|
}
|
|
}
|
|
}
|
|
|
|
let systemNumber = UInt64(info.dwVolumeSerialNumber)
|
|
let systemFileNumber = UInt64(info.nFileIndexHigh << 32) | UInt64(info.nFileIndexLow)
|
|
let referenceCount = UInt64(info.nNumberOfLinks)
|
|
|
|
let isReadOnly = info.dwFileAttributes & FILE_ATTRIBUTE_READONLY != 0
|
|
// Directories are always considered executable, but we check for other types
|
|
let isExecutable = fatType == .typeDirectory || SaferiIsExecutableFileType(pwszPath, 0)
|
|
var posixPermissions = UInt16(_S_IREAD)
|
|
if !isReadOnly {
|
|
posixPermissions |= UInt16(_S_IWRITE)
|
|
}
|
|
if isExecutable {
|
|
posixPermissions |= UInt16(_S_IEXEC)
|
|
}
|
|
|
|
let size: UInt64 = (UInt64(info.nFileSizeHigh) << 32) | UInt64(info.nFileSizeLow)
|
|
let creation: Date = Date(timeIntervalSince1970: info.ftCreationTime.timeIntervalSince1970)
|
|
let modification: Date = Date(timeIntervalSince1970: info.ftLastWriteTime.timeIntervalSince1970)
|
|
return [
|
|
.size: _writeFileAttributePrimitive(size, as: UInt.self),
|
|
.modificationDate: modification,
|
|
.creationDate: creation,
|
|
.type: fatType,
|
|
.systemNumber: _writeFileAttributePrimitive(systemNumber, as: UInt.self),
|
|
.systemFileNumber: _writeFileAttributePrimitive(systemFileNumber, as: UInt.self),
|
|
.posixPermissions: _writeFileAttributePrimitive(posixPermissions, as: UInt.self),
|
|
.referenceCount: _writeFileAttributePrimitive(referenceCount, as: UInt.self),
|
|
|
|
// Uid is always 0 on Windows systems
|
|
.ownerAccountID: _writeFileAttributePrimitive(0, as: UInt.self),
|
|
|
|
// Group id is always 0 on Windows
|
|
.groupOwnerAccountID: _writeFileAttributePrimitive(0, as: UInt.self)
|
|
|
|
// TODO: Support .deviceIdentifier
|
|
]
|
|
}
|
|
#else
|
|
try fileManager.withFileSystemRepresentation(for: path) { fsRep in
|
|
guard let fsRep else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnknown, path)
|
|
}
|
|
|
|
var statAtPath = stat()
|
|
guard lstat(fsRep, &statAtPath) == 0 else {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: true)
|
|
}
|
|
|
|
var attributes = statAtPath.fileAttributes
|
|
try? Self._catInfo(for: URL(filePath: path, directoryHint: .isDirectory), statInfo: statAtPath, into: &attributes)
|
|
#if !os(WASI) && !os(OpenBSD)
|
|
if let extendedAttrs = try? _extendedAttributes(at: fsRep, followSymlinks: false) {
|
|
attributes[._extendedAttributes] = extendedAttrs
|
|
}
|
|
#endif
|
|
|
|
#if !targetEnvironment(simulator) && FOUNDATION_FRAMEWORK
|
|
if statAtPath.isRegular || statAtPath.isDirectory {
|
|
if let protectionClass = Self._fileProtectionValueForPath(fsRep), let pType = FileProtectionType(intValue: protectionClass) {
|
|
// Cast to NSString here so that clients can cast this value to both String and FileProtectionType
|
|
attributes[.protectionKey] = pType as NSString
|
|
} else {
|
|
attributes[.protectionKey] = nil
|
|
}
|
|
}
|
|
#endif
|
|
return attributes
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func attributesOfFileSystem(forPath path: String) throws -> [FileAttributeKey : Any] {
|
|
#if os(Windows)
|
|
return try path.withNTPathRepresentation { pwszPath in
|
|
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
|
|
guard GetFileAttributesExW(pwszPath, GetFileExInfoStandard, &faAttributes) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil)
|
|
guard dwLength > 0 else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { szVolumeName in
|
|
guard GetVolumePathNameW(pwszPath, szVolumeName.baseAddress, dwLength) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
var liTotal: ULARGE_INTEGER = .init()
|
|
var liFree: ULARGE_INTEGER = .init()
|
|
guard GetDiskFreeSpaceExW(szVolumeName.baseAddress, nil, &liTotal, &liFree) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
let hr: HRESULT = PathCchStripToRoot(szVolumeName.baseAddress, szVolumeName.count)
|
|
guard hr == S_OK || hr == S_FALSE else {
|
|
throw CocoaError.errorWithFilePath(path, win32: DWORD(hr & 0xffff), reading: true)
|
|
}
|
|
|
|
var dwVolumeSerialNumber: DWORD = 0
|
|
guard GetVolumeInformationW(szVolumeName.baseAddress, nil, 0, &dwVolumeSerialNumber, nil, nil, nil, 0) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
return [
|
|
.systemSize: _writeFileAttributePrimitive(liTotal.QuadPart, as: UInt64.self),
|
|
.systemFreeSize: _writeFileAttributePrimitive(liFree.QuadPart, as: UInt64.self),
|
|
.systemNumber: _writeFileAttributePrimitive(dwVolumeSerialNumber, as: UInt.self),
|
|
|
|
// TODO(compnerd) support these attributes, remapping the Windows semantics...
|
|
// .systemNodes: ...,
|
|
// .systemFreeNodes: ...,
|
|
]
|
|
}
|
|
}
|
|
#elseif os(WASI)
|
|
// WASI does not support file system attributes
|
|
return [:]
|
|
#else
|
|
try fileManager.withFileSystemRepresentation(for: path) { rep in
|
|
guard let rep else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnknown, path)
|
|
}
|
|
|
|
#if canImport(Darwin)
|
|
var result = statfs()
|
|
let statfsReturnValue = statfs(rep, &result)
|
|
#else
|
|
var result = statvfs()
|
|
let statfsReturnValue = statvfs(rep, &result)
|
|
#endif
|
|
guard statfsReturnValue == 0 else {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: true)
|
|
}
|
|
|
|
#if canImport(Darwin)
|
|
let fsNumber = result.f_fsid.val.0
|
|
let blockSize = UInt64(result.f_bsize)
|
|
#elseif os(OpenBSD)
|
|
let fsNumber = result.f_fsid
|
|
let blockSize = UInt64(result.f_bsize)
|
|
#else
|
|
let fsNumber = result.f_fsid
|
|
let blockSize = UInt(result.f_frsize)
|
|
#endif
|
|
var totalSizeBytes = result.f_blocks * blockSize
|
|
var availSizeBytes = result.f_bavail * blockSize
|
|
var totalFiles = result.f_files
|
|
var availFiles = result.f_ffree
|
|
|
|
|
|
#if canImport(Darwin)
|
|
func QCMD(_ cmd: Int32, _ type: Int32) -> Int32 {
|
|
(cmd << SUBCMDSHIFT) | (type & SUBCMDMASK)
|
|
}
|
|
|
|
func _quotactl<T>(_ path: UnsafePointer<CChar>, _ cmd: Int32, _ type: Int32, _ uid: uid_t, init: T) -> T? {
|
|
var res = `init`
|
|
let success = withUnsafeMutableBytes(of: &res) { buffer in
|
|
// quotactl's parameter is annotated as `int` (signed 32bit) instead of `uid_t` (unsigned 32bit)
|
|
// UIDs greater than Int32.max are valid so we perform a bit-pattern cast here to prevent crashing on such values
|
|
quotactl(path, QCMD(cmd, type), Int32(bitPattern: uid), buffer.baseAddress!) == 0
|
|
}
|
|
return success ? res : nil
|
|
}
|
|
|
|
withUnsafeBytes(of: &result.f_mntonname) { mntonnameBuffer in
|
|
let mntonname = mntonnameBuffer.baseAddress!.assumingMemoryBound(to: CChar.self)
|
|
// If a quota is enabled, get quota info
|
|
let userID = geteuid()
|
|
if let isQuotaOn = _quotactl(mntonname, Q_QUOTASTAT, USRQUOTA, userID, init: 0),
|
|
isQuotaOn != 0,
|
|
let quotaInfo = _quotactl(mntonname, Q_GETQUOTA, USRQUOTA, userID, init: dqblk()) {
|
|
// For each value (total/available bytes, total/available files) report the smaller of the quota hard limit and the statfs value.
|
|
if quotaInfo.dqb_bhardlimit > 0 {
|
|
totalSizeBytes = min(quotaInfo.dqb_bhardlimit, totalSizeBytes)
|
|
availSizeBytes = min(quotaInfo.dqb_bhardlimit - quotaInfo.dqb_curbytes, availSizeBytes)
|
|
}
|
|
if (quotaInfo.dqb_ihardlimit > 0) {
|
|
totalFiles = min(UInt64(quotaInfo.dqb_ihardlimit), totalFiles)
|
|
availFiles = min(UInt64(quotaInfo.dqb_ihardlimit - quotaInfo.dqb_curinodes), availFiles)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return [
|
|
.systemSize : _writeFileAttributePrimitive(totalSizeBytes, as: UInt64.self),
|
|
.systemFreeSize : _writeFileAttributePrimitive(availSizeBytes, as: UInt64.self),
|
|
.systemNodes : _writeFileAttributePrimitive(totalFiles, as: UInt64.self),
|
|
.systemFreeNodes : _writeFileAttributePrimitive(availFiles, as: UInt64.self),
|
|
.systemNumber : _writeFileAttributePrimitive(fsNumber, as: UInt.self)
|
|
]
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func setAttributes(
|
|
_ attributes: [FileAttributeKey : Any],
|
|
ofItemAtPath path: String
|
|
) throws {
|
|
let mode = _readFileAttributePrimitive(attributes[.posixPermissions], as: UInt.self)
|
|
let immutable = _readFileAttributePrimitive(attributes[.immutable], as: Bool.self)
|
|
let appendOnly = _readFileAttributePrimitive(attributes[.appendOnly], as: Bool.self)
|
|
|
|
#if os(Windows)
|
|
try path.withNTPathRepresentation {
|
|
if immutable != nil || appendOnly != nil {
|
|
// Setting these flags is not supported on this platform
|
|
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
|
|
}
|
|
|
|
var attributesToSet: DWORD?
|
|
if let mode {
|
|
let existingAttributes = GetFileAttributesW($0)
|
|
guard existingAttributes != INVALID_FILE_ATTRIBUTES else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
let isReadOnly = (existingAttributes & FILE_ATTRIBUTE_READONLY) != 0
|
|
let requestedReadOnly = (mode & UInt(_S_IWRITE)) == 0
|
|
if isReadOnly && !requestedReadOnly {
|
|
guard SetFileAttributesW($0, existingAttributes & ~FILE_ATTRIBUTE_READONLY) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
|
|
}
|
|
} else if !isReadOnly && requestedReadOnly {
|
|
// Make the file read-only later after setting other attributes
|
|
attributesToSet = existingAttributes | FILE_ATTRIBUTE_READONLY
|
|
}
|
|
}
|
|
|
|
if let modification = attributes[.modificationDate] as? Date {
|
|
let seconds = modification.timeIntervalSince1601
|
|
|
|
var uiTime: ULARGE_INTEGER = .init()
|
|
guard let converted = UInt64(exactly: seconds * 10000000.0) else {
|
|
return
|
|
}
|
|
uiTime.QuadPart = converted
|
|
|
|
var ftTime: FILETIME = .init()
|
|
ftTime.dwLowDateTime = uiTime.LowPart
|
|
ftTime.dwHighDateTime = uiTime.HighPart
|
|
|
|
let hFile: HANDLE = CreateFileW($0, GENERIC_WRITE, FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, nil)
|
|
if hFile == INVALID_HANDLE_VALUE {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
defer { CloseHandle(hFile) }
|
|
|
|
guard SetFileTime(hFile, nil, nil, &ftTime) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
|
|
}
|
|
}
|
|
|
|
// Finally, make the file read-only if requested
|
|
if let attributesToSet {
|
|
guard SetFileAttributesW($0, attributesToSet) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
try fileManager.withFileSystemRepresentation(for: path) { fileSystemRepresentation in
|
|
guard let fileSystemRepresentation else {
|
|
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
|
|
}
|
|
|
|
// Use Result instead of throwing var to avoid compiler hang (rdar://119035093)
|
|
lazy var statAtPath: Result<stat, CocoaError> = {
|
|
var result = stat()
|
|
if lstat(fileSystemRepresentation, &result) != 0 {
|
|
return .failure(CocoaError.errorWithFilePath(path, errno: errno, reading: false))
|
|
}
|
|
return .success(result)
|
|
}()
|
|
|
|
// Set the flags first if we could be clearing the immutable bit. Set them last if we could be setting the immutable bit.
|
|
var setFlags: (() throws -> Void)?
|
|
if (immutable != nil || appendOnly != nil) {
|
|
#if canImport(Darwin)
|
|
setFlags = {
|
|
var flags = try statAtPath.get().st_flags
|
|
if let appendOnly {
|
|
if appendOnly {
|
|
flags |= UInt32(UF_APPEND)
|
|
} else {
|
|
flags &= ~UInt32(UF_APPEND)
|
|
}
|
|
}
|
|
if let immutable {
|
|
if immutable {
|
|
flags |= UInt32(UF_IMMUTABLE)
|
|
} else {
|
|
flags &= ~UInt32(UF_IMMUTABLE)
|
|
}
|
|
}
|
|
|
|
if chflags(fileSystemRepresentation, flags) != 0 {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
|
|
}
|
|
}
|
|
|
|
if !(immutable ?? false) {
|
|
try setFlags?()
|
|
setFlags = nil
|
|
}
|
|
#else
|
|
// Setting these flags is not supported on this platform
|
|
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
|
|
#endif
|
|
}
|
|
|
|
// Like the immutable flag, if write permissions are being set, do it first. If they are being unset, do it last.
|
|
var setMode: (() throws -> Void)?
|
|
if let mode {
|
|
#if os(WASI)
|
|
// WASI does not have the concept of permissions
|
|
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
|
|
#else
|
|
setMode = {
|
|
if chmod(fileSystemRepresentation, mode_t(mode)) != 0 {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
|
|
}
|
|
}
|
|
|
|
if mode_t(mode) & S_IWUSR != 0 {
|
|
try setMode?()
|
|
setMode = nil
|
|
}
|
|
#endif
|
|
}
|
|
|
|
let user = attributes[.ownerAccountName] as? String
|
|
let userID = _readFileAttributePrimitive(attributes[.ownerAccountID], as: UInt.self)
|
|
let group = attributes[.groupOwnerAccountName] as? String
|
|
let groupID = _readFileAttributePrimitive(attributes[.groupOwnerAccountID], as: UInt.self)
|
|
|
|
if user != nil || userID != nil || group != nil || groupID != nil {
|
|
#if os(WASI)
|
|
// WASI does not have the concept of users or groups
|
|
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
|
|
#else
|
|
// Bias toward userID & groupID - try to prevent round trips to getpwnam if possible.
|
|
var leaveUnchanged: UInt32 { UInt32(bitPattern: -1) }
|
|
let rawUserID = userID.flatMap(uid_t.init) ?? user.flatMap(Platform.uid(forName:)) ?? leaveUnchanged
|
|
let rawGroupID = groupID.flatMap(gid_t.init) ?? group.flatMap(Platform.gid(forName:)) ?? leaveUnchanged
|
|
if chown(fileSystemRepresentation, rawUserID, rawGroupID) != 0 {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
try Self._setCatInfoAttributes(attributes, path: path)
|
|
|
|
if let extendedAttrs = attributes[.init("NSFileExtendedAttributes")] as? [String : Data] {
|
|
#if os(WASI) || os(OpenBSD)
|
|
// WASI does not support extended attributes
|
|
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
|
|
#elseif canImport(Android)
|
|
// Android doesn't allow setting this for normal apps, so just skip it.
|
|
#else
|
|
try Self._setAttributes(extendedAttrs, at: fileSystemRepresentation, followSymLinks: false)
|
|
#endif
|
|
}
|
|
|
|
if let date = attributes[.modificationDate] as? Date {
|
|
let (isecs, fsecs) = modf(date.timeIntervalSince1970)
|
|
if let tv_sec = time_t(exactly: isecs),
|
|
let tv_usec = suseconds_t(exactly: round(fsecs * 1000000.0)) {
|
|
var timevals = (timeval(), timeval())
|
|
timevals.0.tv_sec = tv_sec
|
|
timevals.0.tv_usec = tv_usec
|
|
timevals.1 = timevals.0
|
|
try withUnsafePointer(to: timevals) {
|
|
try $0.withMemoryRebound(to: timeval.self, capacity: 2) {
|
|
if utimes(fileSystemRepresentation, $0) != 0 {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove write permissions if it has been requested. This must be done before setting the immutable bit.
|
|
try setMode?()
|
|
|
|
// Set flags now, if we postponed it until now.
|
|
try setFlags?()
|
|
|
|
#if !targetEnvironment(simulator) && FOUNDATION_FRAMEWORK
|
|
// Set per-file protection class on embedded.
|
|
let fileProtection = attributes[.protectionKey] as? FileProtectionType
|
|
if let fileProtection, let fileProtectionClass = fileProtection.intValue {
|
|
// Only set protection class on regular files and directories.
|
|
if try statAtPath.get().isRegular || statAtPath.get().isDirectory {
|
|
// Finally, set the class.
|
|
try Self._setFileProtectionValueForPath(path, fileSystemRepresentation, newValue: fileProtectionClass)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
}
|