package downloader import ( "bytes" "context" "sort" "sync" "github.com/go-faster/errors" "go.mau.fi/mautrix-telegram/pkg/gotd/crypto" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" "go.mau.fi/mautrix-telegram/pkg/gotd/tgerr" ) // ErrHashMismatch means that download hash verification was failed. var ErrHashMismatch = errors.New("file hash mismatch") type verifier struct { client schema hashes []tg.FileHash offset int64 mux sync.Mutex } func newVerifier(client schema, hashes ...tg.FileHash) *verifier { r := make([]tg.FileHash, len(hashes)) copy(r, hashes) sort.SliceStable(r, func(i, j int) bool { return r[i].Offset < r[j].Offset }) return &verifier{client: client, hashes: r} } func (v *verifier) pop() (tg.FileHash, bool) { if len(v.hashes) < 1 { return tg.FileHash{}, false } // Pop and move. hash := v.hashes[0] copy(v.hashes, v.hashes[1:]) v.hashes[len(v.hashes)-1] = tg.FileHash{} v.hashes = v.hashes[:len(v.hashes)-1] return hash, true } func (v *verifier) update(hashes ...tg.FileHash) (tg.FileHash, bool) { // If result is empty and queue is empty, so we can't return next hash. if len(hashes) < 1 { return tg.FileHash{}, false } // Sort hashes by offset. // Usually Telegram server returns sorted parts, but... // you never known what can they do. sort.SliceStable(hashes, func(i, j int) bool { return hashes[i].Offset < hashes[j].Offset }) last := hashes[len(hashes)-1] // Check if we have reached the end. // If current state offset is equal the last offset + limit (right border) // then we got all hashes. if last.Offset == v.offset-int64(last.Limit) { return tg.FileHash{}, false } // Otherwise, we update current offset and add hashes to the end of queue. v.offset = last.Offset + int64(last.Limit) v.hashes = append(v.hashes, hashes...) return v.pop() } func (v *verifier) next(ctx context.Context) (tg.FileHash, bool, error) { v.mux.Lock() defer v.mux.Unlock() hash, ok := v.pop() if ok { return hash, ok, nil } for { hashes, err := v.client.Hashes(ctx, v.offset) if flood, err := tgerr.FloodWait(ctx, err); err != nil { if flood || tgerr.Is(err, tg.ErrTimeout) { continue } return tg.FileHash{}, false, errors.Wrap(err, "get hashes") } hash, ok = v.update(hashes...) return hash, ok, nil } } func (v *verifier) verify(hash tg.FileHash, data []byte) bool { return bytes.Equal(crypto.SHA256(data), hash.Hash) }