Base84 deserves a place in file names
The Case for Base84 in Filename Encoding
Originally published by Frank DENIS on September 9, 2026
The development of TurboCrypt, a tool designed for file and git encryption, began with a focus on Unix-like environments. To handle encrypted filenames, the developers initially implemented Base91.
Base91 was considered a perfect fit because it generates strings that are valid across Unix and macOS filesystems. However, this perspective ignored the complexities of various libraries and the eventual need for cross-platform compatibility.
The Windows Compatibility Hurdle
When the demand for Windows support emerged, the team hit a wall. Windows forbids several characters that are perfectly acceptable in the Unix-safe alphabet used by Base91. While some might suggest adding a dot to the alphabet, a filename ending in a . is unreliable when interacting with standard Windows file APIs or the shell.
By removing the dot entirely, we arrive at a set of 84 characters that are universally safe for filename components across Linux, macOS, and Windows. Furthermore, excluding dots prevents the accidental creation of "hidden" files on Unix systems (those starting with .) or the special . and .. directory references.
The Base84 Alphabet
The characters are ordered as follows for encoding:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$% '()+,-;=@[]^_`{}~
Technical Efficiency and Mathematics
Base84 is designed for machine-to-machine communication; human readability is not a priority. The goal is maximum filesystem compatibility with minimal data expansion compared to Base64.
The "sweet spot" for packing is five characters. Using LaTeX to express the capacity:
This value is only shy of . Consequently, a group of five characters can hold 32 bits of data approximately of the time (given uniformly random input), and 31 bits otherwise. Specifically, if the value is below , a 32nd bit can be stored.
Efficiency Metrics:
- Average bits per character:
- Average bits per group:
| Encoding Scheme | Average Expansion | Worst-case Expansion |
|---|---|---|
Base64 | ||
Base84 |
Note: The worst-case scenario occurs with an input of
0xff, forcing each full group to consume only 31 bits. Because 5 divides 255 exactly, a maximum-length name consists of complete groups, ensuring no bits are lost. Base84 can accommodate bytes of input, whereas unpadded Base64 is limited to .
Comparison with Other Systems
The requirements for filenames vary wildly by OS:
- Unix/Linux: Almost any character is allowed except for the null byte (
NUL) and the forward slash (/). - Windows: Much more restrictive.
Some implementations, like the filesystem variant of zig-base91, attempt to fix Base91 by replacing the slash (/) with an apostrophe ('). This achieves bits per character (roughly expansion), but it still contains characters that Windows rejects.
Avoiding Windows Reserved Names
Windows reserves specific device names (e.g., CON, NUL, COM1, LPT1) regardless of case. Base84 inherently avoids these through its structure:
- Length Constraints: A four-character output always ends in an uppercase letter or
a,b, orc. - Digit Exclusion: It cannot end in a digit, making
COM1-COM9impossible. - Character Set: Superscript digits and dots are excluded.
- No Padding: Unlike Base64, no special padding is required to avoid reserved names.
Handling Case-Insensitivity Collisions
Since Base84 distinguishes between A and a, but some filesystems do not, there is a theoretical risk of collisions. TurboCrypt mitigates this by padding short names to 16 bytes and applying HCTR2.
If we treat the encryption as a random permutation, we get 128-bit ciphertexts. Even if every character were a letter, there are only possible case variations.
The probability of a case collision between two distinct encrypted names is:
For a directory containing distinct names, the birthday bound for a collision is:
Related Reading:
- Sep 09, 2026: Semi-public Git repositories
- Aug 19, 2026: Why compiling Rust to WebAssembly is slow
- Jul 30, 2026: An improved attack on 7-round AES
- Jul 16, 2026: AES gets swizzled
- Jul 08, 2026: The best WebAssembly runtime may still be no runtime at all