Skip to content

Commit 5818960

Browse files
committed
Refactor FilePathTemp, add tests for permission code.
Broke `FilePathTemp.swift` into separate files for POSIX and Windows. Added tests for permissions code on Windows. Added leading underscores to some function names. Made some other functions `fileprivate`. rdar://125087707
1 parent 3147d83 commit 5818960

File tree

10 files changed

+657
-258
lines changed

10 files changed

+657
-258
lines changed

Sources/System/ErrnoWindows.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import WinSDK
1313

1414
extension Errno {
1515
public init(windowsError: DWORD) {
16-
self.init(rawValue: mapWindowsErrorToErrno(windowsError))
16+
self.init(rawValue: _mapWindowsErrorToErrno(windowsError))
1717
}
1818
}
1919

Sources/System/FileOperations.swift

Lines changed: 44 additions & 2 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) }
@@ -506,3 +504,47 @@ extension FileDescriptor {
506504
}
507505
}
508506
}
507+
508+
extension FilePermissions {
509+
/// The file creation permission mask (aka "umask").
510+
///
511+
/// Permissions set in this mask will be cleared by functions that create
512+
/// files or directories. Note that this mask is process-wide, and that
513+
/// *getting* it is not thread safe.
514+
@_alwaysEmitIntoClient
515+
public static var creationMask: FilePermissions {
516+
get {
517+
let oldMask = _umask(0o22)
518+
_ = _umask(oldMask)
519+
return FilePermissions(rawValue: oldMask)
520+
}
521+
set {
522+
_ = _umask(newValue.rawValue)
523+
}
524+
}
525+
526+
/// Change the file creation permission mask, run some code, then
527+
/// restore it to its original value.
528+
///
529+
/// - Parameters:
530+
/// - permissions: The new permission mask.
531+
///
532+
/// This is more efficient than reading `creationMask` and restoring it
533+
/// afterwards, because of the way reading the creation mask works.
534+
@_alwaysEmitIntoClient
535+
public static func withCreationMask<R>(
536+
_ permissions: FilePermissions,
537+
body: () throws -> R
538+
) rethrows -> R {
539+
let oldMask = _umask(permissions.rawValue)
540+
defer {
541+
_ = _umask(oldMask)
542+
}
543+
return try body()
544+
}
545+
546+
@usableFromInline
547+
internal static func _umask(_ mode: CModeT) -> CModeT {
548+
return system_umask(mode)
549+
}
550+
}

Sources/System/FilePath/FilePathTemp.swift

Lines changed: 38 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -9,244 +9,43 @@
99

1010
// MARK: - API
1111

