-
Notifications
You must be signed in to change notification settings - Fork 22
/
check.go
209 lines (189 loc) · 4.96 KB
/
check.go
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/pkg/xattr"
)
const xattrSha256 = "user.shatag.sha256"
const xattrTs = "user.shatag.ts"
const zeroSha256 = "0000000000000000000000000000000000000000000000000000000000000000"
type fileTimestamp struct {
s uint64
ns uint32
}
func (ts *fileTimestamp) prettyPrint() string {
return fmt.Sprintf("%010d.%09d", ts.s, ts.ns)
}
// equalTruncatedTimestamp compares ts and ts2 with 100ns resolution (Linux) or 1s (MacOS).
// Why 100ns? That's what Samba and the Linux SMB client supports.
// Why 1s? That's what the MacOS SMB client supports.
func (ts *fileTimestamp) equalTruncatedTimestamp(ts2 *fileTimestamp) bool {
if ts.s != ts2.s {
return false
}
// We only look at integer seconds on MacOS, so we are done here.
if runtime.GOOS == "darwin" {
return true
}
if ts.ns/100 != ts2.ns/100 {
return false
}
return true
}
type fileAttr struct {
ts fileTimestamp
sha256 []byte
}
func (a *fileAttr) prettyPrint() string {
return fmt.Sprintf("%s %s", string(a.sha256), a.ts.prettyPrint())
}
// getStoredAttr reads the stored extendend attributes from a file. The file
// should look like this:
//
// $ getfattr -d foo.txt
// user.shatag.sha256="dc9fe2260fd6748b29532be0ca2750a50f9eca82046b15497f127eba6dda90e8"
// user.shatag.ts="1560177334.020775051"
func getStoredAttr(f *os.File) (attr fileAttr, err error) {
attr.sha256 = []byte(zeroSha256)
val, err := xattr.FGet(f, xattrSha256)
if err == nil {
copy(attr.sha256, val)
}
val, err = xattr.FGet(f, xattrTs)
if err == nil {
parts := strings.SplitN(string(val), ".", 2)
attr.ts.s, _ = strconv.ParseUint(parts[0], 10, 64)
if len(parts) > 1 {
ns64, _ := strconv.ParseUint(parts[1], 10, 32)
attr.ts.ns = uint32(ns64)
}
}
return attr, nil
}
// getMtime reads the actual modification time of file "f" from disk.
func getMtime(f *os.File) (ts fileTimestamp, err error) {
fi, err := f.Stat()
if err != nil {
return
}
ts.s = uint64(fi.ModTime().Unix())
ts.ns = uint32(fi.ModTime().Nanosecond())
return
}
// getActualAttr reads the actual modification time and hashes the file content.
func getActualAttr(f *os.File) (attr fileAttr, err error) {
attr.sha256 = []byte(zeroSha256)
attr.ts, err = getMtime(f)
if err != nil {
return attr, err
}
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return attr, err
}
// Check if the file was modified while we were computing the hash
ts2, err := getMtime(f)
if err != nil {
return attr, err
} else if attr.ts != ts2 {
return attr, syscall.EINPROGRESS
}
attr.sha256 = []byte(fmt.Sprintf("%x", h.Sum(nil)))
return attr, nil
}
// printComparison prints something like this:
//
// stored: faa28bfa6332264571f28b4131b0673f0d55a31a2ccf5c873c435c235647bf76 1560177189.769244818
// actual: dc9fe2260fd6748b29532be0ca2750a50f9eca82046b15497f127eba6dda90e8 1560177334.020775051
func printComparison(stored fileAttr, actual fileAttr) {
fmt.Printf(" stored: %s\n actual: %s\n", stored.prettyPrint(), actual.prettyPrint())
}
func checkFile(fn string) {
stats.total++
f, err := os.Open(fn)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
stats.errorsOpening++
return
}
defer f.Close()
if args.remove {
if err = removeAttr(f); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
stats.errorsOther++
return
}
if !args.q {
fmt.Printf("<removed xattr> %s\n", fn)
}
stats.ok++
return
}
stored, _ := getStoredAttr(f)
actual, err := getActualAttr(f)
if err == syscall.EINPROGRESS {
if !args.qq {
fmt.Printf("<concurrent modification> %s\n", fn)
}
stats.inprogress++
return
} else if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
stats.errorsOther++
return
}
var allZeroTimeStamp fileTimestamp
if stored.ts.equalTruncatedTimestamp(&actual.ts) {
if bytes.Equal(stored.sha256, actual.sha256) {
if !args.q {
fmt.Printf("<ok> %s\n", fn)
}
stats.ok++
return
}
fixing := " Keeping hash as-is (use -fix to force hash update)."
if args.fix {
fixing = " Fixing hash (-fix was passed)."
}
fmt.Fprintf(os.Stderr, "Error: corrupt file %q. %s\n", fn, fixing)
fmt.Printf("<corrupt> %s\n", fn)
stats.corrupt++
} else if bytes.Equal(stored.sha256, actual.sha256) {
if !args.qq {
fmt.Printf("<timechange> %s\n", fn)
}
stats.timechange++
} else if bytes.Equal(stored.sha256, []byte(zeroSha256)) && (stored.ts == allZeroTimeStamp) {
// no metadata indicates a 'new' file
if !args.qq {
fmt.Printf("<new> %s\n", fn)
}
stats.newfile++
} else {
// timestamp is outdated
if !args.qq {
fmt.Printf("<outdated> %s\n", fn)
}
stats.outdated++
}
if !args.qq {
printComparison(stored, actual)
}
// Only update the stored attribute if it is not corrupted **OR**
// if argument '-fix' been given.
if stored.ts != actual.ts || args.fix {
err = storeAttr(f, actual)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
stats.errorsWritingXattr++
return
}
}