9e767e8541e080d26a22a10d16bd54b1231f7e4a9b1cde075170224949320347.tri (6624B)
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 bundleHexByte? 48 = true 23 bundleHexByte? 49 = true 24 bundleHexByte? 50 = true 25 bundleHexByte? 51 = true 26 bundleHexByte? 52 = true 27 bundleHexByte? 53 = true 28 bundleHexByte? 54 = true 29 bundleHexByte? 55 = true 30 bundleHexByte? 56 = true 31 bundleHexByte? 57 = true 32 bundleHexByte? 97 = true 33 bundleHexByte? 98 = true 34 bundleHexByte? 99 = true 35 bundleHexByte? 100 = true 36 bundleHexByte? 101 = true 37 bundleHexByte? 102 = true 38 bundleHexByte? _ = false 39 40 -- The counter is internal and canonical. At zero inspect only the list shape: 41 -- never force the 65th byte or its suffix, even for arbitrary malformed trees. 42 bundleHashBytes [] 0 = true 43 bundleHashBytes _ 0 = false 44 bundleHashBytes [byte | rest] remaining = (let 45 checked ^true = bundleHashBytes rest (Nat.subtract remaining 1) 46 checked _ = false 47 in checked (bundleHexByte? byte)) 48 bundleHashBytes _ _ = false 49 50 bundleHashValid? hash = bundleHashBytes hash 64 51 bundleHashC = guardC "expected 64 lowercase hexadecimal hash characters" bundleHashValid? 52 53 -- Directory creation already tolerates existing directories in the Haskell 54 -- driver. Propagate real failures and stop; void would discard those failures. 55 ensureDir path = mapErrIO "createDirectory: " (createDirectory path) 56 ensureStore root = (do onOk_ 57 ensureDir root 58 ensureDir (pathJoin root "tmp") 59 ensureDir (pathJoin root "objects") 60 ensureDir (pathJoin root "aliases") 61 ensureDir (pathJoin (pathJoin root "aliases") "names") 62 ensureDir (pathJoin (pathJoin root "aliases") "packages") 63 ensureDir (pathJoin root "manifests") 64 pure (ok t t)) 65 66 -- Hash opaque bytes ourselves: no public unchecked hash-taking writer. Validate 67 -- even the digest reply before constructing filesystem actions. Atomic writes 68 -- use host-managed unique staging in the destination directory, not hash.tmp. 69 -- Existing-object replacement remains supported. A post-publication sync error 70 -- can leave the new object visible; this is not rollback or bundle validation. 71 putBundle root bytes = (let 72 rejected diagnostic = pure (err (append "sha256Hex: " diagnostic) t) 73 checked ((t t), (hash, _)) = (do onOk_ 74 ensureDir (objectDir root (hashShard hash)) 75 mapErrIO "atomicWriteBytes: " (atomicWriteBytes (bundleObjectPath root hash) bytes) 76 pure (ok hash t)) 77 checked (t, (diagnostic, _)) = rejected diagnostic 78 checked _ = rejected "Invalid hash contract result" 79 in do onOk_ 80 hash <- mapErrIO "sha256Hex: " (sha256Hex bytes) 81 checked (checkContract bundleHashC hash)) 82 83 -- Translate contract failures before IO, preserving HTTP status/message roles. 84 getBundleByHash root hash = (let 85 readReply ((t t), (bytes, _)) = pure (ok bytes t) 86 readReply (t, (message, _)) = pure (err 404 message) 87 readReply _ = pure (err 404 "Invalid read result") 88 checked ((t t), (value, _)) = (do bind 89 reply <- readFile (bundleObjectPath root value) 90 readReply reply) 91 checked (t, (diagnostic, _)) = pure (err 400 diagnostic) 92 checked _ = pure (err 400 "Invalid hash contract result") 93 in checked (checkContract bundleHashC hash)) 94 95 -- A proper list prefix; return its exact suffix, without normalizing the tail. 96 stripPrefix [] input = just input 97 stripPrefix [wanted | prefix] [byte | rest] = (let 98 matched ^wanted = stripPrefix prefix rest 99 matched _ = nothing 100 in matched byte) 101 stripPrefix _ _ = nothing 102 103 bundleHashPrefix = "/_arboricx/bundle/hash/" 104 bundlePath = "/_arboricx/bundle" 105 healthPath = "/_arboricx/health" 106 bundleContentType = "application/vnd.arboricx.bundle" 107 108 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>" 109 110 bundleResponse bytes = response 200 bundleContentType bytes 111 112 serveBundleHash root hash = (let 113 respond ((t t), (bytes, _)) = pure (bundleResponse bytes) 114 respond (t, (status, message)) = pure (errorResponse status message) 115 respond _ = pure (errorResponse 500 "Invalid bundle result") 116 in do bind 117 reply <- getBundleByHash root hash 118 respond reply) 119 120 healthRoute "GET" ^healthPath = pure (okResponse "OK\n") 121 healthRoute _ _ = pure notFoundResponse 122 123 putBundleRoute root "POST" ^bundlePath body = (let 124 respond ((t t), (hash, _)) = pure (createdResponse hash) 125 respond (t, (message, _)) = pure (badRequestResponse (append "Upload failed: " message)) 126 respond _ = pure (errorResponse 500 "Invalid upload result") 127 in do bind 128 reply <- putBundle root body 129 respond reply) 130 putBundleRoute _ _ _ _ = pure notFoundResponse 131 132 getBundleRoute root "GET" target = (let 133 selected (t hash) = serveBundleHash root hash 134 selected _ = pure notFoundResponse 135 in selected (stripPrefix bundleHashPrefix target)) 136 getBundleRoute _ _ _ = pure notFoundResponse 137 138 arboricxRouter _ "GET" "/" _ _ = pure (htmlResponse htmlLandingPage) 139 arboricxRouter _ "GET" ^healthPath _ _ = healthRoute "GET" healthPath 140 arboricxRouter root "GET" target _ _ = getBundleRoute root "GET" target 141 arboricxRouter root "POST" target _ body = putBundleRoute root "POST" target body 142 arboricxRouter _ _ _ _ _ = pure notFoundResponse 143 144 arboricxHandler root client peer = httpHandlerIO (arboricxRouter root) client peer 145 146 -- Close a newly acquired socket on ordinary bind/listen failures. As elsewhere, 147 -- finally does not catch terminal abortIO or host exceptions; successful serving 148 -- delegates scheduling/accept policy to serveForever, without a shutdown hook. 149 arboricxServer root addr port = (let 150 failed message = pure (err message t) 151 opened server = (let 152 rejected message = finally (failed message) (closeSocket_ server) 153 ready _ = serveForever server (arboricxHandler root) 154 in onResult_ (do onOk_ 155 bindSocket server addr port 156 listen server 128) 157 rejected ready) 158 in onResult_ socket failed opened)