Skip to content

FileDescriptor: Add resize(to newSize:) and fileSize() APIs #82

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 8 commits into from
May 20, 2022
48 changes: 48 additions & 0 deletions Sources/System/FileOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
8 changes: 8 additions & 0 deletions Sources/System/Internals/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions Tests/SystemTests/FileDescriptorExtras.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
48 changes: 48 additions & 0 deletions Tests/SystemTests/FileOperationsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down Expand Up @@ -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<UInt8>(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<UInt8>(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
}

1 change: 1 addition & 0 deletions Utilities/expand-availability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down