|
#!/usr/bin/env python3 |
|
""" |
|
Scan a Linux panic QR code with ZXingReader and decompress its crash log. |
|
./decode-kernel-panic.py screenshot.png -o panic.log |
|
""" |
|
|
|
import argparse |
|
import json |
|
from pathlib import Path |
|
import subprocess |
|
import sys |
|
from urllib.parse import parse_qs, urlsplit |
|
import zlib |
|
|
|
|
|
def decode_panic_url(url): |
|
"""Decode the kernel's z parameter: 17 decimal digits per 7 little-endian bytes. |
|
|
|
Format reference: https://github.com/kdj0c/panic_report |
|
""" |
|
parts = urlsplit(url) |
|
parameters = parse_qs(parts.query, keep_blank_values=True) |
|
if "z" not in parameters: |
|
parameters = parse_qs(parts.fragment.lstrip("?"), keep_blank_values=True) |
|
if "z" not in parameters: |
|
raise ValueError("QR code URL has no compressed panic data (z parameter)") |
|
if len(parameters["z"]) != 1: |
|
raise ValueError("QR code URL has multiple z parameters") |
|
|
|
digits = parameters["z"][0] |
|
if not digits or not digits.isascii() or not digits.isdecimal(): |
|
raise ValueError("panic data must contain only decimal digits") |
|
|
|
# A final, partial group uses fewer digits and bytes. |
|
byte_counts = {3: 1, 5: 2, 8: 3, 10: 4, 13: 5, 15: 6, 17: 7} |
|
compressed = bytearray() |
|
for offset in range(0, len(digits), 17): |
|
chunk = digits[offset:offset + 17] |
|
if len(chunk) not in byte_counts: |
|
raise ValueError("invalid length of encoded panic data") |
|
try: |
|
compressed.extend(int(chunk).to_bytes(byte_counts[len(chunk)], "little")) |
|
except OverflowError as exc: |
|
raise ValueError("encoded panic data contains an out-of-range chunk") from exc |
|
|
|
return zlib.decompress(compressed) |
|
|
|
|
|
def scan_image(image): |
|
if not image.is_file(): |
|
raise ValueError(f"image does not exist or is not a file: {image}") |
|
try: |
|
result = subprocess.run( |
|
["ZXingReader", "-formats", "QRCode", "-json", str(image.resolve())], |
|
capture_output=True, |
|
text=True, |
|
) |
|
except FileNotFoundError as exc: |
|
raise ValueError("ZXingReader is required; install it and put it on PATH") from exc |
|
if result.returncode != 0: |
|
detail = result.stderr.strip() or "no readable QR code found" |
|
raise ValueError(f"ZXingReader failed: {detail}") |
|
if not result.stdout.strip(): |
|
raise ValueError("no readable QR code found") |
|
try: |
|
qr = json.loads(result.stdout) |
|
except json.JSONDecodeError as exc: |
|
raise ValueError("expected one QR code in ZXingReader's JSON output") from exc |
|
if not isinstance(qr, dict) or not isinstance(qr.get("Text"), str): |
|
raise ValueError("ZXingReader returned no QR code text") |
|
return qr["Text"] |
|
|
|
|
|
def main(): |
|
parser = argparse.ArgumentParser( |
|
description="Scan a Linux panic screenshot with ZXingReader and decompress its log.", |
|
epilog="Requires Python 3 and ZXingReader. Decoding runs locally without network access.", |
|
) |
|
parser.add_argument("image", type=Path, help="screenshot or photo containing the panic QR code") |
|
parser.add_argument("-o", "--output", type=Path, help="write the log to a file instead of stdout") |
|
args = parser.parse_args() |
|
try: |
|
if args.output and args.output.resolve() == args.image.resolve(): |
|
raise ValueError("output must differ from the input image") |
|
log = decode_panic_url(scan_image(args.image)) |
|
if args.output: |
|
args.output.write_bytes(log) |
|
else: |
|
sys.stdout.buffer.write(log) |
|
except BrokenPipeError: |
|
# Allow commands such as `decode-kernel-panic screenshot.png | head`. |
|
import os |
|
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) |
|
return 0 |
|
except (OSError, ValueError, zlib.error) as exc: |
|
print(f"{parser.prog}: {exc}", file=sys.stderr) |
|
return 1 |
|
return 0 |
|
|
|
|
|
if __name__ == "__main__": |
|
sys.exit(main()) |