1-- Users and groups: the IDs the system gives a process and a file, and
2-- the user and group databases that name them. On Darwin and FreeBSD,
3-- and on Linux under --link=libc, an account is looked up through the C
4-- library, which asks every database the system is set to, a directory
5-- service or NSS's among them. A static Linux program has no C library
6-- to ask, and reads /etc/passwd and /etc/group itself: an account kept
7-- anywhere else needs --link=libc. `compiler.c_runtime` tells the two
8-- apart when the program is compiled.
9
10import core/text
11import platform/libc
12
13user_value: type = distinct u32
14group_value: type = distinct u32
15
16--- A user ID, `uid_t`: a handle the system gives an account, not a number
17--- to count with; `user_id(n)` and `u32(u)` convert.
18public user_id: type = user_value
19
20--- A group ID, `gid_t`, as `user_id` is a user's.
21public group_id: type = group_value
22
23--- What `change_owner_at` takes to keep a file's owner: `(uid_t)-1`.
24public unchanged_user: user_id = user_value(0xFFFFFFFF)
25
26--- What `change_owner_at` takes to keep a file's group: `(gid_t)-1`.
27public unchanged_group: group_id = group_value(0xFFFFFFFF)
28
29--- No account or group has the ID or name asked for.
30public no_account: atom
31
32--- A line of a database file that is not an entry: a field missing or
33--- too many, an empty name, or an ID that is not a decimal `u32` below
34--- `0xFFFFFFFF`.
35public malformed_entry: atom
36
37--- One account of the user database, POSIX's `struct passwd`: its name,
38--- its password field as the database keeps it, `x` or `*` where the
39--- password is kept elsewhere, its user and group IDs, the comment that
40--- often holds a full name, its home directory and its shell. Each is a
41--- host string lent from the buffer the account was read into.
42public account: type = struct
43 name: text.host_string
44 password: text.host_string
45 user: user_id
46 group: group_id
47 comment: text.host_string
48 home: text.host_string
49 shell: text.host_string
50end account
51
52--- One group of the group database, POSIX's `struct group`: its name, its
53--- password field, its ID and its members' names, as /etc/group writes
54--- them, separated by commas, which `member_count` and `member` read.
55--- Each is a host string lent from the buffer the group was read into.
56public group_entry: type = struct
57 name: text.host_string
58 password: text.host_string
59 group: group_id
60 members: text.host_string
61end group_entry
62
63--- The process's real user ID, POSIX's `getuid`: who started it.
64public link(symbol: "_landin_posix_real_user") real_user:
65 () -> (user: user_id) =
66 user = user_value(system_real_user())
67end real_user
68
69--- The process's effective user ID, POSIX's `geteuid`, which the system
70--- checks permissions against: zero, `root`, may read and write any file
71--- whatever its permission bits say.
72public link(symbol: "_landin_posix_effective_user") effective_user:
73 () -> (user: user_id) =
74 user = user_value(system_effective_user())
75end effective_user
76
77--- The process's real group ID, POSIX's `getgid`.
78public link(symbol: "_landin_posix_real_group") real_group:
79 () -> (group: group_id) =
80 group = group_value(system_real_group())
81end real_group
82
83--- The process's effective group ID, POSIX's `getegid`.
84public link(symbol: "_landin_posix_effective_group") effective_group:
85 () -> (group: group_id) =
86 group = group_value(system_effective_group())
87end effective_group
88
89-- The fields of a database line: where each begins and ends.
90field: type = struct
91 start: usize
92 stop: usize
93end field
94
95-- Split LINE at its colons into exactly COUNT fields, or fail.
96fields_of: (line: []u8, count: usize, inout into: [7]field)
97 -> none ! malformed_entry =
98 mut found: usize = 0
99 mut start: usize = 0
100 for at in usize(0)..<lenof line do
101 if line[at] == 58 then
102 fail malformed_entry when found + 1 >= count
103 into[found] = (start: start, stop: at)
104 inc found
105 start = at + 1
106 end if
107 end for
108 fail malformed_entry when found + 1 <> count
109 into[found] = (start: start, stop: lenof line)
110end fields_of
111
112-- A database ID: nonempty decimal digits, below 0xFFFFFFFF, which is
113-- `(uid_t)-1` and names no one.
114identity_of: (digits: []u8) -> (value: u32) ! malformed_entry =
115 value = text.parse_u32(digits) else (problem)
116 _ = problem
117 fail malformed_entry
118 end
119 fail malformed_entry when value == 0xFFFFFFFF
120end identity_of
121
122--- Read one line of /etc/passwd's form, without its newline, as an
123--- account lent from `line`: seven fields separated by colons, `name`,
124--- `password`, `user`, `group`, `comment`, `home` and `shell`, the IDs
125--- decimal. A line with another number of fields, an empty name or an
126--- ID that is not a decimal below `0xFFFFFFFF` is `malformed_entry`. A
127--- comment, an empty line and an NIS line beginning `+` or `-`, which a
128--- lookup skips, are no accounts either.
129public link(symbol: "_landin_posix_parse_account") parse_account:
130 (line: []u8) -> (found: account from line) ! malformed_entry =
131 mut at: [7]field = [7 of (start: 0, stop: 0)]
132 try fields_of(line, 7, at)
133 fail malformed_entry when at[0].stop == at[0].start
134 or line[0] == 35 or line[0] == 43 or line[0] == 45
135 user: u32 = try identity_of(line[at[2].start..<at[2].stop])
136 group: u32 = try identity_of(line[at[3].start..<at[3].stop])
137 found = (name: text.host_over(line[at[0].start..<at[0].stop]),
138 password: text.host_over(line[at[1].start..<at[1].stop]),
139 user: user_value(user), group: group_value(group),
140 comment: text.host_over(line[at[4].start..<at[4].stop]),
141 home: text.host_over(line[at[5].start..<at[5].stop]),
142 shell: text.host_over(line[at[6].start..<at[6].stop]))
143end parse_account
144
145--- Read one line of /etc/group's form, without its newline, as a group
146--- lent from `line`: four fields, `name`, `password`, `group` and the
147--- members separated by commas, as `parse_account` reads an account.
148public link(symbol: "_landin_posix_parse_group") parse_group:
149 (line: []u8) -> (found: group_entry from line) ! malformed_entry =
150 mut at: [7]field = [7 of (start: 0, stop: 0)]
151 try fields_of(line, 4, at)
152 fail malformed_entry when at[0].stop == at[0].start
153 or line[0] == 35 or line[0] == 43 or line[0] == 45
154 group: u32 = try identity_of(line[at[2].start..<at[2].stop])
155 found = (name: text.host_over(line[at[0].start..<at[0].stop]),
156 password: text.host_over(line[at[1].start..<at[1].stop]),
157 group: group_value(group),
158 members: text.host_over(line[at[3].start..<at[3].stop]))
159end parse_group
160
161--- How many members `entry` names: the names between its commas, an
162--- empty one, as `,,` or a trailing comma leaves, not among them.
163public link(symbol: "_landin_posix_member_count") member_count:
164 (entry: group_entry) -> (count: usize) =
165 names: []u8 = text.host_bytes(entry.members)
166 count = 0
167 mut start: usize = 0
168 for at in usize(0)..<lenof names + 1 do
169 if at == lenof names or names[at] == 44 then
170 if at > start then inc count end if
171 start = at + 1
172 end if
173 end for
174end member_count
175
176--- The member `index` of `entry`, from zero, as `member_count` counts
177--- them, lent as the group is; an index at or past the count traps.
178public link(symbol: "_landin_posix_member") member:
179 (entry: group_entry, index: usize)
180 -> (name: text.host_string from entry) =
181 names: []u8 = text.host_bytes(entry.members)
182 mut start: usize = 0
183 mut seen: usize = 0
184 for at in usize(0)..<lenof names + 1 do
185 if at == lenof names or names[at] == 44 then
186 if at > start then
187 if seen == index then
188 name = text.host_over(names[start..<at])
189 return
190 end if
191 inc seen
192 end if
193 start = at + 1
194 end if
195 end for
196 compiler.unreachable()
197end member
198
199--- The account with the user ID `user`, read into `buffer` and lent from
200--- it: `no_account` when there is none, `erange` when `buffer` is too small
201--- for it, and the errno of reading the database otherwise. Through the C
202--- library on Darwin, FreeBSD and Linux under --link=libc, which asks
203--- every database the system is set to; a static Linux program reads
204--- /etc/passwd, the first entry with that ID, skipping every line that is
205--- not one. A kilobyte holds an ordinary account.
206public link(symbol: "_landin_posix_account_with_id") account_with_id:
207 (user: user_id, buffer: []mut u8)
208 -> (found: account from buffer) ! errno | no_account =
209 found = try system_account(buffer, true, u32(user), no_name[0..<0])
210end account_with_id
211
212--- The account named `name`, read into `buffer`, as `account_with_id`
213--- finds one by its ID; a name holding a NUL or a colon names none.
214public link(symbol: "_landin_posix_account_named") account_named:
215 (name: text.host_string, buffer: []mut u8)
216 -> (found: account from buffer) ! errno | no_account =
217 found = try system_account(buffer, false, 0, text.host_bytes(name))
218end account_named
219
220--- The group with the group ID `group`, read into `buffer` and lent from
221--- it, as `account_with_id` finds an account, from /etc/group on a static
222--- Linux program. Its members' names are separated by commas whatever
223--- database gave them.
224public link(symbol: "_landin_posix_group_with_id") group_with_id:
225 (group: group_id, buffer: []mut u8)
226 -> (found: group_entry from buffer) ! errno | no_account =
227 found = try system_group(buffer, true, u32(group), no_name[0..<0])
228end group_with_id
229
230--- The group named `name`, read into `buffer`, as `group_with_id` finds
231--- one by its ID.
232public link(symbol: "_landin_posix_group_named") group_named:
233 (name: text.host_string, buffer: []mut u8)
234 -> (found: group_entry from buffer) ! errno | no_account =
235 found = try system_group(buffer, false, 0, text.host_bytes(name))
236end group_named
237
238no_name: [1]u8 = [0]
239
240-- Whether NAME could be in a database: nonempty, with no NUL and no
241-- colon, which no entry's name holds.
242name_valid: (name: []u8) -> (yes: bool) =
243 yes = lenof name > 0
244 for byte in name do
245 if byte == 0 or byte == 58 then yes = false end if
246 end for
247end name_valid
248
249fixed if compiler.os == linux and not compiler.c_runtime then
250 -- A static Linux program reads the files. Each line of the database
251 -- is read into the caller's buffer; a line longer than the buffer is
252 -- skipped, and when no entry matched, one skipped may have, so the
253 -- answer is then `erange`.
254 passwd_path: []u8 = "/etc/passwd"
255 group_path: []u8 = "/etc/group"
256
257 -- Whether an entry's name or ID is the one asked for.
258 matches: (by_id: bool, wanted_id: u32, found_id: u32, wanted: []u8,
259 found: []u8) -> (yes: bool) =
260 if by_id then
261 yes = wanted_id == found_id
262 return
263 end if
264 yes = lenof wanted == lenof found
265 for at in usize(0)..<lenof found do
266 if yes and wanted[at] <> found[at] then yes = false end if
267 end for
268 end matches
269
270 -- The next whole line of the database in BUFFER: where it begins and
271 -- ends, or an empty line marked `ended` at the end of the file. The
272 -- bytes from CURSOR to FILLED are read and not yet scanned.
273 scan: type = struct
274 cursor: usize
275 filled: usize
276 ended: bool
277 skipped: bool
278 end scan
279
280 line_at: type = struct
281 start: usize
282 stop: usize
283 ended: bool
284 end line_at
285
286 -- Read the next line, refilling BUFFER from SOURCE, moving what is
287 -- left of a line to its front first. A line that fills the buffer
288 -- with no newline is skipped to its end, and noted.
289 next_line: (source: descriptor, buffer: []mut u8, inout state: scan)
290 -> (line: line_at) ! errno =
291 mut skipping: bool = false
292 loop do
293 mut at: usize = state.cursor
294 while at < state.filled and buffer[at] <> 10 do
295 inc at
296 end while
297 if at < state.filled then
298 start: usize = state.cursor
299 state.cursor = at + 1
300 if skipping then
301 skipping = false
302 continue
303 end if
304 line = (start: start, stop: at, ended: false)
305 return
306 end if
307 if state.ended then
308 -- The last line need not end with a newline.
309 line = (start: state.cursor, stop: state.filled, ended: false)
310 if skipping or state.cursor == state.filled then
311 line.ended = true
312 end if
313 state.cursor = state.filled
314 return
315 end if
316 -- Keep the start of the line, and read after it.
317 kept: usize = state.filled - state.cursor
318 if kept == lenof buffer then
319 state.skipped = true
320 skipping = true
321 state.cursor = 0
322 state.filled = 0
323 else
324 for index in usize(0)..<kept do
325 buffer[index] = buffer[state.cursor + index]
326 end for
327 state.cursor = 0
328 state.filled = kept
329 end if
330 got: usize = read(source, buffer[state.filled..<lenof buffer])
331 else (problem)
332 continue when problem == eintr
333 fail problem
334 end
335 if got == 0 then
336 state.ended = true
337 else
338 state.filled += got
339 end if
340 end loop
341 end next_line
342
343 -- Open a database file.
344 database: (path: []u8) -> (opened: descriptor) ! errno =
345 loop do
346 opened = open_at(working_directory, text.host_over(path),
347 reading, 0) else (problem)
348 continue when problem == eintr
349 fail problem
350 end
351 return
352 end loop
353 end database
354
355 -- Forget a database file, whatever its close says.
356 closed: (opened: descriptor) -> none =
357 close(opened) else (problem)
358 _ = problem
359 end
360 end closed
361
362 system_account: (buffer: []mut u8, by_id: bool, wanted_id: u32,
363 wanted: []u8)
364 -> (found: account from buffer) ! errno | no_account =
365 fail no_account when not by_id and not name_valid(wanted)
366 fail erange when lenof buffer == 0
367 source: descriptor = try database(passwd_path)
368 defer closed(source)
369 mut state: scan = (cursor: 0, filled: 0, ended: false,
370 skipped: false)
371 loop do
372 line: line_at = try next_line(source, buffer, state)
373 if line.ended then
374 fail erange when state.skipped
375 fail no_account
376 end if
377 entry: account = parse_account(buffer[line.start..<line.stop])
378 else (problem)
379 _ = problem
380 continue
381 end
382 if matches(by_id, wanted_id, u32(entry.user), wanted,
383 text.host_bytes(entry.name)) then
384 found = entry
385 return
386 end if
387 end loop
388 end system_account
389
390 system_group: (buffer: []mut u8, by_id: bool, wanted_id: u32,
391 wanted: []u8)
392 -> (found: group_entry from buffer) ! errno | no_account =
393 fail no_account when not by_id and not name_valid(wanted)
394 fail erange when lenof buffer == 0
395 source: descriptor = try database(group_path)
396 defer closed(source)
397 mut state: scan = (cursor: 0, filled: 0, ended: false,
398 skipped: false)
399 loop do
400 line: line_at = try next_line(source, buffer, state)
401 if line.ended then
402 fail erange when state.skipped
403 fail no_account
404 end if
405 entry: group_entry = parse_group(buffer[line.start..<line.stop])
406 else (problem)
407 _ = problem
408 continue
409 end
410 if matches(by_id, wanted_id, u32(entry.group), wanted,
411 text.host_bytes(entry.name)) then
412 found = entry
413 return
414 end if
415 end loop
416 end system_group
417else
418 -- Through the C library. The record the call fills is this module's,
419 -- and its strings lie in the caller's buffer, which each is held to
420 -- before it is lent: a pointer outside it is `unlisted`.
421
422 -- The string at ADDRESS, which must lie in BUFFER and end there.
423 inside: (buffer: []mut u8, address: usize)
424 -> (bytes: []u8 from buffer) ! errno =
425 bytes = buffer[0..<0]
426 fail unlisted when lenof buffer == 0
427 base: usize = usize(addr buffer[0])
428 fail unlisted when address < base or address >= base + lenof buffer
429 start: usize = address - base
430 mut stop: usize = start
431 while stop < lenof buffer and buffer[stop] <> 0 do
432 inc stop
433 end while
434 fail unlisted when stop == lenof buffer
435 bytes = buffer[start..<stop]
436 end inside
437
438 -- Whether a lookup that found nothing failed only because there was
439 -- nothing to find: POSIX has it return zero then, and the C libraries
440 -- have answered ENOENT, ESRCH, EBADF and EPERM for the same.
441 not_found: (error: i32) -> (yes: bool) =
442 problem: errno = errno_of(error)
443 yes = error == 0 or problem == enoent or problem == esrch
444 or problem == ebadf or problem == eperm
445 end not_found
446
447 -- Copy NAME with a NUL after it into ROOM, for a call that takes one.
448 name_in: (name: []u8, room: []mut u8) -> (terminated: ptr u8 from room)
449 ! errno =
450 fail erange when lenof name >= lenof room
451 for at in usize(0)..<lenof name do
452 room[at] = name[at]
453 end for
454 room[lenof name] = 0
455 terminated = addr room[0]
456 end name_in
457
458 system_account: (buffer: []mut u8, by_id: bool, wanted_id: u32,
459 wanted: []u8)
460 -> (found: account from buffer) ! errno | no_account =
461 fail no_account when not by_id and not name_valid(wanted)
462 fail erange when lenof buffer == 0
463 mut record: libc.account_record = zeroed
464 mut result: usize = 0
465 mut error: i32 = 0
466 if by_id then
467 error = libc.getpwuid_r(wanted_id, addr record, addr buffer[0],
468 lenof buffer, addr result)
469 else
470 mut room: [256]u8 = zeroed
471 named: ptr u8 = try name_in(wanted, room[0..<256])
472 error = libc.getpwnam_r(named, addr record, addr buffer[0],
473 lenof buffer, addr result)
474 end if
475 fail no_account when result == 0 and not_found(error)
476 fail errno_of(error) when error <> 0
477 fail no_account when result == 0
478 found = (name: text.host_over(try inside(buffer, record.name)),
479 password: text.host_over(try inside(buffer, record.password)),
480 user: user_value(record.user),
481 group: group_value(record.group),
482 comment: text.host_over(try inside(buffer, record.comment)),
483 home: text.host_over(try inside(buffer, record.home)),
484 shell: text.host_over(try inside(buffer, record.shell)))
485 end system_account
486
487 -- The group's strings are copied to the front of the caller's buffer,
488 -- members joined by commas, and the C library given the back half to
489 -- fill.
490 system_group: (buffer: []mut u8, by_id: bool, wanted_id: u32,
491 wanted: []u8)
492 -> (found: group_entry from buffer) ! errno | no_account =
493 fail no_account when not by_id and not name_valid(wanted)
494 fail erange when lenof buffer < 2
495 half: usize = lenof buffer / 2
496 scratch: []mut u8 = buffer[half..<lenof buffer]
497 mut record: libc.group_record = zeroed
498 mut result: usize = 0
499 mut error: i32 = 0
500 if by_id then
501 error = libc.getgrgid_r(wanted_id, addr record, addr scratch[0],
502 lenof scratch, addr result)
503 else
504 mut room: [256]u8 = zeroed
505 named: ptr u8 = try name_in(wanted, room[0..<256])
506 error = libc.getgrnam_r(named, addr record, addr scratch[0],
507 lenof scratch, addr result)
508 end if
509 fail no_account when result == 0 and not_found(error)
510 fail errno_of(error) when error <> 0
511 fail no_account when result == 0
512 name: []u8 = try inside(scratch, record.name)
513 password: []u8 = try inside(scratch, record.password)
514 mut used: usize = 0
515 name_at: usize = used
516 used = try placed(buffer, half, used, name)
517 password_at: usize = used
518 used = try placed(buffer, half, used, password)
519 members_at: usize = used
520 -- The members' table: pointers, the last one null.
521 fail unlisted when record.members == 0
522 mut entry: usize = record.members
523 mut first: bool = true
524 loop do
525 fail unlisted when entry < usize(addr scratch[0])
526 or entry + sizeof usize > usize(addr scratch[0]) + lenof scratch
527 slot: ptr usize = ptr(entry)
528 break when slot.val == 0
529 if not first then
530 used = try placed(buffer, half, used, ",")
531 end if
532 first = false
533 used = try placed(buffer, half, used, try inside(scratch, slot.val))
534 entry += sizeof usize
535 end loop
536 name_end: usize = name_at + lenof name
537 password_end: usize = password_at + lenof password
538 found = (name: text.host_over(buffer[name_at..<name_end]),
539 password: text.host_over(buffer[password_at..<password_end]),
540 group: group_value(record.group),
541 members: text.host_over(buffer[members_at..<used]))
542 end system_group
543
544 -- Copy BYTES into the front half of BUFFER at USED: where the next
545 -- goes, or `erange` past the half.
546 placed: (buffer: []mut u8, half: usize, used: usize, bytes: []u8)
547 -> (next: usize) ! errno =
548 fail erange when lenof bytes > half - used
549 for at in usize(0)..<lenof bytes do
550 buffer[used + at] = bytes[at]
551 end for
552 next = used + lenof bytes
553 end placed
554end if