1 import Base 2 import bend-kit-bytes@0.3.2.0/bytes.bend as StorageBytes 3 import 0x6c54b2522c2258baf6f58d2b970c3902/files.bend as PackedFiles 4 import ./FsPolicy.bend as Policy 5 6 # Audited host effects (spec §3.3). Thin syscall wrappers with .c + .js 7 # twins and matching intended semantics. No laws — validated empirically. 8 9 # Read at most 1 MiB as packed words. Short reads are valid progress; zero is EOF. 10 def read_words( 11 file: File, 12 max: U32 13 ) -> 14 IO(File & Result<&1, &1, U32 & String, U32 & Array>): 15 PackedFiles.read.words(file, max) 16 17 # Write all bytes from a packed word array, retaining the file handle on errors. 18 def write_words( 19 file: File, 20 len: U32, 21 words: Array 22 ) -> 23 IO(File & Result<&1, &1, U32 & String, Unit>): 24 PackedFiles.write.words(file, len, words) 25 26 def read_bytes.wrap( 27 result: Result<&1, &1, U32 & String, U32 & Array> 28 ) -> 29 Result<&1, &1, U32 & String, StorageBytes.Bytes>: 30 match result: 31 case Fail{err}: 32 Fail{err} 33 case Done{(len, words)}: 34 Done{StorageBytes.Bytes{len, words}} 35 36 def read_bytes.pair( 37 pair: File & Result<&1, &1, U32 & String, U32 & Array> 38 ) -> 39 File & Result<&1, &1, U32 & String, StorageBytes.Bytes>: 40 match pair: 41 case (handle, result): 42 (handle, read_bytes.wrap(result)) 43 44 # Read one bounded packed chunk; nonempty short reads are not EOF. 45 def read_bytes( 46 file: File, 47 max: U32 48 ) -> 49 IO(File & Result<&1, &1, U32 & String, StorageBytes.Bytes>): 50 do IO>: 51 pair : File & Result<&1, &1, U32 & String, U32 & Array> <- read_words(file, max) 52 return read_bytes.pair(pair) 53 54 # Write the significant bytes of a packed buffer through the pinned host effect. 55 def write_bytes( 56 file: File, 57 bytes: StorageBytes.Bytes 58 ) -> 59 IO(File & Result<&1, &1, U32 & String, Unit>): 60 StorageBytes.Bytes{len, buf} = bytes 61 write_words(file, len, buf) 62 63 # Sync file contents to stable storage. 64 def fsync(path: String) -> IO(Result<&1, &1, U32 & String, Unit>): 65 import "./effs/fsync.c" 66 import "./effs/fsync.js" 67 68 # Handle truncate in the filesystem effects. 69 def truncate(path: String, offset: Nat) -> IO(Result<&1, &1, U32 & String, Unit>): 70 import "./effs/truncate.c" 71 import "./effs/truncate.js" 72 73 # Atomically rename a filesystem path. 74 def rename(old: String, new: String) -> IO(Result<&1, &1, U32 & String, Unit>): 75 PackedFiles.rename(old, new) 76 77 # Remove a file or empty directory. 78 def remove(path: String) -> IO(Result<&1, &1, U32 & String, Unit>): 79 PackedFiles.remove(path) 80 81 # Return the number of entries available through the indexed directory API. 82 def read_dir_count(path: String) -> IO(Result<&1, &1, U32 & String, Nat>): 83 import "./effs/read_dir.c" 84 import "./effs/read_dir.js" 85 86 # Fetch one directory entry by index. 87 def read_dir_at(path: String, idx: Nat) -> IO(Result<&1, &1, U32 & String, String>): 88 import "./effs/read_dir.c" 89 import "./effs/read_dir.js" 90 91 # Create one directory. 92 def make_dir(path: String) -> IO(Result<&1, &1, U32 & String, Unit>): 93 import "./effs/make_dir.c" 94 import "./effs/make_dir.js" 95 96 # Set POSIX permission bits. 97 def chmod(path: String, bits: U32) -> IO(Result<&1, &1, U32 & String, Unit>): 98 import "./effs/chmod.c" 99 import "./effs/chmod.js" 100 101 # Check whether a filesystem path exists. 102 def exists.result(result: Result<&1, &1, U32 & String, PackedFiles.Info>) -> Result<&1, &1, U32 & String, Bool>: 103 match result: 104 case Fail{(+code, message)}: 105 Policy.missing_path_result(U32.is_eq(code, 2), code, message) 106 case Done{_}: 107 Done{True{}} 108 109 # Returns whether a path exists and preserves other stat errors. 110 def exists(path: String) -> IO(Result<&1, &1, U32 & String, Bool>): 111 do IO>: 112 result : Result<&1, &1, U32 & String, PackedFiles.Info> <- PackedFiles.stat(path) 113 return exists.result(result) 114 115 # Read-only byte size for Bend-side accounting (no shell wc needed). 116 def file_size(path: String) -> IO(Result<&1, &1, U32 & String, Nat>): 117 import "./effs/file_size.c" 118 import "./effs/file_size.js" 119 120 # Pure accumulator step (leaf: matches structurally, never recurses). 121 # Past-end fetches answer "" (SNil{}), which is skipped. 122 def grab_name(nm: String, acc: List) -> List: 123 match nm: 124 case SNil{}: 125 acc 126 case SCon{_, _}: 127 Con{nm, acc} 128 129 # Fuel-bounded sweep 0..count-1. Startup-only use (spec §10 below): the 130 # directory is stable during recovery, so count-then-fetch is exact. 131 def read_go(fuel: Nat, +path: String, +idx: Nat, acc: List) -> IO(List): 132 match fuel: 133 case 0n: 134 IO.pure(List, List.reverse(&1, String, acc)) 135 case 1n+f: 136 do IO>: 137 nm : String <- IO.try(String, read_dir_at(path, idx)) 138 read_go(f, path, Nat.add(idx, 1n), grab_name(nm, acc)) 139 140 # List directory names using the bounded count-then-fetch host API. 141 def read_dir(+path: String) -> IO(Result<&1, &1, U32 & String, List>): 142 do IO>>: 143 n : Nat <- IO.try(Nat, read_dir_count(path)) 144 names : List <- read_go(Nat.add(n, 1n), path, 0n, Nil{}) 145 return Done{names}