tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

__init__.py (3285B)


      1 """File integrity work in tricu, with Python-owned manifests and presentation."""
      2 from dataclasses import dataclass
      3 import hashlib
      4 from importlib.resources import files
      5 import os
      6 import re
      7 
      8 from tricu import Library
      9 from ._bundle_pin import BUNDLE_SHA256
     10 
     11 DEFAULT_MAX_BYTES = 64 * 1024 * 1024
     12 
     13 
     14 @dataclass(frozen=True)
     15 class FileDigest:
     16     bytes: int
     17     sha256: str
     18 
     19 
     20 @dataclass(frozen=True)
     21 class Problem:
     22     category: str
     23     operation: str
     24     detail: str
     25     progress: str
     26 
     27 
     28 class FileProblem(Exception):
     29     def __init__(self, problem):
     30         self.problem = problem
     31         super().__init__(f'{problem.category}: {problem.operation}: {problem.detail}')
     32 
     33 
     34 class ProtocolError(RuntimeError):
     35     pass
     36 
     37 
     38 class FileChecker:
     39     """One loaded bundle/context, reused for sequential calls until close()."""
     40     def __init__(self):
     41         bundle = files(__package__).joinpath('worker.arboricx').read_bytes()
     42         if hashlib.sha256(bundle).hexdigest() != BUNDLE_SHA256:
     43             raise ProtocolError('packaged worker bundle checksum mismatch')
     44         self.context = Library().context()
     45         try:
     46             self.worker = self.context.load_bundle(bundle, 'hashFile')
     47         except BaseException:
     48             self.context.close()
     49             raise
     50 
     51     def __enter__(self):
     52         return self
     53 
     54     def __exit__(self, *_):
     55         self.close()
     56 
     57     def close(self):
     58         self.context.close()
     59 
     60     def _list(self, scope, value, count):
     61         result = []
     62         for _ in range(count):
     63             if self.context.kind(value) != 2:
     64                 raise ProtocolError('worker returned an improper list')
     65             result.append(scope.child(value, 0))
     66             value = scope.child(value, 1)
     67         if self.context.kind(value) != 0:
     68             raise ProtocolError('worker returned excess list fields')
     69         return result
     70 
     71     def hash_file(self, path, max_bytes=DEFAULT_MAX_BYTES):
     72         if type(max_bytes) is not int or not 0 <= max_bytes <= 2**63-1:
     73             raise ValueError('max_bytes must be an integer in 0..2**63-1')
     74         path = os.fspath(path)
     75         if not isinstance(path, str):
     76             raise TypeError('path must be Unicode text')
     77         with self.context.scope() as roots:
     78             program = roots.apply_many(self.worker, roots.string(path), roots.number(max_bytes))
     79             outcome, result, cleanup = roots.run_io(program)
     80             if outcome != 0 or self.context.kind(cleanup) != 0:
     81                 # Terminal reasons remain opaque. Ordinary file-operation errors
     82                 # are Results decoded below, independently of task outcomes.
     83                 raise ProtocolError(f'worker task outcome {outcome} or cleanup diagnostics')
     84             tag, payload = self._list(roots, result, 2)
     85             if not self.context.to_bool(tag):
     86                 fields = self._list(roots, payload, 4)
     87                 raise FileProblem(Problem(*(self.context.to_string(field) for field in fields)))
     88             length, digest = self._list(roots, payload, 2)
     89             value = FileDigest(self.context.to_number(length), self.context.to_string(digest))
     90             if value.bytes > max_bytes or not re.fullmatch('[0-9a-f]{64}', value.sha256):
     91                 raise ProtocolError('invalid worker digest result')
     92             return value