Skip to content

Fix swift-system to work on Windows. #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import PackageDescription
let package = Package(
name: "swift-system",
products: [
.library(name: "SystemPackage", targets: ["SystemPackage"]),
.library(name: "SystemPackage", targets: ["SystemPackage"])
],
dependencies: [],
targets: [
Expand All @@ -40,6 +40,6 @@ let package = Package(
swiftSettings: [
.define("SYSTEM_PACKAGE_DARWIN", .when(platforms: [.macOS, .iOS, .watchOS, .tvOS, .visionOS])),
.define("SYSTEM_PACKAGE")
]),
])
]
)
20 changes: 20 additions & 0 deletions Sources/System/ErrnoWindows.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2024 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

#if os(Windows)

import WinSDK

extension Errno {
public init(windowsError: DWORD) {
self.init(rawValue: _mapWindowsErrorToErrno(windowsError))
}
}

#endif
50 changes: 45 additions & 5 deletions Sources/System/FileOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,6 @@ extension FileDescriptor {
if let permissions = permissions {
return system_open(path, oFlag, permissions.rawValue)
}
precondition(!options.contains(.create),
"Create must be given permissions")
return system_open(path, oFlag)
}
return descOrError.map { FileDescriptor(rawValue: $0) }
Expand Down Expand Up @@ -436,7 +434,7 @@ extension FileDescriptor {
}
#endif

