Cat Toolbox
Help CatBlog About Partnership 中文

Base64, URL Escapes and Unicode Escapes, and Why None of Them Hide Anything

EncodingUpdated 2026-09-29

Three pages under 编码 / Encoding convert text from one written form into another: Base64, URL Encoder and Unicode Converter. All three are lossless and unkeyed — anyone can run the reverse operation in one line of code — so none of them is a way to hide something. What differs between them is a set of small rules that decide whether your output is correct, and each page applies its own version of those rules.

The one-line difference

  • Base64 turns bytes into 64 safe characters, so binary data can travel through text-only channels. Output is about 33% larger than the input.
  • Percent-encoding turns characters that have a job in a URL into %HH, so they stop being structural. Only the characters that are actually unsafe in that position need it.
  • \uXXXX escapes turn a character into its number written in the source text, so a file that cannot carry non-ASCII bytes can still name those characters.

All three are reversible. If your threat model involves an reader who is not supposed to recover the original, none of these pages is the tool — that is what the AES page is for.

Base64: it encodes bytes, and this page picks UTF-8 first

The page takes your text, encodes it as UTF-8, then Base64-encodes those bytes. For 中文 🐱 — 11 UTF-8 bytes — the result is 5Lit5paHIPCfkLE=, which is what any standard implementation produces; I checked it against Python's base64 module rather than trusting the shape of it.

Two consequences worth internalising:

  • The output depends on the byte form, not the character form. The same characters typed with a different Unicode normalisation, or with a trailing newline you cannot see, produce different Base64. Pressing Enter at the end of your text is enough to change the result: 中文 alone gives 5Lit5paH, 中文 plus a newline gives 5Lit5paHCg==.
  • ASCII is not exempt. Hello gives SGVsbG8=. Nothing about the input has to be binary for Base64 to apply.

What the decoder tolerates, and where it lies to you

The decode direction is more forgiving than most people expect, which is good news for pasting:

  • Missing padding is fine. 5Lit5paHIPCfkLE without the trailing = decodes correctly.
  • Whitespace inside the payload is ignored. A line-wrapped, MIME-style block pastes in and still decodes.
  • Whitespace at the ends is trimmed before decoding, but the encode direction does not trim — so a trailing newline in your source text really is encoded.

The bad news is the error message. Because the page decodes Base64 and then reads the bytes as UTF-8, a payload that is valid Base64 but not valid UTF-8 is rejected with "the input string is not a valid Base64 encoding". Feeding it /w== (a single 0xFF byte — textbook-valid Base64) produces exactly that message. So the message does not tell you which of the two steps failed: if you are sure the text came from a Base64 encoder, the real problem is that the bytes are not text, and decoding them here cannot work — a JPEG or a ZIP has to be handled as a file, not pasted back as a string.

Two interface details to know before you trust what is on screen:

  • A failed run leaves the previous result in the output box. The error appears, and the stale text next to it is from your last successful run. There is no "output cleared" signal, so check the error area before copying.
  • The output box is an ordinary editable textarea. You can type into it; nothing re-derives it.

Dropping a file puts the answer in the input box

The upload area is labelled "encode to Base64", and the mechanism is: read the file as a data URL, cut off everything up to the comma, and write the remainder into the input field, clearing the output field. So after a drop, the Base64 is where typed text goes, not where results go — you copy it from the input box.

Three side effects of that design:

  • The data:image/png;base64, prefix the FAQ mentions is the part that gets thrown away. To build one you have to re-add data:<your mime type>;base64, yourself.
  • Only the first file of a multi-file drop is used, with no warning.
  • The size label is always printed in KB ((size / 1024) fixed to one decimal), so a large file reads as something like 2097152.0 KB. The whole file is also held in memory twice — once as bytes, once as a Base64 string in a textarea — so multi-hundred-megabyte files are a poor fit for any browser tab, including this one.

Percent-encoding: the unreserved set, and the plus-sign gap

The URL page calls encodeURIComponent and decodeURIComponent. Given a b&c=d#e?f/g%h+i!~*_'()中🐱 it returns:

a%20b%26c%3Dd%23e%3Ff%2Fg%25h%2Bi!~*_'()%E4%B8%AD%F0%9F%90%B1

Read that output carefully, because it defines the tool's boundaries:

  • Left alone: letters, digits, and - _ . ! ~ * ' ( ). Everything else is escaped. That is the whole unreserved-plus list, and it means &, =, ?, #, /, + and % all get escaped — correct for a parameter value.
  • Space becomes %20, not +.
  • Non-ASCII becomes its UTF-8 bytes, so 中 is three escapes (%E4%B8%AD) and an emoji is four (%F0%9F%90%B1).

