forked from TrueCloudLab/restic
Merge pull request #4192 from greatroar/quote
ui/termstatus: Quote funny filenames
This commit is contained in:
commit
fd3ed9e2f4
4 changed files with 68 additions and 8 deletions
13
changelog/unreleased/issue-2260
Normal file
13
changelog/unreleased/issue-2260
Normal 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
|
|
@ -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)",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue