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 }