Skip to content
Bonuspunkt edited this page Feb 28, 2026 · 4 revisions

Frequently Asked Questions

Questions no one has actually asked, preemptively answered for the zero people reading this wiki. Now updated for the Zig era and the Lua era, because apparently one era wasn't enough for a static file server.


What does CaseDropper do?

It serves static files case-insensitively. That's it. The whole thing. You're reading the wiki for a project whose entire contribution to computer science is calling toLowercase() on a path and looking it up in a hash map.

But now it does it in Zig, so it's faster and morally superior. And now it has an embedded Lua scripting engine, so you can generate URL aliases at index time, which means your files can be accessed via Cyrillic homoglyphs and Unicode small caps because someone thought that was a reasonable feature for a static file server and no one stopped them.


Didn't this used to be C#?

We don't talk about that. That was a different time. We were young. We thought the .NET runtime was "fine." We thought Dictionary<string, string> was "good enough." We thought garbage collection was "acceptable."

Then we discovered Zig, and we realized we'd been living in a cave, mistaking shadows on the wall for file servers. The entire project was rewritten in Zig 0.15.2. Zero dependencies. Single static binary. No runtime. No GC. No looking back.

Then we embedded Lua 5.4 because enlightenment, it turns out, has levels.

The C# version is gone. The commits remain as a historical record and a warning.


Why Zig?

Because Zig is what happens when someone looks at C and says "what if we kept the good parts and threw away the last 50 years of accumulated mistakes." Because Zig cross-compiles to 9 architectures from a single build host without QEMU. Because @import("std") gives you an HTTP server, a thread pool, reader-writer locks, a hash map, and inotify bindings, and you don't need a single external dependency. Because the binary is statically linked and the container is FROM scratch and there is nothing in it except purpose.

Also Andrew Kelley seems like he knows what he's doing and at this point we're just following the light.


Why Lua?

Because we looked at our static file server and thought "you know what this needs? A scripting language." And then instead of lying down until the feeling passed, we actually did it.

Lua 5.4.7 was chosen because:

  1. It's 32 C files with no external dependencies. Zig compiles C. This is a match made in heaven, or at least in build.zig.
  2. It's the smallest embeddable scripting language that isn't a toy. Lua runs inside Redis, Nginx, World of Warcraft, and now a static file server that lowercases paths. One of these is not like the others.
  3. It cross-compiles to all 9 target architectures because Zig handles the C compilation. No per-architecture toolchain. No "oh I need to install libluajit-dev-arm64." Zig compiles Lua for loong64 and doesn't even blink.
  4. The entire Lua runtime adds approximately nothing to the binary size. It's C. It's small. It was designed to be embedded. We embedded it.

We link musl libc now because Lua needs it. The binary is still static. The container is still FROM scratch. The purity is... nuanced. We accept this compromise with the quiet dignity of engineers who put a scripting language in a static file server.


How does Lua URL rewriting work?

At index build time — not at request time, we cannot stress this enough — the server calls your Lua rewrite(path) function for each file path. If the function returns an alias that's different from the original path, both are stored in the hash map pointing to the same file.

At request time, it's a regular hash map lookup. O(1). The Lua engine is not consulted. The Lua engine is asleep. The Lua engine did its job at startup and now it rests, like a responsible scripting language embedded inside a static file server.

This means:

  • Zero per-request overhead from Lua
  • No mutex contention from Lua calls during HTTP handling
  • The request handler doesn't know Lua exists
  • The hash map doesn't know Lua exists
  • Only the index builder knows Lua exists, and it's not talking

See Lua URL Rewriting for the full deep dive.


What scripts ship with it?

Example scripts live in scripts/. We're not going to tell you what they do. That's between you and ls. Some of them map characters to other alphabets. Some of them make your URLs look like a ransom note written by a Unicode enthusiast. All of them preserve file extensions, because we're unhinged, not irresponsible.

Write your own. The contract is: define rewrite(path), return a string or nil. The server does the rest.


Why not just rename the files?

Because there are 14,000 of them and they came from a Windows share where someone thought IMG_0001.JPG, img_0001.jpg, and Img_0001.Jpg were three different creative decisions.

Also, if you were the kind of person who renames files, you wouldn't be here. You'd be organized. You'd have a build pipeline that normalizes asset paths. You'd have tests. You're not that person. I'm not that person. We're CaseDropper people. We solve problems with hash maps, Lua scripts, and denial.


