├── .gitignore ├── go.mod ├── go.sum ├── doc.go ├── decode-bench_test.go ├── LICENSE ├── .github └── workflows │ └── test.yml ├── example_test.go ├── README.md ├── CHANGELOG.md ├── jsonstring.go ├── decode.go ├── encode_test.go ├── decode_test.go ├── encode_internal_test.go └── encode.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-logfmt/logfmt 2 | 3 | go 1.21 4 | 5 | require github.com/google/go-cmp v0.7.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package logfmt implements utilities to marshal and unmarshal data in the 2 | // logfmt format. The logfmt format records key/value pairs in a way that 3 | // balances readability for humans and simplicity of computer parsing. It is 4 | // most commonly used as a more human friendly alternative to JSON for 5 | // structured logging. 6 | package logfmt 7 | -------------------------------------------------------------------------------- /decode-bench_test.go: -------------------------------------------------------------------------------- 1 | package logfmt 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkDecodeKeyval(b *testing.B) { 9 | const rows = 10000 10 | data := []byte{} 11 | for i := 0; i < rows; i++ { 12 | data = append(data, "a=1 b=\"bar\" ƒ=2h3s r=\"esc\\tmore stuff\" d x=sf \n"...) 13 | } 14 | 15 | b.SetBytes(int64(len(data))) 16 | b.ResetTimer() 17 | for i := 0; i < b.N; i++ { 18 | var ( 19 | dec = NewDecoder(bytes.NewReader(data)) 20 | j = 0 21 | ) 22 | for dec.ScanRecord() { 23 | for dec.ScanKeyval() { 24 | } 25 | j++ 26 | } 27 | if err := dec.Err(); err != nil { 28 | b.Errorf("got %v, want %v", err, nil) 29 | } 30 | if j != rows { 31 | b.Errorf("got %v, want %v", j, rows) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 go-logfmt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | - cron: "0 0 1,11,21 * *" 6 | name: "Build and Test" 7 | jobs: 8 | test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | go-version: 13 | - 1.24.x 14 | - 1.25.x 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Install Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | - name: Test 25 | run: go test ./... 26 | - name: Test coverage 27 | run: go test -coverprofile="cover.out" ./... # quotes needed for powershell 28 | - name: Send coverage 29 | uses: shogo82148/actions-goveralls@v1 30 | with: 31 | path-to-profile: cover.out 32 | flag-name: go${{ matrix.go-version }}-${{ matrix.os }} 33 | parallel: true 34 | # notifies that all test jobs are finished. 35 | finish: 36 | needs: test 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: shogo82148/actions-goveralls@v1 40 | with: 41 | parallel-finished: true 42 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package logfmt_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-logfmt/logfmt" 11 | ) 12 | 13 | func ExampleEncoder() { 14 | check := func(err error) { 15 | if err != nil { 16 | panic(err) 17 | } 18 | } 19 | 20 | e := logfmt.NewEncoder(os.Stdout) 21 | 22 | check(e.EncodeKeyval("id", 1)) 23 | check(e.EncodeKeyval("dur", time.Second+time.Millisecond)) 24 | check(e.EndRecord()) 25 | 26 | check(e.EncodeKeyval("id", 1)) 27 | check(e.EncodeKeyval("path", "/path/to/file")) 28 | check(e.EncodeKeyval("err", errors.New("file not found"))) 29 | check(e.EndRecord()) 30 | 31 | // Output: 32 | // id=1 dur=1.001s 33 | // id=1 path=/path/to/file err="file not found" 34 | } 35 | 36 | func ExampleDecoder() { 37 | in := ` 38 | id=1 dur=1.001s 39 | id=1 path=/path/to/file err="file not found" 40 | ` 41 | 42 | d := logfmt.NewDecoder(strings.NewReader(in)) 43 | for d.ScanRecord() { 44 | for d.ScanKeyval() { 45 | fmt.Printf("k: %s v: %s\n", d.Key(), d.Value()) 46 | } 47 | fmt.Println() 48 | } 49 | if d.Err() != nil { 50 | panic(d.Err()) 51 | } 52 | 53 | // Output: 54 | // k: id v: 1 55 | // k: dur v: 1.001s 56 | // 57 | // k: id v: 1 58 | // k: path v: /path/to/file 59 | // k: err v: file not found 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logfmt 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-logfmt/logfmt.svg)](https://pkg.go.dev/github.com/go-logfmt/logfmt) 4 | [![Go Report Card](https://goreportcard.com/badge/go-logfmt/logfmt)](https://goreportcard.com/report/go-logfmt/logfmt) 5 | [![Github Actions](https://github.com/go-logfmt/logfmt/actions/workflows/test.yml/badge.svg)](https://github.com/go-logfmt/logfmt/actions/workflows/test.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/go-logfmt/logfmt/badge.svg?branch=master)](https://coveralls.io/github/go-logfmt/logfmt?branch=main) 7 | 8 | Package logfmt implements utilities to marshal and unmarshal data in the [logfmt 9 | format][fmt]. It provides an API similar to [encoding/json][json] and 10 | [encoding/xml][xml]. 11 | 12 | [fmt]: https://brandur.org/logfmt 13 | [json]: https://pkg.go.dev/encoding/json 14 | [xml]: https://pkg.go.dev/encoding/xml 15 | 16 | The logfmt format was first documented by Brandur Leach in [this 17 | article][origin]. The format has not been formally standardized. The most 18 | authoritative public specification to date has been the documentation of a Go 19 | Language [package][parser] written by Blake Mizerany and Keith Rarick. 20 | 21 | [origin]: https://brandur.org/logfmt 22 | [parser]: https://pkg.go.dev/github.com/kr/logfmt 23 | 24 | ## Goals 25 | 26 | This project attempts to conform as closely as possible to the prior art, while 27 | also removing ambiguity where necessary to provide well behaved encoder and 28 | decoder implementations. 29 | 30 | ## Non-goals 31 | 32 | This project does not attempt to formally standardize the logfmt format. In the 33 | event that logfmt is standardized this project would take conforming to the 34 | standard as a goal. 35 | 36 | ## Versioning 37 | 38 | This project publishes releases according to the Go language guidelines for 39 | [developing and publishing modules][pub]. 40 | 41 | [pub]: https://go.dev/doc/modules/developing 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.1] - 2025-10-05 9 | 10 | ### Fixed 11 | 12 | - Encode DEL (0x7f) control character by [@spaceone] 13 | - Modernize code through Go 1.21 by [@ChrisHines] 14 | 15 | [0.6.1]: https://github.com/go-logfmt/logfmt/compare/v0.6.0...v0.6.1 16 | 17 | ## [0.6.0] - 2023-01-30 18 | 19 | [0.6.0]: https://github.com/go-logfmt/logfmt/compare/v0.5.1...v0.6.0 20 | 21 | ### Added 22 | 23 | - NewDecoderSize by [@alexanderjophus] 24 | 25 | ## [0.5.1] - 2021-08-18 26 | 27 | [0.5.1]: https://github.com/go-logfmt/logfmt/compare/v0.5.0...v0.5.1 28 | 29 | ### Changed 30 | 31 | - Update the `go.mod` file for Go 1.17 as described in the [Go 1.17 release 32 | notes](https://golang.org/doc/go1.17#go-command) 33 | 34 | ## [0.5.0] - 2020-01-03 35 | 36 | [0.5.0]: https://github.com/go-logfmt/logfmt/compare/v0.4.0...v0.5.0 37 | 38 | ### Changed 39 | 40 | - Remove the dependency on github.com/kr/logfmt by [@ChrisHines] 41 | - Move fuzz code to github.com/go-logfmt/fuzzlogfmt by [@ChrisHines] 42 | 43 | ## [0.4.0] - 2018-11-21 44 | 45 | [0.4.0]: https://github.com/go-logfmt/logfmt/compare/v0.3.0...v0.4.0 46 | 47 | ### Added 48 | 49 | - Go module support by [@ChrisHines] 50 | - CHANGELOG by [@ChrisHines] 51 | 52 | ### Changed 53 | 54 | - Drop invalid runes from keys instead of returning ErrInvalidKey by [@ChrisHines] 55 | - On panic while printing, attempt to print panic value by [@bboreham] 56 | 57 | ## [0.3.0] - 2016-11-15 58 | 59 | [0.3.0]: https://github.com/go-logfmt/logfmt/compare/v0.2.0...v0.3.0 60 | 61 | ### Added 62 | 63 | - Pool buffers for quoted strings and byte slices by [@nussjustin] 64 | 65 | ### Fixed 66 | 67 | - Fuzz fix, quote invalid UTF-8 values by [@judwhite] 68 | 69 | ## [0.2.0] - 2016-05-08 70 | 71 | [0.2.0]: https://github.com/go-logfmt/logfmt/compare/v0.1.0...v0.2.0 72 | 73 | ### Added 74 | 75 | - Encoder.EncodeKeyvals by [@ChrisHines] 76 | 77 | ## [0.1.0] - 2016-03-28 78 | 79 | [0.1.0]: https://github.com/go-logfmt/logfmt/commits/v0.1.0 80 | 81 | ### Added 82 | 83 | - Encoder by [@ChrisHines] 84 | - Decoder by [@ChrisHines] 85 | - MarshalKeyvals by [@ChrisHines] 86 | 87 | [@ChrisHines]: https://github.com/ChrisHines 88 | [@bboreham]: https://github.com/bboreham 89 | [@judwhite]: https://github.com/judwhite 90 | [@nussjustin]: https://github.com/nussjustin 91 | [@alexanderjophus]: https://github.com/alexanderjophus 92 | [@spaceone]: https://github.com/spaceone 93 | -------------------------------------------------------------------------------- /jsonstring.go: -------------------------------------------------------------------------------- 1 | package logfmt 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strconv" 7 | "sync" 8 | "unicode" 9 | "unicode/utf16" 10 | "unicode/utf8" 11 | ) 12 | 13 | // Taken from Go's encoding/json and modified for use here. 14 | 15 | // Copyright 2010 The Go Authors. All rights reserved. 16 | // Use of this source code is governed by a BSD-style 17 | // license that can be found in the LICENSE file. 18 | 19 | var hex = "0123456789abcdef" 20 | 21 | var bufferPool = sync.Pool{ 22 | New: func() any { 23 | return &bytes.Buffer{} 24 | }, 25 | } 26 | 27 | func getBuffer() *bytes.Buffer { 28 | return bufferPool.Get().(*bytes.Buffer) 29 | } 30 | 31 | func poolBuffer(buf *bytes.Buffer) { 32 | buf.Reset() 33 | bufferPool.Put(buf) 34 | } 35 | 36 | // NOTE: keep in sync with writeQuotedBytes below. 37 | func writeQuotedString(w io.Writer, s string) (int, error) { 38 | buf := getBuffer() 39 | buf.WriteByte('"') 40 | start := 0 41 | for i := 0; i < len(s); { 42 | if b := s[i]; b < utf8.RuneSelf { 43 | if 0x20 <= b && b != '\\' && b != '"' && b != 0x7f { 44 | i++ 45 | continue 46 | } 47 | if start < i { 48 | buf.WriteString(s[start:i]) 49 | } 50 | switch b { 51 | case '\\', '"': 52 | buf.WriteByte('\\') 53 | buf.WriteByte(b) 54 | case '\n': 55 | buf.WriteByte('\\') 56 | buf.WriteByte('n') 57 | case '\r': 58 | buf.WriteByte('\\') 59 | buf.WriteByte('r') 60 | case '\t': 61 | buf.WriteByte('\\') 62 | buf.WriteByte('t') 63 | default: 64 | // This encodes bytes < 0x20 except for \n, \r, and \t. 65 | buf.WriteString(`\u00`) 66 | buf.WriteByte(hex[b>>4]) 67 | buf.WriteByte(hex[b&0xF]) 68 | } 69 | i++ 70 | start = i 71 | continue 72 | } 73 | c, size := utf8.DecodeRuneInString(s[i:]) 74 | if c == utf8.RuneError { 75 | if start < i { 76 | buf.WriteString(s[start:i]) 77 | } 78 | buf.WriteString(`\ufffd`) 79 | i += size 80 | start = i 81 | continue 82 | } 83 | i += size 84 | } 85 | if start < len(s) { 86 | buf.WriteString(s[start:]) 87 | } 88 | buf.WriteByte('"') 89 | n, err := w.Write(buf.Bytes()) 90 | poolBuffer(buf) 91 | return n, err 92 | } 93 | 94 | // NOTE: keep in sync with writeQuotedString above. 95 | func writeQuotedBytes(w io.Writer, s []byte) (int, error) { 96 | buf := getBuffer() 97 | buf.WriteByte('"') 98 | start := 0 99 | for i := 0; i < len(s); { 100 | if b := s[i]; b < utf8.RuneSelf { 101 | if 0x20 <= b && b != '\\' && b != '"' && b != 0x7f { 102 | i++ 103 | continue 104 | } 105 | if start < i { 106 | buf.Write(s[start:i]) 107 | } 108 | switch b { 109 | case '\\', '"': 110 | buf.WriteByte('\\') 111 | buf.WriteByte(b) 112 | case '\n': 113 | buf.WriteByte('\\') 114 | buf.WriteByte('n') 115 | case '\r': 116 | buf.WriteByte('\\') 117 | buf.WriteByte('r') 118 | case '\t': 119 | buf.WriteByte('\\') 120 | buf.WriteByte('t') 121 | default: 122 | // This encodes bytes < 0x20 except for \n, \r, and \t. 123 | buf.WriteString(`\u00`) 124 | buf.WriteByte(hex[b>>4]) 125 | buf.WriteByte(hex[b&0xF]) 126 | } 127 | i++ 128 | start = i 129 | continue 130 | } 131 | c, size := utf8.DecodeRune(s[i:]) 132 | if c == utf8.RuneError { 133 | if start < i { 134 | buf.Write(s[start:i]) 135 | } 136 | buf.WriteString(`\ufffd`) 137 | i += size 138 | start = i 139 | continue 140 | } 141 | i += size 142 | } 143 | if start < len(s) { 144 | buf.Write(s[start:]) 145 | } 146 | buf.WriteByte('"') 147 | n, err := w.Write(buf.Bytes()) 148 | poolBuffer(buf) 149 | return n, err 150 | } 151 | 152 | // getu4 decodes \uXXXX from the beginning of s, returning the hex value, 153 | // or it returns -1. 154 | func getu4(s []byte) rune { 155 | if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { 156 | return -1 157 | } 158 | r, err := strconv.ParseUint(string(s[2:6]), 16, 64) 159 | if err != nil { 160 | return -1 161 | } 162 | return rune(r) 163 | } 164 | 165 | func unquoteBytes(s []byte) (t []byte, ok bool) { 166 | if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { 167 | return 168 | } 169 | s = s[1 : len(s)-1] 170 | 171 | // Check for unusual characters. If there are none, 172 | // then no unquoting is needed, so return a slice of the 173 | // original bytes. 174 | r := 0 175 | for r < len(s) { 176 | c := s[r] 177 | if c == '\\' || c == '"' || c < ' ' { 178 | break 179 | } 180 | if c < utf8.RuneSelf { 181 | r++ 182 | continue 183 | } 184 | rr, size := utf8.DecodeRune(s[r:]) 185 | if rr == utf8.RuneError { 186 | break 187 | } 188 | r += size 189 | } 190 | if r == len(s) { 191 | return s, true 192 | } 193 | 194 | b := make([]byte, len(s)+2*utf8.UTFMax) 195 | w := copy(b, s[0:r]) 196 | for r < len(s) { 197 | // Out of room? Can only happen if s is full of 198 | // malformed UTF-8 and we're replacing each 199 | // byte with RuneError. 200 | if w >= len(b)-2*utf8.UTFMax { 201 | nb := make([]byte, (len(b)+utf8.UTFMax)*2) 202 | copy(nb, b[0:w]) 203 | b = nb 204 | } 205 | switch c := s[r]; { 206 | case c == '\\': 207 | r++ 208 | if r >= len(s) { 209 | return 210 | } 211 | switch s[r] { 212 | default: 213 | return 214 | case '"', '\\', '/', '\'': 215 | b[w] = s[r] 216 | r++ 217 | w++ 218 | case 'b': 219 | b[w] = '\b' 220 | r++ 221 | w++ 222 | case 'f': 223 | b[w] = '\f' 224 | r++ 225 | w++ 226 | case 'n': 227 | b[w] = '\n' 228 | r++ 229 | w++ 230 | case 'r': 231 | b[w] = '\r' 232 | r++ 233 | w++ 234 | case 't': 235 | b[w] = '\t' 236 | r++ 237 | w++ 238 | case 'u': 239 | r-- 240 | rr := getu4(s[r:]) 241 | if rr < 0 { 242 | return 243 | } 244 | r += 6 245 | if utf16.IsSurrogate(rr) { 246 | rr1 := getu4(s[r:]) 247 | if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { 248 | // A valid pair; consume. 249 | r += 6 250 | w += utf8.EncodeRune(b[w:], dec) 251 | break 252 | } 253 | // Invalid surrogate; fall back to replacement rune. 254 | rr = unicode.ReplacementChar 255 | } 256 | w += utf8.EncodeRune(b[w:], rr) 257 | } 258 | 259 | // Quote, control characters are invalid. 260 | case c == '"', c < ' ': 261 | return 262 | 263 | // ASCII 264 | case c < utf8.RuneSelf: 265 | b[w] = c 266 | r++ 267 | w++ 268 | 269 | // Coerce to well-formed UTF-8. 270 | default: 271 | rr, size := utf8.DecodeRune(s[r:]) 272 | r += size 273 | w += utf8.EncodeRune(b[w:], rr) 274 | } 275 | } 276 | return b[0:w], true 277 | } 278 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package logfmt 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "unicode/utf8" 9 | ) 10 | 11 | // A Decoder reads and decodes logfmt records from an input stream. 12 | type Decoder struct { 13 | pos int 14 | key []byte 15 | value []byte 16 | lineNum int 17 | s *bufio.Scanner 18 | err error 19 | } 20 | 21 | // NewDecoder returns a new decoder that reads from r. 22 | // 23 | // The decoder introduces its own buffering and may read data from r beyond 24 | // the logfmt records requested. 25 | func NewDecoder(r io.Reader) *Decoder { 26 | dec := &Decoder{ 27 | s: bufio.NewScanner(r), 28 | } 29 | return dec 30 | } 31 | 32 | // NewDecoderSize returns a new decoder that reads from r. 33 | // 34 | // The decoder introduces its own buffering and may read data from r beyond 35 | // the logfmt records requested. 36 | // The size argument specifies the size of the initial buffer that the 37 | // Decoder will use to read records from r. 38 | // If a log line is longer than the size argument, the Decoder will return 39 | // a bufio.ErrTooLong error. 40 | func NewDecoderSize(r io.Reader, size int) *Decoder { 41 | scanner := bufio.NewScanner(r) 42 | scanner.Buffer(make([]byte, 0, size), size) 43 | dec := &Decoder{ 44 | s: scanner, 45 | } 46 | return dec 47 | } 48 | 49 | // ScanRecord advances the Decoder to the next record, which can then be 50 | // parsed with the ScanKeyval method. It returns false when decoding stops, 51 | // either by reaching the end of the input or an error. After ScanRecord 52 | // returns false, the Err method will return any error that occurred during 53 | // decoding, except that if it was io.EOF, Err will return nil. 54 | func (dec *Decoder) ScanRecord() bool { 55 | if dec.err != nil { 56 | return false 57 | } 58 | if !dec.s.Scan() { 59 | dec.err = dec.s.Err() 60 | return false 61 | } 62 | dec.lineNum++ 63 | dec.pos = 0 64 | return true 65 | } 66 | 67 | // ScanKeyval advances the Decoder to the next key/value pair of the current 68 | // record, which can then be retrieved with the Key and Value methods. It 69 | // returns false when decoding stops, either by reaching the end of the 70 | // current record or an error. 71 | func (dec *Decoder) ScanKeyval() bool { 72 | dec.key, dec.value = nil, nil 73 | if dec.err != nil { 74 | return false 75 | } 76 | 77 | line := dec.s.Bytes() 78 | 79 | // garbage 80 | for p, c := range line[dec.pos:] { 81 | if c > ' ' { 82 | dec.pos += p 83 | goto key 84 | } 85 | } 86 | dec.pos = len(line) 87 | return false 88 | 89 | key: 90 | const invalidKeyError = "invalid key" 91 | 92 | start, multibyte := dec.pos, false 93 | for p, c := range line[dec.pos:] { 94 | switch { 95 | case c == '=': 96 | dec.pos += p 97 | if dec.pos > start { 98 | dec.key = line[start:dec.pos] 99 | if multibyte && bytes.ContainsRune(dec.key, utf8.RuneError) { 100 | dec.syntaxError(invalidKeyError) 101 | return false 102 | } 103 | } 104 | if dec.key == nil { 105 | dec.unexpectedByte(c) 106 | return false 107 | } 108 | goto equal 109 | case c == '"': 110 | dec.pos += p 111 | dec.unexpectedByte(c) 112 | return false 113 | case c <= ' ': 114 | dec.pos += p 115 | if dec.pos > start { 116 | dec.key = line[start:dec.pos] 117 | if multibyte && bytes.ContainsRune(dec.key, utf8.RuneError) { 118 | dec.syntaxError(invalidKeyError) 119 | return false 120 | } 121 | } 122 | return true 123 | case c >= utf8.RuneSelf: 124 | multibyte = true 125 | } 126 | } 127 | dec.pos = len(line) 128 | if dec.pos > start { 129 | dec.key = line[start:dec.pos] 130 | if multibyte && bytes.ContainsRune(dec.key, utf8.RuneError) { 131 | dec.syntaxError(invalidKeyError) 132 | return false 133 | } 134 | } 135 | return true 136 | 137 | equal: 138 | dec.pos++ 139 | if dec.pos >= len(line) { 140 | return true 141 | } 142 | switch c := line[dec.pos]; { 143 | case c <= ' ': 144 | return true 145 | case c == '"': 146 | goto qvalue 147 | } 148 | 149 | // value 150 | start = dec.pos 151 | for p, c := range line[dec.pos:] { 152 | switch { 153 | case c == '=' || c == '"': 154 | dec.pos += p 155 | dec.unexpectedByte(c) 156 | return false 157 | case c <= ' ': 158 | dec.pos += p 159 | if dec.pos > start { 160 | dec.value = line[start:dec.pos] 161 | } 162 | return true 163 | } 164 | } 165 | dec.pos = len(line) 166 | if dec.pos > start { 167 | dec.value = line[start:dec.pos] 168 | } 169 | return true 170 | 171 | qvalue: 172 | const ( 173 | untermQuote = "unterminated quoted value" 174 | invalidQuote = "invalid quoted value" 175 | ) 176 | 177 | hasEsc, esc := false, false 178 | start = dec.pos 179 | for p, c := range line[dec.pos+1:] { 180 | switch { 181 | case esc: 182 | esc = false 183 | case c == '\\': 184 | hasEsc, esc = true, true 185 | case c == '"': 186 | dec.pos += p + 2 187 | if hasEsc { 188 | v, ok := unquoteBytes(line[start:dec.pos]) 189 | if !ok { 190 | dec.syntaxError(invalidQuote) 191 | return false 192 | } 193 | dec.value = v 194 | } else { 195 | start++ 196 | end := dec.pos - 1 197 | if end > start { 198 | dec.value = line[start:end] 199 | } 200 | } 201 | return true 202 | } 203 | } 204 | dec.pos = len(line) 205 | dec.syntaxError(untermQuote) 206 | return false 207 | } 208 | 209 | // Key returns the most recent key found by a call to ScanKeyval. The returned 210 | // slice may point to internal buffers and is only valid until the next call 211 | // to ScanRecord. It does no allocation. 212 | func (dec *Decoder) Key() []byte { 213 | return dec.key 214 | } 215 | 216 | // Value returns the most recent value found by a call to ScanKeyval. The 217 | // returned slice may point to internal buffers and is only valid until the 218 | // next call to ScanRecord. It does no allocation when the value has no 219 | // escape sequences. 220 | func (dec *Decoder) Value() []byte { 221 | return dec.value 222 | } 223 | 224 | // Err returns the first non-EOF error that was encountered by the Scanner. 225 | func (dec *Decoder) Err() error { 226 | return dec.err 227 | } 228 | 229 | func (dec *Decoder) syntaxError(msg string) { 230 | dec.err = &SyntaxError{ 231 | Msg: msg, 232 | Line: dec.lineNum, 233 | Pos: dec.pos + 1, 234 | } 235 | } 236 | 237 | func (dec *Decoder) unexpectedByte(c byte) { 238 | dec.err = &SyntaxError{ 239 | Msg: fmt.Sprintf("unexpected %q", c), 240 | Line: dec.lineNum, 241 | Pos: dec.pos + 1, 242 | } 243 | } 244 | 245 | // A SyntaxError represents a syntax error in the logfmt input stream. 246 | type SyntaxError struct { 247 | Msg string 248 | Line int 249 | Pos int 250 | } 251 | 252 | func (e *SyntaxError) Error() string { 253 | return fmt.Sprintf("logfmt syntax error at pos %d on line %d: %s", e.Pos, e.Line, e.Msg) 254 | } 255 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package logfmt_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-logfmt/logfmt" 13 | ) 14 | 15 | func TestEncodeKeyval(t *testing.T) { 16 | data := []struct { 17 | key, value any 18 | want string 19 | err error 20 | }{ 21 | {key: "k", value: "v", want: "k=v"}, 22 | {key: "k", value: nil, want: "k=null"}, 23 | {key: `\`, value: "v", want: `\=v`}, 24 | {key: "k", value: "", want: "k="}, 25 | {key: "k", value: "null", want: `k="null"`}, 26 | {key: "k", value: "", want: `k=`}, 27 | {key: "k", value: true, want: "k=true"}, 28 | {key: "k", value: 1, want: "k=1"}, 29 | {key: "k", value: 1.025, want: "k=1.025"}, 30 | {key: "k", value: 1e-3, want: "k=0.001"}, 31 | {key: "k", value: 3.5 + 2i, want: "k=(3.5+2i)"}, 32 | {key: "k", value: "v v", want: `k="v v"`}, 33 | {key: "k", value: " ", want: `k=" "`}, 34 | {key: "k", value: `"`, want: `k="\""`}, 35 | {key: "k", value: `=`, want: `k="="`}, 36 | {key: "k", value: `\`, want: `k=\`}, 37 | {key: "k", value: `=\`, want: `k="=\\"`}, 38 | {key: "k", value: `\"`, want: `k="\\\""`}, 39 | {key: "k", value: [2]int{2, 19}, err: logfmt.ErrUnsupportedValueType}, 40 | {key: "k", value: []string{"e1", "e 2"}, err: logfmt.ErrUnsupportedValueType}, 41 | {key: "k", value: structData{"a a", 9}, err: logfmt.ErrUnsupportedValueType}, 42 | {key: "k", value: decimalMarshaler{5, 9}, want: "k=5.9"}, 43 | {key: "k", value: (*decimalMarshaler)(nil), want: "k=null"}, 44 | {key: "k", value: decimalStringer{5, 9}, want: "k=5.9"}, 45 | {key: "k", value: (*decimalStringer)(nil), want: "k=null"}, 46 | {key: "k", value: marshalerStringer{5, 9}, want: "k=5.9"}, 47 | {key: "k", value: (*marshalerStringer)(nil), want: "k=null"}, 48 | {key: "k", value: new(nilMarshaler), want: "k=notnilmarshaler"}, 49 | {key: "k", value: (*nilMarshaler)(nil), want: "k=nilmarshaler"}, 50 | {key: (*marshalerStringer)(nil), value: "v", err: logfmt.ErrNilKey}, 51 | {key: decimalMarshaler{5, 9}, value: "v", want: "5.9=v"}, 52 | {key: (*decimalMarshaler)(nil), value: "v", err: logfmt.ErrNilKey}, 53 | {key: decimalStringer{5, 9}, value: "v", want: "5.9=v"}, 54 | {key: (*decimalStringer)(nil), value: "v", err: logfmt.ErrNilKey}, 55 | {key: marshalerStringer{5, 9}, value: "v", want: "5.9=v"}, 56 | {key: "k", value: "\xbd", want: `k="\ufffd"`}, 57 | {key: "k", value: "\ufffd\x00", want: `k="\ufffd\u0000"`}, 58 | {key: "k", value: "\ufffd", want: `k="\ufffd"`}, 59 | {key: "k", value: "\u007f", want: `k="\u007f"`}, 60 | {key: "k\u007f", value: "v", want: `k=v`}, 61 | {key: "k", value: []byte("\ufffd\x00"), want: `k="\ufffd\u0000"`}, 62 | {key: "k", value: []byte("\ufffd"), want: `k="\ufffd"`}, 63 | {key: "k", value: []byte("\u007f"), want: `k="\u007f"`}, 64 | {key: []byte("k\u007f"), value: "v", want: `k=v`}, 65 | } 66 | 67 | for _, d := range data { 68 | w := &bytes.Buffer{} 69 | enc := logfmt.NewEncoder(w) 70 | err := enc.EncodeKeyval(d.key, d.value) 71 | if err != d.err { 72 | t.Errorf("%#v, %#v: got error: %v, want error: %v", d.key, d.value, err, d.err) 73 | } 74 | if got, want := w.String(), d.want; got != want { 75 | t.Errorf("%#v, %#v: got '%s', want '%s'", d.key, d.value, got, want) 76 | } 77 | } 78 | } 79 | 80 | func TestMarshalKeyvals(t *testing.T) { 81 | one := 1 82 | ptr := &one 83 | nilPtr := (*int)(nil) 84 | 85 | data := []struct { 86 | in []any 87 | want []byte 88 | err error 89 | }{ 90 | {in: nil, want: nil}, 91 | {in: kv(), want: nil}, 92 | {in: kv(nil, "v"), err: logfmt.ErrNilKey}, 93 | {in: kv(nilPtr, "v"), err: logfmt.ErrNilKey}, 94 | {in: kv("\ufffd"), err: logfmt.ErrInvalidKey}, 95 | {in: kv("\xbd"), err: logfmt.ErrInvalidKey}, 96 | {in: kv("k"), want: []byte("k=null")}, 97 | {in: kv("k", nil), want: []byte("k=null")}, 98 | {in: kv("k", ""), want: []byte("k=")}, 99 | {in: kv("k", "null"), want: []byte(`k="null"`)}, 100 | {in: kv("k", "v"), want: []byte("k=v")}, 101 | {in: kv("k", true), want: []byte("k=true")}, 102 | {in: kv("k", 1), want: []byte("k=1")}, 103 | {in: kv("k", ptr), want: []byte("k=1")}, 104 | {in: kv("k", nilPtr), want: []byte("k=null")}, 105 | {in: kv("k", 1.025), want: []byte("k=1.025")}, 106 | {in: kv("k", 1e-3), want: []byte("k=0.001")}, 107 | {in: kv("k", "v v"), want: []byte(`k="v v"`)}, 108 | {in: kv("k", `"`), want: []byte(`k="\""`)}, 109 | {in: kv("k", `=`), want: []byte(`k="="`)}, 110 | {in: kv("k", `\`), want: []byte(`k=\`)}, 111 | {in: kv("k", `=\`), want: []byte(`k="=\\"`)}, 112 | {in: kv("k", `\"`), want: []byte(`k="\\\""`)}, 113 | {in: kv("k1", "v1", "k2", "v2"), want: []byte("k1=v1 k2=v2")}, 114 | {in: kv("k1", "v1", "k2", [2]int{}), want: []byte("k1=v1 k2=\"unsupported value type\"")}, 115 | {in: kv([2]int{}, "v1", "k2", "v2"), want: []byte("k2=v2")}, 116 | {in: kv("k", time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)), want: []byte("k=2009-11-10T23:00:00Z")}, 117 | {in: kv("k", errorMarshaler{}), want: []byte("k=\"error marshaling value of type logfmt_test.errorMarshaler: marshal error\"")}, 118 | {in: kv("k", decimalMarshaler{5, 9}), want: []byte("k=5.9")}, 119 | {in: kv("k", (*decimalMarshaler)(nil)), want: []byte("k=null")}, 120 | {in: kv("k", decimalStringer{5, 9}), want: []byte("k=5.9")}, 121 | {in: kv("k", (*decimalStringer)(nil)), want: []byte("k=null")}, 122 | {in: kv("k", marshalerStringer{5, 9}), want: []byte("k=5.9")}, 123 | {in: kv("k", (*marshalerStringer)(nil)), want: []byte("k=null")}, 124 | {in: kv(one, "v"), want: []byte("1=v")}, 125 | {in: kv(ptr, "v"), want: []byte("1=v")}, 126 | {in: kv((*marshalerStringer)(nil), "v"), err: logfmt.ErrNilKey}, 127 | {in: kv(decimalMarshaler{5, 9}, "v"), want: []byte("5.9=v")}, 128 | {in: kv((*decimalMarshaler)(nil), "v"), err: logfmt.ErrNilKey}, 129 | {in: kv(decimalStringer{5, 9}, "v"), want: []byte("5.9=v")}, 130 | {in: kv((*decimalStringer)(nil), "v"), err: logfmt.ErrNilKey}, 131 | {in: kv(marshalerStringer{5, 9}, "v"), want: []byte("5.9=v")}, 132 | {in: kv("k", panicingStringer{0}), want: []byte("k=ok")}, 133 | {in: kv("k", panicingStringer{1}), want: []byte("k=PANIC:panic1")}, 134 | // Need extra mechanism to test panic-while-printing-panicVal 135 | //{in: kv("k", panicingStringer{2}), want: []byte("?")}, 136 | } 137 | 138 | for _, d := range data { 139 | got, err := logfmt.MarshalKeyvals(d.in...) 140 | if err != d.err { 141 | t.Errorf("%#v: got error: %v, want error: %v", d.in, err, d.err) 142 | } 143 | if !reflect.DeepEqual(got, d.want) { 144 | t.Errorf("%#v: got '%s', want '%s'", d.in, got, d.want) 145 | } 146 | } 147 | } 148 | 149 | func kv(keyvals ...any) []any { 150 | return keyvals 151 | } 152 | 153 | type structData struct { 154 | A string `logfmt:"fieldA"` 155 | B int 156 | } 157 | 158 | type nilMarshaler int 159 | 160 | func (m *nilMarshaler) MarshalText() ([]byte, error) { 161 | if m == nil { 162 | return []byte("nilmarshaler"), nil 163 | } 164 | return []byte("notnilmarshaler"), nil 165 | } 166 | 167 | type decimalMarshaler struct { 168 | a, b int 169 | } 170 | 171 | func (t decimalMarshaler) MarshalText() ([]byte, error) { 172 | buf := &bytes.Buffer{} 173 | fmt.Fprintf(buf, "%d.%d", t.a, t.b) 174 | return buf.Bytes(), nil 175 | } 176 | 177 | type decimalStringer struct { 178 | a, b int 179 | } 180 | 181 | func (s decimalStringer) String() string { 182 | return fmt.Sprintf("%d.%d", s.a, s.b) 183 | } 184 | 185 | type marshalerStringer struct { 186 | a, b int 187 | } 188 | 189 | func (t marshalerStringer) MarshalText() ([]byte, error) { 190 | buf := &bytes.Buffer{} 191 | fmt.Fprintf(buf, "%d.%d", t.a, t.b) 192 | return buf.Bytes(), nil 193 | } 194 | 195 | func (t marshalerStringer) String() string { 196 | return fmt.Sprint(t.a + t.b) 197 | } 198 | 199 | var errMarshal = errors.New("marshal error") 200 | 201 | type errorMarshaler struct{} 202 | 203 | func (errorMarshaler) MarshalText() ([]byte, error) { 204 | return nil, errMarshal 205 | } 206 | 207 | type panicingStringer struct { 208 | a int 209 | } 210 | 211 | func (p panicingStringer) String() string { 212 | switch p.a { 213 | case 1: 214 | panic("panic1") 215 | case 2: 216 | panic(panicingStringer{a: 2}) 217 | } 218 | return "ok" 219 | } 220 | 221 | func BenchmarkEncodeKeyval(b *testing.B) { 222 | b.ReportAllocs() 223 | enc := logfmt.NewEncoder(io.Discard) 224 | for i := 0; i < b.N; i++ { 225 | enc.EncodeKeyval("sk", "10") 226 | enc.EncodeKeyval("some-key", "a rather long string with spaces") 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package logfmt 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/google/go-cmp/cmp/cmpopts" 13 | ) 14 | 15 | type kv struct { 16 | k, v []byte 17 | } 18 | 19 | func (s kv) String() string { 20 | return fmt.Sprintf("{k:%q v:%q}", s.k, s.v) 21 | } 22 | 23 | func TestDecoder_scan(t *testing.T) { 24 | defaultDecoder := func(s string) *Decoder { return NewDecoder(strings.NewReader(s)) } 25 | tests := []struct { 26 | data string 27 | dec func(string) *Decoder 28 | want [][]kv 29 | }{ 30 | { 31 | data: "", 32 | dec: defaultDecoder, 33 | want: nil, 34 | }, 35 | { 36 | data: "\n\n", 37 | dec: defaultDecoder, 38 | want: [][]kv{nil, nil}, 39 | }, 40 | { 41 | data: `x= `, 42 | dec: defaultDecoder, 43 | want: [][]kv{{{[]byte("x"), nil}}}, 44 | }, 45 | { 46 | data: `y=`, 47 | dec: defaultDecoder, 48 | want: [][]kv{{{[]byte("y"), nil}}}, 49 | }, 50 | { 51 | data: `y`, 52 | dec: defaultDecoder, 53 | want: [][]kv{{{[]byte("y"), nil}}}, 54 | }, 55 | { 56 | data: `y=f`, 57 | dec: defaultDecoder, 58 | want: [][]kv{{{[]byte("y"), []byte("f")}}}, 59 | }, 60 | { 61 | data: "y=\"\\tf\"", 62 | dec: defaultDecoder, 63 | want: [][]kv{{{[]byte("y"), []byte("\tf")}}}, 64 | }, 65 | { 66 | data: "a=1\n", 67 | dec: defaultDecoder, 68 | want: [][]kv{{{[]byte("a"), []byte("1")}}}, 69 | }, 70 | { 71 | data: `a=1 b="bar" ƒ=2h3s r="esc\t" d x=sf `, 72 | dec: defaultDecoder, 73 | want: [][]kv{{ 74 | {[]byte("a"), []byte("1")}, 75 | {[]byte("b"), []byte("bar")}, 76 | {[]byte("ƒ"), []byte("2h3s")}, 77 | {[]byte("r"), []byte("esc\t")}, 78 | {[]byte("d"), nil}, 79 | {[]byte("x"), []byte("sf")}, 80 | }}, 81 | }, 82 | { 83 | data: "y=f\ny=g", 84 | dec: defaultDecoder, 85 | want: [][]kv{ 86 | {{[]byte("y"), []byte("f")}}, 87 | {{[]byte("y"), []byte("g")}}, 88 | }, 89 | }, 90 | { 91 | data: "y=f \n\x1e y=g", 92 | dec: defaultDecoder, 93 | want: [][]kv{ 94 | {{[]byte("y"), []byte("f")}}, 95 | {{[]byte("y"), []byte("g")}}, 96 | }, 97 | }, 98 | { 99 | data: "y= d y=g", 100 | dec: defaultDecoder, 101 | want: [][]kv{{ 102 | {[]byte("y"), nil}, 103 | {[]byte("d"), nil}, 104 | {[]byte("y"), []byte("g")}, 105 | }}, 106 | }, 107 | { 108 | data: "y=\"f\"\ny=g", 109 | dec: defaultDecoder, 110 | want: [][]kv{ 111 | {{[]byte("y"), []byte("f")}}, 112 | {{[]byte("y"), []byte("g")}}, 113 | }, 114 | }, 115 | { 116 | data: "y=\"f\\n\"y=g", 117 | dec: defaultDecoder, 118 | want: [][]kv{{ 119 | {[]byte("y"), []byte("f\n")}, 120 | {[]byte("y"), []byte("g")}, 121 | }}, 122 | }, 123 | { 124 | data: strings.Repeat(`y=f `, 5), 125 | dec: func(s string) *Decoder { return NewDecoderSize(strings.NewReader(s), 21) }, 126 | want: [][]kv{{ 127 | {[]byte("y"), []byte("f")}, 128 | {[]byte("y"), []byte("f")}, 129 | {[]byte("y"), []byte("f")}, 130 | {[]byte("y"), []byte("f")}, 131 | {[]byte("y"), []byte("f")}, 132 | }}, 133 | }, 134 | } 135 | 136 | for _, test := range tests { 137 | var got [][]kv 138 | dec := test.dec(test.data) 139 | 140 | for dec.ScanRecord() { 141 | var kvs []kv 142 | for dec.ScanKeyval() { 143 | k := dec.Key() 144 | v := dec.Value() 145 | if k != nil { 146 | kvs = append(kvs, kv{k, v}) 147 | } 148 | } 149 | got = append(got, kvs) 150 | } 151 | if err := dec.Err(); err != nil { 152 | t.Errorf("got err: %v", err) 153 | } 154 | if !reflect.DeepEqual(got, test.want) { 155 | t.Errorf("\n in: %q\n got: %+v\nwant: %+v", test.data, got, test.want) 156 | } 157 | } 158 | } 159 | 160 | func TestDecoder_errors(t *testing.T) { 161 | tests := []struct { 162 | data string 163 | dec func(string) *Decoder 164 | want error 165 | }{ 166 | { 167 | data: "a=1\nb=2", 168 | dec: func(s string) *Decoder { 169 | dec := NewDecoderSize(strings.NewReader(s), 1) 170 | return dec 171 | }, 172 | want: bufio.ErrTooLong, 173 | }, 174 | } 175 | 176 | for _, test := range tests { 177 | dec := test.dec(test.data) 178 | 179 | for dec.ScanRecord() { 180 | for dec.ScanKeyval() { 181 | } 182 | } 183 | if diff := cmp.Diff(test.want, dec.Err(), cmpopts.EquateErrors()); diff != "" { 184 | t.Errorf("%#v: Decoder.Err() value mismatch (-want,+got):\n%s", test.data, diff) 185 | } 186 | } 187 | } 188 | 189 | func TestDecoder_SyntaxError(t *testing.T) { 190 | defaultDecoder := func(s string) *Decoder { return NewDecoder(strings.NewReader(s)) } 191 | tests := []struct { 192 | data string 193 | dec func(string) *Decoder 194 | want error 195 | }{ 196 | { 197 | data: "a=1\n=bar", 198 | dec: defaultDecoder, 199 | want: &SyntaxError{Msg: "unexpected '='", Line: 2, Pos: 1}, 200 | }, 201 | { 202 | data: "a=1\n\"k\"=bar", 203 | dec: defaultDecoder, 204 | want: &SyntaxError{Msg: "unexpected '\"'", Line: 2, Pos: 1}, 205 | }, 206 | { 207 | data: "a=1\nk\"ey=bar", 208 | dec: defaultDecoder, 209 | want: &SyntaxError{Msg: "unexpected '\"'", Line: 2, Pos: 2}, 210 | }, 211 | { 212 | data: "a=1\nk=b\"ar", 213 | dec: defaultDecoder, 214 | want: &SyntaxError{Msg: "unexpected '\"'", Line: 2, Pos: 4}, 215 | }, 216 | { 217 | data: "a=1\nk=b =ar", 218 | dec: defaultDecoder, 219 | want: &SyntaxError{Msg: "unexpected '='", Line: 2, Pos: 5}, 220 | }, 221 | { 222 | data: "a==", 223 | dec: defaultDecoder, 224 | want: &SyntaxError{Msg: "unexpected '='", Line: 1, Pos: 3}, 225 | }, 226 | { 227 | data: "a=1\nk=b=ar", 228 | dec: defaultDecoder, 229 | want: &SyntaxError{Msg: "unexpected '='", Line: 2, Pos: 4}, 230 | }, 231 | { 232 | data: "a=\"1", 233 | dec: defaultDecoder, 234 | want: &SyntaxError{Msg: "unterminated quoted value", Line: 1, Pos: 5}, 235 | }, 236 | { 237 | data: "a=\"1\\", 238 | dec: defaultDecoder, 239 | want: &SyntaxError{Msg: "unterminated quoted value", Line: 1, Pos: 6}, 240 | }, 241 | { 242 | data: "a=\"\\t1", 243 | dec: defaultDecoder, 244 | want: &SyntaxError{Msg: "unterminated quoted value", Line: 1, Pos: 7}, 245 | }, 246 | { 247 | data: "a=\"\\u1\"", 248 | dec: defaultDecoder, 249 | want: &SyntaxError{Msg: "invalid quoted value", Line: 1, Pos: 8}, 250 | }, 251 | { 252 | data: "a\ufffd=bar", 253 | dec: defaultDecoder, 254 | want: &SyntaxError{Msg: "invalid key", Line: 1, Pos: 5}, 255 | }, 256 | { 257 | data: "\x80=bar", 258 | dec: defaultDecoder, 259 | want: &SyntaxError{Msg: "invalid key", Line: 1, Pos: 2}, 260 | }, 261 | { 262 | data: "\x80", 263 | dec: defaultDecoder, 264 | want: &SyntaxError{Msg: "invalid key", Line: 1, Pos: 2}, 265 | }, 266 | } 267 | 268 | for _, test := range tests { 269 | dec := test.dec(test.data) 270 | 271 | for dec.ScanRecord() { 272 | for dec.ScanKeyval() { 273 | } 274 | } 275 | 276 | switch got := dec.Err().(type) { 277 | case nil: 278 | t.Errorf("%#v: dec.Err() == nil, want: *SyntaxError", test.data) 279 | case *SyntaxError: 280 | if diff := cmp.Diff(test.want, got); diff != "" { 281 | t.Errorf("%#v: dec.Err() mismatch (-want,+got):\n%s", test.data, diff) 282 | } 283 | default: 284 | t.Errorf("%#v: dec.Err().(type) == %T, want: *SyntaxError", test.data, got) 285 | } 286 | } 287 | } 288 | 289 | func TestDecoder_decode_encode(t *testing.T) { 290 | tests := []struct { 291 | in, out string 292 | }{ 293 | {"", ""}, 294 | {"\n", "\n"}, 295 | {"\n \n", "\n\n"}, 296 | { 297 | "a=1\nb=2\n", 298 | "a=1\nb=2\n", 299 | }, 300 | { 301 | "a=1 b=\"bar\" ƒ=2h3s r=\"esc\\t\" d x=sf ", 302 | "a=1 b=bar ƒ=2h3s r=\"esc\\t\" d= x=sf\n", 303 | }, 304 | } 305 | 306 | for _, test := range tests { 307 | dec := NewDecoder(strings.NewReader(test.in)) 308 | buf := bytes.Buffer{} 309 | enc := NewEncoder(&buf) 310 | 311 | var err error 312 | loop: 313 | for dec.ScanRecord() && err == nil { 314 | for dec.ScanKeyval() { 315 | if dec.Key() == nil { 316 | continue 317 | } 318 | if err = enc.EncodeKeyval(dec.Key(), dec.Value()); err != nil { 319 | break loop 320 | } 321 | } 322 | enc.EndRecord() 323 | } 324 | if err == nil { 325 | err = dec.Err() 326 | } 327 | if err != nil { 328 | t.Errorf("got err: %v", err) 329 | } 330 | if got, want := buf.String(), test.out; got != want { 331 | t.Errorf("\n got: %q\nwant: %q", got, want) 332 | } 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /encode_internal_test.go: -------------------------------------------------------------------------------- 1 | package logfmt 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/google/go-cmp/cmp/cmpopts" 13 | ) 14 | 15 | func TestSafeString(t *testing.T) { 16 | _, ok := safeString((*stringStringer)(nil)) 17 | if got, want := ok, false; got != want { 18 | t.Errorf(" got %v, want %v", got, want) 19 | } 20 | } 21 | 22 | func TestSafeMarshal(t *testing.T) { 23 | kb, err := safeMarshal((*stringMarshaler)(nil)) 24 | if got := kb; got != nil { 25 | t.Errorf(" got %v, want nil", got) 26 | } 27 | if got, want := err, error(nil); got != want { 28 | t.Errorf(" got %v, want %v", got, want) 29 | } 30 | } 31 | 32 | func TestWriteKeyStrings(t *testing.T) { 33 | keygen := []struct { 34 | name string 35 | fn func(string) any 36 | }{ 37 | { 38 | name: "string", 39 | fn: func(s string) any { return s }, 40 | }, 41 | { 42 | name: "named-string", 43 | fn: func(s string) any { return stringData(s) }, 44 | }, 45 | { 46 | name: "Stringer", 47 | fn: func(s string) any { return stringStringer(s) }, 48 | }, 49 | { 50 | name: "TextMarshaler", 51 | fn: func(s string) any { return stringMarshaler(s) }, 52 | }, 53 | } 54 | 55 | data := []struct { 56 | key string 57 | want string 58 | err error 59 | }{ 60 | {key: "k", want: "k"}, 61 | {key: `\`, want: `\`}, 62 | {key: "\n", err: ErrInvalidKey}, 63 | {key: "\x00", err: ErrInvalidKey}, 64 | {key: "\x10", err: ErrInvalidKey}, 65 | {key: "\x1F", err: ErrInvalidKey}, 66 | {key: "", err: ErrInvalidKey}, 67 | {key: " ", err: ErrInvalidKey}, 68 | {key: "=", err: ErrInvalidKey}, 69 | {key: `"`, err: ErrInvalidKey}, 70 | {key: "k\n", want: "k"}, 71 | {key: "k\nk", want: "kk"}, 72 | {key: "k\tk", want: "kk"}, 73 | {key: "k=k", want: "kk"}, 74 | {key: `"kk"`, want: "kk"}, 75 | } 76 | 77 | for _, g := range keygen { 78 | t.Run(g.name, func(t *testing.T) { 79 | for _, d := range data { 80 | w := &bytes.Buffer{} 81 | key := g.fn(d.key) 82 | err := writeKey(w, key) 83 | if err != d.err { 84 | t.Errorf("%#v: got error: %v, want error: %v", key, err, d.err) 85 | } 86 | if err != nil { 87 | continue 88 | } 89 | if got, want := w.String(), d.want; got != want { 90 | t.Errorf("%#v: got '%s', want '%s'", key, got, want) 91 | } 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestWriteKey(t *testing.T) { 98 | var ( 99 | nilPtr *int 100 | one = 1 101 | ptr = &one 102 | ) 103 | 104 | data := []struct { 105 | key any 106 | want string 107 | err error 108 | }{ 109 | {key: nil, err: ErrNilKey}, 110 | {key: nilPtr, err: ErrNilKey}, 111 | {key: (*stringStringer)(nil), err: ErrNilKey}, 112 | {key: (*stringMarshaler)(nil), err: ErrNilKey}, 113 | {key: (*stringerMarshaler)(nil), err: ErrNilKey}, 114 | {key: ptr, want: "1"}, 115 | 116 | {key: make(chan int), err: ErrUnsupportedKeyType}, 117 | {key: []int{}, err: ErrUnsupportedKeyType}, 118 | {key: map[int]int{}, err: ErrUnsupportedKeyType}, 119 | {key: [2]int{}, err: ErrUnsupportedKeyType}, 120 | {key: struct{}{}, err: ErrUnsupportedKeyType}, 121 | {key: fmt.Sprint, err: ErrUnsupportedKeyType}, 122 | } 123 | 124 | for _, d := range data { 125 | w := &bytes.Buffer{} 126 | err := writeKey(w, d.key) 127 | if diff := cmp.Diff(d.err, err, cmpopts.EquateErrors()); diff != "" { 128 | t.Errorf("%#v: error value mismatch (-want,+got):\n%s", d.key, diff) 129 | } 130 | if err != nil { 131 | continue 132 | } 133 | if got, want := w.String(), d.want; got != want { 134 | t.Errorf("%#v: got '%s', want '%s'", d.key, got, want) 135 | } 136 | } 137 | } 138 | 139 | func TestWriteKeyMarshalError(t *testing.T) { 140 | data := []struct { 141 | key any 142 | want string 143 | err error 144 | }{ 145 | {key: errorMarshaler{}, err: &MarshalerError{Type: reflect.TypeOf(errorMarshaler{}), Err: errMarshaling}}, 146 | } 147 | 148 | for _, d := range data { 149 | w := &bytes.Buffer{} 150 | err := writeKey(w, d.key) 151 | 152 | switch err := err.(type) { 153 | case nil: 154 | t.Errorf("%#v: err == nil, want: not nil", d.key) 155 | case *MarshalerError: 156 | if got, want := err.Type, reflect.TypeOf(errorMarshaler{}); got != want { 157 | t.Errorf("%#v: MarshalerError.Type == %v, want: %v", d.key, got, want) 158 | } 159 | if diff := cmp.Diff(errMarshaling, err.Err, cmpopts.EquateErrors()); diff != "" { 160 | t.Errorf("%#v: MarshalerError.Err value mismatch (-want,+got):\n%s", d.key, diff) 161 | } 162 | default: 163 | t.Errorf("%#v: unexpected error, got: %q, want: a MarshalerError", d.key, err) 164 | } 165 | } 166 | } 167 | 168 | func TestWriteValueStrings(t *testing.T) { 169 | keygen := []func(string) any{ 170 | func(s string) any { return s }, 171 | func(s string) any { return errors.New(s) }, 172 | func(s string) any { return stringData(s) }, 173 | func(s string) any { return stringStringer(s) }, 174 | func(s string) any { return stringMarshaler(s) }, 175 | } 176 | 177 | data := []struct { 178 | value string 179 | want string 180 | err error 181 | }{ 182 | {value: "", want: ""}, 183 | {value: "v", want: "v"}, 184 | {value: " ", want: `" "`}, 185 | {value: "=", want: `"="`}, 186 | {value: `\`, want: `\`}, 187 | {value: `"`, want: `"\""`}, 188 | {value: `\"`, want: `"\\\""`}, 189 | {value: "\n", want: `"\n"`}, 190 | {value: "\x00", want: `"\u0000"`}, 191 | {value: "\x10", want: `"\u0010"`}, 192 | {value: "\x1F", want: `"\u001f"`}, 193 | {value: "µ", want: `µ`}, 194 | } 195 | 196 | for _, g := range keygen { 197 | for _, d := range data { 198 | w := &bytes.Buffer{} 199 | value := g(d.value) 200 | err := writeValue(w, value) 201 | if err != d.err { 202 | t.Errorf("%#v (%[1]T): got error: %v, want error: %v", value, err, d.err) 203 | } 204 | if err != nil { 205 | continue 206 | } 207 | if got, want := w.String(), d.want; got != want { 208 | t.Errorf("%#v (%[1]T): got '%s', want '%s'", value, got, want) 209 | } 210 | } 211 | } 212 | } 213 | 214 | func TestWriteValue(t *testing.T) { 215 | var ( 216 | nilPtr *int 217 | one = 1 218 | ptr = &one 219 | ) 220 | 221 | data := []struct { 222 | value any 223 | want string 224 | err error 225 | }{ 226 | {value: nil, want: "null"}, 227 | {value: nilPtr, want: "null"}, 228 | {value: (*stringStringer)(nil), want: "null"}, 229 | {value: (*stringMarshaler)(nil), want: "null"}, 230 | {value: (*stringerMarshaler)(nil), want: "null"}, 231 | {value: ptr, want: "1"}, 232 | 233 | {value: make(chan int), err: ErrUnsupportedValueType}, 234 | {value: []int{}, err: ErrUnsupportedValueType}, 235 | {value: map[int]int{}, err: ErrUnsupportedValueType}, 236 | {value: [2]int{}, err: ErrUnsupportedValueType}, 237 | {value: struct{}{}, err: ErrUnsupportedValueType}, 238 | {value: fmt.Sprint, err: ErrUnsupportedValueType}, 239 | } 240 | 241 | for _, d := range data { 242 | w := &bytes.Buffer{} 243 | err := writeValue(w, d.value) 244 | if diff := cmp.Diff(d.err, err, cmpopts.EquateErrors()); diff != "" { 245 | t.Errorf("%#v: error value mismatch (-want,+got):\n%s", d.value, diff) 246 | } 247 | if err != nil { 248 | continue 249 | } 250 | if got, want := w.String(), d.want; got != want { 251 | t.Errorf("%#v: got '%s', want '%s'", d.value, got, want) 252 | } 253 | } 254 | } 255 | 256 | func TestWriteValueMarshalError(t *testing.T) { 257 | data := []struct { 258 | value any 259 | want string 260 | err error 261 | }{ 262 | {value: errorMarshaler{}, err: &MarshalerError{Type: reflect.TypeOf(errorMarshaler{}), Err: errMarshaling}}, 263 | } 264 | 265 | for _, d := range data { 266 | w := &bytes.Buffer{} 267 | err := writeValue(w, d.value) 268 | 269 | switch err := err.(type) { 270 | case nil: 271 | t.Errorf("%#v: err == nil, want: not nil", d.value) 272 | case *MarshalerError: 273 | if got, want := err.Type, reflect.TypeOf(errorMarshaler{}); got != want { 274 | t.Errorf("%#v: MarshalerError.Type == %v, want: %v", d.value, got, want) 275 | } 276 | if diff := cmp.Diff(errMarshaling, err.Err, cmpopts.EquateErrors()); diff != "" { 277 | t.Errorf("%#v: MarshalerError.Err value mismatch (-want,+got):\n%s", d.value, diff) 278 | } 279 | default: 280 | t.Errorf("%#v: unexpected error, got: %q, want: a MarshalerError", d.value, err) 281 | } 282 | } 283 | } 284 | 285 | type stringData string 286 | 287 | type stringStringer string 288 | 289 | func (s stringStringer) String() string { 290 | return string(s) 291 | } 292 | 293 | type stringMarshaler string 294 | 295 | func (s stringMarshaler) MarshalText() ([]byte, error) { 296 | return []byte(s), nil 297 | } 298 | 299 | type stringerMarshaler string 300 | 301 | func (s stringerMarshaler) String() string { 302 | return "String() called" 303 | } 304 | 305 | func (s stringerMarshaler) MarshalText() ([]byte, error) { 306 | return []byte(s), nil 307 | } 308 | 309 | var errMarshaling = errors.New("marshal error") 310 | 311 | type errorMarshaler struct{} 312 | 313 | func (errorMarshaler) MarshalText() ([]byte, error) { 314 | return nil, errMarshaling 315 | } 316 | 317 | func BenchmarkWriteStringKey(b *testing.B) { 318 | keys := []string{ 319 | "k", 320 | "caller", 321 | "has space", 322 | `"quoted"`, 323 | } 324 | 325 | for _, k := range keys { 326 | b.Run(k, func(b *testing.B) { 327 | for i := 0; i < b.N; i++ { 328 | writeStringKey(io.Discard, k) 329 | } 330 | }) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package logfmt 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "reflect" 10 | "strings" 11 | "unicode/utf8" 12 | ) 13 | 14 | // MarshalKeyvals returns the logfmt encoding of keyvals, a variadic sequence 15 | // of alternating keys and values. 16 | func MarshalKeyvals(keyvals ...any) ([]byte, error) { 17 | buf := &bytes.Buffer{} 18 | if err := NewEncoder(buf).EncodeKeyvals(keyvals...); err != nil { 19 | return nil, err 20 | } 21 | return buf.Bytes(), nil 22 | } 23 | 24 | // An Encoder writes logfmt data to an output stream. 25 | type Encoder struct { 26 | w io.Writer 27 | scratch bytes.Buffer 28 | needSep bool 29 | } 30 | 31 | // NewEncoder returns a new encoder that writes to w. 32 | func NewEncoder(w io.Writer) *Encoder { 33 | return &Encoder{ 34 | w: w, 35 | } 36 | } 37 | 38 | var ( 39 | space = []byte(" ") 40 | equals = []byte("=") 41 | newline = []byte("\n") 42 | null = []byte("null") 43 | ) 44 | 45 | // EncodeKeyval writes the logfmt encoding of key and value to the stream. A 46 | // single space is written before the second and subsequent keys in a record. 47 | // Nothing is written if a non-nil error is returned. 48 | func (enc *Encoder) EncodeKeyval(key, value any) error { 49 | enc.scratch.Reset() 50 | if enc.needSep { 51 | if _, err := enc.scratch.Write(space); err != nil { 52 | return err 53 | } 54 | } 55 | if err := writeKey(&enc.scratch, key); err != nil { 56 | return err 57 | } 58 | if _, err := enc.scratch.Write(equals); err != nil { 59 | return err 60 | } 61 | if err := writeValue(&enc.scratch, value); err != nil { 62 | return err 63 | } 64 | _, err := enc.w.Write(enc.scratch.Bytes()) 65 | enc.needSep = true 66 | return err 67 | } 68 | 69 | // EncodeKeyvals writes the logfmt encoding of keyvals to the stream. Keyvals 70 | // is a variadic sequence of alternating keys and values. Keys of unsupported 71 | // type are skipped along with their corresponding value. Values of 72 | // unsupported type or that cause a MarshalerError are replaced by their error 73 | // but do not cause EncodeKeyvals to return an error. If a non-nil error is 74 | // returned some key/value pairs may not have be written. 75 | func (enc *Encoder) EncodeKeyvals(keyvals ...any) error { 76 | if len(keyvals) == 0 { 77 | return nil 78 | } 79 | if len(keyvals)%2 == 1 { 80 | keyvals = append(keyvals, nil) 81 | } 82 | for i := 0; i < len(keyvals); i += 2 { 83 | k, v := keyvals[i], keyvals[i+1] 84 | err := enc.EncodeKeyval(k, v) 85 | if err == ErrUnsupportedKeyType { 86 | continue 87 | } 88 | if _, ok := err.(*MarshalerError); ok || err == ErrUnsupportedValueType { 89 | v = err 90 | err = enc.EncodeKeyval(k, v) 91 | } 92 | if err != nil { 93 | return err 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | // MarshalerError represents an error encountered while marshaling a value. 100 | type MarshalerError struct { 101 | Type reflect.Type 102 | Err error 103 | } 104 | 105 | func (e *MarshalerError) Error() string { 106 | return "error marshaling value of type " + e.Type.String() + ": " + e.Err.Error() 107 | } 108 | 109 | // ErrNilKey is returned by Marshal functions and Encoder methods if a key is 110 | // a nil interface or pointer value. 111 | var ErrNilKey = errors.New("nil key") 112 | 113 | // ErrInvalidKey is returned by Marshal functions and Encoder methods if, after 114 | // dropping invalid runes, a key is empty. 115 | var ErrInvalidKey = errors.New("invalid key") 116 | 117 | // ErrUnsupportedKeyType is returned by Encoder methods if a key has an 118 | // unsupported type. 119 | var ErrUnsupportedKeyType = errors.New("unsupported key type") 120 | 121 | // ErrUnsupportedValueType is returned by Encoder methods if a value has an 122 | // unsupported type. 123 | var ErrUnsupportedValueType = errors.New("unsupported value type") 124 | 125 | func writeKey(w io.Writer, key any) error { 126 | if key == nil { 127 | return ErrNilKey 128 | } 129 | 130 | switch k := key.(type) { 131 | case string: 132 | return writeStringKey(w, k) 133 | case []byte: 134 | if k == nil { 135 | return ErrNilKey 136 | } 137 | return writeBytesKey(w, k) 138 | case encoding.TextMarshaler: 139 | kb, err := safeMarshal(k) 140 | if err != nil { 141 | return err 142 | } 143 | if kb == nil { 144 | return ErrNilKey 145 | } 146 | return writeBytesKey(w, kb) 147 | case fmt.Stringer: 148 | ks, ok := safeString(k) 149 | if !ok { 150 | return ErrNilKey 151 | } 152 | return writeStringKey(w, ks) 153 | default: 154 | rkey := reflect.ValueOf(key) 155 | switch rkey.Kind() { 156 | case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice, reflect.Struct: 157 | return ErrUnsupportedKeyType 158 | case reflect.Pointer: 159 | if rkey.IsNil() { 160 | return ErrNilKey 161 | } 162 | return writeKey(w, rkey.Elem().Interface()) 163 | } 164 | return writeStringKey(w, fmt.Sprint(k)) 165 | } 166 | } 167 | 168 | // keyRuneFilter returns r for all valid key runes, and -1 for all invalid key 169 | // runes. When used as the mapping function for strings.Map and bytes.Map 170 | // functions it causes them to remove invalid key runes from strings or byte 171 | // slices respectively. 172 | func keyRuneFilter(r rune) rune { 173 | if r <= ' ' || r == '=' || r == '"' || r == 0x7f || r == utf8.RuneError { 174 | return -1 175 | } 176 | return r 177 | } 178 | 179 | func writeStringKey(w io.Writer, key string) error { 180 | k := strings.Map(keyRuneFilter, key) 181 | if k == "" { 182 | return ErrInvalidKey 183 | } 184 | _, err := io.WriteString(w, k) 185 | return err 186 | } 187 | 188 | func writeBytesKey(w io.Writer, key []byte) error { 189 | k := bytes.Map(keyRuneFilter, key) 190 | if len(k) == 0 { 191 | return ErrInvalidKey 192 | } 193 | _, err := w.Write(k) 194 | return err 195 | } 196 | 197 | func writeValue(w io.Writer, value any) error { 198 | switch v := value.(type) { 199 | case nil: 200 | return writeBytesValue(w, null) 201 | case string: 202 | return writeStringValue(w, v, true) 203 | case []byte: 204 | return writeBytesValue(w, v) 205 | case encoding.TextMarshaler: 206 | vb, err := safeMarshal(v) 207 | if err != nil { 208 | return err 209 | } 210 | if vb == nil { 211 | vb = null 212 | } 213 | return writeBytesValue(w, vb) 214 | case error: 215 | se, ok := safeError(v) 216 | return writeStringValue(w, se, ok) 217 | case fmt.Stringer: 218 | ss, ok := safeString(v) 219 | return writeStringValue(w, ss, ok) 220 | default: 221 | rvalue := reflect.ValueOf(value) 222 | switch rvalue.Kind() { 223 | case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice, reflect.Struct: 224 | return ErrUnsupportedValueType 225 | case reflect.Pointer: 226 | if rvalue.IsNil() { 227 | return writeBytesValue(w, null) 228 | } 229 | return writeValue(w, rvalue.Elem().Interface()) 230 | } 231 | return writeStringValue(w, fmt.Sprint(v), true) 232 | } 233 | } 234 | 235 | func needsQuotedValueRune(r rune) bool { 236 | return r <= ' ' || r == '=' || r == '"' || r == 0x7f || r == utf8.RuneError 237 | } 238 | 239 | func writeStringValue(w io.Writer, value string, ok bool) error { 240 | var err error 241 | if ok && value == "null" { 242 | _, err = io.WriteString(w, `"null"`) 243 | } else if strings.IndexFunc(value, needsQuotedValueRune) != -1 { 244 | _, err = writeQuotedString(w, value) 245 | } else { 246 | _, err = io.WriteString(w, value) 247 | } 248 | return err 249 | } 250 | 251 | func writeBytesValue(w io.Writer, value []byte) error { 252 | var err error 253 | if bytes.IndexFunc(value, needsQuotedValueRune) != -1 { 254 | _, err = writeQuotedBytes(w, value) 255 | } else { 256 | _, err = w.Write(value) 257 | } 258 | return err 259 | } 260 | 261 | // EndRecord writes a newline character to the stream and resets the encoder 262 | // to the beginning of a new record. 263 | func (enc *Encoder) EndRecord() error { 264 | _, err := enc.w.Write(newline) 265 | if err == nil { 266 | enc.needSep = false 267 | } 268 | return err 269 | } 270 | 271 | // Reset resets the encoder to the beginning of a new record. 272 | func (enc *Encoder) Reset() { 273 | enc.needSep = false 274 | } 275 | 276 | func safeError(err error) (s string, ok bool) { 277 | defer func() { 278 | if panicVal := recover(); panicVal != nil { 279 | if v := reflect.ValueOf(err); v.Kind() == reflect.Pointer && v.IsNil() { 280 | s, ok = "null", false 281 | } else { 282 | s, ok = fmt.Sprintf("PANIC:%v", panicVal), false 283 | } 284 | } 285 | }() 286 | s, ok = err.Error(), true 287 | return 288 | } 289 | 290 | func safeString(str fmt.Stringer) (s string, ok bool) { 291 | defer func() { 292 | if panicVal := recover(); panicVal != nil { 293 | if v := reflect.ValueOf(str); v.Kind() == reflect.Pointer && v.IsNil() { 294 | s, ok = "null", false 295 | } else { 296 | s, ok = fmt.Sprintf("PANIC:%v", panicVal), true 297 | } 298 | } 299 | }() 300 | s, ok = str.String(), true 301 | return 302 | } 303 | 304 | func safeMarshal(tm encoding.TextMarshaler) (b []byte, err error) { 305 | defer func() { 306 | if panicVal := recover(); panicVal != nil { 307 | if v := reflect.ValueOf(tm); v.Kind() == reflect.Pointer && v.IsNil() { 308 | b, err = nil, nil 309 | } else { 310 | b, err = nil, fmt.Errorf("panic when marshalling: %s", panicVal) 311 | } 312 | } 313 | }() 314 | b, err = tm.MarshalText() 315 | if err != nil { 316 | return nil, &MarshalerError{ 317 | Type: reflect.TypeOf(tm), 318 | Err: err, 319 | } 320 | } 321 | return 322 | } 323 | --------------------------------------------------------------------------------