Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions docs/source/pcapkit/corekit/io.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
Seekable I/O Object
===================
I/O Objects
===========

.. module:: pcapkit.corekit.io

:mod:`pcapkit.corekit.io` contains
:class:`~pcapkit.corekit.io.SeekableReader`, a seekable customisation of
:class:`io.BufferedReader`.
:class:`io.BufferedReader`, and two stream proxies:
:class:`~pcapkit.corekit.io.PeekableStream`, which adds ``peek`` to a seekable
stream, and :class:`~pcapkit.corekit.io.NamedStream`, which gives a stream a
``name``.

.. autoclass:: pcapkit.corekit.io.SeekableReader
:no-members:
Expand Down Expand Up @@ -41,3 +44,14 @@ Seekable I/O Object

.. automethod:: fileno
.. automethod:: isatty

.. autoclass:: pcapkit.corekit.io.PeekableStream
:no-members:

.. automethod:: peek

.. autoclass:: pcapkit.corekit.io.NamedStream
:no-members:

.. autoattribute:: name
.. automethod:: read
83 changes: 78 additions & 5 deletions pcapkit/corekit/io.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
"""Seekable I/O Object
=========================
"""I/O Objects
===============

.. module:: pcapkit.corekit.io

:mod:`pcapkit.corekit.io` contains seekable I/O object
:class:`~pcapkit.corekit.io.SeekableReader`, which is a customised
implementation to :class:`io.BufferedReader`.
implementation to :class:`io.BufferedReader`, and the stream proxies
:class:`~pcapkit.corekit.io.PeekableStream`, which adds ``peek`` to a
seekable stream, and :class:`~pcapkit.corekit.io.NamedStream`, which gives
a stream a ``name``.

"""
import io
Expand All @@ -19,11 +22,11 @@

if TYPE_CHECKING:
from io import BytesIO, RawIOBase
from typing import IO, Iterable, Optional
from typing import IO, Any, BinaryIO, Iterable, Optional

from typing_extensions import Buffer

__all__ = ['SeekableReader']
__all__ = ['SeekableReader', 'PeekableStream', 'NamedStream']


class SeekableReader(io.BufferedReader):
Expand Down Expand Up @@ -677,3 +680,73 @@ def peek(self, size: 'int' = 0) -> 'bytes':
self._write_buffer(buf_tmp)
buf += buf_tmp
return buf


class PeekableStream:
"""Give a seekable binary stream without ``peek`` one.

:class:`~pcapkit.foundation.extraction.Extractor` and the PCAP-NG engine
read ahead with ``peek``, which :class:`io.BufferedReader` has and a plain
:class:`typing.IO` such as :class:`io.BytesIO` does not (#1506). The
read-ahead is a read and a seek back, and every other attribute is the
stream's own. It is not an :class:`io.IOBase`, so it has no finaliser that
could close the caller's stream behind their back (#610).

Arguments:
stream: seekable binary stream

"""

def __init__(self, stream: 'IO[bytes]') -> 'None':
self._stream = stream

def peek(self, size: 'int' = 0) -> 'bytes':
"""Return up to ``size`` octets, at least one if any remain, without consuming them.

Arguments:
size: number of octets wanted

Returns:
The octets at the current position.

"""
pos = self._stream.tell()
try:
return self._stream.read(max(size, 1))
finally:
self._stream.seek(pos, io.SEEK_SET)

def __getattr__(self, name: 'str') -> 'Any':
return getattr(self._stream, name)


class NamedStream:
"""Read-only proxy that gives a stream the ``name`` attribute.

:func:`pcapfile.savefile.load_savefile` dereferences ``input_file.name``
unconditionally, on the way into its trace helper. That is fine for the
:class:`~io.BufferedReader` :class:`~pcapkit.foundation.extraction.Extractor`
normally holds, but not for the :class:`~pcapkit.corekit.io.SeekableReader`
it substitutes when the caller supplied a non-seekable stream -- that class
exposes no ``name``, so the load would fail with :exc:`AttributeError` before
a single byte was read.

Args:
stream: Underlying binary stream.
name: Name to report as :attr:`name`.

"""

def __init__(self, stream: 'BinaryIO', name: 'str') -> 'None':
self._stream = stream
#: Name of the underlying stream.
self.name = name

def read(self, size: 'int' = -1) -> 'bytes':
"""Read from the underlying stream.

Args:
size: Number of bytes to read; all remaining bytes if negative.

"""
return self._stream.read(size)
37 changes: 3 additions & 34 deletions pcapkit/foundation/engines/pypcapfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from typing import TYPE_CHECKING, cast

from pcapkit.const.reg.linktype import LinkType as Enum_LinkType
from pcapkit.corekit.io import NamedStream
from pcapkit.foundation.engines.engine import EngineBase
from pcapkit.foundation.reassembly import ReassemblyManager
from pcapkit.utilities.exceptions import FormatError, stacklevel
Expand All @@ -24,45 +25,13 @@
__all__ = ['PyPCAPFile']

if TYPE_CHECKING:
from typing import Any, BinaryIO, Callable, Iterator, Optional
from typing import Any, Callable, Iterator, Optional

from pcapfile.structs import pcap_packet as PCAPFilePacket

from pcapkit.foundation.extraction import Extractor


class _NamedStream:
"""Read-only proxy that gives a stream the ``name`` attribute.

:func:`pcapfile.savefile.load_savefile` dereferences ``input_file.name``
unconditionally, on the way into its trace helper. That is fine for the
:class:`~io.BufferedReader` :class:`~pcapkit.foundation.extraction.Extractor`
normally holds, but not for the :class:`~pcapkit.corekit.io.SeekableReader`
it substitutes when the caller supplied a non-seekable stream -- that class
exposes no ``name``, so the load would fail with :exc:`AttributeError` before
a single byte was read.

Args:
stream: Underlying binary stream.
name: Name to report as :attr:`name`.

"""

def __init__(self, stream: 'BinaryIO', name: 'str') -> 'None':
self._stream = stream
#: Name of the underlying stream.
self.name = name

def read(self, size: 'int' = -1) -> 'bytes':
"""Read from the underlying stream.

Args:
size: Number of bytes to read; all remaining bytes if negative.

"""
return self._stream.read(size)


class PyPCAPFile(EngineBase['PCAPFilePacket']):
"""PyPCAPFile engine support.

Expand Down Expand Up @@ -233,7 +202,7 @@ def run(self) -> 'None':
"so 'ipv6=True' will be ignored", AttributeWarning, stacklevel=stacklevel())

