Skip to content

Commit 027c936

Browse files
authored
Merge pull request #158 from al45tair/eng/PR-125087707
Fix swift-system to work on Windows.
2 parents 40b6b0e + f5ad5e5 commit 027c936

16 files changed

+1481
-99
lines changed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import PackageDescription
1515
let package = Package(
1616
name: "swift-system",
1717
products: [
18-
.library(name: "SystemPackage", targets: ["SystemPackage"]),
18+
.library(name: "SystemPackage", targets: ["SystemPackage"])
1919
],
2020
dependencies: [],
2121
targets: [
@@ -40,6 +40,6 @@ let package = Package(
4040
swiftSettings: [
4141
.define("SYSTEM_PACKAGE_DARWIN", .when(platforms: [.macOS, .iOS, .watchOS, .tvOS, .visionOS])),
4242
.define("SYSTEM_PACKAGE")
43-
]),
43+
])
4444
]
4545
)

Sources/System/ErrnoWindows.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
This source file is part of the Swift System open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift System project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
*/
9+
10+
#if os(Windows)
11+
12+
import WinSDK
13+
14+
extension Errno {
15+
public init(windowsError: DWORD) {
16+
self.init(rawValue: _mapWindowsErrorToErrno(windowsError))
17+
}
18+
}
19+
20+
#endif

Sources/System/FileOperations.swift

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,6 @@ extension FileDescriptor {
135135
if let permissions = permissions {
136136
return system_open(path, oFlag, permissions.rawValue)
137137
}
138-
precondition(!options.contains(.create),
139-
"Create must be given permissions")
140138
return system_open(path, oFlag)
141139
}
142140
return descOrError.map { FileDescriptor(rawValue: $0) }
@@ -436,7 +434,7 @@ extension FileDescriptor {
436434
}
437435
#endif
438436

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

