Repository navigation
Releases: Bonuspunkt/CaseDropper
Release list
v0.4.0 — We Put a Scripting Language Inside a Static File Server
We embedded Lua 5.4 into a static file server. On purpose.
🌙 Lua URL Rewriting Engine
CaseDropper now ships with an embedded Lua 5.4.7 scripting engine for URL rewriting. Set LUA_SCRIPT to a Lua file that defines a rewrite(path) function, and CaseDropper will generate URL aliases at index build time. Both the original path and the rewritten alias serve the same file.
The key architectural decision: Lua runs at index time, not per-request. The path map is built once (and rebuilt on file changes), so request handling is still just a hash map lookup. We added a scripting engine and it costs exactly zero nanoseconds at runtime.
LUA_SCRIPT=scripts/smallcaps.lua WWWROOT=wwwroot ./zig-out/bin/casedropper
# /index.html → 200 ✅
# /ɪɴᴅᴇx.html → 200 ✅📜 Write Your Own
function rewrite(path)
-- return string for alias, nil to skip
return path:gsub("hello", "привет")
endDrop it in scripts/, set LUA_SCRIPT, restart. The path map rebuilds with your aliases. File watching picks up changes automatically.
Example scripts ship in scripts/ — discoverable by the curious.
🔗 URL Percent-Decoding
PathMap.normalizePath now percent-decodes URLs before lookup. Unicode characters in URLs work correctly when the browser percent-encodes them. This also fixes edge cases with filenames containing spaces or special characters.
Breaking Changes
- musl libc is now statically linked — Lua needs libc. The binary is still fully static and the scratch container still works, but the binary is slightly larger.
PathMap.init()now takes an optionalLuaEnginepointer as its third argument.
Technical Details
- Lua 5.4.7 source vendored in
deps/lua/src/(32 C files, compiled by Zig's build system) - Zero external dependencies at build time — Zig compiles the Lua C sources alongside the Zig code
- Cross-compilation to all 9 architectures still works (Lua is pure C99)
- Lua engine lifecycle: init once at startup, called during path map build/rebuild, deinit at shutdown
- Error handling: script load failures are fatal; runtime errors in
rewrite()are logged and skipped - File extensions are preserved by bundled scripts (only stems are transformed)
- First-write-wins collision handling — real paths always beat Lua aliases
v0.3.0 — Rewritten in Zig
The One Where We Rewrote Everything In Zig
Remember when we said .NET Native AOT was small? We lied. 12 MB for a hash map and a for loop was always embarrassing. So we rewrote the whole thing in Zig 0.15.2. The image is now 294 KB. Yes, kilobytes. The README is bigger than the binary.
What changed
- Zig 0.15.2 — One static binary. No libc. No runtime. No garbage collector. No exceptions. No regrets. Starts in microseconds because there's nothing to start.
- 9 architectures —
amd64,arm64,armv7,i386,riscv64,ppc64le,s390x,mips64le,loong64. We went from "runs on your server and your Mac" to "runs on your toaster, your mainframe, and that RISC-V board you impulse-bought". Zig cross-compiles natively from a single build host. No QEMU. No separate toolchains. No excuses. - ~294 KB image — Down from ~12 MB. We removed .NET, then we removed the OS, and now we've removed everything except the binary and your files. There is literally nothing left to remove. We checked.
- inotify hot-reload — Same behavior as before, but now it's inotify instead of FileSystemWatcher. 1-second debounce. We considered making it configurable. Then we didn't.
- Thread pool with reader-writer locks — Concurrent reads, exclusive writes during path map rebuild. It's the correct concurrent data structure. We're as surprised as you are.
What didn't change
- Same env vars:
WWWROOT,PORT,ENABLE_DIRECTORY_BROWSING - Same behavior: case-insensitive file serving via a hash map
- Same
FROM scratchimage, just 40x smaller - Still no HTTP/2 (no TLS, no h2c, no plans)
- Still no shell in the container. Still beautiful.
Supported architectures
amd64✓arm64✓armv7✓ — yes, for real this time. Zig doesn't care about your bit width.i386✓riscv64✓ppc64le✓s390x✓mips64le✓loong64✓
Upgrade from v0.2.x
docker pull ghcr.io/bonuspunkt/casedropper:0.3Your files. Any casing. 294 kilobytes. You're welcome.
v0.2.3 — Now With Actual Multi-Arch
The One Where We Actually Shipped Everything
All the things v0.1.0 should have had, plus a painful lesson in cross-compilation.
New since v0.1.0
- Hot-reload via FileSystemWatcher — Drop files in, the path map rebuilds itself within a second. No restart needed. We solved the hard problem so you can keep deploying by drag-and-drop into a mounted volume like it's an FTP server.
FROM scratchimage — The entire Docker image is now a single static binary in an empty container. No OS. No shell. Nodocker exec. No attack surface. Just vibes. ~12 MB total.- Multi-arch:
amd64+arm64— Runs on your server, your Mac, your NAS. We originally promisedarmv7too, but .NET Native AOT never actually supported 32-bit ARM. Take it up with Microsoft. - Cross-compilation instead of QEMU — We tried emulating arm64 under QEMU on GitHub Actions. The ILC compiler threw
InvalidCastExceptionbecause ARM64 atomics don't emulate correctly. We now cross-compile natively from amd64 with an Alpine aarch64 sysroot. It's faster and it doesn't crash. Revolutionary. - Build-only CI workflow — So we can catch Dockerfile disasters before tagging a release. This workflow didn't exist for v0.2.0 through v0.2.2. Those tags were deleted. We don't talk about them.
ENABLE_DIRECTORY_BROWSINGenv var — Settrueor1to list directory contents. Your security team will love it.
Supported architectures
amd64✓arm64✓armv7— no, and stop asking
Upgrade from v0.1.0
docker pull ghcr.io/bonuspunkt/casedropper:0.2Your files will now hot-reload. Your image is now smaller. Your conscience remains your own.
v0.1.0 — It Works On My Machine
The One Where We Shipped A Dictionary
It serves files. Case-insensitively. On Linux. In Docker. You're welcome.
What's included
- A static file server that treats
Logo.PNGandlogo.pngas the same file, because apparently that's something the world needs - A
CaseInsensitiveFileProviderthat is, at its core, aDictionary<string, string>. Not machine learning. Not a blockchain. A dictionary. - Native AOT compilation to a single static binary, because we have trust issues with runtime dependencies
- A
FROM scratchDocker image with nothing in it except the binary. You can't shell into it. There's nothing to shell into. It's perfect.
What's NOT included
- Hot-reload (you'll need to restart like it's 2005)
- Multi-arch support (amd64 only, sorry about your Raspberry Pi)
- Any semblance of a reason this project should exist
Migration guide
If you're migrating from IIS: we're sorry. Not for the migration — for whatever decisions led you here.
If you're migrating from nginx: just fix your paths. Seriously. We'll wait.
If you're starting fresh and chose this: bold move.