diff --git a/dataext/structHash.go b/dataext/structHash.go new file mode 100644 index 0000000..9336ab9 --- /dev/null +++ b/dataext/structHash.go @@ -0,0 +1,254 @@ +package dataext + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "hash" + "io" + "reflect" + "sort" +) + +type StructHashOptions struct { + HashAlgo hash.Hash + Tag *string + SkipChannel bool + SkipFunc bool +} + +func StructHash(dat any, opt ...StructHashOptions) (r []byte, err error) { + defer func() { + if rec := recover(); rec != nil { + r = nil + err = errors.New(fmt.Sprintf("recovered panic: %v", rec)) + } + }() + + shopt := StructHashOptions{} + if len(opt) > 1 { + return nil, errors.New("multiple options supplied") + } else if len(opt) == 1 { + shopt = opt[0] + } + + if shopt.HashAlgo == nil { + shopt.HashAlgo = sha256.New() + } + + writer := new(bytes.Buffer) + + if langext.IsNil(dat) { + shopt.HashAlgo.Reset() + shopt.HashAlgo.Write(writer.Bytes()) + res := shopt.HashAlgo.Sum(nil) + return res, nil + } + + err = binarize(writer, reflect.ValueOf(dat), shopt) + if err != nil { + return nil, err + } + + shopt.HashAlgo.Reset() + shopt.HashAlgo.Write(writer.Bytes()) + res := shopt.HashAlgo.Sum(nil) + + return res, nil +} + +func writeBinarized(writer io.Writer, dat any) error { + tmp := bytes.Buffer{} + err := binary.Write(&tmp, binary.LittleEndian, dat) + if err != nil { + return err + } + err = binary.Write(writer, binary.LittleEndian, uint64(tmp.Len())) + if err != nil { + return err + } + _, err = writer.Write(tmp.Bytes()) + if err != nil { + return err + } + return nil +} + +func binarize(writer io.Writer, dat reflect.Value, opt StructHashOptions) error { + var err error + + err = binary.Write(writer, binary.LittleEndian, uint8(dat.Kind())) + switch dat.Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice, reflect.Interface: + if dat.IsNil() { + err = binary.Write(writer, binary.LittleEndian, uint64(0)) + if err != nil { + return err + } + return nil + } + } + + err = binary.Write(writer, binary.LittleEndian, uint64(len(dat.Type().String()))) + if err != nil { + return err + } + _, err = writer.Write([]byte(dat.Type().String())) + if err != nil { + return err + } + + switch dat.Type().Kind() { + case reflect.Invalid: + return errors.New("cannot binarize value of kind ") + case reflect.Bool: + return writeBinarized(writer, dat.Bool()) + case reflect.Int: + return writeBinarized(writer, int64(dat.Int())) + case reflect.Int8: + fallthrough + case reflect.Int16: + fallthrough + case reflect.Int32: + fallthrough + case reflect.Int64: + return writeBinarized(writer, dat.Interface()) + case reflect.Uint: + return writeBinarized(writer, uint64(dat.Int())) + case reflect.Uint8: + fallthrough + case reflect.Uint16: + fallthrough + case reflect.Uint32: + fallthrough + case reflect.Uint64: + return writeBinarized(writer, dat.Interface()) + case reflect.Uintptr: + return errors.New("cannot binarize value of kind ") + case reflect.Float32: + fallthrough + case reflect.Float64: + return writeBinarized(writer, dat.Interface()) + case reflect.Complex64: + return errors.New("cannot binarize value of kind ") + case reflect.Complex128: + return errors.New("cannot binarize value of kind ") + case reflect.Slice: + fallthrough + case reflect.Array: + return binarizeArrayOrSlice(writer, dat, opt) + case reflect.Chan: + if opt.SkipChannel { + return nil + } + return errors.New("cannot binarize value of kind ") + case reflect.Func: + if opt.SkipFunc { + return nil + } + return errors.New("cannot binarize value of kind ") + case reflect.Interface: + return binarize(writer, dat.Elem(), opt) + case reflect.Map: + return binarizeMap(writer, dat, opt) + case reflect.Pointer: + return binarize(writer, dat.Elem(), opt) + case reflect.String: + v := dat.String() + err = binary.Write(writer, binary.LittleEndian, uint64(len(v))) + if err != nil { + return err + } + _, err = writer.Write([]byte(v)) + if err != nil { + return err + } + return nil + case reflect.Struct: + return binarizeStruct(writer, dat, opt) + case reflect.UnsafePointer: + return errors.New("cannot binarize value of kind ") + default: + return errors.New("cannot binarize value of unknown kind <" + dat.Type().Kind().String() + ">") + } +} + +func binarizeStruct(writer io.Writer, dat reflect.Value, opt StructHashOptions) error { + err := binary.Write(writer, binary.LittleEndian, uint64(dat.NumField())) + if err != nil { + return err + } + + for i := 0; i < dat.NumField(); i++ { + + if opt.Tag != nil { + if _, ok := dat.Type().Field(i).Tag.Lookup(*opt.Tag); !ok { + continue + } + } + + err = binary.Write(writer, binary.LittleEndian, uint64(len(dat.Type().Field(i).Name))) + if err != nil { + return err + } + _, err = writer.Write([]byte(dat.Type().Field(i).Name)) + if err != nil { + return err + } + + err = binarize(writer, dat.Field(i), opt) + if err != nil { + return err + } + } + + return nil +} + +func binarizeArrayOrSlice(writer io.Writer, dat reflect.Value, opt StructHashOptions) error { + err := binary.Write(writer, binary.LittleEndian, uint64(dat.Len())) + if err != nil { + return err + } + + for i := 0; i < dat.Len(); i++ { + err := binarize(writer, dat.Index(i), opt) + if err != nil { + return err + } + } + + return nil +} + +func binarizeMap(writer io.Writer, dat reflect.Value, opt StructHashOptions) error { + err := binary.Write(writer, binary.LittleEndian, uint64(dat.Len())) + if err != nil { + return err + } + + sub := make([][]byte, 0, dat.Len()) + + for _, k := range dat.MapKeys() { + tmp := bytes.Buffer{} + err = binarize(&tmp, dat.MapIndex(k), opt) + if err != nil { + return err + } + sub = append(sub, tmp.Bytes()) + } + + sort.Slice(sub, func(i1, i2 int) bool { return bytes.Compare(sub[i1], sub[i2]) < 0 }) + + for _, v := range sub { + _, err = writer.Write(v) + if err != nil { + return err + } + } + + return nil +} diff --git a/dataext/structHash_test.go b/dataext/structHash_test.go new file mode 100644 index 0000000..7d0f489 --- /dev/null +++ b/dataext/structHash_test.go @@ -0,0 +1,143 @@ +package dataext + +import ( + "encoding/hex" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "testing" +) + +func noErrStructHash(t *testing.T, dat any, opt ...StructHashOptions) []byte { + res, err := StructHash(dat, opt...) + if err != nil { + t.Error(err) + t.FailNow() + return nil + } + return res +} + +func TestStructHashSimple(t *testing.T) { + + assertEqual(t, "209bf774af36cc3a045c152d9f1269ef3684ad819c1359ee73ff0283a308fefa", noErrStructHash(t, "Hello")) + assertEqual(t, "c32f3626b981ae2997db656f3acad3f1dc9d30ef6b6d14296c023e391b25f71a", noErrStructHash(t, 0)) + assertEqual(t, "01b781b03e9586b257d387057dfc70d9f06051e7d3c1e709a57e13cc8daf3e35", noErrStructHash(t, []byte{})) + assertEqual(t, "93e1dcd45c732fe0079b0fb3204c7c803f0921835f6bfee2e6ff263e73eed53c", noErrStructHash(t, []int{})) + assertEqual(t, "54f637a376aad55b3160d98ebbcae8099b70d91b9400df23fb3709855d59800a", noErrStructHash(t, []int{1, 2, 3})) + assertEqual(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", noErrStructHash(t, nil)) + assertEqual(t, "349a7db91aa78fd30bbaa7c7f9c7bfb2fcfe72869b4861162a96713a852f60d3", noErrStructHash(t, []any{1, "", nil})) + assertEqual(t, "ca51aab87808bf0062a4a024de6aac0c2bad54275cc857a4944569f89fd245ad", noErrStructHash(t, struct{}{})) + +} + +func TestStructHashSimpleStruct(t *testing.T) { + + type t0 struct { + F1 int + F2 []string + F3 *int + } + + assertEqual(t, "a90bff751c70c738bb5cfc9b108e783fa9c19c0bc9273458e0aaee6e74aa1b92", noErrStructHash(t, t0{ + F1: 10, + F2: []string{"1", "2", "3"}, + F3: nil, + })) + + assertEqual(t, "5d09090dc34ac59dd645f197a255f653387723de3afa1b614721ea5a081c675f", noErrStructHash(t, t0{ + F1: 10, + F2: []string{"1", "2", "3"}, + F3: langext.Ptr(99), + })) + +} + +func TestStructHashLayeredStruct(t *testing.T) { + + type t1_1 struct { + F10 float32 + F12 float64 + F15 bool + } + type t1_2 struct { + SV1 *t1_1 + SV2 *t1_1 + SV3 t1_1 + } + + assertEqual(t, "fd4ca071fb40a288fee4b7a3dfdaab577b30cb8f80f81ec511e7afd72dc3b469", noErrStructHash(t, t1_2{ + SV1: nil, + SV2: nil, + SV3: t1_1{ + F10: 1, + F12: 2, + F15: false, + }, + })) + assertEqual(t, "3fbf7c67d8121deda075cc86319a4e32d71744feb2cebf89b43bc682f072a029", noErrStructHash(t, t1_2{ + SV1: nil, + SV2: &t1_1{}, + SV3: t1_1{ + F10: 3, + F12: 4, + F15: true, + }, + })) + assertEqual(t, "b1791ccd1b346c3ede5bbffda85555adcd8216b93ffca23f14fe175ec47c5104", noErrStructHash(t, t1_2{ + SV1: &t1_1{}, + SV2: &t1_1{}, + SV3: t1_1{ + F10: 5, + F12: 6, + F15: false, + }, + })) + +} + +func TestStructHashMap(t *testing.T) { + + type t0 struct { + F1 int + F2 map[string]int + } + + assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{ + F1: 10, + F2: map[string]int{ + "x": 1, + "0": 2, + "a": 99, + }, + })) + + assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{ + F1: 10, + F2: map[string]int{ + "a": 99, + "x": 1, + "0": 2, + }, + })) + + m3 := make(map[string]int, 99) + m3["a"] = 0 + m3["x"] = 0 + m3["0"] = 0 + + m3["0"] = 99 + m3["x"] = 1 + m3["a"] = 2 + + assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{ + F1: 10, + F2: m3, + })) + +} + +func assertEqual(t *testing.T, expected string, actual []byte) { + actualStr := hex.EncodeToString(actual) + if actualStr != expected { + t.Errorf("values differ: Actual: '%v', Expected: '%v'", actualStr, expected) + } +}