Skip to content

Commit 63fe688

Browse files
committed
lib/std/posix: test ftruncate via std.fs.File.setEndPos()
Add a test for std.fs.File's `setEndPos` (which is a simple wrapper around `std.posix.ftruncate`) to exercise some success and failure paths. Explicitly check that the `ftruncate` length isn't negative when interpreted as a signed value. This avoids having to decode overloaded `EINVAL` errors. Add errno handling to Windows path to map INVALID_PARAMETER to FileTooBig. Fixes #22960
1 parent ea57fb5 commit 63fe688

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

lib/std/fs/test.zig

+54
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,60 @@ test "pwritev, preadv" {
13911391
try testing.expectEqualStrings(&buf2, "line1\n");
13921392
}
13931393

1394+
test "setEndPos" {
1395+
// https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission)
1396+
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
1397+
1398+
var tmp = tmpDir(.{});
1399+
defer tmp.cleanup();
1400+
1401+
const file_name = "afile.txt";
1402+
try tmp.dir.writeFile(.{ .sub_path = file_name, .data = "ninebytes" });
1403+
const f = try tmp.dir.openFile(file_name, .{ .mode = .read_write });
1404+
defer f.close();
1405+
1406+
const initial_size = try f.getEndPos();
1407+
var buffer: [32]u8 = undefined;
1408+
1409+
{
1410+
try f.setEndPos(initial_size);
1411+
try testing.expectEqual(initial_size, try f.getEndPos());
1412+
try testing.expectEqual(initial_size, try f.preadAll(&buffer, 0));
1413+
try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]);
1414+
}
1415+
1416+
{
1417+
const larger = initial_size + 4;
1418+
try f.setEndPos(larger);
1419+
try testing.expectEqual(larger, try f.getEndPos());
1420+
try testing.expectEqual(larger, try f.preadAll(&buffer, 0));
1421+
try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]);
1422+
}
1423+
1424+
{
1425+
const smaller = initial_size - 5;
1426+
try f.setEndPos(smaller);
1427+
try testing.expectEqual(smaller, try f.getEndPos());
1428+
try testing.expectEqual(smaller, try f.preadAll(&buffer, 0));
1429+
try testing.expectEqualStrings("nine", buffer[0..@intCast(smaller)]);
1430+
}
1431+
1432+
try f.setEndPos(0);
1433+
try testing.expectEqual(0, try f.getEndPos());
1434+
try testing.expectEqual(0, try f.preadAll(&buffer, 0));
1435+
1436+
// Invalid file length should error gracefully. Actual limit is host
1437+
// and file-system dependent, but 1PB should fail most everywhere.
1438+
// Except MacOS APFS limit is 8 exabytes.
1439+
f.setEndPos(0x4_0000_0000_0000) catch |err| if (err != error.FileTooBig) {
1440+
return err;
1441+
};
1442+
1443+
try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u63))); // Maximum signed value
1444+
1445+
try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u64)));
1446+
}
1447+
13941448
test "access file" {
13951449
try testWithAllSupportedPathTypes(struct {
13961450
fn impl(ctx: *TestContext) !void {

lib/std/posix.zig

+9-4
Original file line numberDiff line numberDiff line change
@@ -1037,11 +1037,15 @@ pub const TruncateError = error{
10371037
AccessDenied,
10381038
} || UnexpectedError;
10391039

1040+
/// Length must be positive when treated as an i64.
10401041
pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
1042+
const signed_len: i64 = @bitCast(length);
1043+
if (signed_len < 0) return error.FileTooBig; // avoid ambiguous EINVAL errors
1044+
10411045
if (native_os == .windows) {
10421046
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
10431047
var eof_info = windows.FILE_END_OF_FILE_INFORMATION{
1044-
.EndOfFile = @bitCast(length),
1048+
.EndOfFile = signed_len,
10451049
};
10461050

10471051
const rc = windows.ntdll.NtSetInformationFile(
@@ -1057,6 +1061,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
10571061
.INVALID_HANDLE => unreachable, // Handle not open for writing
10581062
.ACCESS_DENIED => return error.AccessDenied,
10591063
.USER_MAPPED_FILE => return error.AccessDenied,
1064+
.INVALID_PARAMETER => return error.FileTooBig,
10601065
else => return windows.unexpectedStatus(rc),
10611066
}
10621067
}
@@ -1069,23 +1074,23 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
10691074
.PERM => return error.AccessDenied,
10701075
.TXTBSY => return error.FileBusy,
10711076
.BADF => unreachable, // Handle not open for writing
1072-
.INVAL => unreachable, // Handle not open for writing
1077+
.INVAL => unreachable, // Handle not open for writing, negative length, or non-resizable handle
10731078
.NOTCAPABLE => return error.AccessDenied,
10741079
else => |err| return unexpectedErrno(err),
10751080
}
10761081
}
10771082

10781083
const ftruncate_sym = if (lfs64_abi) system.ftruncate64 else system.ftruncate;
10791084
while (true) {
1080-
switch (errno(ftruncate_sym(fd, @bitCast(length)))) {
1085+
switch (errno(ftruncate_sym(fd, signed_len))) {
10811086
.SUCCESS => return,
10821087
.INTR => continue,
10831088
.FBIG => return error.FileTooBig,
10841089
.IO => return error.InputOutput,
10851090
.PERM => return error.AccessDenied,
10861091
.TXTBSY => return error.FileBusy,
10871092
.BADF => unreachable, // Handle not open for writing
1088-
.INVAL => unreachable, // Handle not open for writing
1093+
.INVAL => unreachable, // Handle not open for writing, negative length, or non-resizable handle
10891094
else => |err| return unexpectedErrno(err),
10901095
}
10911096
}

0 commit comments

Comments
 (0)