Why Visually Identical Unicode Text Can Produce Different Hashes
Compare code points, UTF-8 bytes, and normalization rules before diagnosing a text checksum mismatch.

Two pieces of text can look identical and still contain different sequences of characters. A hash compares bytes, not visual appearance. This matters when checksums disagree after copying a name, processing a CSV, or passing text through an editor. The investigation below separates the original representation from any deliberate normalization so you do not “fix” an integrity check by changing its input unnoticed.
Start with a small synthetic pair
Use the accented character é in two representations: U+00E9, and U+0065 followed by U+0301. They can render alike, but their UTF-8 byte sequences differ. Hashing each original sequence therefore need not produce the same result.
Keep the two inputs in a fixture that preserves their actual characters. A screenshot does not preserve code points. Record how you created each string and inspect code-point values before copying them into another tool that might transform the text.
Trace the byte-producing step
Identify the encoding used before hashing. For a web-text workflow this is often UTF-8, but file bytes may include other encodings or a byte-order mark. Newline style and a trailing newline also change the input. Check those differences independently from normalization.
Draw the pipeline as text → optional transformation → encoding → bytes → digest. That sequence explains where an unexpected change could enter. If one side hashes a downloaded file and the other hashes text pasted from that file, the two operations may not use identical bytes.
Select a normalization policy only when appropriate
NFC can bring canonically equivalent sequences into a composed form. NFD decomposes them. Compatibility forms NFKC and NFKD make broader changes and can remove distinctions you intend to preserve. Choose a form based on the application’s contract, not on which makes today’s test pass.
For a user-name search key, a documented normalization step may be appropriate. For a file-integrity checksum, silently normalizing text defeats the purpose of checking the original bytes. Keep the original data and derived comparison key separate.
Compare before and after explicitly
Hash the original pair first, then normalize both using the chosen form and hash the resulting strings. Record both outcomes. This shows whether normalization explains the mismatch rather than hiding it inside a helper function.
Include a control case with an ordinary ASCII word and a case with a trailing newline. The newline should remain a separate issue. Do not describe every mismatch as “Unicode trouble” when a simpler byte difference explains it. Your test notes should identify the transformation that changed the result.
Document the boundary for future consumers
State whether your published digest describes raw file bytes, UTF-8 text, or normalized UTF-8 text. Consumers need that information to reproduce it. Include the normalization form and newline policy if either is part of the agreement.
Use the hash tool on synthetic inputs to investigate a mismatch, then verify the real pipeline in code. A digest match does not identify which transformations happened before hashing. Reproducibility comes from an explicit input specification as well as the digest itself.
| Case or stage | Detail | What to notice |
|---|---|---|
| Composed | U+00E9 | One code point |
| Decomposed | U+0065 + U+0301 | Two code points |
| Agreed NFC | Normalize, then encode | Compare derived text |