hare

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

group.ha (5226B)


      1 // SPDX-License-Identifier: MPL-2.0
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 use bufio;
      5 use encoding::utf8;
      6 use io;
      7 use memio;
      8 use os;
      9 use strconv;
     10 use strings;
     11 use unix;
     12 
     13 // A Unix-like group file entry.
     14 export type grent = struct {
     15 	// Name of the group
     16 	name: str,
     17 	// Optional encrypted password
     18 	password: str,
     19 	// Numerical group ID
     20 	gid: unix::gid,
     21 	// List of usernames that are members of this group, separated by commas
     22 	userlist: str,
     23 };
     24 
     25 export type groupreader = struct {
     26 	scan: bufio::scanner,
     27 };
     28 
     29 // Creates a parser for an /etc/groups-formatted file. Use [[nextgr]] to
     30 // enumerate the groups, and [[groups_finish]] to free resources associated with
     31 // the reader.
     32 export fn groups_read(in: io::handle) groupreader = {
     33 	return groupreader {
     34 		scan = bufio::newscanner(in),
     35 	};
     36 };
     37 
     38 // Frees resources associated with a [[groupreader]].
     39 export fn groups_finish(rd: *groupreader) void = {
     40 	bufio::finish(&rd.scan);
     41 };
     42 
     43 // Reads a Unix-like group entry from a [[grreader]]. The return value is
     44 // borrowed from the scanner.
     45 export fn nextgr(rd: *groupreader) (grent | io::EOF | io::error | invalid) = {
     46 	const line = match (bufio::scan_line(&rd.scan)) {
     47 	case let ln: const str =>
     48 		yield ln;
     49 	case let err: io::error =>
     50 		return err;
     51 	case utf8::invalid =>
     52 		return invalid;
     53 	case io::EOF =>
     54 		return io::EOF;
     55 	};
     56 	const tok = strings::tokenize(line, ":");
     57 
     58 	let i = 0z;
     59 	let fields: [4]str = [""...];
     60 	for (const f => strings::next_token(&tok)) {
     61 		defer i += 1;
     62 		if (i >= len(fields)) {
     63 			return invalid;
     64 		};
     65 		fields[i] = f;
     66 	};
     67 
     68 	let gid = match (strconv::stou64(fields[2])) {
     69 	case let u: u64 =>
     70 		yield u: unix::gid;
     71 	case =>
     72 		return invalid;
     73 	};
     74 
     75 	return grent {
     76 		name      = fields[0],
     77 		password  = fields[1],
     78 		gid       = gid,
     79 		userlist  = fields[3],
     80 	};
     81 };
     82 
     83 // Frees resources associated with a [[grent]].
     84 export fn grent_finish(ent: *grent) void = {
     85 	free(ent.name);
     86 	free(ent.password);
     87 	free(ent.userlist);
     88 };
     89 
     90 // Frees resources associated with a slice of [[grent]]s.
     91 export fn grents_free(ents: []grent) void = {
     92 	for (let ent &.. ents) {
     93 		grent_finish(ent);
     94 	};
     95 	free(ents);
     96 };
     97 
     98 fn grent_dup(ent: *grent) void = {
     99 	ent.name = strings::dup(ent.name);
    100 	ent.password = strings::dup(ent.password);
    101 	ent.userlist = strings::dup(ent.userlist);
    102 };
    103 
    104 // Looks up a group by name in a Unix-like group file. It expects a such file at
    105 // /etc/group. Aborts if that file doesn't exist or is not properly formatted.
    106 //
    107 // The user must pass the return value to [[grent_finish]] to free resources
    108 // associated with the group.
    109 //
    110 // See [[nextgr]] for low-level parsing API.
    111 export fn getgroup(name: str) (grent | void) = {
    112 	const file = match (os::open("/etc/group")) {
    113 	case let f: io::file =>
    114 		yield f;
    115 	case =>
    116 		abort("Unable to open /etc/group");
    117 	};
    118 	defer io::close(file)!;
    119 
    120 	const rd = groups_read(file);
    121 	defer groups_finish(&rd);
    122 	for (const ent => nextgr(&rd)!) {
    123 		if (ent.name == name) {
    124 			grent_dup(&ent);
    125 			return ent;
    126 		};
    127 	};
    128 };
    129 
    130 // Looks up a group by ID in a Unix-like group file. It expects a such file at
    131 // /etc/group. Aborts if that file doesn't exist or is not properly formatted.
    132 //
    133 // The user must pass the return value to [[grent_finish]] to free resources
    134 // associated with the group.
    135 //
    136 // See [[nextgr]] for low-level parsing API.
    137 export fn getgid(gid: unix::gid) (grent | void) = {
    138 	const file = match (os::open("/etc/group")) {
    139 	case let f: io::file =>
    140 		yield f;
    141 	case =>
    142 		abort("Unable to open /etc/group");
    143 	};
    144 	defer io::close(file)!;
    145 
    146 	const rd = groups_read(file);
    147 	defer groups_finish(&rd);
    148 	for (const ent => nextgr(&rd)!) {
    149 		if (ent.gid == gid) {
    150 			grent_dup(&ent);
    151 			return ent;
    152 		};
    153 	};
    154 };
    155 
    156 // Looks up groups by user name in a Unix-like group file. It expects a such
    157 // file at /etc/group. Aborts if that file doesn't exist or is not properly
    158 // formatted. The caller must pass the return value to [[grents_finish]].
    159 //
    160 // See [[nextgr]] for low-level parsing API.
    161 export fn getgroups(name: str) []grent = {
    162 	const file = match (os::open("/etc/group")) {
    163 	case let f: io::file =>
    164 		yield f;
    165 	case =>
    166 		abort("Unable to open /etc/group");
    167 	};
    168 	defer io::close(file)!;
    169 
    170 	const rd = groups_read(file);
    171 	defer groups_finish(&rd);
    172 
    173 	let groups: []grent = [];
    174 	for (const ent => nextgr(&rd)!) {
    175 		const tok = strings::tokenize(ent.userlist, ",");
    176 		for (const tok => strings::next_token(&tok)) {
    177 			if (tok == name) {
    178 				grent_dup(&ent);
    179 				append(groups, ent)!;
    180 			};
    181 		};
    182 	};
    183 
    184 	return groups;
    185 };
    186 
    187 @test fn nextgr() void = {
    188 	const buf = memio::fixed(strings::toutf8(
    189 		"root:x:0:root\n"
    190 		"mail:x:12:\n"
    191 		"video:x:986:alex,wmuser\n"));
    192 	const rd = groups_read(&buf);
    193 	defer groups_finish(&rd);
    194 
    195 	const expect = [
    196 		grent {
    197 			name = "root",
    198 			password = "x",
    199 			gid = 0,
    200 			userlist = "root",
    201 		},
    202 		grent {
    203 			name = "mail",
    204 			password = "x",
    205 			gid = 12,
    206 			userlist = "",
    207 		},
    208 		grent {
    209 			name = "video",
    210 			password = "x",
    211 			gid = 986,
    212 			userlist = "alex,wmuser",
    213 		},
    214 	];
    215 
    216 	let i = 0z;
    217 	for (const ent => nextgr(&rd)!) {
    218 		defer i += 1;
    219 		assert(ent.name == expect[i].name);
    220 		assert(ent.password == expect[i].password);
    221 		assert(ent.gid == expect[i].gid);
    222 		assert(ent.userlist == expect[i].userlist);
    223 	};
    224 	assert(i == len(expect));
    225 };