Learning & reference

How file systems handle names, across Windows, macOS and Linux

A practical guide to how Windows, macOS and Linux store and restrict filenames, and what happens at the moment a file crosses from one to another. Read the background first, or scan the matrix and dig into any row.

Why three operating systems disagree about a filename

None of this is arbitrary, and none of it is a bug. Each system inherited its rules from a different decade and a different set of constraints, and then kept them for compatibility.

Windows still carries MS-DOS with it. Drive letters, the backslash separator and the reserved device names CON, NUL, COM1 and the rest were decisions made in 1981, when a filename was eight characters and a three-letter extension. Every one of them is still enforced today, because breaking them would break software that has shipped for forty years.

Linux comes from Unix, where a filename is simply a sequence of bytes. Exactly two values are forbidden: the forward slash, because it separates path components, and the null byte, because it ends a C string. Everything else is legal, including newlines, quotes and a leading dash. The system does not interpret your filename at all, which is both the freedom and the hazard.

macOS is Unix underneath with a different history above it. It has always been case-insensitive by default, and older Macs rewrote filenames into a decomposed Unicode form on disk, so a name typed one way came back with different bytes. That single design choice is still the most common source of duplicate files when work moves between a Mac and anything else.

What a file system actually decides

Underneath the differences, every system is answering the same five questions. Once you know which answer each one gives, the table below stops being a list of exceptions and becomes predictable.

  • Which characters a name may contain, and whether a forbidden one is refused or silently replaced.
  • How long a name and a path may be, and whether the limit counts characters or bytes. That distinction matters enormously once a name is not plain English.
  • Whether case matters. On one system README and readme are two files; on another they are the same file, and the second one overwrites the first.
  • How text is normalised. Unicode can spell the same accented character more than one way, and systems disagree about whether to store what you typed or convert it.
  • What a path looks like: which separator, whether there is a root or a drive, and what happens at the boundary between them.

Why it comes up constantly

A file today rarely stays where it was made. It gets committed to a repository on a laptop and checked out in a Linux container, uploaded through a browser and written to object storage, zipped on one machine and extracted on another, or synced through a service that has its own rules on top. Every one of those boundaries is a translation, and translations lose things.

The failures are rarely loud. A name comes back subtly altered, two files collapse into one, an extraction stops part-way through, or a filename matches in testing and not in production. The table below is a map of where those translations go wrong, and each row links to a real file you can download and push through your own code to see which behaviour you actually have.

✓ Allowed ✗ Forbidden / breaks ! Silently changed / risky

Scroll the table sideways for more columns

Rule Windows macOS Linux
Special chars : * ? " < > | ✗forbidden !avoid : ✓allowed
Backslash \ in a name ✗path separator ✓ordinary char ✓ordinary char
Reserved names CON NUL ✗forbidden ✓allowed ✓allowed
Case-sensitive names ✗merges ✗merges (default) ✓distinct
Unicode NFC = NFD !bytes as-is !stores NFD !bytes as-is
Trailing . or space ✗stripped ✓kept ✓kept
Path length !260 default ✓1024 ✓4096
Zero-byte file ✓"0 KB" ✓"Zero bytes" ✓0
Text line endings !CRLF !LF !LF

Forbidden characters

Windows rejects these outright. Linux allows almost anything.

<>:"/\|?*

Which of the 9 each system rejects

Windows
Rejects all 9
macOS
Rejects /; : shows as /
Linux
Rejects / only

Trap: report:2024.txt is legal on Linux, impossible on Windows: sync skips it.

Try: question?name.txt · double"quote.txt

Path separators: / vs \

The single biggest cross-platform path bug.

Which separator each system uses

Windows
Uses \, accepts /
macOS
Uses /
Linux
Uses /

Trap: on Linux, folder\file is one filename containing a backslash. Move it to Windows and it becomes a folder ➜ file path, or fails. Web URLs are always /.

Try: backslash\name.txt

Reserved device names

Windows keeps legacy device names, even with an extension.

CONPRNAUXNULCOM1–9LPT1–9

Whether these names can be used at all

Windows
Rejects them
macOS
Allows them
Linux
Allows them

Trap: CON.txt and nul.log can't be created or extracted on Windows.

Try: CON.txt · NUL.txt

Case sensitivity

Are File.txt and file.txt the same file?

What happens to README.txt and readme.txt side by side

Windows
One file, second overwrites
macOS
One file by default
Linux
Two separate files

