files.bend

bend-kit-files@0.3.1.0 · 307 lines · 43 definitions

1# POSIX paths plus directory and metadata effects. Source: https://github.com/paymog/bend-kit/tree/main/files
2import Base
3import bend-kit-bytes@0.3.3.0/bytes.bend as Bytes
4
5# The path.* defs are pure and lexical: they never touch the disk.
6# The effects are thin OS calls; each failure is (errno, text).
7#   import ./files/files.bend as Fs
8
9def path.is_absolute(p: String) -> Bool:
10  String.starts_with(p, "/")
11
12def path.join.rel(bare: Bool, a: String, b: String) -> String:
13  match bare:
14    case True{}:
15      a ++ b
16    case False{}:
17      a ++ "/" ++ b
18
19def path.join.if(abs: Bool, +a: String, b: String) -> String:
20  match abs:
21    case True{}:
22      b
23    case False{}:
24      path.join.rel(String.is_empty(a) || String.ends_with(a, "/"), a, b)
25
26# b when b is absolute; otherwise b under a.
27def path.join(a: String, +b: String) -> String:
28  path.join.if(path.is_absolute(b), a, b)
29
30def path.norm.pop.if(up: Bool, h: String, t: List<&2, String>) ->
31  List<&2, String>:
32  match up:
33    case True{}:
34      ".." <> (h <> t)
35    case False{}:
36      t
37
38# A ".." above the root is dropped; above a relative start it is kept.
39def path.norm.pop(stack: List<&2, String>, abs: Bool) -> List<&2, String>:
40  match stack:
41    case Nil{}:
42      Bool.pick(List<&2, String>, abs, Nil{}, [".."])
43    case Con{+h, t}:
44      path.norm.pop.if(String.eq(h, ".."), h, t)
45
46def path.norm.step(
47  skip: Bool, up: Bool, s: String, stack: List<&2, String>, abs: Bool
48) -> List<&2, String>:
49  match skip:
50    case True{}:
51      stack
52    case False{}:
53      match up:
54        case True{}:
55          path.norm.pop(stack, abs)
56        case False{}:
57          s <> stack
58
59# The kept segments, last first.
60def path.norm.go(segs: List<&2, String>, stack: List<&2, String>, +abs: Bool) ->
61  List<&2, String>:
62  match segs:
63    case Nil{}:
64      stack
65    case Con{+s, t}:
66      path.norm.go(t,
67        path.norm.step(String.eq(s, "") || String.eq(s, "."), String.eq(s, ".."),
68          s, stack, abs),
69        abs)
70
71def path.norm.fin(abs: Bool, body: String) -> String:
72  match abs:
73    case True{}:
74      "/" ++ body
75    case False{}:
76      match body:
77        case SNil{}:
78          "."
79        case SCon{h, t}:
80          SCon{h, t}
81
82# Drops empty and "." segments and resolves "..", as POSIX normpath does
83# (RFC 3986 §5.2.4 for paths), but always to one leading "/".
84def path.normalize(+p: String) -> String:
85  +abs = path.is_absolute(p)
86  path.norm.fin(abs,
87    String.join(List.reverse(&2, String,
88      path.norm.go(String.split(p, '/'), Nil{}, abs)), "/"))
89
90def path.parent(p: String) -> String:
91  +n = path.normalize(p)
92  path.norm.fin(path.is_absolute(n),
93    String.join(List.reverse(&2, String,
94      List.tail(&2, String, List.reverse(&2, String, String.split(n, '/')))), "/"))
95
96def path.file_name.fin(m: Maybe<&2, String>) -> String:
97  match m:
98    case None{}:
99      ""
100    case Some{+s}:
101      Bool.pick(String, String.eq(s, "."), "", s)
102
103# The last segment of the normalized path; "" for "/" and ".".
104def path.file_name(p: String) -> String:
105  path.file_name.fin(List.last(&2, String, String.split(path.normalize(p), '/')))
106
107def path.extension.fin(bare: Bool, ext: String) -> String:
108  match bare:
109    case True{}:
110      ""
111    case False{}:
112      ext
113
114def path.extension.go(rev: List<&2, String>) -> String:
115  match rev:
116    case Nil{}:
117      ""
118    case Con{ext, Nil{}}:
119      ""
120    case Con{ext, Con{prev, rest}}:
121      path.extension.fin(String.is_empty(prev) && List.is_empty(&2, String, rest),
122        ext)
123
124# The text after the file name's last ".", without the dot. A leading dot
125# does not start one: ".bashrc" has none.
126def path.extension(p: String) -> String:
127  path.extension.go(List.reverse(&2, String, String.split(path.file_name(p), '.')))
128
129type FileType is Data:
130  Regular{}
131  Directory{}
132  Other{}
133
134# mtime is whole seconds since the Unix epoch. size and mtime wrap at 2^32.
135type Info is Data:
136  Info{kind: FileType, size: U32, mtime: U32}
137
138# Entry names, NUL-separated, in the order the OS gives them.
139def list_dir.raw(path: String) -> IO(Result<&1, &1, U32 & String, String>):
140  import "./effs/files.c"
141  import "./effs/files.js"
142
143# (kind, size, mtime); kind 0 is a regular file, 1 a directory, 2 other.
144# Symbolic links are followed.
145def stat.raw(path: String) ->
146  IO(Result<&1, &1, U32 & String, U32 & U32 & U32>):
147  import "./effs/files.c"
148  import "./effs/files.js"
149
150def mkdir(path: String) -> IO(Result<&1, &1, U32 & String, Unit>):
151  import "./effs/files.c"
152  import "./effs/files.js"
153
154# Removes a file or an empty directory.
155def remove(path: String) -> IO(Result<&1, &1, U32 & String, Unit>):
156  import "./effs/files.c"
157  import "./effs/files.js"
158
159def rename(from: String, to: String) ->
160  IO(Result<&1, &1, U32 & String, Unit>):
161  import "./effs/files.c"
162  import "./effs/files.js"
163
164# Creates a new, empty, private directory under the system temp dir.
165def temp_dir() -> IO(Result<&1, &1, U32 & String, String>):
166  import "./effs/files.c"
167  import "./effs/files.js"
168
169# A successful zero-length read is EOF (or max = 0). Reads at most 1 MiB
170# per call; a short read is not necessarily EOF. Errors retain the File.
171def read.words(file: File, max: U32) ->
172  IO(File & Result<&1, &1, U32 & String, U32 & Array<U32>>):
173  import "./effs/files.c"
174  import "./effs/files.js"
175
176# The size of an open file as (hi, lo): hi * 2^32 + lo bytes. Errors retain the File.
177def size64(file: File) ->
178  IO(File & Result<&1, &1, U32 & String, U32 & U32>):
179  import "./effs/files.c"
180  import "./effs/files.js"
181
182# read.words at byte offset hi * 2^32 + lo. The file position does not move.
183def read.words.at(file: File, hi: U32, lo: U32, max: U32) ->
184  IO(File & Result<&1, &1, U32 & String, U32 & Array<U32>>):
185  import "./effs/files.c"
186  import "./effs/files.js"
187
188def read_bytes.wrap(result: Result<&1, &1, U32 & String, U32 & Array<U32>>) ->
189  Result<&1, &1, U32 & String, Bytes.Bytes>:
190  match result:
191    case Fail{err}:
192      Fail{err}
193    case Done{(len, words)}:
194      Done{Bytes.Bytes{len, words}}
195
196def read_bytes.pair(pair: File & Result<&1, &1, U32 & String, U32 & Array<U32>>) ->
197  File & Result<&1, &1, U32 & String, Bytes.Bytes>:
198  match pair:
199    case (handle, result):
200      (handle, read_bytes.wrap(result))
201
202# The existing Bytes layout, without an octet List or String.
203def read_bytes(file: File, max: U32) ->
204  IO(File & Result<&1, &1, U32 & String, Bytes.Bytes>):
205  do IO<File & Result<&1, &1, U32 & String, Bytes.Bytes>>:
206    pair : File & Result<&1, &1, U32 & String, U32 & Array<U32>> <- read.words(file, max)
207    return read_bytes.pair(pair)
208
209# Writes the entire buffer or fails. Errors retain the File; writes before
210# an error are not rolled back. A len beyond the word array fails EINVAL.
211def write.words(file: File, len: U32, words: Array<U32>) ->
212  IO(File & Result<&1, &1, U32 & String, Unit>):
213  import "./effs/files.c"
214  import "./effs/files.js"
215
216def write_bytes(file: File, data: Bytes.Bytes) ->
217  IO(File & Result<&1, &1, U32 & String, Unit>):
218  Bytes.Bytes{len, buf} = data
219  write.words(file, len, buf)
220
221def list_dir.fin(r: Result<&1, &1, U32 & String, String>) ->
222  Result<&1, &1, U32 & String, List<&2, String>>:
223  match r:
224    case Fail{e}:
225      Fail{e}
226    case Done{s}:
227      Done{List.sort(~String, ~String.is_le,
228        List.filter(~String, ~(n => Bool.not(String.is_empty(n))),
229          String.split(s, Chr{0})))}
230
231# Entry names, without "." and "..", sorted.
232def list_dir(path: String) ->
233  IO(Result<&1, &1, U32 & String, List<&2, String>>):
234  do IO<Result<&1, &1, U32 & String, List<&2, String>>>:
235    r : Result<&1, &1, U32 & String, String> <- list_dir.raw(path)
236    return list_dir.fin(r)
237
238def stat.kind(+k: U32) -> FileType:
239  Bool.pick(FileType, U32.is_eq(k, 0), Regular{},
240    Bool.pick(FileType, U32.is_eq(k, 1), Directory{}, Other{}))
241
242def stat.fin(r: Result<&1, &1, U32 & String, U32 & U32 & U32>) ->
243  Result<&1, &1, U32 & String, Info>:
244  match r:
245    case Fail{e}:
246      Fail{e}
247    case Done{(k, size, mtime)}:
248      Done{Info{stat.kind(k), size, mtime}}
249
250def stat(path: String) -> IO(Result<&1, &1, U32 & String, Info>):
251  do IO<Result<&1, &1, U32 & String, Info>>:
252    r : Result<&1, &1, U32 & String, U32 & U32 & U32> <- stat.raw(path)
253    return stat.fin(r)
254
255def exists(path: String) -> IO(Bool):
256  do IO<Bool>:
257    r : Result<&1, &1, U32 & String, U32 & U32 & U32> <- stat.raw(path)
258    return Result.is_done(&1, &1, U32 & String, U32 & U32 & U32, r)
259
260def mkdir_all.dir(r: Result<&1, &1, U32 & String, Info>) ->
261  Result<&1, &1, U32 & String, Unit>:
262  match r:
263    case Fail{e}:
264      Fail{e}
265    case Done{Info{Directory{}, size, mtime}}:
266      Done{Unit{}}
267    case Done{i}:
268      Fail{(17, "File exists")}
269
270def mkdir_all.fail(exists: Bool, e: U32 & String,
271  k: IO(Result<&1, &1, U32 & String, Unit>)) ->
272  IO(Result<&1, &1, U32 & String, Unit>):
273  match exists:
274    case True{}:
275      k
276    case False{}:
277      IO.pure(Result<&1, &1, U32 & String, Unit>, Fail{e})
278
279def mkdir_all.step(r: Result<&1, &1, U32 & String, Unit>,
280  k: IO(Result<&1, &1, U32 & String, Unit>)) ->
281  IO(Result<&1, &1, U32 & String, Unit>):
282  match r:
283    case Fail{(+code, text)}:
284      mkdir_all.fail(U32.is_eq(code, 17), (code, text), k)
285    case Done{u}:
286      k
287
288def mkdir_all.go(segs: List<&2, String>, +at: String) ->
289  IO(Result<&1, &1, U32 & String, Unit>):
290  match segs:
291    case Nil{}:
292      do IO<Result<&1, &1, U32 & String, Unit>>:
293        r : Result<&1, &1, U32 & String, Info> <- stat(at)
294        return mkdir_all.dir(r)
295    case Con{s, t}:
296      +next = path.join(at, s)
297      do IO<Result<&1, &1, U32 & String, Unit>>:
298        r : Result<&1, &1, U32 & String, Unit> <- mkdir(next)
299        mkdir_all.step(r, mkdir_all.go(t, next))
300
301# Creates the directory and any missing parents. Succeeds when it is
302# already a directory; fails if a component is not a directory.
303def mkdir_all(+path: String) -> IO(Result<&1, &1, U32 & String, Unit>):
304  mkdir_all.go(
305    List.filter(~String, ~(n => Bool.not(String.eq(n, ""))),
306      String.split(path, '/')),
307    Bool.pick(String, path.is_absolute(path), "/", "."))