1package jsonutil
2
3import (
4	"bytes"
5	"compress/gzip"
6	"encoding/json"
7	"fmt"
8	"io"
9
10	"github.com/hashicorp/errwrap"
11	"github.com/hashicorp/vault/sdk/helper/compressutil"
12)
13
14// Encodes/Marshals the given object into JSON
15func EncodeJSON(in interface{}) ([]byte, error) {
16	if in == nil {
17		return nil, fmt.Errorf("input for encoding is nil")
18	}
19	var buf bytes.Buffer
20	enc := json.NewEncoder(&buf)
21	if err := enc.Encode(in); err != nil {
22		return nil, err
23	}
24	return buf.Bytes(), nil
25}
26
27// EncodeJSONAndCompress encodes the given input into JSON and compresses the
28// encoded value (using Gzip format BestCompression level, by default). A
29// canary byte is placed at the beginning of the returned bytes for the logic
30// in decompression method to identify compressed input.
31func EncodeJSONAndCompress(in interface{}, config *compressutil.CompressionConfig) ([]byte, error) {
32	if in == nil {
33		return nil, fmt.Errorf("input for encoding is nil")
34	}
35
36	// First JSON encode the given input
37	encodedBytes, err := EncodeJSON(in)
38	if err != nil {
39		return nil, err
40	}
41
42	if config == nil {
43		config = &compressutil.CompressionConfig{
44			Type:                 compressutil.CompressionTypeGzip,
45			GzipCompressionLevel: gzip.BestCompression,
46		}
47	}
48
49	return compressutil.Compress(encodedBytes, config)
50}
51
52// DecodeJSON tries to decompress the given data. The call to decompress, fails
53// if the content was not compressed in the first place, which is identified by
54// a canary byte before the compressed data. If the data is not compressed, it
55// is JSON decoded directly. Otherwise the decompressed data will be JSON
56// decoded.
57func DecodeJSON(data []byte, out interface{}) error {
58	if data == nil || len(data) == 0 {
59		return fmt.Errorf("'data' being decoded is nil")
60	}
61	if out == nil {
62		return fmt.Errorf("output parameter 'out' is nil")
63	}
64
65	// Decompress the data if it was compressed in the first place
66	decompressedBytes, uncompressed, err := compressutil.Decompress(data)
67	if err != nil {
68		return errwrap.Wrapf("failed to decompress JSON: {{err}}", err)
69	}
70	if !uncompressed && (decompressedBytes == nil || len(decompressedBytes) == 0) {
71		return fmt.Errorf("decompressed data being decoded is invalid")
72	}
73
74	// If the input supplied failed to contain the compression canary, it
75	// will be notified by the compression utility. Decode the decompressed
76	// input.
77	if !uncompressed {
78		data = decompressedBytes
79	}
80
81	return DecodeJSONFromReader(bytes.NewReader(data), out)
82}
83
84// Decodes/Unmarshals the given io.Reader pointing to a JSON, into a desired object
85func DecodeJSONFromReader(r io.Reader, out interface{}) error {
86	if r == nil {
87		return fmt.Errorf("'io.Reader' being decoded is nil")
88	}
89	if out == nil {
90		return fmt.Errorf("output parameter 'out' is nil")
91	}
92
93	dec := json.NewDecoder(r)
94
95	// While decoding JSON values, interpret the integer values as `json.Number`s instead of `float64`.
96	dec.UseNumber()
97
98	// Since 'out' is an interface representing a pointer, pass it to the decoder without an '&'
99	return dec.Decode(out)
100}
101