// pwfile: /etc/passwd line parser. // // Pure, allocation-free, no externs. One parser for the account database, // shared by every consumer that previously rolled its own or had none: // * the kernel's sys_passwd (src/sys.zig) — maps the caller's uid back // to a login name to enforce "non-root may only change its own record" // * /bin/login (tools/login_elf.zig) — name → {uid, gid, shell} for the // privilege drop after authentication // * fsh's whoami builtin (user_space/fsh/fsh.zig) — uid → name // // Line format: `user:uid:gid:home:shell` (exactly 5 colon-delimited // fields). /etc/passwd itself stays an initramfs file — the account LIST // is build-time-immutable; only passwords (/etc/shadow, /mnt/shadow) are // mutable state. The host tests below pin the format against // user_space/etc/passwd. pub const Entry = struct { user []u8, uid u32, gid u32, home []u8, shell []u8, } // Find the entry whose login name equals `name`. Returns null when absent // or when the matching line is malformed. pub fn lookupByName(content []u8, name []u8) ?Entry { var it = LineIter{ .content = content } while it.next() |line| { const e = parseLine(line) orelse continue if (bytesEqual(e.user, name)) { return e } } return null } // Find the entry whose uid equals `uid`. First match wins (uids are // unique in the seed database). Returns null when absent. pub fn lookupByUid(content []u8, uid u32) ?Entry { var it = LineIter{ .content = content } while it.next() |line| { const e = parseLine(line) orelse continue if (e.uid == uid) { return e } } return null } // Split one passwd line (no trailing newline) into its five fields. // Returns null on a missing or extra field or a non-decimal uid/gid. pub fn parseLine(line []u8) ?Entry { var fields [5][]u8 = undefined var nf usize = 0 var fstart usize = 0 var j usize = 0 while (j <= line.len) { if (j == line.len) || (line[j] == ':') { if (nf == 5) { return null } // a 6th field is malformed fields[nf] = line[fstart..j] nf += 1 fstart = j + 1 } j += 1 } if (nf != 5) { return null } if (fields[0].len == 0) { return null } const uid = parseDecimalU32(fields[1]) orelse return null const gid = parseDecimalU32(fields[2]) orelse return null return .{ .user = fields[0], .uid = uid, .gid = gid, .home = fields[3], .shell = fields[4], } } const LineIter = struct { content []u8, pos usize = 0, fn next(self *mut LineIter) ?[]u8 { if (self.pos >= self.content.len) { return null } const start = self.pos var end = start while (end < self.content.len) && (self.content[end] != '\n') { end += 1 } self.pos = end + 1 var line = self.content[start..end] // CRLF tolerance, mirroring overlay.parse. if (line.len > 0) && (line[line.len - 1] == '\r') { line = line[0 .. line.len - 1] } return line } } fn bytesEqual(a []u8, b []u8) bool { if (a.len != b.len) { return false } var i usize = 0 while (i < a.len) { if (a[i] != b[i]) { return false } i += 1 } return true } // Decimal u32 parse, exact (no sign, no whitespace). fn parseDecimalU32(s []u8) ?u32 { if (s.len == 0) { return null } var v u64 = 0 for c in s { if (c < '0') || (c > '9') { return null } v = v * 10 + (c - '0') if (v > 0xffff_ffff) { return null } } return #intCast(v) } // ---- Host tests ---- const std = #import("std") const testing = std.testing // Mirrors user_space/etc/passwd. const FIXTURE = "root:0:0:/root:/bin/fsh\n" ++ "flash:1000:1000:/home/flash:/bin/fsh\n" test "lookupByName: finds root and flash" { const root = lookupByName(FIXTURE, "root").? try testing.expectEqual(#as(u32, 0), root.uid) try testing.expectEqual(#as(u32, 0), root.gid) try testing.expectEqualStrings("/bin/fsh", root.shell) const flash = lookupByName(FIXTURE, "flash").? try testing.expectEqual(#as(u32, 1000), flash.uid) try testing.expectEqualStrings("/home/flash", flash.home) } test "lookupByName: misses an absent user" { try testing.expectEqual(#as(?Entry, null), lookupByName(FIXTURE, "anton")) // Prefix of an existing name must not match. try testing.expectEqual(#as(?Entry, null), lookupByName(FIXTURE, "fla")) } test "lookupByUid: reverse lookup finds the right record" { const flash = lookupByUid(FIXTURE, 1000).? try testing.expectEqualStrings("flash", flash.user) const root = lookupByUid(FIXTURE, 0).? try testing.expectEqualStrings("root", root.user) } test "lookupByUid: misses an absent uid" { try testing.expectEqual(#as(?Entry, null), lookupByUid(FIXTURE, 4711)) } test "parseLine: rejects missing / extra fields and bad numbers" { try testing.expectEqual(#as(?Entry, null), parseLine("flash:1000:1000:/home/flash")) try testing.expectEqual(#as(?Entry, null), parseLine("flash:1000:1000:/home/flash:/bin/fsh:extra")) try testing.expectEqual(#as(?Entry, null), parseLine("flash:10x0:1000:/home/flash:/bin/fsh")) try testing.expectEqual(#as(?Entry, null), parseLine(":0:0:/root:/bin/fsh")) try testing.expectEqual(#as(?Entry, null), parseLine("")) } test "lookups skip malformed lines instead of failing the file" { const mixed = "# not a passwd line at all\n" ++ "root:0:0:/root:/bin/fsh\n" const root = lookupByName(mixed, "root").? try testing.expectEqual(#as(u32, 0), root.uid) } test "CRLF line endings are tolerated" { const crlf = "root:0:0:/root:/bin/fsh\r\nflash:1000:1000:/home/flash:/bin/fsh\r\n" const flash = lookupByName(crlf, "flash").? try testing.expectEqual(#as(u32, 1000), flash.uid) try testing.expectEqualStrings("/bin/fsh", flash.shell) }