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:
M | unix/passwd/group.ha | | | 246 | +++++++++++++++++++++++++++++++++++++++++-------------------------------------- |
M | unix/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);
+ };
};