exfat: the engine write path (S4 step 3)
create / write / truncate / remove / mkdir / rename, completing the engine's pub-fn contract with the shared harness. The allocation BITMAP is the authority: setAllocated IS the allocation (a set bit), and the 32-bit FAT only records a fragmented chain's order — forgetting the bit would hand a live cluster out twice, so the write path never touches the FAT without also owning the bit. This engine writes FAT-linked (no_fat_chain=0) files: createFile makes an empty set; writeFile allocates+links+zeroes clusters (so a sparse gap reads zero and valid_data_length can honestly equal data_length) and rewrites the Stream entry; a contiguous file opened for growth is first threaded through the FAT. truncate frees the chain; removeFile clears each set entry's InUse bit and frees the chain (a non-empty directory is refused); rename re-homes the same clusters under a new name set. createDirectory allocates one zeroed cluster — exFAT directories carry no "." / ".." entries. Every entry-set mutation recomputes the set checksum. Offsets clamp to the vfs u32 surface. Ten engine host tests now (read + write across clusters, truncate+reuse, remove, subdir+inner file, rename); 17 total with on-disk. bounds green.
This commit is contained in:
@@ -489,6 +489,427 @@ pub const FileSystem = struct {
|
||||
}
|
||||
return got;
|
||||
}
|
||||
|
||||
// --- allocation: the bitmap is the authority ----------------------------
|
||||
|
||||
fn bitmapByteAndMask(cluster: u32) struct { byte: u64, mask: u8 } {
|
||||
const bit = cluster - on_disk.first_data_cluster;
|
||||
return .{ .byte = bit / 8, .mask = @as(u8, 1) << @intCast(bit % 8) };
|
||||
}
|
||||
|
||||
fn bitmapLocate(self: *FileSystem, byte: u64) ?struct { lba: u64, within: usize } {
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const cluster = self.clusterOfChain(self.bitmap_first_cluster, false, @intCast(byte / cluster_bytes)) orelse return null;
|
||||
const in_cluster: u32 = @intCast(byte % cluster_bytes);
|
||||
return .{ .lba = self.clusterSector(cluster, in_cluster / sector_size), .within = in_cluster % sector_size };
|
||||
}
|
||||
|
||||
/// Set or clear a cluster's allocation bit. Setting the bit IS the allocation —
|
||||
/// the bitmap, not the FAT, is what says a cluster is in use; forgetting it
|
||||
/// would corrupt free space (a later allocation could hand the cluster out
|
||||
/// again). The FAT only records the ORDER of a fragmented chain.
|
||||
fn setAllocated(self: *FileSystem, cluster: u32, used: bool) bool {
|
||||
if (!self.validCluster(cluster)) return false;
|
||||
const bb = bitmapByteAndMask(cluster);
|
||||
const loc = self.bitmapLocate(bb.byte) orelse return false;
|
||||
if (!self.blockRead(loc.lba, &self.sector)) return false;
|
||||
if (used) self.sector[loc.within] |= bb.mask else self.sector[loc.within] &= ~bb.mask;
|
||||
return self.blockWrite(loc.lba, &self.sector);
|
||||
}
|
||||
|
||||
fn isAllocated(self: *FileSystem, cluster: u32) bool {
|
||||
const bb = bitmapByteAndMask(cluster);
|
||||
const loc = self.bitmapLocate(bb.byte) orelse return true; // unknown: never hand it out
|
||||
if (!self.blockRead(loc.lba, &self.sector)) return true;
|
||||
return self.sector[loc.within] & bb.mask != 0;
|
||||
}
|
||||
|
||||
/// Allocate one free cluster (mark its bit), or null if the volume is full.
|
||||
fn allocateCluster(self: *FileSystem) ?u32 {
|
||||
var cluster: u32 = on_disk.first_data_cluster;
|
||||
const end = self.geometry.cluster_count + on_disk.first_data_cluster;
|
||||
while (cluster < end) : (cluster += 1) {
|
||||
if (!self.isAllocated(cluster)) {
|
||||
return if (self.setAllocated(cluster, true)) cluster else null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn writeFatEntry(self: *FileSystem, cluster: u32, value: u32) bool {
|
||||
const byte = @as(u64, self.geometry.fat_offset_sectors) * sector_size + @as(u64, cluster) * 4;
|
||||
const lba = byte / sector_size;
|
||||
const within: usize = @intCast(byte % sector_size);
|
||||
if (!self.blockRead(lba, &self.sector)) return false;
|
||||
std.mem.writeInt(u32, self.sector[within..][0..4], value, .little);
|
||||
return self.blockWrite(lba, &self.sector);
|
||||
}
|
||||
|
||||
fn zeroCluster(self: *FileSystem, cluster: u32) bool {
|
||||
const zero = [_]u8{0} ** sector_size;
|
||||
var s: u32 = 0;
|
||||
while (s < self.geometry.sectors_per_cluster) : (s += 1) {
|
||||
if (!self.blockWrite(self.clusterSector(cluster, s), &zero)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Free a chain: clear each cluster's bitmap bit. Contiguous chains are freed by
|
||||
/// count; FAT-linked chains are walked (a corrupt cycle is bounded by the
|
||||
/// cluster count).
|
||||
fn freeChain(self: *FileSystem, first: u32, no_fat_chain: bool, cluster_count: u32) void {
|
||||
if (!self.validCluster(first)) return;
|
||||
if (no_fat_chain) {
|
||||
var c = first;
|
||||
var n = cluster_count;
|
||||
while (n > 0 and self.validCluster(c)) : (n -= 1) {
|
||||
_ = self.setAllocated(c, false);
|
||||
c += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
var cluster = first;
|
||||
var guard: u32 = 0;
|
||||
while (self.validCluster(cluster) and guard < self.geometry.cluster_count) : (guard += 1) {
|
||||
const next = self.readFatEntry(cluster);
|
||||
_ = self.setAllocated(cluster, false);
|
||||
if (self.isEndOfChain(next) or !self.validCluster(next)) break;
|
||||
cluster = next;
|
||||
}
|
||||
}
|
||||
|
||||
// --- entry-set read / write (checksum recomputed on every mutation) ------
|
||||
|
||||
const set_buffer_bytes = (2 + (name_maximum + name_units_per_entry - 1) / name_units_per_entry) * on_disk.entry_bytes;
|
||||
|
||||
fn writeEntry(self: *FileSystem, dir_first: u32, dir_no_fat_chain: bool, index: u64, entry: []const u8) bool {
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const byte = index * on_disk.entry_bytes;
|
||||
const cluster = self.clusterOfChain(dir_first, dir_no_fat_chain, @intCast(byte / cluster_bytes)) orelse return false;
|
||||
const in_cluster: u32 = @intCast(byte % cluster_bytes);
|
||||
const lba = self.clusterSector(cluster, in_cluster / sector_size);
|
||||
const within = in_cluster % sector_size;
|
||||
if (!self.blockRead(lba, &self.sector)) return false;
|
||||
@memcpy(self.sector[within..][0..on_disk.entry_bytes], entry[0..on_disk.entry_bytes]);
|
||||
return self.blockWrite(lba, &self.sector);
|
||||
}
|
||||
|
||||
fn readSet(self: *FileSystem, node: Node, buf: []u8) usize {
|
||||
const count = @as(usize, node.secondary_count) + 1;
|
||||
const total = count * on_disk.entry_bytes;
|
||||
if (buf.len < total) return 0;
|
||||
var i: usize = 0;
|
||||
while (i < count) : (i += 1) {
|
||||
var entry: [on_disk.entry_bytes]u8 = undefined;
|
||||
if (!self.entryAt(node.parent_first_cluster, node.parent_no_fat_chain, node.entry_index + i, &entry)) return 0;
|
||||
@memcpy(buf[i * on_disk.entry_bytes ..][0..on_disk.entry_bytes], &entry);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Write `buf` (a whole set) back, recomputing the set checksum first.
|
||||
fn writeSet(self: *FileSystem, node: Node, buf: []u8) bool {
|
||||
std.mem.writeInt(u16, buf[2..4], on_disk.setChecksum(buf), .little);
|
||||
var i: usize = 0;
|
||||
const count = @as(usize, node.secondary_count) + 1;
|
||||
while (i < count) : (i += 1) {
|
||||
if (!self.writeEntry(node.parent_first_cluster, node.parent_no_fat_chain, node.entry_index + i, buf[i * on_disk.entry_bytes ..][0..on_disk.entry_bytes])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Rewrite a node's Stream entry (sizes, first cluster, chain flag) and the
|
||||
/// File entry's modified timestamp, recomputing the set checksum.
|
||||
fn updateStream(self: *FileSystem, node: Node) void {
|
||||
var buf: [set_buffer_bytes]u8 = undefined;
|
||||
const total = self.readSet(node, &buf);
|
||||
if (total == 0) return;
|
||||
var file = std.mem.bytesToValue(on_disk.FileEntry, buf[0..on_disk.entry_bytes]);
|
||||
file.last_modified_timestamp = on_disk.epochToTimestamp(self.current_time_epoch);
|
||||
@memcpy(buf[0..on_disk.entry_bytes], std.mem.asBytes(&file));
|
||||
var stream = std.mem.bytesToValue(on_disk.StreamExtensionEntry, buf[on_disk.entry_bytes..][0..on_disk.entry_bytes]);
|
||||
stream.first_cluster = node.first_cluster;
|
||||
stream.data_length = node.size;
|
||||
stream.valid_data_length = node.valid_data_length;
|
||||
stream.general_secondary_flags = on_disk.secondary_flag_allocation_possible |
|
||||
(if (node.no_fat_chain) on_disk.secondary_flag_no_fat_chain else 0);
|
||||
@memcpy(buf[on_disk.entry_bytes..][0..on_disk.entry_bytes], std.mem.asBytes(&stream));
|
||||
_ = self.writeSet(node, buf[0..total]);
|
||||
}
|
||||
|
||||
// --- directory-entry placement ------------------------------------------
|
||||
|
||||
/// Ensure the directory's cluster chain holds at least `entry_index+1` entries,
|
||||
/// extending it (a fresh zeroed FAT-linked cluster) as needed. Assumes a
|
||||
/// FAT-linked directory (the root and directories this engine creates).
|
||||
fn ensureDirCapacity(self: *FileSystem, dir: Node, entry_index: u64) bool {
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const clusters_needed: u32 = @intCast(((entry_index + 1) * on_disk.entry_bytes + cluster_bytes - 1) / cluster_bytes);
|
||||
if (dir.no_fat_chain) {
|
||||
// A contiguous directory cannot be grown here; it fits only if it
|
||||
// already spans enough clusters.
|
||||
const have: u32 = @intCast((@as(u64, dir.size) + cluster_bytes - 1) / cluster_bytes);
|
||||
return clusters_needed <= have;
|
||||
}
|
||||
var cluster = dir.first_cluster;
|
||||
if (!self.validCluster(cluster)) return false;
|
||||
var have: u32 = 1;
|
||||
while (have < clusters_needed) : (have += 1) {
|
||||
const next = self.readFatEntry(cluster);
|
||||
if (self.isEndOfChain(next) or !self.validCluster(next)) {
|
||||
const fresh = self.allocateCluster() orelse return false;
|
||||
if (!self.zeroCluster(fresh)) return false;
|
||||
if (!self.writeFatEntry(cluster, fresh)) return false;
|
||||
if (!self.writeFatEntry(fresh, on_disk.end_of_chain)) return false;
|
||||
cluster = fresh;
|
||||
} else {
|
||||
cluster = next;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Place a `count`-entry set at the directory's first end-of-directory marker,
|
||||
/// growing the directory if needed. Returns the starting linear entry index.
|
||||
/// (Deleted-run reuse is a later refinement; this appends.)
|
||||
fn appendEntrySet(self: *FileSystem, dir: Node, set: []const u8, count: u8) ?u64 {
|
||||
var index: u64 = 0;
|
||||
while (index < directory_entry_scan_maximum) : (index += 1) {
|
||||
var entry: [on_disk.entry_bytes]u8 = undefined;
|
||||
if (!self.entryAt(dir.first_cluster, dir.no_fat_chain, index, &entry)) break; // ran off the chain — grow
|
||||
if (entry[0] == on_disk.entry_type_end_of_directory) break;
|
||||
}
|
||||
// Room for the set AND a following end marker (freshly-allocated clusters
|
||||
// are zeroed, so the marker is already there once capacity is ensured).
|
||||
if (!self.ensureDirCapacity(dir, index + count)) return null;
|
||||
var i: u64 = 0;
|
||||
while (i < count) : (i += 1) {
|
||||
if (!self.writeEntry(dir.first_cluster, dir.no_fat_chain, index + i, set[@intCast(i * on_disk.entry_bytes)..][0..on_disk.entry_bytes])) return null;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/// Assemble a File+Stream+Name set for `name` into `buf`, returning the entry
|
||||
/// count. Names are stored in their original case; the Stream's hash is over
|
||||
/// the up-cased name.
|
||||
fn buildFileSet(self: *FileSystem, name: []const u8, is_directory: bool, first_cluster: u32, data_length: u64, no_fat_chain: bool, buf: []u8) u8 {
|
||||
const name_entries: u8 = @intCast((name.len + name_units_per_entry - 1) / name_units_per_entry);
|
||||
const count: u8 = 2 + name_entries;
|
||||
@memset(buf[0 .. @as(usize, count) * on_disk.entry_bytes], 0);
|
||||
|
||||
var file = std.mem.zeroes(on_disk.FileEntry);
|
||||
file.entry_type = on_disk.entry_type_file;
|
||||
file.secondary_count = 1 + name_entries;
|
||||
file.file_attributes = if (is_directory) on_disk.attribute_directory else on_disk.attribute_archive;
|
||||
const stamp = on_disk.epochToTimestamp(self.current_time_epoch);
|
||||
file.create_timestamp = stamp;
|
||||
file.last_modified_timestamp = stamp;
|
||||
file.last_accessed_timestamp = stamp;
|
||||
@memcpy(buf[0..on_disk.entry_bytes], std.mem.asBytes(&file));
|
||||
|
||||
var stream = std.mem.zeroes(on_disk.StreamExtensionEntry);
|
||||
stream.entry_type = on_disk.entry_type_stream_extension;
|
||||
stream.general_secondary_flags = on_disk.secondary_flag_allocation_possible |
|
||||
(if (no_fat_chain) on_disk.secondary_flag_no_fat_chain else 0);
|
||||
stream.name_length = @intCast(name.len);
|
||||
var upname: [name_maximum]u16 = undefined;
|
||||
for (name, 0..) |c, i| upname[i] = self.fold(c);
|
||||
stream.name_hash = on_disk.nameHash(upname[0..name.len]);
|
||||
stream.valid_data_length = data_length;
|
||||
stream.first_cluster = first_cluster;
|
||||
stream.data_length = data_length;
|
||||
@memcpy(buf[on_disk.entry_bytes..][0..on_disk.entry_bytes], std.mem.asBytes(&stream));
|
||||
|
||||
var e: u8 = 0;
|
||||
while (e < name_entries) : (e += 1) {
|
||||
var name_entry = std.mem.zeroes(on_disk.FileNameEntry);
|
||||
name_entry.entry_type = on_disk.entry_type_file_name;
|
||||
var u: usize = 0;
|
||||
while (u < name_units_per_entry) : (u += 1) {
|
||||
const idx = @as(usize, e) * name_units_per_entry + u;
|
||||
if (idx < name.len) name_entry.file_name[u] = name[idx];
|
||||
}
|
||||
@memcpy(buf[(2 + @as(usize, e)) * on_disk.entry_bytes ..][0..on_disk.entry_bytes], std.mem.asBytes(&name_entry));
|
||||
}
|
||||
std.mem.writeInt(u16, buf[2..4], on_disk.setChecksum(buf[0 .. @as(usize, count) * on_disk.entry_bytes]), .little);
|
||||
return count;
|
||||
}
|
||||
|
||||
// --- write / create / truncate / remove / rename ------------------------
|
||||
|
||||
/// Convert a contiguous file to a FAT-linked one (this engine's writes are
|
||||
/// FAT-linked), by threading its existing clusters through the FAT.
|
||||
fn ensureFatChain(self: *FileSystem, node: *Node) bool {
|
||||
if (!node.no_fat_chain) return true;
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const clusters: u32 = if (node.size == 0) 0 else @intCast((@as(u64, node.size) + cluster_bytes - 1) / cluster_bytes);
|
||||
var i: u32 = 0;
|
||||
while (i + 1 < clusters) : (i += 1) {
|
||||
if (!self.writeFatEntry(node.first_cluster + i, node.first_cluster + i + 1)) return false;
|
||||
}
|
||||
if (clusters > 0 and !self.writeFatEntry(node.first_cluster + clusters - 1, on_disk.end_of_chain)) return false;
|
||||
node.no_fat_chain = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Ensure a FAT-linked file spans at least `clusters_needed` clusters, zeroing
|
||||
/// each freshly allocated one (so a sparse gap reads as zero and
|
||||
/// valid_data_length can equal data_length honestly).
|
||||
fn ensureFileClusters(self: *FileSystem, node: *Node, clusters_needed: u32) bool {
|
||||
if (clusters_needed == 0) return true;
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
var have: u32 = if (self.validCluster(node.first_cluster)) @intCast((@as(u64, node.size) + cluster_bytes - 1) / cluster_bytes) else 0;
|
||||
if (have >= clusters_needed) return true;
|
||||
var last: u32 = 0;
|
||||
if (have > 0) last = self.clusterOfChain(node.first_cluster, node.no_fat_chain, have - 1) orelse return false;
|
||||
while (have < clusters_needed) : (have += 1) {
|
||||
const fresh = self.allocateCluster() orelse return false;
|
||||
if (!self.zeroCluster(fresh)) return false;
|
||||
if (!self.writeFatEntry(fresh, on_disk.end_of_chain)) return false;
|
||||
if (have == 0) node.first_cluster = fresh else if (!self.writeFatEntry(last, fresh)) return false;
|
||||
last = fresh;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Write `data` at `offset`, growing the file (bitmap allocation + FAT links)
|
||||
/// as needed. Returns bytes written. Clamped to the vfs u32 offset surface.
|
||||
pub fn writeFile(self: *FileSystem, node: *Node, offset: u32, data: []const u8) usize {
|
||||
if (data.len == 0) return 0;
|
||||
const write_len: u32 = @intCast(@min(data.len, @as(usize, std.math.maxInt(u32) - offset)));
|
||||
if (write_len == 0) return 0;
|
||||
if (node.no_fat_chain and !self.ensureFatChain(node)) return 0;
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const clusters_needed = (offset + write_len + cluster_bytes - 1) / cluster_bytes;
|
||||
if (!self.ensureFileClusters(node, clusters_needed)) return 0;
|
||||
|
||||
var produced: usize = 0;
|
||||
var position = offset;
|
||||
while (produced < write_len) {
|
||||
const cluster = self.clusterOfChain(node.first_cluster, node.no_fat_chain, position / cluster_bytes) orelse break;
|
||||
const in_cluster = position % cluster_bytes;
|
||||
const lba = self.clusterSector(cluster, in_cluster / sector_size);
|
||||
const in_sector = in_cluster % sector_size;
|
||||
const n = @min(write_len - produced, sector_size - in_sector);
|
||||
if (in_sector == 0 and n == sector_size) {
|
||||
if (!self.blockWrite(lba, data[produced .. produced + sector_size])) break;
|
||||
} else {
|
||||
if (!self.blockRead(lba, &self.sector)) break;
|
||||
@memcpy(self.sector[in_sector .. in_sector + n], data[produced .. produced + n]);
|
||||
if (!self.blockWrite(lba, &self.sector)) break;
|
||||
}
|
||||
produced += n;
|
||||
position += @intCast(n);
|
||||
}
|
||||
const written_end = offset + @as(u32, @intCast(produced));
|
||||
if (written_end > node.size) node.size = written_end;
|
||||
// Every allocated cluster is zeroed, so all bytes up to size are valid.
|
||||
node.valid_data_length = node.size;
|
||||
self.updateStream(node.*);
|
||||
return produced;
|
||||
}
|
||||
|
||||
/// Empty a file: free its chain and zero its stream.
|
||||
pub fn truncate(self: *FileSystem, node: *Node) void {
|
||||
if (self.validCluster(node.first_cluster)) {
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const clusters: u32 = @intCast(@max(@as(u64, 1), (@as(u64, node.size) + cluster_bytes - 1) / cluster_bytes));
|
||||
self.freeChain(node.first_cluster, node.no_fat_chain, clusters);
|
||||
}
|
||||
node.first_cluster = 0;
|
||||
node.size = 0;
|
||||
node.valid_data_length = 0;
|
||||
node.no_fat_chain = false;
|
||||
self.updateStream(node.*);
|
||||
}
|
||||
|
||||
pub fn createFile(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
||||
if (name.len == 0 or name.len > name_maximum) return null;
|
||||
if (self.findChild(dir, name) != null) return null;
|
||||
var set_buf: [set_buffer_bytes]u8 = undefined;
|
||||
const count = self.buildFileSet(name, false, 0, 0, false, &set_buf);
|
||||
const index = self.appendEntrySet(dir, set_buf[0 .. @as(usize, count) * on_disk.entry_bytes], count) orelse return null;
|
||||
return .{
|
||||
.first_cluster = 0,
|
||||
.size = 0,
|
||||
.is_directory = false,
|
||||
.no_fat_chain = false,
|
||||
.valid_data_length = 0,
|
||||
.parent_first_cluster = dir.first_cluster,
|
||||
.parent_no_fat_chain = dir.no_fat_chain,
|
||||
.entry_index = index,
|
||||
.secondary_count = count - 1,
|
||||
.has_entry = true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn createDirectory(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
||||
if (name.len == 0 or name.len > name_maximum) return null;
|
||||
if (self.findChild(dir, name) != null) return null;
|
||||
const cluster = self.allocateCluster() orelse return null;
|
||||
if (!self.zeroCluster(cluster)) return null;
|
||||
if (!self.writeFatEntry(cluster, on_disk.end_of_chain)) return null; // exFAT dirs have no . / .. entries
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
var set_buf: [set_buffer_bytes]u8 = undefined;
|
||||
const count = self.buildFileSet(name, true, cluster, cluster_bytes, false, &set_buf);
|
||||
const index = self.appendEntrySet(dir, set_buf[0 .. @as(usize, count) * on_disk.entry_bytes], count) orelse {
|
||||
self.freeChain(cluster, false, 1);
|
||||
return null;
|
||||
};
|
||||
return .{
|
||||
.first_cluster = cluster,
|
||||
.size = cluster_bytes,
|
||||
.is_directory = true,
|
||||
.no_fat_chain = false,
|
||||
.valid_data_length = cluster_bytes,
|
||||
.parent_first_cluster = dir.first_cluster,
|
||||
.parent_no_fat_chain = dir.no_fat_chain,
|
||||
.entry_index = index,
|
||||
.secondary_count = count - 1,
|
||||
.has_entry = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Mark a set's entries deleted (clear the InUse bit) and free its data. A
|
||||
/// non-empty directory is refused.
|
||||
fn deleteSet(self: *FileSystem, dir: Node, node: Node) bool {
|
||||
var i: u64 = 0;
|
||||
while (i <= node.secondary_count) : (i += 1) {
|
||||
var entry: [on_disk.entry_bytes]u8 = undefined;
|
||||
if (!self.entryAt(dir.first_cluster, dir.no_fat_chain, node.entry_index + i, &entry)) return false;
|
||||
entry[0] &= ~on_disk.entry_type_in_use_bit;
|
||||
if (!self.writeEntry(dir.first_cluster, dir.no_fat_chain, node.entry_index + i, &entry)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn removeFile(self: *FileSystem, dir: Node, name: []const u8) bool {
|
||||
const node = self.findChild(dir, name) orelse return false;
|
||||
if (node.is_directory and self.listEntry(node, 0) != null) return false; // not empty
|
||||
if (self.validCluster(node.first_cluster)) {
|
||||
const cluster_bytes = self.clusterBytes();
|
||||
const clusters: u32 = @intCast(@max(@as(u64, 1), (@as(u64, node.size) + cluster_bytes - 1) / cluster_bytes));
|
||||
self.freeChain(node.first_cluster, node.no_fat_chain, clusters);
|
||||
}
|
||||
return self.deleteSet(dir, node);
|
||||
}
|
||||
|
||||
/// Rename within a directory: place a new set under `new_name` pointing at the
|
||||
/// same data, then delete the old set (the clusters move, they are not freed).
|
||||
pub fn rename(self: *FileSystem, dir: Node, old_name: []const u8, new_name: []const u8) bool {
|
||||
if (new_name.len == 0 or new_name.len > name_maximum) return false;
|
||||
const node = self.findChild(dir, old_name) orelse return false;
|
||||
if (self.findChild(dir, new_name) != null) return false;
|
||||
var set_buf: [set_buffer_bytes]u8 = undefined;
|
||||
const count = self.buildFileSet(new_name, node.is_directory, node.first_cluster, node.size, node.no_fat_chain, &set_buf);
|
||||
// Preserve the original valid-data-length (buildFileSet set it to data_length).
|
||||
var stream = std.mem.bytesToValue(on_disk.StreamExtensionEntry, set_buf[on_disk.entry_bytes..][0..on_disk.entry_bytes]);
|
||||
stream.valid_data_length = node.valid_data_length;
|
||||
@memcpy(set_buf[on_disk.entry_bytes..][0..on_disk.entry_bytes], std.mem.asBytes(&stream));
|
||||
std.mem.writeInt(u16, set_buf[2..4], on_disk.setChecksum(set_buf[0 .. @as(usize, count) * on_disk.entry_bytes]), .little);
|
||||
if (self.appendEntrySet(dir, set_buf[0 .. @as(usize, count) * on_disk.entry_bytes], count) == null) return false;
|
||||
return self.deleteSet(dir, node);
|
||||
}
|
||||
};
|
||||
|
||||
fn clampU32(value: u64) u32 {
|
||||
@@ -535,6 +956,10 @@ const test_clusters = 64;
|
||||
const test_fat_sector = 8;
|
||||
const test_heap_sector = 9;
|
||||
const test_read_bytes = 1024; // the two-cluster test files are 1024 bytes
|
||||
const test_notes_bytes = 1500; // a three-cluster write in the create/write test
|
||||
const test_tail_bytes = 4;
|
||||
const test_inner_bytes = 300;
|
||||
const test_rename_bytes = 800;
|
||||
|
||||
fn testCluster(cluster: u32) usize {
|
||||
return (test_heap_sector + (cluster - 2)) * sector_size;
|
||||
@@ -728,3 +1153,126 @@ test "read a fragmented file follows the FAT across non-contiguous clusters" {
|
||||
try std.testing.expectEqual(@as(usize, 10), mid);
|
||||
for (readback[0..10], 600..) |byte, i| try std.testing.expectEqual(@as(u8, @truncate(i +% 100)), byte);
|
||||
}
|
||||
|
||||
test "create, write across clusters, and read back (FAT-linked)" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 128 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
formatExfat(bytes);
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
fs.current_time_epoch = 1_700_000_000;
|
||||
|
||||
var node = fs.createFile(fs.rootNode(), "NOTES.TXT").?;
|
||||
var payload: [test_notes_bytes]u8 = undefined; // three 512-byte clusters
|
||||
for (&payload, 0..) |*b, i| b.* = @truncate(i *% 7);
|
||||
try std.testing.expectEqual(@as(usize, payload.len), fs.writeFile(&node, 0, &payload));
|
||||
|
||||
// Re-resolve from the directory (proving persistence) and read back.
|
||||
const resolved = fs.resolve("/NOTES.TXT").?;
|
||||
try std.testing.expectEqual(@as(u32, payload.len), resolved.size);
|
||||
try std.testing.expect(!resolved.no_fat_chain); // this engine writes FAT-linked
|
||||
var readback: [test_notes_bytes]u8 = undefined;
|
||||
try std.testing.expectEqual(@as(usize, payload.len), fs.readFile(resolved, 0, &readback));
|
||||
try std.testing.expectEqualSlices(u8, &payload, &readback);
|
||||
|
||||
// The root now lists NOTES.TXT alongside the seeded files.
|
||||
try std.testing.expect(fs.resolve("/notes.txt") != null); // case-insensitive
|
||||
const mtime = resolved.mtime;
|
||||
try std.testing.expect(mtime != 0); // the write stamped it
|
||||
}
|
||||
|
||||
test "truncate frees the chain and empties the file" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 128 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
formatExfat(bytes);
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
fs.current_time_epoch = 1_700_000_000;
|
||||
|
||||
var node = fs.createFile(fs.rootNode(), "TMP").?;
|
||||
var payload = [_]u8{0xCD} ** 1000;
|
||||
_ = fs.writeFile(&node, 0, &payload);
|
||||
const cluster = fs.resolve("/TMP").?.first_cluster;
|
||||
try std.testing.expect(fs.validCluster(cluster));
|
||||
try std.testing.expect(fs.isAllocated(cluster));
|
||||
|
||||
fs.truncate(&node);
|
||||
const empty = fs.resolve("/TMP").?;
|
||||
try std.testing.expectEqual(@as(u32, 0), empty.size);
|
||||
try std.testing.expect(!fs.isAllocated(cluster)); // its cluster is free again
|
||||
|
||||
// Overwrite after truncate leaves no stale tail.
|
||||
var fresh = [_]u8{0xEE} ** 4;
|
||||
_ = fs.writeFile(&node, 0, &fresh);
|
||||
var readback: [test_tail_bytes]u8 = undefined;
|
||||
_ = fs.readFile(fs.resolve("/TMP").?, 0, &readback);
|
||||
try std.testing.expectEqualSlices(u8, &fresh, &readback);
|
||||
}
|
||||
|
||||
test "remove deletes the entry and frees its chain" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 128 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
formatExfat(bytes);
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
fs.current_time_epoch = 1_700_000_000;
|
||||
|
||||
var node = fs.createFile(fs.rootNode(), "GONE").?;
|
||||
_ = fs.writeFile(&node, 0, &[_]u8{0x11} ** 700);
|
||||
const cluster = fs.resolve("/GONE").?.first_cluster;
|
||||
try std.testing.expect(fs.removeFile(fs.rootNode(), "GONE"));
|
||||
try std.testing.expect(fs.resolve("/GONE") == null);
|
||||
try std.testing.expect(!fs.isAllocated(cluster));
|
||||
try std.testing.expect(!fs.removeFile(fs.rootNode(), "GONE")); // already gone
|
||||
}
|
||||
|
||||
test "create a subdirectory and a file inside it" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 128 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
formatExfat(bytes);
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
fs.current_time_epoch = 1_700_000_000;
|
||||
|
||||
const dir = fs.createDirectory(fs.rootNode(), "SUB").?;
|
||||
try std.testing.expect(dir.is_directory);
|
||||
// exFAT directories carry no "." / ".." — a fresh one lists nothing.
|
||||
try std.testing.expect(fs.listEntry(dir, 0) == null);
|
||||
|
||||
const resolved_dir = fs.resolve("/SUB").?;
|
||||
var inner = fs.createFile(resolved_dir, "INNER.DAT").?;
|
||||
_ = fs.writeFile(&inner, 0, &[_]u8{0xA5} ** 300);
|
||||
const inner_node = fs.resolve("/SUB/INNER.DAT").?;
|
||||
try std.testing.expectEqual(@as(u32, 300), inner_node.size);
|
||||
var readback: [test_inner_bytes]u8 = undefined;
|
||||
_ = fs.readFile(inner_node, 0, &readback);
|
||||
for (readback) |b| try std.testing.expectEqual(@as(u8, 0xA5), b);
|
||||
// The subdirectory now lists exactly its one file.
|
||||
const listing = fs.listEntry(fs.resolve("/SUB").?, 0).?;
|
||||
try std.testing.expectEqualStrings("INNER.DAT", listing.name_buffer[0..listing.name_len]);
|
||||
}
|
||||
|
||||
test "rename keeps the file's contents under the new name" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 128 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
formatExfat(bytes);
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
fs.current_time_epoch = 1_700_000_000;
|
||||
|
||||
var node = fs.createFile(fs.rootNode(), "OLD.TXT").?;
|
||||
var payload = [_]u8{0x5A} ** 800;
|
||||
_ = fs.writeFile(&node, 0, &payload);
|
||||
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").?;
|
||||
try std.testing.expectEqual(@as(u32, 800), renamed.size);
|
||||
var readback: [test_rename_bytes]u8 = undefined;
|
||||
_ = fs.readFile(renamed, 0, &readback);
|
||||
try std.testing.expectEqualSlices(u8, &payload, &readback);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user