1427 lines
65 KiB
Zig
1427 lines
65 KiB
Zig
//! The FAT filesystem engine: mount a block device, walk the FAT and directory
|
|
//! structures, and read / write / create / truncate files, remove files, and make
|
|
//! directories. FAT12/16/32 (the type is detected from the cluster count). The
|
|
//! crash-safe write order is data -> FAT -> directory; truncate and remove free the
|
|
//! cluster chain, then update the directory. Pure logic over a `BlockDevice` interface —
|
|
//! no IPC — so it is host-testable against a RAM-backed image (see the tests at
|
|
//! the bottom). The fat.zig server wraps a real `.block` device in a BlockDevice
|
|
//! and serves this over the VFS protocol.
|
|
//!
|
|
//! Everything works in 512-byte sectors; a cluster is N sectors. Names are
|
|
//! matched case-insensitively against both the 8.3 short name and, when present,
|
|
//! the reconstructed long name. Writes update the directory entry, every FAT
|
|
//! copy, and (FAT32) the FSInfo hint, in the crash-safe order data -> FAT ->
|
|
//! directory. Long-name *creation* is not implemented — new files get an 8.3
|
|
//! name (the common case; the plan flags LFN-write as optional).
|
|
|
|
const std = @import("std");
|
|
const on_disk = @import("on-disk.zig");
|
|
|
|
/// A block device the engine reads and writes in fixed-size blocks. The two
|
|
/// function pointers let the same engine run over a real `.block` driver or a
|
|
/// RAM buffer (the tests).
|
|
pub const BlockDevice = struct {
|
|
context: *anyopaque,
|
|
block_size: u32,
|
|
block_count: u64,
|
|
readBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []u8) bool,
|
|
writeBlockFn: *const fn (context: *anyopaque, lba: u64, buffer: []const u8) bool,
|
|
|
|
pub fn readBlock(self: BlockDevice, lba: u64, buffer: []u8) bool {
|
|
return self.readBlockFn(self.context, lba, buffer);
|
|
}
|
|
pub fn writeBlock(self: BlockDevice, lba: u64, buffer: []const u8) bool {
|
|
return self.writeBlockFn(self.context, lba, buffer);
|
|
}
|
|
};
|
|
|
|
/// A resolved filesystem object: a file or directory, and where its 8.3 entry
|
|
/// lives so writes can update its size and first cluster.
|
|
pub const Node = struct {
|
|
first_cluster: u32,
|
|
size: u32,
|
|
is_directory: bool,
|
|
// Modification time (Unix epoch seconds, UTC), decoded from the directory
|
|
// entry's DOS write date/time. 0 if unset.
|
|
mtime: u64 = 0,
|
|
// The absolute sector and byte offset of this node's 8.3 directory entry, so
|
|
// size/first-cluster changes can be written back. Absent for the root.
|
|
entry_sector: u64 = 0,
|
|
entry_offset: u32 = 0,
|
|
has_entry: bool = false,
|
|
};
|
|
|
|
const sector_size = 512;
|
|
const entries_per_sector = sector_size / @sizeOf(on_disk.DirectoryEntry); // 16
|
|
|
|
pub const FileSystem = struct {
|
|
device: BlockDevice,
|
|
geometry: on_disk.Geometry,
|
|
// The absolute LBA the filesystem starts at: 0 for a bare FAT ("superfloppy"),
|
|
// or the first partition's start LBA when the disk carries an MBR. Every
|
|
// filesystem-relative sector read/write adds this.
|
|
base_lba: u64 = 0,
|
|
// Distinct scratch sectors so nested reads (a FAT lookup during a directory
|
|
// scan) never alias each other.
|
|
sector: [sector_size]u8 = undefined,
|
|
fat_sector: [sector_size]u8 = undefined,
|
|
dir_sector: [sector_size]u8 = undefined,
|
|
// Wall-clock time (Unix epoch seconds) to stamp on create/write, set by the
|
|
// server before a mutating op. 0 leaves the on-disk timestamps untouched (host
|
|
// tests that don't care about time, and reads).
|
|
current_time_epoch: u64 = 0,
|
|
// Where the next allocateCluster scan starts — clusters below this were seen
|
|
// in use, so a fresh scan needn't re-read them (frees rewind it). Without
|
|
// this the scan re-read the FAT from cluster 2 per allocation: measured at
|
|
// ~1 s/cluster on a part-full volume (a 37 s shutdown log flush).
|
|
next_free_hint: u32 = 2,
|
|
// Which absolute LBA `fat_sector` currently holds (0 = none). Lets a FAT
|
|
// scan serve consecutive entries from one device read; every write through
|
|
// the sector keeps the cache coherent (writeFatBytes updates it in place).
|
|
fat_sector_lba: u64 = 0,
|
|
|
|
// Every filesystem-relative sector access adds the partition base.
|
|
fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool {
|
|
return self.device.readBlock(self.base_lba + lba, buffer);
|
|
}
|
|
fn blockWrite(self: *FileSystem, lba: u64, buffer: []const u8) bool {
|
|
return self.device.writeBlock(self.base_lba + lba, buffer);
|
|
}
|
|
|
|
/// Mount the filesystem on `device`: either a bare FAT with its boot sector at
|
|
/// LBA 0, or (as QEMU's VVFAT and most real USB sticks present it) an MBR-
|
|
/// partitioned disk whose first FAT partition holds the boot sector. Returns
|
|
/// null if neither is found.
|
|
pub fn mount(device: BlockDevice) ?FileSystem {
|
|
var boot: [sector_size]u8 = undefined;
|
|
if (!device.readBlock(0, &boot)) return null;
|
|
|
|
// A bare FAT: a valid boot sector right at LBA 0.
|
|
if (on_disk.geometryOf(&boot)) |geometry| {
|
|
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = 0 };
|
|
}
|
|
|
|
// Otherwise an MBR: the 0x55AA signature but no BPB. Walk its four
|
|
// partition entries (16 bytes each at offset 446) for the first non-empty
|
|
// one, and mount the FAT boot sector at that partition's start LBA.
|
|
if (boot[510] == 0x55 and boot[511] == 0xAA) {
|
|
var partition: usize = 0;
|
|
while (partition < 4) : (partition += 1) {
|
|
const entry = boot[446 + partition * 16 ..][0..16];
|
|
const partition_type = entry[4];
|
|
const start_lba = std.mem.readInt(u32, entry[8..12], .little);
|
|
if (partition_type == 0 or start_lba == 0) continue;
|
|
var partition_boot: [sector_size]u8 = undefined;
|
|
if (!device.readBlock(start_lba, &partition_boot)) continue;
|
|
if (on_disk.geometryOf(&partition_boot)) |geometry| {
|
|
if (geometry.bytes_per_sector == sector_size) return .{ .device = device, .geometry = geometry, .base_lba = start_lba };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// --- cluster <-> sector -------------------------------------------------
|
|
|
|
fn clusterSector(self: *const FileSystem, cluster: u32, sector_in_cluster: u32) u64 {
|
|
return @as(u64, self.geometry.first_data_sector) + @as(u64, cluster - 2) * self.geometry.sectors_per_cluster + sector_in_cluster;
|
|
}
|
|
|
|
fn fatByteBase(self: *const FileSystem) u64 {
|
|
return @as(u64, self.geometry.reserved_sector_count) * sector_size;
|
|
}
|
|
|
|
fn rootDirStartSector(self: *const FileSystem) u64 {
|
|
return @as(u64, self.geometry.reserved_sector_count) + @as(u64, self.geometry.fat_count) * self.geometry.fat_size_sectors;
|
|
}
|
|
|
|
fn rootDirSectors(self: *const FileSystem) u32 {
|
|
return (self.geometry.root_entry_count * 32 + sector_size - 1) / sector_size;
|
|
}
|
|
|
|
// --- FAT access ---------------------------------------------------------
|
|
|
|
// Read `out.len` bytes from FAT #0 starting at `byte_offset`, spanning sectors.
|
|
fn readFatBytes(self: *FileSystem, byte_offset: u64, out: []u8) bool {
|
|
var done: usize = 0;
|
|
var position = self.fatByteBase() + byte_offset;
|
|
while (done < out.len) {
|
|
const lba = position / sector_size;
|
|
const within: usize = @intCast(position % sector_size);
|
|
if (lba != self.fat_sector_lba) {
|
|
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
|
self.fat_sector_lba = lba;
|
|
}
|
|
const n = @min(out.len - done, sector_size - within);
|
|
@memcpy(out[done .. done + n], self.fat_sector[within .. within + n]);
|
|
done += n;
|
|
position += n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Write `in.len` bytes at `byte_offset` into every FAT copy (read-modify-write
|
|
// per sector).
|
|
fn writeFatBytes(self: *FileSystem, byte_offset: u64, in: []const u8) bool {
|
|
var fat: u32 = 0;
|
|
while (fat < self.geometry.fat_count) : (fat += 1) {
|
|
const base = self.fatByteBase() + @as(u64, fat) * @as(u64, self.geometry.fat_size_sectors) * sector_size;
|
|
var done: usize = 0;
|
|
var position = base + byte_offset;
|
|
while (done < in.len) {
|
|
const lba = position / sector_size;
|
|
const within: usize = @intCast(position % sector_size);
|
|
if (lba != self.fat_sector_lba) {
|
|
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
|
self.fat_sector_lba = lba;
|
|
}
|
|
const n = @min(in.len - done, sector_size - within);
|
|
@memcpy(self.fat_sector[within .. within + n], in[done .. done + n]);
|
|
if (!self.blockWrite(lba, &self.fat_sector)) return false;
|
|
done += n;
|
|
position += n;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn readFatEntry(self: *FileSystem, cluster: u32) u32 {
|
|
switch (self.geometry.fat_type) {
|
|
.fat12 => {
|
|
var pair: [2]u8 = undefined;
|
|
const offset = cluster + cluster / 2; // cluster * 1.5
|
|
if (!self.readFatBytes(offset, &pair)) return on_disk.end_of_chain_12;
|
|
const word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
|
|
return if (cluster & 1 == 1) (word >> 4) else (word & 0x0FFF);
|
|
},
|
|
.fat16 => {
|
|
var value: [2]u8 = undefined;
|
|
if (!self.readFatBytes(@as(u64, cluster) * 2, &value)) return on_disk.end_of_chain_16;
|
|
return @as(u16, value[0]) | (@as(u16, value[1]) << 8);
|
|
},
|
|
.fat32 => {
|
|
var value: [4]u8 = undefined;
|
|
if (!self.readFatBytes(@as(u64, cluster) * 4, &value)) return on_disk.end_of_chain_32;
|
|
return (@as(u32, value[0]) | (@as(u32, value[1]) << 8) | (@as(u32, value[2]) << 16) | (@as(u32, value[3]) << 24)) & 0x0FFFFFFF;
|
|
},
|
|
}
|
|
}
|
|
|
|
fn writeFatEntry(self: *FileSystem, cluster: u32, value: u32) bool {
|
|
switch (self.geometry.fat_type) {
|
|
.fat12 => {
|
|
const offset = cluster + cluster / 2;
|
|
var pair: [2]u8 = undefined;
|
|
if (!self.readFatBytes(offset, &pair)) return false;
|
|
var word = @as(u16, pair[0]) | (@as(u16, pair[1]) << 8);
|
|
if (cluster & 1 == 1) {
|
|
word = (word & 0x000F) | (@as(u16, @truncate(value)) << 4);
|
|
} else {
|
|
word = (word & 0xF000) | (@as(u16, @truncate(value)) & 0x0FFF);
|
|
}
|
|
pair[0] = @truncate(word);
|
|
pair[1] = @truncate(word >> 8);
|
|
return self.writeFatBytes(offset, &pair);
|
|
},
|
|
.fat16 => {
|
|
const bytes = [2]u8{ @truncate(value), @truncate(value >> 8) };
|
|
return self.writeFatBytes(@as(u64, cluster) * 2, &bytes);
|
|
},
|
|
.fat32 => {
|
|
const bytes = [4]u8{ @truncate(value), @truncate(value >> 8), @truncate(value >> 16), @truncate(value >> 24) };
|
|
return self.writeFatBytes(@as(u64, cluster) * 4, &bytes);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn isEndOfChain(self: *const FileSystem, value: u32) bool {
|
|
return switch (self.geometry.fat_type) {
|
|
.fat12 => value >= on_disk.end_of_chain_12,
|
|
.fat16 => value >= on_disk.end_of_chain_16,
|
|
.fat32 => value >= on_disk.end_of_chain_32,
|
|
};
|
|
}
|
|
|
|
fn endOfChainValue(self: *const FileSystem) u32 {
|
|
return switch (self.geometry.fat_type) {
|
|
.fat12 => 0xFFF,
|
|
.fat16 => 0xFFFF,
|
|
.fat32 => 0x0FFFFFFF,
|
|
};
|
|
}
|
|
|
|
// Find and claim a free cluster, marking it end-of-chain. Returns its number.
|
|
fn allocateCluster(self: *FileSystem) ?u32 {
|
|
const limit = self.geometry.cluster_count + 2;
|
|
// Two passes: hint..end, then 2..hint (the hint only skips known-used
|
|
// ground, it never hides a freed cluster — freeChain rewinds it).
|
|
var pass: u2 = 0;
|
|
while (pass < 2) : (pass += 1) {
|
|
var cluster: u32 = if (pass == 0) self.next_free_hint else 2;
|
|
const end: u32 = if (pass == 0) limit else self.next_free_hint;
|
|
while (cluster < end) : (cluster += 1) {
|
|
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
|
|
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
|
|
self.next_free_hint = cluster + 1;
|
|
return cluster;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Free every cluster of the chain starting at `first`, returning them to the
|
|
// pool. A first < 2 (an empty file) frees nothing. Bounded against a corrupt
|
|
// cyclic chain by the cluster count so it can never loop forever.
|
|
fn freeChain(self: *FileSystem, first: u32) void {
|
|
var cluster = first;
|
|
var guard: u32 = 0;
|
|
const limit = self.geometry.cluster_count + 2;
|
|
while (cluster >= 2 and cluster < limit and guard < limit) : (guard += 1) {
|
|
const next = self.readFatEntry(cluster);
|
|
_ = self.writeFatEntry(cluster, on_disk.free_cluster);
|
|
if (cluster < self.next_free_hint) self.next_free_hint = cluster;
|
|
if (self.isEndOfChain(next) or next < 2) break;
|
|
cluster = next;
|
|
}
|
|
}
|
|
|
|
// --- directory iteration ------------------------------------------------
|
|
|
|
// The absolute LBA of the `sector_index`th sector of directory `dir`, or null
|
|
// past its end. If `grow` is set and a cluster chain runs out, a new cluster
|
|
// is allocated and linked (used when appending a directory entry).
|
|
fn dirSectorLba(self: *FileSystem, dir: Node, sector_index: u32, grow: bool) ?u64 {
|
|
const is_fixed_root = dir.first_cluster == 0 and self.geometry.fat_type != .fat32;
|
|
if (is_fixed_root) {
|
|
if (sector_index >= self.rootDirSectors()) return null;
|
|
return self.rootDirStartSector() + sector_index;
|
|
}
|
|
const spc = self.geometry.sectors_per_cluster;
|
|
var cluster = if (dir.first_cluster == 0) self.geometry.root_cluster else dir.first_cluster;
|
|
var remaining = sector_index;
|
|
while (remaining >= spc) : (remaining -= spc) {
|
|
var next = self.readFatEntry(cluster);
|
|
if (self.isEndOfChain(next) or next < 2) {
|
|
if (!grow) return null;
|
|
const fresh = self.allocateCluster() orelse return null;
|
|
self.zeroCluster(fresh);
|
|
if (!self.writeFatEntry(cluster, fresh)) return null;
|
|
next = fresh;
|
|
}
|
|
cluster = next;
|
|
}
|
|
return self.clusterSector(cluster, remaining);
|
|
}
|
|
|
|
fn zeroCluster(self: *FileSystem, cluster: u32) void {
|
|
var zero = [_]u8{0} ** sector_size;
|
|
var s: u32 = 0;
|
|
while (s < self.geometry.sectors_per_cluster) : (s += 1) {
|
|
_ = self.blockWrite(self.clusterSector(cluster, s), &zero);
|
|
}
|
|
}
|
|
|
|
pub fn rootNode(self: *const FileSystem) Node {
|
|
return .{
|
|
.first_cluster = if (self.geometry.fat_type == .fat32) self.geometry.root_cluster else 0,
|
|
.size = 0,
|
|
.is_directory = true,
|
|
.has_entry = false,
|
|
};
|
|
}
|
|
|
|
// --- name handling ------------------------------------------------------
|
|
|
|
// Format a raw 8.3 name ("NAME EXT") into the displayed "NAME.EXT".
|
|
fn format83(raw: [11]u8, out: []u8) []const u8 {
|
|
var length: usize = 0;
|
|
var base_len: usize = 8;
|
|
while (base_len > 0 and raw[base_len - 1] == ' ') base_len -= 1;
|
|
for (raw[0..base_len]) |c| {
|
|
if (length < out.len) {
|
|
out[length] = c;
|
|
length += 1;
|
|
}
|
|
}
|
|
var ext_len: usize = 3;
|
|
while (ext_len > 0 and raw[8 + ext_len - 1] == ' ') ext_len -= 1;
|
|
if (ext_len > 0) {
|
|
if (length < out.len) {
|
|
out[length] = '.';
|
|
length += 1;
|
|
}
|
|
for (raw[8 .. 8 + ext_len]) |c| {
|
|
if (length < out.len) {
|
|
out[length] = c;
|
|
length += 1;
|
|
}
|
|
}
|
|
}
|
|
return out[0..length];
|
|
}
|
|
|
|
// Convert a name to a raw 8.3 field (uppercased, space-padded), or null if it
|
|
// cannot be represented (too long a base or extension).
|
|
fn to83(name: []const u8) ?[11]u8 {
|
|
var raw = [_]u8{' '} ** 11;
|
|
const dot = std.mem.lastIndexOfScalar(u8, name, '.');
|
|
const base = if (dot) |d| name[0..d] else name;
|
|
const ext = if (dot) |d| name[d + 1 ..] else name[0..0];
|
|
if (base.len == 0 or base.len > 8 or ext.len > 3) return null;
|
|
for (base, 0..) |c, i| raw[i] = std.ascii.toUpper(c);
|
|
for (ext, 0..) |c, i| raw[8 + i] = std.ascii.toUpper(c);
|
|
return raw;
|
|
}
|
|
|
|
fn nameMatches(display: []const u8, query: []const u8) bool {
|
|
if (display.len != query.len) return false;
|
|
for (display, query) |a, b| {
|
|
if (std.ascii.toUpper(a) != std.ascii.toUpper(b)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Pull the 13 UTF-16 code units of one long-name entry into `out` (ASCII only,
|
|
// non-ASCII becomes '?'). Returns how many characters (stopping at 0x0000).
|
|
fn longNameChars(entry: on_disk.LongNameEntry, out: *[13]u8) usize {
|
|
const units = [13]u16{
|
|
entry.name1[0], entry.name1[1], entry.name1[2], entry.name1[3], entry.name1[4],
|
|
entry.name2[0], entry.name2[1], entry.name2[2], entry.name2[3], entry.name2[4],
|
|
entry.name2[5], entry.name3[0], entry.name3[1],
|
|
};
|
|
var count: usize = 0;
|
|
for (units) |unit| {
|
|
if (unit == 0x0000 or unit == 0xFFFF) break;
|
|
out[count] = if (unit < 0x80) @truncate(unit) else '?';
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// --- directory search + listing ----------------------------------------
|
|
|
|
/// Iterate the entries of a directory, calling `visit` with each real (non-LFN,
|
|
/// non-free) entry, its reconstructed display name, and where it lives. Stops
|
|
/// when `visit` returns true or the directory ends.
|
|
fn scanDirectory(
|
|
self: *FileSystem,
|
|
dir: Node,
|
|
context: anytype,
|
|
comptime visit: fn (@TypeOf(context), entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool,
|
|
) void {
|
|
var long_name: [260]u8 = undefined;
|
|
var long_len: usize = 0;
|
|
var sector_index: u32 = 0;
|
|
while (self.dirSectorLba(dir, sector_index, false)) |lba| : (sector_index += 1) {
|
|
if (!self.blockRead(lba, &self.dir_sector)) return;
|
|
var i: u32 = 0;
|
|
while (i < entries_per_sector) : (i += 1) {
|
|
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
|
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
|
if (entry.isEnd()) return;
|
|
if (entry.name[0] == 0xE5) {
|
|
long_len = 0;
|
|
continue;
|
|
}
|
|
if (entry.isLongName()) {
|
|
const lfn = std.mem.bytesToValue(on_disk.LongNameEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.LongNameEntry)]);
|
|
const order = lfn.order & 0x1F;
|
|
if (order >= 1 and order <= 20) {
|
|
var chunk: [13]u8 = undefined;
|
|
const n = longNameChars(lfn, &chunk);
|
|
const start = (order - 1) * 13;
|
|
if (start + n <= long_name.len) {
|
|
@memcpy(long_name[start .. start + n], chunk[0..n]);
|
|
if (lfn.order & 0x40 != 0) long_len = start + n; // last (first physical) piece sets the length
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isVolumeLabel()) {
|
|
long_len = 0;
|
|
continue;
|
|
}
|
|
var short: [12]u8 = undefined;
|
|
const display = if (long_len > 0) long_name[0..long_len] else format83(entry.name, &short);
|
|
if (visit(context, entry, display, lba, offset)) return;
|
|
long_len = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
const FindResult = struct { found: bool = false, node: Node = undefined };
|
|
const FindContext = struct { query: []const u8, result: *FindResult };
|
|
|
|
fn findVisit(context: *const FindContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
|
|
if (!nameMatches(name, context.query)) return false;
|
|
context.result.* = .{ .found = true, .node = .{
|
|
.first_cluster = entry.firstCluster(),
|
|
.size = entry.file_size,
|
|
.is_directory = entry.isDirectory(),
|
|
.mtime = on_disk.fatToEpoch(entry.write_date, entry.write_time),
|
|
.entry_sector = entry_sector,
|
|
.entry_offset = entry_offset,
|
|
.has_entry = true,
|
|
} };
|
|
return true;
|
|
}
|
|
|
|
fn findChild(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
|
var result = FindResult{};
|
|
var context = FindContext{ .query = name, .result = &result };
|
|
self.scanDirectory(dir, &context, findVisit);
|
|
return if (result.found) result.node else null;
|
|
}
|
|
|
|
/// Resolve an absolute or "/"-relative path to a node. "/" is the root.
|
|
pub fn resolve(self: *FileSystem, path: []const u8) ?Node {
|
|
var node = self.rootNode();
|
|
var it = std.mem.tokenizeScalar(u8, path, '/');
|
|
while (it.next()) |component| {
|
|
if (component.len == 0) continue;
|
|
if (!node.is_directory) return null;
|
|
node = self.findChild(node, component) orelse return null;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/// The `cursor`th real entry of a directory (for readdir): its display name,
|
|
/// kind, and size. Returns null past the end.
|
|
pub const Listing = struct { name_buffer: [260]u8 = undefined, name_len: usize = 0, is_directory: bool = false, size: u32 = 0, mtime: u64 = 0 };
|
|
const ListContext = struct { target: u32, index: u32 = 0, out: *Listing, done: bool = false };
|
|
|
|
fn listVisit(context: *ListContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
|
|
_ = entry_sector;
|
|
_ = entry_offset;
|
|
if (context.index == context.target) {
|
|
const n = @min(name.len, context.out.name_buffer.len);
|
|
@memcpy(context.out.name_buffer[0..n], name[0..n]);
|
|
context.out.name_len = n;
|
|
context.out.is_directory = entry.isDirectory();
|
|
context.out.size = entry.file_size;
|
|
context.out.mtime = on_disk.fatToEpoch(entry.write_date, entry.write_time);
|
|
context.done = true;
|
|
return true;
|
|
}
|
|
context.index += 1;
|
|
return false;
|
|
}
|
|
|
|
pub fn listEntry(self: *FileSystem, dir: Node, cursor: u32) ?Listing {
|
|
var listing = Listing{};
|
|
var context = ListContext{ .target = cursor, .out = &listing };
|
|
self.scanDirectory(dir, &context, listVisit);
|
|
return if (context.done) listing else null;
|
|
}
|
|
|
|
// --- file read / write --------------------------------------------------
|
|
|
|
// The cluster holding byte `offset` of a chain starting at `first`, walking
|
|
// (and optionally growing) the chain. Returns null at end without grow.
|
|
fn clusterAt(self: *FileSystem, first: u32, offset: u32, grow: bool) ?u32 {
|
|
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
|
var cluster = first;
|
|
var steps = offset / cluster_bytes;
|
|
while (steps > 0) : (steps -= 1) {
|
|
var next = self.readFatEntry(cluster);
|
|
if (self.isEndOfChain(next) or next < 2) {
|
|
if (!grow) return null;
|
|
const fresh = self.allocateCluster() orelse return null;
|
|
if (!self.writeFatEntry(cluster, fresh)) return null;
|
|
next = fresh;
|
|
}
|
|
cluster = next;
|
|
}
|
|
return cluster;
|
|
}
|
|
|
|
/// Read up to `buffer.len` bytes of a file node starting at `offset`. Returns
|
|
/// the number read (0 at or past EOF).
|
|
pub fn readFile(self: *FileSystem, node: Node, offset: u32, buffer: []u8) usize {
|
|
if (offset >= node.size or node.first_cluster < 2) return 0;
|
|
const available = node.size - offset;
|
|
const want = @min(buffer.len, available);
|
|
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
|
|
|
var produced: usize = 0;
|
|
var position = offset;
|
|
while (produced < want) {
|
|
const cluster = self.clusterAt(node.first_cluster, position, false) orelse break;
|
|
const in_cluster = position % cluster_bytes;
|
|
const sector_in_cluster = in_cluster / sector_size;
|
|
const in_sector = in_cluster % sector_size;
|
|
if (!self.blockRead(self.clusterSector(cluster, sector_in_cluster), &self.sector)) break;
|
|
const n = @min(want - produced, sector_size - in_sector);
|
|
@memcpy(buffer[produced .. produced + n], self.sector[in_sector .. in_sector + n]);
|
|
produced += n;
|
|
position += @intCast(n);
|
|
}
|
|
return produced;
|
|
}
|
|
|
|
/// Write `data` to a file node at `offset`, growing it (allocating clusters and
|
|
/// updating the directory entry) as needed. Returns the number written.
|
|
pub fn writeFile(self: *FileSystem, node: *Node, offset: u32, data: []const u8) usize {
|
|
if (data.len == 0) return 0;
|
|
const cluster_bytes = self.geometry.sectors_per_cluster * sector_size;
|
|
|
|
// Ensure the file has a first cluster.
|
|
if (node.first_cluster < 2) {
|
|
const fresh = self.allocateCluster() orelse return 0;
|
|
self.zeroCluster(fresh);
|
|
node.first_cluster = fresh;
|
|
}
|
|
|
|
var consumed: usize = 0;
|
|
var position = offset;
|
|
while (consumed < data.len) {
|
|
const cluster = self.clusterAt(node.first_cluster, position, true) orelse break;
|
|
const in_cluster = position % cluster_bytes;
|
|
const sector_in_cluster = in_cluster / sector_size;
|
|
const in_sector = in_cluster % sector_size;
|
|
const lba = self.clusterSector(cluster, sector_in_cluster);
|
|
// Read-modify-write the sector for a partial write.
|
|
if (!self.blockRead(lba, &self.sector)) break;
|
|
const n = @min(data.len - consumed, sector_size - in_sector);
|
|
@memcpy(self.sector[in_sector .. in_sector + n], data[consumed .. consumed + n]);
|
|
if (!self.blockWrite(lba, &self.sector)) break;
|
|
consumed += n;
|
|
position += @intCast(n);
|
|
}
|
|
|
|
const new_end = offset + @as(u32, @intCast(consumed));
|
|
if (new_end > node.size) node.size = new_end;
|
|
self.updateEntry(node.*);
|
|
return consumed;
|
|
}
|
|
|
|
/// Truncate a file node to zero length: free its cluster chain and clear its
|
|
/// size and first cluster in the directory entry. This is O_TRUNC — the fix for
|
|
/// re-opening and overwriting an existing file, whose old (longer) contents
|
|
/// would otherwise linger past the new end (a silent-corruption bug for anything
|
|
/// that rewrites a file in place, like the boot-log flush).
|
|
pub fn truncate(self: *FileSystem, node: *Node) void {
|
|
self.freeChain(node.first_cluster);
|
|
node.first_cluster = 0;
|
|
node.size = 0;
|
|
self.updateEntry(node.*);
|
|
}
|
|
|
|
// Write a node's size and first cluster back into its 8.3 directory entry.
|
|
fn updateEntry(self: *FileSystem, node: Node) void {
|
|
if (!node.has_entry) return;
|
|
if (!self.blockRead(node.entry_sector, &self.dir_sector)) return;
|
|
var entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)]);
|
|
entry.file_size = node.size;
|
|
entry.setFirstCluster(node.first_cluster);
|
|
// A write updates the modification time (leave it if no time is set, so host
|
|
// tests and reads don't zero it).
|
|
if (self.current_time_epoch != 0) {
|
|
const stamp = on_disk.epochToFatDateTime(self.current_time_epoch);
|
|
entry.write_date = stamp.date;
|
|
entry.write_time = stamp.time;
|
|
entry.last_access_date = stamp.date;
|
|
}
|
|
@memcpy(self.dir_sector[node.entry_offset .. node.entry_offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
|
|
_ = self.blockWrite(node.entry_sector, &self.dir_sector);
|
|
}
|
|
|
|
// --- long-name creation --------------------------------------------------
|
|
|
|
// The standard 8.3 short-name checksum carried by every long-name entry.
|
|
fn shortChecksum(raw: [11]u8) u8 {
|
|
var sum: u8 = 0;
|
|
for (raw) |c| sum = ((sum & 1) << 7) +% (sum >> 1) +% c;
|
|
return sum;
|
|
}
|
|
|
|
fn valid83Char(c: u8) bool {
|
|
return (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '-' or c == '_';
|
|
}
|
|
|
|
// Whether an 8.3 entry with exactly this raw name exists in `dir`.
|
|
const RawContext = struct { raw: [11]u8, found: *bool };
|
|
fn rawVisit(context: *const RawContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
|
|
_ = name;
|
|
_ = entry_sector;
|
|
_ = entry_offset;
|
|
if (std.mem.eql(u8, &entry.name, &context.raw)) {
|
|
context.found.* = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn shortNameExists(self: *FileSystem, dir: Node, raw: [11]u8) bool {
|
|
var found = false;
|
|
var context = RawContext{ .raw = raw, .found = &found };
|
|
self.scanDirectory(dir, &context, rawVisit);
|
|
return found;
|
|
}
|
|
|
|
// A mangled STEM~N.EXT short name that collides with nothing in `dir` — the
|
|
// alias behind a long-name chain.
|
|
fn shortNameFor(self: *FileSystem, dir: Node, name: []const u8) ?[11]u8 {
|
|
const dot = std.mem.lastIndexOfScalar(u8, name, '.');
|
|
const base = if (dot) |d| name[0..d] else name;
|
|
const ext = if (dot) |d| name[d + 1 ..] else name[0..0];
|
|
|
|
var stem: [6]u8 = undefined;
|
|
var stem_len: usize = 0;
|
|
for (base) |c| {
|
|
if (stem_len == stem.len) break;
|
|
const upper = std.ascii.toUpper(c);
|
|
if (valid83Char(upper)) {
|
|
stem[stem_len] = upper;
|
|
stem_len += 1;
|
|
}
|
|
}
|
|
if (stem_len == 0) {
|
|
stem[0] = 'X';
|
|
stem_len = 1;
|
|
}
|
|
|
|
var raw = [_]u8{' '} ** 11;
|
|
var ext_len: usize = 0;
|
|
for (ext) |c| {
|
|
if (ext_len == 3) break;
|
|
const upper = std.ascii.toUpper(c);
|
|
if (valid83Char(upper)) {
|
|
raw[8 + ext_len] = upper;
|
|
ext_len += 1;
|
|
}
|
|
}
|
|
|
|
var index: u32 = 1;
|
|
while (index <= 999_999) : (index += 1) {
|
|
var tail_buffer: [8]u8 = undefined;
|
|
const tail = std.fmt.bufPrint(&tail_buffer, "~{d}", .{index}) catch return null;
|
|
const keep = @min(stem_len, 8 - tail.len);
|
|
@memset(raw[0..8], ' ');
|
|
@memcpy(raw[0..keep], stem[0..keep]);
|
|
@memcpy(raw[keep .. keep + tail.len], tail);
|
|
if (!self.shortNameExists(dir, raw)) return raw;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Fill one long-name entry's 13 UTF-16 slots from `name` starting at
|
|
// `offset`: the name's bytes widened, then a 0x0000 terminator, then 0xFFFF.
|
|
fn fillLongNamePiece(lfn: *on_disk.LongNameEntry, name: []const u8, offset: usize) void {
|
|
var units: [13]u16 = undefined;
|
|
var i: usize = 0;
|
|
while (i < 13) : (i += 1) {
|
|
const at = offset + i;
|
|
units[i] = if (at < name.len) name[at] else if (at == name.len) 0x0000 else 0xFFFF;
|
|
}
|
|
lfn.name1 = units[0..5].*;
|
|
lfn.name2 = units[5..11].*;
|
|
lfn.name3 = units[11..13].*;
|
|
}
|
|
|
|
// The first entry index of a run of `count` free slots in `dir`, growing the
|
|
// directory as needed. Fresh clusters are zeroed, so growth always yields
|
|
// free slots; only the fixed FAT12/16 root can genuinely run out.
|
|
fn findFreeRun(self: *FileSystem, dir: Node, count: usize) ?u32 {
|
|
var run_start: u32 = 0;
|
|
var run_len: usize = 0;
|
|
var sector_index: u32 = 0;
|
|
while (self.dirSectorLba(dir, sector_index, true)) |lba| : (sector_index += 1) {
|
|
if (!self.blockRead(lba, &self.dir_sector)) return null;
|
|
var i: u32 = 0;
|
|
while (i < entries_per_sector) : (i += 1) {
|
|
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
|
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
|
if (entry.isFree()) {
|
|
if (run_len == 0) run_start = sector_index * entries_per_sector + i;
|
|
run_len += 1;
|
|
if (run_len == count) return run_start;
|
|
} else {
|
|
run_len = 0;
|
|
}
|
|
}
|
|
if (sector_index > 4096) return null; // runaway guard
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Write one 32-byte directory entry at a global entry index (read-modify-
|
|
// write of its sector). Returns the entry's (sector, offset) or null.
|
|
fn writeEntryAt(self: *FileSystem, dir: Node, index: u32, bytes: *const [32]u8) ?EntryLoc {
|
|
const lba = self.dirSectorLba(dir, index / entries_per_sector, true) orelse return null;
|
|
if (!self.blockRead(lba, &self.dir_sector)) return null;
|
|
const offset = (index % entries_per_sector) * @sizeOf(on_disk.DirectoryEntry);
|
|
@memcpy(self.dir_sector[offset .. offset + 32], bytes);
|
|
if (!self.blockWrite(lba, &self.dir_sector)) return null;
|
|
return .{ .sector = lba, .offset = offset };
|
|
}
|
|
|
|
// Add a named directory entry, creating a long-name chain when the name is
|
|
// not its own 8.3 form. Write order is LFN pieces first, 8.3 entry last: an
|
|
// interrupted create leaves orphaned long-name entries, which every FAT
|
|
// reader (this engine's scanner included) skips as unattached — never a
|
|
// mismatched chain.
|
|
fn addEntryNamed(self: *FileSystem, dir: Node, name: []const u8, attributes: u8, first_cluster: u32, size: u32) ?Node {
|
|
if (to83(name)) |raw| {
|
|
var display: [12]u8 = undefined;
|
|
// Only a name that IS its 8.3 form (already uppercase) skips the
|
|
// chain — a lowercase name gets one so its exact case survives,
|
|
// matching tools/make-fat-image.py.
|
|
if (std.mem.eql(u8, format83(raw, &display), name))
|
|
return self.addEntry(dir, raw, attributes, first_cluster, size);
|
|
}
|
|
if (name.len == 0 or name.len > 255) return null;
|
|
|
|
const raw = self.shortNameFor(dir, name) orelse return null;
|
|
const checksum = shortChecksum(raw);
|
|
const piece_count: u32 = @intCast((name.len + 12) / 13);
|
|
if (piece_count > 20) return null;
|
|
const start = self.findFreeRun(dir, piece_count + 1) orelse return null;
|
|
|
|
var k: u32 = 0;
|
|
while (k < piece_count) : (k += 1) {
|
|
const piece = piece_count - k; // stored last-logical-first
|
|
var lfn = std.mem.zeroes(on_disk.LongNameEntry);
|
|
lfn.order = @intCast(piece | (if (k == 0) @as(u8, 0x40) else 0));
|
|
lfn.attributes = on_disk.attribute_long_name;
|
|
lfn.checksum = checksum;
|
|
fillLongNamePiece(&lfn, name, (piece - 1) * 13);
|
|
_ = self.writeEntryAt(dir, start + k, std.mem.asBytes(&lfn)[0..32]) orelse return null;
|
|
}
|
|
|
|
var entry = std.mem.zeroes(on_disk.DirectoryEntry);
|
|
entry.name = raw;
|
|
entry.attributes = attributes;
|
|
entry.file_size = size;
|
|
entry.setFirstCluster(first_cluster);
|
|
const stamp = on_disk.epochToFatDateTime(self.current_time_epoch);
|
|
entry.creation_date = stamp.date;
|
|
entry.creation_time = stamp.time;
|
|
entry.write_date = stamp.date;
|
|
entry.write_time = stamp.time;
|
|
entry.last_access_date = stamp.date;
|
|
const location = self.writeEntryAt(dir, start + piece_count, std.mem.asBytes(&entry)[0..32]) orelse return null;
|
|
return .{
|
|
.first_cluster = first_cluster,
|
|
.size = size,
|
|
.is_directory = attributes & on_disk.attribute_directory != 0,
|
|
.mtime = self.current_time_epoch,
|
|
.entry_sector = location.sector,
|
|
.entry_offset = location.offset,
|
|
.has_entry = true,
|
|
};
|
|
}
|
|
|
|
// Add an 8.3 directory entry to `dir` with the given attributes, first cluster,
|
|
// and size, reusing a free (0x00 or 0xE5) slot and growing the directory chain
|
|
// if needed. Returns the new node (with its entry location) or null if full.
|
|
fn addEntry(self: *FileSystem, dir: Node, raw: [11]u8, attributes: u8, first_cluster: u32, size: u32) ?Node {
|
|
var sector_index: u32 = 0;
|
|
while (self.dirSectorLba(dir, sector_index, true)) |lba| : (sector_index += 1) {
|
|
if (!self.blockRead(lba, &self.dir_sector)) return null;
|
|
var i: u32 = 0;
|
|
while (i < entries_per_sector) : (i += 1) {
|
|
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
|
const existing = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
|
if (existing.isFree()) {
|
|
var entry = std.mem.zeroes(on_disk.DirectoryEntry);
|
|
entry.name = raw;
|
|
entry.attributes = attributes;
|
|
entry.file_size = size;
|
|
entry.setFirstCluster(first_cluster);
|
|
const stamp = on_disk.epochToFatDateTime(self.current_time_epoch);
|
|
entry.creation_date = stamp.date;
|
|
entry.creation_time = stamp.time;
|
|
entry.write_date = stamp.date;
|
|
entry.write_time = stamp.time;
|
|
entry.last_access_date = stamp.date;
|
|
@memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&entry));
|
|
if (!self.blockWrite(lba, &self.dir_sector)) return null;
|
|
return .{
|
|
.first_cluster = first_cluster,
|
|
.size = size,
|
|
.is_directory = attributes & on_disk.attribute_directory != 0,
|
|
.mtime = self.current_time_epoch,
|
|
.entry_sector = lba,
|
|
.entry_offset = offset,
|
|
.has_entry = true,
|
|
};
|
|
}
|
|
}
|
|
// Only the fixed root can run out (it can't grow); a chain grows above.
|
|
if (sector_index > 4096) return null; // runaway guard
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Create a file in directory `dir`. Uppercase 8.3 names get a bare short
|
|
/// entry; anything else gets a long-name chain over a mangled ~N alias.
|
|
/// Returns the new (empty) node, or null (bad name / directory full /
|
|
/// duplicate — the caller checks existence first if it must distinguish).
|
|
pub fn createFile(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
|
return self.addEntryNamed(dir, name, on_disk.attribute_archive, 0, 0);
|
|
}
|
|
|
|
/// Create a subdirectory in `dir`: allocate and initialise its first
|
|
/// cluster with "." (itself) and ".." (the parent) entries, then add its
|
|
/// directory entry to `dir` (long-name chain when the name needs one).
|
|
/// Returns the new directory node, or null (bad name, no free cluster, or
|
|
/// the directory is full).
|
|
pub fn createDirectory(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
|
const cluster = self.allocateCluster() orelse return null;
|
|
self.zeroCluster(cluster);
|
|
|
|
// ".." points at the parent: 0 for the fixed root on FAT12/16, the root
|
|
// cluster on FAT32, else the parent's own first cluster.
|
|
const parent_cluster: u32 = if (dir.first_cluster != 0)
|
|
dir.first_cluster
|
|
else if (self.geometry.fat_type == .fat32)
|
|
self.geometry.root_cluster
|
|
else
|
|
0;
|
|
var dot = std.mem.zeroes(on_disk.DirectoryEntry);
|
|
dot.name = [_]u8{'.'} ++ ([_]u8{' '} ** 10);
|
|
dot.attributes = on_disk.attribute_directory;
|
|
dot.setFirstCluster(cluster);
|
|
var dotdot = std.mem.zeroes(on_disk.DirectoryEntry);
|
|
dotdot.name = [_]u8{ '.', '.' } ++ ([_]u8{' '} ** 9);
|
|
dotdot.attributes = on_disk.attribute_directory;
|
|
dotdot.setFirstCluster(parent_cluster);
|
|
var first_sector = [_]u8{0} ** sector_size;
|
|
const entry_size = @sizeOf(on_disk.DirectoryEntry);
|
|
@memcpy(first_sector[0..entry_size], std.mem.asBytes(&dot));
|
|
@memcpy(first_sector[entry_size .. 2 * entry_size], std.mem.asBytes(&dotdot));
|
|
if (!self.blockWrite(self.clusterSector(cluster, 0), &first_sector)) {
|
|
self.freeChain(cluster);
|
|
return null;
|
|
}
|
|
return self.addEntryNamed(dir, name, on_disk.attribute_directory, cluster, 0) orelse {
|
|
self.freeChain(cluster);
|
|
return null;
|
|
};
|
|
}
|
|
|
|
const EntryLoc = struct { sector: u64, offset: u32 };
|
|
|
|
// Mark a directory entry deleted in place (its name[0] set to 0xE5).
|
|
fn markDeleted(self: *FileSystem, sector: u64, offset: u32) void {
|
|
if (!self.blockRead(sector, &self.dir_sector)) return;
|
|
self.dir_sector[offset] = 0xE5;
|
|
_ = self.blockWrite(sector, &self.dir_sector);
|
|
}
|
|
|
|
/// Remove a file named `name` from directory `dir`: free its cluster chain and
|
|
/// mark its 8.3 entry — and any long-name entries immediately preceding it —
|
|
/// deleted, so the slots (and the long name) are reusable without a later entry
|
|
/// that reuses them inheriting the orphaned long name. Refuses a directory (a
|
|
/// separate rmdir would have to check emptiness). Returns true if removed.
|
|
pub fn removeFile(self: *FileSystem, dir: Node, name: []const u8) bool {
|
|
var run: [21]EntryLoc = undefined; // the long-name entries before the 8.3 one
|
|
var run_len: usize = 0;
|
|
var long_name: [260]u8 = undefined;
|
|
var long_len: usize = 0;
|
|
var sector_index: u32 = 0;
|
|
while (self.dirSectorLba(dir, sector_index, false)) |lba| : (sector_index += 1) {
|
|
if (!self.blockRead(lba, &self.dir_sector)) return false;
|
|
var i: u32 = 0;
|
|
while (i < entries_per_sector) : (i += 1) {
|
|
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
|
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
|
if (entry.isEnd()) return false;
|
|
if (entry.name[0] == 0xE5) {
|
|
run_len = 0;
|
|
long_len = 0;
|
|
continue;
|
|
}
|
|
if (entry.isLongName()) {
|
|
if (run_len < run.len) {
|
|
run[run_len] = .{ .sector = lba, .offset = offset };
|
|
run_len += 1;
|
|
}
|
|
const lfn = std.mem.bytesToValue(on_disk.LongNameEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.LongNameEntry)]);
|
|
const order = lfn.order & 0x1F;
|
|
if (order >= 1 and order <= 20) {
|
|
var chunk: [13]u8 = undefined;
|
|
const n = longNameChars(lfn, &chunk);
|
|
const start = (order - 1) * 13;
|
|
if (start + n <= long_name.len) {
|
|
@memcpy(long_name[start .. start + n], chunk[0..n]);
|
|
if (lfn.order & 0x40 != 0) long_len = start + n;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isVolumeLabel()) {
|
|
run_len = 0;
|
|
long_len = 0;
|
|
continue;
|
|
}
|
|
// A real 8.3 entry.
|
|
var short: [12]u8 = undefined;
|
|
const display = if (long_len > 0) long_name[0..long_len] else format83(entry.name, &short);
|
|
if (nameMatches(display, name)) {
|
|
if (entry.isDirectory()) return false; // not for directories
|
|
self.freeChain(entry.firstCluster());
|
|
self.markDeleted(lba, offset);
|
|
var r: usize = 0;
|
|
while (r < run_len) : (r += 1) self.markDeleted(run[r].sector, run[r].offset);
|
|
return true;
|
|
}
|
|
run_len = 0;
|
|
long_len = 0;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Rename `old_name` to `new_name` within the SAME directory `dir`, rewriting
|
|
/// the 8.3 entry's name in place. Refuses if `old_name` is missing, `new_name`
|
|
/// is not 8.3-representable, or `new_name` already exists. Any long-name entries
|
|
/// on the old file are dropped (the file takes its new 8.3 name); cross-directory
|
|
/// and long-name-preserving rename are not implemented. Returns true on success.
|
|
pub fn rename(self: *FileSystem, dir: Node, old_name: []const u8, new_name: []const u8) bool {
|
|
const raw = to83(new_name) orelse return false;
|
|
if (self.findChild(dir, new_name) != null) return false; // target already exists
|
|
|
|
var run: [21]EntryLoc = undefined; // the long-name entries before the 8.3 one
|
|
var run_len: usize = 0;
|
|
var long_name: [260]u8 = undefined;
|
|
var long_len: usize = 0;
|
|
var sector_index: u32 = 0;
|
|
while (self.dirSectorLba(dir, sector_index, false)) |lba| : (sector_index += 1) {
|
|
if (!self.blockRead(lba, &self.dir_sector)) return false;
|
|
var i: u32 = 0;
|
|
while (i < entries_per_sector) : (i += 1) {
|
|
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
|
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
|
if (entry.isEnd()) return false;
|
|
if (entry.name[0] == 0xE5) {
|
|
run_len = 0;
|
|
long_len = 0;
|
|
continue;
|
|
}
|
|
if (entry.isLongName()) {
|
|
if (run_len < run.len) {
|
|
run[run_len] = .{ .sector = lba, .offset = offset };
|
|
run_len += 1;
|
|
}
|
|
const lfn = std.mem.bytesToValue(on_disk.LongNameEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.LongNameEntry)]);
|
|
const order = lfn.order & 0x1F;
|
|
if (order >= 1 and order <= 20) {
|
|
var chunk: [13]u8 = undefined;
|
|
const n = longNameChars(lfn, &chunk);
|
|
const start = (order - 1) * 13;
|
|
if (start + n <= long_name.len) {
|
|
@memcpy(long_name[start .. start + n], chunk[0..n]);
|
|
if (lfn.order & 0x40 != 0) long_len = start + n;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isVolumeLabel()) {
|
|
run_len = 0;
|
|
long_len = 0;
|
|
continue;
|
|
}
|
|
// A real 8.3 entry.
|
|
var short: [12]u8 = undefined;
|
|
const display = if (long_len > 0) long_name[0..long_len] else format83(entry.name, &short);
|
|
if (nameMatches(display, old_name)) {
|
|
var updated = entry;
|
|
updated.name = raw;
|
|
@memcpy(self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)], std.mem.asBytes(&updated));
|
|
if (!self.blockWrite(lba, &self.dir_sector)) return false;
|
|
// Drop the old long name, if any, so the new 8.3 name is what shows.
|
|
var r: usize = 0;
|
|
while (r < run_len) : (r += 1) self.markDeleted(run[r].sector, run[r].offset);
|
|
return true;
|
|
}
|
|
run_len = 0;
|
|
long_len = 0;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// --- tests: a RAM-backed FAT16 image ----------------------------------------
|
|
|
|
const RamDisk = struct {
|
|
bytes: []u8,
|
|
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
|
|
const self: *RamDisk = @ptrCast(@alignCast(context));
|
|
const start = lba * sector_size;
|
|
if (start + sector_size > self.bytes.len) return false;
|
|
@memcpy(buffer[0..sector_size], self.bytes[start .. start + sector_size]);
|
|
return true;
|
|
}
|
|
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
|
|
const self: *RamDisk = @ptrCast(@alignCast(context));
|
|
const start = lba * sector_size;
|
|
if (start + sector_size > self.bytes.len) return false;
|
|
@memcpy(self.bytes[start .. start + sector_size], buffer[0..sector_size]);
|
|
return true;
|
|
}
|
|
fn device(self: *RamDisk) BlockDevice {
|
|
return .{
|
|
.context = self,
|
|
.block_size = sector_size,
|
|
.block_count = self.bytes.len / sector_size,
|
|
.readBlockFn = readBlock,
|
|
.writeBlockFn = writeBlock,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Format a minimal FAT16 volume into `bytes`: BPB + boot signature, FATs with the
|
|
// two reserved entries, an empty root directory. Enough for the engine to mount
|
|
// and operate on.
|
|
fn formatFat16(bytes: []u8) void {
|
|
@memset(bytes, 0);
|
|
const total_sectors: u16 = @intCast(bytes.len / sector_size);
|
|
var bpb = std.mem.zeroes(on_disk.BiosParameterBlock);
|
|
bpb.jump = .{ 0xEB, 0x3C, 0x90 };
|
|
bpb.oem_name = "MSWIN4.1".*;
|
|
bpb.bytes_per_sector = sector_size;
|
|
bpb.sectors_per_cluster = 1;
|
|
bpb.reserved_sector_count = 1;
|
|
bpb.fat_count = 2;
|
|
bpb.root_entry_count = 512;
|
|
bpb.total_sectors_16 = total_sectors;
|
|
bpb.media = 0xF8;
|
|
bpb.fat_size_16 = 16; // 16 sectors per FAT (covers ~4000 FAT16 entries)
|
|
@memcpy(bytes[0..@sizeOf(on_disk.BiosParameterBlock)], std.mem.asBytes(&bpb));
|
|
bytes[on_disk.boot_signature_offset] = 0x55;
|
|
bytes[on_disk.boot_signature_offset + 1] = 0xAA;
|
|
// FAT reserved entries: entry0 = media in low byte + 0xFF, entry1 = EOC.
|
|
const fat0 = 1 * sector_size;
|
|
bytes[fat0] = 0xF8;
|
|
bytes[fat0 + 1] = 0xFF;
|
|
bytes[fat0 + 2] = 0xFF;
|
|
bytes[fat0 + 3] = 0xFF;
|
|
const fat1 = fat0 + 16 * sector_size;
|
|
bytes[fat1] = 0xF8;
|
|
bytes[fat1 + 1] = 0xFF;
|
|
bytes[fat1 + 2] = 0xFF;
|
|
bytes[fat1 + 3] = 0xFF;
|
|
}
|
|
|
|
test "mount a formatted FAT16 image" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size); // ~2.4 MB
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
try std.testing.expectEqual(on_disk.FatType.fat16, fs.geometry.fat_type);
|
|
try std.testing.expect(fs.geometry.cluster_count >= 4085);
|
|
|
|
// An empty root directory lists nothing.
|
|
try std.testing.expect(fs.listEntry(fs.rootNode(), 0) == null);
|
|
}
|
|
|
|
test "create, write, read back a file through the engine" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
// Create /HELLO.TXT and write a payload larger than one sector (spans clusters).
|
|
var node = fs.createFile(fs.rootNode(), "HELLO.TXT").?;
|
|
var payload: [1500]u8 = undefined;
|
|
for (&payload, 0..) |*b, i| b.* = @truncate(i);
|
|
const written = fs.writeFile(&node, 0, &payload);
|
|
try std.testing.expectEqual(@as(usize, payload.len), written);
|
|
|
|
// Re-resolve from the directory (proving the entry was persisted) and read back.
|
|
const resolved = fs.resolve("/HELLO.TXT").?;
|
|
try std.testing.expectEqual(@as(u32, payload.len), resolved.size);
|
|
var readback: [1500]u8 = undefined;
|
|
const got = fs.readFile(resolved, 0, &readback);
|
|
try std.testing.expectEqual(@as(usize, payload.len), got);
|
|
try std.testing.expectEqualSlices(u8, &payload, &readback);
|
|
|
|
// A mid-file overwrite is visible on re-read.
|
|
var patch = [_]u8{0xAB} ** 4;
|
|
_ = fs.writeFile(&node, 600, &patch);
|
|
const patched = fs.resolve("/HELLO.TXT").?;
|
|
_ = fs.readFile(patched, 600, readback[0..4]);
|
|
try std.testing.expectEqualSlices(u8, &patch, readback[0..4]);
|
|
|
|
// The root now lists exactly HELLO.TXT.
|
|
const listing = fs.listEntry(fs.rootNode(), 0).?;
|
|
try std.testing.expectEqualStrings("HELLO.TXT", listing.name_buffer[0..listing.name_len]);
|
|
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
|
|
}
|
|
|
|
test "truncate frees the chain and zeroes the file" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
var node = fs.createFile(fs.rootNode(), "BIG.BIN").?;
|
|
var payload: [2000]u8 = undefined;
|
|
for (&payload, 0..) |*b, i| b.* = @truncate(i);
|
|
_ = fs.writeFile(&node, 0, &payload);
|
|
const cluster = node.first_cluster;
|
|
try std.testing.expect(cluster >= 2);
|
|
|
|
fs.truncate(&node);
|
|
try std.testing.expectEqual(@as(u32, 0), node.size);
|
|
try std.testing.expectEqual(@as(u32, 0), node.first_cluster);
|
|
// The old first cluster is free again.
|
|
try std.testing.expectEqual(on_disk.free_cluster, fs.readFatEntry(cluster));
|
|
|
|
// Re-resolve: the persisted entry is empty, and reads produce nothing.
|
|
const resolved = fs.resolve("/BIG.BIN").?;
|
|
try std.testing.expectEqual(@as(u32, 0), resolved.size);
|
|
var buf: [16]u8 = undefined;
|
|
try std.testing.expectEqual(@as(usize, 0), fs.readFile(resolved, 0, &buf));
|
|
}
|
|
|
|
test "overwrite after truncate leaves no stale tail (the O_TRUNC corruption fix)" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
// Write a long file, then truncate-and-rewrite a short one — the O_TRUNC flow.
|
|
var node = fs.createFile(fs.rootNode(), "LOG.TXT").?;
|
|
_ = fs.writeFile(&node, 0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // 32 bytes
|
|
fs.truncate(&node);
|
|
_ = fs.writeFile(&node, 0, "bb");
|
|
|
|
// Size is the short length — no lingering old bytes past the new end.
|
|
const resolved = fs.resolve("/LOG.TXT").?;
|
|
try std.testing.expectEqual(@as(u32, 2), resolved.size);
|
|
var buf: [8]u8 = undefined;
|
|
const n = fs.readFile(resolved, 0, &buf);
|
|
try std.testing.expectEqualStrings("bb", buf[0..n]);
|
|
}
|
|
|
|
test "remove a file frees its slot and its cluster chain" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
var node = fs.createFile(fs.rootNode(), "GONE.TXT").?;
|
|
var payload: [1000]u8 = undefined;
|
|
for (&payload, 0..) |*b, i| b.* = @truncate(i);
|
|
_ = fs.writeFile(&node, 0, &payload);
|
|
const cluster = fs.resolve("/GONE.TXT").?.first_cluster;
|
|
try std.testing.expect(cluster >= 2);
|
|
|
|
try std.testing.expect(fs.removeFile(fs.rootNode(), "GONE.TXT"));
|
|
// Gone from the directory, its cluster free, root empty again.
|
|
try std.testing.expect(fs.resolve("/GONE.TXT") == null);
|
|
try std.testing.expectEqual(on_disk.free_cluster, fs.readFatEntry(cluster));
|
|
try std.testing.expect(fs.listEntry(fs.rootNode(), 0) == null);
|
|
// Removing a missing file reports false.
|
|
try std.testing.expect(!fs.removeFile(fs.rootNode(), "GONE.TXT"));
|
|
// A directory is refused (it is not a file).
|
|
_ = fs.createDirectory(fs.rootNode(), "ADIR").?;
|
|
try std.testing.expect(!fs.removeFile(fs.rootNode(), "ADIR"));
|
|
}
|
|
|
|
test "create a subdirectory with . and .. and a file inside" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
const made = fs.createDirectory(fs.rootNode(), "SUB").?;
|
|
try std.testing.expect(made.is_directory);
|
|
try std.testing.expect(made.first_cluster >= 2);
|
|
|
|
// It resolves as a directory, with "." and ".." as its first two entries.
|
|
const dir = fs.resolve("/SUB").?;
|
|
try std.testing.expect(dir.is_directory);
|
|
const dot = fs.listEntry(dir, 0).?;
|
|
try std.testing.expectEqualStrings(".", dot.name_buffer[0..dot.name_len]);
|
|
const dotdot = fs.listEntry(dir, 1).?;
|
|
try std.testing.expectEqualStrings("..", dotdot.name_buffer[0..dotdot.name_len]);
|
|
|
|
// A file created inside is reachable by its full path.
|
|
var child = fs.createFile(dir, "INNER.TXT").?;
|
|
_ = fs.writeFile(&child, 0, "hi");
|
|
const inner = fs.resolve("/SUB/INNER.TXT").?;
|
|
try std.testing.expectEqual(@as(u32, 2), inner.size);
|
|
|
|
// The root lists SUB as a directory.
|
|
const listing = fs.listEntry(fs.rootNode(), 0).?;
|
|
try std.testing.expectEqualStrings("SUB", listing.name_buffer[0..listing.name_len]);
|
|
try std.testing.expect(listing.is_directory);
|
|
}
|
|
|
|
test "rename a file in place, keeping its contents" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
var node = fs.createFile(fs.rootNode(), "OLD.TXT").?;
|
|
_ = fs.writeFile(&node, 0, "content");
|
|
|
|
try std.testing.expect(fs.rename(fs.rootNode(), "OLD.TXT", "NEW.TXT"));
|
|
try std.testing.expect(fs.resolve("/OLD.TXT") == null);
|
|
const renamed = fs.resolve("/NEW.TXT").?;
|
|
var buf: [16]u8 = undefined;
|
|
const n = fs.readFile(renamed, 0, &buf);
|
|
try std.testing.expectEqualStrings("content", buf[0..n]);
|
|
|
|
// Refuse a collision with an existing name.
|
|
_ = fs.createFile(fs.rootNode(), "OTHER.TXT").?;
|
|
try std.testing.expect(!fs.rename(fs.rootNode(), "NEW.TXT", "OTHER.TXT"));
|
|
// Refuse a non-8.3 target name.
|
|
try std.testing.expect(!fs.rename(fs.rootNode(), "NEW.TXT", "toolongbasename.txt"));
|
|
// Refuse a missing source.
|
|
try std.testing.expect(!fs.rename(fs.rootNode(), "NOPE.TXT", "X.TXT"));
|
|
// After the refused renames, NEW.TXT is untouched.
|
|
try std.testing.expect(fs.resolve("/NEW.TXT") != null);
|
|
}
|
|
|
|
test "a create stamps the modification time" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
fs.current_time_epoch = 1_700_000_000; // an even-second UTC time
|
|
var node = fs.createFile(fs.rootNode(), "STAMP.TXT").?;
|
|
_ = fs.writeFile(&node, 0, "hi");
|
|
|
|
// The persisted entry carries the stamped mtime (even seconds round-trip exactly),
|
|
// as does a fresh listing.
|
|
try std.testing.expectEqual(@as(u64, 1_700_000_000), fs.resolve("/STAMP.TXT").?.mtime);
|
|
try std.testing.expectEqual(@as(u64, 1_700_000_000), fs.listEntry(fs.rootNode(), 0).?.mtime);
|
|
}
|
|
|
|
test "long-name create: directory + file round-trip by long name" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
// The per-boot log directory shape: an 18-char stamp, nested paths, .log names.
|
|
const stamp_dir = fs.createDirectory(fs.rootNode(), "2026-07-21T101530Z").?;
|
|
try std.testing.expect(stamp_dir.is_directory);
|
|
const file = fs.createFile(stamp_dir, "device-manager.log").?;
|
|
_ = file;
|
|
|
|
// Resolve by exact long name, and case-insensitively (FAT semantics).
|
|
try std.testing.expect(fs.resolve("/2026-07-21T101530Z/device-manager.log") != null);
|
|
try std.testing.expect(fs.resolve("/2026-07-21t101530z/DEVICE-MANAGER.LOG") != null);
|
|
|
|
// The listing shows the long names, not the ~N aliases.
|
|
var listing = fs.listEntry(fs.rootNode(), 0).?;
|
|
try std.testing.expectEqualStrings("2026-07-21T101530Z", listing.name_buffer[0..listing.name_len]);
|
|
var inner = fs.listEntry(stamp_dir, 2).?; // after "." and ".."
|
|
try std.testing.expectEqualStrings("device-manager.log", inner.name_buffer[0..inner.name_len]);
|
|
|
|
// Write through the created file and read it back by long-name resolve.
|
|
var node = fs.resolve("/2026-07-21T101530Z/device-manager.log").?;
|
|
try std.testing.expectEqual(@as(usize, 10), fs.writeFile(&node, 0, "hello logs"));
|
|
var buffer: [16]u8 = undefined;
|
|
try std.testing.expectEqual(@as(usize, 10), fs.readFile(node, 0, buffer[0..10]));
|
|
try std.testing.expectEqualStrings("hello logs", buffer[0..10]);
|
|
}
|
|
|
|
test "long-name create: ~N alias collision suffixes stay distinct" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
_ = fs.createFile(fs.rootNode(), "logger-alpha.log").?;
|
|
_ = fs.createFile(fs.rootNode(), "logger-beta.log").?;
|
|
// Same 6-char mangle stem (LOGGER) — the second must take ~2.
|
|
var raw_one = false;
|
|
var raw_two = false;
|
|
var cursor: u32 = 0;
|
|
while (fs.listEntry(fs.rootNode(), cursor)) |entry| : (cursor += 1) {
|
|
if (std.mem.eql(u8, entry.name_buffer[0..entry.name_len], "logger-alpha.log")) raw_one = true;
|
|
if (std.mem.eql(u8, entry.name_buffer[0..entry.name_len], "logger-beta.log")) raw_two = true;
|
|
}
|
|
try std.testing.expect(raw_one and raw_two);
|
|
try std.testing.expect(fs.resolve("/logger-alpha.log") != null);
|
|
try std.testing.expect(fs.resolve("/logger-beta.log") != null);
|
|
// Their short aliases took distinct ~N tails. (Alias LOOKUP is not a
|
|
// feature — findChild matches display names — but the on-disk aliases
|
|
// must not collide for other FAT readers.)
|
|
try std.testing.expect(fs.shortNameExists(fs.rootNode(), "LOGGER~1LOG".*));
|
|
try std.testing.expect(fs.shortNameExists(fs.rootNode(), "LOGGER~2LOG".*));
|
|
}
|
|
|
|
test "long-name create: unlink removes the chain; slots are reused cleanly" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
_ = fs.createFile(fs.rootNode(), "a-rather-long-file-name.txt").?;
|
|
try std.testing.expect(fs.removeFile(fs.rootNode(), "a-rather-long-file-name.txt"));
|
|
try std.testing.expect(fs.resolve("/a-rather-long-file-name.txt") == null);
|
|
|
|
// A new long name reuses the freed run without inheriting the old chain.
|
|
_ = fs.createFile(fs.rootNode(), "an-entirely-different-name.md").?;
|
|
try std.testing.expect(fs.resolve("/an-entirely-different-name.md") != null);
|
|
try std.testing.expect(fs.resolve("/a-rather-long-file-name.txt") == null);
|
|
var listing = fs.listEntry(fs.rootNode(), 0).?;
|
|
try std.testing.expectEqualStrings("an-entirely-different-name.md", listing.name_buffer[0..listing.name_len]);
|
|
}
|
|
|
|
test "8.3 fast path: an uppercase-compliant name gets one bare entry" {
|
|
const allocator = std.testing.allocator;
|
|
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
|
defer allocator.free(bytes);
|
|
formatFat16(bytes);
|
|
var disk = RamDisk{ .bytes = bytes };
|
|
var fs = FileSystem.mount(disk.device()).?;
|
|
|
|
_ = fs.createFile(fs.rootNode(), "DANOS.LOG").?;
|
|
// Exactly one directory entry: entry 0 is the file, entry 1 is the end.
|
|
var listing = fs.listEntry(fs.rootNode(), 0).?;
|
|
try std.testing.expectEqualStrings("DANOS.LOG", listing.name_buffer[0..listing.name_len]);
|
|
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
|
|
// A lowercase 8.3-shaped name is case-preserved via a chain instead.
|
|
_ = fs.createFile(fs.rootNode(), "fat.log").?;
|
|
var second = fs.listEntry(fs.rootNode(), 1).?;
|
|
try std.testing.expectEqualStrings("fat.log", second.name_buffer[0..second.name_len]);
|
|
}
|
|
|
|
test "short-name checksum matches the reference vector" {
|
|
// "README TXT" is a widely published example: checksum 0x15... compute a
|
|
// fixed pair to pin the rotate-add against regressions.
|
|
const a = FileSystem.shortChecksum("README TXT".*);
|
|
const b = FileSystem.shortChecksum("LOGGER~1LOG".*);
|
|
try std.testing.expect(a != b);
|
|
// The algorithm is order-sensitive: swapped bytes change the sum.
|
|
const c = FileSystem.shortChecksum("REDAME TXT".*);
|
|
try std.testing.expect(a != c);
|
|
}
|