Skip to content

Releases: Bonuspunkt/CaseDropper

v0.4.0 — We Put a Scripting Language Inside a Static File Server

Choose a tag to compare

@Bonuspunkt Bonuspunkt released this 28 Feb 00:09

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", "привет")
end

Drop 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 optional LuaEngine pointer 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

Choose a tag to compare

@Bonuspunkt Bonuspunkt released this 28 Feb 20:39

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 scratch image, 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.3

Your files. Any casing. 294 kilobytes. You're welcome.

v0.2.3 — Now With Actual Multi-Arch

Choose a tag to compare

@Bonuspunkt Bonuspunkt released this 27 Feb 17:56

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 scratch image — The entire Docker image is now a single static binary in an empty container. No OS. No shell. No docker exec. No attack surface. Just vibes. ~12 MB total.
  • Multi-arch: amd64 + arm64 — Runs on your server, your Mac, your NAS. We originally promised armv7 too, 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 InvalidCastException because 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_BROWSING env var — Set true or 1 to 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.2

Your files will now hot-reload. Your image is now smaller. Your conscience remains your own.

v0.1.0 — It Works On My Machine

Choose a tag to compare

@Bonuspunkt Bonuspunkt released this 27 Feb 17:55

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.PNG and logo.png as the same file, because apparently that's something the world needs
  • A CaseInsensitiveFileProvider that is, at its core, a Dictionary<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 scratch Docker 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.