diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index 01025632..d55b2e86 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -398,3 +398,51 @@ extension FileDescriptor { } #endif } + +/*System 1.2.0, @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)*/ +extension FileDescriptor { + #if !os(Windows) + /// Truncate or extend the file referenced by this file descriptor. + /// + /// - Parameters: + /// - newSize: The length in bytes to resize the file to. + /// - retryOnInterrupt: Whether to retry the write operation + /// if it throws ``Errno/interrupted``. The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// + /// The file referenced by this file descriptor will by truncated (or extended) to `newSize`. + /// + /// If the current size of the file exceeds `newSize`, any extra data is discarded. If the current + /// size of the file is smaller than `newSize`, the file is extended and filled with zeros to the + /// provided size. + /// + /// This function requires that the file has been opened for writing. + /// + /// - Note: This function does not modify the current offset for any open file descriptors + /// associated with the file. + /// + /// The corresponding C function is `ftruncate`. + /*System 1.2.0, @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)*/ + @_alwaysEmitIntoClient + public func resize( + to newSize: Int64, + retryOnInterrupt: Bool = true + ) throws { + try _resize( + to: newSize, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /*System 1.2.0, @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)*/ + @usableFromInline + internal func _resize( + to newSize: Int64, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_ftruncate(self.rawValue, _COffT(newSize)) + } + } + #endif +} diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index 453c02fc..493155b7 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -52,6 +52,14 @@ internal func system_close(_ fd: Int32) -> Int32 { return close(fd) } +// truncate +internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, length) } +#endif + return ftruncate(fd, length) +} + // read internal func system_read( _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int diff --git a/Tests/SystemTests/FileDescriptorExtras.swift b/Tests/SystemTests/FileDescriptorExtras.swift new file mode 100644 index 00000000..b72816fa --- /dev/null +++ b/Tests/SystemTests/FileDescriptorExtras.swift @@ -0,0 +1,25 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 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 SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +extension FileDescriptor { + internal func fileSize( + retryOnInterrupt: Bool = true + ) throws -> Int64 { + let current = try seek(offset: 0, from: .current) + let size = try seek(offset: 0, from: .end) + try seek(offset: current, from: .start) + return size + } +} diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index e28efd5d..2b0a910b 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -81,6 +81,10 @@ final class FileOperationsTest: XCTestCase { _ = try fd.duplicate(as: FileDescriptor(rawValue: 42), retryOnInterrupt: retryOnInterrupt) }, + + MockTestCase(name: "ftruncate", .interruptable, rawFD, 42) { retryOnInterrupt in + _ = try fd.resize(to: 42, retryOnInterrupt: retryOnInterrupt) + }, ] for test in syscallTestCases { test.runAllTests() } @@ -160,5 +164,49 @@ final class FileOperationsTest: XCTestCase { issue26.runAllTests() } + +#if !os(Windows) + func testResizeFile() throws { + let fd = try FileDescriptor.open("/tmp/\(UUID().uuidString).txt", .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) + try fd.closeAfter { + // File should be empty initially. + XCTAssertEqual(try fd.fileSize(), 0) + // Write 3 bytes. + try fd.writeAll("abc".utf8) + // File should now be 3 bytes. + XCTAssertEqual(try fd.fileSize(), 3) + // Resize to 6 bytes. + try fd.resize(to: 6) + // File should now be 6 bytes. + XCTAssertEqual(try fd.fileSize(), 6) + // Read in the 6 bytes. + let readBytes = try Array(unsafeUninitializedCapacity: 6) { (buf, count) in + try fd.seek(offset: 0, from: .start) + // Should have read all 6 bytes. + count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) + XCTAssertEqual(count, 6) + } + // First 3 bytes should be unaffected by resize. + XCTAssertEqual(Array(readBytes[..<3]), Array("abc".utf8)) + // Extension should be padded with zeros. + XCTAssertEqual(Array(readBytes[3...]), Array(repeating: 0, count: 3)) + // File should still be 6 bytes. + XCTAssertEqual(try fd.fileSize(), 6) + // Resize to 2 bytes. + try fd.resize(to: 2) + // File should now be 2 bytes. + XCTAssertEqual(try fd.fileSize(), 2) + // Read in file with a buffer big enough for 6 bytes. + let readBytesAfterTruncation = try Array(unsafeUninitializedCapacity: 6) { (buf, count) in + try fd.seek(offset: 0, from: .start) + count = try fd.read(into: UnsafeMutableRawBufferPointer(buf)) + // Should only have read 2 bytes. + XCTAssertEqual(count, 2) + } + // Written content was trunctated. + XCTAssertEqual(readBytesAfterTruncation, Array("ab".utf8)) + } + } +#endif } diff --git a/Utilities/expand-availability.py b/Utilities/expand-availability.py index 94b9aed7..63190802 100755 --- a/Utilities/expand-availability.py +++ b/Utilities/expand-availability.py @@ -47,6 +47,7 @@ "System 0.0.1": "macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0", "System 0.0.2": "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0", "System 1.1.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999", + "System 1.2.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999", } parser = argparse.ArgumentParser(description="Expand availability macros.")