From 97b07968a0a239c862309bcdffe31848dfbf128c Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 14 Sep 2026 20:11:26 +0200 Subject: plan 8: escape terminal controls in everything krino prints --- cmd/krino/display_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 cmd/krino/display_test.go (limited to 'cmd/krino/display_test.go') diff --git a/cmd/krino/display_test.go b/cmd/krino/display_test.go new file mode 100644 index 0000000..4c1cefe --- /dev/null +++ b/cmd/krino/display_test.go @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package main + +import ( + "fmt" + "strings" + "testing" + "unicode/utf8" +) + +// firstUnsafe names the first thing in s a terminal could act on - a C0 +// control (a newline too, unless allowNewline), DEL, a C1 control, a +// bidirectional control, or invalid UTF-8 - or returns "" when there is +// none. +func firstUnsafe(s string, allowNewline bool) string { + if !utf8.ValidString(s) { + return "invalid UTF-8" + } + for _, r := range s { + if r == '\n' && allowNewline { + continue + } + if r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f) || (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) { + return fmt.Sprintf("%U", r) + } + } + return "" +} + +// TestDisplayEscapesTerminalControls: display shows every character a +// terminal would act on as an escape, and leaves ordinary text - Polish +// letters and a backslash included - exactly as it is. +func TestDisplayEscapesTerminalControls(t *testing.T) { + for in, want := range map[string]string{ + "plain name.pdf": "plain name.pdf", + "zażółć gęślą jaźń.pdf": "zażółć gęślą jaźń.pdf", + "esc\x1b[2Kx.pdf": `esc\x1b[2Kx.pdf`, + "bell\a.pdf": `bell\x07.pdf`, + "cr\rline.pdf": `cr\x0dline.pdf`, + "new\nline.pdf": `new\x0aline.pdf`, + "tab\t.pdf": `tab\x09.pdf`, + "del\x7f.pdf": `del\x7f.pdf`, + "c1\u009bcsi.pdf": `c1\u009bcsi.pdf`, + "bidi\u202egnp.pdf": `bidi\u202egnp.pdf`, + "isolate\u2066x\u2069.pdf": `isolate\u2066x\u2069.pdf`, + "bad\xffbyte.pdf": `bad\xffbyte.pdf`, + `back\slash.pdf`: `back\slash.pdf`, + "replacement\ufffd.pdf": "replacement\ufffd.pdf", + } { + if got := display(in); got != want { + t.Errorf("display(%q) = %q, want %q", in, got, want) + } + } +} + +// FuzzDisplay: whatever a name holds, display's result holds nothing a +// terminal could act on. +func FuzzDisplay(f *testing.F) { + f.Add("esc\x1b[2K\u202e\xff\x00") + f.Add("plain") + f.Fuzz(func(t *testing.T, s string) { + if bad := firstUnsafe(display(s), false); bad != "" { + t.Fatalf("display(%q) = %q still holds %s", s, display(s), bad) + } + }) +} + +// TestReviewEscapesHostileNames: the per-file header and the delete +// confirmation show a hostile name escaped, in review and in undo's review. +func TestReviewEscapesHostileNames(t *testing.T) { + out := new(strings.Builder) + if _, _, _, err := reviewChains(strings.NewReader("cdnn"), out, chains("esc\x1b[2Kx.pdf"), "", palette{}); err != nil { + t.Fatal(err) + } + if bad := firstUnsafe(out.String(), true); bad != "" { + t.Errorf("review printed %s:\n%q", bad, out) + } + undoOut := new(strings.Builder) + if _, _, err := reviewUndoFiles(strings.NewReader("cn"), undoOut, undoFiles("esc\x1b[2Kx.pdf"), palette{}); err != nil { + t.Fatal(err) + } + if bad := firstUnsafe(undoOut.String(), true); bad != "" { + t.Errorf("undo review printed %s:\n%q", bad, undoOut) + } +} -- cgit v1.3