Two Bugs That Only Korean Files Could Find

One character wrong in a PDF title, and two days to explain it. “latin1” does not mean Latin-1, and your test fixtures are a demographic statement.

Date

/

Category

Insights

Insights

/

Writer

Keon Hwui Kim

Keon Hwui Kim

A PDF arrived and the product reported its title as RAR 2026 시쥒 브리프.

The real title was RAR 2026 시즌 브리프. One character wrong. Just the one, in the middle of an otherwise perfect string.

That single character cost two days, and the two bugs behind it are, I think, the most useful thing I have found all year, because neither was catchable by any test I would have thought to write.

Bug one: "latin1" does not mean Latin-1

The reader was decoding bytes with this:

const decoder = new TextDecoder("latin1");
const decoder = new TextDecoder("latin1");
const decoder = new TextDecoder("latin1");

Which any reasonable person reads as ISO-8859-1. It is not.

Per the WHATWG Encoding Standard, the following labels all map to windows-1252:

ansi_x3.4-1968   ascii        cp1252
cp819            csisolatin1  ibm819
iso-8859-1       iso-ir-100   iso8859-1
iso88591         iso_8859-1   iso_8859-1:1987
l1               latin1       us-ascii
windows-1252     x-cp1252
ansi_x3.4-1968   ascii        cp1252
cp819            csisolatin1  ibm819
iso-8859-1       iso-ir-100   iso8859-1
iso88591         iso_8859-1   iso_8859-1:1987
l1               latin1       us-ascii
windows-1252     x-cp1252
ansi_x3.4-1968   ascii        cp1252
cp819            csisolatin1  ibm819
iso-8859-1       iso-ir-100   iso8859-1
iso88591         iso_8859-1   iso_8859-1:1987
l1               latin1       us-ascii
windows-1252     x-cp1252

The specification even includes a note acknowledging the confusion directly, observing that these labels have historically been confusing for developers and that on the web, "latin1" and "ascii" are just labels for windows-1252.

The history is straightforward once you know it. Both ISO-8859-1 and ASCII leave part of the byte range undefined or unused. Implementations filled that range in differently. Browsers converged on windows-1252 for compatibility, and the Encoding Standard then wrote the convergence down rather than pretending otherwise.

Where the difference actually lives

Byte range

ISO-8859-1

windows-1252

Same?

0x00 to 0x7F

ASCII

ASCII

Yes

0x80 to 0x9F

C1 control codes

Printable: quotes, dashes, €, ™

No

0xA0 to 0xFF

Latin-1 supplement

Latin-1 supplement

Yes

Table 1. Thirty-two of 256 values differ. One byte in eight.

For English text you will never see this. Those bytes essentially do not occur in English content. For Korean text encoded in a byte-oriented format they occur constantly, and every one of them decodes to something near-plausible. Not garbage. Not an exception. Just a slightly different Korean character, in a slightly different word, inside a title that still reads like a title.

That is what makes it expensive. A crash is cheap. A wrong answer that looks like a right answer is not.

Bug two: PDF escapes non-ASCII in octal

With the first bug fixed, the title was still wrong, differently.

PDF string literals escape characters with backslashes. A reader written by someone who has only ever handled English PDFs will handle the obvious three and stop:

// What our reader handled
\(   →  (
\)   →  )
\\   →  \

// What the format also does, and we did not handle
\ddd →  the byte with octal value ddd

// So a Korean character arriving as three bytes:
\355\225\234   →  한

// Unhandled, this produces literal backslashes
// and digits in the middle of the title.
// What our reader handled
\(   →  (
\)   →  )
\\   →  \

// What the format also does, and we did not handle
\ddd →  the byte with octal value ddd

// So a Korean character arriving as three bytes:
\355\225\234   →  한

// Unhandled, this produces literal backslashes
// and digits in the middle of the title.
// What our reader handled
\(   →  (
\)   →  )
\\   →  \

// What the format also does, and we did not handle
\ddd →  the byte with octal value ddd

// So a Korean character arriving as three bytes:
\355\225\234   →  한

// Unhandled, this produces literal backslashes
// and digits in the middle of the title.

Every non-ASCII character in a PDF string can arrive in that form. A reader that ignores octal escapes will mangle every title that is not in English, and will do it in a way that produces output rather than an error, which means nothing anywhere reports a problem.

Why neither was findable

This is the part I actually want to write about.

The project had 316 unit tests when these shipped. Neither bug was catchable by any of them, and no quantity of additional tests of the same kind would have helped, because the tests ran against fixtures, and I wrote the fixtures.

I wrote English filenames because I write code in English. I generated test PDFs with ASCII titles because that was the path of least friction at the moment I needed a fixture. Every fixture encoded, invisibly, an assumption about who the user is.

Your test fixtures are a demographic statement. They describe the user you were imagining when you wrote them.

Both bugs surfaced the first time a real Korean file went through a real browser. Not a generated one. A real document, with a real title, from real work, dropped in by a person who wanted to keep it.

The fix at the fixture level was to make the generator produce material that looks like the material the product will actually receive: Korean filenames, emoji, overlong names, mixed scripts, files whose declared type is wrong. That generator now runs on a fresh clone, so the rehearsal is real rather than a recording.

The instrument bug, which is worse

There is a coda, and it is the one I would most want another builder to know about.

While writing browser tests to cover this, Playwright's setInputFiles silently dropped every file whose name was not ASCII. No error. No exception. No change event. No files.

The test simply asserted against an empty state, and the empty state was, technically, exactly what the page showed.

So for a while, the tests designed to catch the Korean encoding bugs were themselves structurally unable to deliver a Korean file.

// Silently drops non-ASCII filenames
await input.setInputFiles("계약서.pdf");

// Works: hand the file over by value
await input.setInputFiles({
  name: "계약서.pdf",
  mimeType: "application/pdf",
  buffer: fs.readFileSync(path)
});
// Silently drops non-ASCII filenames
await input.setInputFiles("계약서.pdf");

// Works: hand the file over by value
await input.setInputFiles({
  name: "계약서.pdf",
  mimeType: "application/pdf",
  buffer: fs.readFileSync(path)
});
// Silently drops non-ASCII filenames
await input.setInputFiles("계약서.pdf");

// Works: hand the file over by value
await input.setInputFiles({
  name: "계약서.pdf",
  mimeType: "application/pdf",
  buffer: fs.readFileSync(path)
});

The lesson generalises well past this one API:

When a test proves nothing, suspect the instrument before you suspect the code.

A green suite that cannot deliver the input it claims to be testing is not weak coverage. It is a false statement, and it is stated in the most confident typography your CI has. Every other signal in the system agrees with it.

The general version

Three things I took from two days:

  1. A wrong answer is more expensive than a crash, and systems that produce plausible output on malformed input are optimised for the wrong failure. The octal bug and the encoding bug both produced strings. Neither produced an error.

  2. Fixtures encode assumptions you do not know you hold. Not values, not opinions. Defaults so deep they were never decided. The only reliable way to surface them is to run real material from a context other than your own.

  3. Test infrastructure is code, and it fails. We treat the harness as ground truth because it is the thing that tells us about truth. It has bugs at the same rate as everything else, and its bugs are worse, because they fail toward false confidence.

The character was 즌 becoming 쥒. Everything above came out of that.

Sources

  • WHATWG Encoding Standard, the encodings table and the note on legacy labels. encoding.spec.whatwg.org

Related reading

More from the Moc Mallen Studio journal