Why not use nginx?

You could. You absolutely could. nginx has case-insensitive rewriting, a decades-long track record, and documentation written by people who aren't deranged.

But nginx is written in C. CaseDropper is written in Zig. nginx needs a configuration file. CaseDropper needs four environment variables. nginx runs on an operating system. CaseDropper runs on the concept of an operating system — FROM scratch, no OS, just a binary in the void, serving files with the quiet intensity of a monk who has achieved enlightenment through hash maps.

Also, nginx can't cross-compile to 9 architectures from a single zig build command, and nginx doesn't have an embedded Lua engine that generates Cyrillic URL aliases at index time. Actually, nginx does have embedded Lua (OpenResty), but it runs Lua per-request like a commoner. CaseDropper runs Lua once, at startup, and bakes the results into a hash map. We are not the same.


Does it scale?

It indexes every file path into a std.StringHashMap at startup, plus any Lua-generated aliases. Hash map lookups are O(1). Reads are concurrent via std.Thread.RwLock. The thread pool scales to your CPU count. Lua only runs during rebuilds, which are single-threaded and hold the write lock.

So it scales until you have more files than fit in memory, or until your Lua script generates so many aliases that the hash map becomes sentient and starts making demands. Both scenarios are equally distant. Or until Zig releases 1.0 and the standard library API changes, at which point we have bigger problems than casing.


What happened to the FileSystemWatcher?

Dead. Replaced by inotify. The old C# FileSystemWatcher was a well-meaning .NET abstraction that occasionally noticed files existed. It appended to the dictionary but never removed. Files were indexed forever. The hash map grew monotonically, like entropy, or a node_modules folder.

The Zig version uses Linux inotify directly — raw kernel file descriptors, not a managed wrapper — and does full rebuilds on changes. The old map is deallocated. A new map is constructed from a fresh directory walk. All Lua aliases are regenerated. Deletions work. Renames work. Everything works because Zig doesn't hide the kernel behind seven layers of abstraction and a NuGet package.


Why is the default branch called misstress?

Next question.


No seriously, why is it called misstress?

We've moved on. The branch name stays. Like the Zig rewrite and the Lua engine, it is permanent, unexplained, and correct.


Is this production-ready?

It serves files. The files arrive at the browser. The casing doesn't matter. The alphabet doesn't matter, apparently. The binary is statically linked. The container has no attack surface. The memory is manually managed. There is no garbage collection pause. There is no runtime startup delay. There is a hash map, and the hash map is fast, and the hash map is correct, and the hash map contains aliases generated by a Lua script that was decoded from obfuscated JavaScript found on a Unicode symbol generator website.

If that's your bar for production-ready, then yes. If your bar is higher than that, you should probably not be running a project you found by searching "case insensitive static file server" on GitHub at 2 AM.


Can I contribute?

You can, but I want you to think very carefully about what feature you'd be adding to a static file server that lowercases paths and generates Cyrillic URL aliases via an embedded Lua scripting engine. Really sit with that.

Also, you'll need to write Zig. If you haven't written Zig before, this is a great opportunity to learn. And by "great opportunity" I mean "you will fight the compiler for three hours, emerge victorious, and understand memory allocation at a spiritual level." Andrew Kelley built a language that makes you a better programmer by refusing to let you be a worse one. Embrace it.

If you want to write a Lua script instead, that's easier. Define a rewrite(path) function that takes a path string and returns a rewritten alias or nil. Drop it in scripts/. Set the LUA_SCRIPT env var. The server will pick it up, run it at index time, and suddenly your static files have aliases in whatever alphabet or encoding you've chosen. We recommend Cyrillic homoglyphs, but we're not the cops.


Why is there a wiki for this?

Because the project was rewritten in Zig, then had a Lua scripting engine embedded into it, and that demanded documentation. Not because Zig is complicated — it's the simplest thing in the project — but because when you rewrite something from C# to Zig, embed Lua 5.4, decode a URL rewriting scheme from obfuscated JavaScript, and ship the whole thing in a FROM scratch container across 9 architectures, you want to write it down. You want to tell someone. The wiki is that someone. The wiki doesn't judge. The wiki just holds our documentation and our shame.


Should I rewrite my project in Zig too?

Yes. And then embed Lua. Next question.

Clone this wiki locally