Files
rose-ash/lib/dream/static.sx
giles 04b44401fb
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
dream: static file serving — mime, etags, 304, ranges, traversal guard + 28 tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:51:25 +00:00

183 lines
6.0 KiB
Plaintext

;; lib/dream/static.sx — Dream-on-SX static file serving.
;; dream-static mounts at a ** route and serves files under a root: content-type by
;; extension, ETags + If-None-Match (304), and Range requests (206). The filesystem
;; is injectable: production reads via (perform op); tests pass an in-memory map.
;; Depends on types.sx.
;; ── filesystem backends ────────────────────────────────────────────
;; An fs is (fn (op) result); op {:op "file/read" :path p} -> content | nil.
(define dream-static-perform-fs (fn (op) (perform op)))
;; in-memory fs over a {path -> content} dict (tests + demos)
(define
dream-memory-fs
(fn
(files)
(fn
(op)
(if (= (get op :op) "file/read") (get files (get op :path)) nil))))
;; ── content-type by extension ──────────────────────────────────────
(define dr/mime-types {:js "application/javascript" :jpeg "image/jpeg" :css "text/css; charset=utf-8" :ico "image/x-icon" :mjs "application/javascript" :html "text/html; charset=utf-8" :pdf "application/pdf" :jpg "image/jpeg" :json "application/json" :htm "text/html; charset=utf-8" :wasm "application/wasm" :webp "image/webp" :gif "image/gif" :png "image/png" :svg "image/svg+xml" :md "text/markdown; charset=utf-8" :xml "application/xml" :sx "text/plain; charset=utf-8" :txt "text/plain; charset=utf-8"})
(define
dr/ext-of
(fn
(path)
(let
((segs (split path ".")))
(if
(> (len segs) 1)
(lower (nth segs (- (len segs) 1)))
""))))
(define
dream-content-type-for
(fn
(path)
(or (get dr/mime-types (dr/ext-of path)) "application/octet-stream")))
;; ── ETag (weak content hash) ───────────────────────────────────────
(define
dr/static-hash
(fn (s) (dr/static-hash-loop s 0 (string-length s) 7)))
(define
dr/static-hash-loop
(fn
(s i n h)
(if
(>= i n)
h
(dr/static-hash-loop
s
(+ i 1)
n
(mod (+ (* h 131) (char-code (char-at s i))) 2147483647)))))
(define
dr/etag-of
(fn
(content)
(str "\"" (dr/static-hash content) "-" (string-length content) "\"")))
(define
dr/etag-match?
(fn (inm etag) (and (not (nil? inm)) (or (= inm "*") (= inm etag)))))
;; ── path safety ────────────────────────────────────────────────────
(define
dr/static-relpath
(fn
(req)
(or (dream-param req "**") (substr (dream-path req) 1))))
(define
dr/unsafe-path?
(fn (rel) (or (contains? rel "..") (starts-with? rel "/"))))
(define
dr/path-join
(fn
(root rel)
(if (ends-with? root "/") (str root rel) (str root "/" rel))))
;; ── range requests ─────────────────────────────────────────────────
(define
dr/parse-range
(fn
(header total)
(let
((eq (index-of header "=")))
(if
(< eq 0)
nil
(let
((spec (substr header (+ eq 1))))
(let
((dash (index-of spec "-")))
(if
(< dash 0)
nil
(let
((s (substr spec 0 dash))
(e (substr spec (+ dash 1))))
(let
((start (if (= s "") 0 (parse-int s)))
(end (if (= e "") (- total 1) (parse-int e))))
(if
(or
(< start 0)
(>= start total)
(> end (- total 1))
(> start end))
nil
{:start start :end end}))))))))))
(define
dr/serve-range
(fn
(req content etag ctype)
(let
((total (string-length content)))
(let
((r (dr/parse-range (dream-header req "range") total)))
(if
(nil? r)
(dream-add-header
(dream-response 416 {:content-type ctype} "")
"content-range"
(str "bytes */" total))
(let
((start (get r :start)) (end (get r :end)))
(dream-add-header
(dream-add-header
(dream-response
206
{:content-type ctype}
(substr content start (+ 1 (- end start))))
"content-range"
(str "bytes " start "-" end "/" total))
"etag"
etag)))))))
;; ── serving ────────────────────────────────────────────────────────
(define
dr/serve-file
(fn
(req content)
(let
((rel (dr/static-relpath req)))
(let
((etag (dr/etag-of content)) (ctype (dream-content-type-for rel)))
(cond
((dr/etag-match? (dream-header req "if-none-match") etag)
(dream-add-header (dream-empty 304) "etag" etag))
((dream-header req "range")
(dr/serve-range req content etag ctype))
(else
(dream-add-header
(dream-add-header
(dream-response 200 {:content-type ctype} content)
"etag"
etag)
"accept-ranges"
"bytes")))))))
(define
dream-static-with
(fn
(root fs)
(fn
(req)
(let
((rel (dr/static-relpath req)))
(if
(dr/unsafe-path? rel)
(dream-html-status 403 "Forbidden")
(let
((content (fs {:path (dr/path-join root rel) :op "file/read"})))
(if
(nil? content)
(dream-not-found)
(dr/serve-file req content))))))))
(define
dream-static
(fn (root) (dream-static-with root dream-static-perform-fs)))