Skip to content

Commit

Permalink
Merge pull request #96 from hashicorp/f-quote
Browse files Browse the repository at this point in the history
add Quote type to enable safe concise output of untrusted strings
  • Loading branch information
schmichael authored Jul 7, 2021
2 parents f07802e + 2d22fae commit 480ace9
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 7 deletions.
3 changes: 3 additions & 0 deletions intlogger.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string,
continue FOR
case Format:
val = fmt.Sprintf(st[0].(string), st[1:]...)
case Quote:
raw = true
val = strconv.Quote(string(st))
default:
v := reflect.ValueOf(st)
if v.Kind() == reflect.Slice {
Expand Down
6 changes: 6 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ type Octal int
// text output. For example: L.Info("bits", Binary(17))
type Binary int

// A simple shortcut to format strings with Go quoting. Control and
// non-printable characters will be escaped with their backslash equivalents in
// output. Intended for untrusted or multiline strings which should be logged
// as concisely as possible.
type Quote string

// ColorOption expresses how the output should be colored, if at all.
type ColorOption uint8

Expand Down
83 changes: 76 additions & 7 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ func TestLogger(t *testing.T) {
}

logger := New(&LoggerOptions{
Name: "test",
Output: &buf,
IncludeLocation: true,
Name: "test",
Output: &buf,
IncludeLocation: true,
AdditionalLocationOffset: 1,
})

Expand Down Expand Up @@ -414,6 +414,32 @@ func TestLogger(t *testing.T) {
assert.Equal(t, "[INFO] test: this is test: bytes=0xc perms=0755 bits=0b101\n", rest)
})

t.Run("supports quote formatting", func(t *testing.T) {
var buf bytes.Buffer

logger := New(&LoggerOptions{
Name: "test",
Output: &buf,
})

// unsafe is a string containing control characters and a byte
// sequence which is invalid utf8 ("\xFFa") to assert that all
// characters are properly encoded and produce valid utf8 output
unsafe := "foo\nbar\bbaz\xFFa"

logger.Info("this is test",
"unquoted", "unquoted", "quoted", Quote("quoted"),
"unsafeq", Quote(unsafe))

str := buf.String()
dataIdx := strings.IndexByte(str, ' ')
rest := str[dataIdx+1:]

assert.Equal(t, "[INFO] test: this is test: "+
"unquoted=unquoted quoted=\"quoted\" "+
"unsafeq=\"foo\\nbar\\bbaz\\xffa\"\n", rest)
})

t.Run("supports resetting the output", func(t *testing.T) {
var first, second bytes.Buffer

Expand Down Expand Up @@ -804,6 +830,49 @@ func TestLogger_JSON(t *testing.T) {
assert.Equal(t, float64(5), raw["bits"])
})

t.Run("ignores quote formatting requests", func(t *testing.T) {
var buf bytes.Buffer

logger := New(&LoggerOptions{
Name: "test",
Output: &buf,
JSONFormat: true,
})

// unsafe is a string containing control characters and a byte
// sequence which is invalid utf8 ("\xFFa") to assert that all
// characters are properly encoded and produce valid json
unsafe := "foo\nbar\bbaz\xFFa"

logger.Info("this is test",
"unquoted", "unquoted", "quoted", Quote("quoted"),
"unsafeq", Quote(unsafe), "unsafe", unsafe)

b := buf.Bytes()

// Assert the JSON only contains valid utf8 strings with the
// illegal byte replaced with the utf8 replacement character,
// and not invalid json with byte(255)
// Note: testify/assert.Contains did not work here
if needle := []byte(`\ufffda`); !bytes.Contains(b, needle) {
t.Fatalf("could not find %q (%v) in json bytes: %q", needle, needle, b)
}
if needle := []byte{255, 'a'}; bytes.Contains(b, needle) {
t.Fatalf("found %q (%v) in json bytes: %q", needle, needle, b)
}

var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
t.Fatal(err)
}

assert.Equal(t, "this is test", raw["@message"])
assert.Equal(t, "unquoted", raw["unquoted"])
assert.Equal(t, "quoted", raw["quoted"])
assert.Equal(t, "foo\nbar\bbaz\uFFFDa", raw["unsafe"])
assert.Equal(t, "foo\nbar\bbaz\uFFFDa", raw["unsafeq"])
})

t.Run("includes the caller location", func(t *testing.T) {
var buf bytes.Buffer

Expand Down Expand Up @@ -837,10 +906,10 @@ func TestLogger_JSON(t *testing.T) {
}

logger := New(&LoggerOptions{
Name: "test",
Output: &buf,
JSONFormat: true,
IncludeLocation: true,
Name: "test",
Output: &buf,
JSONFormat: true,
IncludeLocation: true,
AdditionalLocationOffset: 1,
})

Expand Down

0 comments on commit 480ace9

Please sign in to comment.