/
FileSpec.py
244 lines (197 loc) · 8.53 KB
/
FileSpec.py
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import os
import time
from panda3d.core import Filename, HashVal, VirtualFileSystem
class FileSpec:
""" This class represents a disk file whose hash and size
etc. were read from an xml file. This class provides methods to
verify whether the file on disk matches the version demanded by
the xml. """
def __init__(self):
self.actualFile = None
self.filename = None
self.size = 0
self.timestamp = 0
self.hash = None
def fromFile(self, packageDir, filename, pathname = None, st = None):
""" Reads the file information from the indicated file. If st
is supplied, it is the result of os.stat on the filename. """
vfs = VirtualFileSystem.getGlobalPtr()
filename = Filename(filename)
if pathname is None:
pathname = Filename(packageDir, filename)
self.filename = str(filename)
self.basename = filename.getBasename()
if st is None:
st = os.stat(pathname.toOsSpecific())
self.size = st.st_size
self.timestamp = st.st_mtime
self.readHash(pathname)
def readHash(self, pathname):
""" Reads the hash only from the indicated pathname. """
hv = HashVal()
hv.hashFile(pathname)
self.hash = hv.asHex()
def loadXml(self, xelement):
""" Reads the file information from the indicated XML
element. """
self.filename = xelement.Attribute('filename')
self.basename = None
if self.filename:
self.basename = Filename(self.filename).getBasename()
size = xelement.Attribute('size')
try:
self.size = int(size)
except:
self.size = 0
timestamp = xelement.Attribute('timestamp')
try:
self.timestamp = int(timestamp)
except:
self.timestamp = 0
self.hash = xelement.Attribute('hash')
def storeXml(self, xelement):
""" Adds the file information to the indicated XML
element. """
if self.filename:
xelement.SetAttribute('filename', self.filename)
if self.size:
xelement.SetAttribute('size', str(self.size))
if self.timestamp:
xelement.SetAttribute('timestamp', str(int(self.timestamp)))
if self.hash:
xelement.SetAttribute('hash', self.hash)
def storeMiniXml(self, xelement):
""" Adds the just the "mini" file information--size and
hash--to the indicated XML element. """
if self.size:
xelement.SetAttribute('size', str(self.size))
if self.hash:
xelement.SetAttribute('hash', self.hash)
def quickVerify(self, packageDir = None, pathname = None,
notify = None, correctSelf = False):
""" Performs a quick test to ensure the file has not been
modified. This test is vulnerable to people maliciously
attempting to fool the program (by setting datestamps etc.).
if correctSelf is True, then any discrepency is corrected by
updating the appropriate fields internally, making the
assumption that the file on disk is the authoritative version.
Returns true if it is intact, false if it is incorrect. If
correctSelf is true, raises OSError if the self-update is
impossible (for instance, because the file does not exist)."""
if not pathname:
pathname = Filename(packageDir, self.filename)
try:
st = os.stat(pathname.toOsSpecific())
except OSError:
# If the file is missing, the file fails.
if notify:
notify.debug("file not found: %s" % (pathname))
if correctSelf:
raise
return False
if st.st_size != self.size:
# If the size is wrong, the file fails.
if notify:
notify.debug("size wrong: %s" % (pathname))
if correctSelf:
self.__correctHash(packageDir, pathname, st, notify)
return False
if st.st_mtime == self.timestamp:
# If the size is right and the timestamp is right, the
# file passes.
if notify:
notify.debug("file ok: %s" % (pathname))
return True
if notify:
notify.debug("modification time wrong: %s" % (pathname))
# If the size is right but the timestamp is wrong, the file
# soft-fails. We follow this up with a hash check.
if not self.checkHash(packageDir, pathname, st):
# Hard fail, the hash is wrong.
if notify:
notify.debug("hash check wrong: %s" % (pathname))
notify.debug(" found %s, expected %s" % (self.actualFile.hash, self.hash))
if correctSelf:
self.__correctHash(packageDir, pathname, st, notify)
return False
if notify:
notify.debug("hash check ok: %s" % (pathname))
# The hash is OK after all. Change the file's timestamp back
# to what we expect it to be, so we can quick-verify it
# successfully next time.
if correctSelf:
# Or update our own timestamp.
self.__correctTimestamp(pathname, st, notify)
return False
else:
self.__updateTimestamp(pathname, st)
return True
def fullVerify(self, packageDir = None, pathname = None, notify = None):
""" Performs a more thorough test to ensure the file has not
been modified. This test is less vulnerable to malicious
attacks, since it reads and verifies the entire file.
Returns true if it is intact, false if it needs to be
redownloaded. """
if not pathname:
pathname = Filename(packageDir, self.filename)
try:
st = os.stat(pathname.toOsSpecific())
except OSError:
# If the file is missing, the file fails.
if notify:
notify.debug("file not found: %s" % (pathname))
return False
if st.st_size != self.size:
# If the size is wrong, the file fails;
if notify:
notify.debug("size wrong: %s" % (pathname))
return False
if not self.checkHash(packageDir, pathname, st):
# Hard fail, the hash is wrong.
if notify:
notify.debug("hash check wrong: %s" % (pathname))
notify.debug(" found %s, expected %s" % (self.actualFile.hash, self.hash))
return False
if notify:
notify.debug("hash check ok: %s" % (pathname))
# The hash is OK. If the timestamp is wrong, change it back
# to what we expect it to be, so we can quick-verify it
# successfully next time.
if st.st_mtime != self.timestamp:
self.__updateTimestamp(pathname, st)
return True
def __updateTimestamp(self, pathname, st):
# On Windows, we have to change the file to read-write before
# we can successfully update its timestamp.
try:
os.chmod(pathname.toOsSpecific(), 0o755)
os.utime(pathname.toOsSpecific(), (st.st_atime, self.timestamp))
os.chmod(pathname.toOsSpecific(), 0o555)
except OSError:
pass
def __correctTimestamp(self, pathname, st, notify):
""" Corrects the internal timestamp to match the one on
disk. """
if notify:
notify.info("Correcting timestamp of %s to %d (%s)" % (
self.filename, st.st_mtime, time.asctime(time.localtime(st.st_mtime))))
self.timestamp = st.st_mtime
def checkHash(self, packageDir, pathname, st):
""" Returns true if the file has the expected md5 hash, false
otherwise. As a side effect, stores a FileSpec corresponding
to the on-disk file in self.actualFile. """
fileSpec = FileSpec()
fileSpec.fromFile(packageDir, self.filename,
pathname = pathname, st = st)
self.actualFile = fileSpec
return (fileSpec.hash == self.hash)
def __correctHash(self, packageDir, pathname, st, notify):
""" Corrects the internal hash to match the one on disk. """
if not self.actualFile:
self.checkHash(packageDir, pathname, st)
if notify:
notify.info("Correcting hash %s to %s" % (
self.filename, self.actualFile.hash))
self.hash = self.actualFile.hash
self.size = self.actualFile.size
self.timestamp = self.actualFile.timestamp