A snippet for parsing `/etc/passwd`

I was answering some question on Discord and dug into parsing /etc/passwd, I don’t have a use for it but maybe this can be useful for someone.

const Entry = struct {
    name: []const u8,
    password: []const u8,
    uid: []const u8,
    gid: []const u8,
    gecos: []const u8,
    directory: []const u8,
    shell: []const u8,

    pub fn format(self: *const Entry, writer: *Io.Writer) Io.Writer.Error!void {
        return try writer.print(
            \\.{{.name = "{s}", .password = "{s}", .uid = "{s}", .gid = "{s}", .gecos = "{s}", .directory = "{s}", .shell = "{s}"}}"
        ,
            .{ self.name, self.password, self.uid, self.gid, self.gecos, self.directory, self.shell },
        );
    }
};

const PsswdParser = struct {
    content: std.ArrayList(u8) = .empty,
    reader: *Io.Reader,

    pub fn init(reader: *Io.Reader) PsswdParser {
        return .{ .reader = reader };
    }
    pub fn deinit(self: *PsswdParser, gpa: Allocator) void {
        self.content.deinit(gpa);
    }

    pub fn next(self: *PsswdParser, gpa: Allocator) error{ ReadFailed, OutOfMemory }!?Entry {
        self.content.clearRetainingCapacity();

        const fields = @typeInfo(Entry).@"struct".fields;
        var ranges: [fields.len]struct { start: usize, end: usize } = undefined;
        var count: u8 = 0;
        var start: usize = 0;

        while (self.reader.takeByte()) |c| {
            if (c == '\n') {
                ranges[count] = .{ .start = start, .end = self.content.items.len };
                count += 1;
                break;
            }

            try self.content.append(gpa, c);

            if (c == ':') {
                ranges[count] = .{ .start = start, .end = self.content.items.len - 1 };
                count += 1;
                start = self.content.items.len;
            }
        } else |e| {
            switch (e) {
                error.EndOfStream => {
                    if (self.content.items.len == 0) return null;
                    ranges[count] = .{ .start = start, .end = self.content.items.len - 1 };
                    count += 1;
                },
                else => |v| return v,
            }
        }

        if (count != fields.len) {
            return error.Malformed;
        }

        return .{
            .name = self.content.items[ranges[0].start..ranges[0].end],
            .password = self.content.items[ranges[1].start..ranges[1].end],
            .uid = self.content.items[ranges[2].start..ranges[2].end],
            .gid = self.content.items[ranges[3].start..ranges[3].end],
            .gecos = self.content.items[ranges[4].start..ranges[4].end],
            .directory = self.content.items[ranges[5].start..ranges[5].end],
            .shell = self.content.items[ranges[6].start..ranges[6].end],
        };
    }
};

pub fn main(init: Init) !void {
    const f = try Io.Dir.openFileAbsolute(init.io, "/etc/passwd", .{});

    var buffer: [2048]u8 = undefined;
    var streaming_reader = f.readerStreaming(init.io, &buffer);
    const reader: *Io.Reader = &streaming_reader.interface;

    var parser: PsswdParser = .init(reader);
    defer parser.deinit(init.gpa);

    const uid = std.os.linux.getuid();

    while (try parser.next(init.gpa)) |entry| {
        if (uid != try std.fmt.parseInt(std.os.linux.uid_t, entry.uid, 10)) {
            continue;
        }
        std.debug.print("{f}\n", .{entry});
    }
}

const std = @import("std");
const Allocator = std.mem.Allocator;
const Init = std.process.Init;
const Io = std.Io;
15 Likes

FYI the standard library also provides this functionality.

I haven’t dug deep to see what the differences are between yours and std’s but might be worth looking at it for pedagogic reasons.

https://codeberg.org/ziglang/zig/src/commit/e5dc5a6eb5608c100e78d6116942a4cd17f56d00/lib/std/process.zig#L142

2 Likes

huh, I was totally not aware of that, thank you very much.

I checked the code, there is actually no way to get the user information by supplying a user id which makes difficult to use since I don’t think there’s a function to get the username at all. Also it only supplies user id and group id from the username, so it is pretty different from the snippet I provided above.

1 Like

I also looked for a way using std to get the user- and groupnames. And there seems to be none. Very likely because its very platform independent.

For now I’m using std.c.getpwuid and std.c.getgrgid.

But since thats my last dependency on libc, at least when building on Linux, I would like to replace it with pure Zig. Will def try out your snippet!

Zig has std.os.linux.getuid and std.os.linux.getgid already so you don’t have to rely on libc for that.

Yes, but that only returns the user ID as u32. I still need to look up the username in /etc/passwd.

std.c.getpwuid returns a struct which contains a name field where the username of the given uid is resolved. Same accounts for the group version of the functions.

I just mentioned them since you linked to the C versions of them, just wanted to make sure you know that there Zig versions of them also. I’m blind, sorry.

Overlooking stuff like that is a common experience for me :wink:

But your snippet looks very similar to the struct the libc function returns. Always thought myself that i’ll maybe try to impl it in Zig, since parsing /etc/passwd isn’t that hard. But now i’ll give your snipprt a shot first :slight_smile:

2 Likes

I’ve refactored the code a little bit. Using a state machine like approach (heavily inspired by std.process.posixGetUserInfoPasswdStream).

Furthermore, there is the possibility to parse either /etc/passwd or /etc/group. And the returned structs are simplified, defining only name, gid and uid. On the other hand, no allocator is needed with this approach.

