@grain/stdlib 0.6.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +1 -1
  3. package/README.md +2 -2
  4. package/array.gr +55 -7
  5. package/array.md +606 -560
  6. package/bigint.md +228 -228
  7. package/buffer.gr +85 -53
  8. package/buffer.md +442 -319
  9. package/bytes.gr +112 -35
  10. package/bytes.md +299 -219
  11. package/char.gr +201 -99
  12. package/char.md +447 -120
  13. package/exception.gr +11 -11
  14. package/exception.md +29 -4
  15. package/float32.gr +327 -3
  16. package/float32.md +698 -111
  17. package/float64.gr +320 -3
  18. package/float64.md +698 -111
  19. package/fs.gr +1082 -0
  20. package/fs.md +630 -0
  21. package/hash.gr +142 -88
  22. package/hash.md +105 -17
  23. package/int16.md +178 -178
  24. package/int32.gr +26 -5
  25. package/int32.md +266 -231
  26. package/int64.gr +27 -2
  27. package/int64.md +266 -231
  28. package/int8.md +178 -178
  29. package/json.gr +366 -51
  30. package/json.md +431 -15
  31. package/list.gr +328 -31
  32. package/list.md +759 -336
  33. package/map.gr +20 -12
  34. package/map.md +266 -260
  35. package/marshal.gr +41 -40
  36. package/marshal.md +14 -14
  37. package/number.gr +278 -35
  38. package/number.md +688 -269
  39. package/option.md +162 -162
  40. package/package.json +5 -3
  41. package/path.gr +48 -0
  42. package/path.md +180 -89
  43. package/pervasives.gr +2 -2
  44. package/pervasives.md +275 -275
  45. package/priorityqueue.gr +7 -7
  46. package/priorityqueue.md +131 -131
  47. package/queue.gr +183 -29
  48. package/queue.md +404 -148
  49. package/random.md +43 -43
  50. package/range.gr +4 -4
  51. package/range.md +42 -42
  52. package/rational.md +123 -123
  53. package/regex.gr +52 -51
  54. package/regex.md +102 -102
  55. package/result.md +118 -118
  56. package/runtime/atof/common.md +39 -39
  57. package/runtime/atof/decimal.gr +6 -6
  58. package/runtime/atof/decimal.md +14 -14
  59. package/runtime/atof/lemire.gr +5 -5
  60. package/runtime/atof/lemire.md +1 -1
  61. package/runtime/atof/parse.gr +16 -16
  62. package/runtime/atof/parse.md +2 -2
  63. package/runtime/atof/slow.md +1 -1
  64. package/runtime/atof/table.md +2 -2
  65. package/runtime/atoi/parse.gr +3 -3
  66. package/runtime/atoi/parse.md +1 -1
  67. package/runtime/bigint.gr +15 -47
  68. package/runtime/bigint.md +54 -60
  69. package/runtime/compare.gr +2 -2
  70. package/runtime/compare.md +8 -8
  71. package/runtime/dataStructures.md +211 -211
  72. package/runtime/debugPrint.gr +4 -1
  73. package/runtime/debugPrint.md +9 -9
  74. package/runtime/equal.gr +99 -77
  75. package/runtime/equal.md +8 -8
  76. package/runtime/exception.gr +62 -82
  77. package/runtime/exception.md +62 -11
  78. package/runtime/gc.gr +39 -45
  79. package/runtime/gc.md +4 -4
  80. package/runtime/malloc.gr +7 -7
  81. package/runtime/malloc.md +13 -13
  82. package/runtime/math/kernel/cos.gr +70 -0
  83. package/runtime/math/kernel/cos.md +14 -0
  84. package/runtime/math/kernel/sin.gr +65 -0
  85. package/runtime/math/kernel/sin.md +14 -0
  86. package/runtime/math/kernel/tan.gr +136 -0
  87. package/runtime/math/kernel/tan.md +14 -0
  88. package/runtime/math/rempio2.gr +244 -0
  89. package/runtime/math/rempio2.md +14 -0
  90. package/runtime/math/trig.gr +130 -0
  91. package/runtime/math/trig.md +28 -0
  92. package/runtime/math/umuldi.gr +26 -0
  93. package/runtime/math/umuldi.md +14 -0
  94. package/runtime/numberUtils.gr +29 -29
  95. package/runtime/numberUtils.md +12 -12
  96. package/runtime/numbers.gr +373 -381
  97. package/runtime/numbers.md +348 -342
  98. package/runtime/string.gr +37 -105
  99. package/runtime/string.md +20 -26
  100. package/runtime/unsafe/constants.md +24 -24
  101. package/runtime/unsafe/conv.md +19 -19
  102. package/runtime/unsafe/memory.gr +24 -20
  103. package/runtime/unsafe/memory.md +27 -7
  104. package/runtime/unsafe/offsets.gr +36 -0
  105. package/runtime/unsafe/offsets.md +88 -0
  106. package/runtime/unsafe/panic.gr +28 -0
  107. package/runtime/unsafe/panic.md +14 -0
  108. package/runtime/unsafe/tags.md +32 -32
  109. package/runtime/unsafe/wasmf32.md +28 -28
  110. package/runtime/unsafe/wasmf64.md +28 -28
  111. package/runtime/unsafe/wasmi32.md +47 -47
  112. package/runtime/unsafe/wasmi64.md +50 -50
  113. package/runtime/utf8.gr +189 -0
  114. package/runtime/utf8.md +117 -0
  115. package/runtime/wasi.gr +4 -2
  116. package/runtime/wasi.md +147 -147
  117. package/set.gr +18 -11
  118. package/set.md +253 -247
  119. package/stack.gr +171 -2
  120. package/stack.md +371 -89
  121. package/string.gr +352 -557
  122. package/string.md +298 -255
  123. package/uint16.md +170 -170
  124. package/uint32.gr +25 -4
  125. package/uint32.md +249 -214
  126. package/uint64.gr +25 -5
  127. package/uint64.md +249 -214
  128. package/uint8.md +170 -170
  129. package/uri.gr +57 -53
  130. package/uri.md +88 -89
  131. package/wasi/file.gr +67 -59
  132. package/wasi/file.md +308 -308
  133. package/wasi/process.md +26 -26
  134. package/wasi/random.md +12 -12
  135. package/wasi/time.md +16 -16
  136. package/runtime/utils/printing.gr +0 -60
  137. package/runtime/utils/printing.md +0 -26
