reassemble-voc-database.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """Reassemble the two private transport ZIPs; never install or replace a database."""
  2. import argparse
  3. import hashlib
  4. import json
  5. import os
  6. from pathlib import Path
  7. import sys
  8. import tempfile
  9. import zipfile
  10. sys.dont_write_bytecode = True
  11. FILENAME = 'ksf-voc-database-20261106-r1.zip'
  12. SIZE = 25913256
  13. SHA256 = '1ba1ac2fa6a8b774f8945d88e014c07dd5e03332792af298a41f015ba6946f9f'
  14. PART_SIZES = [14000000, 11913256]
  15. def require(value, code):
  16. if not value:
  17. raise ValueError(code)
  18. def digest(p):
  19. with Path(p).open('rb') as f:
  20. result = hashlib.sha256()
  21. for block in iter(lambda: f.read(1024 * 1024), b''):
  22. result.update(block)
  23. return result.hexdigest()
  24. def regular_path(p):
  25. p = Path(p)
  26. require(p.is_absolute(), 'ABSOLUTE_PATH_REQUIRED')
  27. for parent in (p, *p.parents):
  28. require(not parent.is_symlink(), 'SYMLINK_PATH_REFUSED')
  29. return p
  30. def reassemble(parts, output):
  31. require(len(parts) == 2, 'EXACTLY_TWO_PARTS_REQUIRED')
  32. paths = [regular_path(p) for p in parts]
  33. require(len(set(paths)) == 2 and all(p.is_file() for p in paths), 'PART_MISSING_OR_DUPLICATED')
  34. output = regular_path(output)
  35. require(output.name == FILENAME and output.parent.is_dir(), 'INVALID_OUTPUT_PATH')
  36. ordered = {}
  37. common = None
  38. for p in paths:
  39. with zipfile.ZipFile(p) as z:
  40. require(z.getinfo('TRANSPORT.json').file_size < 16000, 'INVALID_TRANSPORT_MANIFEST')
  41. meta = json.loads(z.read('TRANSPORT.json'))
  42. require(meta.get('filename') == FILENAME and meta.get('bytes') == SIZE and
  43. meta.get('sha256') == SHA256 and len(meta.get('parts', [])) == 2,
  44. 'WRONG_SOURCE_PACKAGE')
  45. require(common is None or common == meta, 'TRANSPORT_MANIFEST_MISMATCH')
  46. common = meta
  47. found = [i for i in (1, 2) if FILENAME + f'.part{i:02}' in z.namelist()]
  48. require(len(found) == 1 and found[0] not in ordered, 'PART_MISSING_OR_DUPLICATED')
  49. index = found[0]
  50. entry = meta['parts'][index - 1]
  51. name = FILENAME + f'.part{index:02}'
  52. require(entry.get('chunk') == name and entry.get('bytes') == PART_SIZES[index - 1] and
  53. isinstance(entry.get('sha256'), str) and len(entry['sha256']) == 64 and
  54. z.getinfo(name).file_size == PART_SIZES[index - 1], 'INVALID_PART_SIZE_OR_NAME')
  55. require(len(z.namelist()) == len(set(z.namelist())) == 4 and
  56. set(z.namelist()) == {name, 'TRANSPORT.json', 'reassemble-voc-database.py', 'README.md'},
  57. 'UNEXPECTED_TRANSPORT_CONTENTS')
  58. ordered[index] = (p, entry)
  59. require(set(ordered) == {1, 2}, 'PART_MISSING_OR_DUPLICATED')
  60. if output.exists():
  61. require(output.is_file() and output.stat().st_size == SIZE and digest(output) == SHA256,
  62. 'DIFFERENT_OUTPUT_ALREADY_EXISTS')
  63. return {'ok': True, 'operation': 'unchanged', 'output': str(output), 'bytes': SIZE,
  64. 'sha256': SHA256, 'fileCreated': False, 'installed': False, 'databaseWrites': 0}
  65. fd, temporary = tempfile.mkstemp(prefix='.ksf-voc-reassemble-', dir=output.parent)
  66. try:
  67. total = 0
  68. whole = hashlib.sha256()
  69. with os.fdopen(fd, 'wb') as dest:
  70. for index in (1, 2):
  71. p, entry = ordered[index]
  72. chunk_hash = hashlib.sha256()
  73. length = 0
  74. with zipfile.ZipFile(p) as z, z.open(entry['chunk']) as source:
  75. while data := source.read(1024 * 1024):
  76. length += len(data)
  77. require(length <= entry['bytes'], 'PART_TOO_LARGE')
  78. chunk_hash.update(data)
  79. whole.update(data)
  80. dest.write(data)
  81. require(length == entry['bytes'] and chunk_hash.hexdigest() == entry['sha256'],
  82. 'PART_HASH_MISMATCH')
  83. total += length
  84. require(total == SIZE and whole.hexdigest() == SHA256, 'REASSEMBLED_HASH_MISMATCH')
  85. dest.flush()
  86. os.fsync(dest.fileno())
  87. # A same-filesystem hard link publishes the verified file without overwriting a race winner.
  88. os.link(temporary, output)
  89. return {'ok': True, 'operation': 'reassembled', 'output': str(output), 'bytes': total,
  90. 'sha256': whole.hexdigest(), 'fileCreated': True, 'installed': False, 'databaseWrites': 0}
  91. finally:
  92. os.unlink(temporary)
  93. def main():
  94. parser = argparse.ArgumentParser(description=__doc__)
  95. parser.add_argument('--part', action='append', required=True, help='Actual absolute path to each transport ZIP; repeat twice')
  96. parser.add_argument('--output', required=True, help='Absolute path of the original ZIP under an existing private directory')
  97. args = parser.parse_args()
  98. try:
  99. print(json.dumps(reassemble(args.part, args.output), ensure_ascii=False, indent=2))
  100. except Exception as exc:
  101. code = str(exc) if isinstance(exc, ValueError) else 'REASSEMBLY_STOPPED'
  102. print(json.dumps({'ok': False, 'error': {'code': code}, 'installed': False, 'databaseWrites': 0}))
  103. return 1
  104. return 0
  105. if __name__ == '__main__':
  106. raise SystemExit(main())