Skip to content

Refuse to convert when the bytes do not identify the encoding - #45

Merged
amrali-eg merged 2 commits into
masterfrom
feat/ambiguity-refusal
Aug 26, 2026
Merged

Refuse to convert when the bytes do not identify the encoding#45
amrali-eg merged 2 commits into
masterfrom
feat/ambiguity-refusal

Conversation

@amrali-eg

@amrali-eg amrali-eg commented Aug 26, 2026

Copy link
Copy Markdown
Owner

The audit's largest remaining risk category: 262 of 5,078 files where several encodings read the bytes and disagree about what they say. Single-byte code pages map 256 values independently, so a file valid in windows-1252 is equally valid in iso-8859-1, and nothing in the bytes decides. Detection answers anyway — and on short or ASCII-heavy input that answer is close to a guess, which EC then acted on.

Three outcomes, not two

Class Meaning Action
Unambiguous One codec reads these bytes convert
TextEquivalent Several read them, all agree on the text convert
TextChanging Several read them and disagree refuse

The middle case matters as much as the last. A pure-ASCII file is ambiguous in label and identical in text — refusing it would protect nothing. The test is whether the candidates produce different Unicode, not whether more than one can decode.

Observed end to end:

ambiguous_cp1252.txt   iso-8859-1  →  Error      (refused)
ambiguous_koi8r.txt    koi8-r      →  Error      (refused)
ascii_only.txt         us-ascii    →  Converted
determined_utf8.txt    utf-8       →  Unchanged
determined_sjis.txt    shift_jis   →  Converted
determined_big5.txt    big5        →  Converted
determined_utf16.txt   utf-16      →  Converted

Candidate set and placement

Candidates come from EC's supported encodings, deduplicated by code page — never from a corpus. The audit already demonstrated that deriving candidates from test data makes the answer depend on what the tests happened to contain. That list moved from MainForm to TextEncoding so the engine can reach it.

The decision is made beside the detection that produced it and carried on the entry, so the conversion path reads a verdict rather than re-deriving one. A caller who supplies the source encoding rather than detecting it is unaffected — there is nothing ambiguous about an answer somebody gave.

Two mistakes on the way, both caught by the existing tests

Counting rival readings alone refused every UTF-8 file, because iso-8859-1 "reads" it too. A codec that cannot refuse anything is not offering an alternative. Fixed by requiring the detected codec to have no hold on the bytes before rivals count.

Measuring that hold by bit flips alone refused UTF-16, which survives nearly any flip — most produce another valid character. Fixed by also deleting a byte, which tests alignment, the only structure a fixed-width encoding has.

The measurement is deterministic — evenly-spaced probes, not random ones — after an earlier version gave answers that moved with the seed on short files. Whether a conversion is refused should not depend on a random draw.

The threshold sits just above zero because the separation is not a matter of degree: unconstrained single-byte pages reject exactly nothing, and every structured encoding measured on real text lands at 0.111 or above.

The message

The encoding could not be determined uniquely from the file's contents. iso-8859-1 and cp866, ibm852, ibm855, iso-8859-13, and 17 more all match this file and would produce different text. No conversion was performed; specify the source encoding explicitly to convert it.

Rather than "low confidence", which gives a user nothing to act on.

345 tests passing (was 330).

🤖 Generated with Claude Code


Update: -From folded in

The refusal told users to specify the source encoding, and the CLI had no way to do it — a safety feature issuing instructions its own interface could not follow. Fixed before merge:

EncodingChecker.exe -BasePath . -Include "*.txt" -From "windows-1252" -Target "utf-8"

It replaces detection and nothing else. Verified end to end:

Auto-detect on ambiguous file Error + advice
Same file with -From windows-1252 Converted, text intact
-From euc-jp on undecodable bytes still refusedSourceDecodeError
-From + target that cannot hold the content still refused
-From + failing backup still refused
-From + -DetectOnly / -Validate rejected as meaningless

It changes the interpretation, not the permission: naming koi8-r for windows-1252 bytes produces koi8-r text. The user is choosing a reading, not waiving a check. The recovery record is still written.

Also adds AmbiguityReason (SingleCandidate, StructurallyDetermined, MultipleCodecsSameText, MultipleCodecsDifferentText, ExplicitlySpecified) and records per entry whether the encoding was detected or specified — different claims, since detection can be wrong in ways an explicit choice cannot.

354 tests passing (was 345).

