-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinvalidate.go
More file actions
171 lines (146 loc) · 3.96 KB
/
Copy pathinvalidate.go
File metadata and controls
171 lines (146 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package smartremote
import (
"crypto/sha256"
"errors"
"io"
)
// ErrChecksumMismatch is returned when a checksum verification fails.
// The affected blocks are automatically invalidated so they will be
// re-downloaded on next access.
var ErrChecksumMismatch = errors.New("checksum mismatch")
// InvalidateRange marks all blocks overlapping the byte range [start, end)
// as not downloaded, causing them to be re-downloaded on next read access
// or by the idle background downloader. This is useful when a checksum
// verification detects corrupted data.
func (f *File) InvalidateRange(start, end int64) error {
f.lk.Lock()
defer f.lk.Unlock()
return f.invalidateRange(start, end)
}
// invalidateRange is the internal implementation that assumes the write lock
// is already held.
func (f *File) invalidateRange(start, end int64) error {
if start < 0 {
return errors.New("invalid range: negative start")
}
if end < start {
return errors.New("invalid range: end before start")
}
if start == end {
return nil // empty range, nothing to do
}
if err := f.getSize(); err != nil {
return err
}
if end > f.size {
end = f.size
}
if start >= f.size {
return nil // range is beyond file
}
firstBlock := uint64(start / f.blkSize)
lastBlock := uint64((end - 1) / f.blkSize)
f.status.RemoveRange(firstBlock, lastBlock+1)
f.complete = false
return f.savePart()
}
// Verify downloads the entire file if needed, then computes its SHA-256 hash
// and compares it to expected. If the hashes do not match, all blocks are
// invalidated and ErrChecksumMismatch is returned.
func (f *File) Verify(expected [32]byte) error {
// Ensure the entire file is downloaded
if err := f.Complete(); err != nil {
return err
}
// Read size (file is complete so this is safe without lock)
f.lk.RLock()
size := f.size
f.lk.RUnlock()
// Compute SHA-256 of the local file (no lock needed - file is complete
// and os.File.ReadAt is goroutine-safe)
actual, err := f.hashRange(0, size)
if err != nil {
return err
}
if actual == expected {
return nil
}
// Mismatch: invalidate everything
f.lk.Lock()
f.invalidateRange(0, f.size)
f.lk.Unlock()
return ErrChecksumMismatch
}
// VerifyRange downloads the blocks covering [start, end) if needed, then
// computes the SHA-256 hash of that byte range and compares it to expected.
// If the hashes do not match, the blocks in the range are invalidated and
// ErrChecksumMismatch is returned.
func (f *File) VerifyRange(start, end int64, expected [32]byte) error {
if start < 0 {
return errors.New("invalid range: negative start")
}
if end <= start {
return errors.New("invalid range: end must be after start")
}
// Ensure blocks are downloaded
f.lk.Lock()
if err := f.getSize(); err != nil {
f.lk.Unlock()
return err
}
if end > f.size {
end = f.size
}
if start >= f.size {
f.lk.Unlock()
return errors.New("invalid range: start beyond file size")
}
firstBlock := uint32(start / f.blkSize)
lastBlock := uint32((end - 1) / f.blkSize)
err := f.needBlocks(firstBlock, lastBlock)
f.lk.Unlock()
if err != nil {
return err
}
// Compute SHA-256 of the range (no lock needed - blocks are downloaded
// and os.File.ReadAt is goroutine-safe)
actual, err := f.hashRange(start, end)
if err != nil {
return err
}
if actual == expected {
return nil
}
// Mismatch: invalidate the range
f.lk.Lock()
f.invalidateRange(start, end)
f.lk.Unlock()
return ErrChecksumMismatch
}
// hashRange computes SHA-256 of bytes [start, end) from the local file.
func (f *File) hashRange(start, end int64) ([32]byte, error) {
h := sha256.New()
buf := make([]byte, f.blkSize)
pos := start
for pos < end {
n := int64(len(buf))
if pos+n > end {
n = end - pos
}
rn, err := f.local.ReadAt(buf[:n], pos)
if rn > 0 {
h.Write(buf[:rn])
}
if err != nil && err != io.EOF {
var zero [32]byte
return zero, err
}
pos += int64(rn)
if rn == 0 {
break
}
}
var result [32]byte
copy(result[:], h.Sum(nil))
return result, nil
}