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