diff --git a/array.go b/array.go index 99612ee9..2b00882a 100644 --- a/array.go +++ b/array.go @@ -1,6 +1,7 @@ package zerolog import ( + "encoding/base64" "net" "sync" "time" @@ -85,6 +86,19 @@ func (a *Array) Hex(val []byte) *Array { return a } +// Base64 appends the val as a standard padded base64 string to the array. +func (a *Array) Base64(val []byte) *Array { + a.buf = enc.AppendBase64(base64.StdEncoding, enc.AppendArrayDelim(a.buf), val) + return a +} + +// Base64Custom appends the val as a base64 string to the array. +// The specific form of base64 can be specified with the first parameter. +func (a *Array) Base64Custom(b64 *base64.Encoding, val []byte) *Array { + a.buf = enc.AppendBase64(b64, enc.AppendArrayDelim(a.buf), val) + return a +} + // RawJSON adds already encoded JSON to the array. func (a *Array) RawJSON(val []byte) *Array { a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val) diff --git a/array_test.go b/array_test.go index d37d8fb2..5b4b02f5 100644 --- a/array_test.go +++ b/array_test.go @@ -1,6 +1,7 @@ package zerolog import ( + "encoding/base64" "net" "testing" "time" @@ -24,6 +25,8 @@ func TestArray(t *testing.T) { Str("a"). Bytes([]byte("b")). Hex([]byte{0x1f}). + Base64([]byte{0x12, 0xef, 0x29, 0x30, 0xff}). + Base64Custom(base64.RawURLEncoding, []byte{0xcc, 0xbb, 0xaa, 0xff}). RawJSON([]byte(`{"some":"json"}`)). Time(time.Time{}). IPAddr(net.IP{192, 168, 0, 10}). @@ -32,7 +35,7 @@ func TestArray(t *testing.T) { Str("bar", "baz"). Int("n", 1), ) - want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},"0001-01-01T00:00:00Z","192.168.0.10",0,{"bar":"baz","n":1}]` + want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f","Eu8pMP8=","zLuq_w",{"some":"json"},"0001-01-01T00:00:00Z","192.168.0.10",0,{"bar":"baz","n":1}]` if got := decodeObjectToStr(a.write([]byte{})); got != want { t.Errorf("Array.write()\ngot: %s\nwant: %s", got, want) } diff --git a/context.go b/context.go index ff48b1fe..714cf263 100644 --- a/context.go +++ b/context.go @@ -2,6 +2,7 @@ package zerolog import ( "context" + "encoding/base64" "fmt" "io/ioutil" "math" @@ -108,6 +109,19 @@ func (c Context) Hex(key string, val []byte) Context { return c } +// Base64 adds the field key with val as a standard padded base64 string to the logger context. +func (c Context) Base64(key string, val []byte) Context { + c.l.context = enc.AppendBase64(base64.StdEncoding, enc.AppendKey(c.l.context, key), val) + return c +} + +// Base64Custom adds the field key with val as a base64 string to the logger context. +// The specific form of base64 can be specified with the first parameter. +func (c Context) Base64Custom(b64 *base64.Encoding, key string, val []byte) Context { + c.l.context = enc.AppendBase64(b64, enc.AppendKey(c.l.context, key), val) + return c +} + // RawJSON adds already encoded JSON to context. // // No sanity check is performed on b; it must not contain carriage returns and diff --git a/event.go b/event.go index 5c949f8a..91830db6 100644 --- a/event.go +++ b/event.go @@ -2,6 +2,7 @@ package zerolog import ( "context" + "encoding/base64" "fmt" "net" "os" @@ -308,6 +309,25 @@ func (e *Event) Hex(key string, val []byte) *Event { return e } +// Base64 adds the field key with val as a standard padded base64 string to the *Event context. +func (e *Event) Base64(key string, val []byte) *Event { + if e == nil { + return e + } + e.buf = enc.AppendBase64(base64.StdEncoding, enc.AppendKey(e.buf, key), val) + return e +} + +// Base64Custom adds the field key with val as a base64 string to the *Event context. +// The specific form of base64 can be specified with the first parameter. +func (e *Event) Base64Custom(b64 *base64.Encoding, key string, val []byte) *Event { + if e == nil { + return e + } + e.buf = enc.AppendBase64(b64, enc.AppendKey(e.buf, key), val) + return e +} + // RawJSON adds already encoded JSON to the log line under key. // // No sanity check is performed on b; it must not contain carriage returns and diff --git a/internal/cbor/cbor.go b/internal/cbor/cbor.go index 1bf14438..d714098f 100644 --- a/internal/cbor/cbor.go +++ b/internal/cbor/cbor.go @@ -35,6 +35,10 @@ const ( additionalTypeEmbeddedJSON uint16 = 262 additionalTypeTagHexString uint16 = 263 + // Expected later encoding for CBOR-to-JSON converters - from https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.5.2 + additionalTypeTagBase64Standard byte = 22 + additionalTypeTagBase64RawURL byte = 23 + // Unspecified number of elements. additionalTypeInfiniteCount byte = 31 ) diff --git a/internal/cbor/decode_stream.go b/internal/cbor/decode_stream.go index 616bed65..b9a93d23 100644 --- a/internal/cbor/decode_stream.go +++ b/internal/cbor/decode_stream.go @@ -386,6 +386,17 @@ func decodeTagData(src *bufio.Reader) []byte { } src.UnreadByte() return decodeStringToDataUrl(src, "application/cbor") + case additionalTypeTagBase64Standard, additionalTypeTagBase64RawURL: + data := decodeString(src, true) + enc := base64.StdEncoding + if byte(val) == additionalTypeTagBase64RawURL { + enc = base64.RawURLEncoding + } + output := make([]byte, enc.EncodedLen(len(data))+2) + output[0] = '"' + output[len(output)-1] = '"' + enc.Encode(output[1:len(output)-1], data) + return output default: panic(fmt.Errorf("Unsupported Additional Tag Type: %d in decodeTagData", val)) } diff --git a/internal/cbor/types.go b/internal/cbor/types.go index 6f538328..cf7a8a77 100644 --- a/internal/cbor/types.go +++ b/internal/cbor/types.go @@ -1,6 +1,7 @@ package cbor import ( + "encoding/base64" "fmt" "math" "net" @@ -484,3 +485,19 @@ func (e Encoder) AppendHex(dst []byte, val []byte) []byte { dst = append(dst, byte(additionalTypeTagHexString&0xff)) return e.AppendBytes(dst, val) } + +// AppendBase64 adds a TAG and inserts base64 bytes as a string. +func (e Encoder) AppendBase64(enc *base64.Encoding, dst []byte, val []byte) []byte { + switch enc { + case base64.StdEncoding: + dst = append(dst, majorTypeTags|additionalTypeIntUint8) + dst = append(dst, additionalTypeTagBase64Standard) + return e.AppendBytes(dst, val) + case base64.RawURLEncoding: + dst = append(dst, majorTypeTags|additionalTypeIntUint8) + dst = append(dst, additionalTypeTagBase64RawURL) + return e.AppendBytes(dst, val) + default: + return e.AppendString(dst, enc.EncodeToString(val)) + } +} diff --git a/internal/json/bytes.go b/internal/json/bytes.go index de64120d..8c403114 100644 --- a/internal/json/bytes.go +++ b/internal/json/bytes.go @@ -1,6 +1,9 @@ package json -import "unicode/utf8" +import ( + "encoding/base64" + "unicode/utf8" +) // AppendBytes is a mirror of appendString with []byte arg func (Encoder) AppendBytes(dst, s []byte) []byte { @@ -28,6 +31,20 @@ func (Encoder) AppendHex(dst, s []byte) []byte { return append(dst, '"') } +// AppendBase64 encodes the input bytes to a base64 string and appends +// the encoded string to the input byte slice. +func (Encoder) AppendBase64(e *base64.Encoding, dst, s []byte) []byte { + dst = append(dst, '"') + start := len(dst) + targetLen := start + e.EncodedLen(len(s)) + for cap(dst) < targetLen { + dst = append(dst[:cap(dst)], 0) + } + dst = dst[:targetLen] + e.Encode(dst[start:], s) + return append(dst, '"') +} + // appendBytesComplex is a mirror of the appendStringComplex // with []byte arg func appendBytesComplex(dst, s []byte, i int) []byte { diff --git a/internal/json/bytes_test.go b/internal/json/bytes_test.go index d1a370ab..bdc8153d 100644 --- a/internal/json/bytes_test.go +++ b/internal/json/bytes_test.go @@ -1,6 +1,8 @@ package json import ( + "crypto/rand" + "encoding/base64" "testing" "unicode" ) @@ -25,6 +27,30 @@ func TestAppendHex(t *testing.T) { } } +var base64Encodings = []struct { + name string + enc *base64.Encoding +}{ + {"base64.StdEncoding", base64.StdEncoding}, + {"base64.RawStdEncoding", base64.RawStdEncoding}, + {"base64.URLEncoding", base64.URLEncoding}, + {"base64.RawURLEncoding", base64.RawURLEncoding}, +} + +func TestAppendBase64(t *testing.T) { + random := make([]byte, 19) + _, _ = rand.Read(random) + tests := [][]byte{{}, {'\x00'}, {'\xff'}, random} + for _, input := range tests { + for _, tt := range base64Encodings { + b := enc.AppendBase64(tt.enc, []byte{}, input) + if got, want := string(b), "\""+tt.enc.EncodeToString(input)+"\""; got != want { + t.Errorf("appendBase64(%s, %x) = %s, want %s", tt.name, input, got, want) + } + } + } +} + func TestStringBytes(t *testing.T) { t.Parallel() // Test that encodeState.stringBytes and encodeState.string use the same encoding. diff --git a/log_test.go b/log_test.go index 4d0d93b1..06ef544e 100644 --- a/log_test.go +++ b/log_test.go @@ -3,6 +3,7 @@ package zerolog import ( "bytes" "context" + "encoding/base64" "errors" "fmt" "net" @@ -104,6 +105,8 @@ func TestWith(t *testing.T) { Stringer("stringer_nil", nil). Bytes("bytes", []byte("bar")). Hex("hex", []byte{0x12, 0xef}). + Base64("base64", []byte{0x12, 0xef, 0x29, 0x30, 0xff}). + Base64Custom(base64.RawURLEncoding, "base64url", []byte{0xcc, 0xbb, 0xaa, 0xff}). RawJSON("json", []byte(`{"some":"json"}`)). AnErr("some_err", nil). Err(errors.New("some error")). @@ -126,7 +129,7 @@ func TestWith(t *testing.T) { caller := fmt.Sprintf("%s:%d", file, line+3) log := ctx.Caller().Logger() log.Log().Msg("") - if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want { + if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","base64":"Eu8pMP8=","base64url":"zLuq_w","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } @@ -140,7 +143,7 @@ func TestWith(t *testing.T) { }() // The above line is a little contrived, but the line above should be the line due // to the extra frame skip. - if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want { + if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","base64":"Eu8pMP8=","base64url":"zLuq_w","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -321,6 +324,8 @@ func TestFields(t *testing.T) { Stringer("stringer_nil", nil). Bytes("bytes", []byte("bar")). Hex("hex", []byte{0x12, 0xef}). + Base64("base64", []byte{0x12, 0xef, 0x29, 0x30, 0xff}). + Base64Custom(base64.RawURLEncoding, "base64url", []byte{0xcc, 0xbb, 0xaa, 0xff}). RawJSON("json", []byte(`{"some":"json"}`)). RawCBOR("cbor", []byte{0x83, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05}). Func(func(e *Event) { e.Str("func", "func_output") }). @@ -348,7 +353,7 @@ func TestFields(t *testing.T) { TimeDiff("diff", now, now.Add(-10*time.Second)). Ctx(context.Background()). Msg("") - if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"cbor":"data:application/cbor;base64,gwGCAgOCBAU=","func":"func_output","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want { + if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","base64":"Eu8pMP8=","base64url":"zLuq_w","json":{"some":"json"},"cbor":"data:application/cbor;base64,gwGCAgOCBAU=","func":"func_output","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -446,6 +451,8 @@ func TestFieldsDisabled(t *testing.T) { Stringer("stringer", net.IP{127, 0, 0, 1}). Bytes("bytes", []byte("bar")). Hex("hex", []byte{0x12, 0xef}). + Base64("base64", []byte{0x12, 0xef, 0x29, 0x30, 0xff}). + Base64Custom(base64.RawURLEncoding, "base64url", []byte{0xcc, 0xbb, 0xaa, 0xff}). AnErr("some_err", nil). Err(errors.New("some error")). Func(func(e *Event) { e.Str("func", "func_output") }).