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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user