468-
#if !os(Windows)
469466
@available(/*System 1.2.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *)
470467
extension FileDescriptor {
471468
/// Truncates or extends the file referenced by this file descriptor.
@@ -511,4 +508,47 @@ extension FileDescriptor {
511508
}
512509
}
513510
}
514-
#endif
511+
512+
extension FilePermissions {
513+
/// The file creation permission mask (aka "umask").
514+
///
515+
/// Permissions set in this mask will be cleared by functions that create
516+
/// files or directories. Note that this mask is process-wide, and that
517+
/// *getting* it is not thread safe.
518+
@_alwaysEmitIntoClient
519+
public static var creationMask: FilePermissions {
520+
get {
521+
let oldMask = _umask(0o22)
522+
_ = _umask(oldMask)
523+
return FilePermissions(rawValue: oldMask)
524+
}
525+
set {
526+
_ = _umask(newValue.rawValue)
527+
}
528+
}
529+
530+
/// Change the file creation permission mask, run some code, then
531+
/// restore it to its original value.
532+
///
533+
/// - Parameters:
534+
/// - permissions: The new permission mask.
535+
///
536+
/// This is more efficient than reading `creationMask` and restoring it
537+
/// afterwards, because of the way reading the creation mask works.
538+
@_alwaysEmitIntoClient
539+
public static func withCreationMask<R>(
540+
_ permissions: FilePermissions,
541+
body: () throws -> R
542+
) rethrows -> R {
543+
let oldMask = _umask(permissions.rawValue)
544+
defer {
545+
_ = _umask(oldMask)
546+
}
547+
return try body()
548+
}
549+
550+
@usableFromInline
551+
internal static func _umask(_ mode: CModeT) -> CModeT {
552+
return system_umask(mode)
553+
}
554+
}

Sources/System/FilePath/FilePathParsing.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ extension SystemString {
8989
// `_prenormalizeWindowsRoots` and resume.
9090
readIdx = _prenormalizeWindowsRoots()
9191
writeIdx = readIdx
92+
93+
// Skip redundant separators
94+
while readIdx < endIndex && isSeparator(self[readIdx]) {
95+
self.formIndex(after: &readIdx)
96+
}
9297
} else {
9398
assert(genericSeparator == platformSeparator)
9499
}
@@ -330,10 +335,13 @@ extension FilePath {
330335
// Whether we are providing Windows paths
331336
@inline(__always)
332337
internal var _windowsPaths: Bool {
338+
if let forceWindowsPaths = forceWindowsPaths {
339+
return forceWindowsPaths
340+
}
333341
#if os(Windows)
334342
return true
335343
#else
336-
return forceWindowsPaths
344+
return false
337345
#endif
338346
}
339347

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
This source file is part of the Swift System open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift System project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
*/
9+
10+
// MARK: - API
11+
12+
/// Create a temporary path for the duration of the closure.
13+
///
14+
/// - Parameters:
15+
/// - basename: The base name for the temporary path.
16+
/// - body: The closure to execute.
17+
///
18+
/// Creates a temporary directory with a name based on the given `basename`,
19+
/// executes `body`, passing in the path of the created directory, then
20+
/// deletes the directory and all of its contents before returning.
21+
public func withTemporaryFilePath<R>(
22+
basename: FilePath.Component,
23+
_ body: (FilePath) throws -> R
24+
) throws -> R {
25+
let temporaryDir = try createUniqueTemporaryDirectory(basename: basename)
26+
defer {
27+
try? _recursiveRemove(at: temporaryDir)
28+
}
29+
30+
return try body(temporaryDir)
31+
}
32+
33+
// MARK: - Internals
34+
35+
fileprivate let base64 = Array<UInt8>(
36+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".utf8
37+
)
38+
39+
/// Create a directory that is only accessible to the current user.
40+
///
41+
/// - Parameters:
42+
/// - path: The path of the directory to create.
43+
/// - Returns: `true` if a new directory was created.
44+
///
45+
/// This function will throw if there is an error, except if the error
46+
/// is that the directory exists, in which case it returns `false`.
47+
fileprivate func makeLockedDownDirectory(at path: FilePath) throws -> Bool {
48+
return try path.withPlatformString {
49+
if system_mkdir($0, 0o700) == 0 {
50+
return true
51+
}
52+
let err = system_errno
53+
if err == Errno.fileExists.rawValue {
54+
return false
55+
} else {
56+
throw Errno(rawValue: err)
57+
}
58+
}
59+
}
60+
61+
/// Generate a random string of base64 filename safe characters.
62+
///
63+
/// - Parameters:
64+
/// - length: The number of characters in the returned string.
65+
/// - Returns: A random string of length `length`.
66+
fileprivate func createRandomString(length: Int) -> String {
67+
return String(
68+
decoding: (0..<length).map{
69+
_ in base64[Int.random(in: 0..<64)]
70+
},
71+
as: UTF8.self
72+
)
73+
}
74+
75+
/// Given a base name, create a uniquely named temporary directory.
76+
///
77+
/// - Parameters:
78+
/// - basename: The base name for the new directory.
79+
/// - Returns: The path to the new directory.
80+
///
81+
/// Creates a directory in the system temporary directory whose name
82+
/// starts with `basename`, followed by a `.` and then a random
83+
/// string of characters.
84+
fileprivate func createUniqueTemporaryDirectory(
85+
basename: FilePath.Component
86+
) throws -> FilePath {
87+
var tempDir = try _getTemporaryDirectory()
88+
tempDir.append(basename)
89+
90+
while true {
91+
tempDir.extension = createRandomString(length: 16)
92+
93+
if try makeLockedDownDirectory(at: tempDir) {
94+
return tempDir
95+
}
96+
}
97+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
This source file is part of the Swift System open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift System project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
*/
9+
10+
#if !os(Windows)
11+
12+
/// Get the path to the system temporary directory.
13+
internal func _getTemporaryDirectory() throws -> FilePath {
14+
guard let tmp = system_getenv("TMPDIR") else {
15+
return "/tmp"
16+
}
17+
18+
return FilePath(SystemString(platformString: tmp))
19+
}
20+
21+
/// Delete the entire contents of a directory, including its subdirectories.
22+
///
23+
/// - Parameters:
24+
/// - path: The directory to be deleted.
25+
///
26+
/// Removes a directory completely, including all of its contents.
27+
internal func _recursiveRemove(
28+
at path: FilePath
29+
) throws {
30+
let dirfd = try FileDescriptor.open(path, .readOnly, options: .directory)
31+
defer {
32+
try? dirfd.close()
33+
}
34+
35+
let dot: (CInterop.PlatformChar, CInterop.PlatformChar) = (46, 0)
36+
try withUnsafeBytes(of: dot) {
37+
try recursiveRemove(
38+
in: dirfd.rawValue,
39+
name: $0.assumingMemoryBound(to: CInterop.PlatformChar.self).baseAddress!
40+
)
41+
}
42+
43+
try path.withPlatformString {
44+
if system_rmdir($0) != 0 {
45+
throw Errno.current
46+
}
47+
}
48+
}
49+
50+
/// Open a directory by reference to its parent and name.
51+
///
52+
/// - Parameters:
53+
/// - dirfd: An open file descriptor for the parent directory.
54+
/// - name: The name of the directory to open.
55+
/// - Returns: A pointer to a `DIR` structure.
56+
///
57+
/// This is like `opendir()`, but instead of taking a path, it uses a
58+
/// file descriptor pointing at the parent, thus avoiding path length
59+
/// limits.
60+
fileprivate func impl_opendirat(
61+
_ dirfd: CInt,
62+
_ name: UnsafePointer<CInterop.PlatformChar>
63+
) -> system_DIRPtr? {
64+
let fd = system_openat(dirfd, name,
65+
FileDescriptor.AccessMode.readOnly.rawValue
66+
| FileDescriptor.OpenOptions.directory.rawValue)
67+
if fd < 0 {
68+
return nil
69+
}
70+
return system_fdopendir(fd)
71+
}
72+
73+
/// Invoke a closure for each file within a particular directory.
74+
///
75+
/// - Parameters:
76+
/// - dirfd: The parent of the directory to be enumerated.
77+
/// - subdir: The subdirectory to be enumerated.
78+
/// - body: The closure that will be invoked.
79+
///
80+
/// We skip the `.` and `..` pseudo-entries.
81+
fileprivate func forEachFile(
82+
in dirfd: CInt,
83+
subdir: UnsafePointer<CInterop.PlatformChar>,
84+
_ body: (system_dirent) throws -> ()
85+
) throws {
86+
guard let dir = impl_opendirat(dirfd, subdir) else {
87+
throw Errno.current
88+
}
89+
defer {
90+
_ = system_closedir(dir)
91+
}
92+
93+
while let dirent = system_readdir(dir) {
94+
// Skip . and ..
95+
if dirent.pointee.d_name.0 == 46
96+
&& (dirent.pointee.d_name.1 == 0
97+
|| (dirent.pointee.d_name.1 == 46
98+
&& dirent.pointee.d_name.2 == 0)) {
99+
continue
100+
}
101+
102+
try body(dirent.pointee)
103+
}
104+
}
105+
106+
/// Delete the entire contents of a directory, including its subdirectories.
107+
///
108+
/// - Parameters:
109+
/// - dirfd: The parent of the directory to be removed.
110+
/// - name: The name of the directory to be removed.
111+
///
112+
/// Removes a directory completely, including all of its contents.
113+
fileprivate func recursiveRemove(
114+
in dirfd: CInt,
115+
name: UnsafePointer<CInterop.PlatformChar>
116+
) throws {
117+
// First, deal with subdirectories
118+
try forEachFile(in: dirfd, subdir: name) { dirent in
119+
if dirent.d_type == SYSTEM_DT_DIR {
120+
try withUnsafeBytes(of: dirent.d_name) {
121+
try recursiveRemove(
122+
in: dirfd,
123+
name: $0.assumingMemoryBound(to: CInterop.PlatformChar.self)
124+
.baseAddress!
125+
)
126+
}
127+
}
128+
}
129+
130+
// Now delete the contents of this directory
131+
try forEachFile(in: dirfd, subdir: name) { dirent in
132+
let flag: CInt
133+
134+
if dirent.d_type == SYSTEM_DT_DIR {
135+
flag = SYSTEM_AT_REMOVE_DIR
136+
} else {
137+
flag = 0
138+
}
139+
140+
let result = withUnsafeBytes(of: dirent.d_name) {
141+
system_unlinkat(dirfd,
142+
$0.assumingMemoryBound(to: CInterop.PlatformChar.self)
143+
.baseAddress!,
144+
flag)
145+
}
146+
147+
if result != 0 {
148+
throw Errno.current
149+
}
150+
}
151+
}
152+
153+
#endif // !os(Windows)

0 commit comments

Comments
 (0)