7bce76817a1c9a6df5253b7f983a395d833e3622533a2d209c12a5c0f111aa5f.tri (9031B)
1 !import prelude * 2 !import io * 3 !import http * 4 !import socket * 5 !import native_math as Nat 6 !export * 7 8 -- Store root is trusted configuration. These pure path helpers are not guards; 9 -- every read/write entrypoint must validate a hash before using it as a path. 10 pathJoin parent child = append parent (append "/" child) 11 objectDir root shard = pathJoin (pathJoin root "objects") shard 12 13 hashShard [a | [b | [c | _]]] = [a b c] 14 hashShard [a | [b | _]] = [a b] 15 hashShard [a | _] = [a] 16 hashShard _ = [] 17 18 bundleObjectPath root hash = pathJoin (objectDir root (hashShard hash)) (append hash ".arboricx") 19 20 -- Exact canonical lowercase SHA-256 spelling. Literal patterns, unlike numeric 21 -- range predicates, reject malformed numeral trees without normalizing them. 22 -- Disjoint compact-bit ranges: 48..55, 56..57, 97, 98..99, 23 -- 100..101, 102. Every captured bit is checked as an exact Boolean. 24 bundleBit? t = true 25 bundleBit? (t t) = true 26 bundleBit? _ = false 27 bundleHexByte? (a, (b, (c, 6))) = and? (bundleBit? a) (and? (bundleBit? b) (bundleBit? c)) 28 bundleHexByte? (a, 28) = bundleBit? a 29 bundleHexByte? 97 = true 30 bundleHexByte? (a, 49) = bundleBit? a 31 bundleHexByte? (a, 50) = bundleBit? a 32 bundleHexByte? 102 = true 33 bundleHexByte? _ = false 34 35 -- The counter is internal and canonical. At zero inspect only the list shape: 36 -- never force the 65th byte or its suffix, even for arbitrary malformed trees. 37 bundleHashBytes [] 0 = true 38 bundleHashBytes _ 0 = false 39 bundleHashBytes [byte | rest] remaining = case (bundleHexByte? byte) of 40 ^true = bundleHashBytes rest (Nat.subtract remaining 1) 41 _ = false 42 43 bundleHashBytes _ _ = false 44 45 -- The fixed SHA-256 boundary uses structural fuel instead of subtracting a 46 -- compact-natural counter per character. Retain bundleHashBytes for callers 47 -- that explicitly supply a length. Both stop before inspecting a 65th element. 48 bundleHashFuel8 tail = t (t (t (t (t (t (t (t tail))))))) 49 bundleHashFuel64 = bundleHashFuel8 (bundleHashFuel8 (bundleHashFuel8 (bundleHashFuel8 50 (bundleHashFuel8 (bundleHashFuel8 (bundleHashFuel8 (bundleHashFuel8 t))))))) 51 bundleHashScan [] t = true 52 bundleHashScan _ t = false 53 bundleHashScan [byte | rest] (t fuel) = case bundleHexByte? byte of 54 ^true = bundleHashScan rest fuel 55 _ = false 56 bundleHashScan _ _ = false 57 bundleHashValid? hash = bundleHashScan hash bundleHashFuel64 58 bundleHashC = guardC "expected 64 lowercase hexadecimal hash characters" bundleHashValid? 59 60 -- Directory creation already tolerates existing directories in the Haskell 61 -- driver. Propagate real failures and stop; void would discard those failures. 62 ensureDir path = mapErrIO "createDirectory: " (createDirectory path) 63 ensureStore root = (do onOk_ 64 ensureDir root 65 ensureDir (pathJoin root "tmp") 66 ensureDir (pathJoin root "objects") 67 ensureDir (pathJoin root "aliases") 68 ensureDir (pathJoin (pathJoin root "aliases") "names") 69 ensureDir (pathJoin (pathJoin root "aliases") "packages") 70 ensureDir (pathJoin root "manifests") 71 pure (ok t t)) 72 73 -- Hash opaque bytes ourselves: no public unchecked hash-taking writer. Validate 74 -- even the digest reply before constructing filesystem actions. Atomic writes 75 -- use host-managed unique staging in the destination directory, not hash.tmp. 76 -- Existing-object replacement remains supported. A post-publication sync error 77 -- can leave the new object visible; this is not rollback or bundle validation. 78 putBundle root bytes = (do onOk_ 79 hash <- mapErrIO "sha256Hex: " (sha256Hex bytes) 80 case (checkContract bundleHashC hash) of 81 ((t t), (value, _)) = (do onOk_ 82 ensureDir (objectDir root (hashShard value)) 83 mapErrIO "atomicWriteBytes: " (atomicWriteBytes (bundleObjectPath root value) bytes) 84 pure (ok value t)) 85 (t, (diagnostic, _)) = pure (err (append "sha256Hex: " diagnostic) t) 86 _ = pure (err "sha256Hex: Invalid hash contract result" t)) 87 88 -- Translate contract failures before IO, preserving HTTP status/message roles. 89 getBundleByHash root hash = case (checkContract bundleHashC hash) of 90 ((t t), (value, _)) = (do bind 91 reply <- readFile (bundleObjectPath root value) 92 case reply of 93 ((t t), (bytes, _)) = pure (ok bytes t) 94 (t, (message, _)) = pure (err 404 message) 95 _ = pure (err 404 "Invalid read result")) 96 (t, (diagnostic, _)) = pure (err 400 diagnostic) 97 _ = pure (err 400 "Invalid hash contract result") 98 99 -- A proper list prefix; return its exact suffix, without normalizing the tail. 100 stripPrefix [] input = just input 101 stripPrefix [wanted | prefix] [byte | rest] = case byte of 102 ^wanted = stripPrefix prefix rest 103 _ = nothing 104 105 stripPrefix _ _ = nothing 106 107 bundleHashPrefix = "/_arboricx/bundle/hash/" 108 bundlePath = "/_arboricx/bundle" 109 healthPath = "/_arboricx/health" 110 bundleContentType = "application/vnd.arboricx.bundle" 111 112 htmlLandingPage = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'><title>Arboricx Server</title></head><body><h1>Arboricx Server</h1><p>Bundle registry</p><p><a href='https://git.eversole.co/James/tricu'>Made with Love (and trees, lots of trees)</a></p></body></html>" 113 114 bundleResponse bytes = response 200 bundleContentType bytes 115 116 serveBundleHash root hash = (do bind 117 reply <- getBundleByHash root hash 118 case reply of 119 ((t t), (bytes, _)) = pure (bundleResponse bytes) 120 (t, (status, message)) = pure (errorResponse status message) 121 _ = pure (errorResponse 500 "Invalid bundle result")) 122 123 healthRoute "GET" ^healthPath = pure (okResponse "OK\n") 124 healthRoute _ _ = pure notFoundResponse 125 126 putBundleRoute root "POST" ^bundlePath body = (do bind 127 reply <- putBundle root body 128 case reply of 129 ((t t), (hash, _)) = pure (createdResponse hash) 130 (t, (message, _)) = pure (badRequestResponse (append "Upload failed: " message)) 131 _ = pure (errorResponse 500 "Invalid upload result")) 132 133 putBundleRoute _ _ _ _ = pure notFoundResponse 134 135 getBundleRoute root "GET" target = case (stripPrefix bundleHashPrefix target) of 136 (t hash) = serveBundleHash root hash 137 _ = pure notFoundResponse 138 139 getBundleRoute _ _ _ = pure notFoundResponse 140 141 -- Read-only exact-version index. Administrative publication happens locally; 142 -- this route never resolves dependencies or executes fetched code. HTTP input 143 -- bytes are canonical naturals. Scan at most 128 bytes per path component. 144 packagePrefix = "/_arboricx/package/" 145 packageAlphaNum? byte = (or? (Nat.inRange byte 48 57) 146 (or? (Nat.inRange byte 65 90) (Nat.inRange byte 97 122))) 147 packageByte? byte = (or? (packageAlphaNum? byte) 148 (or? (equal? byte 46) (or? (equal? byte 95) (equal? byte 45)))) 149 packageVersion [] reversed _ = just (reverse reversed) 150 packageVersion _ _ 0 = nothing 151 packageVersion [byte | rest] reversed remaining = case (packageByte? byte) of 152 ^true = packageVersion rest (pair byte reversed) (Nat.subtract remaining 1) 153 _ = nothing 154 155 packageVersion _ _ _ = nothing 156 packageName [47 | [first | rest]] reversed _ = case (packageAlphaNum? first) of 157 ^true = case (packageVersion rest [first] 127) of 158 (t version) = just (pair (reverse reversed) version) 159 _ = nothing 160 _ = nothing 161 162 packageName _ _ 0 = nothing 163 packageName [byte | rest] reversed remaining = case (packageByte? byte) of 164 ^true = packageName rest (pair byte reversed) (Nat.subtract remaining 1) 165 _ = nothing 166 167 packageName _ _ _ = nothing 168 packageTarget [first | rest] = case (packageAlphaNum? first) of 169 ^true = packageName rest [first] 127 170 _ = nothing 171 172 packageTarget _ = nothing 173 174 servePackage root target = case (packageTarget target) of 175 (t (name, version)) = (do bind 176 reply <- readFile (pathJoin (pathJoin (pathJoin root "packages") name) version) 177 case reply of 178 ((t t), (bytes, _)) = pure (response 200 "application/vnd.arboricx.package-release" bytes) 179 _ = pure (errorResponse 404 "Package release not found")) 180 _ = pure (badRequestResponse "Invalid package name/version") 181 182 getRegistryRoute root target = case (stripPrefix packagePrefix target) of 183 (t suffix) = servePackage root suffix 184 _ = getBundleRoute root "GET" target 185 186 arboricxRouter _ "GET" "/" _ _ = pure (htmlResponse htmlLandingPage) 187 arboricxRouter _ "GET" ^healthPath _ _ = healthRoute "GET" healthPath 188 arboricxRouter root "GET" target _ _ = getRegistryRoute root target 189 arboricxRouter root "POST" target _ body = putBundleRoute root "POST" target body 190 arboricxRouter _ _ _ _ _ = pure notFoundResponse 191 192 arboricxHandler root client peer = httpHandlerIO (arboricxRouter root) client peer 193 194 -- Close a newly acquired socket on ordinary bind/listen failures. As elsewhere, 195 -- finally does not catch terminal abortIO or host exceptions; successful serving 196 -- delegates scheduling/accept policy to serveForever, without a shutdown hook. 197 arboricxServer root addr port = (let 198 failed message = pure (err message t) 199 opened server = (let 200 rejected message = finally (failed message) (closeSocket_ server) 201 ready _ = serveForever server (arboricxHandler root) 202 in onResult_ (do onOk_ 203 bindSocket server addr port 204 listen server 128) 205 rejected ready) 206 in onResult_ socket failed opened)