Long code snippet
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const user = UserInfo;
const group = GroupInfo;

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    var buf: [4096]u8 = undefined;

    const group_file = try Io.Dir.openFileAbsolute(io, "/etc/group", .{});
    defer group_file.close(io);
    var greader = group_file.reader(io, &buf);
    var group_info = try group.init(&greader.interface);

    while (try group_info.next()) |e| {
        std.debug.print("{f}\n", .{e});
    }

    const passwd_file = try Io.Dir.openFileAbsolute(io, "/etc/passwd", .{});
    defer passwd_file.close(io);
    var freader = passwd_file.reader(io, &buf);
    var user_info = try user.init(&freader.interface);

    while (try user_info.next()) |e| {
        std.debug.print("{f}\n", .{e});
    }
}

const UserEntry = struct {
    name: []const u8,
    uid: u32,
    gid: u32,

    pub fn format(self: *const UserEntry, writer: *Io.Writer) Io.Writer.Error!void {
        return try writer.print(
            \\.{{ .name = "{s}", .uid = "{d}", .gid = "{d}" }}"
        , .{
            self.name,
            self.uid,
            self.gid,
        });
    }
};

const GroupEntry = struct {
    name: []const u8,
    gid: u32,

    pub fn format(self: *const GroupEntry, writer: *Io.Writer) Io.Writer.Error!void {
        return try writer.print(
            \\.{{ .name = "{s}", .gid = "{d}" }}"
        , .{
            self.name,
            self.gid,
        });
    }
};

const UserInfo = struct {
    reader: *Io.Reader,

    var buffer: [4096]u8 = undefined;

    pub fn init(reader: *Io.Reader) !UserInfo {
        return .{ .reader = reader };
    }

    pub fn next(self: *UserInfo) !?UserEntry {
        const entry = parsePasswdOrGroup(.user, self.reader) catch |err| switch (err) {
            error.EndOfStream => return null,
            else => return err,
        };
        return entry;
    }
};

const GroupInfo = struct {
    reader: *Io.Reader,

    var buffer: [4096]u8 = undefined;

    pub fn init(reader: *Io.Reader) !GroupInfo {
        return .{ .reader = reader };
    }

    pub fn next(self: *GroupInfo) !?GroupEntry {
        const entry = parsePasswdOrGroup(.group, self.reader) catch |err| switch (err) {
            error.EndOfStream => return null,
            else => return err,
        };
        return entry;
    }
};

const InfoType = enum {
    user,
    group,
};

fn parsePasswdOrGroup(comptime kind: InfoType, reader: *std.Io.Reader) !switch (kind) {
    .user => UserEntry,
    .group => GroupEntry,
} {
    const State = enum {
        read_name,
        wait_for_next_line,
        skip_password,
        read_user_id,
        read_group_id,
    };

    const name_start: usize = reader.seek;
    var name_index: usize = name_start;
    var name: []const u8 = undefined;
    var uid: u32 = 0;
    var gid: u32 = 0;

    sw: switch (State.read_name) {
        .read_name => switch (try reader.takeByte()) {
            ':' => {
                name = reader.buffer[name_start..name_index];
                continue :sw .skip_password;
            },
            '\n' => return error.CorruptPasswordFile,
            else => {
                name_index += 1;
                continue :sw .read_name;
            },
        },
        .wait_for_next_line => switch (try reader.takeByte()) {
            '\n' => {
                switch (kind) {
                    .user => {
                        return .{
                            .name = name,
                            .uid = uid,
                            .gid = gid,
                        };
                    },
                    .group => {
                        return .{
                            .name = name,
                            .gid = gid,
                        };
                    },
                }
            },
            else => continue :sw .wait_for_next_line,
        },
        .skip_password => switch (try reader.takeByte()) {
            '\n' => return error.CorruptPasswordFile,
            ':' => {
                switch (kind) {
                    .user => continue :sw .read_user_id,
                    .group => continue :sw .read_group_id,
                }
            },
            else => continue :sw .skip_password,
        },
        .read_user_id => switch (try reader.takeByte()) {
            ':' => {
                continue :sw .read_group_id;
            },
            '\n' => return error.CorruptPasswordFile,
            else => |byte| {
                const digit = switch (byte) {
                    '0'...'9' => byte - '0',
                    else => return error.CorruptPasswordFile,
                };
                {
                    const ov = @mulWithOverflow(uid, 10);
                    if (ov[1] != 0) return error.CorruptPasswordFile;
                    uid = ov[0];
                }
                {
                    const ov = @addWithOverflow(uid, digit);
                    if (ov[1] != 0) return error.CorruptPasswordFile;
                    uid = ov[0];
                }
                continue :sw .read_user_id;
            },
        },
        .read_group_id => switch (try reader.takeByte()) {
            '\n', ':' => {
                continue :sw .wait_for_next_line;
            },
            else => |byte| {
                const digit = switch (byte) {
                    '0'...'9' => byte - '0',
                    else => return error.CorruptPasswordFile,
                };
                {
                    const ov = @mulWithOverflow(gid, 10);
                    if (ov[1] != 0) return error.CorruptPasswordFile;
                    gid = ov[0];
                }
                {
                    const ov = @addWithOverflow(gid, digit);
                    if (ov[1] != 0) return error.CorruptPasswordFile;
                    gid = ov[0];
                }
                continue :sw .read_group_id;
            },
        },
    }
    comptime unreachable;
}

There might be very inefficient parts in the code since I’m still learning…

I also don’t wanted to capture this thread, but my code is just a follow-up inspired by @hachanuy 's initial snippet :slightly_smiling_face: