Skip to content

Pin Sanitize's substitution and what it leaves alone #119

Description

@kikashy

Goal

Add focused tests for Sanitize in internal/display/sanitize.go.

Why

// Sanitize removes terminal controls and bidi overrides from untrusted labels.
func Sanitize(value string) string {
	var output strings.Builder
	for _, char := range value {
		if unicode.IsControl(char) || unicode.Is(unicode.Cf, char) || unicode.Is(unicode.Zl, char) || unicode.Is(unicode.Zp, char) {
			output.WriteRune('?')
			continue
		}
		output.WriteRune(char)
	}
	return output.String()
}

This is the one place the runtime neutralizes text it did not write. display.Sanitize appears on 296 lines across 20 files, among them the human renderer (internal/cli/render.go), the MCP tool surface (internal/mcp/tools.go), and the carrier decoder (internal/carrier/decode.go). The strings handed to it are pack ids, decision ids, reasons, fact pointers, and diagnostic text — authored elsewhere, so untrusted whenever someone runs jpack against a pack they did not write.

It has one test, and Sanitize sits at 100% statement coverage. Those two facts are not in tension. internal/display/sanitize_test.go is twenty lines and holds a single case that feeds one string and then re-checks the same four Unicode predicates the function itself uses; full coverage of a line says the line ran, not that anything about it is held in place. What that test pins is the class of character removed, and nothing else around it:

  • ESC (U+001B), the introducer of an ANSI escape sequence and the most common terminal-injection vector, does not appear anywhere in the test file — nor in any test file in the repository;
  • nothing pins what a removed character becomes — only that it is no longer a control, so replacing '?' with anything, or with nothing, still passes;
  • nothing pins one-rune-in-one-rune-out, so a change that dropped characters instead of substituting them would pass;
  • U+2029 (Zp) appears nowhere, so that predicate can be deleted from the if with the whole suite still green;
  • nothing pins the far side of the boundary. The existing case does show the ASCII runs safe and text surviving, but nothing shows that an ordinary space, or a non-breaking space (U+00A0, category Zs), survives unchanged — which is what keeps a sanitized label readable rather than mangled.

Behaviour today, for the cases worth pinning:

"\x1b[31mred"  -> "?[31mred"     // the introducer goes; the rest is inert text
"a\tb\nc"      -> "a?b?c"        // a label cannot open a new line
"\x7f"         -> "?"            // DEL
"a b"          -> "a b"          // an ordinary space survives
"a b"     -> "a b"     // a non-breaking space survives — Zs, not Cf
"\U0001F600"   -> "\U0001F600"   // a multi-byte rune survives

Scope

  • Change internal/display/sanitize_test.go only.
  • Table-driven, t.Run per case, asserting the exact output string rather than a property of it. Cover at least: ESC and a full ANSI colour sequence; tab, newline and carriage return; DEL (U+007F); a C1 control (U+009B); a bidi override (U+202E, Cf); a line separator (U+2028, Zl); a paragraph separator (U+2029, Zp); plain ASCII; a string containing an ordinary space; a non-breaking space (U+00A0); a multi-byte character and an emoji; the empty string.
  • Add one case pinning that the output has the same rune count as the input.
  • Add one case pinning idempotency: Sanitize(Sanitize(s)) == Sanitize(s).
  • Do not change Sanitize, and do not change any caller. If the substitution character or the predicate list looks wrong, open a separate issue rather than editing it here.

Acceptance criteria

  • Every case asserts the exact expected string.
  • ESC (U+001B) has a case, and that case shows what survives it.
  • The substitution character is pinned by at least one case.
  • At least one case shows a character the function deliberately leaves alone.
  • Rune count and idempotency each have a case.
  • Mutation check, run locally before opening the pull request and reported in it: changing '?' to another rune fails a case in your new file, and deleting any one of the four predicates from the if fails a case in your new file. Both of those pass the whole suite as it stands today — and deleting the unicode.Zp predicate is caught by nothing at all — so a criterion met only by the existing test does not count.
  • env GO111MODULE=on go test ./internal/display and env GO111MODULE=on go test ./... pass; gofmt -l . prints nothing; env GO111MODULE=on go vet ./... is clean.
  • Every commit includes a DCO sign-off created with git commit -s.

Contributor learning

Why a tool that prints text it did not author routes all of it through one sanitizer instead of trusting call sites; how to pin a transformation by its output instead of restating its implementation in the assertion; and why 100% statement coverage said nothing about either.

Metadata

Metadata

Assignees

No one assigned

    Labels

    goPull requests that update go codegood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions