summaryrefslogtreecommitdiffstats
path: root/cjson/canonicaljson.go
blob: abc860a491bf2ac34850068382545a9a1c97de93 (plain)
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package cjson

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"sort"
	"strings"
)

/*
encodeCanonicalString is a helper function to canonicalize the passed string
according to the OLPC canonical JSON specification for strings (see
http://wiki.laptop.org/go/Canonical_JSON).  String canonicalization consists of
escaping backslashes ("\") and double quotes (") and wrapping the resulting
string in double quotes (").
*/
func encodeCanonicalString(s string) string {
	// Escape backslashes
	s = strings.ReplaceAll(s, "\\", "\\\\")
	// Escape double quotes
	s = strings.ReplaceAll(s, "\"", "\\\"")
	// Wrap with double quotes
	return fmt.Sprintf("\"%s\"", s)
}

/*
encodeCanonical is a helper function to recursively canonicalize the passed
object according to the OLPC canonical JSON specification (see
http://wiki.laptop.org/go/Canonical_JSON) and write it to the passed
*bytes.Buffer.  If canonicalization fails it returns an error.
*/
func encodeCanonical(obj interface{}, result *strings.Builder) (err error) {
	switch objAsserted := obj.(type) {
	case string:
		result.WriteString(encodeCanonicalString(objAsserted))

	case bool:
		if objAsserted {
			result.WriteString("true")
		} else {
			result.WriteString("false")
		}

	// The wrapping `EncodeCanonical` function decodes the passed json data with
	// `decoder.UseNumber` so that any numeric value is stored as `json.Number`
	// (instead of the default `float64`). This allows us to assert that it is a
	// non-floating point number, which are the only numbers allowed by the used
	// canonicalization specification.
	case json.Number:
		if _, err := objAsserted.Int64(); err != nil {
			panic(fmt.Sprintf("Can't canonicalize floating point number '%s'",
				objAsserted))
		}
		result.WriteString(objAsserted.String())

	case nil:
		result.WriteString("null")

	// Canonicalize slice
	case []interface{}:
		result.WriteString("[")
		for i, val := range objAsserted {
			if err := encodeCanonical(val, result); err != nil {
				return err
			}
			if i < (len(objAsserted) - 1) {
				result.WriteString(",")
			}
		}
		result.WriteString("]")

	case map[string]interface{}:
		result.WriteString("{")

		// Make a list of keys
		var mapKeys []string
		for key := range objAsserted {
			mapKeys = append(mapKeys, key)
		}
		// Sort keys
		sort.Strings(mapKeys)

		// Canonicalize map
		for i, key := range mapKeys {
			if err := encodeCanonical(key, result); err != nil {
				return err
			}

			result.WriteString(":")
			if err := encodeCanonical(objAsserted[key], result); err != nil {
				return err
			}
			if i < (len(mapKeys) - 1) {
				result.WriteString(",")
			}
			i++
		}
		result.WriteString("}")

	default:
		// We recover in a deferred function defined above
		panic(fmt.Sprintf("Can't canonicalize '%s' of type '%s'",
			objAsserted, reflect.TypeOf(objAsserted)))
	}
	return nil
}

/*
EncodeCanonical JSON canonicalizes the passed object and returns it as a byte
slice.  It uses the OLPC canonical JSON specification (see
http://wiki.laptop.org/go/Canonical_JSON).  If canonicalization fails the byte
slice is nil and the second return value contains the error.
*/
func EncodeCanonical(obj interface{}) (out []byte, err error) {
	// We use panic if an error occurs and recover in a deferred function,
	// which is always called before returning.
	// There we set the error that is returned eventually.
	defer func() {
		if r := recover(); r != nil {
			err = errors.New(r.(string))
		}
	}()

	// FIXME: Terrible hack to turn the passed struct into a map, converting
	// the struct's variable names to the json key names defined in the struct
	data, err := json.Marshal(obj)
	if err != nil {
		return nil, err
	}
	var jsonMap interface{}

	dec := json.NewDecoder(bytes.NewReader(data))
	dec.UseNumber()
	if err := dec.Decode(&jsonMap); err != nil {
		return nil, err
	}

	// Create a buffer and write the canonicalized JSON bytes to it
	var result strings.Builder
	// Allocate output result buffer with the input size.
	result.Grow(len(data))
	// Recursively encode the jsonmap
	if err := encodeCanonical(jsonMap, &result); err != nil {
		return nil, err
	}

	return []byte(result.String()), nil
}