1// Copyright 2022 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package slog
6
7import (
8	"bytes"
9	"context"
10	"encoding/json"
11	"errors"
12	"fmt"
13	"io"
14	"log/slog/internal/buffer"
15	"strconv"
16	"sync"
17	"time"
18	"unicode/utf8"
19)
20
21// JSONHandler is a [Handler] that writes Records to an [io.Writer] as
22// line-delimited JSON objects.
23type JSONHandler struct {
24	*commonHandler
25}
26
27// NewJSONHandler creates a [JSONHandler] that writes to w,
28// using the given options.
29// If opts is nil, the default options are used.
30func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler {
31	if opts == nil {
32		opts = &HandlerOptions{}
33	}
34	return &JSONHandler{
35		&commonHandler{
36			json: true,
37			w:    w,
38			opts: *opts,
39			mu:   &sync.Mutex{},
40		},
41	}
42}
43
44// Enabled reports whether the handler handles records at the given level.
45// The handler ignores records whose level is lower.
46func (h *JSONHandler) Enabled(_ context.Context, level Level) bool {
47	return h.commonHandler.enabled(level)
48}
49
50// WithAttrs returns a new [JSONHandler] whose attributes consists
51// of h's attributes followed by attrs.
52func (h *JSONHandler) WithAttrs(attrs []Attr) Handler {
53	return &JSONHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
54}
55
56func (h *JSONHandler) WithGroup(name string) Handler {
57	return &JSONHandler{commonHandler: h.commonHandler.withGroup(name)}
58}
59
60// Handle formats its argument [Record] as a JSON object on a single line.
61//
62// If the Record's time is zero, the time is omitted.
63// Otherwise, the key is "time"
64// and the value is output as with json.Marshal.
65//
66// If the Record's level is zero, the level is omitted.
67// Otherwise, the key is "level"
68// and the value of [Level.String] is output.
69//
70// If the AddSource option is set and source information is available,
71// the key is "source", and the value is a record of type [Source].
72//
73// The message's key is "msg".
74//
75// To modify these or other attributes, or remove them from the output, use
76// [HandlerOptions.ReplaceAttr].
77//
78// Values are formatted as with an [encoding/json.Encoder] with SetEscapeHTML(false),
79// with two exceptions.
80//
81// First, an Attr whose Value is of type error is formatted as a string, by
82// calling its Error method. Only errors in Attrs receive this special treatment,
83// not errors embedded in structs, slices, maps or other data structures that
84// are processed by the [encoding/json] package.
85//
86// Second, an encoding failure does not cause Handle to return an error.
87// Instead, the error message is formatted as a string.
88//
89// Each call to Handle results in a single serialized call to io.Writer.Write.
90func (h *JSONHandler) Handle(_ context.Context, r Record) error {
91	return h.commonHandler.handle(r)
92}
93
94// Adapted from time.Time.MarshalJSON to avoid allocation.
95func appendJSONTime(s *handleState, t time.Time) {
96	if y := t.Year(); y < 0 || y >= 10000 {
97		// RFC 3339 is clear that years are 4 digits exactly.
98		// See golang.org/issue/4556#c15 for more discussion.
99		s.appendError(errors.New("time.Time year outside of range [0,9999]"))
100	}
101	s.buf.WriteByte('"')
102	*s.buf = t.AppendFormat(*s.buf, time.RFC3339Nano)
103	s.buf.WriteByte('"')
104}
105
106func appendJSONValue(s *handleState, v Value) error {
107	switch v.Kind() {
108	case KindString:
109		s.appendString(v.str())
110	case KindInt64:
111		*s.buf = strconv.AppendInt(*s.buf, v.Int64(), 10)
112	case KindUint64:
113		*s.buf = strconv.AppendUint(*s.buf, v.Uint64(), 10)
114	case KindFloat64:
115		// json.Marshal is funny about floats; it doesn't
116		// always match strconv.AppendFloat. So just call it.
117		// That's expensive, but floats are rare.
118		if err := appendJSONMarshal(s.buf, v.Float64()); err != nil {
119			return err
120		}
121	case KindBool:
122		*s.buf = strconv.AppendBool(*s.buf, v.Bool())
123	case KindDuration:
124		// Do what json.Marshal does.
125		*s.buf = strconv.AppendInt(*s.buf, int64(v.Duration()), 10)
126	case KindTime:
127		s.appendTime(v.Time())
128	case KindAny:
129		a := v.Any()
130		_, jm := a.(json.Marshaler)
131		if err, ok := a.(error); ok && !jm {
132			s.appendString(err.Error())
133		} else {
134			return appendJSONMarshal(s.buf, a)
135		}
136	default:
137		panic(fmt.Sprintf("bad kind: %s", v.Kind()))
138	}
139	return nil
140}
141
142func appendJSONMarshal(buf *buffer.Buffer, v any) error {
143	// Use a json.Encoder to avoid escaping HTML.
144	var bb bytes.Buffer
145	enc := json.NewEncoder(&bb)
146	enc.SetEscapeHTML(false)
147	if err := enc.Encode(v); err != nil {
148		return err
149	}
150	bs := bb.Bytes()
151	buf.Write(bs[:len(bs)-1]) // remove final newline
152	return nil
153}
154
155// appendEscapedJSONString escapes s for JSON and appends it to buf.
156// It does not surround the string in quotation marks.
157//
158// Modified from encoding/json/encode.go:encodeState.string,
159// with escapeHTML set to false.
160func appendEscapedJSONString(buf []byte, s string) []byte {
161	char := func(b byte) { buf = append(buf, b) }
162	str := func(s string) { buf = append(buf, s...) }
163
164	start := 0
165	for i := 0; i < len(s); {
166		if b := s[i]; b < utf8.RuneSelf {
167			if safeSet[b] {
168				i++
169				continue
170			}
171			if start < i {
172				str(s[start:i])
173			}
174			char('\\')
175			switch b {
176			case '\\', '"':
177				char(b)
178			case '\n':
179				char('n')
180			case '\r':
181				char('r')
182			case '\t':
183				char('t')
184			default:
185				// This encodes bytes < 0x20 except for \t, \n and \r.
186				str(`u00`)
187				char(hex[b>>4])
188				char(hex[b&0xF])
189			}
190			i++
191			start = i
192			continue
193		}
194		c, size := utf8.DecodeRuneInString(s[i:])
195		if c == utf8.RuneError && size == 1 {
196			if start < i {
197				str(s[start:i])
198			}
199			str(`\ufffd`)
200			i += size
201			start = i
202			continue
203		}
204		// U+2028 is LINE SEPARATOR.
205		// U+2029 is PARAGRAPH SEPARATOR.
206		// They are both technically valid characters in JSON strings,
207		// but don't work in JSONP, which has to be evaluated as JavaScript,
208		// and can lead to security holes there. It is valid JSON to
209		// escape them, so we do so unconditionally.
210		// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
211		if c == '\u2028' || c == '\u2029' {
212			if start < i {
213				str(s[start:i])
214			}
215			str(`\u202`)
216			char(hex[c&0xF])
217			i += size
218			start = i
219			continue
220		}
221		i += size
222	}
223	if start < len(s) {
224		str(s[start:])
225	}
226	return buf
227}
228
229const hex = "0123456789abcdef"
230
231// Copied from encoding/json/tables.go.
232//
233// safeSet holds the value true if the ASCII character with the given array
234// position can be represented inside a JSON string without any further
235// escaping.
236//
237// All values are true except for the ASCII control characters (0-31), the
238// double quote ("), and the backslash character ("\").
239var safeSet = [utf8.RuneSelf]bool{
240	' ':      true,
241	'!':      true,
242	'"':      false,
243	'#':      true,
244	'$':      true,
245	'%':      true,
246	'&':      true,
247	'\'':     true,
248	'(':      true,
249	')':      true,
250	'*':      true,
251	'+':      true,
252	',':      true,
253	'-':      true,
254	'.':      true,
255	'/':      true,
256	'0':      true,
257	'1':      true,
258	'2':      true,
259	'3':      true,
260	'4':      true,
261	'5':      true,
262	'6':      true,
263	'7':      true,
264	'8':      true,
265	'9':      true,
266	':':      true,
267	';':      true,
268	'<':      true,
269	'=':      true,
270	'>':      true,
271	'?':      true,
272	'@':      true,
273	'A':      true,
274	'B':      true,
275	'C':      true,
276	'D':      true,
277	'E':      true,
278	'F':      true,
279	'G':      true,
280	'H':      true,
281	'I':      true,
282	'J':      true,
283	'K':      true,
284	'L':      true,
285	'M':      true,
286	'N':      true,
287	'O':      true,
288	'P':      true,
289	'Q':      true,
290	'R':      true,
291	'S':      true,
292	'T':      true,
293	'U':      true,
294	'V':      true,
295	'W':      true,
296	'X':      true,
297	'Y':      true,
298	'Z':      true,
299	'[':      true,
300	'\\':     false,
301	']':      true,
302	'^':      true,
303	'_':      true,
304	'`':      true,
305	'a':      true,
306	'b':      true,
307	'c':      true,
308	'd':      true,
309	'e':      true,
310	'f':      true,
311	'g':      true,
312	'h':      true,
313	'i':      true,
314	'j':      true,
315	'k':      true,
316	'l':      true,
317	'm':      true,
318	'n':      true,
319	'o':      true,
320	'p':      true,
321	'q':      true,
322	'r':      true,
323	's':      true,
324	't':      true,
325	'u':      true,
326	'v':      true,
327	'w':      true,
328	'x':      true,
329	'y':      true,
330	'z':      true,
331	'{':      true,
332	'|':      true,
333	'}':      true,
334	'~':      true,
335	'\u007f': true,
336}
337