75f284aec3ff84d0a392fdf8e144512304f3adfda91c96b27748c6160a1ff7b5.tri (6377B)
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 = case (bundleHexByte? byte) of 45 ^true = bundleHashBytes rest (Nat.subtract remaining 1) 46 _ = false 47 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 = (do onOk_ 72 hash <- mapErrIO "sha256Hex: " (sha256Hex bytes) 73 case (checkContract bundleHashC hash) of 74 ((t t), (value, _)) = (do onOk_ 75 ensureDir (objectDir root (hashShard value)) 76 mapErrIO "atomicWriteBytes: " (atomicWriteBytes (bundleObjectPath root value) bytes) 77 pure (ok value t)) 78 (t, (diagnostic, _)) = pure (err (append "sha256Hex: " diagnostic) t) 79 _ = pure (err "sha256Hex: Invalid hash contract result" t)) 80 81 -- Translate contract failures before IO, preserving HTTP status/message roles. 82 getBundleByHash root hash = case (checkContract bundleHashC hash) of 83 ((t t), (value, _)) = (do bind 84 reply <- readFile (bundleObjectPath root value) 85 case reply of 86 ((t t), (bytes, _)) = pure (ok bytes t) 87 (t, (message, _)) = pure (err 404 message) 88 _ = pure (err 404 "Invalid read result")) 89 (t, (diagnostic, _)) = pure (err 400 diagnostic) 90 _ = pure (err 400 "Invalid hash contract result") 91 92 -- A proper list prefix; return its exact suffix, without normalizing the tail. 93 stripPrefix [] input = just input 94 stripPrefix [wanted | prefix] [byte | rest] = case byte of 95 ^wanted = stripPrefix prefix rest 96 _ = nothing 97 98 stripPrefix _ _ = nothing 99 100 bundleHashPrefix = "/_arboricx/bundle/hash/" 101 bundlePath = "/_arboricx/bundle" 102 healthPath = "/_arboricx/health" 103 bundleContentType = "application/vnd.arboricx.bundle" 104 105 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>" 106 107 bundleResponse bytes = response 200 bundleContentType bytes 108 109 serveBundleHash root hash = (do bind 110 reply <- getBundleByHash root hash 111 case reply of 112 ((t t), (bytes, _)) = pure (bundleResponse bytes) 113 (t, (status, message)) = pure (errorResponse status message) 114 _ = pure (errorResponse 500 "Invalid bundle result")) 115 116 healthRoute "GET" ^healthPath = pure (okResponse "OK\n") 117 healthRoute _ _ = pure notFoundResponse 118 119 putBundleRoute root "POST" ^bundlePath body = (do bind 120 reply <- putBundle root body 121 case reply of 122 ((t t), (hash, _)) = pure (createdResponse hash) 123 (t, (message, _)) = pure (badRequestResponse (append "Upload failed: " message)) 124 _ = pure (errorResponse 500 "Invalid upload result")) 125 126 putBundleRoute _ _ _ _ = pure notFoundResponse 127 128 getBundleRoute root "GET" target = case (stripPrefix bundleHashPrefix target) of 129 (t hash) = serveBundleHash root hash 130 _ = pure notFoundResponse 131 132 getBundleRoute _ _ _ = pure notFoundResponse 133 134 arboricxRouter _ "GET" "/" _ _ = pure (htmlResponse htmlLandingPage) 135 arboricxRouter _ "GET" ^healthPath _ _ = healthRoute "GET" healthPath 136 arboricxRouter root "GET" target _ _ = getBundleRoute root "GET" target 137 arboricxRouter root "POST" target _ body = putBundleRoute root "POST" target body 138 arboricxRouter _ _ _ _ _ = pure notFoundResponse 139 140 arboricxHandler root client peer = httpHandlerIO (arboricxRouter root) client peer 141 142 -- Close a newly acquired socket on ordinary bind/listen failures. As elsewhere, 143 -- finally does not catch terminal abortIO or host exceptions; successful serving 144 -- delegates scheduling/accept policy to serveForever, without a shutdown hook. 145 arboricxServer root addr port = (let 146 failed message = pure (err message t) 147 opened server = (let 148 rejected message = finally (failed message) (closeSocket_ server) 149 ready _ = serveForever server (arboricxHandler root) 150 in onResult_ (do onOk_ 151 bindSocket server addr port 152 listen server 128) 153 rejected ready) 154 in onResult_ socket failed opened)