When someone downloads a video from my site, the file that lands in their Downloads folder should be named after the video. That sounds simple until you see what video titles actually look like in the wild.

A title can contain slashes, colons, question marks, quotes, emoji, and characters from half a dozen alphabets. It can be 200 characters long, or it can be empty. On Windows, a filename cannot contain < > : " / \ | ? *. On every system, a trailing space or a full stop causes trouble. And a filename with a path separator in it is not a filename at all — it is a directory traversal waiting to happen.

Step one: decode, then strip

Titles arrive from provider APIs as JSON strings, so the first job is to make sure I am working with real UTF-8 text, not double-encoded junk. A title that went through two layers of encoding comes out as mojibake — the classic é where é should be. I check the string with mb_check_encoding() and only then start cleaning. If the check fails, I fall back to a plain default name like video and move on. A bad title must never break a good download.

Step two: keep the language, drop the clutter

Here is where a lot of sanitisers go wrong. The lazy approach is to strip everything that is not a letter or digit in the Latin alphabet. That works fine if every video is in English. It is a disaster if the title is in Urdu, Arabic, Chinese, or Russian, because the whole title disappears and the user gets a file called video.mp4.

So instead of a Latin-only allowlist, I keep any letter or number from any script. PHP's Unicode-aware regular expressions make this easy: \p{L} matches a letter in any language and \p{N} matches a number. I keep those, plus spaces, dashes, underscores, and full stops, and I remove the rest. Emoji get dropped in this pass — a filename can technically hold them, but they cause trouble in some download managers and on older systems, so out they go.

Step three: trim, collapse, and cap the length

A cleaned title still needs tidying. I collapse runs of spaces and dashes into one, trim spaces, dashes and full stops from both ends, and cap the name at about 100 characters — filesystems cap a name at 255 bytes, and 100 UTF-8 characters can be close to 400 bytes. I cut on a character boundary, never mid-character, and trim again so the name does not end on a dash.

Step four: the extension is mine, not theirs

The last rule is the one I care about most. The extension never comes from the title or from user input. It comes from the file type I have actually verified — .mp4 for video, .m4a for audio. The Content-Disposition header then looks like this:

Content-Disposition: attachment; filename="my-video-title.mp4"; filename*=UTF-8''my-video-title.mp4 

The filename* part is the RFC 5987 form, and it is what lets browsers handle non-ASCII names properly. The plain filename part is the fallback for older clients. Both carry the same cleaned name, and neither carries anything I did not generate myself.

What I learned

The lesson generalises: every piece of text from outside your system — a video title, a username, a pasted link — is data you did not write, and should be treated as untrusted until it has passed through a cleaning step you control. Sanitising a filename is not glamorous work, but it is the difference between a download that just works and one that fails, or worse, writes somewhere it should not. On ramrom.io it is about thirty lines of PHP, it runs on every single download, and I have not had to think about it since.