33"""header schema for stream control transmission protocol"""
44
55import io
6- from typing import TYPE_CHECKING , cast
6+ from typing import TYPE_CHECKING
77
88from pcapkit .const .reg .apptype import AppType as Enum_AppType
99from pcapkit .const .reg .apptype import TransportProtocol as Enum_TransportProtocol
5050
5151if TYPE_CHECKING :
5252 from ipaddress import IPv4Address , IPv6Address
53- from typing import IO , Any , Callable , Optional
54-
55- from typing_extensions import Self
53+ from typing import IO , Any , Callable
5654
5755 from pcapkit .protocols .protocol import ProtocolBase
5856
@@ -96,13 +94,15 @@ def padding_length(pkt: 'dict[str, Any]') -> 'int':
9694 pkt: Packet data.
9795
9896 Returns:
99- Number of padding bytes, clamped to the number of bytes left in the
100- enclosing structure, so that a final chunk captured without (all of)
101- its padding still parses, although :rfc:`9260#section-3.2` has the
102- sender pad every chunk. When packing, it is clamped as well
103- to the ``__padding__`` width :func:`pre_pack_padding` hands over, so a
104- final chunk captured without (all of) its padding rebuilds without it
105- (:issue:`1474`).
97+ Number of padding bytes the ``length`` calls for. A final chunk captured
98+ without (all of) its padding, although :rfc:`9260#section-3.2` has the
99+ sender pad every chunk, reads short, which :meth:`Schema.unpack
100+ <pcapkit.protocols.schema.schema.Schema.unpack>` records so that
101+ :meth:`Schema.pack <pcapkit.protocols.schema.schema.Schema.pack>` writes
102+ back only the octets read, and :class:`PaddedOptionField` lets it parse
103+ (:issue:`2680`). When packing, it is clamped to the ``__padding__``
104+ width :func:`pre_pack_padding` hands over, so such a chunk rebuilt from
105+ its data model is short of its padding as well (:issue:`1474`).
106106
107107 """
108108 length = pkt .get ('length' ) or 0
@@ -111,11 +111,7 @@ def padding_length(pkt: 'dict[str, Any]') -> 'int':
111111 width = pkt .get ('__padding__' )
112112 if isinstance (width , int ) and width >= 0 :
113113 padding = min (padding , width )
114-
115- remaining = pkt .get ('__length__' )
116- if not isinstance (remaining , int ) or remaining < 0 :
117- return padding
118- return min (padding , remaining )
114+ return padding
119115
120116
121117def pre_pack_padding (schema : 'Schema' , packet : 'dict[str, Any]' ) -> 'None' :
@@ -195,7 +191,8 @@ def nested_length(base: 'int') -> 'Callable[[dict[str, Any]], int]':
195191 ``__option_padding__``. The last chunk of a packet may omit that padding
196192 altogether, so the chunks wrap this callback in :func:`bounded`: the span
197193 then ends with the data, and the final parameter or cause reads only the
198- padding that is there (:issue:`1479`).
194+ padding that is there (:issue:`1479`), recording the cut
195+ (:issue:`2680`).
199196 * On construction, a separate padding field could not compute its own size:
200197 :meth:`Schema.pack <pcapkit.protocols.schema.schema.Schema.pack>` passes
201198 one shared ``packet`` mapping down into the nested schemas, and each
@@ -213,6 +210,39 @@ def callback(pkt: 'dict[str, Any]') -> 'int':
213210 return callback
214211
215212
213+ class PaddedOptionField (OptionField ):
214+ """List of chunks, parameters or error causes, the last of which may be cut
215+ inside its trailing padding.
216+
217+ :meth:`OptionField.unpack <pcapkit.corekit.fields.collections.OptionField.unpack>`
218+ refuses an item that reads past the end of the data (:issue:`1325`). The
219+ last chunk of a packet may lack (some of) its padding, though, and so may
220+ the last parameter or cause of that chunk, which share it
221+ (:func:`nested_length`). An item's ``length`` leaves its padding out and
222+ the receiver ignores it (:rfc:`9260#section-3.2`), and
223+ :class:`ChunkListField` hands on only chunks whose ``length`` octets are
224+ all present, so an item short of its padding alone is cut rather than
225+ overrun. It records that as a short read of its ``padding``, and is kept
226+ (:issue:`2680`).
227+
228+ """
229+
230+ def _accepts_short_read (self , option : 'Schema' ) -> 'bool' :
231+ """Whether ``option`` read short inside its trailing padding alone.
232+
233+ Args:
234+ option: Item just unpacked, which read short.
235+
236+ Returns:
237+ Whether the short read :meth:`Schema.unpack
238+ <pcapkit.protocols.schema.schema.Schema.unpack>` recorded is of the
239+ item's ``padding``.
240+
241+ """
242+ record = option .__dict__ .get ('__short_read__' )
243+ return record is not None and record [0 ] == 'padding'
244+
245+
216246class ErrorCause (EnumSchema [Enum_CauseCode ]):
217247 """Header schema for SCTP error causes."""
218248
@@ -644,7 +674,7 @@ class INITChunk(Chunk, code=Enum_Chunk.Initiation):
644674 init_tsn : 'int' = UInt32Field ()
645675 #: Optional and variable-length parameters, including the chunk's own
646676 #: trailing padding; see :func:`nested_length`.
647- parameters : 'list[Parameter]' = OptionField (
677+ parameters : 'list[Parameter]' = PaddedOptionField (
648678 length = bounded (nested_length (20 )),
649679 base_schema = Parameter ,
650680 type_name = 'type' ,
@@ -674,7 +704,7 @@ class INITACKChunk(Chunk, code=Enum_Chunk.Initiation_Acknowledgement):
674704 init_tsn : 'int' = UInt32Field ()
675705 #: Optional and variable-length parameters, including the chunk's own
676706 #: trailing padding; see :func:`nested_length`.
677- parameters : 'list[Parameter]' = OptionField (
707+ parameters : 'list[Parameter]' = PaddedOptionField (
678708 length = bounded (nested_length (20 )),
679709 base_schema = Parameter ,
680710 type_name = 'type' ,
@@ -729,7 +759,7 @@ class HeartbeatChunk(Chunk, code=Enum_Chunk.Heartbeat_Request):
729759
730760 #: Heartbeat information parameters, including the chunk's own trailing
731761 #: padding; see :func:`nested_length`.
732- parameters : 'list[Parameter]' = OptionField (
762+ parameters : 'list[Parameter]' = PaddedOptionField (
733763 length = bounded (nested_length (4 )),
734764 base_schema = Parameter ,
735765 type_name = 'type' ,
@@ -747,7 +777,7 @@ class HeartbeatACKChunk(Chunk, code=Enum_Chunk.Heartbeat_Acknowledgement):
747777
748778 #: Heartbeat information parameters, including the chunk's own trailing
749779 #: padding; see :func:`nested_length`.
750- parameters : 'list[Parameter]' = OptionField (
780+ parameters : 'list[Parameter]' = PaddedOptionField (
751781 length = bounded (nested_length (4 )),
752782 base_schema = Parameter ,
753783 type_name = 'type' ,
@@ -770,7 +800,7 @@ class AbortChunk(Chunk, code=Enum_Chunk.Abort):
770800 })
771801 #: Zero or more error causes, including the chunk's own trailing padding;
772802 #: see :func:`nested_length`.
773- error : 'list[ErrorCause]' = OptionField (
803+ error : 'list[ErrorCause]' = PaddedOptionField (
774804 length = bounded (nested_length (4 )),
775805 base_schema = ErrorCause ,
776806 type_name = 'code' ,
@@ -813,7 +843,7 @@ class ErrorChunk(Chunk, code=Enum_Chunk.Operation_Error):
813843
814844 #: One or more error causes, including the chunk's own trailing padding;
815845 #: see :func:`nested_length`.
816- error : 'list[ErrorCause]' = OptionField (
846+ error : 'list[ErrorCause]' = PaddedOptionField (
817847 length = bounded (nested_length (4 )),
818848 base_schema = ErrorCause ,
819849 type_name = 'code' ,
@@ -875,7 +905,7 @@ def whole_chunks_length(data: 'bytes') -> 'int':
875905 Returns:
876906 Number of octets up to the end of the last chunk whose declared
877907 ``length`` octets are all present, with as much of its padding as there
878- is, see :func:`padding_length `. A ``length`` below the four-octet chunk
908+ is, see :class:`PaddedOptionField `. A ``length`` below the four-octet chunk
879909 header (:rfc:`9260#section-3.2`) delimits nothing, so the run stops
880910 there too; four zero octets would otherwise read as a DATA chunk.
881911
@@ -890,17 +920,20 @@ def whole_chunks_length(data: 'bytes') -> 'int':
890920 return end
891921
892922
893- class ChunkListField (OptionField ):
923+ class ChunkListField (PaddedOptionField ):
894924 """Chunk list that stops at the last whole chunk.
895925
896926 SCTP has no packet length field, so octets after the last whole chunk --
897927 fewer than a chunk header, or a chunk whose ``length`` runs past the data --
898928 cannot be told apart from a chunk cut short. They are not parsed but
899929 reported as option padding, which :meth:`Schema.unpack
900930 <pcapkit.protocols.schema.schema.Schema.unpack>` hands back to the stream for
901- :attr:`SCTP.trailer` to keep as captured (:issue:`1468`). The area reaches no
902- further than the octets present, as :meth:`SCTP.unpack` sizes it
903- (:issue:`2673`).
931+ :attr:`SCTP.trailer` to keep as captured (:issue:`1468`). The padding is the
932+ rest of the area declared, as for any
933+ :class:`~pcapkit.corekit.fields.collections.OptionField`, so an area the data
934+ ends inside hands back only the octets present (:issue:`2685`). The last
935+ whole chunk may be cut inside its padding, and is kept as
936+ :class:`PaddedOptionField` keeps one (:issue:`2680`).
904937
905938 """
906939
@@ -928,7 +961,7 @@ def unpack(self, buffer: 'bytes | IO[bytes]', packet: 'dict[str, Any]') -> 'list
928961 chunks = super ().unpack (data [:whole ], packet )
929962 finally :
930963 self ._length = area
931- self ._option_padding += len ( data ) - whole
964+ self ._option_padding += max ( area , 0 ) - whole
932965 return chunks
933966
934967
@@ -967,49 +1000,6 @@ class SCTP(Schema):
9671000 #: and :attr:`IPv4.trailer <pcapkit.protocols.schema.internet.ipv4.IPv4.trailer>`.
9681001 trailer : 'bytes' = PayloadField (default = b'' )
9691002
970- @classmethod
971- def unpack (cls , data : 'bytes | IO[bytes]' , # type: ignore[override]
972- length : 'Optional[int]' = None , packet : 'Optional[dict[str, Any]]' = None ) -> 'Self' :
973- """Unpack :obj:`bytes` into :class:`SCTP`, no further than the octets present.
974-
975- Args:
976- data: Packed data.
977- length: Length of data, as the enclosing layer declares it.
978- packet: Unpacked data.
979-
980- Returns:
981- Unpacked data as :class:`SCTP`.
982-
983- SCTP has no packet length field (:rfc:`9260#section-3`), so a ``length``
984- past the octets present, as an IPv4 Total Length the capture cut short of
985- declares, is cut to them. Sized past them, :attr:`chunks` would record the
986- data as ending inside it while :class:`ChunkListField` hands the octets
987- after the last whole chunk to :attr:`trailer`, so :meth:`Schema.pack
988- <pcapkit.protocols.schema.schema.Schema.pack>` would drop them and
989- ``bytes(schema)`` count them twice (:issue:`2673`).
990-
991- When no octet follows the last whole chunk, which the capture may have cut
992- inside its padding, the data does end inside :attr:`chunks`, so the cut is
993- recorded there as :meth:`Schema.unpack
994- <pcapkit.protocols.schema.schema.Schema.unpack>` records a short read, and
995- packing writes back only the octets read.
996-
997- """
998- cut = False
999- if length is not None :
1000- if isinstance (data , bytes ):
1001- present = len (data )
1002- else :
1003- start = data .tell ()
1004- present = data .seek (0 , io .SEEK_END ) - start
1005- data .seek (start , io .SEEK_SET )
1006- cut , length = length > present , min (length , present )
1007-
1008- schema = cast ('Self' , super ().unpack (data , length , packet )) # type: ignore[call-arg,misc]
1009- if cut and not schema .__buffer__ ['trailer' ] and '__short_read__' not in schema .__dict__ :
1010- schema .__dict__ ['__short_read__' ] = ('chunks' , len (schema .__buffer__ ['chunks' ]))
1011- return schema
1012-
10131003 if TYPE_CHECKING :
10141004 def __init__ (self , srcport : 'Enum_AppType | int' , dstport : 'Enum_AppType | int' ,
10151005 vtag : 'int' , chksum : 'bytes' ,
0 commit comments