ui/termstatus: Quote funny filenames

Fixes #2260, #4191.
This commit is contained in:
greatroar 2023-02-11 14:51:58 +01:00 committed by Michael Eischer
parent 07a44a88f2
commit f342db7666
4 changed files with 68 additions and 8 deletions

View File

@ -0,0 +1,13 @@
Bugfix: Exotic filenames no longer break restic backup's status output
Restic backup shows the names of files that it is working on. In previous
versions of restic, those names were printed without first sanitizing them,
so that filenames containing newlines or terminal control characters could
mess up restic backup's output or even change the state of a terminal.
Filenames are now checked and quoted if they contain non-printable or
non-Unicode characters.
https://github.com/restic/restic/issues/2260
https://github.com/restic/restic/issues/4191
https://github.com/restic/restic/pull/4192

View File

@ -86,6 +86,8 @@ func (b *TextProgress) Error(item string, err error) error {
// CompleteItem is the status callback function for the archiver when a // CompleteItem is the status callback function for the archiver when a
// file/dir has been saved successfully. // file/dir has been saved successfully.
func (b *TextProgress) CompleteItem(messageType, item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration) { func (b *TextProgress) CompleteItem(messageType, item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration) {
item = termstatus.Quote(item)
switch messageType { switch messageType {
case "dir new": case "dir new":
b.VV("new %v, saved in %.3fs (%v added, %v stored, %v metadata)", b.VV("new %v, saved in %.3fs (%v added, %v stored, %v metadata)",

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"strings" "strings"
"unicode" "unicode"
@ -325,6 +326,7 @@ func wideRune(r rune) bool {
} }
// SetStatus updates the status lines. // SetStatus updates the status lines.
// The lines should not contain newlines; this method adds them.
func (t *Terminal) SetStatus(lines []string) { func (t *Terminal) SetStatus(lines []string) {
if len(lines) == 0 { if len(lines) == 0 {
return return
@ -341,21 +343,34 @@ func (t *Terminal) SetStatus(lines []string) {
} }
} }
// make sure that all lines have a line break and are not too long // Sanitize lines and truncate them if they're too long.
for i, line := range lines { for i, line := range lines {
line = strings.TrimRight(line, "\n") line = Quote(line)
if width > 0 { if width > 0 {
line = Truncate(line, width-2) line = Truncate(line, width-2)
} }
lines[i] = line + "\n" if i < len(lines)-1 { // Last line gets no line break.
lines[i] = line + "\n"
}
} }
// make sure the last line does not have a line break
last := len(lines) - 1
lines[last] = strings.TrimRight(lines[last], "\n")
select { select {
case t.status <- status{lines: lines}: case t.status <- status{lines: lines}:
case <-t.closed: case <-t.closed:
} }
} }
// Quote lines with funny characters in them, meaning control chars, newlines,
// tabs, anything else non-printable and invalid UTF-8.
//
// This is intended to produce a string that does not mess up the terminal
// rather than produce an unambiguous quoted string.
func Quote(line string) string {
for _, r := range line {
// The replacement character usually means the input is not UTF-8.
if r == unicode.ReplacementChar || !unicode.IsPrint(r) {
return strconv.Quote(line)
}
}
return line
}

View File

@ -1,6 +1,36 @@
package termstatus package termstatus
import "testing" import (
"strconv"
"testing"
rtest "github.com/restic/restic/internal/test"
)
func TestQuote(t *testing.T) {
for _, c := range []struct {
in string
needQuote bool
}{
{"foo.bar/baz", false},
{"föó_bàŕ-bãẑ", false},
{" foo ", false},
{"foo bar", false},
{"foo\nbar", true},
{"foo\rbar", true},
{"foo\abar", true},
{"\xff", true},
{`c:\foo\bar`, false},
// Issue #2260: terminal control characters.
{"\x1bm_red_is_beautiful", true},
} {
if c.needQuote {
rtest.Equals(t, strconv.Quote(c.in), Quote(c.in))
} else {
rtest.Equals(t, c.in, Quote(c.in))
}
}
}
func TestTruncate(t *testing.T) { func TestTruncate(t *testing.T) {
var tests = []struct { var tests = []struct {