python-pages 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pages/protobuf.py ADDED
@@ -0,0 +1,162 @@
1
+ """Small lossless protobuf wire parser for schema discovery and mutation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Iterable
7
+
8
+
9
+ class ProtobufError(ValueError):
10
+ pass
11
+
12
+
13
+ def read_varint(data: bytes, pos: int = 0) -> tuple[int, int]:
14
+ value = 0
15
+ shift = 0
16
+ for _ in range(10):
17
+ if pos >= len(data):
18
+ raise ProtobufError("truncated varint")
19
+ byte = data[pos]
20
+ pos += 1
21
+ value |= (byte & 0x7F) << shift
22
+ if byte < 0x80:
23
+ return value, pos
24
+ shift += 7
25
+ raise ProtobufError("varint is longer than 10 bytes")
26
+
27
+
28
+ def encode_varint(value: int) -> bytes:
29
+ if value < 0:
30
+ value &= (1 << 64) - 1
31
+ out = bytearray()
32
+ while value >= 0x80:
33
+ out.append((value & 0x7F) | 0x80)
34
+ value >>= 7
35
+ out.append(value)
36
+ return bytes(out)
37
+
38
+
39
+ @dataclass
40
+ class Field:
41
+ number: int
42
+ wire_type: int
43
+ value: int | bytes
44
+ original: bytes = b""
45
+ dirty: bool = False
46
+ children: "Message | None" = None
47
+
48
+ def encode(self) -> bytes:
49
+ if not self.dirty and self.original:
50
+ return self.original
51
+ key = encode_varint((self.number << 3) | self.wire_type)
52
+ if self.wire_type == 0:
53
+ return key + encode_varint(int(self.value))
54
+ if self.wire_type == 1:
55
+ return key + int(self.value).to_bytes(8, "little")
56
+ if self.wire_type == 2:
57
+ payload = self.children.encode() if self.children is not None else bytes(self.value)
58
+ return key + encode_varint(len(payload)) + payload
59
+ if self.wire_type == 5:
60
+ return key + int(self.value).to_bytes(4, "little")
61
+ raise ProtobufError(f"unsupported wire type {self.wire_type}")
62
+
63
+ def set_bytes(self, value: bytes) -> None:
64
+ if self.wire_type != 2:
65
+ raise TypeError("not a length-delimited field")
66
+ self.value = value
67
+ self.children = None
68
+ self.dirty = True
69
+
70
+ def set_varint(self, value: int) -> None:
71
+ if self.wire_type != 0:
72
+ raise TypeError("not a varint field")
73
+ self.value = value
74
+ self.dirty = True
75
+
76
+ def set_fixed32(self, value: int) -> None:
77
+ if self.wire_type != 5:
78
+ raise TypeError("not a fixed32 field")
79
+ self.value = value
80
+ self.dirty = True
81
+
82
+
83
+ @dataclass
84
+ class Message:
85
+ fields: list[Field] = field(default_factory=list)
86
+
87
+ @classmethod
88
+ def parse(cls, data: bytes) -> "Message":
89
+ fields: list[Field] = []
90
+ pos = 0
91
+ while pos < len(data):
92
+ start = pos
93
+ key, pos = read_varint(data, pos)
94
+ number, wire = key >> 3, key & 7
95
+ if number == 0:
96
+ raise ProtobufError(f"field zero at offset {start}")
97
+ if wire == 0:
98
+ value, pos = read_varint(data, pos)
99
+ elif wire == 1:
100
+ if pos + 8 > len(data):
101
+ raise ProtobufError("truncated fixed64")
102
+ value = int.from_bytes(data[pos : pos + 8], "little")
103
+ pos += 8
104
+ elif wire == 2:
105
+ length, pos = read_varint(data, pos)
106
+ if pos + length > len(data):
107
+ raise ProtobufError("truncated length-delimited field")
108
+ value = data[pos : pos + length]
109
+ pos += length
110
+ elif wire == 5:
111
+ if pos + 4 > len(data):
112
+ raise ProtobufError("truncated fixed32")
113
+ value = int.from_bytes(data[pos : pos + 4], "little")
114
+ pos += 4
115
+ else:
116
+ raise ProtobufError(f"unsupported wire type {wire} at {start}")
117
+ fields.append(Field(number, wire, value, data[start:pos]))
118
+ return cls(fields)
119
+
120
+ def encode(self) -> bytes:
121
+ return b"".join(f.encode() for f in self.fields)
122
+
123
+ def get(self, number: int) -> list[Field]:
124
+ return [f for f in self.fields if f.number == number]
125
+
126
+ def one(self, number: int, default=None):
127
+ items = self.get(number)
128
+ return items[0] if items else default
129
+
130
+ def add_varint(self, number: int, value: int) -> Field:
131
+ result = Field(number, 0, value, dirty=True)
132
+ self.fields.append(result)
133
+ return result
134
+
135
+ def add_bytes(self, number: int, value: bytes) -> Field:
136
+ result = Field(number, 2, value, dirty=True)
137
+ self.fields.append(result)
138
+ return result
139
+
140
+ def add_fixed32(self, number: int, value: int) -> Field:
141
+ result = Field(number, 5, value, dirty=True)
142
+ self.fields.append(result)
143
+ return result
144
+
145
+ def iter_strings(self, min_length: int = 2) -> Iterable[tuple[Field, str]]:
146
+ for item in self.fields:
147
+ if item.wire_type != 2 or len(item.value) < min_length:
148
+ continue
149
+ try:
150
+ text = bytes(item.value).decode("utf-8")
151
+ except UnicodeDecodeError:
152
+ continue
153
+ if text.isprintable() or any(c in text for c in "\n\t"):
154
+ yield item, text
155
+
156
+
157
+ def try_message(data: bytes) -> Message | None:
158
+ try:
159
+ message = Message.parse(data)
160
+ except ProtobufError:
161
+ return None
162
+ return message if message.fields else None
pages/snappy.py ADDED
@@ -0,0 +1,140 @@
1
+ """Raw Snappy and iWork's small custom framing.
2
+
3
+ iWork uses blocks consisting of byte 0 followed by a 24-bit little-endian
4
+ compressed length and a raw (not standard framed-stream) Snappy block.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class SnappyError(ValueError):
11
+ pass
12
+
13
+
14
+ def _read_varint(data: bytes, pos: int = 0) -> tuple[int, int]:
15
+ value = 0
16
+ shift = 0
17
+ while pos < len(data) and shift < 35:
18
+ byte = data[pos]
19
+ pos += 1
20
+ value |= (byte & 0x7F) << shift
21
+ if byte < 0x80:
22
+ return value, pos
23
+ shift += 7
24
+ raise SnappyError("invalid Snappy length varint")
25
+
26
+
27
+ def _varint(value: int) -> bytes:
28
+ out = bytearray()
29
+ while value >= 0x80:
30
+ out.append((value & 0x7F) | 0x80)
31
+ value >>= 7
32
+ out.append(value)
33
+ return bytes(out)
34
+
35
+
36
+ def decompress_raw(data: bytes) -> bytes:
37
+ expected, pos = _read_varint(data)
38
+ out = bytearray()
39
+ while pos < len(data):
40
+ tag = data[pos]
41
+ pos += 1
42
+ kind = tag & 3
43
+ if kind == 0:
44
+ n = tag >> 2
45
+ if n < 60:
46
+ length = n + 1
47
+ else:
48
+ width = n - 59
49
+ if width > 4 or pos + width > len(data):
50
+ raise SnappyError("invalid literal length")
51
+ length = int.from_bytes(data[pos : pos + width], "little") + 1
52
+ pos += width
53
+ if pos + length > len(data):
54
+ raise SnappyError("truncated literal")
55
+ out += data[pos : pos + length]
56
+ pos += length
57
+ continue
58
+
59
+ if kind == 1:
60
+ if pos >= len(data):
61
+ raise SnappyError("truncated COPY_1")
62
+ length = ((tag >> 2) & 7) + 4
63
+ offset = ((tag & 0xE0) << 3) | data[pos]
64
+ pos += 1
65
+ elif kind == 2:
66
+ if pos + 2 > len(data):
67
+ raise SnappyError("truncated COPY_2")
68
+ length = (tag >> 2) + 1
69
+ offset = int.from_bytes(data[pos : pos + 2], "little")
70
+ pos += 2
71
+ else:
72
+ if pos + 4 > len(data):
73
+ raise SnappyError("truncated COPY_4")
74
+ length = (tag >> 2) + 1
75
+ offset = int.from_bytes(data[pos : pos + 4], "little")
76
+ pos += 4
77
+ if offset <= 0 or offset > len(out):
78
+ raise SnappyError(f"invalid copy offset {offset}")
79
+ for _ in range(length):
80
+ out.append(out[-offset])
81
+
82
+ if len(out) != expected:
83
+ raise SnappyError(f"decoded {len(out)} bytes, expected {expected}")
84
+ return bytes(out)
85
+
86
+
87
+ def compress_raw_literal(data: bytes) -> bytes:
88
+ """Create a valid raw Snappy block using literals only.
89
+
90
+ It is deliberately simple: ZIP will deflate the outer .pages archive, and
91
+ correctness/preservation is more important here than IWA-level ratio.
92
+ """
93
+ out = bytearray(_varint(len(data)))
94
+ pos = 0
95
+ while pos < len(data):
96
+ length = min(len(data) - pos, 65536)
97
+ n = length - 1
98
+ if n < 60:
99
+ out.append(n << 2)
100
+ else:
101
+ width = max(1, (n.bit_length() + 7) // 8)
102
+ out.append((59 + width) << 2)
103
+ out += n.to_bytes(width, "little")
104
+ out += data[pos : pos + length]
105
+ pos += length
106
+ return bytes(out)
107
+
108
+
109
+ def decompress_iwork(data: bytes) -> tuple[bytes, list[int]]:
110
+ out = bytearray()
111
+ decoded_sizes: list[int] = []
112
+ pos = 0
113
+ while pos < len(data):
114
+ if pos + 4 > len(data) or data[pos] != 0:
115
+ raise SnappyError(f"bad iWork frame at offset {pos}")
116
+ length = int.from_bytes(data[pos + 1 : pos + 4], "little")
117
+ pos += 4
118
+ if pos + length > len(data):
119
+ raise SnappyError("truncated iWork frame")
120
+ block = decompress_raw(data[pos : pos + length])
121
+ out += block
122
+ decoded_sizes.append(len(block))
123
+ pos += length
124
+ return bytes(out), decoded_sizes
125
+
126
+
127
+ def compress_iwork(data: bytes, decoded_sizes: list[int] | None = None) -> bytes:
128
+ if not decoded_sizes or sum(decoded_sizes) != len(data):
129
+ decoded_sizes = [min(65536, len(data) - pos) for pos in range(0, len(data), 65536)]
130
+ out = bytearray()
131
+ pos = 0
132
+ for size in decoded_sizes:
133
+ compressed = compress_raw_literal(data[pos : pos + size])
134
+ if len(compressed) >= 1 << 24:
135
+ raise SnappyError("compressed iWork block exceeds 24-bit length")
136
+ out.append(0)
137
+ out += len(compressed).to_bytes(3, "little")
138
+ out += compressed
139
+ pos += size
140
+ return bytes(out)