Repository: hashicorp/go-hclog
Pull request #119 (2022-09-16 21:35:41)

When rendering multiple line output, still quote the individual lines
This also pulls over a specialized version of strconv.Quote for us to use.


Patch:
diff --git a/intlogger.go b/intlogger.go
index 9312f3c..e4cd8ed 100644
--- a/intlogger.go
+++ b/intlogger.go
@@ -17,6 +17,8 @@ import (
 	"sync"
 	"sync/atomic"
 	"time"
+	"unicode"
+	"unicode/utf8"
 
 	"github.com/fatih/color"
 )
@@ -420,7 +422,9 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string,
 				} else {
 					l.writer.WriteByte('=')
 				}
-				l.writer.WriteString(strconv.Quote(val))
+				l.writer.WriteByte('"')
+				writeEscapedForOutput(l.writer, val, true)
+				l.writer.WriteByte('"')
 			} else {
 				l.writer.WriteByte(' ')
 				l.writer.WriteString(key)
@@ -448,19 +452,98 @@ func writeIndent(w *writer, str string, indent string) {
 		if nl == -1 {
 			if str != "" {
 				w.WriteString(indent)
-				w.WriteString(str)
+				writeEscapedForOutput(w, str, false)
 				w.WriteString("\n")
 			}
 			return
 		}
 
 		w.WriteString(indent)
-		w.WriteString(str[:nl])
+		writeEscapedForOutput(w, str[:nl], false)
 		w.WriteString("\n")
 		str = str[nl+1:]
 	}
 }
 
+func needsEscaping(str string) bool {
+	for _, b := range str {
+		if !unicode.IsPrint(b) || b == '"' {
+			return true
+		}
+	}
+
+	return false
+}
+
+const (
+	lowerhex = "0123456789abcdef"
+)
+
+var bufPool = sync.Pool{
+	New: func() interface{} {
+		return new(bytes.Buffer)
+	},
+}
+
+func writeEscapedForOutput(w io.Writer, str string, escapeQuotes bool) {
+	if !needsEscaping(str) {
+		w.Write([]byte(str))
+		return
+	}
+
+	bb := bufPool.Get().(*bytes.Buffer)
+	bb.Reset()
+
+	defer bufPool.Put(bb)
+
+	for _, r := range str {
+		if escapeQuotes && r == '"' {
+			bb.WriteString(`\"`)
+		} else if unicode.IsPrint(r) {
+			bb.WriteRune(r)
+		} else {
+			switch r {
+			case '\a':
+				bb.WriteString(`\a`)
+			case '\b':
+				bb.WriteString(`\b`)
+			case '\f':
+				bb.WriteString(`\f`)
+			case '\n':
+				bb.WriteString(`\n`)
+			case '\r':
+				bb.WriteString(`\r`)
+			case '\t':
+				bb.WriteString(`\t`)
+			case '\v':
+				bb.WriteString(`\v`)
+			default:
+				switch {
+				case r < ' ':
+					bb.WriteString(`\x`)
+					bb.WriteByte(lowerhex[byte(r)>>4])
+					bb.WriteByte(lowerhex[byte(r)&0xF])
+				case !utf8.ValidRune(r):
+					r = 0xFFFD
+					fallthrough
+				case r < 0x10000:
+					bb.WriteString(`\u`)
+					for s := 12; s >= 0; s -= 4 {
+						bb.WriteByte(lowerhex[r>>uint(s)&0xF])
+					}
+				default:
+					bb.WriteString(`\U`)
+					for s := 28; s >= 0; s -= 4 {
+						bb.WriteByte(lowerhex[r>>uint(s)&0xF])
+					}
+				}
+			}
+		}
+	}
+
+	w.Write(bb.Bytes())
+}
+
 func (l *intLogger) renderSlice(v reflect.Value) string {
 	var buf bytes.Buffer
 


Test patch:
diff --git a/logger_test.go b/logger_test.go
index 4c419ed..22b3f72 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -176,6 +176,28 @@ func TestLogger(t *testing.T) {
 		assert.Equal(t, expected, rest)
 	})
 
+	t.Run("handles backslash r in entries", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:   "test",
+			Output: &buf,
+		})
+
+		logger.Info("this is test", "who", "programmer", "why", "testing\n\rand other\n\rpretty cool things like \x01 and \u1680 and \U00101120")
+
+		str := buf.String()
+		dataIdx := strings.IndexByte(str, ' ')
+		rest := str[dataIdx+1:]
+
+		expected := `[INFO]  test: this is test: who=programmer
+  why=
+  | testing
+  | \rand other
+  | \rpretty cool things like \x01 and \u1680 and \U00101120` + "\n  \n"
+		assert.Equal(t, expected, rest)
+	})
+
 	t.Run("outputs stack traces", func(t *testing.T) {
 		var buf bytes.Buffer