fix(qemu): validate PPM headers and raster length (#18)
Parse the P6 header explicitly, keep every raster byte, and reject non-positive dimensions, maxval other than 255, and mismatched raster lengths. Add scripts/test-qemu-ppm.py.
This commit is contained in:
1 parent
ffa88fe34e
commit
61e9f1312a
2 files changed
+74
-8
No files matched your search
@@ -78,14 +78,19 @@ class Qmp:
|
||||
|
||||
def ppm_to_png(ppm: Path, png: Path) -> None:
|
||||
data = ppm.read_bytes()
|
||||
parts = data.split(maxsplit=4)
|
||||
if parts[0] != b"P6":
|
||||
raise ValueError("not a binary PPM")
|
||||
width, height = int(parts[1]), int(parts[2])
|
||||
# The pixels are the last bytes of the file. Splitting them off the header
|
||||
# would drop leading bytes that happen to be ASCII whitespace (0x09-0x0D,
|
||||
# 0x20), and every channel after them would shift.
|
||||
pixels = data[len(data) - width * height * 3:]
|
||||
# Comments and whitespace separate header tokens, but raster bytes must
|
||||
# never be stripped: every byte value is valid in an RGB channel.
|
||||
separator = rb"(?:[ \t\r\n\v\f]|#[^\r\n]*(?:\r\n|[\r\n]))+"
|
||||
header = re.match(rb"P6" + separator + rb"([0-9]+)" + separator
|
||||
+ rb"([0-9]+)" + separator + rb"([0-9]+)[ \t\r\n\v\f]", data)
|
||||
if header is None:
|
||||
raise ValueError("invalid binary PPM header")
|
||||
width, height, maxval = map(int, header.groups())
|
||||
if width < 1 or height < 1 or maxval != 255:
|
||||
raise ValueError("PPM requires positive dimensions and maxval 255")
|
||||
pixels = data[header.end():]
|
||||
if len(pixels) != width * height * 3:
|
||||
raise ValueError("PPM raster length does not match dimensions")
|
||||
raw = b"".join(b"\x00" + pixels[y * width * 3:(y + 1) * width * 3] for y in range(height))
|
||||
|
||||
def chunk(tag, body):
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that ppm_to_png keeps every raster byte and rejects malformed PPM files."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
|
||||
source = Path(__file__).resolve().parents[1] / 'package' / 'Scripts' / 'qemu-run.py'
|
||||
spec = importlib.util.spec_from_file_location('qemu_run', source)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
valid = [
|
||||
(b'P6\n1 1\n255\n', b'\x01\x02\x03', 1, 1),
|
||||
(b'P6\n2 1\n255\n', b'\x09\x0a\x0b\x0c\x0d\x20', 2, 1),
|
||||
(b'P6\n1 1\n255\n', b'#\x00\xff', 1, 1),
|
||||
(b'P6\n# size\n1 1\n255\n', b'abc', 1, 1),
|
||||
(b'P6 # magic\n1 1\n255\n', b'abc', 1, 1),
|
||||
(b'P6\n1 # width\n1\n255\n', b'abc', 1, 1),
|
||||
(b'P6\n1 2\n255\n', b'abcdef', 1, 2),
|
||||
]
|
||||
invalid = [b'', b'P3\n1 1\n255\nabc', b'P6\n0 1\n255\n',
|
||||
b'P6\n-1 1\n255\nabc', b'P6\n1 1\n65535\nabcdef',
|
||||
b'P6\n1 1\n255\nab', b'P6\n1 1\n255\nabcd']
|
||||
passed = rejected = 0
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
ppm, png = Path(work) / 'in.ppm', Path(work) / 'out.png'
|
||||
for index, (header, pixels, width, height) in enumerate(valid):
|
||||
ppm.write_bytes(header + pixels)
|
||||
try:
|
||||
module.ppm_to_png(ppm, png)
|
||||
data = png.read_bytes()
|
||||
assert data[:8] == b'\x89PNG\r\n\x1a\n'
|
||||
offset, compressed = 8, b''
|
||||
while offset < len(data):
|
||||
size = struct.unpack_from('>I', data, offset)[0]
|
||||
tag = data[offset + 4:offset + 8]
|
||||
body = data[offset + 8:offset + 8 + size]
|
||||
crc = struct.unpack_from('>I', data, offset + 8 + size)[0]
|
||||
assert crc == zlib.crc32(tag + body) & 0xffffffff
|
||||
if tag == b'IHDR':
|
||||
assert struct.unpack('>IIBBBBB', body) == (width, height, 8, 2, 0, 0, 0)
|
||||
if tag == b'IDAT':
|
||||
compressed += body
|
||||
offset += size + 12
|
||||
expected = b''.join(b'\0' + pixels[y*width*3:(y+1)*width*3] for y in range(height))
|
||||
assert zlib.decompress(compressed) == expected
|
||||
passed += 1
|
||||
except Exception as error:
|
||||
print('valid case', index + 1, 'FAIL', type(error).__name__)
|
||||
for index, data in enumerate(invalid):
|
||||
ppm.write_bytes(data)
|
||||
try:
|
||||
module.ppm_to_png(ppm, png)
|
||||
except ValueError:
|
||||
rejected += 1
|
||||
except Exception as error:
|
||||
print('invalid case', index + 1, 'wrong error', type(error).__name__)
|
||||
print(f'valid={passed}/{len(valid)} invalid_rejected={rejected}/{len(invalid)}')
|
||||
sys.exit(0 if passed == len(valid) and rejected == len(invalid) else 1)
|
||||
Reference in new issue
Block a user