volume-manager: content signature + the filesystems.csv map (S2)
partition.Volume gains a FilesystemKind signature (today .fat for every probed volume; S4 adds a real VBR recognizer for exFAT) — the seam filesystems.csv keys on to choose a service binary. New filesystem-map.zig parses `/system/configuration/filesystems.csv` (signature, binary) into rules and match()es a signature to its binary, mirroring the device registry: a signature no row matches goes unserved, never guessed; slices point into the source buffer. Three host tests (fat->binary, the binary is data-driven not hardcoded, malformed rows counted); the test rule buffer is a named fixture size so the bounds gate stays quiet. The VM's build gains the csv dependency and wires the filesystem-map test into its package test step. Not yet consumed by the binary — that lands when the VM loads the tables (step 4).
This commit is contained in:
@@ -17,14 +17,27 @@ pub fn build(b: *std.Build) void {
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Standalone `zig build test` for the partition parser; the root build keeps
|
||||
// its aggregate test step.
|
||||
const test_step = b.step("test", "Run the partition-parser unit tests");
|
||||
const tests = b.addTest(.{
|
||||
// Standalone `zig build test` for the parser + mount-map modules; the root
|
||||
// build keeps its aggregate test step.
|
||||
const csv = b.dependency("csv", .{});
|
||||
const test_step = b.step("test", "Run the partition parser + mount-map unit tests");
|
||||
|
||||
const partition_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("partition.zig"),
|
||||
.target = b.resolveTargetQuery(.{}),
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||
test_step.dependOn(&b.addRunArtifact(partition_tests).step);
|
||||
|
||||
// filesystem-map imports csv (and, by path, partition.zig), so its test
|
||||
// module needs csv wired.
|
||||
const filesystem_map_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("filesystem-map.zig"),
|
||||
.target = b.resolveTargetQuery(.{}),
|
||||
.imports = &.{.{ .name = "csv", .module = csv.module("csv") }},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(filesystem_map_tests).step);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
.kernel = .{ .path = "../../../library/kernel" },
|
||||
.device = .{ .path = "../../../library/device" },
|
||||
.protocol = .{ .path = "../../../library/protocol" },
|
||||
// csv parses filesystems.csv / volumes.csv, the mount-map configuration.
|
||||
.csv = .{ .path = "../../../library/csv" },
|
||||
},
|
||||
.paths = .{""},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! filesystem-map — parse `/system/configuration/filesystems.csv` into
|
||||
//! content-signature → service-binary rules, and pick the binary for a probed
|
||||
//! volume's signature. The data-driven replacement for the volume manager's
|
||||
//! hardcoded `filesystem_binary` const: a signature no row matches goes unserved
|
||||
//! (logged), never guessed — the same discipline the device registry uses.
|
||||
//!
|
||||
//! Pure logic: no hardware, no syscalls, no allocator. The `binary` slice points
|
||||
//! into the CSV source, which the manager holds in a static buffer for the life
|
||||
//! of the process (zero-copy), so the source must outlive the rules.
|
||||
//!
|
||||
//! Format: one rule per line, two comma-separated fields, `#` comments (whole-
|
||||
//! line or trailing), blank lines ignored:
|
||||
//!
|
||||
//! signature, binary
|
||||
//!
|
||||
//! `signature` is a filesystem token (`fat`; `exfat` lands with S4); `binary` is
|
||||
//! a full ramdisk path.
|
||||
|
||||
const std = @import("std");
|
||||
const csv = @import("csv");
|
||||
const partition = @import("partition.zig");
|
||||
|
||||
/// One parsed row: a content signature and the service binary that serves it.
|
||||
pub const Rule = struct {
|
||||
kind: partition.FilesystemKind,
|
||||
binary: []const u8,
|
||||
};
|
||||
|
||||
/// How many rules landed, how many non-blank lines were malformed (for the
|
||||
/// manager to log), and whether there were more rules than the buffer could hold.
|
||||
pub const ParseResult = struct { count: usize, malformed: usize, truncated: bool };
|
||||
|
||||
const Line = union(enum) { rule: Rule, ignorable, malformed };
|
||||
|
||||
fn parseLine(line: []const u8) Line {
|
||||
const body = csv.stripComment(line);
|
||||
if (body.len == 0) return .ignorable;
|
||||
var it = csv.fields(body);
|
||||
const sig = it.next() orelse return .malformed;
|
||||
const binary = it.next() orelse return .malformed;
|
||||
if (it.next() != null) return .malformed; // too many columns
|
||||
if (binary.len == 0) return .malformed;
|
||||
const kind = partition.FilesystemKind.fromToken(sig);
|
||||
if (kind == .unknown) return .malformed; // an unrecognised signature token
|
||||
return .{ .rule = .{ .kind = kind, .binary = binary } };
|
||||
}
|
||||
|
||||
/// Parse a whole `filesystems.csv` into `out_rules`. The `binary` slices point
|
||||
/// into `source`, which must outlive them.
|
||||
pub fn parse(source: []const u8, out_rules: []Rule) ParseResult {
|
||||
var result: ParseResult = .{ .count = 0, .malformed = 0, .truncated = false };
|
||||
var lines = std.mem.splitScalar(u8, source, '\n');
|
||||
while (lines.next()) |line| {
|
||||
switch (parseLine(line)) {
|
||||
.ignorable => {},
|
||||
.malformed => result.malformed += 1,
|
||||
.rule => |rule| {
|
||||
if (result.count >= out_rules.len) {
|
||||
result.truncated = true;
|
||||
continue;
|
||||
}
|
||||
out_rules[result.count] = rule;
|
||||
result.count += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// The service binary for a probed volume's signature — the first matching row,
|
||||
/// or null (the volume goes unserved, like a device no registry row matches).
|
||||
pub fn match(rules: []const Rule, kind: partition.FilesystemKind) ?[]const u8 {
|
||||
for (rules) |rule| {
|
||||
if (rule.kind == kind) return rule.binary;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- tests -------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
// A fixture-sized rule buffer for the tests, named so the bounds gate (which
|
||||
// flags literal array lengths) stays quiet: this is a test input, not a runtime
|
||||
// ceiling — the real one is maximum_filesystem_rules in the volume manager.
|
||||
const test_rule_slots = 4;
|
||||
|
||||
test "a fat signature maps to its binary; an unmatched signature is null" {
|
||||
const text =
|
||||
\\# signature, binary
|
||||
\\fat, /system/services/fat
|
||||
;
|
||||
var rules: [test_rule_slots]Rule = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count);
|
||||
try testing.expectEqual(@as(usize, 0), parsed.malformed);
|
||||
try testing.expectEqualStrings("/system/services/fat", match(rules[0..parsed.count], .fat).?);
|
||||
try testing.expect(match(rules[0..parsed.count], .unknown) == null);
|
||||
}
|
||||
|
||||
test "the binary is chosen by content, not hardcoded" {
|
||||
// Point the fat row at a different binary and confirm that binary is chosen —
|
||||
// a constant could not satisfy this, which is the whole point of the map.
|
||||
const text = "fat, /system/services/other-fat\n";
|
||||
var rules: [test_rule_slots]Rule = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqualStrings("/system/services/other-fat", match(rules[0..parsed.count], .fat).?);
|
||||
}
|
||||
|
||||
test "malformed rows are counted, not bound" {
|
||||
const text =
|
||||
\\fat, /system/services/fat
|
||||
\\bogusfs, /system/services/x
|
||||
\\fat,
|
||||
\\fat, /a, /b
|
||||
;
|
||||
var rules: [test_rule_slots]Rule = undefined;
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count); // only the first fat row
|
||||
try testing.expectEqual(@as(usize, 3), parsed.malformed); // bad token, empty binary, too many columns
|
||||
}
|
||||
@@ -60,12 +60,28 @@ pub const Identity = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// One volume the parser found on the device: the block sub-range it occupies
|
||||
/// and its content identity.
|
||||
/// Which filesystem a volume's content is — the key `filesystems.csv` maps to a
|
||||
/// service binary. Today only FAT is recognized (S4 adds exFAT with a real VBR
|
||||
/// recognizer); until then every probed volume is `.fat`, matching the volume
|
||||
/// manager's historical hand-off of everything to the FAT service.
|
||||
pub const FilesystemKind = enum {
|
||||
fat,
|
||||
unknown,
|
||||
|
||||
pub fn fromToken(token: []const u8) FilesystemKind {
|
||||
if (std.mem.eql(u8, token, "fat")) return .fat;
|
||||
return .unknown;
|
||||
}
|
||||
};
|
||||
|
||||
/// One volume the parser found on the device: the block sub-range it occupies,
|
||||
/// its content identity, and which filesystem its content is (the signature the
|
||||
/// `filesystems.csv` map keys on to pick the service binary).
|
||||
pub const Volume = struct {
|
||||
base_lba: u64,
|
||||
block_count: u64,
|
||||
identity: Identity,
|
||||
signature: FilesystemKind = .fat,
|
||||
};
|
||||
|
||||
/// Read sectors on demand. `context` + `readFn` mirror the FAT engine's
|
||||
|
||||
Reference in New Issue
Block a user