Landin library reference discuss source

platform/posix/accounts.ldn

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