mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-28 09:47:07 +08:00
548 lines
23 KiB
Swift
548 lines
23 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 containermanager
|
|
internal import _ForSwiftFoundation
|
|
internal import os
|
|
#endif
|
|
|
|
#if canImport(Darwin)
|
|
import Darwin
|
|
#elseif canImport(Android)
|
|
import Android
|
|
import unistd
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
#elseif canImport(Musl)
|
|
import Musl
|
|
#elseif os(Windows)
|
|
import CRT
|
|
import WinSDK
|
|
#elseif os(WASI)
|
|
import WASILibc
|
|
#endif
|
|
|
|
internal import _FoundationCShims
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
func _LogSpecialFolderRecreation(_ fileManager: FileManager, _ path: String) {
|
|
if UserDefaults.standard.bool(forKey: "NSLogSpecialFolderRecreation") && !fileManager.fileExists(atPath: path) {
|
|
Logger().info("*** Application: \(Bundle.main.bundleIdentifier ?? "(null)") just recreated special folder: \(path)")
|
|
}
|
|
}
|
|
#endif
|
|
|
|
extension _FileManagerImpl {
|
|
var homeDirectoryForCurrentUser: URL {
|
|
URL(filePath: String.homeDirectoryPath(), directoryHint: .isDirectory)
|
|
}
|
|
|
|
func homeDirectory(forUser userName: String?) -> URL? {
|
|
guard let userName else {
|
|
return homeDirectoryForCurrentUser
|
|
}
|
|
guard let path = String.homeDirectoryPath(forUser: userName) else {
|
|
return nil
|
|
}
|
|
return URL(filePath: path, directoryHint: .isDirectory)
|
|
}
|
|
|
|
var temporaryDirectory: URL {
|
|
URL(filePath: String.temporaryDirectoryPath, directoryHint: .isDirectory)
|
|
}
|
|
|
|
func url(
|
|
for directory: FileManager.SearchPathDirectory,
|
|
in domain: FileManager.SearchPathDomainMask,
|
|
appropriateFor url: URL?,
|
|
create shouldCreate: Bool
|
|
) throws -> URL {
|
|
#if FOUNDATION_FRAMEWORK
|
|
// TODO: Support correct trash/replacement locations in swift-foundation
|
|
#if os(macOS) || os(iOS)
|
|
if let url, directory == .trashDirectory {
|
|
return try fileManager._URLForTrashingItem(at: url, create: shouldCreate)
|
|
}
|
|
#endif
|
|
if let url, domain == .userDomainMask, directory == .itemReplacementDirectory {
|
|
// The only place we need to do this is for certain operations, namely the replacing item API.
|
|
return try fileManager._URLForReplacingItem(at: url)
|
|
}
|
|
var domain = domain
|
|
if domain == .systemDomainMask {
|
|
domain = ._partitionedSystemDomainMask
|
|
}
|
|
let lastElement = domain == ._partitionedSystemDomainMask
|
|
#else
|
|
let lastElement = false
|
|
#endif
|
|
let urls = Array(_SearchPathURLs(for: directory, in: domain, expandTilde: true))
|
|
guard let url = lastElement ? urls.last : urls.first else {
|
|
throw CocoaError(.fileReadUnknown)
|
|
}
|
|
|
|
if shouldCreate {
|
|
#if FOUNDATION_FRAMEWORK
|
|
_LogSpecialFolderRecreation(fileManager, url.path)
|
|
#endif
|
|
var isUserDomain = domain == .userDomainMask
|
|
#if os(macOS) && FOUNDATION_FRAMEWORK
|
|
isUserDomain = isUserDomain || domain == ._sharedUserDomainMask
|
|
#endif
|
|
var attrDictionary: [FileAttributeKey : Any] = [:]
|
|
if isUserDomain {
|
|
attrDictionary[.posixPermissions] = 0o700
|
|
} else {
|
|
#if FOUNDATION_FRAMEWORK
|
|
if domain == ._partitionedSystemDomainMask {
|
|
attrDictionary[.posixPermissions] = 0o755
|
|
attrDictionary[.ownerAccountID] = 0 // root
|
|
attrDictionary[.groupOwnerAccountID] = 80 // admin
|
|
}
|
|
#endif
|
|
}
|
|
try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: attrDictionary)
|
|
}
|
|
return url
|
|
}
|
|
|
|
func urls(
|
|
for directory: FileManager.SearchPathDirectory,
|
|
in domainMask: FileManager.SearchPathDomainMask
|
|
) -> [URL] {
|
|
Array(_SearchPathURLs(for: directory, in: domainMask, expandTilde: true))
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
func containerURL(forSecurityApplicationGroupIdentifier groupIdentifier: String) -> URL? {
|
|
groupIdentifier.withCString {
|
|
guard let path = container_create_or_lookup_app_group_path_by_app_group_identifier($0, nil) else {
|
|
return nil
|
|
}
|
|
|
|
defer { path.deallocate() }
|
|
return URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
func contentsOfDirectory(atPath path: String) throws -> [String] {
|
|
#if os(macOS)
|
|
// CFURLEnumerator/CarbonCore does not operate on /dev paths
|
|
if !path.standardizingPath.starts(with: "/dev") {
|
|
guard fileManager.fileExists(atPath: path) else {
|
|
throw CocoaError.errorWithFilePath(path, osStatus: -43 /*fnfErr*/, reading: true, variant: "Folder")
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
// Use CFURLEnumerator in Foundation framework, otherwise fallback to POSIX sequence below
|
|
var err: NSError?
|
|
guard let result = _NSDirectoryContentsFromCFURLEnumeratorError(URL(fileURLWithPath: path, isDirectory: true), nil, 0, true, &err) else {
|
|
throw err!
|
|
}
|
|
return result
|
|
#endif
|
|
}
|
|
#endif
|
|
var result: [String] = []
|
|
#if os(Windows)
|
|
let iterator = _Win32DirectoryContentsSequence(path: path, appendSlashForDirectory: false).makeIterator()
|
|
#else
|
|
let iterator = _POSIXDirectoryContentsSequence(path: path, appendSlashForDirectory: false).makeIterator()
|
|
#endif
|
|
if let error = iterator.error {
|
|
throw error
|
|
} else {
|
|
while let item = iterator.next() {
|
|
result.append(item.fileName)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func subpathsOfDirectory(atPath path: String) throws -> [String] {
|
|
#if os(Windows)
|
|
try path.withNTPathRepresentation {
|
|
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
|
|
guard GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
}
|
|
|
|
var results: [String] = []
|
|
for item in _Win32DirectoryContentsSequence(path: path, appendSlashForDirectory: true) {
|
|
results.append(item.fileName)
|
|
if item.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY &&
|
|
item.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != FILE_ATTRIBUTE_REPARSE_POINT {
|
|
|
|
var pwszSubPath: PWSTR? = nil
|
|
let hr = PathAllocCombine(path, item.fileName, PATHCCH_ALLOW_LONG_PATHS, &pwszSubPath)
|
|
guard hr == S_OK else {
|
|
throw CocoaError.errorWithFilePath(path, win32: WIN32_FROM_HRESULT(hr), reading: true)
|
|
}
|
|
defer { LocalFree(pwszSubPath) }
|
|
|
|
results.append(contentsOf: try subpathsOfDirectory(atPath: String(decodingCString: pwszSubPath!, as: UTF16.self)).map {
|
|
var pwszFullPath: PWSTR? = nil
|
|
_ = PathAllocCombine(item.fileName, $0, PATHCCH_ALLOW_LONG_PATHS, &pwszFullPath)
|
|
defer { LocalFree(pwszFullPath) }
|
|
return String(decodingCString: pwszFullPath!, as: UTF16.self).standardizingPath.replacing("\\", with: "/")
|
|
})
|
|
}
|
|
}
|
|
return results
|
|
#elseif os(WASI)
|
|
// wasi-libc does not support FTS for now
|
|
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
|
|
#else
|
|
return try path.withFileSystemRepresentation { fileSystemRep in
|
|
guard let fileSystemRep else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, path)
|
|
}
|
|
|
|
let subpaths = _FTSSequence(fileSystemRep, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT).subpaths
|
|
var realFirstPath: String?
|
|
|
|
var results: [String] = []
|
|
for item in subpaths {
|
|
var subpath: String
|
|
switch item {
|
|
case .error(let errNum, let p):
|
|
throw CocoaError.errorWithFilePath(p, errno: errNum, reading: true)
|
|
case .entry(let path):
|
|
subpath = path
|
|
}
|
|
|
|
guard let realFirstPath else {
|
|
realFirstPath = subpath
|
|
continue
|
|
}
|
|
|
|
let trueSubpath = subpath.trimmingPrefix(realFirstPath)
|
|
if trueSubpath.first == "/" {
|
|
results.append(String(trueSubpath.dropFirst()))
|
|
} else if !trueSubpath.isEmpty {
|
|
results.append(String(trueSubpath))
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func createDirectory(
|
|
at url: URL,
|
|
withIntermediateDirectories createIntermediates: Bool,
|
|
attributes: [FileAttributeKey : Any]? = nil
|
|
) throws {
|
|
guard url.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileWriteUnsupportedScheme, url)
|
|
}
|
|
|
|
let path = url.path
|
|
guard !path.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, url)
|
|
}
|
|
|
|
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes)
|
|
}
|
|
|
|
#if os(Windows)
|
|
/// If `path` is absolute, this is the same as `path.withNTPathRepresentation`.
|
|
/// If `path` is relative, this creates an absolute path of `path` relative to `currentDirectoryPath` and runs
|
|
/// `body` with that path.
|
|
private func withAbsoluteNTPathRepresentation<Result>(
|
|
of path: String,
|
|
_ body: (UnsafePointer<WCHAR>) throws -> Result
|
|
) throws -> Result {
|
|
try path.withNTPathRepresentation { pwszPath in
|
|
if !PathIsRelativeW(pwszPath) {
|
|
// We already have an absolute path. Nothing to do
|
|
return try body(pwszPath)
|
|
}
|
|
guard let currentDirectoryPath else {
|
|
preconditionFailure("We should always have a current directory on Windows")
|
|
}
|
|
|
|
// We have a relateive path. Make it absolute.
|
|
let absoluteUrl = URL(
|
|
filePath: path,
|
|
directoryHint: .isDirectory,
|
|
relativeTo: URL(filePath: currentDirectoryPath, directoryHint: .isDirectory)
|
|
)
|
|
return try absoluteUrl.path.withNTPathRepresentation { pwszPath in
|
|
return try body(pwszPath)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
func createDirectory(
|
|
atPath path: String,
|
|
withIntermediateDirectories createIntermediates: Bool,
|
|
attributes: [FileAttributeKey : Any]? = nil
|
|
) throws {
|
|
#if os(Windows)
|
|
var saAttributes: SECURITY_ATTRIBUTES =
|
|
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
|
|
lpSecurityDescriptor: nil,
|
|
bInheritHandle: false)
|
|
// `SHCreateDirectoryExW` creates intermediate directories while `CreateDirectoryW` does not.
|
|
if createIntermediates {
|
|
// `SHCreateDirectoryExW` requires an absolute path while `CreateDirectoryW` works based on the current working
|
|
// directory.
|
|
try withAbsoluteNTPathRepresentation(of: path) { pwszPath in
|
|
let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes)
|
|
guard let errorCode = DWORD(exactly: errorCode) else {
|
|
// `SHCreateDirectoryExW` returns `Int` but all error codes are defined in terms of `DWORD`, aka
|
|
// `UInt`. We received an unknown error code.
|
|
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
|
|
}
|
|
switch errorCode {
|
|
case ERROR_SUCCESS:
|
|
if let attributes {
|
|
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
|
|
}
|
|
case ERROR_ALREADY_EXISTS:
|
|
var isDirectory: Bool = false
|
|
if fileExists(atPath: path, isDirectory: &isDirectory), isDirectory {
|
|
// A directory already exists at this path, which is not an error if we have
|
|
// `createIntermediates == true`.
|
|
break
|
|
}
|
|
// A file (not a directory) exists at the given path or the file creation failed and the item
|
|
// at this path has been deleted before the call to `fileExists`. Throw the original error.
|
|
fallthrough
|
|
default:
|
|
throw CocoaError.errorWithFilePath(path, win32: errorCode, reading: false)
|
|
}
|
|
}
|
|
} else {
|
|
try path.withNTPathRepresentation { pwszPath in
|
|
guard CreateDirectoryW(pwszPath, &saAttributes) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
|
|
}
|
|
}
|
|
if let attributes {
|
|
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
|
|
}
|
|
}
|
|
#else
|
|
try fileManager.withFileSystemRepresentation(for: path) { pathPtr in
|
|
guard let pathPtr else {
|
|
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
|
|
}
|
|
|
|
guard createIntermediates else {
|
|
guard mkdir(pathPtr, 0o777) == 0 else {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
|
|
}
|
|
if let attributes {
|
|
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
|
|
}
|
|
return
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
var firstDirectoryPtr: UnsafePointer<CChar>?
|
|
defer { firstDirectoryPtr?.deallocate() }
|
|
let result = _mkpath_np(pathPtr, S_IRWXU | S_IRWXG | S_IRWXO, &firstDirectoryPtr)
|
|
|
|
guard result == 0 else {
|
|
guard result != EEXIST else { return }
|
|
var errNum = result
|
|
var errPath = path
|
|
if result == ENOTDIR {
|
|
// _mkpath_np reports ENOTDIR when any component in the path is a regular file. We need to do two things to ensure binary compatibility: 1) find that file -- we have to report it in the error, and 2) special-case the last component. For whatever reason, we've always reported EEXIST for this case. This requires some extra stat'ing.
|
|
var currentDirectory = path
|
|
var isLastComponent = true
|
|
// This shouldn't happen unless there are file system races going on, but stop iterating when we reach "/".
|
|
while currentDirectory.count > 1 {
|
|
if fileManager.fileExists(atPath: currentDirectory) {
|
|
errPath = currentDirectory
|
|
if isLastComponent {
|
|
errNum = EEXIST
|
|
}
|
|
break
|
|
}
|
|
currentDirectory = currentDirectory.deletingLastPathComponent()
|
|
isLastComponent = false
|
|
}
|
|
}
|
|
throw CocoaError.errorWithFilePath(errPath, errno: errNum, reading: false)
|
|
}
|
|
|
|
guard let attributes else {
|
|
return // Nothing left to do
|
|
}
|
|
|
|
// The directory was successfully created. To keep binary compatibility, we need to post-process the newly created directories and set attributes.
|
|
// We're relying on the knowledge that _mkpath_np does not change any of the parent path components of firstDirectory. Otherwise, I think we'd have to canonicalize paths or check for IDs, which would probably require more file system calls than is worthwhile.
|
|
var currentDirectory = firstDirectoryPtr.flatMap(String.init(cString:)) ?? path
|
|
|
|
// Start with the first newly created directory.
|
|
try? fileManager.setAttributes(attributes, ofItemAtPath: currentDirectory)// Not returning error to preserve binary compatibility.
|
|
|
|
// Now append each subsequent path component.
|
|
let fullComponents = path.pathComponents
|
|
let currentComponents = currentDirectory.pathComponents
|
|
for component in fullComponents[currentComponents.count...] {
|
|
currentDirectory = currentDirectory.appendingPathComponent(component)
|
|
try? fileManager.setAttributes(attributes, ofItemAtPath: currentDirectory) // Not returning error to preserve binary compatibility.
|
|
}
|
|
#else
|
|
func _create(path: String, leafFile: Bool = true) throws {
|
|
var isDir = false
|
|
guard !fileManager.fileExists(atPath: path, isDirectory: &isDir) else {
|
|
if !isDir && leafFile {
|
|
throw CocoaError.errorWithFilePath(path, errno: EEXIST, reading: false)
|
|
}
|
|
return
|
|
}
|
|
let parent = path.deletingLastPathComponent()
|
|
if !parent.isEmpty {
|
|
try _create(path: parent, leafFile: false)
|
|
}
|
|
try fileManager.withFileSystemRepresentation(for: path) { pathFsRep in
|
|
guard let pathFsRep else {
|
|
throw CocoaError.errorWithFilePath(.fileWriteInvalidFileName, path)
|
|
}
|
|
guard mkdir(pathFsRep, 0o777) == 0 else {
|
|
let posixErrno = errno
|
|
if posixErrno == EEXIST && fileManager.fileExists(atPath: path, isDirectory: &isDir) && isDir {
|
|
// Continue; if there is an existing file and it is a directory, that is still a success.
|
|
// There can be an existing file if another thread or process concurrently creates the
|
|
// same file.
|
|
return
|
|
} else {
|
|
throw CocoaError.errorWithFilePath(path, errno: posixErrno, reading: false)
|
|
}
|
|
}
|
|
if let attr = attributes {
|
|
try? fileManager.setAttributes(attr, ofItemAtPath: path)
|
|
}
|
|
}
|
|
}
|
|
try _create(path: path)
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
func getRelationship(
|
|
_ outRelationship: UnsafeMutablePointer<FileManager.URLRelationship>,
|
|
ofDirectoryAt directoryURL: URL,
|
|
toItemAt otherURL: URL
|
|
) throws {
|
|
// Get url's resource identifier, volume identifier, and make sure it is a directory
|
|
let dirValues = try directoryURL.resourceValues(forKeys: [.fileResourceIdentifierKey, .volumeIdentifierKey, .isDirectoryKey])
|
|
|
|
guard let isDirectory = dirValues.isDirectory, isDirectory else {
|
|
outRelationship.pointee = .other
|
|
return
|
|
}
|
|
|
|
// Get other's resource identifier and make sure it is not the same resource as otherURL
|
|
let otherValues = try otherURL.resourceValues(forKeys: [.fileIdentifierKey, .fileResourceIdentifierKey, .volumeIdentifierKey])
|
|
guard !otherValues.fileResourceIdentifier!.isEqual(dirValues.fileResourceIdentifier!) else {
|
|
outRelationship.pointee = .same
|
|
return
|
|
}
|
|
|
|
guard otherValues.volumeIdentifier!.isEqual(dirValues.volumeIdentifier!) else {
|
|
outRelationship.pointee = .other
|
|
return
|
|
}
|
|
|
|
// Start looking through the parent chain up to the volume root for a parent that is equal to 'url'. Stop when the current URL reaches the volume root
|
|
var currentURL = otherURL
|
|
while try !currentURL.resourceValues(forKeys: [.isVolumeKey]).isVolume! {
|
|
// Get url's parentURL
|
|
let parentURL = try currentURL.resourceValues(forKeys: [.parentDirectoryURLKey]).parentDirectory!
|
|
|
|
let parentResourceID = try parentURL.resourceValues(forKeys: [.fileResourceIdentifierKey]).fileResourceIdentifier!
|
|
|
|
if parentResourceID.isEqual(dirValues.fileResourceIdentifier!) {
|
|
outRelationship.pointee = .contains
|
|
return
|
|
}
|
|
|
|
currentURL = parentURL
|
|
}
|
|
|
|
outRelationship.pointee = .other
|
|
return
|
|
}
|
|
|
|
func getRelationship(
|
|
_ outRelationship: UnsafeMutablePointer<FileManager.URLRelationship>,
|
|
of directory: FileManager.SearchPathDirectory,
|
|
in domainMask: FileManager.SearchPathDomainMask,
|
|
toItemAt url: URL
|
|
) throws {
|
|
// Figure out the standard directory, then call the other API
|
|
let directoryURL = try fileManager.url(
|
|
for: directory,
|
|
in: domainMask,
|
|
appropriateFor: domainMask.isEmpty ? url : nil,
|
|
create: false)
|
|
return try fileManager.getRelationship(
|
|
outRelationship,
|
|
ofDirectoryAt: directoryURL,
|
|
toItemAt: url)
|
|
}
|
|
#endif
|
|
|
|
func changeCurrentDirectoryPath(_ path: String) -> Bool {
|
|
#if os(Windows)
|
|
return (try? path.withNTPathRepresentation {
|
|
SetCurrentDirectoryW($0)
|
|
}) ?? false
|
|
#else
|
|
fileManager.withFileSystemRepresentation(for: path) { rep in
|
|
guard let rep else { return false }
|
|
return chdir(rep) == 0
|
|
}
|
|
#endif
|
|
}
|
|
|
|
var currentDirectoryPath: String? {
|
|
#if os(Windows)
|
|
var dwLength: DWORD = GetCurrentDirectoryW(0, nil)
|
|
guard dwLength > 0 else { return nil }
|
|
|
|
for _ in 0 ... 8 {
|
|
if let szCurrentDirectory = withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength), {
|
|
let dwResult: DWORD = GetCurrentDirectoryW(dwLength, $0.baseAddress)
|
|
if dwResult == dwLength - 1 {
|
|
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
|
|
}
|
|
dwLength = dwResult
|
|
return nil
|
|
}) {
|
|
return szCurrentDirectory
|
|
}
|
|
}
|
|
return nil
|
|
#else
|
|
withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in
|
|
guard getcwd(buffer.baseAddress!, FileManager.MAX_PATH_SIZE) != nil else {
|
|
return nil
|
|
}
|
|
return fileManager.string(withFileSystemRepresentation: buffer.baseAddress!, length: strlen(buffer.baseAddress!))
|
|
}
|
|
#endif
|
|
}
|
|
}
|