vcdiff-decoder 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.
@@ -0,0 +1,106 @@
1
+ """VCDIFF default code table implementation - RFC 3284 Section 5"""
2
+
3
+ from typing import List, Tuple
4
+
5
+ from .types import Instruction, InstructionType
6
+
7
+
8
+ class CodeTable:
9
+ """VCDIFF instruction code table"""
10
+
11
+ def __init__(self):
12
+ """Initialize empty code table"""
13
+ # Initialize all entries to NO_OP
14
+ self.entries: List[Tuple[Instruction, Instruction]] = [
15
+ (
16
+ Instruction(InstructionType.NO_OP, 0, 0),
17
+ Instruction(InstructionType.NO_OP, 0, 0)
18
+ )
19
+ for _ in range(256)
20
+ ]
21
+
22
+ def get(self, code: int, slot: int) -> Instruction:
23
+ """Get the instruction at the given code and slot
24
+
25
+ Args:
26
+ code: Instruction code (0-255)
27
+ slot: Slot number (0 or 1)
28
+
29
+ Returns:
30
+ The instruction at the specified code and slot
31
+ """
32
+ return self.entries[code][slot]
33
+
34
+
35
+ def build_default_code_table() -> CodeTable:
36
+ """Create the default code table specified in RFC 3284
37
+
38
+ Returns:
39
+ The default code table
40
+ """
41
+ ct = CodeTable()
42
+
43
+ # Entry 0: RUN with size 0
44
+ ct.entries[0] = (
45
+ Instruction(InstructionType.RUN, 0, 0),
46
+ Instruction(InstructionType.NO_OP, 0, 0)
47
+ )
48
+
49
+ # Entries 1-18: ADD with sizes 0-17
50
+ for i in range(18):
51
+ ct.entries[i + 1] = (
52
+ Instruction(InstructionType.ADD, i, 0),
53
+ Instruction(InstructionType.NO_OP, 0, 0)
54
+ )
55
+
56
+ index = 19
57
+
58
+ # Entries 19-162: COPY instructions with different modes and sizes
59
+ for mode in range(9):
60
+ # COPY with size 0 (size will be read from stream)
61
+ ct.entries[index] = (
62
+ Instruction(InstructionType.COPY, 0, mode),
63
+ Instruction(InstructionType.NO_OP, 0, 0)
64
+ )
65
+ index += 1
66
+
67
+ # COPY with sizes 4-18
68
+ for size in range(4, 19):
69
+ ct.entries[index] = (
70
+ Instruction(InstructionType.COPY, size, mode),
71
+ Instruction(InstructionType.NO_OP, 0, 0)
72
+ )
73
+ index += 1
74
+
75
+ # Entries 163-234: Combined ADD+COPY instructions
76
+ for mode in range(6):
77
+ for add_size in range(1, 5):
78
+ for copy_size in range(4, 7):
79
+ ct.entries[index] = (
80
+ Instruction(InstructionType.ADD, add_size, 0),
81
+ Instruction(InstructionType.COPY, copy_size, mode)
82
+ )
83
+ index += 1
84
+
85
+ # Entries 235-246: More combined ADD+COPY instructions
86
+ for mode in range(6, 9):
87
+ for add_size in range(1, 5):
88
+ ct.entries[index] = (
89
+ Instruction(InstructionType.ADD, add_size, 0),
90
+ Instruction(InstructionType.COPY, 4, mode)
91
+ )
92
+ index += 1
93
+
94
+ # Entries 247-255: COPY+ADD combinations
95
+ for mode in range(9):
96
+ ct.entries[index] = (
97
+ Instruction(InstructionType.COPY, 4, mode),
98
+ Instruction(InstructionType.ADD, 1, 0)
99
+ )
100
+ index += 1
101
+
102
+ return ct
103
+
104
+
105
+ # Default code table instance
106
+ DEFAULT_CODE_TABLE = build_default_code_table()
@@ -0,0 +1,435 @@
1
+ """VCDIFF decoder implementation - RFC 3284 compliance"""
2
+
3
+ import io
4
+ from typing import BinaryIO, List, Optional, Union
5
+
6
+ from .types import (
7
+ Header, Window, ParsedDelta, RuntimeInstruction, InstructionType,
8
+ VCDIFF_MAGIC, VCDIFF_VERSION, MINIMUM_FILE_SIZE,
9
+ VCD_DECOMPRESS, VCD_CODETABLE, VCD_APPHEADER,
10
+ VCD_SOURCE, VCD_TARGET, VCD_ADLER32,
11
+ NEAR_CACHE_SIZE, SAME_CACHE_SIZE
12
+ )
13
+ from .exceptions import (
14
+ VCDIFFError, InvalidMagicError, InvalidVersionError, InvalidFormatError,
15
+ CorruptedDataError, InvalidChecksumError,
16
+ err_unexpected_eof, err_data_overrun, err_invalid_value, err_out_of_bounds
17
+ )
18
+ from .varint import read_varint
19
+ from .adler32 import compute_checksum
20
+ from .addresscache import AddressCache
21
+ from .codetable import DEFAULT_CODE_TABLE
22
+
23
+
24
+ class Decoder:
25
+ """VCDIFF decoder interface"""
26
+
27
+ def __init__(self, source: Union[bytes, bytearray]):
28
+ """Initialize decoder with source data
29
+
30
+ Args:
31
+ source: Source data for applying deltas
32
+ """
33
+ self.source = bytes(source)
34
+
35
+ def decode(self, delta: Union[bytes, bytearray]) -> bytes:
36
+ """Decode a VCDIFF delta and return target data
37
+
38
+ Args:
39
+ delta: VCDIFF delta data
40
+
41
+ Returns:
42
+ The decoded target data
43
+
44
+ Raises:
45
+ VCDIFFError: If the delta is malformed or cannot be decoded
46
+ """
47
+ # Parse the delta to get structured information
48
+ parsed = parse_delta(delta)
49
+
50
+ # Process all windows and accumulate target data
51
+ target = bytearray()
52
+
53
+ for window in parsed.windows:
54
+ # Decode this window's target data
55
+ window_target = self._decode_window(window, self.source)
56
+ target.extend(window_target)
57
+
58
+ return bytes(target)
59
+
60
+ def _decode_window(self, window: Window, source: bytes) -> bytes:
61
+ """Decode a single window using the source data and window instructions
62
+
63
+ Args:
64
+ window: Window to decode
65
+ source: Source data
66
+
67
+ Returns:
68
+ Decoded target data for this window
69
+
70
+ Raises:
71
+ VCDIFFError: If the window cannot be decoded
72
+ """
73
+ # Initialize address cache
74
+ address_cache = AddressCache(NEAR_CACHE_SIZE, SAME_CACHE_SIZE)
75
+ address_cache.reset(window.address_section)
76
+
77
+ # Create target buffer
78
+ target = bytearray()
79
+
80
+ # Get source segment for this window
81
+ source_segment = b''
82
+ source_length = 0
83
+ if window.win_indicator & VCD_SOURCE:
84
+ # Use source data
85
+ start = window.source_segment_position
86
+ end = start + window.source_segment_size
87
+ if end > len(source):
88
+ raise InvalidFormatError("source segment extends beyond source data")
89
+ source_segment = source[start:end]
90
+ source_length = len(source_segment)
91
+
92
+ # Parse and execute the actual instructions
93
+ instructions = self._parse_instructions(
94
+ window.instruction_section,
95
+ window.data_section,
96
+ address_cache
97
+ )
98
+
99
+ # Execute each instruction
100
+ for instruction in instructions:
101
+ if instruction.type == InstructionType.NO_OP:
102
+ # Skip
103
+ continue
104
+
105
+ elif instruction.type == InstructionType.ADD:
106
+ # Add data from the instruction's data
107
+ if len(instruction.data) != instruction.size:
108
+ raise InvalidFormatError("ADD instruction data size mismatch")
109
+ target.extend(instruction.data)
110
+
111
+ elif instruction.type == InstructionType.COPY:
112
+ # Decode the address using the address cache
113
+ here = len(target) + source_length
114
+ addr = address_cache.decode_address(here, instruction.mode)
115
+
116
+ # Determine if copying from source or target
117
+ if addr < source_length:
118
+ # Copy from source segment
119
+ end = addr + instruction.size
120
+ if end > source_length:
121
+ raise err_out_of_bounds("COPY", addr, instruction.size, source_length)
122
+ target.extend(source_segment[addr:end])
123
+ else:
124
+ # Copy from target data (self-referential copy)
125
+ target_addr = addr - source_length
126
+ if target_addr >= len(target):
127
+ raise VCDIFFError(
128
+ f"COPY instruction address {addr} references target position "
129
+ f"{target_addr} but target only has {len(target)} bytes"
130
+ )
131
+
132
+ # Handle overlapping copies byte by byte
133
+ for i in range(instruction.size):
134
+ if target_addr + i >= len(target):
135
+ raise VCDIFFError(
136
+ f"COPY instruction would read beyond target bounds: "
137
+ f"position {target_addr + i}, target size {len(target)}"
138
+ )
139
+ target.append(target[target_addr + i])
140
+
141
+ elif instruction.type == InstructionType.RUN:
142
+ # Repeat a single byte
143
+ if len(instruction.data) != 1:
144
+ raise InvalidFormatError("RUN instruction must have exactly 1 data byte")
145
+ run_byte = instruction.data[0]
146
+ target.extend([run_byte] * instruction.size)
147
+
148
+ else:
149
+ raise InvalidFormatError(f"unknown instruction type: {instruction.type}")
150
+
151
+ # Validate Adler32 checksum if present
152
+ if window.has_checksum:
153
+ computed = compute_checksum(1, target) # Adler32 starts with initial value 1
154
+ if computed != window.checksum:
155
+ raise InvalidChecksumError(
156
+ f"checksum validation failed: expected 0x{window.checksum:08x}, "
157
+ f"got 0x{computed:08x}"
158
+ )
159
+
160
+ return bytes(target)
161
+
162
+ def _parse_instructions(
163
+ self,
164
+ instruction_data: bytes,
165
+ data_section: bytes,
166
+ address_cache: AddressCache
167
+ ) -> List[RuntimeInstruction]:
168
+ """Parse the instruction data from a window using the code table
169
+
170
+ Args:
171
+ instruction_data: Raw instruction bytes
172
+ data_section: Data section for ADD and RUN instructions
173
+ address_cache: Address cache for this window
174
+
175
+ Returns:
176
+ List of runtime instructions
177
+
178
+ Raises:
179
+ VCDIFFError: If instructions cannot be parsed
180
+ """
181
+ stream = io.BytesIO(instruction_data)
182
+ instructions = []
183
+ data_index = 0
184
+ instruction_offset = 0
185
+
186
+ while True:
187
+ code_data = stream.read(1)
188
+ if not code_data:
189
+ break
190
+
191
+ code = code_data[0]
192
+
193
+ # Each code can have up to 2 instructions
194
+ for slot in range(2):
195
+ instruction = DEFAULT_CODE_TABLE.get(code, slot)
196
+ if instruction.type == InstructionType.NO_OP:
197
+ continue
198
+
199
+ size = instruction.size
200
+ if size == 0 and instruction.type != InstructionType.NO_OP:
201
+ size = read_varint(stream)
202
+
203
+ runtime_inst = RuntimeInstruction(
204
+ type=instruction.type,
205
+ size=size,
206
+ mode=instruction.mode
207
+ )
208
+
209
+ # Handle instruction-specific data
210
+ if instruction.type == InstructionType.ADD:
211
+ if data_index + size > len(data_section):
212
+ raise err_data_overrun("ADD", instruction_offset, size, len(data_section) - data_index)
213
+ runtime_inst.data = data_section[data_index:data_index + size]
214
+ data_index += size
215
+
216
+ elif instruction.type == InstructionType.RUN:
217
+ if data_index >= len(data_section):
218
+ raise VCDIFFError(
219
+ f"RUN instruction at offset {instruction_offset} requires 1 byte "
220
+ "but no data available in data section"
221
+ )
222
+ runtime_inst.data = bytes([data_section[data_index]])
223
+ data_index += 1
224
+
225
+ elif instruction.type == InstructionType.COPY:
226
+ # Address will be decoded when needed during execution
227
+ runtime_inst.mode = instruction.mode
228
+
229
+ instructions.append(runtime_inst)
230
+
231
+ instruction_offset += 1
232
+
233
+ return instructions
234
+
235
+
236
+ def decode(source: Union[bytes, bytearray], delta: Union[bytes, bytearray]) -> bytes:
237
+ """Decode a VCDIFF delta against source data
238
+
239
+ Args:
240
+ source: Source data
241
+ delta: VCDIFF delta data
242
+
243
+ Returns:
244
+ The decoded target data
245
+
246
+ Raises:
247
+ VCDIFFError: If the delta cannot be decoded
248
+ """
249
+ decoder = Decoder(source)
250
+ return decoder.decode(delta)
251
+
252
+
253
+ def parse_delta(delta: Union[bytes, bytearray]) -> ParsedDelta:
254
+ """Parse a VCDIFF delta and return a structured representation
255
+
256
+ Args:
257
+ delta: VCDIFF delta data
258
+
259
+ Returns:
260
+ Parsed delta structure
261
+
262
+ Raises:
263
+ VCDIFFError: If the delta cannot be parsed
264
+ """
265
+ delta_bytes = bytes(delta)
266
+ if len(delta_bytes) < MINIMUM_FILE_SIZE:
267
+ raise InvalidFormatError("delta too small to be valid VCDIFF")
268
+
269
+ parsed = ParsedDelta(
270
+ header=Header(b'', 0, 0),
271
+ windows=[],
272
+ instructions=[]
273
+ )
274
+
275
+ stream = io.BytesIO(delta_bytes)
276
+
277
+ _parse_header(stream, parsed)
278
+
279
+ while stream.tell() < len(delta_bytes):
280
+ window = _parse_window(stream)
281
+ parsed.windows.append(window)
282
+
283
+ # Create address cache for this window
284
+ address_cache = AddressCache(NEAR_CACHE_SIZE, SAME_CACHE_SIZE)
285
+ address_cache.reset(window.address_section)
286
+
287
+ # Parse instructions using the instruction section and data section
288
+ decoder_instance = Decoder(b'') # Temporary decoder for parsing
289
+ instructions = decoder_instance._parse_instructions(
290
+ window.instruction_section,
291
+ window.data_section,
292
+ address_cache
293
+ )
294
+ parsed.instructions.extend(instructions)
295
+
296
+ return parsed
297
+
298
+
299
+ def _parse_header(stream: BinaryIO, parsed: ParsedDelta) -> None:
300
+ """Parse the VCDIFF header section
301
+
302
+ Args:
303
+ stream: Input stream
304
+ parsed: Parsed delta to update
305
+
306
+ Raises:
307
+ VCDIFFError: If the header is invalid
308
+ """
309
+ # Read 3 magic bytes as defined in RFC 3284
310
+ magic = stream.read(3)
311
+ if len(magic) < 3:
312
+ raise err_unexpected_eof("VCDIFF magic bytes", 3 - len(magic))
313
+
314
+ # Compare magic bytes - RFC 3284 Section 4.1
315
+ if magic != VCDIFF_MAGIC:
316
+ raise InvalidMagicError(
317
+ f"invalid VCDIFF magic bytes at offset 0: expected "
318
+ f"{VCDIFF_MAGIC.hex()} but got {magic.hex()}"
319
+ )
320
+
321
+ version_data = stream.read(1)
322
+ if not version_data:
323
+ raise err_unexpected_eof("version byte", 1)
324
+
325
+ version = version_data[0]
326
+ if version != VCDIFF_VERSION:
327
+ raise InvalidVersionError(
328
+ f"invalid version at offset 3: value {version}, "
329
+ f"only version {VCDIFF_VERSION} is supported"
330
+ )
331
+
332
+ indicator_data = stream.read(1)
333
+ if not indicator_data:
334
+ raise err_unexpected_eof("header indicator", 1)
335
+
336
+ indicator = indicator_data[0]
337
+
338
+ # Check for reserved bits in header indicator
339
+ valid_header_bits = VCD_DECOMPRESS | VCD_CODETABLE | VCD_APPHEADER
340
+ if indicator & ~valid_header_bits != 0:
341
+ raise err_invalid_value("header indicator", 4, indicator, "reserved bits must be zero")
342
+
343
+ parsed.header = Header(magic, version, indicator)
344
+
345
+
346
+ def _parse_window(stream: BinaryIO) -> Window:
347
+ """Parse a single VCDIFF window
348
+
349
+ Args:
350
+ stream: Input stream
351
+
352
+ Returns:
353
+ Parsed window
354
+
355
+ Raises:
356
+ VCDIFFError: If the window is invalid
357
+ """
358
+ indicator_data = stream.read(1)
359
+ if not indicator_data:
360
+ raise err_unexpected_eof("window indicator", 1)
361
+
362
+ indicator = indicator_data[0]
363
+
364
+ # Check for reserved bits in window indicator
365
+ valid_bits = VCD_SOURCE | VCD_TARGET | VCD_ADLER32
366
+ if indicator & ~valid_bits != 0:
367
+ raise err_invalid_value("window indicator", stream.tell() - 1, indicator, "reserved bits must be zero")
368
+
369
+ window = Window(win_indicator=indicator)
370
+
371
+ if indicator & VCD_SOURCE:
372
+ window.source_segment_size = read_varint(stream)
373
+ window.source_segment_position = read_varint(stream)
374
+
375
+ # Read the length of the delta encoding
376
+ window.delta_encoding_length = read_varint(stream)
377
+
378
+ # Read the delta encoding section - RFC 3284 Section 4.3
379
+ delta_data = stream.read(window.delta_encoding_length)
380
+ if len(delta_data) != window.delta_encoding_length:
381
+ raise err_unexpected_eof("delta encoding", window.delta_encoding_length - len(delta_data))
382
+
383
+ # Parse the delta encoding according to RFC 3284 Section 4.3
384
+ delta_stream = io.BytesIO(delta_data)
385
+
386
+ # 1. Length of the target window
387
+ window.target_window_length = read_varint(delta_stream)
388
+
389
+ # 2. Delta_Indicator byte
390
+ delta_indicator_data = delta_stream.read(1)
391
+ if not delta_indicator_data:
392
+ raise err_unexpected_eof("delta indicator", 1)
393
+ window.delta_indicator = delta_indicator_data[0]
394
+
395
+ # 3. Length of data for ADDs and RUNs
396
+ window.data_section_length = read_varint(delta_stream)
397
+
398
+ # 4. Length of instructions section
399
+ window.instruction_section_length = read_varint(delta_stream)
400
+
401
+ # 5. Length of addresses for COPYs
402
+ window.address_section_length = read_varint(delta_stream)
403
+
404
+ # Handle VCD_ADLER32 extension - checksum comes AFTER section lengths but BEFORE data sections
405
+ if indicator & VCD_ADLER32:
406
+ window.has_checksum = True
407
+ # Read the 4-byte checksum from the delta encoding data
408
+ checksum_bytes = delta_stream.read(4)
409
+ if len(checksum_bytes) != 4:
410
+ raise err_unexpected_eof("Adler32 checksum", 4 - len(checksum_bytes))
411
+ # Convert to uint32 (big-endian)
412
+ window.checksum = (
413
+ (checksum_bytes[0] << 24) |
414
+ (checksum_bytes[1] << 16) |
415
+ (checksum_bytes[2] << 8) |
416
+ checksum_bytes[3]
417
+ )
418
+
419
+ # 6. Data section for ADDs and RUNs
420
+ window.data_section = delta_stream.read(window.data_section_length)
421
+ if len(window.data_section) != window.data_section_length:
422
+ raise err_unexpected_eof("data section", window.data_section_length - len(window.data_section))
423
+
424
+ # 7. Instructions and sizes section
425
+ window.instruction_section = delta_stream.read(window.instruction_section_length)
426
+ if len(window.instruction_section) != window.instruction_section_length:
427
+ raise err_unexpected_eof("instruction section", window.instruction_section_length - len(window.instruction_section))
428
+
429
+ # 8. Addresses section for COPYs
430
+ if window.address_section_length > 0:
431
+ window.address_section = delta_stream.read(window.address_section_length)
432
+ if len(window.address_section) != window.address_section_length:
433
+ raise err_unexpected_eof("address section", window.address_section_length - len(window.address_section))
434
+
435
+ return window
@@ -0,0 +1,56 @@
1
+ """VCDIFF exception classes"""
2
+
3
+
4
+ class VCDIFFError(Exception):
5
+ """Base exception for VCDIFF operations"""
6
+ pass
7
+
8
+
9
+ class InvalidMagicError(VCDIFFError):
10
+ """Invalid VCDIFF magic bytes"""
11
+ pass
12
+
13
+
14
+ class InvalidVersionError(VCDIFFError):
15
+ """Unsupported VCDIFF version"""
16
+ pass
17
+
18
+
19
+ class InvalidFormatError(VCDIFFError):
20
+ """Invalid VCDIFF format"""
21
+ pass
22
+
23
+
24
+ class CorruptedDataError(VCDIFFError):
25
+ """Corrupted VCDIFF data"""
26
+ pass
27
+
28
+
29
+ class InvalidChecksumError(VCDIFFError):
30
+ """Invalid checksum"""
31
+ pass
32
+
33
+
34
+ def err_unexpected_eof(context: str, bytes_needed: int) -> VCDIFFError:
35
+ """Create unexpected EOF error with context"""
36
+ return VCDIFFError(f"unexpected EOF while reading {context}: need {bytes_needed} bytes")
37
+
38
+
39
+ def err_data_overrun(instruction: str, offset: int, needed: int, available: int) -> VCDIFFError:
40
+ """Create data overrun error"""
41
+ return VCDIFFError(
42
+ f"{instruction} instruction at offset {offset} requires {needed} bytes "
43
+ f"but only {available} available in data section"
44
+ )
45
+
46
+
47
+ def err_invalid_value(field: str, offset: int, value, reason: str) -> VCDIFFError:
48
+ """Create invalid value error"""
49
+ return VCDIFFError(f"invalid {field} at offset {offset}: value {value}, {reason}")
50
+
51
+
52
+ def err_out_of_bounds(instruction: str, address: int, size: int, max_bound: int) -> VCDIFFError:
53
+ """Create out of bounds error"""
54
+ return VCDIFFError(
55
+ f"{instruction} instruction address {address} + size {size} exceeds bounds (max {max_bound})"
56
+ )