Trap: a repo with README.md + Readme.md loses one file when cloned on Windows/Mac.

Unicode normalization

café can be 4 code points (NFC) or 5 (NFD). Same look, different bytes.

Windows
as typed; NFC ≠ NFD
macOS
as typed; NFC = NFD
Linux
as typed; NFC ≠ NFD

Trap: a Mac-saved café.txt won't match the same name typed on Windows: "file not found" for a name you can see.

Try: nfc-café.txt vs nfd-café.txt

Trailing dots & spaces

Windows silently strips a trailing . or space.

What happens to a name ending in a dot or a space

Windows
Silently stripped
macOS
Kept as typed
Linux
Kept as typed

Trap: report. becomes report: two files collide into one.

Try: trailing-dot.txt.

Length limits

Per-name and full-path caps.

Windows
260-char path*
macOS
1024-byte path
Linux
4096-byte path

Names: 255 units (Win/Mac) or 255 bytes (Linux, so ~63 emoji). *Past 260 only via the \\?\ prefix, or LongPathsEnabled plus an app that opts in.

Try: a 204-char name

Zero-byte files

A file with no content is valid everywhere, but shown and handled differently.

Explorer
empty.txt
0 KB
Finder
empty.txt
Zero bytes
ls -l
-rw-r--r-- 1 0 empty.txt

Trap: upload validators that check MIME but not size > 0 accept empty files; some sync clients skip them; Git needs an explicit add. Used deliberately as markers (.gitkeep).

Try: 0b.bin (exactly 0 bytes)

Text line endings

Same text, different bytes per platform.

The line ending each system writes by default

Windows
\r\n CRLF
macOS
\n LF
Linux
\n LF

Trap: breaks diffs, checksums and line-by-line parsers. Classic Mac used bare \r (CR).

Try: crlf.txt vs lf.txt

Cloud sync services add their own rules

OneDrive, Dropbox and Drive apply the strictest (Windows-style) rules on every platform, plus extra reserved names and path caps.

☁️
OneDrive / SharePoint
Blocks " * : < > ? / \ |, .lock, CON…LPT9, ~$…, leading/trailing spaces, ~400-char paths
📦
Dropbox
Blocks / \ < > : " | ? *, .dropbox, desktop.ini; case-insensitive; no trailing spaces/dots
🗂️
Google Drive
Permissive storage, but case- & normalization-insensitive matching can conflict on the desktop client

Safe-everywhere rule: use only A–Z a–z 0–9 . _ -, no leading/trailing dots or spaces, avoid Windows reserved names, keep paths under ~255 chars, normalize Unicode to NFC.

Prove your app handles these

Every rule above has a matching test file in the catalog. Upload one and confirm your app fails gracefully, not silently.

→ Browse the filename & encoding test files

More from Learning

Guides and references for test data, file handling and AI evals. All free, no sign-up. See the full hub.

Synthetic data for AI evalsWhat you can download, what you buildRAG test corpusDocuments with extraction trapsJSONL for evalsThe format, and ten ways it breaksMock API for agentsRecords, pagination, failures on demandSafe test dataReserved domains, IPs, numbers & cardsSynthetic PIIFake people that reach nobodyTest card numbersEvery brand, declines, 3D SecureTest phone numbersReserved US, UK and Australian rangesReserved IP addressesDocumentation ranges and SSRF casesExample domains.test, .invalid, example.com, and trapsQA test stringsUnicode, emoji, injection & edge casesFile systemsPOSIX, NTFS, APFS, ext4, SMB, NFS, NASFile signaturesMagic numbers that identify every formatCharacter encodingsUTF-8, UTF-16, BOM & normalizationUpload validationThe checklist, with a file for each checkMIME typesThe right Content-Type for every extensionHTTP status codesEvery code and when you actually see itDates & timesISO 8601, epoch, Excel serials, DSTHTTP headersContent-Type, Disposition, Range, CORSRegex cheat sheetSyntax, flags and ready-made patternsGenerate files (CLI)Make test files on any OSCrypto & certificatesSSL/PEM, hashing, encryptionWeb app testingWhat to test, with a file for each caseDesktop app testingPaths, names, encodings and savingMobile app testingPickers, photos, flaky networksCLI testingArguments, pipes and exit codesLarge file upload testingDefault limits, 413s and exact sizes1 GB test file1 GB and 2 GB, with SHA-25610 GB test fileMake one locally in secondsDownload speed test files10 MB to 2 GB of random data