Skip to content

Commit

Permalink
Merge pull request #10 from adityasaky/add-cjson
Browse files Browse the repository at this point in the history
Move cjson code from in-toto-golang
  • Loading branch information
adityasaky authored Dec 1, 2021
2 parents 1966c4a + 68a4286 commit 994000d
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 0 deletions.
145 changes: 145 additions & 0 deletions cjson/canonicaljson.go
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
}
110 changes: 110 additions & 0 deletions cjson/canonicaljson_test.go
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)
}
}
}

0 comments on commit 994000d

Please sign in to comment.