Landin library reference discuss source

platform/posix/directories.ldn

1--  Directories: opening one as a stream of entries read through the
2--  caller's buffer, and making and removing directories and files, each
3--  relative to an open directory or the working one.
4
5import core/text
6import platform/libc
7import platform/linux
8
9--- The least a buffer `read_directory` reads into holds: room for any
10--- entry any of the three systems gives, its record and its NUL included.
11public directory_buffer: usize = 1024
12
13--- An open directory stream, POSIX's `DIR`: a value its caller keeps and
14--- passes `inout` to `read_directory`, with the same buffer each time.
15--- Its fields belong to this module: the directory's descriptor, the C
16--- library's stream where there is one, and how much of the buffer the
17--- system has filled and the caller has read.
18public directory_stream: type = struct
19    descriptor: descriptor
20    stream: usize
21    filled: usize
22    cursor: usize
23    finished: bool
24end directory_stream
25
26--- An entry of a directory: its name, lent from the buffer it was read
27--- into until the next read, and its kind, `unknown_kind` where the file
28--- system records none.
29public directory_entry: type = struct
30    name: text.host_string
31    kind: file_kind
32end directory_entry
33
34--- Open the directory `path` names, relative to `at`, for reading, its
35--- descriptor closed on exec as `open_at`'s is: `enotdir` when it is not
36--- a directory, `enoent` when it is missing.
37--- Close it with `close_directory`, which also closes its descriptor.
38public link(symbol: "_landin_posix_open_directory") open_directory:
39    (at: descriptor, path: text.host_string)
40    -> (opened: directory_stream) ! errno =
41    mut room: path_room = zeroed
42    terminated: ptr u8 = try terminate(path, room[0..<lenof room])
43    opened = try system_open_directory(at, terminated)
44end open_directory
45
46--- The directory's next entry, read into `buffer`, which must be the
47--- buffer every earlier read of this stream was given and hold at least
48--- `directory_buffer` bytes, or `einval`.  The entry lends its name from
49--- `buffer` until the next read.  An entry with an empty name is the end
50--- of the directory.  Entries come in the system's order, `.` and `..`
51--- among them, as POSIX's `readdir` gives them.
52public link(symbol: "_landin_posix_read_directory") read_directory:
53    (inout opened: directory_stream, buffer: []mut u8)
54    -> (item: directory_entry from buffer) ! errno =
55    fail einval when lenof buffer < directory_buffer
56    item = try system_read_directory(opened, buffer)
57end read_directory
58
59--- Close the directory stream and its descriptor, which are released even
60--- when this fails.
61public link(symbol: "_landin_posix_close_directory") close_directory:
62    (opened: directory_stream) -> none ! errno =
63    try system_close_directory(opened)
64end close_directory
65
66--- Make the directory `path` names, relative to `at`, with the permission
67--- bits `permissions`, which the process umask filters: `eexist` when the
68--- name is taken, `enoent` when its parent is missing.
69public link(symbol: "_landin_posix_make_directory_at") make_directory_at:
70    (at: descriptor, path: text.host_string,
71     permissions: u32) -> none ! errno =
72    mut room: path_room = zeroed
73    terminated: ptr u8 = try terminate(path, room[0..<lenof room])
74    try system_make_directory(at, terminated, permissions)
75end make_directory_at
76
77--- Remove the name `path`, relative to `at`, of a file that is not a
78--- directory, POSIX's `unlinkat`.
79public link(symbol: "_landin_posix_unlink_at") unlink_at:
80    (at: descriptor, path: text.host_string) -> none ! errno =
81    mut room: path_room = zeroed
82    terminated: ptr u8 = try terminate(path, room[0..<lenof room])
83    try system_remove(at, terminated, false)
84end unlink_at
85
86--- Remove the empty directory `path` names, relative to `at`, POSIX's
87--- `unlinkat` with `AT_REMOVEDIR`: `enotempty` when it holds anything.
88public link(symbol: "_landin_posix_remove_directory_at") remove_directory_at:
89    (at: descriptor, path: text.host_string)
90    -> none ! errno =
91    mut room: path_room = zeroed
92    terminated: ptr u8 = try terminate(path, room[0..<lenof room])
93    try system_remove(at, terminated, true)
94end remove_directory_at
95
96--  The system's own calls beneath the facility above: Linux's system
97--  calls through platform/linux, and the C library through
98--  platform/libc on Darwin and FreeBSD, whose system interface it is.
99
100fixed if compiler.os == linux then
101    system_open_directory: (at: descriptor, path: ptr u8)
102                           -> (opened: directory_stream) ! errno =
103        result: i32 = linux.openat(i32(at), path,
104                                   linux.o_rdonly | linux.o_directory
105                                   | linux.o_cloexec, 0)
106        fail errno_of(-result) when result < 0
107        opened = (descriptor: descriptor(result), stream: 0, filled: 0,
108                  cursor: 0, finished: false)
109    end system_open_directory
110
111    --  A record's little-endian u16 at `at`.
112    record_u16: (bytes: []u8, at: usize) -> (value: usize) =
113        value = usize(bytes[at]) | (usize(bytes[at + 1]) << 8)
114    end record_u16
115
116    --  The next record in the buffer, refilling it from getdents64 when
117    --  every record in it has been read.  A record is checked to lie
118    --  within what the kernel filled before any byte of it is read, and
119    --  its name to end within it, so a buffer's last record is never read
120    --  past; one that does not is `unlisted`, which no errno describes.
121    system_read_directory: (inout opened: directory_stream, buffer: []mut u8)
122                           -> (item: directory_entry from buffer) ! errno =
123        loop do
124            if opened.cursor >= opened.filled then
125                if opened.finished then
126                    item = (name: text.host_over(buffer[0..<0]),
127                            kind: unknown_kind)
128                    return
129                end if
130                got: isize = linux.getdents64(i32(opened.descriptor),
131                                              addr buffer[0], lenof buffer)
132                fail errno_of(i32(-got)) when got < 0
133                fail unlisted when usize(got) > lenof buffer
134                if got == 0 then
135                    opened.finished = true
136                else
137                    opened.filled = usize(got)
138                    opened.cursor = 0
139                end if
140            else
141                at: usize = opened.cursor
142                --  The fixed part is 19 bytes and the name at least one
143                --  byte and its NUL.
144                fail unlisted when opened.filled - at < 21
145                length: usize = record_u16(buffer, at + 16)
146                fail unlisted when length < 21
147                  or length > opened.filled - at
148                opened.cursor = at + length
149                mut stop: usize = at + 19
150                while stop < at + length and buffer[stop] <> 0 do
151                    inc stop
152                end while
153                fail unlisted when stop == at + length
154                item = (name: text.host_over(buffer[at + 19..<stop]),
155                        kind: kind_of_type(buffer[at + 18]))
156                return
157            end if
158        end loop
159    end system_read_directory
160
161    system_close_directory: (opened: directory_stream) -> none ! errno =
162        try system_close(opened.descriptor)
163    end system_close_directory
164
165    system_make_directory: (at: descriptor, path: ptr u8, permissions: u32)
166                           -> none ! errno =
167        result: i32 = linux.mkdirat(i32(at), path, permissions)
168        fail errno_of(-result) when result < 0
169    end system_make_directory
170
171    system_remove: (at: descriptor, path: ptr u8, directory: bool)
172                   -> none ! errno =
173        result: i32 = linux.unlinkat(i32(at), path,
174                                     if directory then linux.at_removedir
175                                     else 0 end if)
176        fail errno_of(-result) when result < 0
177    end system_remove
178elsif compiler.os == darwin or compiler.os == freebsd then
179    --  The directory is opened with o_directory, which makes anything else
180    --  ENOTDIR, and a C directory stream made over it, which then owns the
181    --  descriptor.
182    system_open_directory: (at: descriptor, path: ptr u8)
183                           -> (opened: directory_stream) ! errno =
184        result: i32 = opened_at(at, path,
185                                libc.o_rdonly | libc.o_directory
186                                | libc.o_cloexec, 0)
187        fail failure() when result < 0
188        made: libc.directory_stream = libc.fdopendir(result)
189        match made
190            libc.no_directory: begin
191                problem: errno = failure()
192                _ = libc.close(result)
193                fail problem
194            end
195            ptr (stream): opened = (descriptor: descriptor(result),
196                                    stream: usize(stream), filled: 0,
197                                    cursor: 0, finished: false)
198        end match
199    end system_open_directory
200
201    --  The stream's next record, its name copied into the buffer with a
202    --  NUL after it, since the record is the C library's and good only
203    --  until the next readdir.  The buffer holds any name a record can:
204    --  `directory_buffer` is larger than every d_name.  readdir's end and
205    --  its failure are told apart by errno, zeroed before the call.
206    system_read_directory: (inout opened: directory_stream, buffer: []mut u8)
207                           -> (item: directory_entry from buffer) ! errno =
208        cell: ptr mut i32 = libc.errno_cell()
209        cell.val = 0
210        if opened.finished then
211            item = (name: text.host_over(buffer[0..<0]), kind: unknown_kind)
212            return
213        end if
214        stream: ptr mut u8 = ptr(opened.stream)
215        record: libc.directory_entry = libc.readdir(stream)
216        match record
217            libc.no_directory: begin
218                fail errno_of(cell.val) when cell.val <> 0
219                opened.finished = true
220                item = (name: text.host_over(buffer[0..<0]),
221                        kind: unknown_kind)
222            end
223            ptr (found): begin
224                length: usize = usize(found.val.name_length)
225                fail unlisted when length >= lenof buffer
226                for at in usize(0)..<length do
227                    buffer[at] = found.val.name[at]
228                end for
229                buffer[length] = 0
230                item = (name: text.host_over(buffer[0..<length]),
231                        kind: kind_of_type(found.val.kind))
232            end
233        end match
234    end system_read_directory
235
236    system_close_directory: (opened: directory_stream) -> none ! errno =
237        stream: ptr mut u8 = ptr(opened.stream)
238        fail failure() when libc.closedir(stream) <> 0
239    end system_close_directory
240
241    system_make_directory: (at: descriptor, path: ptr u8, permissions: u32)
242                           -> none ! errno =
243        fail failure() when libc.mkdirat(i32(at), path, u16(permissions)) <> 0
244    end system_make_directory
245
246    system_remove: (at: descriptor, path: ptr u8, directory: bool)
247                   -> none ! errno =
248        flags: i32 = if directory then libc.at_removedir else 0 end if
249        fail failure() when libc.unlinkat(i32(at), path, flags) <> 0
250    end system_remove
251end if