Skip to content

Commit 480ace9

Browse files
authored
Merge pull request #96 from hashicorp/f-quote
add Quote type to enable safe concise output of untrusted strings
2 parents f07802e + 2d22fae commit 480ace9

File tree

3 files changed

+85
-7
lines changed

3 files changed

+85
-7
lines changed

intlogger.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string,
295295
continue FOR
296296
case Format:
297297
val = fmt.Sprintf(st[0].(string), st[1:]...)
298+
case Quote:
299+
raw = true
300+
val = strconv.Quote(string(st))
298301
default:
299302
v := reflect.ValueOf(st)
300303
if v.Kind() == reflect.Slice {

logger.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ type Octal int
6767
// text output. For example: L.Info("bits", Binary(17))
6868
type Binary int
6969

70+
// A simple shortcut to format strings with Go quoting. Control and
71+
// non-printable characters will be escaped with their backslash equivalents in
72+
// output. Intended for untrusted or multiline strings which should be logged
73+
// as concisely as possible.
74+
type Quote string
75+
7076
// ColorOption expresses how the output should be colored, if at all.
7177
type ColorOption uint8
7278

logger_test.go

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,9 @@ func TestLogger(t *testing.T) {
184184
}
185185

186186
logger := New(&LoggerOptions{
187-
Name: "test",
188-
Output: &buf,
189-
IncludeLocation: true,
187+
Name: "test",
188+
Output: &buf,
189+
IncludeLocation: true,
190190
AdditionalLocationOffset: 1,
191191
})
192192

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

417+
t.Run("supports quote formatting", func(t *testing.T) {
418+
var buf bytes.Buffer
419+
420+
logger := New(&LoggerOptions{
421+
Name: "test",
422+
Output: &buf,
423+
})
424+
425+
// unsafe is a string containing control characters and a byte
426+
// sequence which is invalid utf8 ("\xFFa") to assert that all
427+
// characters are properly encoded and produce valid utf8 output
428+
unsafe := "foo\nbar\bbaz\xFFa"
429+
430+
logger.Info("this is test",
431+
"unquoted", "unquoted", "quoted", Quote("quoted"),
432+
"unsafeq", Quote(unsafe))
433+
434+
str := buf.String()
435+
dataIdx := strings.IndexByte(str, ' ')
436+
rest := str[dataIdx+1:]
437+
438+
assert.Equal(t, "[INFO] test: this is test: "+
439+
"unquoted=unquoted quoted=\"quoted\" "+
440+
"unsafeq=\"foo\\nbar\\bbaz\\xffa\"\n", rest)
441+
})
442+
417443
t.Run("supports resetting the output", func(t *testing.T) {
418444
var first, second bytes.Buffer
419445

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

833+
t.Run("ignores quote formatting requests", func(t *testing.T) {
834+
var buf bytes.Buffer
835+
836+
logger := New(&LoggerOptions{
837+
Name: "test",
838+
Output: &buf,
839+
JSONFormat: true,
840+
})
841+
842+
// unsafe is a string containing control characters and a byte
843+
// sequence which is invalid utf8 ("\xFFa") to assert that all
844+
// characters are properly encoded and produce valid json
845+
unsafe := "foo\nbar\bbaz\xFFa"
846+
847+
logger.Info("this is test",
848+
"unquoted", "unquoted", "quoted", Quote("quoted"),
849+
"unsafeq", Quote(unsafe), "unsafe", unsafe)
850+
851+
b := buf.Bytes()
852+
853+
// Assert the JSON only contains valid utf8 strings with the
854+
// illegal byte replaced with the utf8 replacement character,
855+
// and not invalid json with byte(255)
856+
// Note: testify/assert.Contains did not work here
857+
if needle := []byte(`\ufffda`); !bytes.Contains(b, needle) {
858+
t.Fatalf("could not find %q (%v) in json bytes: %q", needle, needle, b)
859+
}
860+
if needle := []byte{255, 'a'}; bytes.Contains(b, needle) {
861+
t.Fatalf("found %q (%v) in json bytes: %q", needle, needle, b)
862+
}
863+
864+
var raw map[string]interface{}
865+
if err := json.Unmarshal(b, &raw); err != nil {
866+
t.Fatal(err)
867+
}
868+
869+
assert.Equal(t, "this is test", raw["@message"])
870+
assert.Equal(t, "unquoted", raw["unquoted"])
871+
assert.Equal(t, "quoted", raw["quoted"])
872+
assert.Equal(t, "foo\nbar\bbaz\uFFFDa", raw["unsafe"])
873+
assert.Equal(t, "foo\nbar\bbaz\uFFFDa", raw["unsafeq"])
874+
})
875+
807876
t.Run("includes the caller location", func(t *testing.T) {
808877
var buf bytes.Buffer
809878

@@ -837,10 +906,10 @@ func TestLogger_JSON(t *testing.T) {
837906
}
838907

839908
logger := New(&LoggerOptions{
840-
Name: "test",
841-
Output: &buf,
842-
JSONFormat: true,
843-
IncludeLocation: true,
909+
Name: "test",
910+
Output: &buf,
911+
JSONFormat: true,
912+
IncludeLocation: true,
844913
AdditionalLocationOffset: 1,
845914
})
846915

0 commit comments

Comments
 (0)