Phase 2d (ii): filesystem modification time (mtime)
Completes Phase 2: the FAT filesystem now stamps and reports a real modification time, built on the Phase 2d(i) kernel wall-clock. This is the last stat field the compiler's build cache needs to reason about (source vs cached output). - on-disk.zig: fatToEpoch / epochToFatDateTime convert between the two 16-bit DOS date/time fields and Unix epoch seconds (UTC — FAT has no timezone). Host-tested round-trip + an absolute check (1577836800 == 2020-01-01). - engine: a settable current_time_epoch that create/write stamp into the entry's write (and creation) date/time; Node/Listing gained an mtime decoded from those fields on read. Host test: a create stamps the mtime, read back through resolve and listEntry. - vfs protocol FileStatus + runtime.fs.Attributes gained an mtime field; the fat server sets current_time_epoch from runtime.system.wallClock() per request and returns mtime from stat. The flat ramfs reports 0 (it has no timestamps). - fat-test reads the created file's mtime through stat and checks it is a real current time, behind a new `fat-mtime` QEMU case. Verified against the host: the guest stamped mtime 1783971676 while the host clock was 1783971680 (boot+test lag) — the file's mtime is real current time. zig build, zig build test (the epoch<->DOS conversions + the engine mtime test), zig build check-fat-image, and a sequential QEMU sweep — fat-mount, fat-mutations, fat-rename, fat-mtime, vfs, vfs-client-death, log-flush, orderly-shutdown, initial-ramdisk, smoke, wall-clock, usb-storage — all green. mode/inode remain.
This commit is contained in:
@@ -202,6 +202,96 @@ pub fn geometryOf(sector: []const u8) ?Geometry {
|
||||
};
|
||||
}
|
||||
|
||||
// --- DOS date/time <-> Unix epoch --------------------------------------------
|
||||
//
|
||||
// FAT stamps a file's modification time as two 16-bit DOS fields. There is no
|
||||
// timezone, so danos treats them as UTC. `date`: year-1980(7)|month(4)|day(5);
|
||||
// `time`: hour(5)|minute(6)|(second/2)(5).
|
||||
|
||||
fn isLeapYear(year: u32) bool {
|
||||
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0);
|
||||
}
|
||||
|
||||
const days_in_month = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
|
||||
/// Convert a FAT date+time to Unix epoch seconds (UTC). Returns 0 for an unset
|
||||
/// (zero) date.
|
||||
pub fn fatToEpoch(date: u16, time: u16) u64 {
|
||||
if (date == 0) return 0;
|
||||
const day: u32 = date & 0x1F;
|
||||
const month: u32 = (date >> 5) & 0x0F;
|
||||
const year: u32 = 1980 + (date >> 9);
|
||||
if (month < 1 or month > 12 or day < 1) return 0;
|
||||
const second: u32 = @as(u32, time & 0x1F) * 2;
|
||||
const minute: u32 = (time >> 5) & 0x3F;
|
||||
const hour: u32 = (time >> 11) & 0x1F;
|
||||
|
||||
var days: u64 = 0;
|
||||
var y: u32 = 1970;
|
||||
while (y < year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
|
||||
var m: u32 = 1;
|
||||
while (m < month) : (m += 1) {
|
||||
days += days_in_month[m - 1];
|
||||
if (m == 2 and isLeapYear(year)) days += 1;
|
||||
}
|
||||
days += day - 1;
|
||||
return ((days * 24 + hour) * 60 + minute) * 60 + second;
|
||||
}
|
||||
|
||||
pub const FatDateTime = struct { date: u16, time: u16 };
|
||||
|
||||
/// Convert Unix epoch seconds (UTC) to a FAT date+time. Returns {0,0} for epoch 0 or
|
||||
/// any time before 1980 (which DOS cannot represent).
|
||||
pub fn epochToFatDateTime(epoch: u64) FatDateTime {
|
||||
if (epoch == 0) return .{ .date = 0, .time = 0 };
|
||||
var remaining = epoch;
|
||||
const second: u32 = @intCast(remaining % 60);
|
||||
remaining /= 60;
|
||||
const minute: u32 = @intCast(remaining % 60);
|
||||
remaining /= 60;
|
||||
const hour: u32 = @intCast(remaining % 24);
|
||||
remaining /= 24;
|
||||
var days: u32 = @intCast(remaining); // whole days since 1970-01-01
|
||||
|
||||
var year: u32 = 1970;
|
||||
while (true) {
|
||||
const y_days: u32 = if (isLeapYear(year)) 366 else 365;
|
||||
if (days < y_days) break;
|
||||
days -= y_days;
|
||||
year += 1;
|
||||
}
|
||||
if (year < 1980) return .{ .date = 0, .time = 0 };
|
||||
var month: u32 = 1;
|
||||
while (true) {
|
||||
var m_days: u32 = days_in_month[month - 1];
|
||||
if (month == 2 and isLeapYear(year)) m_days += 1;
|
||||
if (days < m_days) break;
|
||||
days -= m_days;
|
||||
month += 1;
|
||||
}
|
||||
const day = days + 1;
|
||||
return .{
|
||||
.date = @intCast(((year - 1980) << 9) | (month << 5) | day),
|
||||
.time = @intCast((hour << 11) | (minute << 5) | (second / 2)),
|
||||
};
|
||||
}
|
||||
|
||||
test "FAT date/time <-> Unix epoch round trip" {
|
||||
// Even-second UTC times (FAT stores seconds/2, so even seconds round-trip exactly).
|
||||
for ([_]u64{ 1_577_836_800, 1_700_000_000, 1_262_304_000, 1_783_971_244 }) |epoch| {
|
||||
const fat = epochToFatDateTime(epoch);
|
||||
try std.testing.expectEqual(epoch, fatToEpoch(fat.date, fat.time));
|
||||
}
|
||||
// Absolute check: 1577836800 is 2020-01-01 00:00:00 UTC.
|
||||
const y2020 = epochToFatDateTime(1_577_836_800);
|
||||
try std.testing.expectEqual(@as(u16, 2020), 1980 + (y2020.date >> 9));
|
||||
try std.testing.expectEqual(@as(u16, 1), (y2020.date >> 5) & 0x0F); // month
|
||||
try std.testing.expectEqual(@as(u16, 1), y2020.date & 0x1F); // day
|
||||
// 0 is "unset" both ways.
|
||||
try std.testing.expectEqual(@as(u64, 0), fatToEpoch(0, 0));
|
||||
try std.testing.expectEqual(@as(u16, 0), epochToFatDateTime(0).date);
|
||||
}
|
||||
|
||||
test "on-disk struct sizes match the specification" {
|
||||
try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock));
|
||||
try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16));
|
||||
|
||||
Reference in New Issue
Block a user