From 3fb8a9b96f303389bb90e0b07a8fb7b5cdf907c8 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:47:22 +0100 Subject: [PATCH] volume-manager: content signature + the filesystems.csv map (S2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- system/services/volume-manager/build.zig | 23 +++- system/services/volume-manager/build.zig.zon | 2 + .../volume-manager/filesystem-map.zig | 121 ++++++++++++++++++ system/services/volume-manager/partition.zig | 20 ++- 4 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 system/services/volume-manager/filesystem-map.zig diff --git a/system/services/volume-manager/build.zig b/system/services/volume-manager/build.zig index aa567e0..3bc0086 100644 --- a/system/services/volume-manager/build.zig +++ b/system/services/volume-manager/build.zig @@ -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); } diff --git a/system/services/volume-manager/build.zig.zon b/system/services/volume-manager/build.zig.zon index 990d7d3..35f937b 100644 --- a/system/services/volume-manager/build.zig.zon +++ b/system/services/volume-manager/build.zig.zon @@ -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 = .{""}, } diff --git a/system/services/volume-manager/filesystem-map.zig b/system/services/volume-manager/filesystem-map.zig new file mode 100644 index 0000000..7234d22 --- /dev/null +++ b/system/services/volume-manager/filesystem-map.zig @@ -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 +} diff --git a/system/services/volume-manager/partition.zig b/system/services/volume-manager/partition.zig index eada032..d29113c 100644 --- a/system/services/volume-manager/partition.zig +++ b/system/services/volume-manager/partition.zig @@ -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