#if !os(Windows) && !os(WASI)
#if !os(WASI)
@available(/*System 1.1.0: macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4*/iOS 8, *)
extension FileDescriptor {
/// Creates a unidirectional data channel, which can be used for interprocess communication.
Expand Down Expand Up @@ -465,7 +463,6 @@ extension FileDescriptor {
}
#endif

#if !os(Windows)
@available(/*System 1.2.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *)
extension FileDescriptor {
/// Truncates or extends the file referenced by this file descriptor.
Expand Down Expand Up @@ -511,4 +508,47 @@ extension FileDescriptor {
}
}
}
#endif

extension FilePermissions {
/// The file creation permission mask (aka "umask").
///
/// Permissions set in this mask will be cleared by functions that create
/// files or directories. Note that this mask is process-wide, and that
/// *getting* it is not thread safe.
@_alwaysEmitIntoClient
public static var creationMask: FilePermissions {
get {
let oldMask = _umask(0o22)
_ = _umask(oldMask)
return FilePermissions(rawValue: oldMask)
}
set {
_ = _umask(newValue.rawValue)
}
}

/// Change the file creation permission mask, run some code, then
/// restore it to its original value.
///
/// - Parameters:
/// - permissions: The new permission mask.
///
/// This is more efficient than reading `creationMask` and restoring it
/// afterwards, because of the way reading the creation mask works.
@_alwaysEmitIntoClient
public static func withCreationMask<R>(
_ permissions: FilePermissions,
body: () throws -> R
) rethrows -> R {
let oldMask = _umask(permissions.rawValue)
defer {
_ = _umask(oldMask)
}
return try body()
}

@usableFromInline
internal static func _umask(_ mode: CModeT) -> CModeT {
return system_umask(mode)
}
}
10 changes: 9 additions & 1 deletion Sources/System/FilePath/FilePathParsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ extension SystemString {
// `_prenormalizeWindowsRoots` and resume.
readIdx = _prenormalizeWindowsRoots()
writeIdx = readIdx

// Skip redundant separators
while readIdx < endIndex && isSeparator(self[readIdx]) {
self.formIndex(after: &readIdx)
}
} else {
assert(genericSeparator == platformSeparator)
}
Expand Down Expand Up @@ -330,10 +335,13 @@ extension FilePath {
// Whether we are providing Windows paths
@inline(__always)
internal var _windowsPaths: Bool {
if let forceWindowsPaths = forceWindowsPaths {
return forceWindowsPaths
}
#if os(Windows)
return true
#else
return forceWindowsPaths
return false
#endif
}

Expand Down
97 changes: 97 additions & 0 deletions Sources/System/FilePath/FilePathTemp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2024 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

// MARK: - API

/// Create a temporary path for the duration of the closure.
///
/// - Parameters:
/// - basename: The base name for the temporary path.
/// - body: The closure to execute.
///
/// Creates a temporary directory with a name based on the given `basename`,
/// executes `body`, passing in the path of the created directory, then
/// deletes the directory and all of its contents before returning.
public func withTemporaryFilePath<R>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that any API additions go through some kind of review, ideally on the forums. At the very least, a dedicated PR that highlights it's an API addition PR as opposed to just a bug-fix PR for Windows.

For this function, would a static method on FilePath work instead of a global? E.g. FilePath.withTemporaryPath { ... }.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would. My only reservation is that (to me) things like FilePath.withTemporaryPath read like a function that constructs and returns a FilePath from some kind of TemporaryPath thing. This isn't the same as e.g. Array's withUnsafeBytes, because that's called on an instance rather than on the type itself. Maybe that's just because I'm used to ObjC. It's obvious from the arguments that it takes a closure, of course.

I only needed this for the tests, so for now I think the thing to do is to make this non-public (I'll raise a PR to fix that). I'll check through to make sure I didn't leak any other new API and do the same there.

basename: FilePath.Component,
_ body: (FilePath) throws -> R
) throws -> R {
let temporaryDir = try createUniqueTemporaryDirectory(basename: basename)
defer {
try? _recursiveRemove(at: temporaryDir)
}

return try body(temporaryDir)
}

// MARK: - Internals

fileprivate let base64 = Array<UInt8>(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".utf8
)

/// Create a directory that is only accessible to the current user.
///
/// - Parameters:
/// - path: The path of the directory to create.
/// - Returns: `true` if a new directory was created.
///
/// This function will throw if there is an error, except if the error
/// is that the directory exists, in which case it returns `false`.
fileprivate func makeLockedDownDirectory(at path: FilePath) throws -> Bool {
return try path.withPlatformString {
if system_mkdir($0, 0o700) == 0 {
return true
}
let err = system_errno
if err == Errno.fileExists.rawValue {
return false
} else {
throw Errno(rawValue: err)
}
}
}

/// Generate a random string of base64 filename safe characters.
///
/// - Parameters:
/// - length: The number of characters in the returned string.
/// - Returns: A random string of length `length`.
fileprivate func createRandomString(length: Int) -> String {
return String(
decoding: (0..<length).map{
_ in base64[Int.random(in: 0..<64)]
},
as: UTF8.self
)
}

/// Given a base name, create a uniquely named temporary directory.
///
/// - Parameters:
/// - basename: The base name for the new directory.
/// - Returns: The path to the new directory.
///
/// Creates a directory in the system temporary directory whose name
/// starts with `basename`, followed by a `.` and then a random
/// string of characters.
fileprivate func createUniqueTemporaryDirectory(
basename: FilePath.Component
) throws -> FilePath {
var tempDir = try _getTemporaryDirectory()
tempDir.append(basename)

while true {
tempDir.extension = createRandomString(length: 16)

if try makeLockedDownDirectory(at: tempDir) {
return tempDir
}
}
}
153 changes: 153 additions & 0 deletions Sources/System/FilePath/FilePathTempPosix.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2024 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

#if !os(Windows)

/// Get the path to the system temporary directory.
internal func _getTemporaryDirectory() throws -> FilePath {
guard let tmp = system_getenv("TMPDIR") else {
return "/tmp"
}

return FilePath(SystemString(platformString: tmp))
}

/// Delete the entire contents of a directory, including its subdirectories.
///
/// - Parameters:
/// - path: The directory to be deleted.
///
/// Removes a directory completely, including all of its contents.
internal func _recursiveRemove(
at path: FilePath
) throws {
let dirfd = try FileDescriptor.open(path, .readOnly, options: .directory)
defer {
try? dirfd.close()
}

let dot: (CInterop.PlatformChar, CInterop.PlatformChar) = (46, 0)
try withUnsafeBytes(of: dot) {
try recursiveRemove(
in: dirfd.rawValue,
name: $0.assumingMemoryBound(to: CInterop.PlatformChar.self).baseAddress!
)
}

try path.withPlatformString {
if system_rmdir($0) != 0 {
throw Errno.current
}
}
}

/// Open a directory by reference to its parent and name.
///
/// - Parameters:
/// - dirfd: An open file descriptor for the parent directory.
/// - name: The name of the directory to open.
/// - Returns: A pointer to a `DIR` structure.
///
/// This is like `opendir()`, but instead of taking a path, it uses a
/// file descriptor pointing at the parent, thus avoiding path length
/// limits.
fileprivate func impl_opendirat(
_ dirfd: CInt,
_ name: UnsafePointer<CInterop.PlatformChar>
) -> system_DIRPtr? {
let fd = system_openat(dirfd, name,
FileDescriptor.AccessMode.readOnly.rawValue
| FileDescriptor.OpenOptions.directory.rawValue)
if fd < 0 {
return nil
}
return system_fdopendir(fd)
}

/// Invoke a closure for each file within a particular directory.
///
/// - Parameters:
/// - dirfd: The parent of the directory to be enumerated.
/// - subdir: The subdirectory to be enumerated.
/// - body: The closure that will be invoked.
///
/// We skip the `.` and `..` pseudo-entries.
fileprivate func forEachFile(
in dirfd: CInt,
subdir: UnsafePointer<CInterop.PlatformChar>,
_ body: (system_dirent) throws -> ()
) throws {
guard let dir = impl_opendirat(dirfd, subdir) else {
throw Errno.current
}
defer {
_ = system_closedir(dir)
}

while let dirent = system_readdir(dir) {
// Skip . and ..
if dirent.pointee.d_name.0 == 46
&& (dirent.pointee.d_name.1 == 0
|| (dirent.pointee.d_name.1 == 46
&& dirent.pointee.d_name.2 == 0)) {
continue
}

try body(dirent.pointee)
}
}

/// Delete the entire contents of a directory, including its subdirectories.
///
/// - Parameters:
/// - dirfd: The parent of the directory to be removed.
/// - name: The name of the directory to be removed.
///
/// Removes a directory completely, including all of its contents.
fileprivate func recursiveRemove(
in dirfd: CInt,
name: UnsafePointer<CInterop.PlatformChar>
) throws {
// First, deal with subdirectories
try forEachFile(in: dirfd, subdir: name) { dirent in
if dirent.d_type == SYSTEM_DT_DIR {
try withUnsafeBytes(of: dirent.d_name) {
try recursiveRemove(
in: dirfd,
name: $0.assumingMemoryBound(to: CInterop.PlatformChar.self)
.baseAddress!
)
}
}
}

// Now delete the contents of this directory
try forEachFile(in: dirfd, subdir: name) { dirent in
let flag: CInt

if dirent.d_type == SYSTEM_DT_DIR {
flag = SYSTEM_AT_REMOVE_DIR
} else {
flag = 0
}

let result = withUnsafeBytes(of: dirent.d_name) {
system_unlinkat(dirfd,
$0.assumingMemoryBound(to: CInterop.PlatformChar.self)
.baseAddress!,
flag)
}

if result != 0 {
throw Errno.current
}
}
}

#endif // !os(Windows)
Loading