12-
public func withTemporaryPath<R>(
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>(
1322
basename: FilePath.Component,
1423
_ body: (FilePath) throws -> R
1524
) throws -> R {
1625
let temporaryDir = try createUniqueTemporaryDirectory(basename: basename)
1726
defer {
18-
try? recursiveRemove(at: temporaryDir)
27+
try? _recursiveRemove(at: temporaryDir)
1928
}
2029

2130
return try body(temporaryDir)
2231
}
2332

2433
// MARK: - Internals
2534

26-
#if os(Windows)
27-
import WinSDK
28-
29-
fileprivate func getTemporaryDirectory() throws -> FilePath {
30-
return try withUnsafeTemporaryAllocation(of: CInterop.PlatformChar.self,
31-
capacity: Int(MAX_PATH) + 1) {
32-
buffer in
33-
34-
guard GetTempPath2W(DWORD(buffer.count), buffer.baseAddress) != 0 else {
35-
throw Errno(windowsError: GetLastError())
36-
}
37-
38-
return FilePath(SystemString(platformString: buffer.baseAddress!))
39-
}
40-
}
41-
42-
fileprivate func forEachFile(
43-
at path: FilePath,
44-
_ body: (WIN32_FIND_DATAW) throws -> ()
45-
) rethrows {
46-
let searchPath = path.appending("\\*")
47-
48-
try searchPath.withPlatformString { szPath in
49-
var findData = WIN32_FIND_DATAW()
50-
let hFind = FindFirstFileW(szPath, &findData)
51-
if hFind == INVALID_HANDLE_VALUE {
52-
throw Errno(windowsError: GetLastError())
53-
}
54-
defer {
55-
FindClose(hFind)
56-
}
57-
58-
repeat {
59-
// Skip . and ..
60-
if findData.cFileName.0 == 46
61-
&& (findData.cFileName.1 == 0
62-
|| (findData.cFileName.1 == 46
63-
&& findData.cFileName.2 == 0)) {
64-
continue
65-
}
66-
67-
try body(findData)
68-
} while FindNextFileW(hFind, &findData)
69-
}
70-
}
71-
72-
fileprivate func recursiveRemove(at path: FilePath) throws {
73-
// First, deal with subdirectories
74-
try forEachFile(at: path) { findData in
75-
if (findData.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) != 0 {
76-
let name = withUnsafeBytes(of: findData.cFileName) {
77-
return SystemString(platformString: $0.assumingMemoryBound(
78-
to: CInterop.PlatformChar.self).baseAddress!)
79-
}
80-
let component = FilePath.Component(name)!
81-
let subpath = path.appending(component)
82-
83-
try recursiveRemove(at: subpath)
84-
}
85-
}
86-
87-
// Now delete everything else
88-
try forEachFile(at: path) { findData in
89-
let name = withUnsafeBytes(of: findData.cFileName) {
90-
return SystemString(platformString: $0.assumingMemoryBound(
91-
to: CInterop.PlatformChar.self).baseAddress!)
92-
}
93-
let component = FilePath.Component(name)!
94-
let subpath = path.appending(component)
95-
96-
if (findData.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) == 0 {
97-
try subpath.withPlatformString {
98-
if !DeleteFileW($0) {
99-
throw Errno(windowsError: GetLastError())
100-
}
101-
}
102-
}
103-
}
104-
105-
// Finally, delete the parent
106-
try path.withPlatformString {
107-
if !RemoveDirectoryW($0) {
108-
throw Errno(windowsError: GetLastError())
109-
}
110-
}
111-
}
112-
113-
#else
114-
fileprivate func getTemporaryDirectory() throws -> FilePath {
115-
#if SYSTEM_PACKAGE_DARWIN
116-
var capacity = 1024
117-
while true {
118-
let path: FilePath? = withUnsafeTemporaryAllocation(
119-
of: CInterop.PlatformChar.self,
120-
capacity: capacity
121-
) { buffer in
122-
let len = system_confstr(SYSTEM_CS_DARWIN_USER_TEMP_DIR,
123-
buffer.baseAddress!,
124-
buffer.count)
125-
if len == 0 {
126-
// Fall back to "/tmp" if we can't read the temp directory
127-
return "/tmp"
128-
}
129-
// If it was truncated, increase capaciy and try again
130-
if len > buffer.count {
131-
capacity = len
132-
return nil
133-
}
134-
return FilePath(SystemString(platformString: buffer.baseAddress!))
135-
}
136-
if let path = path {
137-
return path
138-
}
139-
}
140-
#else
141-
return "/tmp"
142-
#endif
143-
}
144-
145-
fileprivate func recursiveRemove(at path: FilePath) throws {
146-
let dirfd = try FileDescriptor.open(path, .readOnly, options: .directory)
147-
defer {
148-
try? dirfd.close()
149-
}
150-
151-
let dot: (CInterop.PlatformChar, CInterop.PlatformChar) = (46, 0)
152-
try withUnsafeBytes(of: dot) {
153-
try recursiveRemove(
154-
in: dirfd.rawValue,
155-
path: $0.assumingMemoryBound(to: CInterop.PlatformChar.self).baseAddress!
156-
)
157-
}
158-
159-
try path.withPlatformString {
160-
if system_rmdir($0) != 0 {
161-
throw Errno.current
162-
}
163-
}
164-
}
165-
166-
fileprivate func impl_opendirat(
167-
_ dirfd: CInt,
168-
_ path: UnsafePointer<CInterop.PlatformChar>
169-
) -> UnsafeMutablePointer<system_DIR>? {
170-
let fd = system_openat(dirfd, path,
171-
FileDescriptor.AccessMode.readOnly.rawValue
172-
| FileDescriptor.OpenOptions.directory.rawValue)
173-
if fd < 0 {
174-
return nil
175-
}
176-
return system_fdopendir(fd)
177-
}
178-
179-
fileprivate func forEachFile(
180-
in dirfd: CInt, path: UnsafePointer<CInterop.PlatformChar>,
181-
_ body: (system_dirent) throws -> ()
182-
) throws {
183-
guard let dir = impl_opendirat(dirfd, path) else {
184-
throw Errno.current
185-
}
186-
defer {
187-
_ = system_closedir(dir)
188-
}
189-
190-
while let dirent = system_readdir(dir) {
191-
// Skip . and ..
192-
if dirent.pointee.d_name.0 == 46
193-
&& (dirent.pointee.d_name.1 == 0
194-
|| (dirent.pointee.d_name.1 == 46
195-
&& dirent.pointee.d_name.2 == 0)) {
196-
continue
197-
}
198-
199-
try body(dirent.pointee)
200-
}
201-
}
202-
203-
internal func recursiveRemove(
204-
in dirfd: CInt,
205-
path: UnsafePointer<CInterop.PlatformChar>
206-
) throws {
207-
// First, deal with subdirectories
208-
try forEachFile(in: dirfd, path: path) { dirent in
209-
if dirent.d_type == SYSTEM_DT_DIR {
210-
try withUnsafeBytes(of: dirent.d_name) {
211-
try recursiveRemove(
212-
in: dirfd,
213-
path: $0.assumingMemoryBound(to: CInterop.PlatformChar.self)
214-
.baseAddress!
215-
)
216-
}
217-
}
218-
}
219-
220-
// Now delete the contents of this directory
221-
try forEachFile(in: dirfd, path: path) { dirent in
222-
let flag: CInt
223-
224-
if dirent.d_type == SYSTEM_DT_DIR {
225-
flag = SYSTEM_AT_REMOVE_DIR
226-
} else {
227-
flag = 0
228-
}
229-
230-
let result = withUnsafeBytes(of: dirent.d_name) {
231-
system_unlinkat(dirfd,
232-
$0.assumingMemoryBound(to: CInterop.PlatformChar.self)
233-
.baseAddress!,
234-
flag)
235-
}
236-
237-
if result != 0 {
238-
throw Errno.current
239-
}
240-
}
241-
}
242-
#endif
243-
24435
fileprivate let base64 = Array<UInt8>(
24536
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".utf8
24637
)
24738

248-
fileprivate func makeTempDirectory(at: FilePath) throws -> Bool {
249-
return try at.withPlatformString {
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 {
25049
if system_mkdir($0, 0o700) == 0 {
25150
return true
25251
}
@@ -259,6 +58,11 @@ fileprivate func makeTempDirectory(at: FilePath) throws -> Bool {
25958
}
26059
}
26160

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`.
26266
fileprivate func createRandomString(length: Int) -> String {
26367
return String(
26468
decoding: (0..<length).map{
@@ -268,16 +72,25 @@ fileprivate func createRandomString(length: Int) -> String {
26872
)
26973
}
27074

271-
internal func createUniqueTemporaryDirectory(
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(
27285
basename: FilePath.Component
27386
) throws -> FilePath {
274-
var tempDir = try getTemporaryDirectory()
87+
var tempDir = try _getTemporaryDirectory()
27588
tempDir.append(basename)
27689

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

280-
if try makeTempDirectory(at: tempDir) {
93+
if try makeLockedDownDirectory(at: tempDir) {
28194
return tempDir
28295
}
28396
}

0 commit comments

Comments
 (0)