Commit a09acce8 authored by Thorsten Alteholz's avatar Thorsten Alteholz

New upstream version 0.0~git20180104.62a607e

parents
# added hound style checking
language: go
go:
- 1.8
- 1.9
- tip
ISC license
Copyright (c) 2014, Frank Rosquin
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# structhash [![GoDoc](https://godoc.org/github.com/cnf/structhash?status.svg)](https://godoc.org/github.com/cnf/structhash) [![Build Status](https://travis-ci.org/cnf/structhash.svg?branch=master)](https://travis-ci.org/cnf/structhash)
structhash is a Go library for generating hash strings of arbitrary data structures.
## Features
* fields may be ignored or renamed (like in `json.Marshal`, but using different struct tag)
* fields may be serialized
* fields may be versioned
* fields order in struct doesn't matter (unlike `json.Marshal`)
* nil values are treated equally to zero values
## Installation
Standard `go get`:
```
$ go get github.com/cnf/structhash
```
## Documentation
For usage and examples see the [Godoc](http://godoc.org/github.com/cnf/structhash).
## Quick start
```go
package main
import (
"fmt"
"crypto/md5"
"crypto/sha1"
"github.com/cnf/structhash"
)
type S struct {
Str string
Num int
}
func main() {
s := S{"hello", 123}
hash, err := structhash.Hash(s, 1)
if err != nil {
panic(err)
}
fmt.Println(hash)
// Prints: v1_41011bfa1a996db6d0b1075981f5aa8f
fmt.Println(structhash.Version(hash))
// Prints: 1
fmt.Printf("%x\n", structhash.Md5(s, 1))
// Prints: 41011bfa1a996db6d0b1075981f5aa8f
fmt.Printf("%x\n", structhash.Sha1(s, 1))
// Prints: 5ff72df7212ce8c55838fb3ec6ad0c019881a772
fmt.Printf("%x\n", md5.Sum(structhash.Dump(s, 1)))
// Prints: 41011bfa1a996db6d0b1075981f5aa8f
fmt.Printf("%x\n", sha1.Sum(structhash.Dump(s, 1)))
// Prints: 5ff72df7212ce8c55838fb3ec6ad0c019881a772
}
```
## Struct tags
structhash supports struct tags in the following forms:
* `hash:"-"`, or
* `hash:"name:{string} version:{number} lastversion:{number} method:{string}"`
All fields are optional and may be ommitted. Their semantics is:
* `-` - ignore field
* `name:{string}` - rename field (may be useful when you want to rename field but keep hashes unchanged for backward compatibility)
* `version:{number}` - ignore field if version passed to structhash is smaller than given number
* `lastversion:{number}` - ignore field if version passed to structhash is greater than given number
* `method:{string}` - use the return value of a field's method instead of the field itself
Example:
```go
type MyStruct struct {
Ignored string `hash:"-"`
Renamed string `hash:"name:OldName version:1"`
Legacy string `hash:"version:1 lastversion:2"`
Serialized error `hash:"method:Error"`
}
```
## Nil values
When hash is calculated, nil pointers, nil slices, and nil maps are treated equally to zero values of corresponding type. E.g., nil pointer to string is equivalent to empty string, and nil slice is equivalent to empty slice.
/*
Package structhash creates hash strings from arbitrary go data structures.
*/
package structhash
package structhash
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
// Version returns the version of the supplied hash as an integer
// or -1 on failure
func Version(h string) int {
if h == "" {
return -1
}
if h[0] != 'v' {
return -1
}
if spos := strings.IndexRune(h[1:], '_'); spos >= 0 {
n, e := strconv.Atoi(h[1 : spos+1])
if e != nil {
return -1
}
return n
}
return -1
}
// Hash takes a data structure and returns a hash string of that data structure
// at the version asked.
//
// This function uses md5 hashing function and default formatter. See also Dump()
// function.
func Hash(c interface{}, version int) (string, error) {
return fmt.Sprintf("v%d_%x", version, Md5(c, version)), nil
}
// Dump takes a data structure and returns its byte representation. This can be
// useful if you need to use your own hashing function or formatter.
func Dump(c interface{}, version int) []byte {
return serialize(c, version)
}
// Md5 takes a data structure and returns its md5 hash.
// This is a shorthand for md5.Sum(Dump(c, version)).
func Md5(c interface{}, version int) []byte {
sum := md5.Sum(Dump(c, version))
return sum[:]
}
// Sha1 takes a data structure and returns its sha1 hash.
// This is a shorthand for sha1.Sum(Dump(c, version)).
func Sha1(c interface{}, version int) []byte {
sum := sha1.Sum(Dump(c, version))
return sum[:]
}
type item struct {
name string
value reflect.Value
}
type itemSorter []item
func (s itemSorter) Len() int {
return len(s)
}
func (s itemSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s itemSorter) Less(i, j int) bool {
return s[i].name < s[j].name
}
type tagError string
func (e tagError) Error() string {
return "incorrect tag " + string(e)
}
type structFieldFilter func(reflect.StructField, *item) (bool, error)
func writeValue(buf *bytes.Buffer, val reflect.Value, fltr structFieldFilter) {
switch val.Kind() {
case reflect.String:
buf.WriteByte('"')
buf.WriteString(val.String())
buf.WriteByte('"')
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
buf.WriteString(strconv.FormatInt(val.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
buf.WriteString(strconv.FormatUint(val.Uint(), 10))
case reflect.Float32, reflect.Float64:
buf.WriteString(strconv.FormatFloat(val.Float(), 'E', -1, 64))
case reflect.Bool:
if val.Bool() {
buf.WriteByte('t')
} else {
buf.WriteByte('f')
}
case reflect.Ptr:
if !val.IsNil() || val.Type().Elem().Kind() == reflect.Struct {
writeValue(buf, reflect.Indirect(val), fltr)
} else {
writeValue(buf, reflect.Zero(val.Type().Elem()), fltr)
}
case reflect.Array, reflect.Slice:
buf.WriteByte('[')
len := val.Len()
for i := 0; i < len; i++ {
if i != 0 {
buf.WriteByte(',')
}
writeValue(buf, val.Index(i), fltr)
}
buf.WriteByte(']')
case reflect.Map:
mk := val.MapKeys()
items := make([]item, len(mk), len(mk))
// Get all values
for i, _ := range items {
items[i].name = formatValue(mk[i], fltr)
items[i].value = val.MapIndex(mk[i])
}
// Sort values by key
sort.Sort(itemSorter(items))
buf.WriteByte('[')
for i, _ := range items {
if i != 0 {
buf.WriteByte(',')
}
buf.WriteString(items[i].name)
buf.WriteByte(':')
writeValue(buf, items[i].value, fltr)
}
buf.WriteByte(']')
case reflect.Struct:
vtype := val.Type()
flen := vtype.NumField()
items := make([]item, 0, flen)
// Get all fields
for i := 0; i < flen; i++ {
field := vtype.Field(i)
it := item{field.Name, val.Field(i)}
if fltr != nil {
ok, err := fltr(field, &it)
if err != nil && strings.Contains(err.Error(), "method:") {
panic(err)
}
if !ok {
continue
}
}
items = append(items, it)
}
// Sort fields by name
sort.Sort(itemSorter(items))
buf.WriteByte('{')
for i, _ := range items {
if i != 0 {
buf.WriteByte(',')
}
buf.WriteString(items[i].name)
buf.WriteByte(':')
writeValue(buf, items[i].value, fltr)
}
buf.WriteByte('}')
case reflect.Interface:
writeValue(buf, reflect.ValueOf(val.Interface()), fltr)
default:
buf.WriteString(val.String())
}
}
func formatValue(val reflect.Value, fltr structFieldFilter) string {
if val.Kind() == reflect.String {
return "\"" + val.String() + "\""
}
var buf bytes.Buffer
writeValue(&buf, val, fltr)
return string(buf.Bytes())
}
func filterField(f reflect.StructField, i *item, version int) (bool, error) {
var err error
ver := 0
lastver := -1
if str := f.Tag.Get("hash"); str != "" {
if str == "-" {
return false, nil
}
for _, tag := range strings.Split(str, " ") {
args := strings.Split(strings.TrimSpace(tag), ":")
if len(args) != 2 {
return false, tagError(tag)
}
switch args[0] {
case "name":
i.name = args[1]
case "version":
if ver, err = strconv.Atoi(args[1]); err != nil {
return false, tagError(tag)
}
case "lastversion":
if lastver, err = strconv.Atoi(args[1]); err != nil {
return false, tagError(tag)
}
case "method":
property, found := f.Type.MethodByName(strings.TrimSpace(args[1]))
if !found || property.Type.NumOut() != 1 {
return false, tagError(tag)
}
i.value = property.Func.Call([]reflect.Value{i.value})[0]
}
}
} else {
if str := f.Tag.Get("lastversion"); str != "" {
if lastver, err = strconv.Atoi(str); err != nil {
return false, tagError(str)
}
}
if str := f.Tag.Get("version"); str != "" {
if ver, err = strconv.Atoi(str); err != nil {
return false, tagError(str)
}
}
}
if lastver != -1 && lastver < version {
return false, nil
}
if ver > version {
return false, nil
}
return true, nil
}
func serialize(object interface{}, version int) []byte {
var buf bytes.Buffer
writeValue(&buf, reflect.ValueOf(object),
func(f reflect.StructField, i *item) (bool, error) {
return filterField(f, i, version)
})
return buf.Bytes()
}
package structhash
import (
"encoding/json"
"testing"
)
type BenchData struct {
Bool bool
String string
Int int
Uint uint
Map map[string]*BenchData
Slice []*BenchData
Struct *BenchData
}
type BenchTags struct {
Bool bool `json:"f1" hash:"name:f1"`
String string `json:"f2" hash:"name:f2"`
Int int `json:"f3" hash:"name:f3"`
Uint uint `json:"f4" hash:"name:f4"`
}
func benchDataSimple() *BenchData {
return &BenchData{true, "simple", -123, 321, nil, nil, nil}
}
func benchDataFull() *BenchData {
foo := benchDataSimple()
bar := benchDataSimple()
m := make(map[string]*BenchData)
m["foo"] = foo
m["bar"] = bar
s := []*BenchData{
foo,
bar,
}
return &BenchData{true, "hello", -123, 321, m, s, foo}
}
func benchDataTags() *BenchTags {
return &BenchTags{true, "tags", -123, 321}
}
func BenchmarkSimpleJSON(b *testing.B) {
s := benchDataSimple()
for i := 0; i < b.N; i++ {
json.Marshal(s)
}
}
func BenchmarkSimpleDump(b *testing.B) {
s := benchDataSimple()
for i := 0; i < b.N; i++ {
Dump(s, 1)
}
}
func BenchmarkFullJSON(b *testing.B) {
s := benchDataFull()
for i := 0; i < b.N; i++ {
json.Marshal(s)
}
}
func BenchmarkFullDump(b *testing.B) {
s := benchDataFull()
for i := 0; i < b.N; i++ {
Dump(s, 1)
}
}
func BenchmarkTagsJSON(b *testing.B) {
s := benchDataTags()
for i := 0; i < b.N; i++ {
json.Marshal(s)
}
}
func BenchmarkTagsDump(b *testing.B) {
s := benchDataTags()
for i := 0; i < b.N; i++ {
Dump(s, 1)
}
}
package structhash
import (
"crypto/md5"
"crypto/sha1"
"fmt"
)
func ExampleHash() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
Spouse *Person
}
bill := &Person{
Name: "Bill",
Age: 24,
Emails: []string{"bob@foo.org", "bob@bar.org"},
Extra: map[string]string{
"facebook": "Bob42",
},
}
bob := &Person{
Name: "Bob",
Age: 42,
Emails: []string{"bob@foo.org", "bob@bar.org"},
Extra: map[string]string{
"facebook": "Bob42",
},
Spouse: bill,
}
hash, err := Hash(bob, 1)
if err != nil {
panic(err)
}
fmt.Printf("%s", hash)
// Output:
// v1_6a50d73f3bd0b9ebd001a0b610f387f0
}
func ExampleHash_tags() {
type Person struct {
Ignored string `hash:"-"`
NewName string `hash:"name:OldName version:1"`
Age int `hash:"version:1"`
Emails []string `hash:"version:1"`
Extra map[string]string `hash:"version:1 lastversion:2"`
Spouse *Person `hash:"version:2"`
}
bill := &Person{
NewName: "Bill",
Age: 24,
Emails: []string{"bob@foo.org", "bob@bar.org"},
Extra: map[string]string{
"facebook": "Bob42",
},
}
bob := &Person{
NewName: "Bob",
Age: 42,
Emails: []string{"bob@foo.org", "bob@bar.org"},
Extra: map[string]string{
"facebook": "Bob42",
},
Spouse: bill,
}
hashV1, err := Hash(bob, 1)
if err != nil {
panic(err)
}
hashV2, err := Hash(bob, 2)
if err != nil {
panic(err)
}
hashV3, err := Hash(bob, 3)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", hashV1)
fmt.Printf("%s\n", hashV2)
fmt.Printf("%s\n", hashV3)
// Output:
// v1_45d8a54c5f5fd287f197b26d128882cd
// v2_babd7618f29036f5564816bee6c8a037
// v3_012b06239f942549772c9139d66c121e
}
func ExampleDump() {
type Person struct {
Name string
Age int
Emails []string
Extra map[string]string
Spouse *Person
}
bill := &Person{
Name: "Bill",
Age: 24,
Emails: []string{"bob@foo.org", "bob@bar.org"},
Extra: map[string]string{
"facebook": "Bob42",
},
}
bob := &Person{
Name: "Bob",
Age: 42,
Emails: []string{"bob@foo.org", "bob@bar.org"},
Extra: map[string]string{
"facebook": "Bob42",
},
Spouse: bill,
}
fmt.Printf("md5: %x\n", md5.Sum(Dump(bob, 1)))
fmt.Printf("sha1: %x\n", sha1.Sum(Dump(bob, 1)))
// Output:
// md5: 6a50d73f3bd0b9ebd001a0b610f387f0
// sha1: c45f097a37366eaaf6ffbc7357c2272cd8fb64f6
}
func ExampleVersion() {
// A hash string gotten from Hash(). Returns the version as an int.
i := Version("v1_55743877f3ffd5fc834e97bc43a6e7bd")
fmt.Printf("%d", i)
// Output:
// 1
}
func ExampleVersion_errors() {
// A hash string gotten from Hash(). Returns -1 on error.
i := Version("va_55743877f3ffd5fc834e97bc43a6e7bd")
fmt.Printf("%d", i)
// Output:
// -1
}
package structhash
import (
"fmt"
"testing"
)
type First struct {
Bool bool `version:"1"`
String string `version:"2"`
Int int `version:"1" lastversion:"1"`
Float float64 `version:"1"`
Struct *Second `version:"1"`
Uint uint `version:"1"`
}
type Second struct {
Map map[string]string `version:"1"`
Slice []int `version:"1"`
}
type Tags1 struct {
Int int `hash:"-"`
Str string `hash:"name:Foo version:1 lastversion:2"`
Bar string `hash:"version:1"`
}
type Tags2 struct {
Foo string
Bar string
}
type Tags3 struct {
Bar string
}
type Tags4 struct {
Data1 ambiguousData `hash:"method:Serialize"`
Data2 ambiguousData `hash:"method:Normalize"`
}
type Tags5 struct {
Data1 ambiguousData `hash:"method:UnknownMethod"`
}
type Nils struct {
Str *string
Int *int
Bool *bool
Map map[string]string
Slice []string
}
type unexportedTags struct {
foo string
bar string
aMap map[string]string
}
type interfaceStruct struct {
Name string
Interface1 interface{}
InterfaceIgnore interface{} `hash:"-"`
}
type ambiguousData struct {
Prefix string
Suffix string
}
func (p ambiguousData) Serialize() string {
return p.Prefix + p.Suffix
}
func (p ambiguousData) Normalize() ambiguousData {
return ambiguousData{p.Prefix + p.Suffix, ""}
}
func dataSetup() *First {
tmpmap := make(map[string]string)
tmpmap["foo"] = "bar"
tmpmap["baz"] = "go"
tmpslice := make([]int, 3)
tmpslice[0] = 0
tmpslice[1] = 1
tmpslice[2] = 2
return &First{
Bool: true,
String: "test",
Int: 123456789,
Float: 65.3458,
Struct: &Second{
Map: tmpmap,
Slice: tmpslice,
},
Uint: 1,
}
}
func TestHash(t *testing.T) {
v1Hash := "v1_e8e67581aee36d7237603381a9cbd9fc"
v2Hash := "v2_5e51490d7c24c4b7a9e63c04f55734eb"
data := dataSetup()
v1, err := Hash(data, 1)
if err != nil {
t.Error(err)
}
// fmt.Println(string(Dump(data, 1)))
if v1 != v1Hash {
t.Errorf("%s is not %s", v1, v1Hash)
}
v2, err := Hash(data, 2)
if err != nil {
t.Error(err)
}
// fmt.Println(string(Dump(data, 2)))
if v2 != v2Hash {
t.Errorf("%s is not %s", v2, v2Hash)
}
v1md5 := fmt.Sprintf("v1_%x", Md5(data, 1))
if v1md5 != v1Hash {
t.Errorf("%s is not %s", v1md5, v1Hash[3:])
}
v2md5 := fmt.Sprintf("v2_%x", Md5(data, 2))
if v2md5 != v2Hash {
t.Errorf("%s is not %s", v2md5, v2Hash[3:])
}
}
func TestTags(t *testing.T) {
t1 := Tags1{11, "foo", "bar"}