1// Package mrhash implements the mailru hash, which is a modified SHA1.
2// If file size is less than or equal to the SHA1 block size (20 bytes),
3// its hash is simply its data right-padded with zero bytes.
4// Hash sum of a larger file is computed as a SHA1 sum of the file data
5// bytes concatenated with a decimal representation of the data length.
6package mrhash
7
8import (
9	"crypto/sha1"
10	"encoding"
11	"encoding/hex"
12	"errors"
13	"hash"
14	"strconv"
15)
16
17const (
18	// BlockSize of the checksum in bytes.
19	BlockSize = sha1.BlockSize
20	// Size of the checksum in bytes.
21	Size        = sha1.Size
22	startString = "mrCloud"
23	hashError   = "hash function returned error"
24)
25
26// Global errors
27var (
28	ErrorInvalidHash = errors.New("invalid hash")
29)
30
31type digest struct {
32	total int       // bytes written into hash so far
33	sha   hash.Hash // underlying SHA1
34	small []byte    // small content
35}
36
37// New returns a new hash.Hash computing the Mailru checksum.
38func New() hash.Hash {
39	d := &digest{}
40	d.Reset()
41	return d
42}
43
44// Write writes len(p) bytes from p to the underlying data stream. It returns
45// the number of bytes written from p (0 <= n <= len(p)) and any error
46// encountered that caused the write to stop early. Write must return a non-nil
47// error if it returns n < len(p). Write must not modify the slice data, even
48// temporarily.
49//
50// Implementations must not retain p.
51func (d *digest) Write(p []byte) (n int, err error) {
52	n, err = d.sha.Write(p)
53	if err != nil {
54		panic(hashError)
55	}
56	d.total += n
57	if d.total <= Size {
58		d.small = append(d.small, p...)
59	}
60	return n, nil
61}
62
63// Sum appends the current hash to b and returns the resulting slice.
64// It does not change the underlying hash state.
65func (d *digest) Sum(b []byte) []byte {
66	// If content is small, return it padded to Size
67	if d.total <= Size {
68		padded := make([]byte, Size)
69		copy(padded, d.small)
70		return append(b, padded...)
71	}
72	endString := strconv.Itoa(d.total)
73	copy, err := cloneSHA1(d.sha)
74	if err == nil {
75		_, err = copy.Write([]byte(endString))
76	}
77	if err != nil {
78		panic(hashError)
79	}
80	return copy.Sum(b)
81}
82
83// cloneSHA1 clones state of SHA1 hash
84func cloneSHA1(orig hash.Hash) (clone hash.Hash, err error) {
85	state, err := orig.(encoding.BinaryMarshaler).MarshalBinary()
86	if err != nil {
87		return nil, err
88	}
89	clone = sha1.New()
90	err = clone.(encoding.BinaryUnmarshaler).UnmarshalBinary(state)
91	return
92}
93
94// Reset resets the Hash to its initial state.
95func (d *digest) Reset() {
96	d.sha = sha1.New()
97	_, _ = d.sha.Write([]byte(startString))
98	d.total = 0
99}
100
101// Size returns the number of bytes Sum will return.
102func (d *digest) Size() int {
103	return Size
104}
105
106// BlockSize returns the hash's underlying block size.
107// The Write method must be able to accept any amount
108// of data, but it may operate more efficiently if all writes
109// are a multiple of the block size.
110func (d *digest) BlockSize() int {
111	return BlockSize
112}
113
114// Sum returns the Mailru checksum of the data.
115func Sum(data []byte) []byte {
116	var d digest
117	d.Reset()
118	_, _ = d.Write(data)
119	return d.Sum(nil)
120}
121
122// DecodeString converts a string to the Mailru hash
123func DecodeString(s string) ([]byte, error) {
124	b, err := hex.DecodeString(s)
125	if err != nil || len(b) != Size {
126		return nil, ErrorInvalidHash
127	}
128	return b, nil
129}
130
131// must implement this interface
132var (
133	_ hash.Hash = (*digest)(nil)
134)
135