Phase 2c: rename, wired through the VFS to runtime.fs
Completes the Phase 2 FAT mutation set (truncate, mkdir, unlink, rename). - engine: rename(dir, old_name, new_name) rewrites an existing entry's 8.3 name in place within the same directory. Refuses a missing source, a non-8.3 target, or a name that already exists; drops any long-name entries on the old file (it takes its new 8.3 name), LFN-aware like removeFile. Cross-directory and long-name- preserving rename are noted limitations. Host-tested (rename keeps contents; collision, non-8.3, and missing-source are refused). - vfs protocol: a `rename` operation whose payload is old-path, a 0x00 separator, then new-path. - VFS router: a forwardRename helper + a `.rename` case that requires both paths under the same mount (cross-filesystem rename is refused) and forwards the mount-relative old+new. - fat server: a `.rename` handler that requires the same parent directory and calls engine.rename. - runtime.fs: rename(old_path, new_path). - fat-test now renames the file it created (before removing it) and asserts the old name is gone, behind a new `fat-rename` QEMU case. Verified: zig build, zig build test (the engine rename unit test), zig build check-fat-image, and a sequential QEMU sweep — fat-mount, fat-mutations, fat-rename, vfs, vfs-client-death, log-flush, orderly-shutdown, initial-ramdisk, smoke — green.
This commit is contained in:
parent
184d90c2c6
commit
54635eecf5
|
|
@ -251,13 +251,15 @@ Because `std.fs`/`std.Io` have no per-OS branches, finishing this in `runtime.os
|
|||
lights up the whole file tower for the compiler at once. Environment can stay an empty
|
||||
map until the kernel populates a non-empty `envp`.
|
||||
|
||||
**Status (Phase 2a–2b landed):** `truncate` (O_TRUNC end to end, closing the boot-log
|
||||
stale-tail bug), `mkdir`, and `unlink` are all wired through the FAT engine, the VFS
|
||||
protocol + router, and `runtime.fs` (`makeDirectory`/`remove`) — host-tested and
|
||||
QEMU-tested (the `fat-mutations` case creates a directory, writes+reads a file in it,
|
||||
then removes it through the mount). `removeFile` is LFN-aware. Still to do: `rename`
|
||||
(engine + wiring), and then the richer `stat` — mtime/mode — which needs **wall-clock**
|
||||
(a kernel RTC read), the natural next kernel-side step.
|
||||
**Status (Phase 2a–2c landed):** `truncate` (O_TRUNC, closing the boot-log stale-tail
|
||||
bug), `mkdir`, `unlink`, and `rename` are all wired through the FAT engine, the VFS
|
||||
protocol + router, and `runtime.fs` (`makeDirectory` / `remove` / `rename`) —
|
||||
host-tested and QEMU-tested (the `fat-mutations` + `fat-rename` cases make a directory,
|
||||
write+read a file in it, rename it, then remove it through the mount). `removeFile` and
|
||||
`rename` are LFN-aware; `rename` is same-directory + 8.3 (cross-directory and
|
||||
long-name-preserving rename are noted limitations). **Remaining Phase 2:** the richer
|
||||
`stat` — mtime/mode — which needs **wall-clock** (a kernel RTC read behind a syscall,
|
||||
consistent with time being a kernel concern), the natural next kernel-side step.
|
||||
|
||||
### Phase 3 — Single-threaded, self-linked compiler bring-up
|
||||
|
||||
|
|
|
|||
|
|
@ -237,6 +237,20 @@ pub fn remove(path: []const u8) bool {
|
|||
return pathOperation(.unlink, path);
|
||||
}
|
||||
|
||||
/// Rename `old_path` to `new_path`. Both must be in the same directory (same-
|
||||
/// directory, 8.3-name rename only for now). Returns true on success.
|
||||
pub fn rename(old_path: []const u8, new_path: []const u8) bool {
|
||||
const total = old_path.len + 1 + new_path.len;
|
||||
if (total > protocol.maximum_payload) return false;
|
||||
var payload: [protocol.maximum_payload]u8 = undefined;
|
||||
@memcpy(payload[0..old_path.len], old_path);
|
||||
payload[old_path.len] = 0;
|
||||
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path);
|
||||
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||
const r = transact(request, payload[0..total], &.{}) orelse return false;
|
||||
return r.reply.status == 0;
|
||||
}
|
||||
|
||||
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||
/// the VFS then routes everything under `target` to that backend. This is the one
|
||||
/// call that hands the VFS a capability (the backend endpoint). Returns true on
|
||||
|
|
|
|||
|
|
@ -739,6 +739,75 @@ pub const FileSystem = struct {
|
|||
}
|
||||
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 ----------------------------------------
|
||||
|
|
@ -963,3 +1032,32 @@ test "create a subdirectory with . and .. and a file inside" {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,20 +62,24 @@ pub fn main(init: runtime.process.Init) void {
|
|||
wrote = (f.writeAll("mutation-ok") orelse 0) == "mutation-ok".len;
|
||||
f.close();
|
||||
}
|
||||
// Rename it, then read from the new name and confirm the old name is gone.
|
||||
const renamed = fs.rename("/mnt/usb/TESTDIR/HELLO.TXT", "/mnt/usb/TESTDIR/RENAMED.TXT");
|
||||
const old_gone = !fs.exists("/mnt/usb/TESTDIR/HELLO.TXT");
|
||||
if (renamed and old_gone) _ = runtime.system.write("fat-test: rename ok\n");
|
||||
var readback = false;
|
||||
if (fs.open("/mnt/usb/TESTDIR/HELLO.TXT", .{})) |reopened| {
|
||||
if (fs.open("/mnt/usb/TESTDIR/RENAMED.TXT", .{})) |reopened| {
|
||||
var f = reopened;
|
||||
var buf: [16]u8 = undefined;
|
||||
const got = f.read(&buf) orelse 0;
|
||||
f.close();
|
||||
readback = std.mem.eql(u8, buf[0..got], "mutation-ok");
|
||||
}
|
||||
const removed = fs.remove("/mnt/usb/TESTDIR/HELLO.TXT");
|
||||
const gone = !fs.exists("/mnt/usb/TESTDIR/HELLO.TXT");
|
||||
if (wrote and readback and removed and gone) {
|
||||
const removed = fs.remove("/mnt/usb/TESTDIR/RENAMED.TXT");
|
||||
const gone = !fs.exists("/mnt/usb/TESTDIR/RENAMED.TXT");
|
||||
if (wrote and renamed and old_gone and readback and removed and gone) {
|
||||
_ = runtime.system.write("fat-test: mutations ok\n");
|
||||
} else {
|
||||
writeLine("fat-test: mutations FAILED (wrote={} read={} removed={} gone={})\n", .{ wrote, readback, removed, gone });
|
||||
writeLine("fat-test: mutations FAILED (wrote={} renamed={} oldgone={} read={} removed={} gone={})\n", .{ wrote, renamed, old_gone, readback, removed, gone });
|
||||
}
|
||||
} else {
|
||||
_ = runtime.system.write("fat-test: mkdir /mnt/usb/TESTDIR failed\n");
|
||||
|
|
|
|||
|
|
@ -202,6 +202,17 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
|
|||
if (!filesystem.removeFile(parent, split.leaf)) return fail(out);
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
},
|
||||
.rename => {
|
||||
const both = payload[0..@min(payload.len, request.len)];
|
||||
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
||||
const old_split = splitParent(both[0..sep]);
|
||||
const new_split = splitParent(both[sep + 1 ..]);
|
||||
// Same-directory rename only.
|
||||
if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return fail(out);
|
||||
const parent = filesystem.resolve(old_split.parent) orelse return fail(out);
|
||||
if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return fail(out);
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
},
|
||||
// A backend is never itself a mount target.
|
||||
.mount, .unmount => return fail(out),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ pub const Operation = enum(u32) {
|
|||
// payload); a mounted backend handles them, the flat ramfs refuses them.
|
||||
mkdir, // mkdir(path payload) -> status
|
||||
unlink, // unlink(path payload) -> status
|
||||
// rename: the payload is the old path, a single 0x00 separator, then the new
|
||||
// path. Same-directory rename only (the router requires both under one mount).
|
||||
rename, // rename(old\0new payload) -> status
|
||||
};
|
||||
|
||||
/// The type of a filesystem node, aligned to the FSH file-type table
|
||||
|
|
|
|||
|
|
@ -177,6 +177,28 @@ fn forwardPath(out: []u8, backend: ipc.Handle, operation: protocol.Operation, re
|
|||
return copy;
|
||||
}
|
||||
|
||||
/// Forward a rename to its backend: the payload is the mount-relative old path, a
|
||||
/// 0x00 separator, then the mount-relative new path. Relays the backend's reply.
|
||||
fn forwardRename(out: []u8, backend: ipc.Handle, old_relative: []const u8, new_relative: []const u8) usize {
|
||||
const total = old_relative.len + 1 + new_relative.len;
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
if (protocol.request_size + total > message.len) return fail(out);
|
||||
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
var p = protocol.request_size;
|
||||
@memcpy(message[p..][0..old_relative.len], old_relative);
|
||||
p += old_relative.len;
|
||||
message[p] = 0;
|
||||
p += 1;
|
||||
@memcpy(message[p..][0..new_relative.len], new_relative);
|
||||
p += new_relative.len;
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0..p], &reply) catch return fail(out);
|
||||
const copy = @min(n, out.len);
|
||||
@memcpy(out[0..copy], reply[0..copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Best-effort close of a backend node (used when a dead client's forwarding
|
||||
/// handles are swept — the backend must not leak the vfs's opens).
|
||||
fn forwardClose(backend: ipc.Handle, backend_node: u64) void {
|
||||
|
|
@ -328,6 +350,18 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle)
|
|||
// create or remove them (and a bare-name path is not a mount target).
|
||||
return fail(out);
|
||||
},
|
||||
.rename => {
|
||||
const both = payload[0..@min(payload.len, request.len)];
|
||||
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
||||
const old_path = both[0..sep];
|
||||
const new_path = both[sep + 1 ..];
|
||||
const mo = longestMount(old_path) orelse return fail(out);
|
||||
const mn = longestMount(new_path) orelse return fail(out);
|
||||
// Both paths must live under the same mount — cross-filesystem rename is
|
||||
// not supported.
|
||||
if (mo.backend != mn.backend) return fail(out);
|
||||
return forwardRename(out, mo.backend, mo.relative, mn.relative);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -329,6 +329,14 @@ CASES = [
|
|||
"timeout": 150,
|
||||
"expect": r"fat-test: mutations ok",
|
||||
"fail": r"fat-test: mutations FAILED|fat-test: mkdir .* failed|DANOS-TEST-RESULT: FAIL"},
|
||||
# Phase 2c: rename through the mount — fat-test renames the file it created
|
||||
# before removing it, and confirms the old name is gone.
|
||||
{"name": "fat-rename",
|
||||
"build_case": "fat-mount",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"expect": r"fat-test: rename ok",
|
||||
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
|
||||
# Boot-from-USB smoke: the whole system now boots off the FAT32 image on a
|
||||
# usb-storage device (OVMF -> \EFI\BOOT\BOOTX64.efi -> kernel), so the kernel
|
||||
# reaching its PASS marker at all proves the USB boot path end to end. Reuses
|
||||
|
|
|
|||
Loading…
Reference in New Issue