|
15 | 15 | import base64 |
16 | 16 | import builtins |
17 | 17 | import collections |
| 18 | +import collections.abc |
18 | 19 | import datetime |
19 | 20 | import decimal |
20 | 21 | import enum |
|
178 | 179 | from pcapkit.protocols.schema.misc.pcapng import ZigBeeNWKKey as Schema_ZigBeeNWKKey |
179 | 180 | from pcapkit.protocols.schema.schema import Schema |
180 | 181 | from pcapkit.utilities.compat import StrEnum, localcontext |
181 | | -from pcapkit.utilities.exceptions import (EnumValueError, FormatError, ProtocolError, RegistryError, |
182 | | - StreamEOFError, UnsupportedCall, stacklevel) |
| 182 | +from pcapkit.utilities.exceptions import (BaseError, EnumValueError, FormatError, ProtocolError, |
| 183 | + RegistryError, StreamEOFError, UnsupportedCall, |
| 184 | + stacklevel) |
183 | 185 | from pcapkit.utilities.warnings import (AttributeWarning, DeprecatedFormatWarning, ProtocolWarning, |
184 | 186 | RegistryWarning, warn) |
185 | 187 |
|
@@ -1264,7 +1266,15 @@ def read(self, length: 'Optional[int]' = None, *, _read: 'bool' = True, |
1264 | 1266 | getattr(self, meth_name, self._read_block_unknown)) |
1265 | 1267 | else: |
1266 | 1268 | meth = name[0] |
1267 | | - block = meth(schema.block, header=schema) |
| 1269 | + try: |
| 1270 | + block = meth(schema.block, header=schema) |
| 1271 | + except BaseError as error: |
| 1272 | + # NOTE: Fields past the end of a block the capture ended inside are |
| 1273 | + # read as zeros, and a value its parser refuses is an artefact of |
| 1274 | + # that, not of the block; it is kept as captured instead. |
| 1275 | + if not _read or self._captured(_seek_set) >= schema.block.length: |
| 1276 | + raise |
| 1277 | + block = self._read_block_cut(schema, _seek_set, error) |
1268 | 1278 |
|
1269 | 1279 | # NOTE: An option or record area holding a malformed entry is kept as |
1270 | 1280 | # the octets captured, as one :obj:`bytes` item (see |
@@ -1314,6 +1324,14 @@ def read(self, length: 'Optional[int]' = None, *, _read: 'bool' = True, |
1314 | 1324 | stacklevel=stacklevel()) |
1315 | 1325 | seek_cur = min(seek_cur, _seek_set + read) |
1316 | 1326 |
|
| 1327 | + # NOTE: The fields read past the end of the capture are zeros, |
| 1328 | + # and an option, record or packet the end cut through is only |
| 1329 | + # its first octets, so the block cannot be rebuilt from them at |
| 1330 | + # the length it declares. The octets captured are kept as |
| 1331 | + # ``__truncated_raw__`` instead, which :meth:`__post_init__` parses |
| 1332 | + # again in place of building the block (:issue:`1470`). |
| 1333 | + block.__update__([('__truncated_raw__', self._data)]) |
| 1334 | + |
1317 | 1335 | # move backward to the beginning of next block |
1318 | 1336 | self._file.seek(seek_cur, io.SEEK_SET) |
1319 | 1337 |
|
@@ -1413,6 +1431,13 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Opt |
1413 | 1431 | #: collections.Counter: Counter for option types. |
1414 | 1432 | self._opt = collections.Counter() # type: Counter[Enum_OptionType] |
1415 | 1433 |
|
| 1434 | + # NOTE: A block the capture ended inside rebuilds as the octets that |
| 1435 | + # were captured, parsed again as they were the first time (see |
| 1436 | + # :meth:`read`), rather than at the length it declares (:issue:`1470`). |
| 1437 | + block = kwargs.get('block') |
| 1438 | + if file is None and isinstance(block, collections.abc.Mapping) and '__truncated_raw__' in block: |
| 1439 | + file = bytes(block['__truncated_raw__']) |
| 1440 | + |
1416 | 1441 | if file is None: |
1417 | 1442 | _read = False |
1418 | 1443 | #: bytes: Raw packet data. |
@@ -1521,6 +1546,52 @@ def _check_block_floor(self, length: 'Optional[int]') -> 'None': |
1521 | 1546 | raise StreamEOFError(f'PCAP-NG: block truncated: {length} octet(s) left, ' |
1522 | 1547 | 'fewer than the 12 a block needs', quiet=True) |
1523 | 1548 |
|
| 1549 | + def _captured(self, offset: 'int') -> 'int': |
| 1550 | + """Count the octets left in the stream from ``offset`` on. |
| 1551 | +
|
| 1552 | + Args: |
| 1553 | + offset: File offset of the block. |
| 1554 | +
|
| 1555 | + Returns: |
| 1556 | + Octets from ``offset`` to the end of |
| 1557 | + :attr:`self._file <pcapkit.protocols.protocol.Protocol._file>`. |
| 1558 | +
|
| 1559 | + """ |
| 1560 | + current = self._file.tell() |
| 1561 | + end = self._file.seek(0, io.SEEK_END) |
| 1562 | + self._file.seek(current, io.SEEK_SET) |
| 1563 | + return end - offset |
| 1564 | + |
| 1565 | + def _read_block_cut(self, header: 'Schema_PCAPNG', offset: 'int', |
| 1566 | + error: 'BaseError') -> 'Data_UnknownBlock': |
| 1567 | + """Read a PCAP-NG block the capture ended inside, whose parser refused it. |
| 1568 | +
|
| 1569 | + Args: |
| 1570 | + header: Parsed PCAP-NG header schema. |
| 1571 | + offset: File offset of the block. |
| 1572 | + error: What the block's parser raised. |
| 1573 | +
|
| 1574 | + Returns: |
| 1575 | + Parsed packet data, with the octets captured after the Block Total |
| 1576 | + Length as its body. :meth:`read` keeps the block's octets as |
| 1577 | + ``__truncated_raw__`` too, which a rebuild parses again, so that it |
| 1578 | + rebuilds as captured (:issue:`1470`). |
| 1579 | +
|
| 1580 | + """ |
| 1581 | + warn(f'PCAP-NG: [Block {header.type}] block truncated; kept as captured: {error}', |
| 1582 | + ProtocolWarning, stacklevel=stacklevel()) |
| 1583 | + |
| 1584 | + current = self._file.tell() |
| 1585 | + self._file.seek(offset + 8, io.SEEK_SET) |
| 1586 | + body = self._read_fileng(header.block.length - 12) |
| 1587 | + self._file.seek(current, io.SEEK_SET) |
| 1588 | + |
| 1589 | + return Data_UnknownBlock( |
| 1590 | + type=header.type, |
| 1591 | + length=header.block.length, |
| 1592 | + body=body, |
| 1593 | + ) |
| 1594 | + |
1524 | 1595 | def _get_payload(self) -> 'bytes': |
1525 | 1596 | """Get payload of :attr:`self.__header__ <pcapkit.protocols.protocol.Protocol.__header__>`. |
1526 | 1597 |
|
|
0 commit comments