-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from adityasaky/add-cjson
Move cjson code from in-toto-golang
- Loading branch information
Showing
2 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package cjson | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
"regexp" | ||
"sort" | ||
) | ||
|
||
/* | ||
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 { | ||
re := regexp.MustCompile(`([\"\\])`) | ||
return fmt.Sprintf("\"%s\"", re.ReplaceAllString(s, "\\$1")) | ||
} | ||
|
||
/* | ||
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 *bytes.Buffer) (err error) { | ||
// Since this function is called recursively, 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)) | ||
} | ||
}() | ||
|
||
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 { | ||
// Note: `key` must be a `string` (see `case map[string]interface{}`) and | ||
// canonicalization of strings cannot err out (see `case string`), thus | ||
// no error handling is needed here. | ||
encodeCanonical(key, result) | ||
|
||
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{}) ([]byte, error) { | ||
// 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 bytes.Buffer | ||
if err := encodeCanonical(jsonMap, &result); err != nil { | ||
return nil, err | ||
} | ||
|
||
return result.Bytes(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package cjson | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
type KeyVal struct { | ||
Private string `json:"private"` | ||
Public string `json:"public"` | ||
Certificate string `json:"certificate,omitempty"` | ||
} | ||
|
||
type Key struct { | ||
KeyID string `json:"keyid"` | ||
KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"` | ||
KeyType string `json:"keytype"` | ||
KeyVal KeyVal `json:"keyval"` | ||
Scheme string `json:"scheme"` | ||
} | ||
|
||
func TestEncodeCanonical(t *testing.T) { | ||
objects := []interface{}{ | ||
Key{}, | ||
Key{ | ||
KeyVal: KeyVal{ | ||
Private: "priv", | ||
Public: "pub", | ||
}, | ||
KeyIDHashAlgorithms: []string{"hash"}, | ||
KeyID: "id", | ||
KeyType: "type", | ||
Scheme: "scheme", | ||
}, | ||
map[string]interface{}{ | ||
"true": true, | ||
"false": false, | ||
"nil": nil, | ||
"int": 3, | ||
"int2": float64(42), | ||
"string": `\"`, | ||
}, | ||
Key{ | ||
KeyVal: KeyVal{ | ||
Certificate: "cert", | ||
Private: "priv", | ||
Public: "pub", | ||
}, | ||
KeyIDHashAlgorithms: []string{"hash"}, | ||
KeyID: "id", | ||
KeyType: "type", | ||
Scheme: "scheme", | ||
}, | ||
json.RawMessage(`{"_type":"targets","spec_version":"1.0","version":0,"expires":"0001-01-01T00:00:00Z","targets":{},"custom":{"test":true}}`), | ||
} | ||
expectedResult := []string{ | ||
`{"keyid":"","keyid_hash_algorithms":null,"keytype":"","keyval":{"private":"","public":""},"scheme":""}`, | ||
`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"private":"priv","public":"pub"},"scheme":"scheme"}`, | ||
`{"false":false,"int":3,"int2":42,"nil":null,"string":"\\\"","true":true}`, | ||
`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"certificate":"cert","private":"priv","public":"pub"},"scheme":"scheme"}`, | ||
`{"_type":"targets","custom":{"test":true},"expires":"0001-01-01T00:00:00Z","spec_version":"1.0","targets":{},"version":0}`, | ||
} | ||
for i := 0; i < len(objects); i++ { | ||
result, err := EncodeCanonical(objects[i]) | ||
|
||
if string(result) != expectedResult[i] || err != nil { | ||
t.Errorf("EncodeCanonical returned (%s, %s), expected (%s, nil)", | ||
result, err, expectedResult[i]) | ||
} | ||
} | ||
} | ||
|
||
func TestEncodeCanonicalErr(t *testing.T) { | ||
objects := []interface{}{ | ||
map[string]interface{}{"float": 3.14159265359}, | ||
TestEncodeCanonical, | ||
} | ||
errPart := []string{ | ||
"Can't canonicalize floating point number", | ||
"unsupported type: func(", | ||
} | ||
|
||
for i := 0; i < len(objects); i++ { | ||
result, err := EncodeCanonical(objects[i]) | ||
if err == nil || !strings.Contains(err.Error(), errPart[i]) { | ||
t.Errorf("EncodeCanonical returned (%s, %s), expected '%s' error", | ||
result, err, errPart[i]) | ||
} | ||
} | ||
} | ||
|
||
func TestencodeCanonical(t *testing.T) { | ||
expectedError := "Can't canonicalize" | ||
|
||
objects := []interface{}{ | ||
TestencodeCanonical, | ||
[]interface{}{TestencodeCanonical}, | ||
} | ||
|
||
for i := 0; i < len(objects); i++ { | ||
var result bytes.Buffer | ||
err := encodeCanonical(objects[i], &result) | ||
if err == nil || !strings.Contains(err.Error(), expectedError) { | ||
t.Errorf("EncodeCanonical returned '%s', expected '%s' error", | ||
err, expectedError) | ||
} | ||
} | ||
} |