sfile = pcapfile.savefile.load_savefile(
_NamedStream(ext._ifile, ext._ifnm), layers=0, lazy=True,
NamedStream(ext._ifile, ext._ifnm), layers=0, lazy=True,
)
self._dlink = Enum_LinkType.get(sfile.header.ll_type)
self._declf = self._get_decoder(sfile.header.ll_type)
Expand Down
44 changes: 3 additions & 41 deletions pcapkit/foundation/extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from dictdumper.dumper import Dumper

from pcapkit.corekit.context import ContextRegistry
from pcapkit.corekit.io import SeekableReader
from pcapkit.corekit.io import PeekableStream, SeekableReader
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.dumpkit.common import make_dumper
from pcapkit.foundation.engines.engine import Engine, EngineBase
Expand Down Expand Up @@ -187,44 +187,6 @@ def _restore_shipped_dumper(registry: 'dict[str, Any]', format: 'str', # pylint
_PATH_ENGINES = frozenset({'scapy', 'pyshark', 'pypcap', 'pcap_ct'})


class _PeekableStream:
"""Give a seekable binary stream without ``peek`` one.

:class:`Extractor` and the PCAP-NG engine read ahead with ``peek``, which
:class:`io.BufferedReader` has and a plain :class:`typing.IO` such as
:class:`io.BytesIO` does not (#1506). The read-ahead is a read and a seek
back, and every other attribute is the stream's own. It is not an
:class:`io.IOBase`, so it has no finaliser that could close the caller's
stream behind their back (#610).

Arguments:
stream: seekable binary stream

"""

def __init__(self, stream: 'IO[bytes]') -> 'None':
self._stream = stream

def peek(self, size: 'int' = 0) -> 'bytes':
"""Return up to ``size`` octets, at least one if any remain, without consuming them.

Arguments:
size: number of octets wanted

Returns:
The octets at the current position.

"""
pos = self._stream.tell()
try:
return self._stream.read(max(size, 1))
finally:
self._stream.seek(pos, io.SEEK_SET)

def __getattr__(self, name: 'str') -> 'Any':
return getattr(self._stream, name)


class Extractor(Generic[_P]):
"""Extractor for PCAP files.

Expand Down Expand Up @@ -1487,8 +1449,8 @@ def __init__(self,
self._ifile = SeekableReader(self._ifile, buffer_size, buffer_save, buffer_path,
stream_closing=self._flag_s)
elif not hasattr(self._ifile, 'peek'):
logger.debug('input stream has no peek(), wrapping it in _PeekableStream')
self._ifile = cast('BufferedReader', _PeekableStream(self._ifile))
logger.debug('input stream has no peek(), wrapping it in PeekableStream')
self._ifile = cast('BufferedReader', PeekableStream(self._ifile))

if not self._flag_q:
output, ext = self.__output__[fmt]
Expand Down
Loading
Loading