fix(log): show control characters from repository content as visible text #144

Merged
james.maes merged 2 commits from fix/143-visible-control-characters into main 2026-11-10 20:01:00 +00:00
Owner

What

Text that came from a repository is now shown with control characters made visible, in the activity log and in the CLI's text output.

  • New DisplayText.Escape (in gclo.Engine): control characters, line and paragraph separators, and text-direction marks, embeddings, overrides and isolates become a backslash, u, and four hex digits. Ordinary text comes back as the same string.
  • FileActivityLog: the message is always one line. An exception's lines are indented under their entry, so no line of it can start like an entry.
  • CLI text modes: progress and failure lines, invalid-path notes, the sanitize note, the dry-run list, and the repos and orgs listings print repository-provided text through it. The JSON modes are unchanged; the JSON encoder already escapes.

Why

Closes #143.

A file path in a git tree may contain a newline, an escape sequence, or a text-direction override, and such a path is exactly what gclo reports when it cannot be created on Windows. A newline started what read as a new log entry (the Log window's "Errors only" view groups lines by their timestamp prefix), and an escape sequence was obeyed by the terminal.

Behaviour changes worth a release note

  • Log format: the lines of an exception are indented by three spaces. Anything that parses the log by "a line starting with a timestamp is an entry" keeps working, and is now correct.
  • CLI text output shows such characters as visible escapes instead of emitting them.

How it was tested

  • gclo.Engine.Tests: 733 passed (was 703). New DisplayTextTests; log tests for a message and an exception whose text imitates another entry.
  • gclo.Cli.Tests: 144 passed (was 142). Text and quiet modes print the visible form on stderr and stdout; the record for the JSON document keeps the original text.
  • Line coverage 100% on gclo.Engine, gclo.ViewModels, gclo. Zero-warning x64 build with locked restore; dotnet format --verify-no-changes clean.

Not run: the UI suite (the Log window reads the same file; its filter is unchanged). Not covered by a test of their own: the repos and orgs listings and the dry-run list with a hostile name (they call the same helper; names there come from the server).

Checklist

  • Commits are signed and signed off (git commit -S -s)
  • Zero build warnings; format and lint are clean
  • Tests added or updated; the 100% coverage gate still passes
  • Docs updated for user-visible changes; threat model updated if the attack surface changed (docs/CLI.md activity log; threat model T15)
  • No secrets, credentials, or personal data
  • New dependencies are justified below, or none were added (none)
  • Substantial AI assistance, if any, is noted here: written with Claude Code (Claude Fable 5.2)

🤖 Generated with Claude Code

## What Text that came from a repository is now shown with control characters made visible, in the activity log and in the CLI's text output. - New `DisplayText.Escape` (in `gclo.Engine`): control characters, line and paragraph separators, and text-direction marks, embeddings, overrides and isolates become a backslash, `u`, and four hex digits. Ordinary text comes back as the same string. - `FileActivityLog`: the message is always one line. An exception's lines are indented under their entry, so no line of it can start like an entry. - CLI text modes: progress and failure lines, invalid-path notes, the sanitize note, the dry-run list, and the `repos` and `orgs` listings print repository-provided text through it. The JSON modes are unchanged; the JSON encoder already escapes. ## Why Closes #143. A file path in a git tree may contain a newline, an escape sequence, or a text-direction override, and such a path is exactly what gclo reports when it cannot be created on Windows. A newline started what read as a new log entry (the Log window's "Errors only" view groups lines by their timestamp prefix), and an escape sequence was obeyed by the terminal. ## Behaviour changes worth a release note - Log format: the lines of an exception are indented by three spaces. Anything that parses the log by "a line starting with a timestamp is an entry" keeps working, and is now correct. - CLI text output shows such characters as visible escapes instead of emitting them. ## How it was tested - `gclo.Engine.Tests`: 733 passed (was 703). New `DisplayTextTests`; log tests for a message and an exception whose text imitates another entry. - `gclo.Cli.Tests`: 144 passed (was 142). Text and quiet modes print the visible form on stderr and stdout; the record for the JSON document keeps the original text. - Line coverage 100% on `gclo.Engine`, `gclo.ViewModels`, `gclo`. Zero-warning x64 build with locked restore; `dotnet format --verify-no-changes` clean. Not run: the UI suite (the Log window reads the same file; its filter is unchanged). Not covered by a test of their own: the `repos` and `orgs` listings and the dry-run list with a hostile name (they call the same helper; names there come from the server). ## Checklist - [x] Commits are signed and signed off (`git commit -S -s`) - [x] Zero build warnings; format and lint are clean - [x] Tests added or updated; the 100% coverage gate still passes - [x] Docs updated for user-visible changes; threat model updated if the attack surface changed (`docs/CLI.md` activity log; threat model T15) - [x] No secrets, credentials, or personal data - [x] New dependencies are justified below, or none were added (none) - [x] Substantial AI assistance, if any, is noted here: written with Claude Code (Claude Fable 5.2) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-10 19:38:29 +00:00
Text that comes from a repository reaches the activity log and, in the
CLI's text modes, the terminal. A file path in a git tree may contain a
newline, an escape sequence or a text-direction override, and such a path
is exactly what gclo reports when it cannot be created on Windows. A
newline started what read as a new log entry, and an escape sequence was
obeyed by the terminal.

- New DisplayText.Escape turns control characters, line and paragraph
  separators and direction marks, embeddings, overrides and isolates into
  visible \uXXXX text; ordinary text comes back as the same string.
- FileActivityLog writes the message on one line, and an exception's
  lines indented under their entry, so no line of it can start like an
  entry (the Log window's error filter groups lines by that prefix).
- CLI text output (progress and failure lines, path notes, dry run, the
  repos and orgs listings) prints repository-provided text through it.
  The JSON modes are unchanged: the JSON encoder already escapes.

Threat model: T15 added. docs/CLI.md describes the log format.

Closes #143

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
james.maes added 1 commit 2026-11-10 20:00:25 +00:00
Follow-up to the independent review of this branch.

- What reaches a terminal is bytes in the console's code page, and a legacy
  code page turns some printable characters into control bytes (on code
  page 437 a left arrow becomes ESC), after the escaping has passed them.
  The CLI now writes every line through one place, Terminal, which also
  shows any character the console's encoding cannot represent in visible
  form. A test encodes every character of the basic plane for code pages
  437, 850, 1252 and 932 and fails on any control byte.
- That one place replaces the per-call escapes, so the messages the first
  commit missed are covered too: unexpected and usage errors, account,
  organization and target names, the sync summary. A test fails if any
  other CLI file writes to the console.
- DisplayText decides per code point by Unicode category: control, line
  and paragraph separator, and format characters (direction marks and
  overrides including the Arabic letter mark, invisible characters, tag
  characters) and unpaired surrogates become visible; the zero-width
  joiner and non-joiner stay, since emoji and several scripts need them.
- No allocation per escaped character.
- Tests assert the visible form with its backslash. The threat model says
  what is covered (log and CLI) and what is not (the desktop app's own
  surfaces, look-alike text).

Refs #143

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Author
Owner

Independent review of the first commit (a second reviewer session, read-only, with its own experiments) found one blocker and several gaps; addressed in 23d3fdf.

  • Blocker, fixed: what reaches a terminal is bytes in the console's code page, and on a legacy code page (437, 850) Windows turns some ordinary printable characters into control bytes after the escaping has passed them (a left arrow becomes ESC). The CLI now writes every line through one place, Terminal, which also shows any character the console's encoding cannot represent in visible form. A test encodes every character of the basic plane for four code pages and fails on any control byte.
  • Fixed: the first commit escaped 13 call sites and missed the rest (unexpected and usage errors, account and organization names, the sync summary). The single output point covers them, and a test fails if any other CLI file writes to the console.
  • Fixed: escaping is now per code point by Unicode category, so the Arabic letter mark, invisible format characters and tag characters (which sit outside the basic plane) are shown too; the zero-width joiner and non-joiner stay, since emoji and several scripts need them.
  • Fixed: tests assert the visible form with its backslash; no allocation per escaped character.

Not changed, by decision: a path that already contains a literal backslash-u sequence reads the same as an escaped character. Doubling backslashes would make every Windows path in the log unreadable, and nothing reads the log back.

Noted for a later change: the desktop app's own surfaces (row error text, the path-recovery dialog) are not covered by this pull request, and the Log window's error filter looks for the level anywhere in the line. The threat model now says the first.

Gates after the second commit: 745 engine tests, 163 CLI tests, 100% line coverage, zero-warning locked build, format clean.

Independent review of the first commit (a second reviewer session, read-only, with its own experiments) found one blocker and several gaps; addressed in 23d3fdf. - **Blocker, fixed:** what reaches a terminal is bytes in the console's code page, and on a legacy code page (437, 850) Windows turns some ordinary printable characters into control bytes after the escaping has passed them (a left arrow becomes ESC). The CLI now writes every line through one place, `Terminal`, which also shows any character the console's encoding cannot represent in visible form. A test encodes every character of the basic plane for four code pages and fails on any control byte. - **Fixed:** the first commit escaped 13 call sites and missed the rest (unexpected and usage errors, account and organization names, the sync summary). The single output point covers them, and a test fails if any other CLI file writes to the console. - **Fixed:** escaping is now per code point by Unicode category, so the Arabic letter mark, invisible format characters and tag characters (which sit outside the basic plane) are shown too; the zero-width joiner and non-joiner stay, since emoji and several scripts need them. - **Fixed:** tests assert the visible form with its backslash; no allocation per escaped character. Not changed, by decision: a path that already contains a literal backslash-u sequence reads the same as an escaped character. Doubling backslashes would make every Windows path in the log unreadable, and nothing reads the log back. Noted for a later change: the desktop app's own surfaces (row error text, the path-recovery dialog) are not covered by this pull request, and the Log window's error filter looks for the level anywhere in the line. The threat model now says the first. Gates after the second commit: 745 engine tests, 163 CLI tests, 100% line coverage, zero-warning locked build, format clean.
james.maes merged commit 9ccff603c8 into main 2026-11-10 20:01:00 +00:00
Sign in to join this conversation.