Landin library reference discuss source

core/io/file_system.ldn

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)