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 };