1-- The rest of the file system: opening with options, renaming, a file's
2-- status, read-only files, synchronizing, sizes, reading and writing at
3-- an offset, temporary files and the working directory, a concept beside
4-- `files` and `directories` and outside `world`; and terminals, another.
5-- A firmware image that never asks for either carries neither.
6
7import core/text
8
9--- How `open_file` opens a file: to `read`, to `write`, or both; to
10--- `append`, every write going to the end of the file, which needs
11--- `write`; to `create` it when it is missing, and with `exclusive` too to
12--- refuse it when it is not, `already_exists`, which no other program can
13--- come between; and to `truncate` it to nothing as it is opened, which
14--- needs `write`. Any other combination, and one that neither reads nor
15--- writes, is `io_failed` before the provider is asked.
16public open_options: type = struct
17 read: bool
18 write: bool
19 append: bool
20 create: bool
21 exclusive: bool
22 truncate: bool
23end open_options
24
25--- Read and write a file that exists, from its start, keeping what it
26--- holds: `io.updating`.
27public updating: open_options = (read: true, write: true, append: false,
28 create: false, exclusive: false,
29 truncate: false)
30
31--- Write at the end of a file, making it when it is missing: a log.
32public appending: open_options = (read: false, write: true, append: true,
33 create: true, exclusive: false,
34 truncate: false)
35
36--- Read and write a new file, refusing a name that is taken with
37--- `already_exists`: the file is this program's alone.
38public creating: open_options = (read: true, write: true, append: false,
39 create: true, exclusive: true,
40 truncate: false)
41
42--- What a file is and the little every system says of it alike: its
43--- size in bytes, what it is, when it was last written, as the wall clock
44--- read then, and whether it is `read_only`, which on POSIX means that no
45--- permission bit lets anyone write it. A directory's own size differs
46--- between file systems; a symbolic link's is the length of its target.
47public status: type = struct
48 size: u64
49 kind: entry_kind
50 modified: wall_time
51 read_only: bool
52end status
53
54--- A temporary file `make_temporary` made: open for reading and writing,
55--- and its path, the directory's and a name of its own, lent from the
56--- caller's buffer. Nothing removes it: close it, then rename it into
57--- place or remove it, on every way out.
58public temporary: type = struct
59 opened: file
60 path: text.host_string
61end temporary
62
63--- The rest of the file system: a narrow concept of its own beside
64--- `files` and `directories`, outside `world`. A path is a host string,
65--- as `files` takes one, and a file is a `file` of the provider, which
66--- `reader`, `writer` and `files` take too.
67public file_system: type = concept (provider: type)
68 --- Open the file at `path` as `options` say. A missing file without
69 --- `create`, and a path through something that is not a directory,
70 --- are `not_found`; a file `exclusive` finds is `already_exists`; a
71 --- file the provider may not open so is `no_access`.
72 open_file: (self: provider, path: text.host_string,
73 options: open_options)
74 -> (opened: file)
75 ! already_exists | not_found | no_access | io_failed
76 --- Give the file at `old` the name `new` in one step, replacing a file
77 --- already named `new`; a missing `old` or parent of `new` is
78 --- `not_found`. A rename does not cross from one file system to
79 --- another, which is `io_failed`, nor puts a directory where a file is
80 --- or a file where a directory is.
81 rename: (self: provider, old: text.host_string, new: text.host_string)
82 -> none ! not_found | no_access | io_failed
83 --- The status of the file at `path`, of a final symbolic link's target
84 --- when `follow` is true and of the link itself when it is not.
85 status_of: (self: provider, path: text.host_string, follow: bool)
86 -> (found: status) ! not_found | no_access | io_failed
87 --- The status of an open file.
88 status_of_file: (self: provider, opened: file) -> (found: status)
89 ! io_failed
90 --- Make the file at `path` read-only, or with `read_only` false
91 --- writable by its owner again.
92 set_read_only: (self: provider, path: text.host_string, read_only: bool)
93 -> none ! not_found | no_access | io_failed
94 --- Have what the provider holds of an open file written where it
95 --- lasts before returning: its data and its status, or with
96 --- `data_only` its data and only what reading it back needs.
97 synchronize: (self: provider, opened: file, data_only: bool)
98 -> none ! io_failed
99 --- Give a file open for writing the size `size`, cutting it or
100 --- extending it with zeros.
101 truncate: (self: provider, opened: file, size: u64) -> none ! io_failed
102 --- Read at most `lenof into` bytes of an open file from `offset` on,
103 --- without moving the position `read` reads from: the count, zero at or
104 --- past its end.
105 read_at: (self: provider, opened: file, offset: u64, into: []mut u8)
106 -> (count: usize) ! io_failed
107 --- Write all of `bytes` into an open file from `offset` on, without
108 --- moving the position `write` writes at; a write past the end
109 --- extends the file, and what lies between reads as zeros. Never
110 --- write at an offset to a file opened to `append`.
111 write_at: (self: provider, opened: file, offset: u64, bytes: []u8)
112 -> none ! io_failed
113 --- Make and open a new file in `directory`, empty for the working
114 --- directory, with a name no other file has, made from bytes of
115 --- `names` and tried again when it is taken, and put its path in
116 --- `into`. It is made exclusively, so no other program's file is ever
117 --- opened in its place, and readable and writable by its owner alone.
118 make_temporary: (self: provider, directory: text.host_string,
119 names: any entropy, into: []mut u8)
120 -> (made: temporary from into)
121 ! not_found | no_access | io_failed
122 --- The working directory's path, put in `into`; too long for it is
123 --- `io_failed`. Nothing here changes the working directory, which is
124 --- the whole process's: a path is made from this one instead.
125 current_directory: (self: provider, into: []mut u8)
126 -> (path: text.host_string from into) ! io_failed
127end file_system
128
129--- Whether an open can have `options`: it reads or writes, appends or
130--- truncates only when it writes, and is `exclusive` only when it may
131--- `create`. A provider refuses any other with `io_failed` before it asks
132--- anything.
133public valid_options: (options: open_options) -> (yes: bool) =
134 some: bool = options.read or options.write
135 writes: bool = options.write or not (options.append or options.truncate)
136 makes: bool = options.create or not options.exclusive
137 yes = some and writes and makes
138end valid_options
139
140--- Open the file at `path` as `options` say, through `file_system`
141--- evidence; close it through the same provider's `files`.
142public open_file: (provider: type is file_system, host: provider,
143 path: text.host_string, options: open_options)
144 -> (opened: file)
145 ! already_exists | not_found | no_access | io_failed =
146 opened = try provider.open_file(host, path, options)
147end open_file
148
149--- Give the file at `old` the name `new`, replacing a file already named
150--- `new` in the same step: no program sees both names or neither. The way
151--- to replace a file whole is to write a `make_temporary` file beside it,
152--- `synchronize` it, close it and rename it onto the file.
153public rename: (provider: type is file_system, host: provider,
154 old: text.host_string, new: text.host_string)
155 -> none ! not_found | no_access | io_failed =
156 try provider.rename(host, old, new)
157end rename
158
159--- The status of the file at `path`, of a final symbolic link's target
160--- when `follow` is true and of the link itself when it is not.
161public status_of: (provider: type is file_system, host: provider,
162 path: text.host_string, follow: bool)
163 -> (found: status) ! not_found | no_access | io_failed =
164 found = try provider.status_of(host, path, follow)
165end status_of
166
167--- The status of an open file.
168public status_of_file: (provider: type is file_system, host: provider,
169 opened: file) -> (found: status) ! io_failed =
170 found = try provider.status_of_file(host, opened)
171end status_of_file
172
173--- Make the file at `path` read-only, or writable by its owner again. A
174--- read-only file refuses to be opened for writing, `no_access`, except to
175--- a process the system lets past every permission, as POSIX's root.
176public set_read_only: (provider: type is file_system, host: provider,
177 path: text.host_string, read_only: bool)
178 -> none ! not_found | no_access | io_failed =
179 try provider.set_read_only(host, path, read_only)
180end set_read_only
181
182--- Have what the provider holds of an open file written where it lasts,
183--- its status too unless `data_only`, before returning.
184public synchronize: (provider: type is file_system, host: provider,
185 opened: file, data_only: bool) -> none ! io_failed =
186 try provider.synchronize(host, opened, data_only)
187end synchronize
188
189--- Give a file open for writing the size `size`, cutting it or extending
190--- it with zeros.
191public truncate: (provider: type is file_system, host: provider,
192 opened: file, size: u64) -> none ! io_failed =
193 try provider.truncate(host, opened, size)
194end truncate
195
196--- Read at most `lenof into` bytes of an open file from `offset` on: the
197--- count, which may be short, and zero at or past its end.
198public read_at: (provider: type is file_system, host: provider,
199 opened: file, offset: u64, into: []mut u8)
200 -> (count: usize) ! io_failed =
201 count = try provider.read_at(host, opened, offset, into)
202end read_at
203
204--- Write all of `bytes` into an open file from `offset` on, a write past
205--- the end leaving zeros between; a failure may follow part of them.
206public write_at: (provider: type is file_system, host: provider,
207 opened: file, offset: u64, bytes: []u8)
208 -> none ! io_failed =
209 try provider.write_at(host, opened, offset, bytes)
210end write_at
211
212--- Make and open a new file in `directory` whose name no other file has,
213--- made from bytes the entropy `names` gives, and put its path in `into`,
214--- which must hold the directory, a `/` and the 21 bytes of the name; the
215--- name is `.tmp-` and sixteen hexadecimal digits, hidden from an ordinary
216--- listing. The hosted provider is its own entropy too,
217--- `io.make_temporary(host, host, directory, buffer)`, and a test passes
218--- scripted bytes to see a name that is taken tried again.
219public make_temporary: (provider: type is file_system, host: provider,
220 source: type is entropy, names: source,
221 directory: text.host_string, into: []mut u8)
222 -> (made: temporary from into)
223 ! not_found | no_access | io_failed =
224 erased: any entropy = any(addr names)
225 made = try provider.make_temporary(host, directory, erased, into)
226end make_temporary
227
228--- The working directory's path, put in `into`.
229public current_directory: (provider: type is file_system, host: provider,
230 into: []mut u8)
231 -> (path: text.host_string from into)
232 ! io_failed =
233 path = try provider.current_directory(host, into)
234end current_directory
235
236--- How many times `make_temporary` tries a name before it gives up with
237--- `io_failed`: a name is taken only when the bytes `names` gives repeat.
238public temporary_tries: usize = 16
239
240--- The path `make_temporary` tries, from eight bytes of `names`: the
241--- directory, a `/` unless it is empty or ends in one, `.tmp-` and the
242--- bytes as sixteen lower-case hexadecimal digits, put in `into`; one that
243--- does not fit is `io_failed`. For a provider in another module.
244public temporary_path: (directory: text.host_string, bytes: [8]u8,
245 into: []mut u8)
246 -> (path: text.host_string from into) ! io_failed =
247 path = text.host_over(into[0..<0])
248 mut made := text.over(into)
249 folder: []u8 = text.host_bytes(directory)
250 text.append(made, folder) else (problem)
251 _ = problem
252 fail io_failed
253 end
254 divided: bool = lenof folder == 0 or folder[lenof folder - 1] == 47
255 separator: []u8 = if divided then "" else "/" end if
256 text.append(made, separator) else (problem)
257 _ = problem
258 fail io_failed
259 end
260 text.append(made, ".tmp-") else (problem)
261 _ = problem
262 fail io_failed
263 end
264 for byte in bytes do
265 start: usize = text.length(made)
266 text.append_hex_u32(made, u32(byte)) else (problem)
267 _ = problem
268 fail io_failed
269 end
270 text.align_right(made, start, 2, 48) else (problem)
271 _ = problem
272 fail io_failed
273 end
274 end for
275 length: usize = text.length(made)
276 path = text.host_over(into[0..<length])
277end temporary_path
278
279--- The file is not a terminal.
280public not_a_terminal: atom
281
282--- A terminal's size in characters: its rows and columns.
283public window: type = struct
284 rows: u16
285 columns: u16
286end window
287
288--- Terminals: whether a stream is one, and its size, so a program can
289--- tell a person from a pipe and fit what it writes to the screen. A
290--- concept of its own, outside `world`.
291public terminals: type = concept (provider: type)
292 --- Whether `stream` is a terminal.
293 is_terminal: (self: provider, stream: file) -> (yes: bool)
294 --- The size of the terminal `stream`, `not_a_terminal` for anything
295 --- else. A terminal nothing has sized is zero by zero.
296 window_size: (self: provider, stream: file) -> (size: window)
297 ! not_a_terminal | io_failed
298end terminals
299
300--- Whether `stream` is a terminal, through `terminals` evidence: false
301--- for a file, a pipe and a socket, whatever a program writes to it.
302public is_terminal: (provider: type is terminals, host: provider,
303 stream: file) -> (yes: bool) =
304 yes = provider.is_terminal(host, stream)
305end is_terminal
306
307--- The size of the terminal `stream` in rows and columns.
308public window_size: (provider: type is terminals, host: provider,
309 stream: file) -> (size: window)
310 ! not_a_terminal | io_failed =
311 size = try provider.window_size(host, stream)
312end window_size
313
314--- One terminal of `terminals_over`'s fake: the file it is and its size.
315public screen: type = struct
316 stream: file
317 size: window
318end screen
319
320screens_value: type = struct
321 screens: []screen
322end screens_value
323
324--- Terminals a test gives: each `screen` is a terminal of its size, and
325--- every other file is no terminal. A provider of `terminals`; copies
326--- share the screens.
327public memory_terminals: type = screens_value
328
329--- Terminals over `screens`, which the handle lends; keep them alive while
330--- it is used.
331public terminals_over: (screens: []screen)
332 -> (host: memory_terminals from screens) =
333 host = (screens: screens)
334end terminals_over
335
336screen_count: (values: []screen) -> (count: usize) =
337 count = lenof values
338end screen_count
339
340memory_is_terminal: (self: memory_terminals, stream: file) -> (yes: bool) =
341 yes = false
342 for index in usize(0)..<screen_count(self.screens) do
343 if self.screens[index].stream == stream then yes = true end if
344 end for
345end memory_is_terminal
346
347memory_window_size: (self: memory_terminals, stream: file)
348 -> (size: window) ! not_a_terminal | io_failed =
349 size = (rows: 0, columns: 0)
350 for index in usize(0)..<screen_count(self.screens) do
351 if self.screens[index].stream == stream then
352 size = self.screens[index].size
353 return
354 end if
355 end for
356 fail not_a_terminal
357end memory_window_size
358
359memory_terminals is terminals
360 (is_terminal: memory_is_terminal, window_size: memory_window_size)