Skip to content

Commit 5c18991

Browse files
authored
Merge pull request #82 from simonjbeaumont/sb/resize
FileDescriptor: Add resize(to newSize:) and fileSize() APIs
2 parents fea7d50 + 7bb5279 commit 5c18991

File tree

5 files changed

+130
-0
lines changed

5 files changed

+130
-0
lines changed

Sources/System/FileOperations.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,51 @@ extension FileDescriptor {
398398
}
399399
#endif
400400
}
401+
402+
/*System 1.2.0, @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)*/
403+
extension FileDescriptor {
404+
#if !os(Windows)
405+
/// Truncate or extend the file referenced by this file descriptor.
406+
///
407+
/// - Parameters:
408+
/// - newSize: The length in bytes to resize the file to.
409+
/// - retryOnInterrupt: Whether to retry the write operation
410+
/// if it throws ``Errno/interrupted``. The default is `true`.
411+
/// Pass `false` to try only once and throw an error upon interruption.
412+
///
413+
/// The file referenced by this file descriptor will by truncated (or extended) to `newSize`.
414+
///
415+
/// If the current size of the file exceeds `newSize`, any extra data is discarded. If the current
416+
/// size of the file is smaller than `newSize`, the file is extended and filled with zeros to the
417+
/// provided size.
418+
///
419+
/// This function requires that the file has been opened for writing.
420+
///
421+
/// - Note: This function does not modify the current offset for any open file descriptors
422+
/// associated with the file.
423+
///
424+
/// The corresponding C function is `ftruncate`.
425+
/*System 1.2.0, @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)*/
426+
@_alwaysEmitIntoClient
427+
public func resize(
428+
to newSize: Int64,
429+
retryOnInterrupt: Bool = true
430+
) throws {
431+
try _resize(
432+
to: newSize,
433+
retryOnInterrupt: retryOnInterrupt
434+
).get()
435+
}
436+
437+
/*System 1.2.0, @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)*/
438+
@usableFromInline
439+
internal func _resize(
440+
to newSize: Int64,
441+
retryOnInterrupt: Bool
442+
) -> Result<(), Errno> {
443+
nothingOrErrno(retryOnInterrupt: retryOnInterrupt) {
444+
system_ftruncate(self.rawValue, _COffT(newSize))
445+
}
446+
}
447+
#endif
448+
}

Sources/System/Internals/Syscalls.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ internal func system_close(_ fd: Int32) -> Int32 {
5252
return close(fd)
5353
}
5454

55+
// truncate
56+
internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {
57+
#if ENABLE_MOCKING
58+
if mockingEnabled { return _mock(fd, length) }
59+
#endif
60+
return ftruncate(fd, length)
61+
}
62+
5563
// read
5664
internal func system_read(
5765
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
This source file is part of the Swift System open source project
3+
4+
Copyright (c) 2020 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 SYSTEM_PACKAGE
11+
@testable import SystemPackage
12+
#else
13+
@testable import System
14+
#endif
15+
16+
extension FileDescriptor {
17+
internal func fileSize(
18+
retryOnInterrupt: Bool = true
19+
) throws -> Int64 {
20+
let current = try seek(offset: 0, from: .current)
21+
let size = try seek(offset: 0, from: .end)
22+
try seek(offset: current, from: .start)
23+
return size
24+
}
25+
}

Tests/SystemTests/FileOperationsTest.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ final class FileOperationsTest: XCTestCase {
8181
_ = try fd.duplicate(as: FileDescriptor(rawValue: 42),
8282
retryOnInterrupt: retryOnInterrupt)
8383
},
84+
85+
MockTestCase(name: "ftruncate", .interruptable, rawFD, 42) { retryOnInterrupt in
86+
_ = try fd.resize(to: 42, retryOnInterrupt: retryOnInterrupt)
87+
},
8488
]
8589

8690
for test in syscallTestCases { test.runAllTests() }
@@ -160,5 +164,49 @@ final class FileOperationsTest: XCTestCase {
160164
issue26.runAllTests()
161165

162166
}
167+
168+
#if !os(Windows)
169+
func testResizeFile() throws {
170+
let fd = try FileDescriptor.open("/tmp/\(UUID().uuidString).txt", .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite)
171+
try fd.closeAfter {
172+
// File should be empty initially.
173+
XCTAssertEqual(try fd.fileSize(), 0)
174+
// Write 3 bytes.
175+
try fd.writeAll("abc".utf8)
176+
// File should now be 3 bytes.
177+
XCTAssertEqual(try fd.fileSize(), 3)
178+
// Resize to 6 bytes.
179+
try fd.resize(to: 6)
180+
// File should now be 6 bytes.
181+
XCTAssertEqual(try fd.fileSize(), 6)
182+
// Read in the 6 bytes.
183+
let readBytes = try Array<UInt8>(unsafeUninitializedCapacity: 6) { (buf, count) in
184+
try fd.seek(offset: 0, from: .start)
185+
// Should have read all 6 bytes.
186+
count = try fd.read(into: UnsafeMutableRawBufferPointer(buf))
187+
XCTAssertEqual(count, 6)
188+
}
189+
// First 3 bytes should be unaffected by resize.
190+
XCTAssertEqual(Array(readBytes[..<3]), Array("abc".utf8))
191+
// Extension should be padded with zeros.
192+
XCTAssertEqual(Array(readBytes[3...]), Array(repeating: 0, count: 3))
193+
// File should still be 6 bytes.
194+
XCTAssertEqual(try fd.fileSize(), 6)
195+
// Resize to 2 bytes.
196+
try fd.resize(to: 2)
197+
// File should now be 2 bytes.
198+
XCTAssertEqual(try fd.fileSize(), 2)
199+
// Read in file with a buffer big enough for 6 bytes.
200+
let readBytesAfterTruncation = try Array<UInt8>(unsafeUninitializedCapacity: 6) { (buf, count) in
201+
try fd.seek(offset: 0, from: .start)
202+
count = try fd.read(into: UnsafeMutableRawBufferPointer(buf))
203+
// Should only have read 2 bytes.
204+
XCTAssertEqual(count, 2)
205+
}
206+
// Written content was trunctated.
207+
XCTAssertEqual(readBytesAfterTruncation, Array("ab".utf8))
208+
}
209+
}
210+
#endif
163211
}
164212

Utilities/expand-availability.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"System 0.0.1": "macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0",
4848
"System 0.0.2": "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0",
4949
"System 1.1.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999",
50+
"System 1.2.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999",
5051
}
5152

5253
parser = argparse.ArgumentParser(description="Expand availability macros.")

0 commit comments

Comments
 (0)