FileSpec.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import os
  2. import time
  3. from pandac.PandaModules import Filename, HashVal, VirtualFileSystem
  4. class FileSpec:
  5. """ This class represents a disk file whose hash and size
  6. etc. were read from an xml file. This class provides methods to
  7. verify whether the file on disk matches the version demanded by
  8. the xml. """
  9. def __init__(self):
  10. self.actualFile = None
  11. self.filename = None
  12. self.size = 0
  13. self.timestamp = 0
  14. self.hash = None
  15. def fromFile(self, packageDir, filename, pathname = None, st = None):
  16. """ Reads the file information from the indicated file. If st
  17. is supplied, it is the result of os.stat on the filename. """
  18. vfs = VirtualFileSystem.getGlobalPtr()
  19. filename = Filename(filename)
  20. if pathname is None:
  21. pathname = Filename(packageDir, filename)
  22. self.filename = filename.cStr()
  23. self.basename = filename.getBasename()
  24. if st is None:
  25. st = os.stat(pathname.toOsSpecific())
  26. self.size = st.st_size
  27. self.timestamp = st.st_mtime
  28. self.readHash(pathname)
  29. def readHash(self, pathname):
  30. """ Reads the hash only from the indicated pathname. """
  31. hv = HashVal()
  32. hv.hashFile(pathname)
  33. self.hash = hv.asHex()
  34. def loadXml(self, xelement):
  35. """ Reads the file information from the indicated XML
  36. element. """
  37. self.filename = xelement.Attribute('filename')
  38. self.basename = None
  39. if self.filename:
  40. self.basename = Filename(self.filename).getBasename()
  41. size = xelement.Attribute('size')
  42. try:
  43. self.size = int(size)
  44. except:
  45. self.size = 0
  46. timestamp = xelement.Attribute('timestamp')
  47. try:
  48. self.timestamp = int(timestamp)
  49. except:
  50. self.timestamp = 0
  51. self.hash = xelement.Attribute('hash')
  52. def storeXml(self, xelement):
  53. """ Adds the file information to the indicated XML
  54. element. """
  55. if self.filename:
  56. xelement.SetAttribute('filename', self.filename)
  57. if self.size:
  58. xelement.SetAttribute('size', str(self.size))
  59. if self.timestamp:
  60. xelement.SetAttribute('timestamp', str(int(self.timestamp)))
  61. if self.hash:
  62. xelement.SetAttribute('hash', self.hash)
  63. def storeMiniXml(self, xelement):
  64. """ Adds the just the "mini" file information--size and
  65. hash--to the indicated XML element. """
  66. if self.size:
  67. xelement.SetAttribute('size', str(self.size))
  68. if self.hash:
  69. xelement.SetAttribute('hash', self.hash)
  70. def quickVerify(self, packageDir = None, pathname = None,
  71. notify = None, correctSelf = False):
  72. """ Performs a quick test to ensure the file has not been
  73. modified. This test is vulnerable to people maliciously
  74. attempting to fool the program (by setting datestamps etc.).
  75. if correctSelf is True, then any discrepency is corrected by
  76. updating the appropriate fields internally, making the
  77. assumption that the file on disk is the authoritative version.
  78. Returns true if it is intact, false if it is incorrect. If
  79. correctSelf is true, raises OSError if the self-update is
  80. impossible (for instance, because the file does not exist)."""
  81. if not pathname:
  82. pathname = Filename(packageDir, self.filename)
  83. try:
  84. st = os.stat(pathname.toOsSpecific())
  85. except OSError:
  86. # If the file is missing, the file fails.
  87. if notify:
  88. notify.debug("file not found: %s" % (pathname))
  89. if correctSelf:
  90. raise
  91. return False
  92. if st.st_size != self.size:
  93. # If the size is wrong, the file fails.
  94. if notify:
  95. notify.debug("size wrong: %s" % (pathname))
  96. if correctSelf:
  97. self.__correctHash(packageDir, pathname, st, notify)
  98. return False
  99. if st.st_mtime == self.timestamp:
  100. # If the size is right and the timestamp is right, the
  101. # file passes.
  102. if notify:
  103. notify.debug("file ok: %s" % (pathname))
  104. return True
  105. if notify:
  106. notify.debug("modification time wrong: %s" % (pathname))
  107. # If the size is right but the timestamp is wrong, the file
  108. # soft-fails. We follow this up with a hash check.
  109. if not self.checkHash(packageDir, pathname, st):
  110. # Hard fail, the hash is wrong.
  111. if notify:
  112. notify.debug("hash check wrong: %s" % (pathname))
  113. notify.debug(" found %s, expected %s" % (self.actualFile.hash, self.hash))
  114. if correctSelf:
  115. self.__correctHash(packageDir, pathname, st, notify)
  116. return False
  117. if notify:
  118. notify.debug("hash check ok: %s" % (pathname))
  119. # The hash is OK after all. Change the file's timestamp back
  120. # to what we expect it to be, so we can quick-verify it
  121. # successfully next time.
  122. if correctSelf:
  123. # Or update our own timestamp.
  124. self.__correctTimestamp(pathname, st, notify)
  125. return False
  126. else:
  127. self.__updateTimestamp(pathname, st)
  128. return True
  129. def fullVerify(self, packageDir = None, pathname = None, notify = None):
  130. """ Performs a more thorough test to ensure the file has not
  131. been modified. This test is less vulnerable to malicious
  132. attacks, since it reads and verifies the entire file.
  133. Returns true if it is intact, false if it needs to be
  134. redownloaded. """
  135. if not pathname:
  136. pathname = Filename(packageDir, self.filename)
  137. try:
  138. st = os.stat(pathname.toOsSpecific())
  139. except OSError:
  140. # If the file is missing, the file fails.
  141. if notify:
  142. notify.debug("file not found: %s" % (pathname))
  143. return False
  144. if st.st_size != self.size:
  145. # If the size is wrong, the file fails;
  146. if notify:
  147. notify.debug("size wrong: %s" % (pathname))
  148. return False
  149. if not self.checkHash(packageDir, pathname, st):
  150. # Hard fail, the hash is wrong.
  151. if notify:
  152. notify.debug("hash check wrong: %s" % (pathname))
  153. notify.debug(" found %s, expected %s" % (self.actualFile.hash, self.hash))
  154. return False
  155. if notify:
  156. notify.debug("hash check ok: %s" % (pathname))
  157. # The hash is OK. If the timestamp is wrong, change it back
  158. # to what we expect it to be, so we can quick-verify it
  159. # successfully next time.
  160. if st.st_mtime != self.timestamp:
  161. self.__updateTimestamp(pathname, st)
  162. return True
  163. def __updateTimestamp(self, pathname, st):
  164. # On Windows, we have to change the file to read-write before
  165. # we can successfully update its timestamp.
  166. try:
  167. os.chmod(pathname.toOsSpecific(), 0o755)
  168. os.utime(pathname.toOsSpecific(), (st.st_atime, self.timestamp))
  169. os.chmod(pathname.toOsSpecific(), 0o555)
  170. except OSError:
  171. pass
  172. def __correctTimestamp(self, pathname, st, notify):
  173. """ Corrects the internal timestamp to match the one on
  174. disk. """
  175. if notify:
  176. notify.info("Correcting timestamp of %s to %d (%s)" % (
  177. self.filename, st.st_mtime, time.asctime(time.localtime(st.st_mtime))))
  178. self.timestamp = st.st_mtime
  179. def checkHash(self, packageDir, pathname, st):
  180. """ Returns true if the file has the expected md5 hash, false
  181. otherwise. As a side effect, stores a FileSpec corresponding
  182. to the on-disk file in self.actualFile. """
  183. fileSpec = FileSpec()
  184. fileSpec.fromFile(packageDir, self.filename,
  185. pathname = pathname, st = st)
  186. self.actualFile = fileSpec
  187. return (fileSpec.hash == self.hash)
  188. def __correctHash(self, packageDir, pathname, st, notify):
  189. """ Corrects the internal hash to match the one on disk. """
  190. if not self.actualFile:
  191. self.checkHash(packageDir, pathname, st)
  192. if notify:
  193. notify.info("Correcting hash %s to %s" % (
  194. self.filename, self.actualFile.hash))
  195. self.hash = self.actualFile.hash
  196. self.size = self.actualFile.size
  197. self.timestamp = self.actualFile.timestamp