package/fs.gr ADDED
@@ -0,0 +1,1082 @@
1
+ /**
2
+ * Utilities for high-level file system interactions. Utilizes WASI Preview 1 for underlying API
3
+ *
4
+ * @example from "fs" include Fs
5
+ * @example Fs.Utf8.readFile(Path.fromString("baz.txt"))
6
+ * @example Fs.Utf8.writeFile(Path.fromString("baz.txt"), "Hello World\n")
7
+ * @example Fs.copy(Path.fromString("foo.txt"), Path.fromString("foocopy.txt"))
8
+ *
9
+ * @since v0.7.0
10
+ */
11
+ module Fs
12
+
13
+ from "array" include Array
14
+ from "bytes" include Bytes
15
+ from "int64" include Int64
16
+ from "option" include Option
17
+ from "path" include Path
18
+ from "result" include Result
19
+ from "string" include String
20
+ from "wasi/file" include File
21
+ from "runtime/wasi" include Wasi
22
+ from "runtime/unsafe/wasmi32" include WasmI32
23
+ from "runtime/dataStructures" include DataStructures
24
+
25
+ /**
26
+ * Potential errors that can be raised from system interactions.
27
+ */
28
+ provide enum FileError {
29
+ NoPreopenedDirectories,
30
+ PermissionDenied,
31
+ AddressInUse,
32
+ AddressNotAvailable,
33
+ AddressFamilyNotSupported,
34
+ ResourceUnavailableOrOperationWouldBlock,
35
+ ConnectionAlreadyInProgress,
36
+ BadFileDescriptor,
37
+ BadMessage,
38
+ DeviceOrResourceBusy,
39
+ OperationCanceled,
40
+ NoChildProcesses,
41
+ ConnectionAborted,
42
+ ConnectionRefused,
43
+ ConnectionReset,
44
+ ResourceDeadlockWouldOccur,
45
+ DestinationAddressRequired,
46
+ MathematicsArgumentOutOfDomainOfFunction,
47
+ FileExists,
48
+ BadAddress,
49
+ FileTooLarge,
50
+ HostIsUnreachable,
51
+ IdentifierRemoved,
52
+ IllegalByteSequence,
53
+ OperationInProgress,
54
+ InterruptedFunction,
55
+ InvalidArgument,
56
+ IOError,
57
+ SocketIsConnected,
58
+ IsADirectory,
59
+ TooManyLevelsOfSymbolicLinks,
60
+ FileDescriptorValueTooLarge,
61
+ TooManyLinks,
62
+ MessageTooLarge,
63
+ FilenameTooLong,
64
+ NetworkIsDown,
65
+ ConnectionAbortedByNetwork,
66
+ NetworkUnreachable,
67
+ TooManyFilesOpenInSystem,
68
+ NoBufferSpaceAvailable,
69
+ NoSuchDevice,
70
+ NoSuchFileOrDirectory,
71
+ ExecutableFileFormatError,
72
+ NoLocksAvailable,
73
+ NotEnoughSpace,
74
+ NoMessageOfTheDesiredType,
75
+ ProtocolNotAvailable,
76
+ NoSpaceLeftOnDevice,
77
+ FunctionNotSupported,
78
+ TheSocketIsNotConnected,
79
+ NotADirectoryOrASymbolicLinkToADirectory,
80
+ DirectoryNotEmpty,
81
+ StateNotRecoverable,
82
+ NotASocket,
83
+ NotSupportedOrOperationNotSupportedOnSocket,
84
+ InappropriateIOControlOperation,
85
+ NoSuchDeviceOrAddress,
86
+ ValueTooLargeToBeStoredInDataType,
87
+ PreviousOwnerDied,
88
+ OperationNotPermitted,
89
+ BrokenPipe,
90
+ ProtocolError,
91
+ ProtocolNotSupported,
92
+ ProtocolWrongTypeForSocket,
93
+ ResultTooLarge,
94
+ ReadOnlyFileSystem,
95
+ InvalidSeek,
96
+ NoSuchProcess,
97
+ ConnectionTimedOut,
98
+ TextFileBusy,
99
+ CrossDeviceLink,
100
+ ExtensionCapabilitiesInsufficient,
101
+ UnknownFileError(Number),
102
+ }
103
+
104
+ let errnoToFileError = errno => {
105
+ match (errno) {
106
+ 2 => PermissionDenied,
107
+ 3 => AddressInUse,
108
+ 4 => AddressNotAvailable,
109
+ 5 => AddressFamilyNotSupported,
110
+ 6 => ResourceUnavailableOrOperationWouldBlock,
111
+ 7 => ConnectionAlreadyInProgress,
112
+ 8 => BadFileDescriptor,
113
+ 9 => BadMessage,
114
+ 10 => DeviceOrResourceBusy,
115
+ 11 => OperationCanceled,
116
+ 12 => NoChildProcesses,
117
+ 13 => ConnectionAborted,
118
+ 14 => ConnectionRefused,
119
+ 15 => ConnectionReset,
120
+ 16 => ResourceDeadlockWouldOccur,
121
+ 17 => DestinationAddressRequired,
122
+ 18 => MathematicsArgumentOutOfDomainOfFunction,
123
+ 20 => FileExists,
124
+ 21 => BadAddress,
125
+ 22 => FileTooLarge,
126
+ 23 => HostIsUnreachable,
127
+ 24 => IdentifierRemoved,
128
+ 25 => IllegalByteSequence,
129
+ 26 => OperationInProgress,
130
+ 27 => InterruptedFunction,
131
+ 28 => InvalidArgument,
132
+ 29 => IOError,
133
+ 30 => SocketIsConnected,
134
+ 31 => IsADirectory,
135
+ 32 => TooManyLevelsOfSymbolicLinks,
136
+ 33 => FileDescriptorValueTooLarge,
137
+ 34 => TooManyLinks,
138
+ 35 => MessageTooLarge,
139
+ 37 => FilenameTooLong,
140
+ 38 => NetworkIsDown,
141
+ 39 => ConnectionAbortedByNetwork,
142
+ 40 => NetworkUnreachable,
143
+ 41 => TooManyFilesOpenInSystem,
144
+ 42 => NoBufferSpaceAvailable,
145
+ 43 => NoSuchDevice,
146
+ 44 => NoSuchFileOrDirectory,
147
+ 45 => ExecutableFileFormatError,
148
+ 46 => NoLocksAvailable,
149
+ 48 => NotEnoughSpace,
150
+ 49 => NoMessageOfTheDesiredType,
151
+ 50 => ProtocolNotAvailable,
152
+ 51 => NoSpaceLeftOnDevice,
153
+ 52 => FunctionNotSupported,
154
+ 53 => TheSocketIsNotConnected,
155
+ 54 => NotADirectoryOrASymbolicLinkToADirectory,
156
+ 55 => DirectoryNotEmpty,
157
+ 56 => StateNotRecoverable,
158
+ 57 => NotASocket,
159
+ 58 => NotSupportedOrOperationNotSupportedOnSocket,
160
+ 59 => InappropriateIOControlOperation,
161
+ 60 => NoSuchDeviceOrAddress,
162
+ 61 => ValueTooLargeToBeStoredInDataType,
163
+ 62 => PreviousOwnerDied,
164
+ 63 => OperationNotPermitted,
165
+ 64 => BrokenPipe,
166
+ 65 => ProtocolError,
167
+ 66 => ProtocolNotSupported,
168
+ 67 => ProtocolWrongTypeForSocket,
169
+ 68 => ResultTooLarge,
170
+ 69 => ReadOnlyFileSystem,
171
+ 70 => InvalidSeek,
172
+ 71 => NoSuchProcess,
173
+ 73 => ConnectionTimedOut,
174
+ 74 => TextFileBusy,
175
+ 75 => CrossDeviceLink,
176
+ 76 => ExtensionCapabilitiesInsufficient,
177
+ _ => UnknownFileError(errno),
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Represents non-standard system file types.
183
+ */
184
+ provide enum SpecialFileType {
185
+ Unknown,
186
+ BlockDevice,
187
+ CharacterDevice,
188
+ SocketDatagram,
189
+ SocketStream,
190
+ }
191
+
192
+ /**
193
+ * Represents different system file types.
194
+ */
195
+ provide enum FileType {
196
+ File,
197
+ Directory,
198
+ SymbolicLink,
199
+ Special(SpecialFileType),
200
+ }
201
+
202
+ /**
203
+ * Represents metadata about a file.
204
+ */
205
+ provide record Stats {
206
+ fileType: FileType,
207
+ /** File size in bytes */
208
+ size: Number,
209
+ /** Last accessed timestamp in nanoseconds */
210
+ accessedTimestamp: Number,
211
+ /** Last modified timestamp in nanoseconds */
212
+ modifiedTimestamp: Number,
213
+ /** Last file status change timestamp in nanoseconds */
214
+ changedTimestamp: Number,
215
+ }
216
+
217
+ /**
218
+ * Represents information about an item in a directory.
219
+ */
220
+ provide record DirectoryEntry {
221
+ name: String,
222
+ fileType: FileType,
223
+ }
224
+
225
+ /**
226
+ * The type of removal operation to perform when calling `remove`.
227
+ */
228
+ provide enum RemoveMode {
229
+ RemoveFile,
230
+ RemoveEmptyDirectory,
231
+ RemoveRecursive,
232
+ }
233
+
234
+ /**
235
+ * The type of copy operation to perform when calling `copy`.
236
+ */
237
+ provide enum CopyMode {
238
+ CopyFile,
239
+ CopyRecursive,
240
+ }
241
+
242
+ /**
243
+ * The type of write operation to perform when calling `writeFile`.
244
+ */
245
+ provide enum WriteMode {
246
+ Truncate,
247
+ Append,
248
+ }
249
+
250
+ enum OpenMode {
251
+ Unlink,
252
+ RmDir,
253
+ RmrfDir,
254
+ CopySourceBase,
255
+ CopyTargetBase,
256
+ CopySource,
257
+ CopyTarget,
258
+ RenameSource,
259
+ RenameTarget,
260
+ ReadDir,
261
+ MakeDir,
262
+ MakeSymlink,
263
+ ReadLink,
264
+ Stats,
265
+ Read(Bool),
266
+ WriteFile(Bool),
267
+ AppendFile(Bool),
268
+ }
269
+
270
+ let fileResult = result => {
271
+ match (result) {
272
+ Ok(ok) => Ok(ok),
273
+ Err(Wasi.SystemError(err)) => Err(errnoToFileError(err)),
274
+ _ => fail "Impossible: non-wasi error",
275
+ }
276
+ }
277
+
278
+ let openInfo = openMode => {
279
+ match (openMode) {
280
+ Unlink => ([File.Directory: File.OpenFlag], [File.PathUnlinkFile], [], []),
281
+ RmDir => ([File.Directory], [File.PathRemoveDirectory], [], []),
282
+ RmrfDir => {
283
+ let rights = [
284
+ File.PathOpen,
285
+ File.PathFilestats,
286
+ File.PathRemoveDirectory,
287
+ File.PathUnlinkFile,
288
+ File.FdReaddir,
289
+ ]
290
+ ([File.Directory], rights, rights, [])
291
+ },
292
+ CopySourceBase => {
293
+ let rightsInheriting = [
294
+ File.PathOpen,
295
+ File.FdReaddir,
296
+ File.PathFilestats,
297
+ File.PathReadlink,
298
+ ]
299
+ (
300
+ [File.Directory],
301
+ rightsInheriting,
302
+ [File.FdRead, ...rightsInheriting],
303
+ [],
304
+ )
305
+ },
306
+ CopyTargetBase => {
307
+ let rightsInheriting = [
308
+ File.PathOpen,
309
+ File.PathCreateDirectory,
310
+ File.PathCreateFile,
311
+ File.PathSetSize,
312
+ File.PathSymlink,
313
+ ]
314
+ (
315
+ [File.Directory],
316
+ rightsInheriting,
317
+ [File.FdWrite, ...rightsInheriting],
318
+ [],
319
+ )
320
+ },
321
+ CopySource => ([], [File.FdRead], [], []),
322
+ CopyTarget => ([File.Create, File.Truncate], [File.FdWrite], [], []),
323
+ RenameSource => ([File.Directory], [File.PathRenameSource], [], []),
324
+ RenameTarget =>
325
+ ([File.Directory], [File.PathRenameTarget, File.PathFilestats], [], []),
326
+ ReadDir => ([File.Directory], [File.FdReaddir], [], []),
327
+ MakeDir => ([File.Directory], [File.PathCreateDirectory], [], []),
328
+ MakeSymlink => ([File.Directory], [File.PathSymlink], [], []),
329
+ ReadLink =>
330
+ ([File.Directory], [File.PathFilestats, File.PathReadlink], [], []),
331
+ Stats => ([File.Directory], [File.PathFilestats], [], []),
332
+ Read(sync) =>
333
+ ([], [File.FdRead, File.FdFilestats], [], if (sync) [File.Rsync] else []),
334
+ WriteFile(sync) =>
335
+ (
336
+ [File.Create, File.Truncate],
337
+ [File.FdWrite],
338
+ [],
339
+ if (sync) [File.Sync] else [],
340
+ ),
341
+ AppendFile(sync) =>
342
+ (
343
+ [File.Create],
344
+ [File.FdWrite, File.FdSeek],
345
+ [],
346
+ [File.Append, ...if (sync) [File.Sync] else []],
347
+ ),
348
+ }
349
+ }
350
+
351
+ let open = (parentFd=None, path, openInfo) => {
352
+ let (openFlags, rights, rightsInheriting, flags) = openInfo
353
+ let noPreopens = Result.isErr(File.fdPrestatGet(File.FileDescriptor(3)))
354
+ if (noPreopens) {
355
+ // Exists for UX purposes, to let the user know if they forgot to specify preopens
356
+ Err(NoPreopenedDirectories)
357
+ } else {
358
+ fileResult(match (parentFd) {
359
+ None =>
360
+ File.open(
361
+ Path.toString(path),
362
+ openFlags,
363
+ rights,
364
+ rightsInheriting,
365
+ flags
366
+ ),
367
+ Some(parentFd) =>
368
+ File.pathOpen(
369
+ parentFd,
370
+ [File.SymlinkFollow],
371
+ Path.toString(path),
372
+ openFlags,
373
+ rights,
374
+ rightsInheriting,
375
+ flags
376
+ ),
377
+ })
378
+ }
379
+ }
380
+
381
+ let applyFileOp = (fileDescrResult, fn) => {
382
+ Result.flatMap(fd => {
383
+ let result = fn(fd)
384
+ File.fdClose(fd)
385
+ result
386
+ }, fileDescrResult)
387
+ }
388
+
389
+ // Opens the specified file and applies a "wasi/file" function to the resulting FD, yielding a wrapped FileError
390
+ let fileOp = (
391
+ parentFd=None,
392
+ path,
393
+ openMode,
394
+ fn: File.FileDescriptor => Result<a, Exception>,
395
+ ) => {
396
+ let fdResult = open(parentFd=parentFd, path, openInfo(openMode))
397
+ applyFileOp(fdResult, fd => fileResult(fn(fd)))
398
+ }
399
+
400
+ // Similar to the above function, but for when the passed in function yields a FileError rather than a "wasi/file" error
401
+ let wrappingFileOp = (
402
+ parentFd=None,
403
+ path,
404
+ openMode,
405
+ fn: File.FileDescriptor => Result<a, FileError>,
406
+ ) => {
407
+ let fdResult = open(parentFd=parentFd, path, openInfo(openMode))
408
+ applyFileOp(fdResult, fn)
409
+ }
410
+
411
+ // Convenience function for when 2 files need to be opened in tandem for an operation
412
+ let fileOp2 = (
413
+ firstParentFd=None,
414
+ firstPath,
415
+ firstOpenType,
416
+ secondParentFd=None,
417
+ secondPath,
418
+ secondOpenType,
419
+ fn,
420
+ ) => {
421
+ wrappingFileOp(
422
+ parentFd=firstParentFd,
423
+ firstPath,
424
+ firstOpenType,
425
+ firstFd => {
426
+ fileOp(
427
+ parentFd=secondParentFd,
428
+ secondPath,
429
+ secondOpenType,
430
+ secondFd => {
431
+ fn(firstFd, secondFd)
432
+ }
433
+ )
434
+ }
435
+ )
436
+ }
437
+
438
+ let wrappingFileOp2 = (
439
+ firstParentFd=None,
440
+ firstPath,
441
+ firstOpenType,
442
+ secondParentFd=None,
443
+ secondPath,
444
+ secondOpenType,
445
+ fn,
446
+ ) => {
447
+ wrappingFileOp(
448
+ parentFd=firstParentFd,
449
+ firstPath,
450
+ firstOpenType,
451
+ firstFd => {
452
+ wrappingFileOp(
453
+ parentFd=secondParentFd,
454
+ secondPath,
455
+ secondOpenType,
456
+ secondFd => {
457
+ fn(firstFd, secondFd)
458
+ }
459
+ )
460
+ }
461
+ )
462
+ }
463
+
464
+ let joinPathOnBaseDir = (baseDirPath, path) => {
465
+ match (baseDirPath) {
466
+ None => path,
467
+ Some(base) => {
468
+ // Provide leniency if the base dir path does not contain a trailing slash
469
+ let base = if (!Path.isDirectory(base))
470
+ Path.fromString(Path.toString(base) ++ "/")
471
+ else
472
+ base
473
+
474
+ match (Path.append(base, path)) {
475
+ Ok(appended) => appended,
476
+ Err(Path.AppendAbsolute) => path,
477
+ Err(Path.AppendToFile) =>
478
+ fail "Impossible: getPathWithBaseDir append to directory failed with AppendToFile",
479
+ }
480
+ },
481
+ }
482
+ }
483
+
484
+ // This function is used rather than the above one when the base dir is needed
485
+ let getBaseDirAndPath = (baseDirPath, path) => {
486
+ match (baseDirPath) {
487
+ None =>
488
+ (
489
+ Path.parent(path),
490
+ Path.fromString(Option.unwrapWithDefault(".", Path.basename(path))),
491
+ ),
492
+ Some(base) => (base, path),
493
+ }
494
+ }
495
+
496
+ let wrapFileType = (filetype: File.Filetype) => match (filetype) {
497
+ x when x == File.Directory => Directory,
498
+ File.RegularFile => File,
499
+ File.SymbolicLink => SymbolicLink,
500
+ File.Unknown => Special(Unknown),
501
+ File.BlockDevice => Special(BlockDevice),
502
+ File.CharacterDevice => Special(CharacterDevice),
503
+ File.SocketDatagram => Special(SocketDatagram),
504
+ File.SocketStream => Special(SocketStream),
505
+ _ => fail "Impossible: filetype match",
506
+ }
507
+
508
+ let rec removeRecursive = (parentFd, path) => {
509
+ let statsResult = fileResult(File.pathFilestats(parentFd, [], path))
510
+ // Attempt remove on all directory items but keep track of success
511
+ Result.flatMap((stats: File.Filestats) => {
512
+ if (stats.filetype == File.Directory) {
513
+ wrappingFileOp(
514
+ parentFd=Some(parentFd),
515
+ Path.fromString(path),
516
+ RmrfDir,
517
+ fd => {
518
+ use Result.{ (&&) }
519
+ let removeEntries = entries => {
520
+ Array.reduce(
521
+ (acc, entry: File.DirectoryEntry) => {
522
+ let result = removeRecursive(fd, entry.path)
523
+ acc && result
524
+ },
525
+ Ok(void),
526
+ entries
527
+ )
528
+ }
529
+ Result.flatMap(removeEntries, fileResult(File.fdReaddir(fd)))
530
+ && fileResult(File.pathRemoveDirectory(parentFd, path))
531
+ }
532
+ )
533
+ } else {
534
+ fileResult(File.pathUnlink(parentFd, path))
535
+ }
536
+ }, statsResult)
537
+ }
538
+
539
+ /**
540
+ * Removes a file or directory.
541
+ *
542
+ * @param removeMode: The type of removal to perform; `RemoveFile` by default
543
+ * @param baseDirPath: The path to the directory in which path resolution starts
544
+ * @param path: The path of the file or directory to remove
545
+ * @returns `Ok(void)` if the operation succeeds, `Err(err)` if a file system error is encountered
546
+ *
547
+ * @example Fs.remove(Path.fromString("file.txt")) // removes a file
548
+ * @example Fs.remove(removeMode=Fs.RemoveEmptyDirectory, Path.fromString("dir")) // removes an empty directory
549
+ * @example Fs.remove(removeMode=Fs.RemoveRecursive, Path.fromString("dir")) // removes the directory and its contents
550
+ *
551
+ * @since v0.7.0
552
+ */
553
+ provide let remove = (removeMode=RemoveFile, baseDirPath=None, path) => {
554
+ let (baseDirPath, path) = getBaseDirAndPath(baseDirPath, path)
555
+ match (removeMode) {
556
+ RemoveFile =>
557
+ fileOp(
558
+ baseDirPath,
559
+ Unlink,
560
+ baseDirFd => File.pathUnlink(baseDirFd, Path.toString(path))
561
+ ),
562
+ RemoveEmptyDirectory =>
563
+ fileOp(
564
+ baseDirPath,
565
+ RmDir,
566
+ baseDirFd => File.pathRemoveDirectory(baseDirFd, Path.toString(path))
567
+ ),
568
+ RemoveRecursive =>
569
+ wrappingFileOp(
570
+ baseDirPath,
571
+ RmrfDir,
572
+ baseDirFd => removeRecursive(baseDirFd, Path.toString(path))
573
+ ),
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Reads the contents of a directory.
579
+ *
580
+ * @param baseDirPath: The path to the directory in which resolution should begin
581
+ * @param path: The path to the directory to read
582
+ * @returns `Ok(contents)` containing the directory contents or `Err(err)` if a file system error is encountered
583
+ *
584
+ * @since v0.7.0
585
+ */
586
+ provide let readDir = (baseDirPath=None, path) => {
587
+ let path = joinPathOnBaseDir(baseDirPath, path)
588
+ let result = fileOp(path, ReadDir, File.fdReaddir)
589
+ Result.map(entries => {
590
+ let entries = Array.map(
591
+ (entry: File.DirectoryEntry) =>
592
+ { name: entry.path, fileType: wrapFileType(entry.filetype) },
593
+ entries
594
+ )
595
+ Array.toList(entries)
596
+ }, result)
597
+ }
598
+
599
+ /**
600
+ * Creates a new empty directory at the given path.
601
+ *
602
+ * @param baseDirPath: The path to the directory in which resolution should begin
603
+ * @param path: The path to create the new directory, relative to the base directory
604
+ * @returns `Ok(void)` if the operation succeeds, `Err(err)` if a file system error is encountered
605
+ *
606
+ * @since v0.7.0
607
+ */
608
+ provide let createDir = (baseDirPath=None, path) => {
609
+ let (baseDirPath, path) = getBaseDirAndPath(baseDirPath, path)
610
+ fileOp(
611
+ baseDirPath,
612
+ MakeDir,
613
+ dirFd => File.pathCreateDirectory(dirFd, Path.toString(path))
614
+ )
615
+ }
616
+
617
+ /**
618
+ * Creates a new symbolic link with the given contents.
619
+ *
620
+ * @param linkContents: The path to store into the link
621
+ * @param targetBaseDirPath: The path to the directory in which the target path resolution starts
622
+ * @param targetPath: The path to the target of the link
623
+ * @returns `Ok(void)` if the operation succeeds, `Err(err)` if a file system error or relativization error is encountered
624
+ *
625
+ * @since v0.7.0
626
+ */
627
+ provide let createSymlink = (linkContents, targetBaseDirPath=None, targetPath) => {
628
+ let (targetBaseDirPath, targetPath) = getBaseDirAndPath(
629
+ targetBaseDirPath,
630
+ targetPath
631
+ )
632
+ fileOp(
633
+ targetBaseDirPath,
634
+ MakeSymlink,
635
+ dirFd =>
636
+ File.pathSymlink(
637
+ dirFd,
638
+ Path.toString(linkContents),
639
+ Path.toString(targetPath)
640
+ )
641
+ )
642
+ }
643
+
644
+ /**
645
+ * Queries information about a file.
646
+ *
647
+ * @param followSymlink: Whether to follow symlinks or not; if `true` then the stats of a valid symlink's underlying file will be returned. `true` by default
648
+ * @param baseDirPath: The path to the directory in which the path resolution starts
649
+ * @param path: The path of the file to query
650
+ * @returns `Ok(stats)` containing metadata or `Err(err)` if a file system error is encountered
651
+ *
652
+ * @since v0.7.0
653
+ */
654
+ provide let stats = (followSymlink=true, baseDirPath=None, path) => {
655
+ let (baseDirPath, path) = getBaseDirAndPath(baseDirPath, path)
656
+ let statsResult = fileOp(baseDirPath, Stats, dirFd => {
657
+ File.pathFilestats(
658
+ dirFd,
659
+ if (followSymlink) [File.SymlinkFollow] else [],
660
+ Path.toString(path)
661
+ )
662
+ })
663
+ Result.map(
664
+ (stats: File.Filestats) =>
665
+ {
666
+ size: Int64.toNumber(stats.size),
667
+ fileType: wrapFileType(stats.filetype),
668
+ accessedTimestamp: Int64.toNumber(stats.accessed),
669
+ modifiedTimestamp: Int64.toNumber(stats.modified),
670
+ changedTimestamp: Int64.toNumber(stats.changed),
671
+ },
672
+ statsResult
673
+ )
674
+ }
675
+
676
+ /**
677
+ * Polls whether or not a file or directory exists at the given path.
678
+ *
679
+ * @param baseDirPath: The path to the directory in which the path resolution starts
680
+ * @param path: The path of the file to query
681
+ * @returns `true` if a file or directory exists at the path or `false` otherwise
682
+ *
683
+ * @since v0.7.0
684
+ */
685
+ provide let exists = (baseDirPath=None, path) => {
686
+ stats(baseDirPath=baseDirPath, path, followSymlink=false)
687
+ != Err(NoSuchFileOrDirectory)
688
+ }
689
+
690
+ let readLinkHelper = (dirFd, path, stats: File.Filestats) => {
691
+ // Need to strip trailing null terminator
692
+ let linkContentsResult = File.pathReadlink(
693
+ dirFd,
694
+ Path.toString(path),
695
+ Int64.toNumber(stats.size) + 1
696
+ )
697
+ let fst = ((contents, _)) =>
698
+ String.slice(0, end=String.byteLength(contents) - 1, contents)
699
+ Result.map(fst, linkContentsResult)
700
+ }
701
+
702
+ /**
703
+ * Reads the contents of a symbolic link.
704
+ *
705
+ * @param baseDirPath: The path to the directory to begin path resolution
706
+ * @param path: The path to the link to read
707
+ * @returns `Ok(path)` containing the link contents or `Err(err)` if a file system error is encountered
708
+ *
709
+ * @since v0.7.0
710
+ */
711
+ provide let readLink = (baseDirPath=None, path) => {
712
+ let (baseDirPath, path) = getBaseDirAndPath(baseDirPath, path)
713
+ wrappingFileOp(baseDirPath, ReadLink, dirFd => {
714
+ let statsResult = File.pathFilestats(dirFd, [], Path.toString(path))
715
+ let linkContentsResult = fileResult(
716
+ Result.flatMap(stats => readLinkHelper(dirFd, path, stats), statsResult)
717
+ )
718
+ Result.map(x => Path.fromString(x), linkContentsResult)
719
+ })
720
+ }
721
+
722
+ let copySymlink = (
723
+ sourceParentFd,
724
+ sourcePath,
725
+ targetParentFd,
726
+ targetPath,
727
+ stats,
728
+ ) => {
729
+ let linkContentsResult = readLinkHelper(sourceParentFd, sourcePath, stats)
730
+ fileResult(Result.flatMap(contents => {
731
+ File.pathSymlink(targetParentFd, contents, Path.toString(targetPath))
732
+ }, linkContentsResult))
733
+ }
734
+
735
+ let copyFile = (
736
+ sourceParentFd,
737
+ sourcePath,
738
+ targetParentFd,
739
+ targetPath,
740
+ followSymlink,
741
+ ) => {
742
+ if (!followSymlink) {
743
+ match (File.pathFilestats(sourceParentFd, [], Path.toString(sourcePath))) {
744
+ Ok(stats) when stats.filetype == File.SymbolicLink => {
745
+ return copySymlink(
746
+ sourceParentFd,
747
+ sourcePath,
748
+ targetParentFd,
749
+ targetPath,
750
+ stats
751
+ )
752
+ },
753
+ _ => void,
754
+ }
755
+ }
756
+
757
+ return fileOp2(
758
+ firstParentFd=Some(sourceParentFd),
759
+ sourcePath,
760
+ CopySource,
761
+ secondParentFd=Some(targetParentFd),
762
+ targetPath,
763
+ CopyTarget,
764
+ (srcFd, targetFd) => {
765
+ let _BUFSIZE = 8192
766
+ let rec copyChunk = () => {
767
+ match (File.fdRead(srcFd, _BUFSIZE)) {
768
+ Ok((data, numRead)) => {
769
+ if (numRead == 0) {
770
+ Ok(void)
771
+ } else {
772
+ let bufLen = Bytes.length(data)
773
+ let rec writeRemaining = startPos => {
774
+ if (startPos >= bufLen) {
775
+ Ok(void)
776
+ } else {
777
+ let toWrite = Bytes.slice(startPos, bufLen - startPos, data)
778
+ match (File.fdWrite(targetFd, toWrite)) {
779
+ Ok(numWritten) => writeRemaining(startPos + numWritten),
780
+ Err(err) => Err(err),
781
+ }
782
+ }
783
+ }
784
+ use Result.{ (&&) }
785
+ writeRemaining(0) && copyChunk()
786
+ }
787
+ },
788
+ Err(err) => Err(err),
789
+ }
790
+ }
791
+ copyChunk()
792
+ }
793
+ )
794
+ }
795
+
796
+ let rec copyRecursive = (
797
+ sourceParentFd,
798
+ sourcePath,
799
+ targetParentFd,
800
+ targetPath,
801
+ followSymlink,
802
+ ) => {
803
+ let copyDirectoryContents = () => {
804
+ wrappingFileOp2(
805
+ firstParentFd=Some(sourceParentFd),
806
+ sourcePath,
807
+ CopySourceBase,
808
+ secondParentFd=Some(targetParentFd),
809
+ targetPath,
810
+ CopyTargetBase,
811
+ (sourceDirFd, targetDirFd) => {
812
+ let entries = fileResult(File.fdReaddir(sourceDirFd))
813
+ match (entries) {
814
+ Ok(entries) => {
815
+ Array.reduce(
816
+ (acc, entry: File.DirectoryEntry) => {
817
+ use Result.{ (&&) }
818
+ acc
819
+ && copyRecursive(
820
+ sourceDirFd,
821
+ Path.fromString(entry.path),
822
+ targetDirFd,
823
+ Path.fromString(entry.path),
824
+ false
825
+ )
826
+ },
827
+ Ok(void),
828
+ entries
829
+ )
830
+ },
831
+ Err(err) => Err(err),
832
+ }
833
+ }
834
+ )
835
+ }
836
+
837
+ match (
838
+ fileResult(
839
+ File.pathFilestats(
840
+ sourceParentFd,
841
+ if (followSymlink) [File.SymlinkFollow] else [],
842
+ Path.toString(sourcePath)
843
+ )
844
+ )
845
+ ) {
846
+ Ok(stats) => {
847
+ match (stats.filetype) {
848
+ x when x == File.Directory => {
849
+ use Result.{ (&&) }
850
+ fileResult(
851
+ File.pathCreateDirectory(targetParentFd, Path.toString(targetPath))
852
+ )
853
+ && copyDirectoryContents()
854
+ },
855
+ File.SymbolicLink =>
856
+ copySymlink(
857
+ sourceParentFd,
858
+ sourcePath,
859
+ targetParentFd,
860
+ targetPath,
861
+ stats
862
+ ),
863
+ _ =>
864
+ copyFile(
865
+ sourceParentFd,
866
+ sourcePath,
867
+ targetParentFd,
868
+ targetPath,
869
+ false
870
+ ),
871
+ }
872
+ },
873
+ Err(err) => Err(err),
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Copies a file or directory.
879
+ *
880
+ * @param copyMode: The type of copy to perform; `CopyFile` by default
881
+ * @param followSymlink: Whether to follow symlinks or not; if `true` then the stats of a valid symlink's underlying file will be returned. `true` by default
882
+ * @param sourceBaseDirPath: The path to the directory in which the source path resolution starts
883
+ * @param sourcePath: The path of the file or directory to copy
884
+ * @param targetBaseDirPath: The path to the directory in which the target path resolution starts
885
+ * @param targetPath: The path to copy the file or directory to
886
+ * @returns `Ok(void)` if the operation succeeds, `Err(err)` if a file system error is encountered
887
+ */
888
+ provide let copy = (
889
+ copyMode=CopyFile,
890
+ followSymlink=true,
891
+ sourceBaseDirPath=None,
892
+ sourcePath,
893
+ targetBaseDirPath=None,
894
+ targetPath,
895
+ ) => {
896
+ let (sourceBaseDirPath, sourcePath) = getBaseDirAndPath(
897
+ sourceBaseDirPath,
898
+ sourcePath
899
+ )
900
+ let (targetBaseDirPath, targetPath) = getBaseDirAndPath(
901
+ targetBaseDirPath,
902
+ targetPath
903
+ )
904
+ wrappingFileOp2(
905
+ sourceBaseDirPath,
906
+ CopySourceBase,
907
+ targetBaseDirPath,
908
+ CopyTargetBase,
909
+ (sourceDirFd, targetDirFd) => {
910
+ let fn = match (copyMode) {
911
+ CopyFile => copyFile,
912
+ CopyRecursive => copyRecursive,
913
+ }
914
+ fn(sourceDirFd, sourcePath, targetDirFd, targetPath, followSymlink)
915
+ }
916
+ )
917
+ }
918
+
919
+ /**
920
+ * Renames a file or directory.
921
+ *
922
+ * @param sourceBaseDirPath: The path to the directory in which the source path resolution starts
923
+ * @param sourcePath: The path of the file to rename
924
+ * @param targetBaseDirPath: The path to the directory in which the target path resolution starts
925
+ * @param targetPath: The new path of the file
926
+ * @returns `Ok(void)` if the operation succeeds, `Err(err)` if a file system error is encountered
927
+ *
928
+ * @since v0.7.0
929
+ */
930
+ provide let rename = (
931
+ sourceBaseDirPath=None,
932
+ sourcePath,
933
+ targetBaseDirPath=None,
934
+ targetPath,
935
+ ) => {
936
+ let (sourceBaseDirPath, sourcePath) = getBaseDirAndPath(
937
+ sourceBaseDirPath,
938
+ sourcePath
939
+ )
940
+ let (targetBaseDirPath, targetPath) = getBaseDirAndPath(
941
+ targetBaseDirPath,
942
+ targetPath
943
+ )
944
+ wrappingFileOp2(
945
+ sourceBaseDirPath,
946
+ RenameSource,
947
+ targetBaseDirPath,
948
+ RenameTarget,
949
+ (srcDirFd, targetDirFd) => {
950
+ fileResult(
951
+ File.pathRename(
952
+ srcDirFd,
953
+ Path.toString(sourcePath),
954
+ targetDirFd,
955
+ Path.toString(targetPath)
956
+ )
957
+ )
958
+ }
959
+ )
960
+ }
961
+
962
+ /**
963
+ * Functionality for reading and writing `Bytes` to files.
964
+ *
965
+ * @since v0.7.0
966
+ */
967
+ provide module Binary {
968
+ /**
969
+ * Read the contents of a file as `Bytes`.
970
+ *
971
+ * @param sync: Whether to synchronously read; `true` by default
972
+ * @param baseDirPath: The path to the directory to begin path resolution
973
+ * @param path: The file path to read from
974
+ * @returns `Ok(contents)` containing the bytes read if successful or `Err(err)` if a file system error is encountered
975
+ *
976
+ * @since v0.7.0
977
+ */
978
+ provide let readFile = (sync=true, baseDirPath=None, path) => {
979
+ let path = joinPathOnBaseDir(baseDirPath, path)
980
+ fileOp(path, Read(sync), fd => {
981
+ match (File.fdFilestats(fd)) {
982
+ Ok(stats) => {
983
+ let fst = ((contents, _)) => contents
984
+ Result.map(fst, File.fdRead(fd, Int64.toNumber(stats.size)))
985
+ },
986
+ Err(err) => Err(err),
987
+ }
988
+ })
989
+ }
990
+
991
+ /**
992
+ * Write `Bytes` to a file.
993
+ *
994
+ * @param writeMode: The type of write operation to perform; `Truncate` by default
995
+ * @param sync: Whether to synchronously write; `true` by default
996
+ * @param baseDirPath: The path to the directory to begin path resolution
997
+ * @param path: The file path to write to
998
+ * @param data: The bytes to write to the file
999
+ * @returns `Ok(void)` if the operation is successful or `Err(err)` if a file system error is encountered
1000
+ *
1001
+ * @since v0.7.0
1002
+ */
1003
+ provide let writeFile = (
1004
+ writeMode=Truncate,
1005
+ sync=true,
1006
+ baseDirPath=None,
1007
+ path,
1008
+ data,
1009
+ ) => {
1010
+ let path = joinPathOnBaseDir(baseDirPath, path)
1011
+ let openMode = match (writeMode) {
1012
+ Truncate => WriteFile(sync),
1013
+ Append => AppendFile(sync),
1014
+ }
1015
+ let len = Bytes.length(data)
1016
+ Result.map(ignore, fileOp(path, openMode, fd => {
1017
+ let rec writeRemaining = startPos => {
1018
+ if (startPos >= len) {
1019
+ Ok(void)
1020
+ } else {
1021
+ let toWrite = Bytes.slice(startPos, len - startPos, data)
1022
+ match (File.fdWrite(fd, toWrite)) {
1023
+ Ok(numWritten) => writeRemaining(startPos + numWritten),
1024
+ Err(err) => Err(err),
1025
+ }
1026
+ }
1027
+ }
1028
+ writeRemaining(0)
1029
+ }))
1030
+ }
1031
+ }
1032
+
1033
+ /**
1034
+ * Functionality for reading and writing `String`s to files.
1035
+ *
1036
+ * @since v0.7.0
1037
+ */
1038
+ provide module Utf8 {
1039
+ /**
1040
+ * Read the contents of a file as a `String`.
1041
+ *
1042
+ * @param sync: Whether to synchronously read; `true` by default
1043
+ * @param baseDirPath: The path to the directory to begin path resolution
1044
+ * @param path: The file path to read from
1045
+ * @returns `Ok(contents)` containing the string read if successful or `Err(err)` if a file system error is encountered
1046
+ *
1047
+ * @since v0.7.0
1048
+ */
1049
+ provide let readFile = (sync=true, baseDirPath=None, path) => {
1050
+ let bytes = Binary.readFile(sync=sync, baseDirPath=baseDirPath, path)
1051
+ Result.map(bytes => String.decode(bytes, String.UTF8), bytes)
1052
+ }
1053
+
1054
+ /**
1055
+ * Write a `String` to a file.
1056
+ *
1057
+ * @param writeMode: The type of write operation to perform; `Truncate` by default
1058
+ * @param sync: Whether to synchronously write; `true` by default
1059
+ * @param baseDirPath: The path to the directory to begin path resolution
1060
+ * @param path: The file path to write to
1061
+ * @param data: The string to write to the file
1062
+ * @returns `Ok(void)` if the operation is successful or `Err(err)` if a file system error is encountered
1063
+ *
1064
+ * @since v0.7.0
1065
+ */
1066
+ provide let writeFile = (
1067
+ writeMode=Truncate,
1068
+ sync=true,
1069
+ baseDirPath=None,
1070
+ path,
1071
+ data,
1072
+ ) => {
1073
+ let bytes = String.encode(data, String.UTF8)
1074
+ Binary.writeFile(
1075
+ writeMode=writeMode,
1076
+ sync=sync,
1077
+ baseDirPath=baseDirPath,
1078
+ path,
1079
+ bytes
1080
+ )
1081
+ }
1082
+ }