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);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
// Standalone `zig build test` for the partition parser; the root build keeps
|
// Standalone `zig build test` for the parser + mount-map modules; the root
|
||||||
// its aggregate test step.
|
// build keeps its aggregate test step.
|
||||||
const test_step = b.step("test", "Run the partition-parser unit tests");
|
const csv = b.dependency("csv", .{});
|
||||||
const tests = b.addTest(.{
|
const test_step = b.step("test", "Run the partition parser + mount-map unit tests");
|
||||||
|
|
||||||
|
const partition_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path("partition.zig"),
|
.root_source_file = b.path("partition.zig"),
|
||||||
.target = b.resolveTargetQuery(.{}),
|
.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" },
|
.kernel = .{ .path = "../../../library/kernel" },
|
||||||
.device = .{ .path = "../../../library/device" },
|
.device = .{ .path = "../../../library/device" },
|
||||||
.protocol = .{ .path = "../../../library/protocol" },
|
.protocol = .{ .path = "../../../library/protocol" },
|
||||||
|
// csv parses filesystems.csv / volumes.csv, the mount-map configuration.
|
||||||
|
.csv = .{ .path = "../../../library/csv" },
|
||||||
},
|
},
|
||||||
.paths = .{""},
|
.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
|
/// Which filesystem a volume's content is — the key `filesystems.csv` maps to a
|
||||||
/// and its content identity.
|
/// 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 {
|
pub const Volume = struct {
|
||||||
base_lba: u64,
|
base_lba: u64,
|
||||||
block_count: u64,
|
block_count: u64,
|
||||||
identity: Identity,
|
identity: Identity,
|
||||||
|
signature: FilesystemKind = .fat,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Read sectors on demand. `context` + `readFn` mirror the FAT engine's
|
/// Read sectors on demand. `context` + `readFn` mirror the FAT engine's
|
||||||
|
|||||||
Reference in New Issue
Block a user