The asymmetry that bites people

decodeURIComponent does not treat + as a space. Give it a+b%20c and you get back a+b c — the percent-escape became a space, the plus stayed a plus. That matters because HTML forms and most server query-string parsers do use + for a space (the application/x-www-form-urlencoded convention). So decoding a query string copied out of a browser address bar or a server log with this page leaves literal + where a space belonged. Either replace + with %20 first, or use a real form-decoder.

Encoding in the other direction has the mirror-image gap: the page escapes a space as %20, which is valid in a query string but is not what a browser submits for a form field.

Two ways this page fails quietly

  • Empty input clears the output and says nothing. No message, no error state — blank in, blank out. (The Base64 page prints a prompt instead, so the two pages behave differently on the same action.)
  • Errors are written into the output box itself, not a separate area. So an error string can be sitting where your result was, and the copy button will happily copy it.

Failures themselves come from only a handful of inputs, all worth knowing:

  • A stray % that is not a valid escape, and a percent sequence whose bytes are not valid UTF-8 (%FF alone) → "decode failed" text in the output box.
  • A lone surrogate — the half of an emoji pair, which appears when a string has been truncated in the wrong place — makes encoding fail outright rather than produce partial output.

And one detection trick: because %25 is the escape for a percent sign, a value that decodes to something still containing % was encoded twice. Decoding %2520 gives %20; decoding that again gives a space. If your URL shows %252C or %253A anywhere, the fix is to decode once, not to keep patching.

Unicode escapes: every character, six at a time

The converter goes both directions between text and \uXXXX notation, and its forward rule is broader than most people assume: it escapes every character, one output group per UTF-16 unit. A space becomes \u0020, A becomes \u0041, a double quote becomes \u0022, and a line feed becomes \u000a. Hex is lower-case and zero-padded to four digits, so the output is always exactly six times the number of UTF-16 units in the input.

That "per UTF-16 unit" detail is what makes the escape form work for emoji: 🐱 is emitted as the pair \ud83d\udc31, the two halves of a surrogate pair, not as a single \u{1f431} code-point escape.

The reverse direction matches only the exact \u + four hex digits pattern, and it replaces each group independently. Because JS strings are themselves UTF-16, the two lone halves land next to each other and rejoin into the emoji — so the round trip is lossless in practice, which I verified on mixed input (Chinese, emoji, quotes, a backslash and a newline came back character-for-character).

But "only that exact pattern" means several neighbouring forms pass through untouched:

  • \u{1f431} — the ES6 code-point escape — is not recognised, because { is not a hex digit.
  • \x41, \n, \t as two-character sequences are left alone.
  • HTML entities such as &#x4E2D; are left alone.

So this page is a \uXXXX converter, not a general unescaper. If your JSON uses \n or \/, this is the wrong tool.

Not the same as what JSON does. JSON.stringify("中文") returns "中文" unchanged — real JSON is UTF-8 and does not need escapes. The output of 转 Unicode is longer and looks different, though both forms parse back to the same string. Escape everything is only useful when the file you are writing must stay ASCII: old Java .properties files, some config formats, source files shipped in a non-UTF-8 encoding.

Choosing in one pass

  1. Data has to survive a text-only channel and might be binary → Base64. Remember the +33% and that it says nothing about secrecy.
  2. A value going into a query string → percent-encoding on the value only. Never run the whole URL through encodeURIComponent; that escapes the ? and & you depend on. encodeURI keeps URL structure, encodeURIComponent does not — the page's FAQ states this correctly.
  3. A file that must remain readable in an ASCII-only editor → \uXXXX.
  4. Something you do not want a third party to read → none of the above; that needs a real cipher and a key you did not publish on a web page.

Shared limits

All three pages are plain browser calls — btoa/atob, encodeURIComponent/decodeURIComponent, and a character loop — so there is no server round-trip, and also no chunking, no progress reporting and no file streaming. None of them offers the alternatives people most often need next: no URL-safe Base64 variant (-/_ instead of +//), no hex or Base32 output, no way to choose an encoding other than UTF-8, no \xNN support, and no single-page-at-a-time decode for a query string containing +. Where those matter, a command-line tool is the honest answer; what these pages are good for is checking one small, awkward case quickly and seeing exactly what the browser will do with it.

Tools used in this guide

Advertisement