241 lines
5.3 KiB
Go
241 lines
5.3 KiB
Go
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 structFieldFilter func(reflect.StructField) (string, bool)
|
|
|
|
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.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
buf.WriteString(strconv.FormatUint(val.Uint(), 10))
|
|
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 {
|
|
if name, ok := fltr(field); ok {
|
|
it.name = name
|
|
} else {
|
|
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('}')
|
|
default:
|
|
buf.WriteString(val.String())
|
|
}
|
|
}
|
|
|
|
func formatValue(val reflect.Value, fltr structFieldFilter) string {
|
|
if val.Kind() == reflect.String {
|
|
return "\"" + val.Interface().(string) + "\""
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
writeValue(&buf, val, fltr)
|
|
|
|
return string(buf.Bytes())
|
|
}
|
|
|
|
func filterField(f reflect.StructField, version int) (string, bool) {
|
|
var err error
|
|
name := f.Name
|
|
ver := 0
|
|
lastver := -1
|
|
if str := f.Tag.Get("hash"); str != "" {
|
|
if str == "-" {
|
|
return "", false
|
|
}
|
|
for _, tag := range strings.Split(str, " ") {
|
|
args := strings.Split(strings.TrimSpace(tag), ":")
|
|
if len(args) != 2 {
|
|
return "", false
|
|
}
|
|
switch args[0] {
|
|
case "name":
|
|
name = args[1]
|
|
case "version":
|
|
if ver, err = strconv.Atoi(args[1]); err != nil {
|
|
return "", false
|
|
}
|
|
case "lastversion":
|
|
if lastver, err = strconv.Atoi(args[1]); err != nil {
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if str := f.Tag.Get("lastversion"); str != "" {
|
|
if lastver, err = strconv.Atoi(str); err != nil {
|
|
return "", false
|
|
}
|
|
}
|
|
if str := f.Tag.Get("version"); str != "" {
|
|
if ver, err = strconv.Atoi(str); err != nil {
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
if lastver != -1 && lastver < version {
|
|
return "", false
|
|
}
|
|
if ver > version {
|
|
return "", false
|
|
}
|
|
return name, true
|
|
}
|
|
|
|
func serialize(object interface{}, version int) []byte {
|
|
var buf bytes.Buffer
|
|
|
|
writeValue(&buf, reflect.ValueOf(object),
|
|
func(f reflect.StructField) (string, bool) {
|
|
return filterField(f, version)
|
|
})
|
|
|
|
return buf.Bytes()
|
|
}
|