amrali-eg and others added 2 commits August 27, 2026 00:16
The audit's largest remaining risk category: 262 of 5,078 files where several
encodings read the bytes and disagree about what they say. Single-byte code
pages map 256 values independently, so a file valid in windows-1252 is equally
valid in iso-8859-1 and nothing in the bytes decides. Detection answers anyway,
and on short or ASCII-heavy input that answer is close to a guess - which EC
then acted on, rewriting the file into one of several readings without saying
so.

EC now classifies each detection and refuses only where it must:

  Unambiguous     one codec reads these bytes            convert
  TextEquivalent  several read them, all agree on text   convert
  TextChanging    several read them and disagree         refuse

The middle case matters as much as the last. A pure-ASCII file is ambiguous in
label and identical in text; refusing it would protect nothing. The test is
whether the candidates produce different Unicode, not whether more than one can
decode.

The candidate set comes from EC's supported encodings, deduplicated by code
page, never from any corpus - the audit already demonstrated that deriving
candidates from test data makes the answer depend on what the tests happened to
contain. That list moved from MainForm to TextEncoding so the engine can reach
it.

The decision is made beside the detection that produced it and carried on the
entry, so the conversion path reads a verdict rather than re-deriving one, and a
caller who supplies the source encoding instead of detecting it is unaffected.
There is nothing ambiguous about an answer somebody gave.

Two mistakes on the way to the discriminator, both caught by the existing tests:

  Counting rival readings alone refused every UTF-8 file, because iso-8859-1
  "reads" it too. A codec that cannot refuse anything is not offering an
  alternative. Fixed by requiring the detected codec to have no hold on the
  bytes before rivals count.

  Measuring that hold by bit flips alone refused UTF-16, which survives nearly
  any flip - most produce another valid character. Fixed by also deleting a
  byte, which tests alignment, the only structure a fixed-width encoding has.

The measurement is deterministic: evenly-spaced probes rather than random ones,
after an earlier version gave answers that moved with the seed on short files.
Whether a conversion is refused should not depend on a random draw. The
threshold sits just above zero because the separation is not a matter of degree:
unconstrained single-byte pages reject exactly nothing, and every structured
encoding measured on real text lands at 0.111 or above.

The refusal names the encodings in conflict and the next step, rather than
reporting low confidence.

345 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The refusal tells a user to specify the source encoding. Until now the CLI had
no way to do that, which made a safety feature issue instructions its own
interface could not follow.

  EncodingChecker.exe -BasePath . -Include "*.txt" -From "windows-1252" -Target "utf-8"

-From replaces detection and nothing else. It answers "which encoding is this?",
not "convert it regardless", so every guarantee from the conversion engine still
applies: the bytes must strictly decode as the named encoding, the output must
re-decode to exactly the same text, a failed backup still aborts, and the
recovery record is still written before anything is installed. Verified by
tests for each of those paths rather than asserted.

It also genuinely changes the interpretation rather than granting permission:
naming koi8-r for windows-1252 bytes produces koi8-r text, which is the point -
the user is choosing a reading, not waiving a check.

Rejected where it cannot mean anything: -DetectOnly and -Validate report what
the detector finds, so overriding the detector there would make the result a
tautology.

Also adds AmbiguityReason - SingleCandidate, StructurallyDetermined,
MultipleCodecsSameText, MultipleCodecsDifferentText, ExplicitlySpecified - and
records on each entry whether the encoding was detected or specified. Those are
different claims, since detection can be wrong in ways an explicit choice
cannot, and the journal will need to say which produced a conversion.

The Phase C contract now has an end-to-end regression suite: ambiguous detection
refused, same-text alternatives allowed, explicit source converts, and explicit
source still refused on undecodable bytes, on content the target cannot hold,
and on backup failure.

354 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@amrali-eg
amrali-eg merged commit 4b48578 into master Aug 26, 2026
1 check passed
@amrali-eg
amrali-eg deleted the feat/ambiguity-refusal branch August 26, 2026 21:35
@amrali-eg

Copy link
Copy Markdown
Owner Author

Phase C status: CLI-complete, GUI override pending.

Automatic refusal on text-changing ambiguity
Actionable CLI override (-From)
Explicit codec still subject to every safety check
GUI source-encoding override ✗ — tracked for Phase E

The GUI plumbing exists (ScanDirectoryOptions.SourceCharset) but has no UI, so a GUI user hitting an ambiguity refusal currently has no in-app escape. That belongs with the risk-aware confirmation work, where the interface needs to surface ambiguity, alternatives, consequences and the source-selection control together rather than as an isolated dropdown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant