hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 6ef232b174283656dddc6ee3822d4bc01c06ae0b
parent 04eda2e65fa49b4e7af0aedb55cc41131a97b9a5
Author: Drew DeVault <sir@cmpwn.com>
Date:   Thu, 30 May 2024 11:04:48 +0200

unix::passwd: use bufio::scanner and refine API

Programs which enumerate all users or groups on the system are affected
by the breaking change. The API is refined such that the user must
create a parser object, from which each entry in the passwd or group
file is borrowed while iterating. Users of one-off functions (e.g.
unix::passwd::getuid) are not affected.

This is a breaking change.

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Munix/passwd/group.ha | 246+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Munix/passwd/passwd.ha | 174++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
2 files changed, 226 insertions(+), 194 deletions(-)

diff --git a/unix/passwd/group.ha b/unix/passwd/group.ha @@ -2,6 +2,7 @@ // (c) Hare authors <https://harelang.org> use bufio; +use encoding::utf8; use io; use memio; use os; @@ -17,31 +18,51 @@ export type grent = struct { password: str, // Numerical group ID gid: unix::gid, - // List of usernames that are members of this group - userlist: []str, + // List of usernames that are members of this group, separated by commas + userlist: str, }; -// Reads a Unix-like group entry from an [[io::handle]]. The caller must free -// the return value using [[grent_finish]]. -export fn nextgr(in: io::handle) (grent | io::EOF | io::error | invalid) = { - let line = match (bufio::read_line(in)?) { - case let ln: []u8 => - yield ln; - case io::EOF => - return io::EOF; - }; - let line = match (strings::fromutf8(line)) { - case let s: str => - yield s; - case => - return invalid; +export type groupreader = struct { + scan: bufio::scanner, +}; + +// Creates a parser for an /etc/groups-formatted file. Use [[nextgr]] to +// enumerate the groups, and [[groups_finish]] to free resources associated with +// the reader. +export fn groups_read(in: io::handle) groupreader = { + return groupreader { + scan = bufio::newscanner(in), }; +}; - let fields = strings::split(line, ":"); - defer free(fields); +// Frees resources associated with a [[groupreader]]. +export fn groups_finish(rd: *groupreader) void = { + bufio::finish(&rd.scan); +}; - if (len(fields) != 4) { +// Reads a Unix-like group entry from a [[grreader]]. The return value is +// borrowed from the scanner. +export fn nextgr(rd: *groupreader) (grent | io::EOF | io::error | invalid) = { + const line = match (bufio::scan_line(&rd.scan)) { + case let ln: const str => + yield ln; + case let err: io::error => + return err; + case utf8::invalid => return invalid; + case io::EOF => + return io::EOF; + }; + const tok = strings::tokenize(line, ":"); + + let i = 0z; + let fields: [4]str = [""...]; + for (const f => strings::next_token(&tok)) { + defer i += 1; + if (i >= len(fields)) { + return invalid; + }; + fields[i] = f; }; let gid = match (strconv::stou64(fields[2])) { @@ -52,26 +73,43 @@ export fn nextgr(in: io::handle) (grent | io::EOF | io::error | invalid) = { }; return grent { - // Borrows the return value of bufio::read_line name = fields[0], password = fields[1], gid = gid, - userlist = strings::split(fields[3], ","), + userlist = fields[3], }; }; -// Frees resources associated with [[grent]]. +// Frees resources associated with a [[grent]]. export fn grent_finish(ent: *grent) void = { free(ent.name); + free(ent.password); free(ent.userlist); }; +// Frees resources associated with a slice of [[grent]]s. +export fn grents_free(ents: []grent) void = { + for (let ent &.. ents) { + grent_finish(ent); + }; + free(ents); +}; + +fn grent_dup(ent: *grent) void = { + ent.name = strings::dup(ent.name); + ent.password = strings::dup(ent.password); + ent.userlist = strings::dup(ent.userlist); +}; + // Looks up a group by name in a Unix-like group file. It expects a such file at // /etc/group. Aborts if that file doesn't exist or is not properly formatted. // +// The user must pass the return value to [[grent_finish]] to free resources +// associated with the group. +// // See [[nextgr]] for low-level parsing API. export fn getgroup(name: str) (grent | void) = { - let file = match (os::open("/etc/group")) { + const file = match (os::open("/etc/group")) { case let f: io::file => yield f; case => @@ -79,33 +117,25 @@ export fn getgroup(name: str) (grent | void) = { }; defer io::close(file)!; - let rbuf: [os::BUFSZ]u8 = [0...]; - let strm = bufio::init(file, rbuf, []); - for (true) { - let ent = match (nextgr(&strm)) { - case let e: grent => - yield e; - case io::EOF => - break; - case => - abort("Invalid entry in /etc/group"); - }; - + const rd = groups_read(file); + defer groups_finish(&rd); + for (const ent => nextgr(&rd)!) { if (ent.name == name) { + grent_dup(&ent); return ent; - } else { - grent_finish(&ent); }; }; }; -// Looks up groups by user name in a Unix-like group file. It expects a such -// file at /etc/group. Aborts if that file doesn't exist or is not properly -// formatted. +// Looks up a group by ID in a Unix-like group file. It expects a such file at +// /etc/group. Aborts if that file doesn't exist or is not properly formatted. +// +// The user must pass the return value to [[grent_finish]] to free resources +// associated with the group. // // See [[nextgr]] for low-level parsing API. -export fn getgroups(name: str) []grent = { - let file = match (os::open("/etc/group")) { +export fn getgid(gid: unix::gid) (grent | void) = { + const file = match (os::open("/etc/group")) { case let f: io::file => yield f; case => @@ -113,42 +143,23 @@ export fn getgroups(name: str) []grent = { }; defer io::close(file)!; - let groups = []: []grent; - - let rbuf: [os::BUFSZ]u8 = [0...]; - let strm = bufio::init(file, rbuf, []); - for (true) { - let ent = match (nextgr(&strm)) { - case let e: grent => - yield e; - case io::EOF => - break; - case => - abort("Invalid entry in /etc/group"); - }; - - let matched = false; - for (let i = 0z; i < len(ent.userlist); i += 1) { - if (ent.userlist[i] == name) { - matched = true; - append(groups, ent); - break; - }; - }; - if (!matched) { - grent_finish(&ent); + const rd = groups_read(file); + defer groups_finish(&rd); + for (const ent => nextgr(&rd)!) { + if (ent.gid == gid) { + grent_dup(&ent); + return ent; }; }; - - return groups; }; -// Looks up a group by ID in a Unix-like group file. It expects a such file at -// /etc/group. Aborts if that file doesn't exist or is not properly formatted. +// Looks up groups by user name in a Unix-like group file. It expects a such +// file at /etc/group. Aborts if that file doesn't exist or is not properly +// formatted. The caller must pass the return value to [[grents_finish]]. // // See [[nextgr]] for low-level parsing API. -export fn getgid(gid: unix::gid) (grent | void) = { - let file = match (os::open("/etc/group")) { +export fn getgroups(name: str) []grent = { + const file = match (os::open("/etc/group")) { case let f: io::file => yield f; case => @@ -156,59 +167,58 @@ export fn getgid(gid: unix::gid) (grent | void) = { }; defer io::close(file)!; - let rbuf: [os::BUFSZ]u8 = [0...]; - let strm = bufio::init(file, rbuf, []); - for (true) { - let ent = match (nextgr(&strm)) { - case let e: grent => - yield e; - case io::EOF => - break; - case => - abort("Invalid entry in /etc/group"); - }; + const rd = groups_read(file); + defer groups_finish(&rd); - if (ent.gid == gid) { - return ent; - } else { - grent_finish(&ent); + let groups: []grent = []; + for (const ent => nextgr(&rd)!) { + const tok = strings::tokenize(ent.userlist, ","); + for (const tok => strings::next_token(&tok)) { + if (tok == name) { + grent_dup(&ent); + append(groups, ent); + }; }; }; + + return groups; }; @test fn nextgr() void = { - let buf = memio::fixed(strings::toutf8( + const buf = memio::fixed(strings::toutf8( "root:x:0:root\n" "mail:x:12:\n" "video:x:986:alex,wmuser")); - - let ent = nextgr(&buf) as grent; - defer grent_finish(&ent); - - assert(ent.name == "root"); - assert(ent.password == "x"); - assert(ent.gid == 0); - assert(len(ent.userlist) == 1); - assert(ent.userlist[0] == "root"); - - let ent = nextgr(&buf) as grent; - defer grent_finish(&ent); - - assert(ent.name == "mail"); - assert(ent.password == "x"); - assert(ent.gid == 12); - assert(len(ent.userlist) == 0); - - let ent = nextgr(&buf) as grent; - defer grent_finish(&ent); - - assert(ent.name == "video"); - assert(ent.password == "x"); - assert(ent.gid == 986); - assert(len(ent.userlist) == 2); - assert(ent.userlist[0] == "alex"); - assert(ent.userlist[1] == "wmuser"); - - // No more entries - assert(nextgr(&buf) is io::EOF); + const rd = groups_read(&buf); + defer groups_finish(&rd); + + const expect = [ + grent { + name = "root", + password = "x", + gid = 0, + userlist = "root", + }, + grent { + name = "mail", + password = "x", + gid = 12, + userlist = "", + }, + grent { + name = "video", + password = "x", + gid = 986, + userlist = "alex,wmuser", + }, + ]; + + let i = 0z; + for (const ent => nextgr(&rd)!) { + defer i += 1; + assert(ent.name == expect[i].name); + assert(ent.password == expect[i].password); + assert(ent.gid == expect[i].gid); + assert(ent.userlist == expect[i].userlist); + }; }; diff --git a/unix/passwd/passwd.ha b/unix/passwd/passwd.ha @@ -2,6 +2,7 @@ // (c) Hare authors <https://harelang.org> use bufio; +use encoding::utf8; use io; use memio; use os; @@ -27,37 +28,57 @@ export type pwent = struct { shell: str, }; -// Reads a Unix-like password entry from an [[io::handle]]. The caller must free -// the return value using [[pwent_finish]]. -export fn nextpw(in: io::handle) (pwent | io::EOF | io::error | invalid) = { - let line = match (bufio::read_line(in)?) { +export type userreader = struct { + scan: bufio::scanner, +}; + +// Creates a parser for an /etc/passwd-formatted file. Use [[nextpw]] to +// enumerate the users, and [[users_finish]] to free resources associated with +// the reader. +export fn users_read(in: io::handle) userreader = { + return groupreader { + scan = bufio::newscanner(in), + }; +}; + +// Frees resources associated with a [[groupreader]]. +export fn users_finish(rd: *userreader) void = { + bufio::finish(&rd.scan); +}; + +// Reads a Unix-like password entry from an [[io::handle]]. The return value is +// borrowed from the reader. +export fn nextpw(rd: *userreader) (pwent | io::EOF | io::error | invalid) = { + const line = match (bufio::scan_line(&rd.scan)) { case io::EOF => return io::EOF; - case let ln: []u8 => + case let ln: const str => yield ln; - }; - let line = match (strings::fromutf8(line)) { - case let s: str => - yield s; - case => + case utf8::invalid => return invalid; + case let err: io::error => + return err; }; - - let fields = strings::split(line, ":"); - defer free(fields); - - if (len(fields) != 7) { - return invalid; + const tok = strings::tokenize(line, ":"); + + let i = 0z; + let fields: [7]str = [""...]; + for (const f => strings::next_token(&tok)) { + defer i += 1; + if (i >= len(fields)) { + return invalid; + }; + fields[i] = f; }; - let uid = match (strconv::stou64(fields[2])) { + const uid = match (strconv::stou64(fields[2])) { case let u: u64 => yield u: unix::uid; case => return invalid; }; - let gid = match (strconv::stou64(fields[3])) { + const gid = match (strconv::stou64(fields[3])) { case let u: u64 => yield u: unix::gid; case => @@ -65,7 +86,6 @@ export fn nextpw(in: io::handle) (pwent | io::EOF | io::error | invalid) = { }; return pwent { - // Borrows the return value of bufio::read_line username = fields[0], password = fields[1], uid = uid, @@ -78,10 +98,19 @@ export fn nextpw(in: io::handle) (pwent | io::EOF | io::error | invalid) = { // Frees resources associated with a [[pwent]]. export fn pwent_finish(ent: *pwent) void = { - // pwent fields are sliced from one allocated string returned by - // bufio::read_line. Freeing the first field frees the entire string in - // one go. free(ent.username); + free(ent.password); + free(ent.comment); + free(ent.homedir); + free(ent.shell); +}; + +fn pwent_dup(ent: *pwent) void = { + ent.username = strings::dup(ent.username); + ent.password = strings::dup(ent.password); + ent.comment = strings::dup(ent.comment); + ent.homedir = strings::dup(ent.homedir); + ent.shell = strings::dup(ent.shell); }; // Looks up a user by name in a Unix-like password file. It expects a password @@ -90,7 +119,7 @@ export fn pwent_finish(ent: *pwent) void = { // // See [[nextpw]] for low-level parsing API. export fn getuser(username: str) (pwent | void) = { - let file = match (os::open("/etc/passwd")) { + const file = match (os::open("/etc/passwd")) { case let f: io::file => yield f; case => @@ -98,22 +127,13 @@ export fn getuser(username: str) (pwent | void) = { }; defer io::close(file)!; - let rbuf: [os::BUFSZ]u8 = [0...]; - let strm = bufio::init(file, rbuf, []); - for (true) { - let ent = match (nextpw(&strm)) { - case let e: pwent => - yield e; - case io::EOF => - break; - case => - abort("Invalid entry in /etc/passwd"); - }; + const rd = users_read(file); + defer users_finish(&rd); + for (const ent => nextpw(&rd)!) { if (ent.username == username) { + pwent_dup(&ent); return ent; - } else { - pwent_finish(&ent); }; }; }; @@ -124,7 +144,7 @@ export fn getuser(username: str) (pwent | void) = { // // See [[nextpw]] for low-level parsing API. export fn getuid(uid: unix::uid) (pwent | void) = { - let file = match (os::open("/etc/passwd")) { + const file = match (os::open("/etc/passwd")) { case let f: io::file => yield f; case => @@ -132,53 +152,55 @@ export fn getuid(uid: unix::uid) (pwent | void) = { }; defer io::close(file)!; - let rbuf: [os::BUFSZ]u8 = [0...]; - let strm = bufio::init(file, rbuf, []); - for (true) { - let ent = match (nextpw(&strm)) { - case let e: pwent => - yield e; - case io::EOF => - break; - case => - abort("Invalid entry in /etc/passwd"); - }; + const rd = users_read(file); + defer users_finish(&rd); + for (const ent => nextpw(&rd)!) { if (ent.uid == uid) { + pwent_dup(&ent); return ent; - } else { - pwent_finish(&ent); }; }; }; @test fn nextpw() void = { - let buf = memio::fixed(strings::toutf8( - "sircmpwn:x:1000:1000:sircmpwn's comment:/home/sircmpwn:/bin/mrsh\n" + const buf = memio::fixed(strings::toutf8( + "sircmpwn:x:1000:1000:sircmpwn's comment:/home/sircmpwn:/bin/rc\n" "alex:x:1001:1001::/home/alex:/bin/zsh")); - let ent = nextpw(&buf) as pwent; - defer pwent_finish(&ent); - - assert(ent.username == "sircmpwn"); - assert(ent.password == "x"); - assert(ent.uid == 1000); - assert(ent.gid == 1000); - assert(ent.comment == "sircmpwn's comment"); - assert(ent.homedir == "/home/sircmpwn"); - assert(ent.shell == "/bin/mrsh"); - - let ent = nextpw(&buf) as pwent; - defer pwent_finish(&ent); - - assert(ent.username == "alex"); - assert(ent.password == "x"); - assert(ent.uid == 1001); - assert(ent.gid == 1001); - assert(ent.comment == ""); - assert(ent.homedir == "/home/alex"); - assert(ent.shell == "/bin/zsh"); - - // No more entries - assert(nextpw(&buf) is io::EOF); + const rd = users_read(&buf); + defer users_finish(&rd); + + const expect = [ + pwent { + username = "sircmpwn", + password = "x", + uid = 1000, + gid = 1000, + comment = "sircmpwn's comment", + homedir = "/home/sircmpwn", + shell = "/bin/rc", + }, + pwent { + username = "alex", + password = "x", + uid = 1001, + gid = 1001, + comment = "", + homedir = "/home/alex", + shell = "/bin/zsh", + }, + ]; + + let i = 0z; + for (const ent => nextpw(&rd)!) { + defer i += 1; + assert(ent.username == expect[i].username); + assert(ent.password == expect[i].password); + assert(ent.uid == expect[i].uid); + assert(ent.gid == expect[i].gid); + assert(ent.comment == expect[i].comment); + assert(ent.homedir == expect[i].homedir); + assert(ent.shell == expect[i].shell); + }; };