Mayx's Home Page
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
22 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """LZMA2 7z quine 生成器,仅依赖 Python 标准库。
  4. 用法:python blogquine.py 源目录 输出.7z [--quine-name 名称]
  5. [--src-dir 前缀目录] [--quine-dir 自身所在目录]
  6. 解压得到源文件及输出归档自身;内容原样存储,辅助信息位于归档内部。
  7. """
  8. import argparse
  9. import binascii
  10. from dataclasses import dataclass
  11. import lzma
  12. import os
  13. from pathlib import Path
  14. import struct
  15. import time
  16. SIG = b"7z\xbc\xaf'\x1c"
  17. VER = b"\x00\x04"
  18. CHUNK = 1 << 16 # LZMA2 未压缩 chunk 上限
  19. MAX_CHUNK_OUTPUT = 1 << 21 # LZMA2 压缩 chunk 解压上限
  20. MAX_MATCH = 273
  21. PROPS_BYTE = 0x5D # lc=3, lp=0, pb=2
  22. FILETIME_EPOCH_OFFSET = 116444736000000000
  23. PROB_INIT = 1024
  24. class RangeEncoder:
  25. """LZMA 区间编码器;所有整数截断与 SDK 的 UInt32 运算一致。"""
  26. def __init__(self):
  27. self.low = 0
  28. self.range = 0xFFFFFFFF
  29. self.cache = 0
  30. self.cache_size = 1
  31. self.buf = bytearray()
  32. def _shift_low(self):
  33. carry = self.low >> 32
  34. if (self.low & 0xFFFFFFFF) < 0xFF000000 or carry:
  35. temp = self.cache
  36. while self.cache_size:
  37. self.buf.append((temp + carry) & 0xFF)
  38. temp = 0xFF
  39. self.cache_size -= 1
  40. self.cache = (self.low >> 24) & 0xFF
  41. self.cache_size += 1
  42. self.low = (self.low << 8) & 0xFFFFFFFF
  43. def _normalize(self):
  44. while self.range < 1 << 24:
  45. self.range = (self.range << 8) & 0xFFFFFFFF
  46. self._shift_low()
  47. def encode_bit(self, probs, index, bit):
  48. probability = probs[index]
  49. bound = (self.range >> 11) * probability
  50. if bit:
  51. self.low += bound
  52. self.range -= bound
  53. probs[index] -= probability >> 5
  54. else:
  55. self.range = bound
  56. probs[index] += (2048 - probability) >> 5
  57. self._normalize()
  58. def encode_direct_bits(self, value, bits):
  59. for shift in range(bits - 1, -1, -1):
  60. self.range >>= 1
  61. if value >> shift & 1:
  62. self.low += self.range
  63. self._normalize()
  64. def bit_tree(self, probs, offset, bits, symbol, reverse=False):
  65. node = 1
  66. shifts = range(bits) if reverse else range(bits - 1, -1, -1)
  67. for shift in shifts:
  68. bit = symbol >> shift & 1
  69. self.encode_bit(probs, offset + node, bit)
  70. node = (node << 1) | bit
  71. def finish(self):
  72. for _ in range(5):
  73. self._shift_low()
  74. return bytes(self.buf)
  75. class LengthEncoder:
  76. """普通 match 和 rep0 使用独立概率,但共享长度编码逻辑。"""
  77. def __init__(self):
  78. self.choice = [PROB_INIT] * 2
  79. self.low = [PROB_INIT] * 32
  80. self.mid = [PROB_INIT] * 32
  81. self.high = [PROB_INIT] * 256
  82. def encode(self, rc, pos_state, length):
  83. value = length - 2
  84. rc.encode_bit(self.choice, 0, value >= 8)
  85. if value < 8:
  86. rc.bit_tree(self.low, pos_state << 3, 3, value)
  87. else:
  88. rc.encode_bit(self.choice, 1, value >= 16)
  89. if value < 16:
  90. rc.bit_tree(self.mid, pos_state << 3, 3, value - 8)
  91. else:
  92. rc.bit_tree(self.high, 0, 8, value - 16)
  93. class LzmaEncoder:
  94. """只编码 match/rep0,编码结果不依赖历史字节,供布局后填充数据。"""
  95. def __init__(self, out_pos=0):
  96. self.rc = RangeEncoder()
  97. self.pos = out_pos
  98. self.state = 0
  99. self.is_match = [PROB_INIT] * (12 << 4)
  100. self.is_rep = [PROB_INIT] * 12
  101. self.is_rep_g0 = [PROB_INIT] * 12
  102. self.rep0_long = [PROB_INIT] * (12 << 4)
  103. self.pos_slot = [PROB_INIT] * (4 << 6)
  104. self.spec_pos = [PROB_INIT] * 114
  105. self.align = [PROB_INIT] * 16
  106. self.match_len = LengthEncoder()
  107. self.rep_len = LengthEncoder()
  108. def _write_dist(self, dist, length):
  109. if not 1 <= dist <= 0xFFFFFFFF:
  110. raise ValueError("LZMA distance must be 1..2^32-1")
  111. value = dist - 1
  112. bits = max(0, value.bit_length() - 2)
  113. slot = value if value < 4 else 2 * (bits + 1) + (value >> bits & 1)
  114. self.rc.bit_tree(self.pos_slot, min(length - 2, 3) << 6, 6, slot)
  115. if slot >= 4:
  116. base = (2 | (slot & 1)) << bits
  117. extra = value - base
  118. if slot < 14:
  119. self.rc.bit_tree(self.spec_pos, base - slot - 1, bits, extra, True)
  120. else:
  121. self.rc.encode_direct_bits(extra >> 4, bits - 4)
  122. self.rc.bit_tree(self.align, 0, 4, extra & 15, True)
  123. def match(self, dist, length):
  124. if not 2 <= length <= MAX_MATCH:
  125. raise ValueError("LZMA match length must be 2..273")
  126. self.rc.encode_bit(self.is_match, (self.state << 4) + (self.pos & 3), 1)
  127. self.rc.encode_bit(self.is_rep, self.state, 0)
  128. self.match_len.encode(self.rc, self.pos & 3, length)
  129. self._write_dist(dist, length)
  130. self.state = 7 if self.state < 7 else 10
  131. self.pos += length
  132. def rep_match(self, length):
  133. if not 2 <= length <= MAX_MATCH:
  134. raise ValueError("LZMA rep0 length must be 2..273")
  135. index = (self.state << 4) + (self.pos & 3)
  136. self.rc.encode_bit(self.is_match, index, 1)
  137. self.rc.encode_bit(self.is_rep, self.state, 1)
  138. self.rc.encode_bit(self.is_rep_g0, self.state, 0)
  139. self.rc.encode_bit(self.rep0_long, index, 1)
  140. self.rep_len.encode(self.rc, self.pos & 3, length)
  141. self.state = 8 if self.state < 7 else 11
  142. self.pos += length
  143. def finish(self):
  144. return self.rc.finish()
  145. def crc32(data, value=0):
  146. return binascii.crc32(data, value) & 0xFFFFFFFF
  147. def varint(value):
  148. """7z UINT64:首字节给出后续小端字节数,最多九字节。"""
  149. if not 0 <= value < 1 << 64:
  150. raise ValueError("7z UINT64 out of range")
  151. for size in range(8):
  152. if value < 1 << (7 * (size + 1)):
  153. first = ((0xFF << (8 - size)) & 0xFF) | (value >> (8 * size))
  154. return bytes([first]) + (value & ((1 << (8 * size)) - 1)).to_bytes(size, "little")
  155. return b"\xff" + value.to_bytes(8, "little")
  156. def store_hdr(payload_len, first=False):
  157. if not 1 <= payload_len <= CHUNK:
  158. raise ValueError("LZMA2 stored chunk must contain 1..65536 bytes")
  159. return bytes([1 if first else 2]) + (payload_len - 1).to_bytes(2, "big")
  160. def matches_for(dist, total):
  161. """固定距离的首个 match 加 rep0;分段时不留下单字节。"""
  162. if total < 2:
  163. raise ValueError("match sequence must output at least two bytes")
  164. tokens = []
  165. while total:
  166. length = min(MAX_MATCH, total)
  167. if total - length == 1:
  168. length -= 1
  169. tokens.append(("r0", length) if tokens else ("m", dist, length))
  170. total -= length
  171. return tokens
  172. def dict_prop_for(maxdist):
  173. for prop in range(41):
  174. size = 0xFFFFFFFF if prop == 40 else (2 | (prop & 1)) << (prop // 2 + 11)
  175. if size >= maxdist:
  176. return prop, size
  177. raise ValueError("LZMA2 dictionary distance exceeds 2^32-1")
  178. def lzma_chunk(tokens, out_pos):
  179. """重置状态和概率,保留字典;21-bit 解压长度,不写结束标记。"""
  180. enc = LzmaEncoder(out_pos)
  181. for token in tokens:
  182. if token[0] == "m":
  183. enc.match(token[1], token[2])
  184. elif token[0] == "r0":
  185. enc.rep_match(token[1])
  186. else:
  187. raise ValueError("unsupported LZMA token: %r" % (token[0],))
  188. data = enc.finish()
  189. total = enc.pos - out_pos
  190. if not 1 <= total <= MAX_CHUNK_OUTPUT or not 1 <= len(data) <= CHUNK:
  191. raise ValueError("LZMA2 compressed chunk size out of range")
  192. unpacked, packed = total - 1, len(data) - 1
  193. header = (bytes([0xC0 | (unpacked >> 16)])
  194. + (unpacked & 0xFFFF).to_bytes(2, "big")
  195. + packed.to_bytes(2, "big") + bytes([PROPS_BYTE]))
  196. return header + data, total
  197. @dataclass(frozen=True)
  198. class Entry:
  199. name: str
  200. is_dir: bool
  201. data: bytes = b""
  202. @dataclass(frozen=True)
  203. class Layout:
  204. prefix_size: int
  205. chunks: tuple
  206. samples: bytes
  207. padding: int
  208. matches: bytes
  209. header: bytes
  210. crc_offset: int
  211. dict_size: int
  212. @property
  213. def packed_size(self):
  214. return sum(self.chunks) + 3 * len(self.chunks) + len(self.matches) + 1
  215. class LayoutError(RuntimeError):
  216. """当前填充量没有满足布局或 CRC 定点,换一个填充量重试。"""
  217. def split_sizes(size, limit):
  218. """每片至少两字节,避免 match 无法表达的一字节尾片。"""
  219. if size < 2:
  220. raise ValueError("分片总长度必须至少为 2")
  221. sizes = [limit] * (size // limit)
  222. tail = size % limit
  223. if tail == 1:
  224. sizes[-1] -= 1
  225. tail = 2
  226. if tail:
  227. sizes.append(tail)
  228. return tuple(sizes)
  229. def bitfield(flags):
  230. data = bytearray((len(flags) + 7) // 8)
  231. for i, flag in enumerate(flags):
  232. if flag:
  233. data[i // 8] |= 1 << (7 - i % 8)
  234. return bytes(data)
  235. def archive_path(value, allow_empty=False):
  236. value = value.replace("\\", "/")
  237. if not value and allow_empty:
  238. return ""
  239. parts = value.split("/")
  240. if any(part in ("", ".", "..") for part in parts) or any(c in value for c in ("\0", ":")):
  241. raise ValueError("归档内路径必须是相对路径,不能包含空段、. 或 ..:%r" % value)
  242. return "/".join(parts)
  243. def encode_segments(segments, out_pos):
  244. """把 (历史起点, 长度) 转成 match 流;跨 chunk 时保留字典。"""
  245. result, tokens = bytearray(), []
  246. used, max_distance = 0, 0
  247. for source, length in segments:
  248. for size in split_sizes(length, MAX_CHUNK_OUTPUT):
  249. if used + size > MAX_CHUNK_OUTPUT:
  250. block, _ = lzma_chunk(tokens, out_pos)
  251. result += block
  252. out_pos += used
  253. tokens, used = [], 0
  254. distance = out_pos + used - source
  255. if distance <= 0:
  256. raise LayoutError("回引源尚未输出")
  257. max_distance = max(max_distance, distance)
  258. tokens.extend(matches_for(distance, size))
  259. used += size
  260. source += size
  261. if tokens:
  262. block, _ = lzma_chunk(tokens, out_pos)
  263. result += block
  264. return bytes(result), max_distance
  265. def solve_fixed_crc(data, groups, targets):
  266. """CRC 是 GF(2) 仿射函数;同步所有镜像,解三个 CRC 的 96-bit 定点。"""
  267. def vector(values):
  268. return sum(value << (32 * i) for i, value in enumerate(values))
  269. base = vector(targets())
  270. columns = []
  271. for copies in groups:
  272. for bit in range(32):
  273. positions = [copy[bit // 8] for copy in copies]
  274. for pos in positions:
  275. data[pos] ^= 1 << (bit % 8)
  276. columns.append(vector(targets()) ^ base)
  277. for pos in positions:
  278. data[pos] ^= 1 << (bit % 8)
  279. rows = []
  280. for bit in range(96):
  281. coeff = 1 << bit
  282. for j, column in enumerate(columns):
  283. coeff ^= ((column >> bit) & 1) << j
  284. rows.append([coeff, (base >> bit) & 1])
  285. pivots = []
  286. for column in range(96):
  287. rank = len(pivots)
  288. pivot = next((i for i in range(rank, 96) if rows[i][0] >> column & 1), None)
  289. if pivot is None:
  290. continue
  291. rows[rank], rows[pivot] = rows[pivot], rows[rank]
  292. for i in range(96):
  293. if i != rank and rows[i][0] >> column & 1:
  294. rows[i][0] ^= rows[rank][0]
  295. rows[i][1] ^= rows[rank][1]
  296. pivots.append(column)
  297. if any(not coeff and rhs for coeff, rhs in rows):
  298. raise LayoutError("CRC 定点方程无解")
  299. solution = sum(rows[i][1] << column for i, column in enumerate(pivots))
  300. values = [(solution >> (32 * i)) & 0xFFFFFFFF for i in range(3)]
  301. for copies, value in zip(groups, values):
  302. packed = struct.pack("<I", value)
  303. for copy in copies:
  304. for i, pos in enumerate(copy):
  305. data[pos] = packed[i]
  306. if tuple(values) != targets():
  307. raise RuntimeError("CRC 定点校验失败")
  308. class BlogQuine:
  309. """单层 LZMA2:解码(P) = content || Q,无独立的辅助文件条目。
  310. Q = V || P || H,V = S32 || H副本 || chunk头样本 || pad || M副本 || 00。
  311. P 先 store(content || V),再由 M 回引已输出的字节,复现 P || H。
  312. 内容按原样存储;M 的编码不依赖历史字节,可先求布局,再填头部与 CRC。
  313. """
  314. def __init__(self, root, quine_name="MayxBlog.7z", src_dir="", exclude=None):
  315. root = Path(root).resolve()
  316. if not root.is_dir():
  317. raise ValueError("源目录不存在:%s" % root)
  318. self.quine_name = archive_path(quine_name)
  319. prefix = archive_path(src_dir.strip("/\\"), allow_empty=True)
  320. excluded = Path(exclude).resolve() if exclude is not None else None
  321. self.entries, self.files = [], []
  322. names = {}
  323. def add(name, is_dir, data=b""):
  324. key = name.casefold()
  325. if key in names:
  326. if is_dir and names[key].is_dir and names[key].name == name:
  327. return
  328. raise ValueError("归档内路径冲突:%s" % name)
  329. entry = Entry(name, is_dir, data)
  330. names[key] = entry
  331. self.entries.append(entry)
  332. if not is_dir:
  333. self.files.append(entry)
  334. def add_parents(name):
  335. parts = name.split("/")
  336. for i in range(1, len(parts)):
  337. add("/".join(parts[:i]), True)
  338. if prefix:
  339. add_parents(prefix)
  340. add(prefix, True)
  341. def walk_error(error):
  342. raise error
  343. for directory, dirs, files in os.walk(root, onerror=walk_error):
  344. dirs.sort()
  345. children = [(name, True) for name in dirs]
  346. children += [(name, False) for name in sorted(files)]
  347. for name, is_dir in children:
  348. path = Path(directory) / name
  349. if not is_dir and path.resolve() == excluded:
  350. continue
  351. rel = path.relative_to(root).as_posix()
  352. name = archive_path(prefix + "/" + rel if prefix else rel)
  353. add_parents(name)
  354. add(name, is_dir, b"" if is_dir else path.read_bytes())
  355. add_parents(self.quine_name)
  356. if self.quine_name.casefold() in names:
  357. raise ValueError("quine 文件名与源条目冲突:%s" % self.quine_name)
  358. self.entries.append(Entry(self.quine_name, False))
  359. self.content = b"".join(entry.data for entry in self.files)
  360. self.stream_files = [entry for entry in self.files if entry.data]
  361. self.stream_sizes = b"".join(varint(len(entry.data)) for entry in self.stream_files)
  362. self.stream_crcs = b"".join(struct.pack("<I", crc32(entry.data)) for entry in self.stream_files)
  363. self.mtime = time.time_ns() // 100 + FILETIME_EPOCH_OFFSET
  364. def build_header(self, prefix_size, packed_size, archive_size, dict_prop):
  365. h = bytearray(b"\x01\x04\x06" + varint(prefix_size - 32)
  366. + b"\x01\x09" + varint(packed_size) + b"\x00\x07\x0b\x01\x00")
  367. h += b"\x01\x21\x21\x01" + bytes([dict_prop]) # 一个 LZMA2 coder
  368. h += b"\x0c" + varint(len(self.content) + archive_size) + b"\x00"
  369. h += b"\x08\x0d" + varint(len(self.stream_files) + 1) + b"\x09" + self.stream_sizes
  370. h += b"\x0a\x01" + self.stream_crcs
  371. crc_offset = len(h)
  372. h += b"\x00" * 4 + b"\x00\x00\x05" + varint(len(self.entries))
  373. def prop(kind, data):
  374. return bytes([kind]) + varint(len(data)) + data
  375. empty = [entry.is_dir or (not entry.data and i != len(self.entries) - 1)
  376. for i, entry in enumerate(self.entries)]
  377. h += prop(0x0E, bitfield(empty))
  378. if any(flag and not entry.is_dir for flag, entry in zip(empty, self.entries)):
  379. empty_files = [not entry.is_dir for flag, entry in zip(empty, self.entries) if flag]
  380. h += prop(0x0F, bitfield(empty_files))
  381. h += prop(0x14, b"\x01\x00" + struct.pack("<Q", self.mtime) * len(self.entries))
  382. names = b"".join(entry.name.encode("utf-16-le") + b"\x00\x00" for entry in self.entries)
  383. attrs = b"".join(struct.pack("<I", 0x10 if entry.is_dir else 0x20) for entry in self.entries)
  384. h += prop(0x11, b"\x00" + names)
  385. h += prop(0x15, b"\x01\x00" + attrs)
  386. return bytes(h + b"\x00\x00"), crc_offset
  387. def layout(self, padding):
  388. header_size, match_size = 256, 256
  389. seen = set()
  390. dc = len(self.content)
  391. for _ in range(40):
  392. prefix_size = 32 + header_size + 12 + padding + match_size + 1
  393. output_size = dc + prefix_size
  394. chunks = split_sizes(output_size, CHUNK)
  395. samples = (store_hdr(chunks[0], True) + store_hdr(CHUNK)
  396. + store_hdr(CHUNK - 1) + store_hdr(chunks[-1]))
  397. segments, offset = [], 0
  398. for i, size in enumerate(chunks):
  399. sample = 0 if i == 0 else 1 if size == CHUNK else 2 if size == CHUNK - 1 else 3
  400. segments += [(dc + 32 + header_size + 3 * sample, 3), (offset, size)]
  401. offset += size
  402. segments += [(dc + 32 + header_size + 12 + padding, match_size + 1),
  403. (dc + 32, header_size)]
  404. matches, max_distance = encode_segments(segments, output_size)
  405. dict_prop, dict_size = dict_prop_for(max_distance)
  406. packed_size = output_size + 3 * len(chunks) + len(matches) + 1
  407. header, crc_offset = self.build_header(
  408. prefix_size, packed_size, prefix_size + packed_size + header_size, dict_prop)
  409. key = len(header), len(matches)
  410. if key == (header_size, match_size):
  411. return Layout(prefix_size, chunks, samples, padding, matches,
  412. header, crc_offset, dict_size)
  413. if key in seen:
  414. break
  415. seen.add(key)
  416. header_size, match_size = key
  417. raise LayoutError("布局没有收敛")
  418. def assemble(self, layout):
  419. start = struct.pack("<QQI", layout.prefix_size - 32 + layout.packed_size,
  420. len(layout.header), 0)
  421. signature = SIG + VER + b"\x00" * 4 + start
  422. prefix = (signature + layout.header + layout.samples + b"\x00" * layout.padding
  423. + layout.matches + b"\x00")
  424. initial = self.content + prefix
  425. data, offset = bytearray(prefix), 0
  426. for i, size in enumerate(layout.chunks):
  427. data += store_hdr(size, first=i == 0) + initial[offset:offset + size]
  428. offset += size
  429. data += layout.matches + b"\x00" + layout.header
  430. if len(prefix) != layout.prefix_size or offset != len(initial):
  431. raise RuntimeError("装配布局不一致")
  432. def stored_positions(output_offset, size):
  433. result, consumed, physical = [], 0, layout.prefix_size
  434. for length in layout.chunks:
  435. begin = max(output_offset, consumed)
  436. end = min(output_offset + size, consumed + length)
  437. result.extend(range(physical + 3 + begin - consumed, physical + 3 + end - consumed))
  438. consumed += length
  439. physical += 3 + length
  440. if len(result) != size:
  441. raise RuntimeError("无法定位 store 载荷")
  442. return result
  443. slot = layout.crc_offset
  444. hoff = layout.prefix_size + layout.packed_size
  445. groups = [[list(range(32 + slot, 36 + slot)),
  446. stored_positions(len(self.content) + 32 + slot, 4),
  447. list(range(hoff + slot, hoff + slot + 4))]]
  448. for slot in (28, 8):
  449. groups.append([list(range(slot, slot + 4)), stored_positions(len(self.content) + slot, 4)])
  450. view = memoryview(data)
  451. solve_fixed_crc(data, groups, lambda: (crc32(view), crc32(view[hoff:]), crc32(view[12:32])))
  452. return bytes(data)
  453. def verify(self, data, layout):
  454. dec = lzma.LZMADecompressor(format=lzma.FORMAT_RAW,
  455. filters=[{"id": lzma.FILTER_LZMA2, "dict_size": layout.dict_size}])
  456. expected = len(self.content) + len(data)
  457. packed = data[layout.prefix_size:layout.prefix_size + layout.packed_size]
  458. decoded = dec.decompress(packed, max_length=expected + 1)
  459. view = memoryview(decoded)
  460. if (not dec.eof or dec.unused_data or len(decoded) != expected
  461. or view[:len(self.content)] != self.content or view[len(self.content):] != data):
  462. raise RuntimeError("自解码校验失败")
  463. def build(self):
  464. last_error = None
  465. for padding in range(256):
  466. try:
  467. layout = self.layout(padding)
  468. data = self.assemble(layout)
  469. self.verify(data, layout)
  470. return data
  471. except LayoutError as error:
  472. last_error = error
  473. raise RuntimeError("无法找到布局/CRC 定点:%s" % last_error)
  474. def main():
  475. parser = argparse.ArgumentParser(description="LZMA2 7z quine 生成器")
  476. parser.add_argument("srcdir", help="要打包的源目录")
  477. parser.add_argument("output", help="输出的 7z 文件")
  478. parser.add_argument("--quine-name", help="归档内自身文件名,默认使用输出文件名")
  479. parser.add_argument("--src-dir", default="", help="源文件在归档内的前缀目录")
  480. parser.add_argument("--quine-dir", default="", help="归档内自身文件所在目录")
  481. args = parser.parse_args()
  482. try:
  483. output = Path(args.output)
  484. name = args.quine_name or output.name
  485. if args.quine_dir:
  486. name = archive_path(args.quine_dir.strip("/\\")) + "/" + name
  487. quine = BlogQuine(args.srcdir, name, args.src_dir, exclude=output)
  488. data = quine.build() # 验证成功后才写出,失败时保留已有输出。
  489. output.write_bytes(data)
  490. except (OSError, ValueError, RuntimeError, lzma.LZMAError) as error:
  491. parser.exit(1, "blogquine: %s\n" % error)
  492. print("[blogquine] %d 个文件,%d 个目录,内容 %d 字节" %
  493. (len(quine.files), sum(entry.is_dir for entry in quine.entries), len(quine.content)))
  494. print("[blogquine] 已写入 %s:%d 字节;自解码通过" % (output, len(data)))
  495. if __name__ == "__main__":
  496. main()