fat: long-file-name creation, fast cluster allocation, makePath
createFile/createDirectory now build long-name chains: a mangled STEM~N 8.3 alias (collision-checked per directory), the standard rotate-add checksum, and 13-UCS-2-per-entry pieces written last-logical-first into a contiguous free-slot run (found sector-wise, growing the directory as today). Write order is chain first, 8.3 entry last, so an interrupted create leaves only skippable orphans. Uppercase-compliant 8.3 names keep the bare-entry fast path; lowercase names now get a chain so their exact case survives — matching tools/make-fat-image.py. rename stays 8.3-only (documented; nothing needs more yet). allocateCluster drops its from-cluster-2 rescan (measured ~1 s/cluster on a part-full volume, a 37 s shutdown flush in the lost M19 build): a next-free hint (rewound by frees) plus a FAT-sector LBA cache turn the scan into one device read per FAT sector. mkdir now refuses an existing name (no duplicate entries), and runtime.fs gains makePath (mkdir -p) for the logger's nested per-boot directories. Engine tests cover the log-directory shape, ~N collisions, chain unlink/reuse, the 8.3 fast path, and the checksum.
This commit is contained in:
@@ -236,6 +236,22 @@ pub fn makeDirectory(path: []const u8) bool {
|
||||
return pathOperation(.mkdir, path);
|
||||
}
|
||||
|
||||
/// Create every missing directory along `path` (mkdir -p). Probes each prefix
|
||||
/// with `exists` first — a FAT mkdir of an existing name is refused, and the
|
||||
/// probe keeps the common "already there" case cheap. Returns true when the
|
||||
/// whole path exists afterwards.
|
||||
pub fn makePath(path: []const u8) bool {
|
||||
var end: usize = 0;
|
||||
while (end < path.len) {
|
||||
end += 1;
|
||||
while (end < path.len and path[end] != '/') end += 1;
|
||||
const prefix = path[0..end];
|
||||
if (prefix.len == 0 or (prefix.len == 1 and prefix[0] == '/')) continue;
|
||||
if (!exists(prefix) and !makeDirectory(prefix)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Remove the file at `path`. Returns true on success. Directories are refused
|
||||
/// (a separate directory-removal would have to check emptiness).
|
||||
pub fn remove(path: []const u8) bool {
|
||||
|
||||
+337
-16
@@ -70,6 +70,15 @@ pub const FileSystem = struct {
|
||||
// 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 {
|
||||
@@ -139,7 +148,10 @@ pub const FileSystem = struct {
|
||||
while (done < out.len) {
|
||||
const lba = position / sector_size;
|
||||
const within: usize = @intCast(position % sector_size);
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
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;
|
||||
@@ -159,7 +171,10 @@ pub const FileSystem = struct {
|
||||
while (done < in.len) {
|
||||
const lba = position / sector_size;
|
||||
const within: usize = @intCast(position % sector_size);
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
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;
|
||||
@@ -237,11 +252,19 @@ pub const FileSystem = struct {
|
||||
|
||||
// Find and claim a free cluster, marking it end-of-chain. Returns its number.
|
||||
fn allocateCluster(self: *FileSystem) ?u32 {
|
||||
var cluster: u32 = 2;
|
||||
while (cluster < self.geometry.cluster_count + 2) : (cluster += 1) {
|
||||
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
|
||||
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
|
||||
return cluster;
|
||||
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;
|
||||
@@ -257,6 +280,7 @@ pub const FileSystem = struct {
|
||||
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;
|
||||
}
|
||||
@@ -603,6 +627,192 @@ pub const FileSystem = struct {
|
||||
_ = 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.
|
||||
@@ -645,19 +855,20 @@ pub const FileSystem = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Create an 8.3-named file in directory `dir`. Returns the new (empty) node,
|
||||
/// or null if the name is not 8.3-representable or no directory slot is free.
|
||||
/// 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 {
|
||||
const raw = to83(name) orelse return null;
|
||||
return self.addEntry(dir, raw, on_disk.attribute_archive, 0, 0);
|
||||
return self.addEntryNamed(dir, name, on_disk.attribute_archive, 0, 0);
|
||||
}
|
||||
|
||||
/// Create an 8.3-named subdirectory in `dir`: allocate and initialise its first
|
||||
/// Create a subdirectory in `dir`: allocate and initialise its first
|
||||
/// cluster with "." (itself) and ".." (the parent) entries, then add its
|
||||
/// directory entry to `dir`. Returns the new directory node, or null (bad name,
|
||||
/// no free cluster, or the directory is full). Long names are not created.
|
||||
/// 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 raw = to83(name) orelse return null;
|
||||
const cluster = self.allocateCluster() orelse return null;
|
||||
self.zeroCluster(cluster);
|
||||
|
||||
@@ -685,7 +896,7 @@ pub const FileSystem = struct {
|
||||
self.freeChain(cluster);
|
||||
return null;
|
||||
}
|
||||
return self.addEntry(dir, raw, on_disk.attribute_directory, cluster, 0) orelse {
|
||||
return self.addEntryNamed(dir, name, on_disk.attribute_directory, cluster, 0) orelse {
|
||||
self.freeChain(cluster);
|
||||
return null;
|
||||
};
|
||||
@@ -1103,3 +1314,113 @@ test "a create stamps the modification time" {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,9 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
},
|
||||
.mkdir => {
|
||||
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
||||
const path = payload[0..@min(payload.len, request.len)];
|
||||
if (filesystem.resolve(path) != null) return fail(out); // already exists — no duplicate entries
|
||||
const split = splitParent(path);
|
||||
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
||||
if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out);
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
|
||||
Reference in New Issue
Block a user