authorgravatar for pat.github@tullmann.orgPat Tullmann <pat.github@tullmann.org> 2025-03-14 15:14:27-07:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2025-03-26 02:57:23+01:00
log2210c4c3604522ad0b07e15bc2a2d050923161c5
treeb3a2e60974edcd3f4ffedb574ba3dfce844215ee
parent1408288b95952b486c8bdce3b1e8eb6910de4cb7

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

2 files changed, 63 insertions(+), 4 deletions(-)

lib/std/fs/test.zig+54
...@@ -1391,6 +1391,60 @@ test "pwritev, preadv" {...@@ -1391,6 +1391,60 @@ test "pwritev, preadv" {
1391 try testing.expectEqualStrings(&buf2, "line1\n");1391 try testing.expectEqualStrings(&buf2, "line1\n");
1392}1392}
13931393
1394test "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
1394test "access file" {1448test "access file" {
1395 try testWithAllSupportedPathTypes(struct {1449 try testWithAllSupportedPathTypes(struct {
1396 fn impl(ctx: *TestContext) !void {1450 fn impl(ctx: *TestContext) !void {
lib/std/posix.zig+9-4
...@@ -1037,11 +1037,15 @@ pub const TruncateError = error{...@@ -1037,11 +1037,15 @@ pub const TruncateError = error{
1037 PermissionDenied,1037 PermissionDenied,
1038} || UnexpectedError;1038} || UnexpectedError;
10391039
1040/// Length must be positive when treated as an i64.
1040pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {1041pub 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
1041 if (native_os == .windows) {1045 if (native_os == .windows) {
1042 var io_status_block: windows.IO_STATUS_BLOCK = undefined;1046 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1043 var eof_info = windows.FILE_END_OF_FILE_INFORMATION{1047 var eof_info = windows.FILE_END_OF_FILE_INFORMATION{
1044 .EndOfFile = @bitCast(length),1048 .EndOfFile = signed_len,
1045 };1049 };
10461050
1047 const rc = windows.ntdll.NtSetInformationFile(1051 const rc = windows.ntdll.NtSetInformationFile(
...@@ -1057,6 +1061,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {...@@ -1057,6 +1061,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
1057 .INVALID_HANDLE => unreachable, // Handle not open for writing1061 .INVALID_HANDLE => unreachable, // Handle not open for writing
1058 .ACCESS_DENIED => return error.AccessDenied,1062 .ACCESS_DENIED => return error.AccessDenied,
1059 .USER_MAPPED_FILE => return error.AccessDenied,1063 .USER_MAPPED_FILE => return error.AccessDenied,
1064 .INVALID_PARAMETER => return error.FileTooBig,
1060 else => return windows.unexpectedStatus(rc),1065 else => return windows.unexpectedStatus(rc),
1061 }1066 }
1062 }1067 }
...@@ -1069,7 +1074,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {...@@ -1069,7 +1074,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
1069 .PERM => return error.PermissionDenied,1074 .PERM => return error.PermissionDenied,
1070 .TXTBSY => return error.FileBusy,1075 .TXTBSY => return error.FileBusy,
1071 .BADF => unreachable, // Handle not open for writing1076 .BADF => unreachable, // Handle not open for writing
1072 .INVAL => unreachable, // Handle not open for writing1077 .INVAL => unreachable, // Handle not open for writing, negative length, or non-resizable handle
1073 .NOTCAPABLE => return error.AccessDenied,1078 .NOTCAPABLE => return error.AccessDenied,
1074 else => |err| return unexpectedErrno(err),1079 else => |err| return unexpectedErrno(err),
1075 }1080 }
...@@ -1077,7 +1082,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {...@@ -1077,7 +1082,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
10771082
1078 const ftruncate_sym = if (lfs64_abi) system.ftruncate64 else system.ftruncate;1083 const ftruncate_sym = if (lfs64_abi) system.ftruncate64 else system.ftruncate;
1079 while (true) {1084 while (true) {
1080 switch (errno(ftruncate_sym(fd, @bitCast(length)))) {1085 switch (errno(ftruncate_sym(fd, signed_len))) {
1081 .SUCCESS => return,1086 .SUCCESS => return,
1082 .INTR => continue,1087 .INTR => continue,
1083 .FBIG => return error.FileTooBig,1088 .FBIG => return error.FileTooBig,
...@@ -1085,7 +1090,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {...@@ -1085,7 +1090,7 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
1085 .PERM => return error.PermissionDenied,1090 .PERM => return error.PermissionDenied,
1086 .TXTBSY => return error.FileBusy,1091 .TXTBSY => return error.FileBusy,
1087 .BADF => unreachable, // Handle not open for writing1092 .BADF => unreachable, // Handle not open for writing
1088 .INVAL => unreachable, // Handle not open for writing1093 .INVAL => unreachable, // Handle not open for writing, negative length, or non-resizable handle
1089 else => |err| return unexpectedErrno(err),1094 else => |err| return unexpectedErrno(err),
1090 }1095 }
1091 }1096 }