Repository navigation
Expand file tree
/
Copy pathencode.go
More file actions
100 lines (93 loc) · 2.92 KB
/
Copy pathencode.go
File metadata and controls
100 lines (93 loc) · 2.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package log
import (
"log/slog"
"strconv"
"unicode"
"unicode/utf8"
)
// EncodeValue renders s for a key=value stream. It is exported because it
// is the format contract, not an implementation detail: any handler that
// emits these records must quote by the same rule or the two drift apart.
//
// A bare token stays unambiguous only while it holds no separator: a space
// ends the field, '=' opens a new one and '"' starts a quoted run, so
// reason=email already exists reads back as reason="email" followed by two
// stray tokens. An empty value is quoted too, otherwise note= and a bare
// trailing note are the same bytes.
//
// Anything else goes through strconv.Quote, which escapes \n and \r as well.
// That is the part that matters most: an unescaped newline lets a value close
// the record it belongs to and forge the next one, so a user-supplied string
// can mint a log line that looks exactly like the service's own.
// strconv.Quote leaves printable non-ASCII alone, so accents and symbols
// survive intact, and escapes invalid UTF-8 rather than emitting it raw.
func EncodeValue(s string) string {
if s == "" {
return `""`
}
if !needsQuote(s) {
return s
}
return strconv.Quote(s)
}
// EncodeKey applies the same rules to a key. Keys are usually well-behaved
// literals, but they come from variadic call sites and reach here unchecked.
func EncodeKey(s string) string {
return EncodeValue(s)
}
func needsQuote(s string) bool {
if !utf8.ValidString(s) {
return true
}
for _, r := range s {
if r == '"' || r == '\\' || r == '=' ||
unicode.IsSpace(r) || !strconv.IsPrint(r) {
return true
}
}
return false
}
// EncodeMessage keeps the positional message readable — spaces and
// punctuation belong there and cannot be confused with a key=value pair —
// but quotes it whole once it carries a control character, so a message can
// no more forge a second record than an attribute value can.
func EncodeMessage(s string) string {
if s == "" {
return s
}
if !utf8.ValidString(s) {
return strconv.Quote(s)
}
for _, r := range s {
if !strconv.IsPrint(r) && r != ' ' {
return strconv.Quote(s)
}
}
return s
}
// EncodeAttrValue renders one attribute value: the per-kind formatting first,
// then the quoting rule. Both handlers go through here so the same log call
// reads identically on a terminal and in Loki.
func EncodeAttrValue(v slog.Value) string {
return EncodeValue(formatValue(v))
}
func formatValue(v slog.Value) string {
switch v.Kind() {
case slog.KindString:
return v.String()
case slog.KindBool:
return strconv.FormatBool(v.Bool())
case slog.KindInt64:
return strconv.FormatInt(v.Int64(), 10)
case slog.KindUint64:
return strconv.FormatUint(v.Uint64(), 10)
case slog.KindFloat64:
return strconv.FormatFloat(v.Float64(), 'f', -1, 64)
case slog.KindDuration:
return v.Duration().String()
case slog.KindTime:
return v.Time().Format(TerminalTimeFormat)
default:
return v.String()
}
}