diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index e2332eee..44effdc3 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -371,3 +371,109 @@ extension FileDescriptor { fatalError("Not implemented") } } + +extension FileDescriptor { + /// Opens or creates a file for reading or writing. + /// + /// - Parameters: + /// - path: The location of the file to open. + /// - at: if `path` is relative, treat it as relative to this file descriptor + /// rather than relative to the current working directory + /// - mode: The read and write access to use. + /// - options: The behavior for opening the file. + /// - permissions: The file permissions to use for created files. + /// - retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A file descriptor for the open file + /// + /// The corresponding C function is `openat`. + @_alwaysEmitIntoClient + public static func open( + _ path: FilePath, + at: FileDescriptor, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + try path.withPlatformString { + try FileDescriptor._open( + $0, at: at, mode, options: options, permissions: permissions, + retryOnInterrupt: retryOnInterrupt + ).get() + } + } + + @usableFromInline + internal static func _open( + _ path: UnsafePointer, + at: FileDescriptor, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions?, + retryOnInterrupt: Bool + ) -> Result { + let oFlag = mode.rawValue | options.rawValue + let descOrError: Result = valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + if let permissions = permissions { + return system_openat(at.rawValue, path, oFlag, permissions.rawValue) + } + precondition(!options.contains(.create), + "Create must be given permissions") + return system_openat(at.rawValue, path, oFlag) + } + return descOrError.map { FileDescriptor(rawValue: $0) } + } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "open") + public static func openat() throws -> FileDescriptor { + fatalError("Not implemented") + } + + /// Synchronize a file's state with that on disk. + /// + /// Note that while this will flush all data from the host to the drive + /// (i.e. the "permanent storage device"), the drive itself may not + /// physically write the data to the platters for quite some time and it + /// may be written in an out-of-order sequence. + /// + /// For applications that require tighter guarantees about the integrity of + /// their data, Mac OS X provides the F_FULLFSYNC fcntl. + /// + /// The corresponding C function is `fsync`. + @_alwaysEmitIntoClient + public func sync(retryOnInterrupt: Bool = true) throws { + try _sync(retryOnInterrupt: retryOnInterrupt).get() } + + @usableFromInline + internal func _sync(retryOnInterrupt: Bool) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fsync(self.rawValue) + } + } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "sync") + public static func fsync() throws -> FileDescriptor { + fatalError("Not implemented") + } + + // TODO: Figure out the deal with fdatasync. Darwin does not have it available. + +} + +/// A namespace for global, file-system operations +public +enum FileSystem {} + +extension FileSystem { + /// Synchronize disk block status with that on disk. + /// + /// The corresponding C function is `sync`. + public static func sync() { + system_sync() + } +} diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index ecfdc843..3c6901b6 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -44,6 +44,27 @@ internal func system_open( return open(path, oflag, mode) } +// NOTE: To workaround compiler liminations, we mock `at` after `path`. +internal func system_openat( + _ at: CInt, _ path: UnsafePointer, _ oflag: Int32 +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(path: path, at, oflag) } +#endif + return openat(at, path, oflag) +} + +internal func system_openat( + _ at: CInt, + _ path: UnsafePointer, + _ oflag: Int32, _ mode: CInterop.Mode +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(path: path, at, oflag, mode) } +#endif + return openat(at, path, oflag, mode) +} + // close internal func system_close(_ fd: Int32) -> Int32 { #if ENABLE_MOCKING @@ -115,3 +136,18 @@ internal func system_dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { #endif return dup2(fd, fd2) } + +internal func system_fsync(_ fd: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd) } + #endif + return fsync(fd) +} + +internal func system_sync() { + #if ENABLE_MOCKING + if mockingEnabled { _ = _mock(); return } + #endif + return sync() +} + diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index f39a052a..c09874c7 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -86,6 +86,48 @@ final class FileOperationsTest: XCTestCase { for test in syscallTestCases { test.runAllTests() } } + // Version 2 because availability... + func testSysCallsv2() { + let fd = FileDescriptor(rawValue: 1) + + let rawBuf = UnsafeMutableRawBufferPointer.allocate(byteCount: 100, alignment: 4) + defer { rawBuf.deallocate() } + let rawFD = fd.rawValue + + // NOTE: To workaround compiler liminations, we mock `at` after `path`. + let syscallTestCases: Array = [ + MockTestCase( + name: "openat", .interruptable, "a path", rawFD, O_RDWR | O_APPEND + ) { + retry in + _ = try FileDescriptor.open( + "a path", at: fd, .readWrite, + options: [.append], retryOnInterrupt: retry) + }, + + MockTestCase( + name: "openat", .interruptable, "a path", rawFD, O_WRONLY | O_CREAT | O_APPEND, 0o777 + ) { + retry in + _ = try FileDescriptor.open( + "a path", at: fd, .writeOnly, + options: [.create, .append], + permissions: [.groupReadWriteExecute, .ownerReadWriteExecute, .otherReadWriteExecute], + retryOnInterrupt: retry) + }, + + MockTestCase(name: "fsync", .interruptable, rawFD) { retry in + _ = try fd.sync(retryOnInterrupt: retry) + }, + + MockTestCase(name: "sync", .noError) { _ in + FileSystem.sync() + }, + ] + + syscallTestCases.forEach { $0.runAllTests() } + } + func testHelpers() { // TODO: Test writeAll, writeAll(toAbsoluteOffset), closeAfter }