From 3cc72d82994f04072d5abe3bf5e0a5b3ffa75efe Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 11:25:43 +0200 Subject: [PATCH 01/46] this is actually a bit tricky.. --- spacepackets/cfdp/pdu/__init__.py | 6 +- spacepackets/cfdp/pdu/ack.py | 14 ++-- spacepackets/cfdp/pdu/eof.py | 4 +- spacepackets/cfdp/pdu/file_directive.py | 104 ++++++++++++++++-------- spacepackets/cfdp/pdu/finished.py | 4 +- spacepackets/cfdp/pdu/header.py | 100 ++++++++++++++--------- spacepackets/cfdp/pdu/keep_alive.py | 4 +- spacepackets/cfdp/pdu/metadata.py | 9 +- spacepackets/cfdp/pdu/nak.py | 12 +-- spacepackets/cfdp/pdu/prompt.py | 17 ++-- spacepackets/cfdp/tlv.py | 24 +++--- tests/test_cfdp.py | 16 ++-- tests/test_cfdp_pdus.py | 14 ++-- 13 files changed, 196 insertions(+), 132 deletions(-) diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 4680e14..84f773f 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -5,7 +5,11 @@ from .header import PduHeader, PduConfig from .ack import TransactionStatus, AckPdu from .eof import EofPdu -from .file_directive import FileDirectivePduBase, DirectiveCodes, IsFileDirective +from .file_directive import ( + FileDirectivePduBase, + DirectiveType, + AbstractFileDirectiveBase, +) from .finished import FinishedPdu from .keep_alive import KeepAlivePdu from .metadata import MetadataPdu diff --git a/spacepackets/cfdp/pdu/ack.py b/spacepackets/cfdp/pdu/ack.py index d04d4d4..a2d4893 100644 --- a/spacepackets/cfdp/pdu/ack.py +++ b/spacepackets/cfdp/pdu/ack.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import PduConfig @@ -20,7 +20,7 @@ class AckPdu: def __init__( self, - directive_code_of_acked_pdu: DirectiveCodes, + directive_code_of_acked_pdu: DirectiveType, condition_code_of_acked_pdu: ConditionCode, transaction_status: TransactionStatus, pdu_conf: PduConfig, @@ -34,18 +34,18 @@ def __init__( :raises ValueError: Directive code invalid. Only EOF and Finished PDUs can be acknowledged """ self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.ACK_PDU, + directive_code=DirectiveType.ACK_PDU, directive_param_field_len=2, pdu_conf=pdu_conf, ) if directive_code_of_acked_pdu not in [ - DirectiveCodes.FINISHED_PDU, - DirectiveCodes.EOF_PDU, + DirectiveType.FINISHED_PDU, + DirectiveType.EOF_PDU, ]: raise ValueError self.directive_code_of_acked_pdu = directive_code_of_acked_pdu self.directive_subtype_code = 0 - if self.directive_code_of_acked_pdu == DirectiveCodes.FINISHED_PDU: + if self.directive_code_of_acked_pdu == DirectiveType.FINISHED_PDU: self.directive_subtype_code = 0b0001 else: self.directive_subtype_code = 0b0000 @@ -61,7 +61,7 @@ def __empty(cls) -> AckPdu: empty_conf = PduConfig.empty() return cls( # Still set valid directive code, otherwise ctor will explode - directive_code_of_acked_pdu=DirectiveCodes.FINISHED_PDU, + directive_code_of_acked_pdu=DirectiveType.FINISHED_PDU, condition_code_of_acked_pdu=ConditionCode.NO_ERROR, transaction_status=TransactionStatus.UNDEFINED, pdu_conf=empty_conf, diff --git a/spacepackets/cfdp/pdu/eof.py b/spacepackets/cfdp/pdu/eof.py index c739f94..1bca90b 100644 --- a/spacepackets/cfdp/pdu/eof.py +++ b/spacepackets/cfdp/pdu/eof.py @@ -2,7 +2,7 @@ import struct from typing import Optional -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.tlv import EntityIdTlv @@ -36,7 +36,7 @@ def __init__( self.file_size = file_size self._fault_location = fault_location self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.EOF_PDU, + directive_code=DirectiveType.EOF_PDU, pdu_conf=pdu_conf, directive_param_field_len=0, ) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 8225dc4..61fe90f 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import abc import enum import struct @@ -6,14 +8,14 @@ PduHeader, PduType, SegmentMetadataFlag, - HasPduHeader, + AbstractPduBase, ) from spacepackets.cfdp.defs import FileSize from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.log import get_console_logger -class DirectiveCodes(enum.IntEnum): +class DirectiveType(enum.IntEnum): EOF_PDU = 0x04 FINISHED_PDU = 0x05 ACK_PDU = 0x06 @@ -24,7 +26,53 @@ class DirectiveCodes(enum.IntEnum): NONE = 0x0A -class FileDirectivePduBase: +class AbstractFileDirectiveBase(AbstractPduBase): + """Encapsulate common functions for classes which are FileDirectives""" + + @property + def pdu_type(self) -> PduType: + return PduType.FILE_DIRECTIVE + + @property + @abc.abstractmethod + def directive_type(self) -> DirectiveType: + pass + + @property + @abc.abstractmethod + def pdu_header(self) -> PduHeader: + pass + + @property + def pdu_data_field_len(self): + return self.pdu_header.pdu_data_field_len + + @property + def source_entity_id(self) -> bytes: + return self.pdu_header.source_entity_id + + @property + def dest_entity_id(self) -> bytes: + return self.pdu_header.dest_entity_id + + @pdu_data_field_len.setter + def pdu_data_field_len(self, pdu_data_field_len: int): + self.pdu_header.pdu_data_field_len = pdu_data_field_len + + @property + def header_len(self) -> int: + """Returns the length of the PDU header plus the directive code octet length""" + return self.pdu_header.header_len + 1 + + @property + def packet_len(self) -> int: + """Get length of the packet when packing it + :return: + """ + return self.pdu_header.pdu_len + + +class FileDirectivePduBase(AbstractFileDirectiveBase): FILE_DIRECTIVE_PDU_LEN = 5 """Base class for file directive PDUs encapsulating all its common components. All other file directive PDU classes implement this class @@ -32,7 +80,7 @@ class FileDirectivePduBase: def __init__( self, - directive_code: DirectiveCodes, + directive_code: DirectiveType, directive_param_field_len: int, pdu_conf: PduConfig, ): @@ -44,22 +92,24 @@ def __init__( the PDU data field will be this length plus the one octet / byte of the directive code :param pdu_conf: Generic PDU transfer configuration """ - self.pdu_header = PduHeader( + + self._pdu_header = PduHeader( pdu_type=PduType.FILE_DIRECTIVE, pdu_data_field_len=directive_param_field_len + 1, pdu_conf=pdu_conf, # This flag is not relevant for file directive PDUs segment_metadata_flag=SegmentMetadataFlag.NOT_PRESENT, ) - self.directive_code = directive_code + super().__init__() + self._directive_code = directive_code @property - def pdu_data_field_len(self): - return self.pdu_header.pdu_data_field_len + def pdu_header(self) -> PduHeader: + return self._pdu_header - @pdu_data_field_len.setter - def pdu_data_field_len(self, pdu_data_field_len: int): - self.pdu_header.pdu_data_field_len = pdu_data_field_len + @property + def directive_type(self) -> DirectiveType: + return self._directive_code @property def directive_param_field_len(self): @@ -76,38 +126,27 @@ def is_large_file(self): def __empty(cls) -> FileDirectivePduBase: empty_conf = PduConfig.empty() return cls( - directive_code=DirectiveCodes.NONE, + directive_code=DirectiveType.NONE, directive_param_field_len=0, pdu_conf=empty_conf, ) - @property - def header_len(self) -> int: - """Returns the length of the PDU header plus the directive code octet length""" - return self.pdu_header.header_len + 1 - - @property - def packet_len(self) -> int: - """Get length of the packet when packing it - :return: - """ - return self.pdu_header.pdu_len - def pack(self) -> bytearray: data = bytearray() data.extend(self.pdu_header.pack()) - data.append(self.directive_code) + data.append(self._directive_code) return data @classmethod def unpack(cls, raw_packet: bytes) -> FileDirectivePduBase: - """Unpack a raw bytearray into the File Directive PDU object representation + """Unpack a raw bytearray into the File Directive PDU object representation. + :param raw_packet: Unpack PDU file directive base :raise ValueError: Passed bytearray is too short :return: """ file_directive = cls.__empty() - file_directive.pdu_header = PduHeader.unpack(raw_packet=raw_packet) + file_directive._pdu_header = PduHeader.unpack(raw_packet=raw_packet) # + 1 because a file directive has the directive code in addition to the PDU header header_len = file_directive.pdu_header.header_len + 1 if not check_packet_length(raw_packet_len=len(raw_packet), min_len=header_len): @@ -133,7 +172,8 @@ def _verify_file_len(self, file_size: int) -> bool: def _parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): """Parse the FSS field, which has different size depending on the large file flag being - set or not. Returns the current index incremented and the parsed file size + set or not. Returns the current index incremented and the parsed file size. + :raise ValueError: Packet not large enough """ if self.pdu_header.pdu_conf.file_size == FileSize.LARGE: @@ -151,11 +191,3 @@ def _parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): ] current_idx += 4 return current_idx, file_size - - -class IsFileDirective(HasPduHeader): - """Encapsulate common functions for classes which are FileDirectives""" - - def __init__(self, pdu_file_directive: FileDirectivePduBase): - self.pdu_file_directive = pdu_file_directive - HasPduHeader.__init__(self, pdu_header=pdu_file_directive.pdu_header) diff --git a/spacepackets/cfdp/pdu/finished.py b/spacepackets/cfdp/pdu/finished.py index b6ac875..4b0f846 100644 --- a/spacepackets/cfdp/pdu/finished.py +++ b/spacepackets/cfdp/pdu/finished.py @@ -2,7 +2,7 @@ import enum from typing import List, Optional -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.cfdp.tlv import TlvTypes, FileStoreResponseTlv, EntityIdTlv @@ -34,7 +34,7 @@ def __init__( fault_location: Optional[EntityIdTlv] = None, ): self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.FINISHED_PDU, + directive_code=DirectiveType.FINISHED_PDU, pdu_conf=pdu_conf, directive_param_field_len=1, ) diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 60d3a12..e4a2098 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -1,5 +1,7 @@ from __future__ import annotations +import abc + from spacepackets.log import get_console_logger from spacepackets.cfdp.defs import ( LenInBytes, @@ -14,12 +16,55 @@ from spacepackets.cfdp.conf import ( PduConfig, get_default_pdu_crc_mode, - get_default_file_size, get_entity_ids, ) -class PduHeader: +class AbstractPduBase(abc.ABC): + """Encapsulate common functions for classes which have a PDU header""" + + @property + @abc.abstractmethod + def pdu_type(self) -> PduType: + pass + + @property + @abc.abstractmethod + def file_size(self) -> FileSize: + pass + + @file_size.setter + @abc.abstractmethod + def file_size(self, file_size: FileSize): + pass + + @property + @abc.abstractmethod + def source_entity_id(self) -> bytes: + pass + + @property + @abc.abstractmethod + def dest_entity_id(self) -> bytes: + pass + + @property + @abc.abstractmethod + def packet_len(self) -> int: + pass + + @property + @abc.abstractmethod + def crc_flag(self): + pass + + @crc_flag.setter + @abc.abstractmethod + def crc_flag(self, crc_flag: CrcFlag): + pass + + +class PduHeader(AbstractPduBase): """This class encapsulates the fixed-format PDU header. For more, information, refer to CCSDS 727.0-B-5 p.75""" @@ -41,7 +86,7 @@ def __init__( :param pdu_conf: :raise ValueError: If some field are invalid or default values were unset """ - self.pdu_type = pdu_type + self._pdu_type = pdu_type self.pdu_conf = pdu_conf self.pdu_data_field_len = pdu_data_field_len @@ -55,6 +100,14 @@ def __init__( self.transaction_seq_num = pdu_conf.transaction_seq_num self.segment_metadata_flag = segment_metadata_flag + @property + def pdu_type(self) -> PduType: + return self._pdu_type + + @pdu_type.setter + def pdu_type(self, pdu_type: PduType): + self._pdu_type = pdu_type + @property def source_entity_id(self): return self.pdu_conf.source_entity_id @@ -178,6 +231,10 @@ def pdu_len(self) -> int: length was already set""" return self.pdu_data_field_len + self.header_len + @property + def packet_len(self) -> int: + return self.pdu_len + def is_large_file(self) -> bool: if self.pdu_conf.file_size == FileSize.LARGE: return True @@ -230,7 +287,7 @@ def unpack(cls, raw_packet: bytes) -> PduHeader: logger.warning("Can not unpack less than four bytes into PDU header") raise ValueError pdu_header = cls.__empty() - pdu_header.pdu_type = (raw_packet[0] & 0x10) >> 4 + pdu_header._pdu_type = (raw_packet[0] & 0x10) >> 4 pdu_header.direction = (raw_packet[0] & 0x08) >> 3 pdu_header.trans_mode = (raw_packet[0] & 0x04) >> 2 pdu_header.crc_flag = (raw_packet[0] & 0x02) >> 1 @@ -282,38 +339,3 @@ def check_len_in_bytes(detected_len: int) -> LenInBytes: ) raise ValueError return len_in_bytes - - -class HasPduHeader: - """Encapsulate common functions for classes which have a PDU header""" - - def __init__(self, pdu_header: PduHeader): - self.pdu_header = pdu_header - - @property - def file_size(self): - return self.pdu_header.file_size - - @file_size.setter - def file_size(self, file_size: FileSize): - self.pdu_header.file_size = file_size - - @property - def source_entity_id(self): - return self.pdu_header.source_entity_id - - @property - def dest_entity_id(self): - return self.pdu_header.dest_entity_id - - @property - def packet_len(self): - return self.pdu_header.pdu_len - - @property - def crc_flag(self): - return self.pdu_header.crc_flag - - @crc_flag.setter - def crc_flag(self, crc_flag: CrcFlag): - self.pdu_header.crc_flag = crc_flag diff --git a/spacepackets/cfdp/pdu/keep_alive.py b/spacepackets/cfdp/pdu/keep_alive.py index 3cfdfb9..c913936 100644 --- a/spacepackets/cfdp/pdu/keep_alive.py +++ b/spacepackets/cfdp/pdu/keep_alive.py @@ -2,7 +2,7 @@ import struct -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.cfdp.conf import PduConfig, FileSize, get_default_file_size from spacepackets.log import get_console_logger @@ -18,7 +18,7 @@ def __init__(self, progress: int, pdu_conf: PduConfig): directive_param_field_len = 8 # Directive param field length is minimum FSS size which is 4 bytes self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.KEEP_ALIVE_PDU, + directive_code=DirectiveType.KEEP_ALIVE_PDU, pdu_conf=pdu_conf, directive_param_field_len=directive_param_field_len, ) diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 14f5840..836fe73 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -1,18 +1,17 @@ from __future__ import annotations import struct -from typing import List, Optional, Type, Union +from typing import List, Optional -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.cfdp.conf import PduConfig, FileSize from spacepackets.cfdp.tlv import CfdpTlv, TlvList from spacepackets.cfdp.lv import CfdpLv from spacepackets.cfdp.defs import ChecksumTypes from spacepackets.cfdp.conf import check_packet_length -from spacepackets.log import get_console_logger class MetadataPdu: - """Encapsulates the Keep Alive file directive PDU, see CCSDS 727.0-B-5 p.83""" + """Encapsulates the Metadata file directive PDU, see CCSDS 727.0-B-5 p.83""" def __init__( self, @@ -42,7 +41,7 @@ def __init__( else: self._options = options self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.METADATA_PDU, + directive_code=DirectiveType.METADATA_PDU, pdu_conf=pdu_conf, directive_param_field_len=5, ) diff --git a/spacepackets/cfdp/pdu/nak.py b/spacepackets/cfdp/pdu/nak.py index 34971e2..ffaba9e 100644 --- a/spacepackets/cfdp/pdu/nak.py +++ b/spacepackets/cfdp/pdu/nak.py @@ -3,16 +3,16 @@ from typing import List, Tuple, Optional from spacepackets.cfdp.pdu.file_directive import ( - IsFileDirective, + AbstractFileDirectiveBase, FileDirectivePduBase, - DirectiveCodes, + DirectiveType, FileSize, ) from spacepackets.cfdp.conf import PduConfig from spacepackets.log import get_console_logger -class NakPdu(IsFileDirective): +class NakPdu(AbstractFileDirectiveBase): """Encapsulates the NAK file directive PDU, see CCSDS 727.0-B-5 p.84""" def __init__( @@ -31,13 +31,15 @@ def __init__( list element is the start offset and the second entry is the end offset """ self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.ACK_PDU, + directive_code=DirectiveType.ACK_PDU, directive_param_field_len=8, pdu_conf=pdu_conf, ) # Calling this will also update the directive parameter field length self.segment_requests = segment_requests - IsFileDirective.__init__(self, pdu_file_directive=self.pdu_file_directive) + AbstractFileDirectiveBase.__init__( + self, pdu_file_directive=self.pdu_file_directive + ) self.start_of_scope = start_of_scope self.end_of_scope = end_of_scope diff --git a/spacepackets/cfdp/pdu/prompt.py b/spacepackets/cfdp/pdu/prompt.py index a9ed5a1..26cb296 100644 --- a/spacepackets/cfdp/pdu/prompt.py +++ b/spacepackets/cfdp/pdu/prompt.py @@ -1,9 +1,9 @@ from __future__ import annotations import enum -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.cfdp.conf import PduConfig -from spacepackets.cfdp.pdu import IsFileDirective +from spacepackets.cfdp.pdu import AbstractFileDirectiveBase, PduHeader from spacepackets.log import get_console_logger @@ -12,18 +12,25 @@ class ResponseRequired(enum.IntEnum): KEEP_ALIVE = 1 -class PromptPdu(IsFileDirective): +class PromptPdu(AbstractFileDirectiveBase): """Encapsulates the Prompt file directive PDU, see CCSDS 727.0-B-5 p.84""" def __init__(self, reponse_required: ResponseRequired, pdu_conf: PduConfig): self.pdu_file_directive = FileDirectivePduBase( - directive_code=DirectiveCodes.PROMPT_PDU, + directive_code=DirectiveType.PROMPT_PDU, pdu_conf=pdu_conf, directive_param_field_len=1, ) - IsFileDirective.__init__(self, pdu_file_directive=self.pdu_file_directive) self.response_required = reponse_required + @property + def directive_type(self) -> DirectiveType: + return self.pdu_file_directive.directive_type + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header + @classmethod def __empty(cls) -> PromptPdu: empty_conf = PduConfig.empty() diff --git a/spacepackets/cfdp/tlv.py b/spacepackets/cfdp/tlv.py index d1c3880..a467655 100644 --- a/spacepackets/cfdp/tlv.py +++ b/spacepackets/cfdp/tlv.py @@ -1,7 +1,7 @@ from __future__ import annotations import abc -from typing import Tuple, Optional, TypeVar, Type, Union, List, Any, cast +from typing import Tuple, Optional, Type, Union, List, Any, cast import enum from spacepackets.log import get_console_logger from spacepackets.cfdp.lv import CfdpLv @@ -147,9 +147,7 @@ def __init__(self, tlv_type: TlvTypes, value: bytes): """ self.length = len(value) if self.length > 255: - logger = get_console_logger() - logger.warning("Length larger than allowed 255 bytes") - raise ValueError + raise ValueError("Length larger than allowed 255 bytes") self.tlv_type = tlv_type self.value = value @@ -169,25 +167,21 @@ def unpack(cls, raw_bytes: bytes) -> CfdpTlv: :return: """ if len(raw_bytes) < 2: - logger = get_console_logger() - logger.warning("Invalid length for TLV field, less than 2") - raise ValueError + raise ValueError("Invalid length for TLV field, less than 2") try: tlv_type = TlvTypes(raw_bytes[0]) except ValueError: - logger = get_console_logger() - logger.warning( + raise ValueError( f"TLV field invalid, found value {raw_bytes[0]} is not a possible TLV parameter" ) - raise ValueError value = bytearray() if len(raw_bytes) > 2: length = raw_bytes[1] if 2 + length > len(raw_bytes): - logger = get_console_logger() - logger.warning(f"Detected TLV length exceeds size of passed bytearray") - raise ValueError + raise ValueError( + f"Detected TLV length exceeds size of passed bytearray" + ) value.extend(raw_bytes[2 : 2 + length]) return cls(tlv_type=tlv_type, value=value) @@ -540,6 +534,10 @@ class TlvWrapper: def __init__(self, tlv_base: Optional[ConcreteTlvBase]): self.base = tlv_base + @property + def tlv_type(self): + return self.base.tlv_type + def __cast_internally( self, obj_type: Type[ConcreteTlvBase], diff --git a/tests/test_cfdp.py b/tests/test_cfdp.py index ddd39ba..1c6c017 100644 --- a/tests/test_cfdp.py +++ b/tests/test_cfdp.py @@ -2,7 +2,7 @@ from unittest import TestCase from spacepackets.cfdp.defs import FileSize -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveCodes +from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.util import get_printable_data_string, PrintFormats from spacepackets.cfdp.pdu.prompt import PromptPdu, ResponseRequired from spacepackets.cfdp.defs import LenInBytes, get_transaction_seq_num_as_bytes @@ -139,12 +139,12 @@ def test_pdu_header(self): ) self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 9) self.assertEqual(prompt_pdu.packet_len, 10) - self.assertEqual(prompt_pdu.crc_flag, CrcFlag.WITH_CRC) - self.assertEqual(prompt_pdu.source_entity_id, bytes([0])) - self.assertEqual(prompt_pdu.dest_entity_id, bytes([0])) - self.assertEqual(prompt_pdu.file_size, FileSize.LARGE) - prompt_pdu.file_size = FileSize.NORMAL - self.assertEqual(prompt_pdu.file_size, FileSize.NORMAL) + self.assertEqual(prompt_pdu.pdu_header.crc_flag, CrcFlag.WITH_CRC) + self.assertEqual(prompt_pdu.pdu_header.source_entity_id, bytes([0])) + self.assertEqual(prompt_pdu.pdu_header.dest_entity_id, bytes([0])) + self.assertEqual(prompt_pdu.pdu_header.file_size, FileSize.LARGE) + prompt_pdu.pdu_header.file_size = FileSize.NORMAL + self.assertEqual(prompt_pdu.pdu_header.file_size, FileSize.NORMAL) self.assertEqual( prompt_pdu.pdu_file_directive.pdu_header.file_size, FileSize.NORMAL ) @@ -219,7 +219,7 @@ def check_fields_case_two(self, pdu_header_packed: bytes): def test_file_directive(self): pdu_conf = PduConfig.empty() file_directive_header = FileDirectivePduBase( - directive_code=DirectiveCodes.METADATA_PDU, + directive_code=DirectiveType.METADATA_PDU, pdu_conf=pdu_conf, directive_param_field_len=0, ) diff --git a/tests/test_cfdp_pdus.py b/tests/test_cfdp_pdus.py index 8f41d88..f10157a 100644 --- a/tests/test_cfdp_pdus.py +++ b/tests/test_cfdp_pdus.py @@ -3,7 +3,7 @@ from spacepackets.cfdp.pdu.ack import ( AckPdu, ConditionCode, - DirectiveCodes, + DirectiveType, TransactionStatus, ) from spacepackets.cfdp.conf import PduConfig, TransmissionModes, Direction, FileSize @@ -37,7 +37,7 @@ def test_ack_pdu(self): file_size=FileSize.GLOBAL_CONFIG, ) ack_pdu = AckPdu( - directive_code_of_acked_pdu=DirectiveCodes.FINISHED_PDU, + directive_code_of_acked_pdu=DirectiveType.FINISHED_PDU, condition_code_of_acked_pdu=ConditionCode.NO_ERROR, transaction_status=TransactionStatus.TERMINATED, pdu_conf=pdu_conf, @@ -81,7 +81,7 @@ def test_ack_pdu(self): file_size=FileSize.NORMAL, ) ack_pdu_2 = AckPdu( - directive_code_of_acked_pdu=DirectiveCodes.EOF_PDU, + directive_code_of_acked_pdu=DirectiveType.EOF_PDU, condition_code_of_acked_pdu=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, transaction_status=TransactionStatus.ACTIVE, pdu_conf=pdu_conf, @@ -125,7 +125,7 @@ def test_ack_pdu(self): self.assertRaises( ValueError, AckPdu, - directive_code_of_acked_pdu=DirectiveCodes.NAK_PDU, + directive_code_of_acked_pdu=DirectiveType.NAK_PDU, condition_code_of_acked_pdu=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, transaction_status=TransactionStatus.ACTIVE, pdu_conf=pdu_conf, @@ -133,7 +133,7 @@ def test_ack_pdu(self): def check_fields_packet_0(self, ack_pdu: AckPdu): self.assertEqual( - ack_pdu.directive_code_of_acked_pdu, DirectiveCodes.FINISHED_PDU + ack_pdu.directive_code_of_acked_pdu, DirectiveType.FINISHED_PDU ) self.assertEqual(ack_pdu.condition_code_of_acked_pdu, ConditionCode.NO_ERROR) self.assertEqual(ack_pdu.transaction_status, TransactionStatus.TERMINATED) @@ -156,7 +156,7 @@ def check_fields_packet_0(self, ack_pdu: AckPdu): self.assertEqual(ack_pdu.packet_len, 13) def check_fields_packet_1(self, ack_pdu: AckPdu): - self.assertEqual(ack_pdu.directive_code_of_acked_pdu, DirectiveCodes.EOF_PDU) + self.assertEqual(ack_pdu.directive_code_of_acked_pdu, DirectiveType.EOF_PDU) self.assertEqual( ack_pdu.condition_code_of_acked_pdu, ConditionCode.POSITIVE_ACK_LIMIT_REACHED, @@ -432,7 +432,7 @@ def test_keep_alive_pdu(self): 0x00, 0x00, 0x00, - DirectiveCodes.KEEP_ALIVE_PDU, + DirectiveType.KEEP_ALIVE_PDU, 0x00, 0x00, 0x00, From ae3586b44e864e2c9a406d5872e6e6070c50b2dd Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 11:29:04 +0200 Subject: [PATCH 02/46] improved abstract base type --- spacepackets/cfdp/pdu/file_directive.py | 26 ++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 61fe90f..eea5e8f 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -10,7 +10,7 @@ SegmentMetadataFlag, AbstractPduBase, ) -from spacepackets.cfdp.defs import FileSize +from spacepackets.cfdp.defs import FileSize, CrcFlag from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.log import get_console_logger @@ -29,10 +29,6 @@ class DirectiveType(enum.IntEnum): class AbstractFileDirectiveBase(AbstractPduBase): """Encapsulate common functions for classes which are FileDirectives""" - @property - def pdu_type(self) -> PduType: - return PduType.FILE_DIRECTIVE - @property @abc.abstractmethod def directive_type(self) -> DirectiveType: @@ -43,6 +39,26 @@ def directive_type(self) -> DirectiveType: def pdu_header(self) -> PduHeader: pass + @property + def pdu_type(self) -> PduType: + return PduType.FILE_DIRECTIVE + + @property + def file_size(self) -> FileSize: + return self.pdu_header.file_size + + @file_size.setter + def file_size(self, file_size: FileSize): + self.pdu_header.file_size = file_size + + @property + def crc_flag(self): + return self.pdu_header.crc_flag + + @crc_flag.setter + def crc_flag(self, crc_flag: CrcFlag): + self.pdu_header.crc_flag = crc_flag + @property def pdu_data_field_len(self): return self.pdu_header.pdu_data_field_len From 0cd617d80da8aceccb6dd49801b38ca638c9ba5a Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 14:17:45 +0200 Subject: [PATCH 03/46] CFDP: refactored large file flag component --- spacepackets/cfdp/__init__.py | 2 +- spacepackets/cfdp/conf.py | 18 ++-------- spacepackets/cfdp/defs.py | 3 +- spacepackets/cfdp/pdu/eof.py | 23 ++++++++++--- spacepackets/cfdp/pdu/file_directive.py | 33 +++++++++--------- spacepackets/cfdp/pdu/header.py | 24 ++++++------- spacepackets/cfdp/pdu/keep_alive.py | 23 +++++-------- spacepackets/cfdp/pdu/metadata.py | 4 +-- spacepackets/cfdp/pdu/nak.py | 34 +++++++++++-------- tests/test_cfdp.py | 28 ++++++--------- tests/test_cfdp_file_data.py | 4 +-- tests/test_cfdp_pdus.py | 45 ++++++++++++++----------- 12 files changed, 120 insertions(+), 121 deletions(-) diff --git a/spacepackets/cfdp/__init__.py b/spacepackets/cfdp/__init__.py index f235ec6..12e4296 100644 --- a/spacepackets/cfdp/__init__.py +++ b/spacepackets/cfdp/__init__.py @@ -2,7 +2,7 @@ PduType, ChecksumTypes, CrcFlag, - FileSize, + LargeFileFlag, SegmentationControl, SegmentMetadataFlag, TransmissionModes, diff --git a/spacepackets/cfdp/conf.py b/spacepackets/cfdp/conf.py index b0a422c..89a42ea 100644 --- a/spacepackets/cfdp/conf.py +++ b/spacepackets/cfdp/conf.py @@ -4,7 +4,7 @@ from spacepackets.cfdp.defs import ( TransmissionModes, - FileSize, + LargeFileFlag, CrcFlag, Direction, SegmentationControl, @@ -23,7 +23,7 @@ class PduConfig: transaction_seq_num: bytes trans_mode: TransmissionModes - file_size: FileSize = FileSize.GLOBAL_CONFIG + file_flag: LargeFileFlag = LargeFileFlag.NORMAL crc_flag: CrcFlag = CrcFlag.GLOBAL_CONFIG direction: Direction = Direction.TOWARDS_RECEIVER seg_ctrl: SegmentationControl = ( @@ -39,7 +39,7 @@ def empty(cls) -> PduConfig: trans_mode=TransmissionModes.ACKNOWLEDGED, source_entity_id=bytes([0]), dest_entity_id=bytes([0]), - file_size=FileSize.GLOBAL_CONFIG, + file_flag=LargeFileFlag.NORMAL, crc_flag=CrcFlag.GLOBAL_CONFIG, ) @@ -47,21 +47,17 @@ def __post_init__(self): """Ensure that the global configuration is converted to the actual value immediately""" if self.crc_flag == CrcFlag.GLOBAL_CONFIG: self.crc_flag = get_default_pdu_crc_mode() - if self.file_size == FileSize.GLOBAL_CONFIG: - self.file_size = get_default_file_size() class CfdpDict(TypedDict): source_dest_entity_ids: Tuple[bytes, bytes] with_crc: CrcFlag - file_size: FileSize # TODO: Protect dict access with a dedicated lock for thread-safety __CFDP_DICT: CfdpDict = { "source_dest_entity_ids": (bytes(), bytes()), "with_crc": CrcFlag.NO_CRC, - "file_size": FileSize.NORMAL, } @@ -82,14 +78,6 @@ def get_entity_ids() -> Tuple[bytes, bytes]: return __CFDP_DICT["source_dest_entity_ids"] -def set_default_file_size(file_size: FileSize): - __CFDP_DICT["file_size"] = file_size - - -def get_default_file_size() -> FileSize: - return __CFDP_DICT["file_size"] - - def check_packet_length( raw_packet_len: int, min_len: int, warn_on_fail: bool = True ) -> bool: diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index 5a1c7ab..23d39bc 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -91,12 +91,11 @@ def get_transaction_seq_num_as_bytes( # File sizes, determine the field sizes of FSS fields -class FileSize(enum.IntEnum): +class LargeFileFlag(enum.IntEnum): # 32 bit maximum file size and FSS size NORMAL = 0 # 64 bit maximum file size and FSS size LARGE = 1 - GLOBAL_CONFIG = 2 # Checksum types according to the SANA Checksum Types registry diff --git a/spacepackets/cfdp/pdu/eof.py b/spacepackets/cfdp/pdu/eof.py index 1bca90b..c07a2a5 100644 --- a/spacepackets/cfdp/pdu/eof.py +++ b/spacepackets/cfdp/pdu/eof.py @@ -2,14 +2,19 @@ import struct from typing import Optional -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType +from spacepackets.cfdp.pdu import PduHeader +from spacepackets.cfdp.pdu.file_directive import ( + FileDirectivePduBase, + DirectiveType, + AbstractFileDirectiveBase, +) from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.tlv import EntityIdTlv from spacepackets.cfdp.conf import check_packet_length -class EofPdu: +class EofPdu(AbstractFileDirectiveBase): """Encapsulates the EOF file directive PDU, see CCSDS 727.0-B-5 p.79""" def __init__( @@ -33,15 +38,23 @@ def __init__( raise ValueError self.condition_code = condition_code self.file_checksum = file_checksum - self.file_size = file_size - self._fault_location = fault_location self.pdu_file_directive = FileDirectivePduBase( directive_code=DirectiveType.EOF_PDU, pdu_conf=pdu_conf, directive_param_field_len=0, ) + self.file_size = file_size + self._fault_location = fault_location self._calculate_directive_param_field_len() + @property + def directive_type(self) -> DirectiveType: + return self.pdu_file_directive.directive_type + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header + @property def packet_len(self) -> int: return self.pdu_file_directive.packet_len @@ -97,7 +110,7 @@ def unpack(cls, raw_packet: bytearray) -> EofPdu: if not check_packet_length( raw_packet_len=len(raw_packet), min_len=expected_min_len ): - raise ValueError + raise ValueError("Invalid packet length") current_idx = eof_pdu.pdu_file_directive.header_len eof_pdu.condition_code = raw_packet[current_idx] & 0xF0 expected_min_len = current_idx + 5 diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index eea5e8f..62de808 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -10,7 +10,7 @@ SegmentMetadataFlag, AbstractPduBase, ) -from spacepackets.cfdp.defs import FileSize, CrcFlag +from spacepackets.cfdp.defs import LargeFileFlag, CrcFlag from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.log import get_console_logger @@ -44,12 +44,12 @@ def pdu_type(self) -> PduType: return PduType.FILE_DIRECTIVE @property - def file_size(self) -> FileSize: - return self.pdu_header.file_size + def file_flag(self) -> LargeFileFlag: + return self.pdu_header.file_flag - @file_size.setter - def file_size(self, file_size: FileSize): - self.pdu_header.file_size = file_size + @file_flag.setter + def file_flag(self, field_len: LargeFileFlag): + self.pdu_header.file_flag = field_len @property def crc_flag(self): @@ -116,8 +116,7 @@ def __init__( # This flag is not relevant for file directive PDUs segment_metadata_flag=SegmentMetadataFlag.NOT_PRESENT, ) - super().__init__() - self._directive_code = directive_code + self._directive_type = directive_code @property def pdu_header(self) -> PduHeader: @@ -125,7 +124,7 @@ def pdu_header(self) -> PduHeader: @property def directive_type(self) -> DirectiveType: - return self._directive_code + return self._directive_type @property def directive_param_field_len(self): @@ -150,7 +149,7 @@ def __empty(cls) -> FileDirectivePduBase: def pack(self) -> bytearray: data = bytearray() data.extend(self.pdu_header.pack()) - data.append(self._directive_code) + data.append(self._directive_type) return data @classmethod @@ -167,19 +166,21 @@ def unpack(cls, raw_packet: bytes) -> FileDirectivePduBase: header_len = file_directive.pdu_header.header_len + 1 if not check_packet_length(raw_packet_len=len(raw_packet), min_len=header_len): raise ValueError - file_directive.directive_code = raw_packet[header_len - 1] + file_directive._directive_type = raw_packet[header_len - 1] return file_directive def _verify_file_len(self, file_size: int) -> bool: """Can be used by subclasses to verify a given file size""" - if self.pdu_header.pdu_conf.file_size == FileSize.LARGE and file_size > pow( - 2, 64 + if ( + self.pdu_header.pdu_conf.file_flag == LargeFileFlag.LARGE + and file_size > pow(2, 64) ): logger = get_console_logger() logger.warning(f"File size {file_size} larger than 64 bit field") return False - elif self.pdu_header.pdu_conf.file_size == FileSize.NORMAL and file_size > pow( - 2, 32 + elif ( + self.pdu_header.pdu_conf.file_flag == LargeFileFlag.NORMAL + and file_size > pow(2, 32) ): logger = get_console_logger() logger.warning(f"File size {file_size} larger than 32 bit field") @@ -192,7 +193,7 @@ def _parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): :raise ValueError: Packet not large enough """ - if self.pdu_header.pdu_conf.file_size == FileSize.LARGE: + if self.pdu_header.pdu_conf.file_flag == LargeFileFlag.LARGE: if not check_packet_length(len(raw_packet), current_idx + 8): raise ValueError file_size = struct.unpack("!Q", raw_packet[current_idx : current_idx + 8])[ diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index e4a2098..b17d1df 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -5,7 +5,7 @@ from spacepackets.log import get_console_logger from spacepackets.cfdp.defs import ( LenInBytes, - FileSize, + LargeFileFlag, PduType, SegmentMetadataFlag, CrcFlag, @@ -30,12 +30,12 @@ def pdu_type(self) -> PduType: @property @abc.abstractmethod - def file_size(self) -> FileSize: + def file_flag(self) -> LargeFileFlag: pass - @file_size.setter + @file_flag.setter @abc.abstractmethod - def file_size(self, file_size: FileSize): + def file_flag(self, file_flag: LargeFileFlag): pass @property @@ -163,12 +163,12 @@ def transaction_seq_num(self, transaction_seq_num: bytes): self.pdu_conf.transaction_seq_num = transaction_seq_num @property - def file_size(self): - return self.pdu_conf.file_size + def file_flag(self): + return self.pdu_conf.file_flag - @file_size.setter - def file_size(self, file_size: FileSize): - self.pdu_conf.file_size = file_size + @file_flag.setter + def file_flag(self, file_flag: LargeFileFlag): + self.pdu_conf.file_flag = file_flag @property def crc_flag(self): @@ -236,7 +236,7 @@ def packet_len(self) -> int: return self.pdu_len def is_large_file(self) -> bool: - if self.pdu_conf.file_size == FileSize.LARGE: + if self.pdu_conf.file_flag == LargeFileFlag.LARGE: return True else: return False @@ -249,7 +249,7 @@ def pack(self) -> bytearray: | (self.pdu_conf.direction << 3) | (self.pdu_conf.trans_mode << 2) | (self.pdu_conf.crc_flag << 1) - | self.pdu_conf.file_size + | self.pdu_conf.file_flag ) header.append((self.pdu_data_field_len >> 8) & 0xFF) header.append(self.pdu_data_field_len & 0xFF) @@ -291,7 +291,7 @@ def unpack(cls, raw_packet: bytes) -> PduHeader: pdu_header.direction = (raw_packet[0] & 0x08) >> 3 pdu_header.trans_mode = (raw_packet[0] & 0x04) >> 2 pdu_header.crc_flag = (raw_packet[0] & 0x02) >> 1 - pdu_header.file_size = raw_packet[0] & 0x01 + pdu_header.file_flag = LargeFileFlag(raw_packet[0] & 0x01) pdu_header.pdu_data_field_len = raw_packet[1] << 8 | raw_packet[2] pdu_header.segmentation_control = SegmentationControl( (raw_packet[3] & 0x80) >> 7 diff --git a/spacepackets/cfdp/pdu/keep_alive.py b/spacepackets/cfdp/pdu/keep_alive.py index c913936..10539c4 100644 --- a/spacepackets/cfdp/pdu/keep_alive.py +++ b/spacepackets/cfdp/pdu/keep_alive.py @@ -3,7 +3,7 @@ import struct from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType -from spacepackets.cfdp.conf import PduConfig, FileSize, get_default_file_size +from spacepackets.cfdp.conf import PduConfig, LargeFileFlag from spacepackets.log import get_console_logger @@ -11,10 +11,9 @@ class KeepAlivePdu: """Encapsulates the Keep Alive file directive PDU, see CCSDS 727.0-B-5 p.85""" def __init__(self, progress: int, pdu_conf: PduConfig): - directive_param_field_len = 4 - if pdu_conf.file_size == FileSize.NORMAL: + if pdu_conf.file_flag == LargeFileFlag.NORMAL: directive_param_field_len = 4 - elif pdu_conf.file_size == FileSize.LARGE: + elif pdu_conf.file_flag == LargeFileFlag.LARGE: directive_param_field_len = 8 # Directive param field length is minimum FSS size which is 4 bytes self.pdu_file_directive = FileDirectivePduBase( @@ -25,19 +24,15 @@ def __init__(self, progress: int, pdu_conf: PduConfig): self.progress = progress @property - def file_size(self): - return self.pdu_file_directive.pdu_header.file_size + def file_flag(self): + return self.pdu_file_directive.pdu_header.file_flag - @file_size.setter - def file_size(self, file_size: FileSize): - if file_size == FileSize.GLOBAL_CONFIG: - file_size = get_default_file_size() + @file_flag.setter + def file_flag(self, file_size: LargeFileFlag): directive_param_field_len = 4 - if file_size == FileSize.NORMAL: - directive_param_field_len = 4 - elif file_size == FileSize.LARGE: + if file_size == LargeFileFlag.LARGE: directive_param_field_len = 8 - self.pdu_file_directive.pdu_header.file_size = file_size + self.pdu_file_directive.pdu_header.file_flag = file_size self.pdu_file_directive.directive_param_field_len = directive_param_field_len @classmethod diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 836fe73..250f623 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -3,7 +3,7 @@ from typing import List, Optional from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType -from spacepackets.cfdp.conf import PduConfig, FileSize +from spacepackets.cfdp.conf import PduConfig, LargeFileFlag from spacepackets.cfdp.tlv import CfdpTlv, TlvList from spacepackets.cfdp.lv import CfdpLv from spacepackets.cfdp.defs import ChecksumTypes @@ -76,7 +76,7 @@ def directive_param_field_len(self): def _calculate_directive_field_len(self): directive_param_field_len = 5 - if self.pdu_file_directive.pdu_header.is_large_file() == FileSize.LARGE: + if self.pdu_file_directive.pdu_header.is_large_file() == LargeFileFlag.LARGE: directive_param_field_len = 9 directive_param_field_len += self._source_file_name_lv.packet_len directive_param_field_len += self._dest_file_name_lv.packet_len diff --git a/spacepackets/cfdp/pdu/nak.py b/spacepackets/cfdp/pdu/nak.py index ffaba9e..ef7e5e7 100644 --- a/spacepackets/cfdp/pdu/nak.py +++ b/spacepackets/cfdp/pdu/nak.py @@ -2,11 +2,12 @@ import struct from typing import List, Tuple, Optional +from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( AbstractFileDirectiveBase, FileDirectivePduBase, DirectiveType, - FileSize, + LargeFileFlag, ) from spacepackets.cfdp.conf import PduConfig from spacepackets.log import get_console_logger @@ -37,9 +38,6 @@ def __init__( ) # Calling this will also update the directive parameter field length self.segment_requests = segment_requests - AbstractFileDirectiveBase.__init__( - self, pdu_file_directive=self.pdu_file_directive - ) self.start_of_scope = start_of_scope self.end_of_scope = end_of_scope @@ -51,20 +49,28 @@ def __empty(cls) -> NakPdu: ) @property - def file_size(self): - return self.pdu_file_directive.pdu_header.file_size + def directive_type(self) -> DirectiveType: + return DirectiveType.NAK_PDU + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header - @file_size.setter - def file_size(self, file_size: FileSize): + @property + def file_flag(self): + return self.pdu_file_directive.file_flag + + @file_flag.setter + def file_flag(self, file_flag: LargeFileFlag): """Set the file size. This changes the length of the packet when packed as well which is handled by this function""" - self.pdu_file_directive.pdu_header.file_size = file_size - # GLOBAL_CONFIG might get converted to file size, so the value is updated here - updated_file_size = self.pdu_file_directive.pdu_header.file_size - if updated_file_size == FileSize.LARGE: + self.pdu_file_directive.file_flag = file_flag + if file_flag == LargeFileFlag.NORMAL: + directive_param_field_len = 8 + len(self._segment_requests) * 8 + elif file_flag == LargeFileFlag.LARGE: directive_param_field_len = 16 + len(self._segment_requests) * 16 else: - directive_param_field_len = 8 + len(self._segment_requests) * 8 + raise ValueError("Invalid large file flag argument") self.pdu_file_directive.directive_param_field_len = directive_param_field_len @property @@ -122,7 +128,7 @@ def unpack(cls, raw_packet: bytes) -> NakPdu: nak_pdu = cls.__empty() nak_pdu.pdu_file_directive = FileDirectivePduBase.unpack(raw_packet=raw_packet) current_idx = nak_pdu.pdu_file_directive.header_len - if not nak_pdu.pdu_file_directive.pdu_header.file_size: + if not nak_pdu.pdu_file_directive.pdu_header.is_large_file(): struct_arg_tuple = ("!I", 4) else: struct_arg_tuple = ("!Q", 8) diff --git a/tests/test_cfdp.py b/tests/test_cfdp.py index 1c6c017..f344583 100644 --- a/tests/test_cfdp.py +++ b/tests/test_cfdp.py @@ -1,7 +1,7 @@ import struct from unittest import TestCase -from spacepackets.cfdp.defs import FileSize +from spacepackets.cfdp.defs import LargeFileFlag from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType from spacepackets.util import get_printable_data_string, PrintFormats from spacepackets.cfdp.pdu.prompt import PromptPdu, ResponseRequired @@ -14,8 +14,6 @@ CrcFlag, SegmentationControl, set_default_pdu_crc_mode, - set_default_file_size, - get_default_file_size, get_default_pdu_crc_mode, set_entity_ids, get_entity_ids, @@ -96,7 +94,7 @@ def test_pdu_header(self): pdu_header.trans_mode = TransmissionModes.UNACKNOWLEDGED pdu_header.direction = Direction.TOWARDS_SENDER pdu_header.crc_flag = CrcFlag.WITH_CRC - pdu_header.file_size = FileSize.LARGE + pdu_header.file_flag = LargeFileFlag.LARGE pdu_header.pdu_data_field_len = 300 pdu_header.seg_ctrl = SegmentationControl.RECORD_BOUNDARIES_PRESERVATION pdu_header.segment_metadata_flag = SegmentMetadataFlag.PRESENT @@ -139,15 +137,13 @@ def test_pdu_header(self): ) self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 9) self.assertEqual(prompt_pdu.packet_len, 10) - self.assertEqual(prompt_pdu.pdu_header.crc_flag, CrcFlag.WITH_CRC) - self.assertEqual(prompt_pdu.pdu_header.source_entity_id, bytes([0])) - self.assertEqual(prompt_pdu.pdu_header.dest_entity_id, bytes([0])) - self.assertEqual(prompt_pdu.pdu_header.file_size, FileSize.LARGE) - prompt_pdu.pdu_header.file_size = FileSize.NORMAL - self.assertEqual(prompt_pdu.pdu_header.file_size, FileSize.NORMAL) - self.assertEqual( - prompt_pdu.pdu_file_directive.pdu_header.file_size, FileSize.NORMAL - ) + self.assertEqual(prompt_pdu.crc_flag, CrcFlag.WITH_CRC) + self.assertEqual(prompt_pdu.source_entity_id, bytes([0])) + self.assertEqual(prompt_pdu.dest_entity_id, bytes([0])) + self.assertEqual(prompt_pdu.file_flag, LargeFileFlag.LARGE) + prompt_pdu.file_flag = LargeFileFlag.NORMAL + self.assertEqual(prompt_pdu.file_flag, LargeFileFlag.NORMAL) + self.assertEqual(prompt_pdu.pdu_file_directive.file_flag, LargeFileFlag.NORMAL) prompt_pdu.crc_flag = CrcFlag.NO_CRC self.assertEqual(prompt_pdu.crc_flag, CrcFlag.NO_CRC) self.assertEqual( @@ -239,7 +235,7 @@ def test_file_directive(self): file_directive_header._parse_fss_field( raw_packet=invalid_fss, current_idx=0 ) - file_directive_header.pdu_header.file_size = FileSize.LARGE + file_directive_header.pdu_header.file_size = LargeFileFlag.LARGE self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 65))) with self.assertRaises(ValueError): file_directive_header._parse_fss_field( @@ -251,9 +247,5 @@ def test_config(self): self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.WITH_CRC) set_default_pdu_crc_mode(CrcFlag.NO_CRC) self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.NO_CRC) - set_default_file_size(FileSize.LARGE) - self.assertEqual(get_default_file_size(), FileSize.LARGE) - set_default_file_size(FileSize.NORMAL) - self.assertEqual(get_default_file_size(), FileSize.NORMAL) set_entity_ids(bytes([0x00, 0x01]), bytes([0x02, 0x03])) self.assertEqual(get_entity_ids(), (bytes([0x00, 0x01]), bytes([0x02, 0x03]))) diff --git a/tests/test_cfdp_file_data.py b/tests/test_cfdp_file_data.py index 200e4ab..0301af3 100644 --- a/tests/test_cfdp_file_data.py +++ b/tests/test_cfdp_file_data.py @@ -4,7 +4,7 @@ SegmentMetadataFlag, RecordContinuationState, ) -from spacepackets.cfdp.conf import PduConfig, FileSize +from spacepackets.cfdp.conf import PduConfig, LargeFileFlag class TestFileDataPdu(TestCase): @@ -67,7 +67,7 @@ def test_file_data_pdu(self): ) invalid_pdu.pack() - pdu_conf.file_size = FileSize.LARGE + pdu_conf.file_flag = LargeFileFlag.LARGE fd_pdu_large_offset = FileDataPdu( pdu_conf=pdu_conf, diff --git a/tests/test_cfdp_pdus.py b/tests/test_cfdp_pdus.py index f10157a..04ba381 100644 --- a/tests/test_cfdp_pdus.py +++ b/tests/test_cfdp_pdus.py @@ -6,7 +6,12 @@ DirectiveType, TransactionStatus, ) -from spacepackets.cfdp.conf import PduConfig, TransmissionModes, Direction, FileSize +from spacepackets.cfdp.conf import ( + PduConfig, + TransmissionModes, + Direction, + LargeFileFlag, +) from spacepackets.cfdp.pdu.nak import NakPdu from spacepackets.cfdp.pdu.finished import FinishedPdu, DeliveryCode, FileDeliveryStatus from spacepackets.cfdp.tlv import ( @@ -34,7 +39,7 @@ def test_ack_pdu(self): dest_entity_id=bytes([0x00, 0x01]), crc_flag=CrcFlag.NO_CRC, trans_mode=TransmissionModes.ACKNOWLEDGED, - file_size=FileSize.GLOBAL_CONFIG, + file_flag=LargeFileFlag.NORMAL, ) ack_pdu = AckPdu( directive_code_of_acked_pdu=DirectiveType.FINISHED_PDU, @@ -78,7 +83,7 @@ def test_ack_pdu(self): dest_entity_id=bytes([0x30, 0x00, 0x01, 0x03]), crc_flag=CrcFlag.WITH_CRC, trans_mode=TransmissionModes.UNACKNOWLEDGED, - file_size=FileSize.NORMAL, + file_flag=LargeFileFlag.NORMAL, ) ack_pdu_2 = AckPdu( directive_code_of_acked_pdu=DirectiveType.EOF_PDU, @@ -193,22 +198,22 @@ def test_nak_pdu(self): self.assertEqual(pdu_header.direction, Direction.TOWARDS_RECEIVER) # Start of scope (4) + end of scope (4) + directive code self.assertEqual(pdu_header.pdu_data_field_len, 8 + 1) - self.assertEqual(pdu_header.file_size, FileSize.NORMAL) + self.assertEqual(pdu_header.file_flag, LargeFileFlag.NORMAL) self.assertEqual(pdu_header.trans_mode, TransmissionModes.ACKNOWLEDGED) - self.assertEqual(nak_pdu.file_size, FileSize.NORMAL) + self.assertEqual(nak_pdu.file_flag, LargeFileFlag.NORMAL) self.assertEqual(nak_pdu.packet_len, 19) nak_packed = nak_pdu.pack() self.assertEqual(len(nak_packed), 19) - nak_pdu.file_size = FileSize.LARGE - self.assertEqual(pdu_header.file_size, FileSize.LARGE) - self.assertEqual(nak_pdu.file_size, FileSize.LARGE) + nak_pdu.file_flag = LargeFileFlag.LARGE + self.assertEqual(pdu_header.file_flag, LargeFileFlag.LARGE) + self.assertEqual(nak_pdu.file_flag, LargeFileFlag.LARGE) self.assertEqual(nak_pdu.packet_len, 27) nak_packed = nak_pdu.pack() self.assertEqual(len(nak_packed), 27) - nak_pdu.file_size = FileSize.NORMAL - self.assertEqual(pdu_header.file_size, FileSize.NORMAL) - self.assertEqual(nak_pdu.file_size, FileSize.NORMAL) + nak_pdu.file_flag = LargeFileFlag.NORMAL + self.assertEqual(pdu_header.file_flag, LargeFileFlag.NORMAL) + self.assertEqual(nak_pdu.file_flag, LargeFileFlag.NORMAL) self.assertEqual(nak_pdu.packet_len, 19) nak_packed = nak_pdu.pack() self.assertEqual(len(nak_packed), 19) @@ -229,7 +234,7 @@ def test_nak_pdu(self): nak_unpacked = NakPdu.unpack(raw_packet=nak_packed) self.assertEqual(nak_unpacked.pack(), nak_packed) - nak_pdu.file_size = FileSize.LARGE + nak_pdu.file_flag = LargeFileFlag.LARGE # 2 segment requests with size 16 each plus 16 for start and end of scope self.assertEqual(nak_pdu.pdu_file_directive.pdu_header.header_len, 10) self.assertEqual(nak_pdu.pdu_file_directive.header_len, 11) @@ -246,7 +251,7 @@ def test_nak_pdu(self): nak_packed = nak_pdu.pack() self.assertEqual(len(nak_packed), 59 - 32) - nak_pdu.file_size = FileSize.NORMAL + nak_pdu.file_flag = LargeFileFlag.NORMAL segment_requests = [(pow(2, 32) + 1, 40), (60, 80)] nak_pdu.segment_requests = segment_requests self.assertRaises(ValueError, nak_pdu.pack) @@ -419,7 +424,7 @@ def test_keep_alive_pdu(self): pdu_conf = PduConfig.empty() keep_alive_pdu = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) self.assertEqual(keep_alive_pdu.progress, 0) - self.assertEqual(keep_alive_pdu.file_size, FileSize.NORMAL) + self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) keep_alive_pdu_raw = keep_alive_pdu.pack() self.assertEqual( keep_alive_pdu_raw, @@ -444,19 +449,19 @@ def test_keep_alive_pdu(self): keep_alive_unpacked = KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_raw) self.assertEqual(keep_alive_unpacked.packet_len, 12) self.assertEqual(keep_alive_unpacked.progress, 0) - keep_alive_pdu.file_size = FileSize.LARGE + keep_alive_pdu.file_flag = LargeFileFlag.LARGE self.assertEqual(keep_alive_pdu.packet_len, 16) keep_alive_pdu_large = keep_alive_pdu.pack() self.assertEqual(len(keep_alive_pdu_large), 16) - keep_alive_pdu.file_size = FileSize.GLOBAL_CONFIG - self.assertEqual(keep_alive_pdu.file_size, FileSize.NORMAL) + keep_alive_pdu.file_flag = LargeFileFlag.NORMAL + self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) keep_alive_pdu.progress = pow(2, 32) + 1 with self.assertRaises(ValueError): keep_alive_pdu.pack() - pdu_conf.file_size = FileSize.LARGE + pdu_conf.fss_field_len = LargeFileFlag.LARGE keep_alive_pdu_large = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) keep_alive_pdu_invalid = keep_alive_pdu_large.pack()[:-1] with self.assertRaises(ValueError): @@ -554,7 +559,7 @@ def test_metadata_pdu(self): with self.assertRaises(ValueError): pdu_with_no_options.pack() - pdu_conf.file_size = FileSize.LARGE + pdu_conf.file_flag = LargeFileFlag.LARGE pdu_file_size_large = MetadataPdu( pdu_conf=pdu_conf, closure_requested=False, @@ -618,7 +623,7 @@ def test_eof_pdu(self): with self.assertRaises(ValueError): EofPdu(file_checksum=bytes([0x00]), file_size=0, pdu_conf=pdu_conf) - pdu_conf.file_size = FileSize.LARGE + pdu_conf.file_flag = LargeFileFlag.LARGE eof_pdu_large_file = EofPdu( file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf ) From bd3640b6640da51599a811b36931482e4e063908 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 15:11:52 +0200 Subject: [PATCH 04/46] wrapper/converter class for PDUs --- spacepackets/cfdp/pdu/__init__.py | 2 +- spacepackets/cfdp/pdu/ack.py | 17 ++++++- spacepackets/cfdp/pdu/finished.py | 17 ++++++- spacepackets/cfdp/pdu/header.py | 4 +- spacepackets/cfdp/pdu/keep_alive.py | 2 +- spacepackets/cfdp/pdu/metadata.py | 17 ++++++- spacepackets/cfdp/pdu/wrapper.py | 78 +++++++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 spacepackets/cfdp/pdu/wrapper.py diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 84f773f..64ba1ae 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -1,5 +1,5 @@ # noinspection PyUnresolvedReferences -from ..defs import SegmentMetadataFlag, PduType +from spacepackets.cfdp.defs import SegmentMetadataFlag, PduType # noinspection PyUnresolvedReferences from .header import PduHeader, PduConfig diff --git a/spacepackets/cfdp/pdu/ack.py b/spacepackets/cfdp/pdu/ack.py index a2d4893..c4958ea 100644 --- a/spacepackets/cfdp/pdu/ack.py +++ b/spacepackets/cfdp/pdu/ack.py @@ -1,7 +1,12 @@ from __future__ import annotations import enum -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType +from spacepackets.cfdp.pdu import PduHeader +from spacepackets.cfdp.pdu.file_directive import ( + FileDirectivePduBase, + DirectiveType, + AbstractFileDirectiveBase, +) from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import PduConfig @@ -15,7 +20,7 @@ class TransactionStatus(enum.IntEnum): UNRECOGNIZED = 0b11 -class AckPdu: +class AckPdu(AbstractFileDirectiveBase): """Encapsulates the ACK file directive PDU, see CCSDS 727.0-B-5 p.81""" def __init__( @@ -52,6 +57,14 @@ def __init__( self.condition_code_of_acked_pdu = condition_code_of_acked_pdu self.transaction_status = transaction_status + @property + def directive_type(self) -> DirectiveType: + return DirectiveType.ACK_PDU + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header + @property def packet_len(self) -> int: return self.pdu_file_directive.packet_len diff --git a/spacepackets/cfdp/pdu/finished.py b/spacepackets/cfdp/pdu/finished.py index 4b0f846..ba3c693 100644 --- a/spacepackets/cfdp/pdu/finished.py +++ b/spacepackets/cfdp/pdu/finished.py @@ -2,7 +2,12 @@ import enum from typing import List, Optional -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType +from spacepackets.cfdp.pdu import PduHeader +from spacepackets.cfdp.pdu.file_directive import ( + FileDirectivePduBase, + DirectiveType, + AbstractFileDirectiveBase, +) from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.cfdp.tlv import TlvTypes, FileStoreResponseTlv, EntityIdTlv @@ -21,7 +26,7 @@ class FileDeliveryStatus(enum.IntEnum): FILE_STATUS_UNREPORTED = 3 -class FinishedPdu: +class FinishedPdu(AbstractFileDirectiveBase): """Encapsulates the Finished file directive PDU, see CCSDS 727.0-B-5 p.80""" def __init__( @@ -47,6 +52,14 @@ def __init__( self.file_store_responses = file_store_responses self.file_delivery_status = file_delivery_status + @property + def directive_type(self) -> DirectiveType: + return DirectiveType.FINISHED_PDU + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header + @property def condition_code(self) -> ConditionCode: return self._condition_code diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index b17d1df..05a0980 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -21,7 +21,9 @@ class AbstractPduBase(abc.ABC): - """Encapsulate common functions for classes which have a PDU header""" + """Encapsulate common functions for PDU. PDU or Packet Data Units are the base data unit + which are exchanged for CFDP procedures. Each PDU has the common header. + """ @property @abc.abstractmethod diff --git a/spacepackets/cfdp/pdu/keep_alive.py b/spacepackets/cfdp/pdu/keep_alive.py index 10539c4..740979d 100644 --- a/spacepackets/cfdp/pdu/keep_alive.py +++ b/spacepackets/cfdp/pdu/keep_alive.py @@ -7,7 +7,7 @@ from spacepackets.log import get_console_logger -class KeepAlivePdu: +class KeepAlivePdu(FileDirectivePduBase): """Encapsulates the Keep Alive file directive PDU, see CCSDS 727.0-B-5 p.85""" def __init__(self, progress: int, pdu_conf: PduConfig): diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 250f623..bda9ba6 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -2,7 +2,12 @@ import struct from typing import List, Optional -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType +from spacepackets.cfdp.pdu import PduHeader +from spacepackets.cfdp.pdu.file_directive import ( + FileDirectivePduBase, + DirectiveType, + AbstractFileDirectiveBase, +) from spacepackets.cfdp.conf import PduConfig, LargeFileFlag from spacepackets.cfdp.tlv import CfdpTlv, TlvList from spacepackets.cfdp.lv import CfdpLv @@ -10,7 +15,7 @@ from spacepackets.cfdp.conf import check_packet_length -class MetadataPdu: +class MetadataPdu(AbstractFileDirectiveBase): """Encapsulates the Metadata file directive PDU, see CCSDS 727.0-B-5 p.83""" def __init__( @@ -47,6 +52,14 @@ def __init__( ) self._calculate_directive_field_len() + @property + def directive_type(self) -> DirectiveType: + return DirectiveType.METADATA_PDU + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header + @classmethod def __empty(cls) -> MetadataPdu: empty_conf = PduConfig.empty() diff --git a/spacepackets/cfdp/pdu/wrapper.py b/spacepackets/cfdp/pdu/wrapper.py new file mode 100644 index 0000000..fe45cdb --- /dev/null +++ b/spacepackets/cfdp/pdu/wrapper.py @@ -0,0 +1,78 @@ +from typing import cast, Union, Type + +from spacepackets.cfdp import PduType +from spacepackets.cfdp.pdu import ( + MetadataPdu, + AbstractFileDirectiveBase, + DirectiveType, + AckPdu, + NakPdu, + FinishedPdu, + EofPdu, + KeepAlivePdu, + PromptPdu, +) +from spacepackets.cfdp.pdu.file_data import FileDataPdu +from spacepackets.cfdp.pdu.header import AbstractPduBase + + +class PduWrapper: + """Helper type to store arbitrary PDU types and cast them to a concrete PDU type conveniently""" + + def __init__(self, base: Union[AbstractFileDirectiveBase, AbstractPduBase]): + self.base = base + + def _raise_not_target_exception(self, pdu_type: Type[any]): + raise TypeError( + f"Stored PDU is not {pdu_type.__class__.__name__}: {self.base!r}" + ) + + def _cast_to_concrete_file_directive( + self, pdu_type: Type[any], dir_type: DirectiveType + ): + if ( + isinstance(self.base, AbstractFileDirectiveBase) + and self.base.pdu_type == PduType.FILE_DIRECTIVE + ): + pdu_base = cast(AbstractFileDirectiveBase, self.base) + if pdu_base.directive_type == dir_type: + return cast(pdu_type, self.base) + self._raise_not_target_exception(pdu_type) + + def to_file_data_pdu(self) -> FileDataPdu: + if ( + isinstance(self.base, AbstractPduBase) + and self.base.pdu_type == PduType.FILE_DATA + ): + return cast(FileDataPdu, self.base) + else: + self._raise_not_target_exception(FileDataPdu) + + def to_metadata_pdu(self) -> MetadataPdu: + return self._cast_to_concrete_file_directive( + MetadataPdu, DirectiveType.METADATA_PDU + ) + + def to_ack_pdu(self) -> AckPdu: + return self._cast_to_concrete_file_directive(AckPdu, DirectiveType.ACK_PDU) + + def to_nak_pdu(self) -> NakPdu: + return self._cast_to_concrete_file_directive(NakPdu, DirectiveType.NAK_PDU) + + def to_finished_pdu(self) -> FinishedPdu: + return self._cast_to_concrete_file_directive( + FinishedPdu, DirectiveType.FINISHED_PDU + ) + + def to_eof_pdu(self) -> EofPdu: + return self._cast_to_concrete_file_directive(EofPdu, DirectiveType.EOF_PDU) + + def to_keep_alive_pdu(self) -> KeepAlivePdu: + return self._cast_to_concrete_file_directive( + KeepAlivePdu, DirectiveType.KEEP_ALIVE_PDU + ) + + def to_prompt_pdu(self) -> PromptPdu: + return self._cast_to_concrete_file_directive( + PromptPdu, DirectiveType.PROMPT_PDU + ) From 8cc2ef18c08895018268a2e48cb15ceacf7b97c5 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 15:32:22 +0200 Subject: [PATCH 05/46] improved test structure --- tests/cfdp/__init__.py | 0 tests/cfdp/pdus/__init__.py | 0 tests/cfdp/pdus/test_ack_pdu.py | 160 +++++ tests/{ => cfdp/pdus}/test_cfdp_file_data.py | 0 tests/cfdp/pdus/test_directive.py | 37 + tests/cfdp/pdus/test_eof_pdu.py | 52 ++ tests/cfdp/pdus/test_finish_pdu.py | 179 +++++ tests/cfdp/pdus/test_keep_alive_pdu.py | 54 ++ tests/cfdp/pdus/test_metadata.py | 133 ++++ tests/cfdp/pdus/test_nak_pdu.py | 79 +++ tests/cfdp/pdus/test_pdu_wrapper.py | 6 + tests/cfdp/pdus/test_prompt_pdu.py | 30 + tests/cfdp/test_cfdp.py | 19 + tests/{test_cfdp.py => cfdp/test_header.py} | 63 +- tests/cfdp/test_lvs.py | 32 + .../test_tlvs.py} | 41 +- tests/test_cfdp_pdus.py | 655 ------------------ 17 files changed, 799 insertions(+), 741 deletions(-) create mode 100644 tests/cfdp/__init__.py create mode 100644 tests/cfdp/pdus/__init__.py create mode 100644 tests/cfdp/pdus/test_ack_pdu.py rename tests/{ => cfdp/pdus}/test_cfdp_file_data.py (100%) create mode 100644 tests/cfdp/pdus/test_directive.py create mode 100644 tests/cfdp/pdus/test_eof_pdu.py create mode 100644 tests/cfdp/pdus/test_finish_pdu.py create mode 100644 tests/cfdp/pdus/test_keep_alive_pdu.py create mode 100644 tests/cfdp/pdus/test_metadata.py create mode 100644 tests/cfdp/pdus/test_nak_pdu.py create mode 100644 tests/cfdp/pdus/test_pdu_wrapper.py create mode 100644 tests/cfdp/pdus/test_prompt_pdu.py create mode 100644 tests/cfdp/test_cfdp.py rename tests/{test_cfdp.py => cfdp/test_header.py} (79%) create mode 100644 tests/cfdp/test_lvs.py rename tests/{test_cfdp_tlvs_lvs.py => cfdp/test_tlvs.py} (87%) delete mode 100644 tests/test_cfdp_pdus.py diff --git a/tests/cfdp/__init__.py b/tests/cfdp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cfdp/pdus/__init__.py b/tests/cfdp/pdus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cfdp/pdus/test_ack_pdu.py b/tests/cfdp/pdus/test_ack_pdu.py new file mode 100644 index 0000000..350431d --- /dev/null +++ b/tests/cfdp/pdus/test_ack_pdu.py @@ -0,0 +1,160 @@ +from unittest import TestCase + +from spacepackets.cfdp import CrcFlag, TransmissionModes, LargeFileFlag, ConditionCode +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import AckPdu, DirectiveType, TransactionStatus + + +class TestAckPdu(TestCase): + def test_ack_pdu(self): + pdu_conf = PduConfig( + transaction_seq_num=bytes([0x00, 0x01]), + source_entity_id=bytes([0x00, 0x00]), + dest_entity_id=bytes([0x00, 0x01]), + crc_flag=CrcFlag.NO_CRC, + trans_mode=TransmissionModes.ACKNOWLEDGED, + file_flag=LargeFileFlag.NORMAL, + ) + ack_pdu = AckPdu( + directive_code_of_acked_pdu=DirectiveType.FINISHED_PDU, + condition_code_of_acked_pdu=ConditionCode.NO_ERROR, + transaction_status=TransactionStatus.TERMINATED, + pdu_conf=pdu_conf, + ) + self.check_fields_packet_0(ack_pdu=ack_pdu) + ack_pdu_raw = ack_pdu.pack() + self.assertEqual(len(ack_pdu_raw), 13) + self.assertEqual( + ack_pdu_raw, + # 0x06 because this is the directive code of ACK PDUs + # 0x51 because 0x05 << 4 is the directive code of the Finished PDU and 0b0001 because + # because this is the value of finished PDUs + # 0x02 because this is the terminated transaction status + bytes( + [ + 0x20, + 0x00, + 0x03, + 0x22, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x01, + 0x06, + 0x51, + 0x02, + ] + ), + ) + ack_pdu_unpacked = AckPdu.unpack(raw_packet=ack_pdu_raw) + self.check_fields_packet_0(ack_pdu=ack_pdu_unpacked) + + pdu_conf = PduConfig( + transaction_seq_num=bytes([0x50, 0x00, 0x10, 0x01]), + source_entity_id=bytes([0x10, 0x00, 0x01, 0x02]), + dest_entity_id=bytes([0x30, 0x00, 0x01, 0x03]), + crc_flag=CrcFlag.WITH_CRC, + trans_mode=TransmissionModes.UNACKNOWLEDGED, + file_flag=LargeFileFlag.NORMAL, + ) + ack_pdu_2 = AckPdu( + directive_code_of_acked_pdu=DirectiveType.EOF_PDU, + condition_code_of_acked_pdu=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, + transaction_status=TransactionStatus.ACTIVE, + pdu_conf=pdu_conf, + ) + self.check_fields_packet_1(ack_pdu=ack_pdu_2) + ack_pdu_raw = ack_pdu_2.pack() + self.assertEqual(len(ack_pdu_raw), 19) + self.assertEqual( + ack_pdu_raw, + # 0x06 because this is the directive code of ACK PDUs + # 0x40 because 0x04 << 4 is the directive code of the EOF PDU and 0b0000 because + # because this is not a Finished PDUs + # 0x11 because this has the Active transaction status and the condition code is the + # Positive Ack Limit Reached Condition Code + bytes( + [ + 0x26, + 0x00, + 0x03, + 0x44, + 0x10, + 0x00, + 0x01, + 0x02, + 0x50, + 0x00, + 0x10, + 0x01, + 0x30, + 0x00, + 0x01, + 0x03, + 0x06, + 0x40, + 0x11, + ] + ), + ) + # Invalid directive code + pdu_conf = PduConfig.empty() + self.assertRaises( + ValueError, + AckPdu, + directive_code_of_acked_pdu=DirectiveType.NAK_PDU, + condition_code_of_acked_pdu=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, + transaction_status=TransactionStatus.ACTIVE, + pdu_conf=pdu_conf, + ) + + def check_fields_packet_0(self, ack_pdu: AckPdu): + self.assertEqual( + ack_pdu.directive_code_of_acked_pdu, DirectiveType.FINISHED_PDU + ) + self.assertEqual(ack_pdu.condition_code_of_acked_pdu, ConditionCode.NO_ERROR) + self.assertEqual(ack_pdu.transaction_status, TransactionStatus.TERMINATED) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.transaction_seq_num, + bytes([0x00, 0x01]), + ) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.source_entity_id, + bytes([0x00, 0x00]), + ) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.dest_entity_id, + bytes([0x00, 0x01]), + ) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.trans_mode, + TransmissionModes.ACKNOWLEDGED, + ) + self.assertEqual(ack_pdu.packet_len, 13) + + def check_fields_packet_1(self, ack_pdu: AckPdu): + self.assertEqual(ack_pdu.directive_code_of_acked_pdu, DirectiveType.EOF_PDU) + self.assertEqual( + ack_pdu.condition_code_of_acked_pdu, + ConditionCode.POSITIVE_ACK_LIMIT_REACHED, + ) + self.assertEqual(ack_pdu.transaction_status, TransactionStatus.ACTIVE) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.transaction_seq_num, + bytes([0x50, 0x00, 0x10, 0x01]), + ) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.source_entity_id, + bytes([0x10, 0x00, 0x01, 0x02]), + ) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.dest_entity_id, + bytes([0x30, 0x00, 0x01, 0x03]), + ) + self.assertEqual( + ack_pdu.pdu_file_directive.pdu_header.pdu_conf.trans_mode, + TransmissionModes.UNACKNOWLEDGED, + ) + self.assertEqual(ack_pdu.packet_len, 19) diff --git a/tests/test_cfdp_file_data.py b/tests/cfdp/pdus/test_cfdp_file_data.py similarity index 100% rename from tests/test_cfdp_file_data.py rename to tests/cfdp/pdus/test_cfdp_file_data.py diff --git a/tests/cfdp/pdus/test_directive.py b/tests/cfdp/pdus/test_directive.py new file mode 100644 index 0000000..8a1fded --- /dev/null +++ b/tests/cfdp/pdus/test_directive.py @@ -0,0 +1,37 @@ +from unittest import TestCase + +from spacepackets.cfdp import LargeFileFlag +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import FileDirectivePduBase, DirectiveType + + +class TestDirective(TestCase): + def test_file_directive(self): + pdu_conf = PduConfig.empty() + file_directive_header = FileDirectivePduBase( + directive_code=DirectiveType.METADATA_PDU, + pdu_conf=pdu_conf, + directive_param_field_len=0, + ) + self.assertEqual(file_directive_header.packet_len, 8) + self.assertEqual(file_directive_header.pdu_data_field_len, 1) + file_directive_header.pdu_data_field_len = 2 + self.assertEqual(file_directive_header.packet_len, 9) + file_directive_header_raw = file_directive_header.pack() + file_directive_header.pdu_data_field_len = 1 + self.assertEqual(len(file_directive_header_raw), 8) + file_directive_header_raw_invalid = file_directive_header_raw[:-1] + with self.assertRaises(ValueError): + FileDirectivePduBase.unpack(raw_packet=file_directive_header_raw_invalid) + self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 33))) + invalid_fss = bytes([0x00, 0x01]) + with self.assertRaises(ValueError): + file_directive_header._parse_fss_field( + raw_packet=invalid_fss, current_idx=0 + ) + file_directive_header.pdu_header.file_size = LargeFileFlag.LARGE + self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 65))) + with self.assertRaises(ValueError): + file_directive_header._parse_fss_field( + raw_packet=invalid_fss, current_idx=0 + ) diff --git a/tests/cfdp/pdus/test_eof_pdu.py b/tests/cfdp/pdus/test_eof_pdu.py new file mode 100644 index 0000000..ceaa981 --- /dev/null +++ b/tests/cfdp/pdus/test_eof_pdu.py @@ -0,0 +1,52 @@ +from unittest import TestCase + +from spacepackets.cfdp import LargeFileFlag, EntityIdTlv +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import EofPdu + + +class TestEofPdu(TestCase): + def test_eof_pdu(self): + pdu_conf = PduConfig.empty() + zero_checksum = bytes([0x00, 0x00, 0x00, 0x00]) + eof_pdu = EofPdu(file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf) + self.assertEqual(eof_pdu.pdu_file_directive.header_len, 8) + expected_packet_len = 8 + 1 + 4 + 4 + self.assertEqual(eof_pdu.packet_len, expected_packet_len) + eof_pdu_raw = eof_pdu.pack() + expected_header = bytearray([0x20, 0x00, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x04]) + expected_header.append(0) + expected_header.extend(zero_checksum) + # File size is 0 as 4 bytes + expected_header.extend(bytes([0x00, 0x00, 0x00, 0x00])) + self.assertEqual(eof_pdu_raw, expected_header) + eof_unpacked = EofPdu.unpack(raw_packet=eof_pdu_raw) + self.assertEqual(eof_unpacked.pack(), eof_pdu_raw) + eof_pdu_raw = eof_pdu_raw[:-2] + with self.assertRaises(ValueError): + EofPdu.unpack(raw_packet=eof_pdu_raw) + + fault_loc_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x01])) + self.assertEqual(fault_loc_tlv.packet_len, 4) + eof_pdu.fault_location = fault_loc_tlv + self.assertEqual(eof_pdu.packet_len, expected_packet_len + 4) + eof_pdu_with_fault_loc = eof_pdu + eof_pdu_with_fault_loc_raw = eof_pdu_with_fault_loc.pack() + self.assertEqual(len(eof_pdu_with_fault_loc_raw), expected_packet_len + 4) + eof_pdu_with_fault_loc_unpacked = EofPdu.unpack( + raw_packet=eof_pdu_with_fault_loc_raw + ) + self.assertEqual( + eof_pdu_with_fault_loc_unpacked.fault_location.pack(), fault_loc_tlv.pack() + ) + + with self.assertRaises(ValueError): + EofPdu(file_checksum=bytes([0x00]), file_size=0, pdu_conf=pdu_conf) + + pdu_conf.file_flag = LargeFileFlag.LARGE + eof_pdu_large_file = EofPdu( + file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf + ) + self.assertEqual(eof_pdu_large_file.packet_len, expected_packet_len + 4) + eof_pdu_large_file_raw = eof_pdu_large_file.pack() + self.assertEqual(len(eof_pdu_large_file_raw), expected_packet_len + 4) diff --git a/tests/cfdp/pdus/test_finish_pdu.py b/tests/cfdp/pdus/test_finish_pdu.py new file mode 100644 index 0000000..6842dbb --- /dev/null +++ b/tests/cfdp/pdus/test_finish_pdu.py @@ -0,0 +1,179 @@ +from unittest import TestCase + +from spacepackets.cfdp import ( + ConditionCode, + EntityIdTlv, + FileStoreResponseTlv, + FilestoreActionCode, + FilestoreResponseStatusCode, + TlvTypes, +) +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import FinishedPdu +from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus + + +class TestFinishPdu(TestCase): + def test_finished_pdu(self): + pdu_conf = PduConfig.empty() + finish_pdu = FinishedPdu( + delivery_code=DeliveryCode.DATA_COMPLETE, + file_delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, + condition_code=ConditionCode.NO_ERROR, + pdu_conf=pdu_conf, + ) + self.assertEqual(finish_pdu.delivery_code, DeliveryCode.DATA_COMPLETE) + self.assertEqual( + finish_pdu.file_delivery_status, FileDeliveryStatus.FILE_STATUS_UNREPORTED + ) + self.assertEqual(finish_pdu.pdu_file_directive.packet_len, 9) + finish_pdu_raw = finish_pdu.pack() + self.assertEqual(len(finish_pdu_raw), 9) + # 0x02 because the only parameters is the 0x05 directive code and the 0x03 from the file + # delivery status + self.assertEqual( + finish_pdu_raw, + bytes([0x20, 0x00, 0x02, 0x11, 0x00, 0x00, 0x00, 0x05, 0x03]), + ) + finish_pdu_unpacked = FinishedPdu.unpack(raw_packet=finish_pdu_raw) + self.assertEqual(finish_pdu_unpacked.delivery_code, DeliveryCode.DATA_COMPLETE) + self.assertEqual( + finish_pdu_unpacked.file_delivery_status, + FileDeliveryStatus.FILE_STATUS_UNREPORTED, + ) + self.assertEqual(finish_pdu_unpacked.pdu_file_directive.packet_len, 9) + finish_pdu_repacked = finish_pdu_unpacked.pack() + self.assertEqual(finish_pdu.pdu_file_directive.packet_len, 9) + self.assertEqual(finish_pdu_repacked, finish_pdu_raw) + finish_pdu_repacked = finish_pdu_repacked[:-1] + self.assertRaises( + ValueError, FinishedPdu.unpack, raw_packet=finish_pdu_repacked + ) + + invalid_fault_source = EntityIdTlv(entity_id=bytes([0x0])) + finish_pdu_raw.extend(invalid_fault_source.pack()) + current_size = finish_pdu_raw[1] << 8 | finish_pdu_raw[2] + current_size += invalid_fault_source.packet_len + finish_pdu_raw[1] = (current_size & 0xFF00) >> 8 + finish_pdu_raw[2] = current_size & 0x00FF + with self.assertRaises(ValueError): + FinishedPdu.unpack(raw_packet=finish_pdu_raw) + + # Now generate a packet with a fault location + fault_location_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x02])) + self.assertEqual(fault_location_tlv.packet_len, 4) + finish_pdu_with_fault_loc = FinishedPdu( + delivery_code=DeliveryCode.DATA_INCOMPLETE, + file_delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, + condition_code=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, + fault_location=fault_location_tlv, + pdu_conf=pdu_conf, + ) + self.assertEqual( + finish_pdu_with_fault_loc.delivery_code, DeliveryCode.DATA_INCOMPLETE + ) + self.assertEqual( + finish_pdu_with_fault_loc.file_delivery_status, + FileDeliveryStatus.DISCARDED_DELIBERATELY, + ) + self.assertEqual( + finish_pdu_with_fault_loc.condition_code, + ConditionCode.POSITIVE_ACK_LIMIT_REACHED, + ) + self.assertEqual(finish_pdu_with_fault_loc.fault_location, fault_location_tlv) + # 4 additional bytes because the entity ID in the TLV has 2 bytes + self.assertEqual(finish_pdu_with_fault_loc.packet_len, 13) + self.assertEqual(len(finish_pdu_with_fault_loc.pack()), 13) + self.assertEqual(finish_pdu_with_fault_loc.fault_location_len, 4) + + # Now create a packet with filestore responses + filestore_reponse_1 = FileStoreResponseTlv( + action_code=FilestoreActionCode.REMOVE_DIR_SNN, + first_file_name="test.txt", + status_code=FilestoreResponseStatusCode.REMOVE_DIR_SUCCESS, + ) + filestore_response_1_packed = filestore_reponse_1.pack() + self.assertEqual( + filestore_response_1_packed, + bytes( + [ + TlvTypes.FILESTORE_RESPONSE, + 11, + 0x60, + 0x08, + 0x74, + 0x65, + 0x73, + 0x74, + 0x2E, + 0x74, + 0x78, + 0x74, + 0x00, + ] + ), + ) + self.assertEqual(filestore_reponse_1.packet_len, 13) + pdu_with_response = FinishedPdu( + delivery_code=DeliveryCode.DATA_INCOMPLETE, + file_delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, + condition_code=ConditionCode.FILESTORE_REJECTION, + pdu_conf=pdu_conf, + file_store_responses=[filestore_reponse_1], + ) + self.assertEqual(pdu_with_response.packet_len, 22) + pdu_with_response_raw = pdu_with_response.pack() + expected_array = bytearray( + [0x20, 0x00, 0x0F, 0x11, 0x00, 0x00, 0x00, 0x05, 0x44] + ) + expected_array.extend(filestore_response_1_packed) + self.assertEqual(expected_array, pdu_with_response_raw) + pdu_with_response_unpacked = FinishedPdu.unpack( + raw_packet=pdu_with_response_raw + ) + self.assertEqual(len(pdu_with_response_unpacked.file_store_responses), 1) + + # Pack with 2 responses and 1 fault location + first_file = "test.txt" + second_file = "test2.txt" + filestore_reponse_2 = FileStoreResponseTlv( + action_code=FilestoreActionCode.APPEND_FILE_SNP, + first_file_name=first_file, + second_file_name=second_file, + status_code=FilestoreResponseStatusCode.APPEND_NOT_PERFORMED, + ) + fs_response_2_raw = filestore_reponse_2.pack() + expected_reply = bytearray() + expected_reply.extend(bytes([0x01, 0x15, 0x3F])) + expected_reply.append(len(first_file)) + expected_reply.extend(first_file.encode()) + expected_reply.append(len(second_file)) + expected_reply.extend(second_file.encode()) + # 0 length filestore message + expected_reply.append(0) + self.assertEqual(filestore_reponse_2.packet_len, 23) + self.assertEqual(fs_response_2_raw, expected_reply) + finish_pdu_two_responses_one_fault_loc = FinishedPdu( + delivery_code=DeliveryCode.DATA_COMPLETE, + file_delivery_status=FileDeliveryStatus.FILE_RETAINED, + condition_code=ConditionCode.CHECK_LIMIT_REACHED, + pdu_conf=pdu_conf, + file_store_responses=[filestore_reponse_1, filestore_reponse_2], + fault_location=fault_location_tlv, + ) + # length should be 13 (response 1) + 23 (response 2) + 4 (fault loc) + 9 (base) + self.assertEqual(finish_pdu_two_responses_one_fault_loc.packet_len, 49) + fs_responses = finish_pdu_two_responses_one_fault_loc.file_store_responses + self.assertEqual(len(fs_responses), 2) + complex_pdu_raw = finish_pdu_two_responses_one_fault_loc.pack() + complex_pdu_unpacked = FinishedPdu.unpack(raw_packet=complex_pdu_raw) + self.assertEqual( + complex_pdu_unpacked.fault_location.pack(), fault_location_tlv.pack() + ) + self.assertEqual(filestore_reponse_1.pack(), fs_responses[0].pack()) + self.assertEqual(filestore_reponse_2.pack(), fs_responses[1].pack()) + + # Change TLV type to make it invalid + complex_pdu_raw[-5] = TlvTypes.FILESTORE_RESPONSE + with self.assertRaises(ValueError): + FinishedPdu.unpack(raw_packet=complex_pdu_raw) diff --git a/tests/cfdp/pdus/test_keep_alive_pdu.py b/tests/cfdp/pdus/test_keep_alive_pdu.py new file mode 100644 index 0000000..05eff2f --- /dev/null +++ b/tests/cfdp/pdus/test_keep_alive_pdu.py @@ -0,0 +1,54 @@ +from unittest import TestCase + +from spacepackets.cfdp import LargeFileFlag +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import KeepAlivePdu, DirectiveType + + +class TestKeepAlivePdu(TestCase): + def test_keep_alive_pdu(self): + pdu_conf = PduConfig.empty() + keep_alive_pdu = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) + self.assertEqual(keep_alive_pdu.progress, 0) + self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) + keep_alive_pdu_raw = keep_alive_pdu.pack() + self.assertEqual( + keep_alive_pdu_raw, + bytes( + [ + 0x20, + 0x00, + 0x05, + 0x11, + 0x00, + 0x00, + 0x00, + DirectiveType.KEEP_ALIVE_PDU, + 0x00, + 0x00, + 0x00, + 0x00, + ] + ), + ) + self.assertEqual(keep_alive_pdu.packet_len, 12) + keep_alive_unpacked = KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_raw) + self.assertEqual(keep_alive_unpacked.packet_len, 12) + self.assertEqual(keep_alive_unpacked.progress, 0) + keep_alive_pdu.file_flag = LargeFileFlag.LARGE + self.assertEqual(keep_alive_pdu.packet_len, 16) + keep_alive_pdu_large = keep_alive_pdu.pack() + self.assertEqual(len(keep_alive_pdu_large), 16) + + keep_alive_pdu.file_flag = LargeFileFlag.NORMAL + self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) + + keep_alive_pdu.progress = pow(2, 32) + 1 + with self.assertRaises(ValueError): + keep_alive_pdu.pack() + + pdu_conf.fss_field_len = LargeFileFlag.LARGE + keep_alive_pdu_large = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) + keep_alive_pdu_invalid = keep_alive_pdu_large.pack()[:-1] + with self.assertRaises(ValueError): + KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_invalid) diff --git a/tests/cfdp/pdus/test_metadata.py b/tests/cfdp/pdus/test_metadata.py new file mode 100644 index 0000000..071d9ff --- /dev/null +++ b/tests/cfdp/pdus/test_metadata.py @@ -0,0 +1,133 @@ +from unittest import TestCase + +from spacepackets.cfdp import ( + ChecksumTypes, + FileStoreRequestTlv, + FilestoreActionCode, + ConditionCode, +) +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.defs import FaultHandlerCodes, LargeFileFlag +from spacepackets.cfdp.pdu import MetadataPdu +from spacepackets.cfdp.tlv import TlvWrapper, FaultHandlerOverrideTlv + + +class TestMetadata(TestCase): + def test_metadata_pdu(self): + pdu_conf = PduConfig.empty() + metadata_pdu = MetadataPdu( + pdu_conf=pdu_conf, + closure_requested=False, + file_size=2, + source_file_name="test.txt", + dest_file_name="test2.txt", + checksum_type=ChecksumTypes.MODULAR, + ) + self.check_metadata_fields_0(metadata_pdu=metadata_pdu) + header_len = metadata_pdu.pdu_file_directive.header_len + self.assertEqual(header_len, 8) + # 5 bytes from FSS with normal size and first eight bits + self.assertEqual(metadata_pdu.packet_len, header_len + 5 + 10 + 9) + metadata_pdu_raw = metadata_pdu.pack() + metadata_pdu_unpacked = MetadataPdu.unpack(raw_packet=metadata_pdu_raw) + self.check_metadata_fields_0(metadata_pdu=metadata_pdu_unpacked) + metadata_pdu_raw = metadata_pdu_raw[: 8 + 6] + self.assertRaises(ValueError, MetadataPdu.unpack, raw_packet=metadata_pdu_raw) + + file_name = "hallo.txt" + option_0 = FileStoreRequestTlv( + action_code=FilestoreActionCode.CREATE_FILE_SNM, first_file_name=file_name + ) + self.assertEqual(option_0.packet_len, 13) + expected_bytes = bytearray() + expected_bytes.extend(bytes([0x00, 0x0B, 0x00, 0x09])) + expected_bytes.extend(file_name.encode()) + self.assertEqual(option_0.pack(), expected_bytes) + + # Create completey new packet + pdu_with_option = MetadataPdu( + pdu_conf=pdu_conf, + closure_requested=False, + file_size=2, + source_file_name="test.txt", + dest_file_name="test2.txt", + checksum_type=ChecksumTypes.MODULAR, + options=[option_0], + ) + self.assertEqual(pdu_with_option.options, [option_0]) + expected_len = 10 + 9 + 8 + 5 + 13 + self.assertEqual(pdu_with_option.packet_len, expected_len) + pdu_with_option_raw = pdu_with_option.pack() + self.assertEqual(len(pdu_with_option_raw), expected_len) + pdu_with_option_unpacked = MetadataPdu.unpack(raw_packet=pdu_with_option_raw) + tlv_wrapper = TlvWrapper(pdu_with_option_unpacked.options[0]) + tlv_typed = tlv_wrapper.to_fs_request() + self.assertIsNotNone(tlv_typed) + self.assertEqual(tlv_typed.pack(), option_0.pack()) + + pdu_with_option.source_file_name = None + pdu_with_option.dest_file_name = None + expected_len = header_len + 1 + 1 + 5 + 13 + self.assertEqual(pdu_with_option.directive_param_field_len, 1 + 1 + 5 + 13) + self.assertEqual(pdu_with_option.packet_len, expected_len) + + option_1 = FaultHandlerOverrideTlv( + condition_code=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, + handler_code=FaultHandlerCodes.ABANDON_TRANSACTION, + ) + self.assertEqual(option_1.packet_len, 3) + pdu_with_two_options = MetadataPdu( + pdu_conf=pdu_conf, + closure_requested=False, + file_size=2, + source_file_name=None, + dest_file_name=None, + checksum_type=ChecksumTypes.MODULAR, + options=[option_0, option_1], + ) + pdu_with_two_options_raw = pdu_with_two_options.pack() + expected_len = header_len + 5 + 2 + option_0.packet_len + option_1.packet_len + self.assertEqual(pdu_with_two_options.packet_len, expected_len) + self.assertEqual(len(pdu_with_two_options_raw), expected_len) + pdu_with_two_options.source_file_name = "hello.txt" + expected_len = ( + header_len + 5 + 1 + 10 + option_0.packet_len + option_1.packet_len + ) + self.assertEqual(pdu_with_two_options.packet_len, expected_len) + pdu_with_two_options.dest_file_name = "hello2.txt" + expected_len = ( + header_len + 5 + 11 + 10 + option_0.packet_len + option_1.packet_len + ) + self.assertEqual(pdu_with_two_options.packet_len, expected_len) + pdu_with_no_options = pdu_with_two_options + pdu_with_no_options.options = None + pdu_with_no_options.file_size = pow(2, 32) + 1 + with self.assertRaises(ValueError): + pdu_with_no_options.pack() + + pdu_conf.file_flag = LargeFileFlag.LARGE + pdu_file_size_large = MetadataPdu( + pdu_conf=pdu_conf, + closure_requested=False, + file_size=2, + source_file_name=None, + dest_file_name=None, + checksum_type=ChecksumTypes.MODULAR, + options=None, + ) + self.assertEqual(pdu_file_size_large.pdu_file_directive.header_len, header_len) + self.assertEqual(pdu_file_size_large.packet_len, header_len + 2 + 9) + pdu_file_size_large.options = [option_0] + pdu_file_size_large_raw = pdu_file_size_large.pack() + pdu_file_size_large_raw = pdu_file_size_large_raw[:-2] + with self.assertRaises(ValueError): + MetadataPdu.unpack(raw_packet=pdu_file_size_large_raw) + + def check_metadata_fields_0(self, metadata_pdu: MetadataPdu): + self.assertEqual(metadata_pdu.closure_requested, False) + self.assertEqual(metadata_pdu.file_size, 2) + self.assertEqual(metadata_pdu.source_file_name, "test.txt") + self.assertEqual(metadata_pdu.dest_file_name, "test2.txt") + self.assertEqual(metadata_pdu.pdu_file_directive.header_len, 8) + self.assertEqual(metadata_pdu._source_file_name_lv.packet_len, 9) + self.assertEqual(metadata_pdu._dest_file_name_lv.packet_len, 10) diff --git a/tests/cfdp/pdus/test_nak_pdu.py b/tests/cfdp/pdus/test_nak_pdu.py new file mode 100644 index 0000000..32c44f7 --- /dev/null +++ b/tests/cfdp/pdus/test_nak_pdu.py @@ -0,0 +1,79 @@ +from unittest import TestCase + +from spacepackets.cfdp import TransmissionModes +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.defs import Direction, LargeFileFlag +from spacepackets.cfdp.pdu import NakPdu + + +class TestNakPdu(TestCase): + def test_nak_pdu(self): + pdu_conf = PduConfig( + trans_mode=TransmissionModes.ACKNOWLEDGED, + transaction_seq_num=bytes([0x00, 0x01]), + source_entity_id=bytes([0x00, 0x00]), + dest_entity_id=bytes([0x00, 0x01]), + ) + nak_pdu = NakPdu(start_of_scope=0, end_of_scope=200, pdu_conf=pdu_conf) + self.assertEqual(nak_pdu.segment_requests, []) + pdu_header = nak_pdu.pdu_file_directive.pdu_header + self.assertEqual(pdu_header.direction, Direction.TOWARDS_RECEIVER) + # Start of scope (4) + end of scope (4) + directive code + self.assertEqual(pdu_header.pdu_data_field_len, 8 + 1) + self.assertEqual(pdu_header.file_flag, LargeFileFlag.NORMAL) + self.assertEqual(pdu_header.trans_mode, TransmissionModes.ACKNOWLEDGED) + self.assertEqual(nak_pdu.file_flag, LargeFileFlag.NORMAL) + self.assertEqual(nak_pdu.packet_len, 19) + nak_packed = nak_pdu.pack() + self.assertEqual(len(nak_packed), 19) + nak_pdu.file_flag = LargeFileFlag.LARGE + self.assertEqual(pdu_header.file_flag, LargeFileFlag.LARGE) + self.assertEqual(nak_pdu.file_flag, LargeFileFlag.LARGE) + self.assertEqual(nak_pdu.packet_len, 27) + nak_packed = nak_pdu.pack() + self.assertEqual(len(nak_packed), 27) + + nak_pdu.file_flag = LargeFileFlag.NORMAL + self.assertEqual(pdu_header.file_flag, LargeFileFlag.NORMAL) + self.assertEqual(nak_pdu.file_flag, LargeFileFlag.NORMAL) + self.assertEqual(nak_pdu.packet_len, 19) + nak_packed = nak_pdu.pack() + self.assertEqual(len(nak_packed), 19) + + nak_pdu.start_of_scope = pow(2, 32) + 1 + nak_pdu.end_of_scope = pow(2, 32) + 1 + self.assertRaises(ValueError, nak_pdu.pack) + + nak_pdu.start_of_scope = 0 + nak_pdu.end_of_scope = 200 + segment_requests = [(20, 40), (60, 80)] + nak_pdu.segment_requests = segment_requests + self.assertEqual(nak_pdu.segment_requests, segment_requests) + # Additional 2 segment requests, each has size 8 + self.assertEqual(nak_pdu.packet_len, 35) + nak_packed = nak_pdu.pack() + self.assertEqual(len(nak_packed), 35) + nak_unpacked = NakPdu.unpack(raw_packet=nak_packed) + self.assertEqual(nak_unpacked.pack(), nak_packed) + + nak_pdu.file_flag = LargeFileFlag.LARGE + # 2 segment requests with size 16 each plus 16 for start and end of scope + self.assertEqual(nak_pdu.pdu_file_directive.pdu_header.header_len, 10) + self.assertEqual(nak_pdu.pdu_file_directive.header_len, 11) + self.assertEqual(nak_pdu.packet_len, 11 + 48) + nak_packed = nak_pdu.pack() + self.assertEqual(len(nak_packed), 59) + nak_repacked = nak_unpacked.pack() + nak_unpacked = NakPdu.unpack(raw_packet=nak_packed) + self.assertEqual(nak_unpacked.pack(), nak_packed) + nak_repacked.append(0) + self.assertRaises(ValueError, NakPdu.unpack, raw_packet=nak_repacked) + nak_pdu.segment_requests = [] + self.assertEqual(nak_pdu.packet_len, 59 - 32) + nak_packed = nak_pdu.pack() + self.assertEqual(len(nak_packed), 59 - 32) + + nak_pdu.file_flag = LargeFileFlag.NORMAL + segment_requests = [(pow(2, 32) + 1, 40), (60, 80)] + nak_pdu.segment_requests = segment_requests + self.assertRaises(ValueError, nak_pdu.pack) diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py new file mode 100644 index 0000000..9746529 --- /dev/null +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -0,0 +1,6 @@ +from unittest import TestCase + + +class TestPduWrapper(TestCase): + def test_wrapper(self): + pass diff --git a/tests/cfdp/pdus/test_prompt_pdu.py b/tests/cfdp/pdus/test_prompt_pdu.py new file mode 100644 index 0000000..472c739 --- /dev/null +++ b/tests/cfdp/pdus/test_prompt_pdu.py @@ -0,0 +1,30 @@ +from unittest import TestCase + +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import PromptPdu +from spacepackets.cfdp.pdu.prompt import ResponseRequired + + +class TestPromptPdu(TestCase): + def test_prompt_pdu(self): + pdu_conf = PduConfig.empty() + prompt_pdu = PromptPdu( + pdu_conf=pdu_conf, reponse_required=ResponseRequired.KEEP_ALIVE + ) + print(prompt_pdu.pack().hex(sep=",")) + prompt_pdu_raw = prompt_pdu.pack() + self.assertEqual( + prompt_pdu_raw, + bytes([0x20, 0x00, 0x02, 0x11, 0x00, 0x00, 0x00, 0x09, 0x80]), + ) + self.assertEqual(prompt_pdu.packet_len, 9) + prompt_pdu_unpacked = PromptPdu.unpack(raw_packet=prompt_pdu_raw) + self.assertEqual(prompt_pdu.pdu_file_directive.pdu_data_field_len, 2) + self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 8) + self.assertEqual( + prompt_pdu_unpacked.response_required, ResponseRequired.KEEP_ALIVE + ) + self.assertEqual(prompt_pdu.pdu_file_directive.is_large_file(), False) + prompt_pdu_raw = prompt_pdu_raw[:-1] + with self.assertRaises(ValueError): + PromptPdu.unpack(raw_packet=prompt_pdu_raw) diff --git a/tests/cfdp/test_cfdp.py b/tests/cfdp/test_cfdp.py new file mode 100644 index 0000000..26a6602 --- /dev/null +++ b/tests/cfdp/test_cfdp.py @@ -0,0 +1,19 @@ +from unittest import TestCase + +from spacepackets.cfdp.conf import ( + CrcFlag, + set_default_pdu_crc_mode, + get_default_pdu_crc_mode, + set_entity_ids, + get_entity_ids, +) + + +class TestConfig(TestCase): + def test_config(self): + set_default_pdu_crc_mode(CrcFlag.WITH_CRC) + self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.WITH_CRC) + set_default_pdu_crc_mode(CrcFlag.NO_CRC) + self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.NO_CRC) + set_entity_ids(bytes([0x00, 0x01]), bytes([0x02, 0x03])) + self.assertEqual(get_entity_ids(), (bytes([0x00, 0x01]), bytes([0x02, 0x03]))) diff --git a/tests/test_cfdp.py b/tests/cfdp/test_header.py similarity index 79% rename from tests/test_cfdp.py rename to tests/cfdp/test_header.py index f344583..d9f0048 100644 --- a/tests/test_cfdp.py +++ b/tests/cfdp/test_header.py @@ -1,26 +1,25 @@ import struct from unittest import TestCase -from spacepackets.cfdp.defs import LargeFileFlag -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType -from spacepackets.util import get_printable_data_string, PrintFormats -from spacepackets.cfdp.pdu.prompt import PromptPdu, ResponseRequired -from spacepackets.cfdp.defs import LenInBytes, get_transaction_seq_num_as_bytes -from spacepackets.cfdp.pdu import PduHeader, PduType, SegmentMetadataFlag -from spacepackets.cfdp.conf import ( - PduConfig, +from spacepackets.cfdp.conf import PduConfig, set_entity_ids +from spacepackets.cfdp.defs import ( + get_transaction_seq_num_as_bytes, + LenInBytes, TransmissionModes, Direction, CrcFlag, SegmentationControl, - set_default_pdu_crc_mode, - get_default_pdu_crc_mode, - set_entity_ids, - get_entity_ids, + PduType, + SegmentMetadataFlag, + LargeFileFlag, ) +from spacepackets.cfdp.pdu import PduHeader, PromptPdu +from spacepackets.cfdp.pdu.prompt import ResponseRequired +from spacepackets.util import get_printable_data_string, PrintFormats -class TestTlvsLvsHeader(TestCase): +class TestHeader(TestCase): + # TODO: Split up in smaller test fixtures def test_pdu_header(self): len_in_bytes = get_transaction_seq_num_as_bytes( transaction_seq_num=22, byte_length=LenInBytes.ONE_BYTE @@ -211,41 +210,3 @@ def check_fields_case_two(self, pdu_header_packed: bytes): self.assertEqual(pdu_header_packed[6] << 8 | pdu_header_packed[7], 300) # Destination ID self.assertEqual(pdu_header_packed[8:10], bytes([0, 1])) - - def test_file_directive(self): - pdu_conf = PduConfig.empty() - file_directive_header = FileDirectivePduBase( - directive_code=DirectiveType.METADATA_PDU, - pdu_conf=pdu_conf, - directive_param_field_len=0, - ) - self.assertEqual(file_directive_header.packet_len, 8) - self.assertEqual(file_directive_header.pdu_data_field_len, 1) - file_directive_header.pdu_data_field_len = 2 - self.assertEqual(file_directive_header.packet_len, 9) - file_directive_header_raw = file_directive_header.pack() - file_directive_header.pdu_data_field_len = 1 - self.assertEqual(len(file_directive_header_raw), 8) - file_directive_header_raw_invalid = file_directive_header_raw[:-1] - with self.assertRaises(ValueError): - FileDirectivePduBase.unpack(raw_packet=file_directive_header_raw_invalid) - self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 33))) - invalid_fss = bytes([0x00, 0x01]) - with self.assertRaises(ValueError): - file_directive_header._parse_fss_field( - raw_packet=invalid_fss, current_idx=0 - ) - file_directive_header.pdu_header.file_size = LargeFileFlag.LARGE - self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 65))) - with self.assertRaises(ValueError): - file_directive_header._parse_fss_field( - raw_packet=invalid_fss, current_idx=0 - ) - - def test_config(self): - set_default_pdu_crc_mode(CrcFlag.WITH_CRC) - self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.WITH_CRC) - set_default_pdu_crc_mode(CrcFlag.NO_CRC) - self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.NO_CRC) - set_entity_ids(bytes([0x00, 0x01]), bytes([0x02, 0x03])) - self.assertEqual(get_entity_ids(), (bytes([0x00, 0x01]), bytes([0x02, 0x03]))) diff --git a/tests/cfdp/test_lvs.py b/tests/cfdp/test_lvs.py new file mode 100644 index 0000000..71d260d --- /dev/null +++ b/tests/cfdp/test_lvs.py @@ -0,0 +1,32 @@ +from unittest import TestCase + +from spacepackets.cfdp.lv import CfdpLv +from spacepackets.cfdp.tlv import CfdpTlv + + +class TestLvs(TestCase): + def test_lvs(self): + test_values = bytes([0, 1, 2]) + test_lv = CfdpLv(value=test_values) + self.assertEqual(test_lv.value, test_values) + self.assertEqual(test_lv.len, 3) + self.assertEqual(test_lv.packet_len, 4) + test_lv_packed = test_lv.pack() + self.assertEqual(len(test_lv_packed), 4) + self.assertEqual(test_lv_packed[0], 3) + self.assertEqual(test_lv_packed[1 : 1 + 3], test_values) + + CfdpLv.unpack(raw_bytes=test_lv_packed) + self.assertEqual(test_lv.value, test_values) + self.assertEqual(test_lv.len, 3) + self.assertEqual(test_lv.packet_len, 4) + + # Too much too pack + faulty_values = bytearray(300) + self.assertRaises(ValueError, CfdpLv, faulty_values) + # Too large to unpack + faulty_values[0] = 20 + self.assertRaises(ValueError, CfdpLv.unpack, faulty_values[0:15]) + # Too short to unpack + faulty_lv = bytes([0]) + self.assertRaises(ValueError, CfdpTlv.unpack, faulty_lv) diff --git a/tests/test_cfdp_tlvs_lvs.py b/tests/cfdp/test_tlvs.py similarity index 87% rename from tests/test_cfdp_tlvs_lvs.py rename to tests/cfdp/test_tlvs.py index 320cf67..1fd40c2 100644 --- a/tests/test_cfdp_tlvs_lvs.py +++ b/tests/cfdp/test_tlvs.py @@ -1,27 +1,24 @@ from unittest import TestCase -from spacepackets.cfdp.lv import CfdpLv +from spacepackets.cfdp import TlvTypes, CfdpTlv, ConditionCode +from spacepackets.cfdp.defs import FaultHandlerCodes from spacepackets.cfdp.tlv import ( - CfdpTlv, - TlvTypes, map_enum_status_code_to_action_status_code, FilestoreResponseStatusCode, FilestoreActionCode, map_int_status_code_to_enum, EntityIdTlv, - FlowLabelTlv, + TlvWrapper, FileStoreRequestTlv, FileStoreResponseTlv, - MessageToUserTlv, FaultHandlerOverrideTlv, - ConditionCode, - FaultHandlerCodes, + MessageToUserTlv, create_cfdp_proxy_and_dir_op_message_marker, - TlvWrapper, + FlowLabelTlv, ) -class TestTlvsLvs(TestCase): +class TestTlvs(TestCase): def test_tlvs(self): test_tlv = CfdpTlv( tlv_type=TlvTypes.FILESTORE_REQUEST, value=bytes([0, 1, 2, 3, 4]) @@ -75,32 +72,6 @@ def test_tlvs(self): ) self.assertEqual(invalid_code, FilestoreResponseStatusCode.INVALID) - def test_lvs(self): - test_values = bytes([0, 1, 2]) - test_lv = CfdpLv(value=test_values) - self.assertEqual(test_lv.value, test_values) - self.assertEqual(test_lv.len, 3) - self.assertEqual(test_lv.packet_len, 4) - test_lv_packed = test_lv.pack() - self.assertEqual(len(test_lv_packed), 4) - self.assertEqual(test_lv_packed[0], 3) - self.assertEqual(test_lv_packed[1 : 1 + 3], test_values) - - CfdpLv.unpack(raw_bytes=test_lv_packed) - self.assertEqual(test_lv.value, test_values) - self.assertEqual(test_lv.len, 3) - self.assertEqual(test_lv.packet_len, 4) - - # Too much too pack - faulty_values = bytearray(300) - self.assertRaises(ValueError, CfdpLv, faulty_values) - # Too large to unpack - faulty_values[0] = 20 - self.assertRaises(ValueError, CfdpLv.unpack, faulty_values[0:15]) - # Too short to unpack - faulty_lv = bytes([0]) - self.assertRaises(ValueError, CfdpTlv.unpack, faulty_lv) - def test_entity_id_tlv(self): entity_id_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x01, 0x02, 0x03])) entity_id_tlv_tlv = entity_id_tlv.tlv diff --git a/tests/test_cfdp_pdus.py b/tests/test_cfdp_pdus.py deleted file mode 100644 index 04ba381..0000000 --- a/tests/test_cfdp_pdus.py +++ /dev/null @@ -1,655 +0,0 @@ -from unittest import TestCase -from spacepackets.cfdp.defs import CrcFlag -from spacepackets.cfdp.pdu.ack import ( - AckPdu, - ConditionCode, - DirectiveType, - TransactionStatus, -) -from spacepackets.cfdp.conf import ( - PduConfig, - TransmissionModes, - Direction, - LargeFileFlag, -) -from spacepackets.cfdp.pdu.nak import NakPdu -from spacepackets.cfdp.pdu.finished import FinishedPdu, DeliveryCode, FileDeliveryStatus -from spacepackets.cfdp.tlv import ( - TlvTypes, - FileStoreResponseTlv, - FilestoreActionCode, - FilestoreResponseStatusCode, - EntityIdTlv, - FaultHandlerOverrideTlv, - FileStoreRequestTlv, - FaultHandlerCodes, - TlvWrapper, -) -from spacepackets.cfdp.pdu.metadata import MetadataPdu, ChecksumTypes -from spacepackets.cfdp.pdu.keep_alive import KeepAlivePdu -from spacepackets.cfdp.pdu.eof import EofPdu -from spacepackets.cfdp.pdu.prompt import PromptPdu, ResponseRequired - - -class TestPdus(TestCase): - def test_ack_pdu(self): - pdu_conf = PduConfig( - transaction_seq_num=bytes([0x00, 0x01]), - source_entity_id=bytes([0x00, 0x00]), - dest_entity_id=bytes([0x00, 0x01]), - crc_flag=CrcFlag.NO_CRC, - trans_mode=TransmissionModes.ACKNOWLEDGED, - file_flag=LargeFileFlag.NORMAL, - ) - ack_pdu = AckPdu( - directive_code_of_acked_pdu=DirectiveType.FINISHED_PDU, - condition_code_of_acked_pdu=ConditionCode.NO_ERROR, - transaction_status=TransactionStatus.TERMINATED, - pdu_conf=pdu_conf, - ) - self.check_fields_packet_0(ack_pdu=ack_pdu) - ack_pdu_raw = ack_pdu.pack() - self.assertEqual(len(ack_pdu_raw), 13) - self.assertEqual( - ack_pdu_raw, - # 0x06 because this is the directive code of ACK PDUs - # 0x51 because 0x05 << 4 is the directive code of the Finished PDU and 0b0001 because - # because this is the value of finished PDUs - # 0x02 because this is the terminated transaction status - bytes( - [ - 0x20, - 0x00, - 0x03, - 0x22, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x01, - 0x06, - 0x51, - 0x02, - ] - ), - ) - ack_pdu_unpacked = AckPdu.unpack(raw_packet=ack_pdu_raw) - self.check_fields_packet_0(ack_pdu=ack_pdu_unpacked) - - pdu_conf = PduConfig( - transaction_seq_num=bytes([0x50, 0x00, 0x10, 0x01]), - source_entity_id=bytes([0x10, 0x00, 0x01, 0x02]), - dest_entity_id=bytes([0x30, 0x00, 0x01, 0x03]), - crc_flag=CrcFlag.WITH_CRC, - trans_mode=TransmissionModes.UNACKNOWLEDGED, - file_flag=LargeFileFlag.NORMAL, - ) - ack_pdu_2 = AckPdu( - directive_code_of_acked_pdu=DirectiveType.EOF_PDU, - condition_code_of_acked_pdu=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - transaction_status=TransactionStatus.ACTIVE, - pdu_conf=pdu_conf, - ) - self.check_fields_packet_1(ack_pdu=ack_pdu_2) - ack_pdu_raw = ack_pdu_2.pack() - self.assertEqual(len(ack_pdu_raw), 19) - self.assertEqual( - ack_pdu_raw, - # 0x06 because this is the directive code of ACK PDUs - # 0x40 because 0x04 << 4 is the directive code of the EOF PDU and 0b0000 because - # because this is not a Finished PDUs - # 0x11 because this has the Active transaction status and the condition code is the - # Positive Ack Limit Reached Condition Code - bytes( - [ - 0x26, - 0x00, - 0x03, - 0x44, - 0x10, - 0x00, - 0x01, - 0x02, - 0x50, - 0x00, - 0x10, - 0x01, - 0x30, - 0x00, - 0x01, - 0x03, - 0x06, - 0x40, - 0x11, - ] - ), - ) - # Invalid directive code - pdu_conf = PduConfig.empty() - self.assertRaises( - ValueError, - AckPdu, - directive_code_of_acked_pdu=DirectiveType.NAK_PDU, - condition_code_of_acked_pdu=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - transaction_status=TransactionStatus.ACTIVE, - pdu_conf=pdu_conf, - ) - - def check_fields_packet_0(self, ack_pdu: AckPdu): - self.assertEqual( - ack_pdu.directive_code_of_acked_pdu, DirectiveType.FINISHED_PDU - ) - self.assertEqual(ack_pdu.condition_code_of_acked_pdu, ConditionCode.NO_ERROR) - self.assertEqual(ack_pdu.transaction_status, TransactionStatus.TERMINATED) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.transaction_seq_num, - bytes([0x00, 0x01]), - ) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.source_entity_id, - bytes([0x00, 0x00]), - ) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.dest_entity_id, - bytes([0x00, 0x01]), - ) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.trans_mode, - TransmissionModes.ACKNOWLEDGED, - ) - self.assertEqual(ack_pdu.packet_len, 13) - - def check_fields_packet_1(self, ack_pdu: AckPdu): - self.assertEqual(ack_pdu.directive_code_of_acked_pdu, DirectiveType.EOF_PDU) - self.assertEqual( - ack_pdu.condition_code_of_acked_pdu, - ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - ) - self.assertEqual(ack_pdu.transaction_status, TransactionStatus.ACTIVE) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.transaction_seq_num, - bytes([0x50, 0x00, 0x10, 0x01]), - ) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.source_entity_id, - bytes([0x10, 0x00, 0x01, 0x02]), - ) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.dest_entity_id, - bytes([0x30, 0x00, 0x01, 0x03]), - ) - self.assertEqual( - ack_pdu.pdu_file_directive.pdu_header.pdu_conf.trans_mode, - TransmissionModes.UNACKNOWLEDGED, - ) - self.assertEqual(ack_pdu.packet_len, 19) - - def test_nak_pdu(self): - pdu_conf = PduConfig( - trans_mode=TransmissionModes.ACKNOWLEDGED, - transaction_seq_num=bytes([0x00, 0x01]), - source_entity_id=bytes([0x00, 0x00]), - dest_entity_id=bytes([0x00, 0x01]), - ) - nak_pdu = NakPdu(start_of_scope=0, end_of_scope=200, pdu_conf=pdu_conf) - self.assertEqual(nak_pdu.segment_requests, []) - pdu_header = nak_pdu.pdu_file_directive.pdu_header - self.assertEqual(pdu_header.direction, Direction.TOWARDS_RECEIVER) - # Start of scope (4) + end of scope (4) + directive code - self.assertEqual(pdu_header.pdu_data_field_len, 8 + 1) - self.assertEqual(pdu_header.file_flag, LargeFileFlag.NORMAL) - self.assertEqual(pdu_header.trans_mode, TransmissionModes.ACKNOWLEDGED) - self.assertEqual(nak_pdu.file_flag, LargeFileFlag.NORMAL) - self.assertEqual(nak_pdu.packet_len, 19) - nak_packed = nak_pdu.pack() - self.assertEqual(len(nak_packed), 19) - nak_pdu.file_flag = LargeFileFlag.LARGE - self.assertEqual(pdu_header.file_flag, LargeFileFlag.LARGE) - self.assertEqual(nak_pdu.file_flag, LargeFileFlag.LARGE) - self.assertEqual(nak_pdu.packet_len, 27) - nak_packed = nak_pdu.pack() - self.assertEqual(len(nak_packed), 27) - - nak_pdu.file_flag = LargeFileFlag.NORMAL - self.assertEqual(pdu_header.file_flag, LargeFileFlag.NORMAL) - self.assertEqual(nak_pdu.file_flag, LargeFileFlag.NORMAL) - self.assertEqual(nak_pdu.packet_len, 19) - nak_packed = nak_pdu.pack() - self.assertEqual(len(nak_packed), 19) - - nak_pdu.start_of_scope = pow(2, 32) + 1 - nak_pdu.end_of_scope = pow(2, 32) + 1 - self.assertRaises(ValueError, nak_pdu.pack) - - nak_pdu.start_of_scope = 0 - nak_pdu.end_of_scope = 200 - segment_requests = [(20, 40), (60, 80)] - nak_pdu.segment_requests = segment_requests - self.assertEqual(nak_pdu.segment_requests, segment_requests) - # Additional 2 segment requests, each has size 8 - self.assertEqual(nak_pdu.packet_len, 35) - nak_packed = nak_pdu.pack() - self.assertEqual(len(nak_packed), 35) - nak_unpacked = NakPdu.unpack(raw_packet=nak_packed) - self.assertEqual(nak_unpacked.pack(), nak_packed) - - nak_pdu.file_flag = LargeFileFlag.LARGE - # 2 segment requests with size 16 each plus 16 for start and end of scope - self.assertEqual(nak_pdu.pdu_file_directive.pdu_header.header_len, 10) - self.assertEqual(nak_pdu.pdu_file_directive.header_len, 11) - self.assertEqual(nak_pdu.packet_len, 11 + 48) - nak_packed = nak_pdu.pack() - self.assertEqual(len(nak_packed), 59) - nak_repacked = nak_unpacked.pack() - nak_unpacked = NakPdu.unpack(raw_packet=nak_packed) - self.assertEqual(nak_unpacked.pack(), nak_packed) - nak_repacked.append(0) - self.assertRaises(ValueError, NakPdu.unpack, raw_packet=nak_repacked) - nak_pdu.segment_requests = [] - self.assertEqual(nak_pdu.packet_len, 59 - 32) - nak_packed = nak_pdu.pack() - self.assertEqual(len(nak_packed), 59 - 32) - - nak_pdu.file_flag = LargeFileFlag.NORMAL - segment_requests = [(pow(2, 32) + 1, 40), (60, 80)] - nak_pdu.segment_requests = segment_requests - self.assertRaises(ValueError, nak_pdu.pack) - - def test_finished_pdu(self): - pdu_conf = PduConfig.empty() - finish_pdu = FinishedPdu( - delivery_code=DeliveryCode.DATA_COMPLETE, - file_delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, - condition_code=ConditionCode.NO_ERROR, - pdu_conf=pdu_conf, - ) - self.assertEqual(finish_pdu.delivery_code, DeliveryCode.DATA_COMPLETE) - self.assertEqual( - finish_pdu.file_delivery_status, FileDeliveryStatus.FILE_STATUS_UNREPORTED - ) - self.assertEqual(finish_pdu.pdu_file_directive.packet_len, 9) - finish_pdu_raw = finish_pdu.pack() - self.assertEqual(len(finish_pdu_raw), 9) - # 0x02 because the only parameters is the 0x05 directive code and the 0x03 from the file - # delivery status - self.assertEqual( - finish_pdu_raw, - bytes([0x20, 0x00, 0x02, 0x11, 0x00, 0x00, 0x00, 0x05, 0x03]), - ) - finish_pdu_unpacked = FinishedPdu.unpack(raw_packet=finish_pdu_raw) - self.assertEqual(finish_pdu_unpacked.delivery_code, DeliveryCode.DATA_COMPLETE) - self.assertEqual( - finish_pdu_unpacked.file_delivery_status, - FileDeliveryStatus.FILE_STATUS_UNREPORTED, - ) - self.assertEqual(finish_pdu_unpacked.pdu_file_directive.packet_len, 9) - finish_pdu_repacked = finish_pdu_unpacked.pack() - self.assertEqual(finish_pdu.pdu_file_directive.packet_len, 9) - self.assertEqual(finish_pdu_repacked, finish_pdu_raw) - finish_pdu_repacked = finish_pdu_repacked[:-1] - self.assertRaises( - ValueError, FinishedPdu.unpack, raw_packet=finish_pdu_repacked - ) - - invalid_fault_source = EntityIdTlv(entity_id=bytes([0x0])) - finish_pdu_raw.extend(invalid_fault_source.pack()) - current_size = finish_pdu_raw[1] << 8 | finish_pdu_raw[2] - current_size += invalid_fault_source.packet_len - finish_pdu_raw[1] = (current_size & 0xFF00) >> 8 - finish_pdu_raw[2] = current_size & 0x00FF - with self.assertRaises(ValueError): - FinishedPdu.unpack(raw_packet=finish_pdu_raw) - - # Now generate a packet with a fault location - fault_location_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x02])) - self.assertEqual(fault_location_tlv.packet_len, 4) - finish_pdu_with_fault_loc = FinishedPdu( - delivery_code=DeliveryCode.DATA_INCOMPLETE, - file_delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, - condition_code=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - fault_location=fault_location_tlv, - pdu_conf=pdu_conf, - ) - self.assertEqual( - finish_pdu_with_fault_loc.delivery_code, DeliveryCode.DATA_INCOMPLETE - ) - self.assertEqual( - finish_pdu_with_fault_loc.file_delivery_status, - FileDeliveryStatus.DISCARDED_DELIBERATELY, - ) - self.assertEqual( - finish_pdu_with_fault_loc.condition_code, - ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - ) - self.assertEqual(finish_pdu_with_fault_loc.fault_location, fault_location_tlv) - # 4 additional bytes because the entity ID in the TLV has 2 bytes - self.assertEqual(finish_pdu_with_fault_loc.packet_len, 13) - self.assertEqual(len(finish_pdu_with_fault_loc.pack()), 13) - self.assertEqual(finish_pdu_with_fault_loc.fault_location_len, 4) - - # Now create a packet with filestore responses - filestore_reponse_1 = FileStoreResponseTlv( - action_code=FilestoreActionCode.REMOVE_DIR_SNN, - first_file_name="test.txt", - status_code=FilestoreResponseStatusCode.REMOVE_DIR_SUCCESS, - ) - filestore_response_1_packed = filestore_reponse_1.pack() - self.assertEqual( - filestore_response_1_packed, - bytes( - [ - TlvTypes.FILESTORE_RESPONSE, - 11, - 0x60, - 0x08, - 0x74, - 0x65, - 0x73, - 0x74, - 0x2E, - 0x74, - 0x78, - 0x74, - 0x00, - ] - ), - ) - self.assertEqual(filestore_reponse_1.packet_len, 13) - pdu_with_response = FinishedPdu( - delivery_code=DeliveryCode.DATA_INCOMPLETE, - file_delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, - condition_code=ConditionCode.FILESTORE_REJECTION, - pdu_conf=pdu_conf, - file_store_responses=[filestore_reponse_1], - ) - self.assertEqual(pdu_with_response.packet_len, 22) - pdu_with_response_raw = pdu_with_response.pack() - expected_array = bytearray( - [0x20, 0x00, 0x0F, 0x11, 0x00, 0x00, 0x00, 0x05, 0x44] - ) - expected_array.extend(filestore_response_1_packed) - self.assertEqual(expected_array, pdu_with_response_raw) - pdu_with_response_unpacked = FinishedPdu.unpack( - raw_packet=pdu_with_response_raw - ) - self.assertEqual(len(pdu_with_response_unpacked.file_store_responses), 1) - - # Pack with 2 responses and 1 fault location - first_file = "test.txt" - second_file = "test2.txt" - filestore_reponse_2 = FileStoreResponseTlv( - action_code=FilestoreActionCode.APPEND_FILE_SNP, - first_file_name=first_file, - second_file_name=second_file, - status_code=FilestoreResponseStatusCode.APPEND_NOT_PERFORMED, - ) - fs_response_2_raw = filestore_reponse_2.pack() - expected_reply = bytearray() - expected_reply.extend(bytes([0x01, 0x15, 0x3F])) - expected_reply.append(len(first_file)) - expected_reply.extend(first_file.encode()) - expected_reply.append(len(second_file)) - expected_reply.extend(second_file.encode()) - # 0 length filestore message - expected_reply.append(0) - self.assertEqual(filestore_reponse_2.packet_len, 23) - self.assertEqual(fs_response_2_raw, expected_reply) - finish_pdu_two_responses_one_fault_loc = FinishedPdu( - delivery_code=DeliveryCode.DATA_COMPLETE, - file_delivery_status=FileDeliveryStatus.FILE_RETAINED, - condition_code=ConditionCode.CHECK_LIMIT_REACHED, - pdu_conf=pdu_conf, - file_store_responses=[filestore_reponse_1, filestore_reponse_2], - fault_location=fault_location_tlv, - ) - # length should be 13 (response 1) + 23 (response 2) + 4 (fault loc) + 9 (base) - self.assertEqual(finish_pdu_two_responses_one_fault_loc.packet_len, 49) - fs_responses = finish_pdu_two_responses_one_fault_loc.file_store_responses - self.assertEqual(len(fs_responses), 2) - complex_pdu_raw = finish_pdu_two_responses_one_fault_loc.pack() - complex_pdu_unpacked = FinishedPdu.unpack(raw_packet=complex_pdu_raw) - self.assertEqual( - complex_pdu_unpacked.fault_location.pack(), fault_location_tlv.pack() - ) - self.assertEqual(filestore_reponse_1.pack(), fs_responses[0].pack()) - self.assertEqual(filestore_reponse_2.pack(), fs_responses[1].pack()) - - # Change TLV type to make it invalid - complex_pdu_raw[-5] = TlvTypes.FILESTORE_RESPONSE - with self.assertRaises(ValueError): - FinishedPdu.unpack(raw_packet=complex_pdu_raw) - - def test_keep_alive_pdu(self): - pdu_conf = PduConfig.empty() - keep_alive_pdu = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) - self.assertEqual(keep_alive_pdu.progress, 0) - self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) - keep_alive_pdu_raw = keep_alive_pdu.pack() - self.assertEqual( - keep_alive_pdu_raw, - bytes( - [ - 0x20, - 0x00, - 0x05, - 0x11, - 0x00, - 0x00, - 0x00, - DirectiveType.KEEP_ALIVE_PDU, - 0x00, - 0x00, - 0x00, - 0x00, - ] - ), - ) - self.assertEqual(keep_alive_pdu.packet_len, 12) - keep_alive_unpacked = KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_raw) - self.assertEqual(keep_alive_unpacked.packet_len, 12) - self.assertEqual(keep_alive_unpacked.progress, 0) - keep_alive_pdu.file_flag = LargeFileFlag.LARGE - self.assertEqual(keep_alive_pdu.packet_len, 16) - keep_alive_pdu_large = keep_alive_pdu.pack() - self.assertEqual(len(keep_alive_pdu_large), 16) - - keep_alive_pdu.file_flag = LargeFileFlag.NORMAL - self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) - - keep_alive_pdu.progress = pow(2, 32) + 1 - with self.assertRaises(ValueError): - keep_alive_pdu.pack() - - pdu_conf.fss_field_len = LargeFileFlag.LARGE - keep_alive_pdu_large = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) - keep_alive_pdu_invalid = keep_alive_pdu_large.pack()[:-1] - with self.assertRaises(ValueError): - KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_invalid) - - def test_metadata_pdu(self): - pdu_conf = PduConfig.empty() - metadata_pdu = MetadataPdu( - pdu_conf=pdu_conf, - closure_requested=False, - file_size=2, - source_file_name="test.txt", - dest_file_name="test2.txt", - checksum_type=ChecksumTypes.MODULAR, - ) - self.check_metadata_fields_0(metadata_pdu=metadata_pdu) - header_len = metadata_pdu.pdu_file_directive.header_len - self.assertEqual(header_len, 8) - # 5 bytes from FSS with normal size and first eight bits - self.assertEqual(metadata_pdu.packet_len, header_len + 5 + 10 + 9) - metadata_pdu_raw = metadata_pdu.pack() - metadata_pdu_unpacked = MetadataPdu.unpack(raw_packet=metadata_pdu_raw) - self.check_metadata_fields_0(metadata_pdu=metadata_pdu_unpacked) - metadata_pdu_raw = metadata_pdu_raw[: 8 + 6] - self.assertRaises(ValueError, MetadataPdu.unpack, raw_packet=metadata_pdu_raw) - - file_name = "hallo.txt" - option_0 = FileStoreRequestTlv( - action_code=FilestoreActionCode.CREATE_FILE_SNM, first_file_name=file_name - ) - self.assertEqual(option_0.packet_len, 13) - expected_bytes = bytearray() - expected_bytes.extend(bytes([0x00, 0x0B, 0x00, 0x09])) - expected_bytes.extend(file_name.encode()) - self.assertEqual(option_0.pack(), expected_bytes) - - # Create completey new packet - pdu_with_option = MetadataPdu( - pdu_conf=pdu_conf, - closure_requested=False, - file_size=2, - source_file_name="test.txt", - dest_file_name="test2.txt", - checksum_type=ChecksumTypes.MODULAR, - options=[option_0], - ) - self.assertEqual(pdu_with_option.options, [option_0]) - expected_len = 10 + 9 + 8 + 5 + 13 - self.assertEqual(pdu_with_option.packet_len, expected_len) - pdu_with_option_raw = pdu_with_option.pack() - self.assertEqual(len(pdu_with_option_raw), expected_len) - pdu_with_option_unpacked = MetadataPdu.unpack(raw_packet=pdu_with_option_raw) - tlv_wrapper = TlvWrapper(pdu_with_option_unpacked.options[0]) - tlv_typed = tlv_wrapper.to_fs_request() - self.assertIsNotNone(tlv_typed) - self.assertEqual(tlv_typed.pack(), option_0.pack()) - - pdu_with_option.source_file_name = None - pdu_with_option.dest_file_name = None - expected_len = header_len + 1 + 1 + 5 + 13 - self.assertEqual(pdu_with_option.directive_param_field_len, 1 + 1 + 5 + 13) - self.assertEqual(pdu_with_option.packet_len, expected_len) - - option_1 = FaultHandlerOverrideTlv( - condition_code=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - handler_code=FaultHandlerCodes.ABANDON_TRANSACTION, - ) - self.assertEqual(option_1.packet_len, 3) - pdu_with_two_options = MetadataPdu( - pdu_conf=pdu_conf, - closure_requested=False, - file_size=2, - source_file_name=None, - dest_file_name=None, - checksum_type=ChecksumTypes.MODULAR, - options=[option_0, option_1], - ) - pdu_with_two_options_raw = pdu_with_two_options.pack() - expected_len = header_len + 5 + 2 + option_0.packet_len + option_1.packet_len - self.assertEqual(pdu_with_two_options.packet_len, expected_len) - self.assertEqual(len(pdu_with_two_options_raw), expected_len) - pdu_with_two_options.source_file_name = "hello.txt" - expected_len = ( - header_len + 5 + 1 + 10 + option_0.packet_len + option_1.packet_len - ) - self.assertEqual(pdu_with_two_options.packet_len, expected_len) - pdu_with_two_options.dest_file_name = "hello2.txt" - expected_len = ( - header_len + 5 + 11 + 10 + option_0.packet_len + option_1.packet_len - ) - self.assertEqual(pdu_with_two_options.packet_len, expected_len) - pdu_with_no_options = pdu_with_two_options - pdu_with_no_options.options = None - pdu_with_no_options.file_size = pow(2, 32) + 1 - with self.assertRaises(ValueError): - pdu_with_no_options.pack() - - pdu_conf.file_flag = LargeFileFlag.LARGE - pdu_file_size_large = MetadataPdu( - pdu_conf=pdu_conf, - closure_requested=False, - file_size=2, - source_file_name=None, - dest_file_name=None, - checksum_type=ChecksumTypes.MODULAR, - options=None, - ) - self.assertEqual(pdu_file_size_large.pdu_file_directive.header_len, header_len) - self.assertEqual(pdu_file_size_large.packet_len, header_len + 2 + 9) - pdu_file_size_large.options = [option_0] - pdu_file_size_large_raw = pdu_file_size_large.pack() - pdu_file_size_large_raw = pdu_file_size_large_raw[:-2] - with self.assertRaises(ValueError): - MetadataPdu.unpack(raw_packet=pdu_file_size_large_raw) - - def check_metadata_fields_0(self, metadata_pdu: MetadataPdu): - self.assertEqual(metadata_pdu.closure_requested, False) - self.assertEqual(metadata_pdu.file_size, 2) - self.assertEqual(metadata_pdu.source_file_name, "test.txt") - self.assertEqual(metadata_pdu.dest_file_name, "test2.txt") - self.assertEqual(metadata_pdu.pdu_file_directive.header_len, 8) - self.assertEqual(metadata_pdu._source_file_name_lv.packet_len, 9) - self.assertEqual(metadata_pdu._dest_file_name_lv.packet_len, 10) - - def test_eof_pdu(self): - pdu_conf = PduConfig.empty() - zero_checksum = bytes([0x00, 0x00, 0x00, 0x00]) - eof_pdu = EofPdu(file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf) - self.assertEqual(eof_pdu.pdu_file_directive.header_len, 8) - expected_packet_len = 8 + 1 + 4 + 4 - self.assertEqual(eof_pdu.packet_len, expected_packet_len) - eof_pdu_raw = eof_pdu.pack() - expected_header = bytearray([0x20, 0x00, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x04]) - expected_header.append(0) - expected_header.extend(zero_checksum) - # File size is 0 as 4 bytes - expected_header.extend(bytes([0x00, 0x00, 0x00, 0x00])) - self.assertEqual(eof_pdu_raw, expected_header) - eof_unpacked = EofPdu.unpack(raw_packet=eof_pdu_raw) - self.assertEqual(eof_unpacked.pack(), eof_pdu_raw) - eof_pdu_raw = eof_pdu_raw[:-2] - with self.assertRaises(ValueError): - EofPdu.unpack(raw_packet=eof_pdu_raw) - - fault_loc_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x01])) - self.assertEqual(fault_loc_tlv.packet_len, 4) - eof_pdu.fault_location = fault_loc_tlv - self.assertEqual(eof_pdu.packet_len, expected_packet_len + 4) - eof_pdu_with_fault_loc = eof_pdu - eof_pdu_with_fault_loc_raw = eof_pdu_with_fault_loc.pack() - self.assertEqual(len(eof_pdu_with_fault_loc_raw), expected_packet_len + 4) - eof_pdu_with_fault_loc_unpacked = EofPdu.unpack( - raw_packet=eof_pdu_with_fault_loc_raw - ) - self.assertEqual( - eof_pdu_with_fault_loc_unpacked.fault_location.pack(), fault_loc_tlv.pack() - ) - - with self.assertRaises(ValueError): - EofPdu(file_checksum=bytes([0x00]), file_size=0, pdu_conf=pdu_conf) - - pdu_conf.file_flag = LargeFileFlag.LARGE - eof_pdu_large_file = EofPdu( - file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf - ) - self.assertEqual(eof_pdu_large_file.packet_len, expected_packet_len + 4) - eof_pdu_large_file_raw = eof_pdu_large_file.pack() - self.assertEqual(len(eof_pdu_large_file_raw), expected_packet_len + 4) - - def test_prompt_pdu(self): - pdu_conf = PduConfig.empty() - prompt_pdu = PromptPdu( - pdu_conf=pdu_conf, reponse_required=ResponseRequired.KEEP_ALIVE - ) - print(prompt_pdu.pack().hex(sep=",")) - prompt_pdu_raw = prompt_pdu.pack() - self.assertEqual( - prompt_pdu_raw, - bytes([0x20, 0x00, 0x02, 0x11, 0x00, 0x00, 0x00, 0x09, 0x80]), - ) - self.assertEqual(prompt_pdu.packet_len, 9) - prompt_pdu_unpacked = PromptPdu.unpack(raw_packet=prompt_pdu_raw) - self.assertEqual(prompt_pdu.pdu_file_directive.pdu_data_field_len, 2) - self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 8) - self.assertEqual( - prompt_pdu_unpacked.response_required, ResponseRequired.KEEP_ALIVE - ) - self.assertEqual(prompt_pdu.pdu_file_directive.is_large_file(), False) - prompt_pdu_raw = prompt_pdu_raw[:-1] - with self.assertRaises(ValueError): - PromptPdu.unpack(raw_packet=prompt_pdu_raw) From ad43776e999ac3809989bfef50e8ddc8def08289 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 16:02:47 +0200 Subject: [PATCH 06/46] tests for new pdu cast wrapper --- spacepackets/cfdp/pdu/file_data.py | 34 +++++++++- spacepackets/cfdp/pdu/file_directive.py | 6 ++ spacepackets/cfdp/pdu/header.py | 10 +++ spacepackets/cfdp/pdu/metadata.py | 11 ++++ spacepackets/cfdp/pdu/wrapper.py | 10 +-- ...st_cfdp_file_data.py => test_file_data.py} | 0 tests/cfdp/pdus/test_pdu_wrapper.py | 65 ++++++++++++++++++- 7 files changed, 127 insertions(+), 9 deletions(-) rename tests/cfdp/pdus/{test_cfdp_file_data.py => test_file_data.py} (100%) diff --git a/spacepackets/cfdp/pdu/file_data.py b/spacepackets/cfdp/pdu/file_data.py index a8f3e13..b8880e6 100644 --- a/spacepackets/cfdp/pdu/file_data.py +++ b/spacepackets/cfdp/pdu/file_data.py @@ -3,9 +3,10 @@ from typing import Union import struct +from spacepackets.cfdp import LargeFileFlag from spacepackets.cfdp.pdu.file_directive import SegmentMetadataFlag, PduType from spacepackets.cfdp.conf import PduConfig -from spacepackets.cfdp.pdu.header import PduHeader +from spacepackets.cfdp.pdu.header import PduHeader, AbstractPduBase from spacepackets.log import get_console_logger @@ -23,7 +24,7 @@ class RecordContinuationState(enum.IntEnum): START_AND_END = 0b11 -class FileDataPdu: +class FileDataPdu(AbstractPduBase): def __init__( self, pdu_conf: PduConfig, @@ -62,6 +63,26 @@ def __empty(cls) -> FileDataPdu: pdu_conf=empty_conf, ) + @property + def pdu_type(self) -> PduType: + return self.pdu_header.pdu_type + + @property + def file_flag(self) -> LargeFileFlag: + return self.pdu_header.file_flag + + @property + def source_entity_id(self) -> bytes: + return self.pdu_header.source_entity_id + + @property + def dest_entity_id(self) -> bytes: + return self.pdu_header.dest_entity_id + + @property + def crc_flag(self): + return self.pdu_header.crc_flag + @property def segment_metadata(self): return self._segment_metadata @@ -152,3 +173,12 @@ def unpack(cls, raw_packet: bytearray) -> FileDataPdu: @property def packet_len(self): return self.pdu_header.pdu_len + + def __eq__(self, other: FileDataPdu): + return ( + self.pdu_header == other.pdu_header + and self.segment_metadata == other.segment_metadata + and self.record_continuation_state == other.record_continuation_state + and self.offset == other.offset + and self._file_data == other._file_data + ) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 62de808..6bcefda 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -87,6 +87,12 @@ def packet_len(self) -> int: """ return self.pdu_header.pdu_len + def __eq__(self, other: AbstractFileDirectiveBase): + return ( + self.pdu_header == other.pdu_header + and self.directive_type == other.directive_type + ) + class FileDirectivePduBase(AbstractFileDirectiveBase): FILE_DIRECTIVE_PDU_LEN = 5 diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 05a0980..a301687 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -65,6 +65,16 @@ def crc_flag(self): def crc_flag(self, crc_flag: CrcFlag): pass + def __eq__(self, other: AbstractPduBase): + return ( + self.pdu_type == other.pdu_type + and self.file_flag == other.file_flag + and self.crc_flag == other.crc_flag + and self.dest_entity_id == other.dest_entity_id + and self.source_entity_id == other.source_entity_id + and self.packet_len == other.packet_len + ) + class PduHeader(AbstractPduBase): """This class encapsulates the fixed-format PDU header. diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index bda9ba6..6db51c6 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -187,3 +187,14 @@ def _parse_options(self, raw_packet: bytearray, start_idx: int): raise ValueError elif current_idx == len(raw_packet): break + + def __eq__(self, other: MetadataPdu): + return ( + self.pdu_file_directive == other.pdu_file_directive + and self.closure_requested == other.closure_requested + and self.checksum_type == other.checksum_type + and self.file_size == other.file_size + and self._source_file_name_lv == other._source_file_name_lv + and self._dest_file_name_lv == other._dest_file_name_lv + and self._options == other._options + ) diff --git a/spacepackets/cfdp/pdu/wrapper.py b/spacepackets/cfdp/pdu/wrapper.py index fe45cdb..d8168a3 100644 --- a/spacepackets/cfdp/pdu/wrapper.py +++ b/spacepackets/cfdp/pdu/wrapper.py @@ -1,4 +1,4 @@ -from typing import cast, Union, Type +from typing import cast, Union, Type, Optional from spacepackets.cfdp import PduType from spacepackets.cfdp.pdu import ( @@ -19,13 +19,13 @@ class PduWrapper: """Helper type to store arbitrary PDU types and cast them to a concrete PDU type conveniently""" - def __init__(self, base: Union[AbstractFileDirectiveBase, AbstractPduBase]): + def __init__( + self, base: Optional[Union[AbstractFileDirectiveBase, AbstractPduBase]] + ): self.base = base def _raise_not_target_exception(self, pdu_type: Type[any]): - raise TypeError( - f"Stored PDU is not {pdu_type.__class__.__name__}: {self.base!r}" - ) + raise TypeError(f"Stored PDU is not {pdu_type.__name__!r}: {self.base!r}") def _cast_to_concrete_file_directive( self, pdu_type: Type[any], dir_type: DirectiveType diff --git a/tests/cfdp/pdus/test_cfdp_file_data.py b/tests/cfdp/pdus/test_file_data.py similarity index 100% rename from tests/cfdp/pdus/test_cfdp_file_data.py rename to tests/cfdp/pdus/test_file_data.py diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index 9746529..1c6e7b5 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -1,6 +1,67 @@ from unittest import TestCase +from spacepackets.cfdp import ChecksumTypes +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.pdu import MetadataPdu +from spacepackets.cfdp.pdu.file_data import FileDataPdu +from spacepackets.cfdp.pdu.wrapper import PduWrapper + class TestPduWrapper(TestCase): - def test_wrapper(self): - pass + def setUp(self) -> None: + self.pdu_conf = PduConfig.empty() + self.file_data = "hello world" + self.file_data_bytes = self.file_data.encode() + self.pdu_wrapper = PduWrapper(None) + + def test_file_data(self): + file_data_pdu = FileDataPdu( + pdu_conf=self.pdu_conf, + file_data=self.file_data_bytes, + offset=0, + segment_metadata_flag=False, + ) + self.pdu_wrapper.base = file_data_pdu + pdu_casted_back = self.pdu_wrapper.to_file_data_pdu() + self.assertEqual(pdu_casted_back, file_data_pdu) + + def test_invalid_to_file_data(self): + metadata_pdu = MetadataPdu( + pdu_conf=self.pdu_conf, + closure_requested=False, + file_size=2, + source_file_name="test.txt", + dest_file_name="test2.txt", + checksum_type=ChecksumTypes.MODULAR, + ) + self.pdu_wrapper.base = metadata_pdu + with self.assertRaises(TypeError) as cm: + self.pdu_wrapper.to_file_data_pdu() + exception = cm.exception + self.assertIn("Stored PDU is not 'FileDataPdu'", str(exception)) + + def test_metadata(self): + metadata_pdu = MetadataPdu( + pdu_conf=self.pdu_conf, + closure_requested=False, + file_size=2, + source_file_name="test.txt", + dest_file_name="test2.txt", + checksum_type=ChecksumTypes.MODULAR, + ) + self.pdu_wrapper.base = metadata_pdu + metadata_casted_back = self.pdu_wrapper.to_metadata_pdu() + self.assertEqual(metadata_casted_back, metadata_pdu) + + def test_invalid_cast(self): + file_data_pdu = FileDataPdu( + pdu_conf=self.pdu_conf, + file_data=self.file_data_bytes, + offset=0, + segment_metadata_flag=False, + ) + self.pdu_wrapper.base = file_data_pdu + with self.assertRaises(TypeError) as cm: + self.pdu_wrapper.to_metadata_pdu() + exception = cm.exception + self.assertTrue("Stored PDU is not 'MetadataPdu'" in str(exception)) From ffccee19716bebec8c9ceedbe577ec01431cd43c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 16:37:59 +0200 Subject: [PATCH 07/46] improve PDU API --- spacepackets/cfdp/pdu/ack.py | 12 ++++-- spacepackets/cfdp/pdu/eof.py | 4 +- spacepackets/cfdp/pdu/file_data.py | 16 ++++++-- spacepackets/cfdp/pdu/file_directive.py | 26 ++++++------- spacepackets/cfdp/pdu/finished.py | 2 +- spacepackets/cfdp/pdu/header.py | 49 +++++++++++++++---------- spacepackets/cfdp/pdu/keep_alive.py | 26 +++++++++---- spacepackets/cfdp/pdu/metadata.py | 11 ++++-- spacepackets/cfdp/pdu/nak.py | 11 ++++-- spacepackets/cfdp/pdu/wrapper.py | 6 +-- tests/cfdp/pdus/test_directive.py | 4 +- tests/cfdp/pdus/test_pdu_wrapper.py | 25 ++++++++++++- tests/cfdp/pdus/test_prompt_pdu.py | 2 +- tests/cfdp/test_header.py | 4 +- 14 files changed, 129 insertions(+), 69 deletions(-) diff --git a/spacepackets/cfdp/pdu/ack.py b/spacepackets/cfdp/pdu/ack.py index c4958ea..8601661 100644 --- a/spacepackets/cfdp/pdu/ack.py +++ b/spacepackets/cfdp/pdu/ack.py @@ -1,5 +1,6 @@ from __future__ import annotations import enum +from typing import cast from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( @@ -65,9 +66,14 @@ def directive_type(self) -> DirectiveType: def pdu_header(self) -> PduHeader: return self.pdu_file_directive.pdu_header - @property - def packet_len(self) -> int: - return self.pdu_file_directive.packet_len + def __eq__(self, other: AckPdu): + return ( + self.pdu_file_directive == other.pdu_file_directive + and self.directive_code_of_acked_pdu == other.directive_code_of_acked_pdu + and self.directive_subtype_code == other.directive_subtype_code + and self.condition_code_of_acked_pdu == other.condition_code_of_acked_pdu + and self.transaction_status == other.transaction_status + ) @classmethod def __empty(cls) -> AckPdu: diff --git a/spacepackets/cfdp/pdu/eof.py b/spacepackets/cfdp/pdu/eof.py index c07a2a5..4b8b244 100644 --- a/spacepackets/cfdp/pdu/eof.py +++ b/spacepackets/cfdp/pdu/eof.py @@ -70,7 +70,7 @@ def fault_location(self, fault_location: Optional[EntityIdTlv]): def _calculate_directive_param_field_len(self): directive_param_field_len = 9 - if self.pdu_file_directive.pdu_header.is_large_file(): + if self.pdu_file_directive.pdu_header.large_file_flag_set: directive_param_field_len = 13 if self._fault_location is not None: directive_param_field_len += self._fault_location.packet_len @@ -89,7 +89,7 @@ def pack(self) -> bytearray: eof_pdu = self.pdu_file_directive.pack() eof_pdu.append(self.condition_code << 4) eof_pdu.extend(self.file_checksum) - if self.pdu_file_directive.pdu_header.is_large_file(): + if self.pdu_file_directive.pdu_header.large_file_flag_set: eof_pdu.extend(struct.pack("!Q", self.file_size)) else: eof_pdu.extend(struct.pack("!I", self.file_size)) diff --git a/spacepackets/cfdp/pdu/file_data.py b/spacepackets/cfdp/pdu/file_data.py index b8880e6..490e122 100644 --- a/spacepackets/cfdp/pdu/file_data.py +++ b/spacepackets/cfdp/pdu/file_data.py @@ -63,6 +63,14 @@ def __empty(cls) -> FileDataPdu: pdu_conf=empty_conf, ) + @property + def header_len(self) -> int: + return self.pdu_header.header_len + + @property + def pdu_data_field_len(self) -> int: + return self.pdu_header.pdu_data_field_len + @property def pdu_type(self) -> PduType: return self.pdu_header.pdu_type @@ -105,7 +113,7 @@ def _calculate_pdu_data_field_len(self): pdu_data_field_len = 0 if self.segment_metadata_flag: pdu_data_field_len = 1 + len(self._segment_metadata) - if self.pdu_header.is_large_file(): + if self.pdu_header.large_file_flag_set: pdu_data_field_len += 8 else: pdu_data_field_len += 4 @@ -125,7 +133,7 @@ def pack(self) -> bytearray: file_data_pdu.append(self.record_continuation_state << 6 | len_metadata) if len_metadata > 0: file_data_pdu.extend(self._segment_metadata) - if not self.pdu_header.is_large_file(): + if not self.pdu_header.large_file_flag_set: file_data_pdu.extend(struct.pack("!I", self.offset)) else: file_data_pdu.extend(struct.pack("!Q", self.offset)) @@ -153,7 +161,7 @@ def unpack(cls, raw_packet: bytearray) -> FileDataPdu: current_idx : current_idx + file_data_packet.segment_metadata_length ] current_idx += file_data_packet.segment_metadata_length - if not file_data_packet.pdu_header.is_large_file(): + if not file_data_packet.pdu_header.large_file_flag_set: struct_arg_tuple = ("!I", 4) else: struct_arg_tuple = ("!Q", 8) @@ -172,7 +180,7 @@ def unpack(cls, raw_packet: bytearray) -> FileDataPdu: @property def packet_len(self): - return self.pdu_header.pdu_len + return self.pdu_header.packet_len def __eq__(self, other: FileDataPdu): return ( diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 6bcefda..bab985f 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -27,7 +27,7 @@ class DirectiveType(enum.IntEnum): class AbstractFileDirectiveBase(AbstractPduBase): - """Encapsulate common functions for classes which are FileDirectives""" + """Encapsulate common functions for classes which are PDU file directives""" @property @abc.abstractmethod @@ -37,6 +37,8 @@ def directive_type(self) -> DirectiveType: @property @abc.abstractmethod def pdu_header(self) -> PduHeader: + # Could return abstract class here but I think returning the concrete implementation + # provided here is ok.. pass @property @@ -85,7 +87,7 @@ def packet_len(self) -> int: """Get length of the packet when packing it :return: """ - return self.pdu_header.pdu_len + return self.pdu_header.packet_len def __eq__(self, other: AbstractFileDirectiveBase): return ( @@ -140,9 +142,6 @@ def directive_param_field_len(self): def directive_param_field_len(self, directive_param_field_len: int): self.pdu_header.pdu_data_field_len = directive_param_field_len + 1 - def is_large_file(self): - return self.pdu_header.is_large_file() - @classmethod def __empty(cls) -> FileDirectivePduBase: empty_conf = PduConfig.empty() @@ -175,18 +174,14 @@ def unpack(cls, raw_packet: bytes) -> FileDirectivePduBase: file_directive._directive_type = raw_packet[header_len - 1] return file_directive - def _verify_file_len(self, file_size: int) -> bool: + def verify_file_len(self, file_size: int) -> bool: """Can be used by subclasses to verify a given file size""" - if ( - self.pdu_header.pdu_conf.file_flag == LargeFileFlag.LARGE - and file_size > pow(2, 64) - ): + if self.pdu_header.file_flag == LargeFileFlag.LARGE and file_size > pow(2, 64): logger = get_console_logger() logger.warning(f"File size {file_size} larger than 64 bit field") return False - elif ( - self.pdu_header.pdu_conf.file_flag == LargeFileFlag.NORMAL - and file_size > pow(2, 32) + elif self.pdu_header.file_flag == LargeFileFlag.NORMAL and file_size > pow( + 2, 32 ): logger = get_console_logger() logger.warning(f"File size {file_size} larger than 32 bit field") @@ -199,7 +194,7 @@ def _parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): :raise ValueError: Packet not large enough """ - if self.pdu_header.pdu_conf.file_flag == LargeFileFlag.LARGE: + if self.pdu_header.file_flag == LargeFileFlag.LARGE: if not check_packet_length(len(raw_packet), current_idx + 8): raise ValueError file_size = struct.unpack("!Q", raw_packet[current_idx : current_idx + 8])[ @@ -214,3 +209,6 @@ def _parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): ] current_idx += 4 return current_idx, file_size + + def __eq__(self, other: FileDirectivePduBase): + return AbstractFileDirectiveBase.__eq__(self, other) diff --git a/spacepackets/cfdp/pdu/finished.py b/spacepackets/cfdp/pdu/finished.py index ba3c693..6a266b0 100644 --- a/spacepackets/cfdp/pdu/finished.py +++ b/spacepackets/cfdp/pdu/finished.py @@ -1,6 +1,6 @@ from __future__ import annotations import enum -from typing import List, Optional +from typing import List, Optional, cast from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index a301687..2cb327e 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -22,9 +22,17 @@ class AbstractPduBase(abc.ABC): """Encapsulate common functions for PDU. PDU or Packet Data Units are the base data unit - which are exchanged for CFDP procedures. Each PDU has the common header. + which are exchanged for CFDP procedures. Each PDU has a common header and this class provides + abstract methods to access fields of that common header. + For more, information, refer to CCSDS 727.0-B-5 p.75. + The default implementation provided in this library for this abstract class is the + :py:class:`PduHeader` class. """ + @abc.abstractmethod + def pack(self) -> bytes: + pass + @property @abc.abstractmethod def pdu_type(self) -> PduType: @@ -40,6 +48,11 @@ def file_flag(self) -> LargeFileFlag: def file_flag(self, file_flag: LargeFileFlag): pass + @property + @abc.abstractmethod + def header_len(self) -> int: + pass + @property @abc.abstractmethod def source_entity_id(self) -> bytes: @@ -52,7 +65,12 @@ def dest_entity_id(self) -> bytes: @property @abc.abstractmethod - def packet_len(self) -> int: + def pdu_data_field_len(self) -> int: + pass + + @pdu_data_field_len.setter + @abc.abstractmethod + def pdu_data_field_len(self, pdu_data_field_len: int) -> int: pass @property @@ -65,6 +83,14 @@ def crc_flag(self): def crc_flag(self, crc_flag: CrcFlag): pass + @property + def packet_len(self) -> int: + return self.pdu_data_field_len + self.header_len + + @property + def large_file_flag_set(self) -> bool: + return self.file_flag == LargeFileFlag.LARGE + def __eq__(self, other: AbstractPduBase): return ( self.pdu_type == other.pdu_type @@ -77,8 +103,7 @@ def __eq__(self, other: AbstractPduBase): class PduHeader(AbstractPduBase): - """This class encapsulates the fixed-format PDU header. - For more, information, refer to CCSDS 727.0-B-5 p.75""" + """Concrete implementation of the abstract :py:class:`AbstractPduBase` class""" VERSION_BITS = 0b0010_0000 FIXED_LENGTH = 4 @@ -237,22 +262,6 @@ def header_len(self) -> int: """Get length of PDU header when packing it""" return self.FIXED_LENGTH + 2 * self.len_entity_id + self.len_transaction_seq_num - @property - def pdu_len(self) -> int: - """Get the length of the full PDU. This assumes that the length of the PDU data field - length was already set""" - return self.pdu_data_field_len + self.header_len - - @property - def packet_len(self) -> int: - return self.pdu_len - - def is_large_file(self) -> bool: - if self.pdu_conf.file_flag == LargeFileFlag.LARGE: - return True - else: - return False - def pack(self) -> bytearray: header = bytearray() header.append( diff --git a/spacepackets/cfdp/pdu/keep_alive.py b/spacepackets/cfdp/pdu/keep_alive.py index 740979d..bd747b6 100644 --- a/spacepackets/cfdp/pdu/keep_alive.py +++ b/spacepackets/cfdp/pdu/keep_alive.py @@ -2,18 +2,22 @@ import struct -from spacepackets.cfdp.pdu.file_directive import FileDirectivePduBase, DirectiveType +from spacepackets.cfdp.pdu import PduHeader +from spacepackets.cfdp.pdu.file_directive import ( + FileDirectivePduBase, + DirectiveType, + AbstractFileDirectiveBase, +) from spacepackets.cfdp.conf import PduConfig, LargeFileFlag from spacepackets.log import get_console_logger -class KeepAlivePdu(FileDirectivePduBase): +class KeepAlivePdu(AbstractFileDirectiveBase): """Encapsulates the Keep Alive file directive PDU, see CCSDS 727.0-B-5 p.85""" def __init__(self, progress: int, pdu_conf: PduConfig): - if pdu_conf.file_flag == LargeFileFlag.NORMAL: - directive_param_field_len = 4 - elif pdu_conf.file_flag == LargeFileFlag.LARGE: + directive_param_field_len = 4 + if pdu_conf.file_flag == LargeFileFlag.LARGE: directive_param_field_len = 8 # Directive param field length is minimum FSS size which is 4 bytes self.pdu_file_directive = FileDirectivePduBase( @@ -23,6 +27,14 @@ def __init__(self, progress: int, pdu_conf: PduConfig): ) self.progress = progress + @property + def directive_type(self) -> DirectiveType: + return DirectiveType.KEEP_ALIVE_PDU + + @property + def pdu_header(self) -> PduHeader: + return self.pdu_file_directive.pdu_header + @property def file_flag(self): return self.pdu_file_directive.pdu_header.file_flag @@ -42,7 +54,7 @@ def __empty(cls) -> KeepAlivePdu: def pack(self) -> bytearray: keep_alive_packet = self.pdu_file_directive.pack() - if not self.pdu_file_directive.pdu_header.is_large_file(): + if not self.pdu_file_directive.pdu_header.large_file_flag_set: if self.progress > pow(2, 32) - 1: raise ValueError keep_alive_packet.extend(struct.pack("I", self.progress)) @@ -57,7 +69,7 @@ def unpack(cls, raw_packet: bytearray) -> KeepAlivePdu: raw_packet=raw_packet ) current_idx = keep_alive_pdu.pdu_file_directive.header_len - if not keep_alive_pdu.pdu_file_directive.pdu_header.is_large_file(): + if not keep_alive_pdu.pdu_file_directive.pdu_header.large_file_flag_set: struct_arg_tuple = ("!I", 4) else: struct_arg_tuple = ("!Q", 8) diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 6db51c6..0fa07db 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -1,6 +1,6 @@ from __future__ import annotations import struct -from typing import List, Optional +from typing import List, Optional, cast from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( @@ -89,7 +89,10 @@ def directive_param_field_len(self): def _calculate_directive_field_len(self): directive_param_field_len = 5 - if self.pdu_file_directive.pdu_header.is_large_file() == LargeFileFlag.LARGE: + if ( + self.pdu_file_directive.pdu_header.large_file_flag_set + == LargeFileFlag.LARGE + ): directive_param_field_len = 9 directive_param_field_len += self._source_file_name_lv.packet_len directive_param_field_len += self._dest_file_name_lv.packet_len @@ -128,11 +131,11 @@ def packet_len(self) -> int: return self.pdu_file_directive.packet_len def pack(self) -> bytearray: - if not self.pdu_file_directive._verify_file_len(self.file_size): + if not self.pdu_file_directive.verify_file_len(self.file_size): raise ValueError packet = self.pdu_file_directive.pack() packet.append((self.closure_requested << 6) | self.checksum_type) - if self.pdu_file_directive.pdu_header.is_large_file(): + if self.pdu_file_directive.pdu_header.large_file_flag_set: packet.extend(struct.pack("!Q", self.file_size)) else: packet.extend(struct.pack("!I", self.file_size)) diff --git a/spacepackets/cfdp/pdu/nak.py b/spacepackets/cfdp/pdu/nak.py index ef7e5e7..c5c028e 100644 --- a/spacepackets/cfdp/pdu/nak.py +++ b/spacepackets/cfdp/pdu/nak.py @@ -85,7 +85,7 @@ def segment_requests(self, segment_requests: Optional[List[Tuple[int, int]]]): if self._segment_requests is None: self._segment_requests = [] return - is_large_file = self.pdu_file_directive.pdu_header.is_large_file() + is_large_file = self.pdu_file_directive.pdu_header.large_file_flag_set if not is_large_file: directive_param_field_len = 8 + len(self._segment_requests) * 8 else: @@ -98,7 +98,7 @@ def pack(self) -> bytearray: :raises ValueError: File sizes too large for non-large files """ nak_pdu = self.pdu_file_directive.pack() - if not self.pdu_file_directive.pdu_header.is_large_file(): + if not self.pdu_file_directive.pdu_header.large_file_flag_set: if ( self.start_of_scope > pow(2, 32) - 1 or self.end_of_scope > pow(2, 32) - 1 @@ -110,7 +110,7 @@ def pack(self) -> bytearray: nak_pdu.extend(struct.pack("!Q", self.start_of_scope)) nak_pdu.extend(struct.pack("!Q", self.end_of_scope)) for segment_request in self._segment_requests: - if not self.pdu_file_directive.pdu_header.is_large_file(): + if not self.pdu_file_directive.pdu_header.large_file_flag_set: if ( segment_request[0] > pow(2, 32) - 1 or segment_request[1] > pow(2, 32) - 1 @@ -128,7 +128,7 @@ def unpack(cls, raw_packet: bytes) -> NakPdu: nak_pdu = cls.__empty() nak_pdu.pdu_file_directive = FileDirectivePduBase.unpack(raw_packet=raw_packet) current_idx = nak_pdu.pdu_file_directive.header_len - if not nak_pdu.pdu_file_directive.pdu_header.is_large_file(): + if not nak_pdu.pdu_file_directive.pdu_header.large_file_flag_set: struct_arg_tuple = ("!I", 4) else: struct_arg_tuple = ("!Q", 8) @@ -170,3 +170,6 @@ def unpack(cls, raw_packet: bytes) -> NakPdu: segment_requests.append(tuple_entry) nak_pdu.segment_requests = segment_requests return nak_pdu + + def __eq__(self, other: NakPdu): + return self.pdu_file_directive == other.pdu_file_directive diff --git a/spacepackets/cfdp/pdu/wrapper.py b/spacepackets/cfdp/pdu/wrapper.py index d8168a3..71738c7 100644 --- a/spacepackets/cfdp/pdu/wrapper.py +++ b/spacepackets/cfdp/pdu/wrapper.py @@ -15,13 +15,13 @@ from spacepackets.cfdp.pdu.file_data import FileDataPdu from spacepackets.cfdp.pdu.header import AbstractPduBase +GenericPduPacket = Union[AbstractFileDirectiveBase, AbstractPduBase] + class PduWrapper: """Helper type to store arbitrary PDU types and cast them to a concrete PDU type conveniently""" - def __init__( - self, base: Optional[Union[AbstractFileDirectiveBase, AbstractPduBase]] - ): + def __init__(self, base: Optional[GenericPduPacket]): self.base = base def _raise_not_target_exception(self, pdu_type: Type[any]): diff --git a/tests/cfdp/pdus/test_directive.py b/tests/cfdp/pdus/test_directive.py index 8a1fded..d353285 100644 --- a/tests/cfdp/pdus/test_directive.py +++ b/tests/cfdp/pdus/test_directive.py @@ -23,14 +23,14 @@ def test_file_directive(self): file_directive_header_raw_invalid = file_directive_header_raw[:-1] with self.assertRaises(ValueError): FileDirectivePduBase.unpack(raw_packet=file_directive_header_raw_invalid) - self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 33))) + self.assertFalse(file_directive_header.verify_file_len(file_size=pow(2, 33))) invalid_fss = bytes([0x00, 0x01]) with self.assertRaises(ValueError): file_directive_header._parse_fss_field( raw_packet=invalid_fss, current_idx=0 ) file_directive_header.pdu_header.file_size = LargeFileFlag.LARGE - self.assertFalse(file_directive_header._verify_file_len(file_size=pow(2, 65))) + self.assertFalse(file_directive_header.verify_file_len(file_size=pow(2, 65))) with self.assertRaises(ValueError): file_directive_header._parse_fss_field( raw_packet=invalid_fss, current_idx=0 diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index 1c6e7b5..abe80e9 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -1,8 +1,14 @@ from unittest import TestCase -from spacepackets.cfdp import ChecksumTypes +from spacepackets.cfdp import ChecksumTypes, ConditionCode from spacepackets.cfdp.conf import PduConfig -from spacepackets.cfdp.pdu import MetadataPdu +from spacepackets.cfdp.pdu import ( + MetadataPdu, + AckPdu, + DirectiveType, + TransactionStatus, + NakPdu, +) from spacepackets.cfdp.pdu.file_data import FileDataPdu from spacepackets.cfdp.pdu.wrapper import PduWrapper @@ -65,3 +71,18 @@ def test_invalid_cast(self): self.pdu_wrapper.to_metadata_pdu() exception = cm.exception self.assertTrue("Stored PDU is not 'MetadataPdu'" in str(exception)) + + def test_ack_cast(self): + ack_pdu = AckPdu( + directive_code_of_acked_pdu=DirectiveType.FINISHED_PDU, + condition_code_of_acked_pdu=ConditionCode.NO_ERROR, + transaction_status=TransactionStatus.TERMINATED, + pdu_conf=self.pdu_conf, + ) + self.pdu_wrapper.base = ack_pdu + ack_pdu_converted = self.pdu_wrapper.to_ack_pdu() + self.assertEqual(ack_pdu_converted, ack_pdu) + + def test_nak_cast(self): + nak_pdu = NakPdu(start_of_scope=0, end_of_scope=200, pdu_conf=self.pdu_conf) + self.pdu_wrapper.base = nak_pdu diff --git a/tests/cfdp/pdus/test_prompt_pdu.py b/tests/cfdp/pdus/test_prompt_pdu.py index 472c739..6aa79fb 100644 --- a/tests/cfdp/pdus/test_prompt_pdu.py +++ b/tests/cfdp/pdus/test_prompt_pdu.py @@ -24,7 +24,7 @@ def test_prompt_pdu(self): self.assertEqual( prompt_pdu_unpacked.response_required, ResponseRequired.KEEP_ALIVE ) - self.assertEqual(prompt_pdu.pdu_file_directive.is_large_file(), False) + self.assertEqual(prompt_pdu.pdu_file_directive.large_file_flag_set, False) prompt_pdu_raw = prompt_pdu_raw[:-1] with self.assertRaises(ValueError): PromptPdu.unpack(raw_packet=prompt_pdu_raw) diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index d9f0048..df77e9c 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -63,7 +63,7 @@ def test_pdu_header(self): self.assertEqual( pdu_header.segment_metadata_flag, SegmentMetadataFlag.NOT_PRESENT ) - self.assertFalse(pdu_header.is_large_file()) + self.assertFalse(pdu_header.large_file_flag_set) self.assertEqual(pdu_header.transaction_seq_num, bytes([0])) self.assertEqual(pdu_header.len_transaction_seq_num, 1) self.assertEqual(pdu_header.crc_flag, CrcFlag.NO_CRC) @@ -98,7 +98,7 @@ def test_pdu_header(self): pdu_header.seg_ctrl = SegmentationControl.RECORD_BOUNDARIES_PRESERVATION pdu_header.segment_metadata_flag = SegmentMetadataFlag.PRESENT - self.assertTrue(pdu_header.is_large_file()) + self.assertTrue(pdu_header.large_file_flag_set) pdu_header_packed = pdu_header.pack() self.check_fields_case_two(pdu_header_packed=pdu_header_packed) set_entity_ids(source_entity_id=bytes(), dest_entity_id=bytes()) From 69b1401edf8cf547c2c0316aaee2ceb35d01204d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 16:50:55 +0200 Subject: [PATCH 08/46] tests almost finished --- spacepackets/cfdp/pdu/eof.py | 11 +++++++- spacepackets/cfdp/pdu/file_directive.py | 2 +- spacepackets/cfdp/pdu/metadata.py | 2 +- spacepackets/cfdp/pdu/nak.py | 7 ++++- spacepackets/cfdp/pdu/prompt.py | 12 ++++++--- tests/cfdp/pdus/test_directive.py | 8 ++---- tests/cfdp/pdus/test_pdu_wrapper.py | 35 +++++++++++++++++++++++++ tests/cfdp/pdus/test_prompt_pdu.py | 2 +- tests/cfdp/test_header.py | 2 +- 9 files changed, 66 insertions(+), 15 deletions(-) diff --git a/spacepackets/cfdp/pdu/eof.py b/spacepackets/cfdp/pdu/eof.py index 4b8b244..c931561 100644 --- a/spacepackets/cfdp/pdu/eof.py +++ b/spacepackets/cfdp/pdu/eof.py @@ -117,7 +117,7 @@ def unpack(cls, raw_packet: bytearray) -> EofPdu: current_idx += 1 eof_pdu.file_checksum = raw_packet[current_idx : current_idx + 4] current_idx += 4 - current_idx, eof_pdu.file_size = eof_pdu.pdu_file_directive._parse_fss_field( + current_idx, eof_pdu.file_size = eof_pdu.pdu_file_directive.parse_fss_field( raw_packet=raw_packet, current_idx=current_idx ) if len(raw_packet) > current_idx: @@ -125,3 +125,12 @@ def unpack(cls, raw_packet: bytearray) -> EofPdu: raw_bytes=raw_packet[current_idx:] ) return eof_pdu + + def __eq__(self, other: EofPdu): + return ( + self.pdu_file_directive == other.pdu_file_directive + and self.condition_code == other.condition_code + and self.file_checksum == other.file_checksum + and self.file_size == other.file_size + and self._fault_location == other._fault_location + ) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index bab985f..a092d01 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -188,7 +188,7 @@ def verify_file_len(self, file_size: int) -> bool: return False return True - def _parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): + def parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): """Parse the FSS field, which has different size depending on the large file flag being set or not. Returns the current index incremented and the parsed file size. diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 0fa07db..6ab2653 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -161,7 +161,7 @@ def unpack(cls, raw_packet: bytearray) -> MetadataPdu: ( current_idx, metadata_pdu.file_size, - ) = metadata_pdu.pdu_file_directive._parse_fss_field( + ) = metadata_pdu.pdu_file_directive.parse_fss_field( raw_packet=raw_packet, current_idx=current_idx ) metadata_pdu._source_file_name_lv = CfdpLv.unpack( diff --git a/spacepackets/cfdp/pdu/nak.py b/spacepackets/cfdp/pdu/nak.py index c5c028e..7e6d079 100644 --- a/spacepackets/cfdp/pdu/nak.py +++ b/spacepackets/cfdp/pdu/nak.py @@ -172,4 +172,9 @@ def unpack(cls, raw_packet: bytes) -> NakPdu: return nak_pdu def __eq__(self, other: NakPdu): - return self.pdu_file_directive == other.pdu_file_directive + return ( + self.pdu_file_directive == other.pdu_file_directive + and self._segment_requests == other._segment_requests + and self.start_of_scope == other.start_of_scope + and self.end_of_scope == other.end_of_scope + ) diff --git a/spacepackets/cfdp/pdu/prompt.py b/spacepackets/cfdp/pdu/prompt.py index 26cb296..ed64e34 100644 --- a/spacepackets/cfdp/pdu/prompt.py +++ b/spacepackets/cfdp/pdu/prompt.py @@ -15,13 +15,13 @@ class ResponseRequired(enum.IntEnum): class PromptPdu(AbstractFileDirectiveBase): """Encapsulates the Prompt file directive PDU, see CCSDS 727.0-B-5 p.84""" - def __init__(self, reponse_required: ResponseRequired, pdu_conf: PduConfig): + def __init__(self, response_required: ResponseRequired, pdu_conf: PduConfig): self.pdu_file_directive = FileDirectivePduBase( directive_code=DirectiveType.PROMPT_PDU, pdu_conf=pdu_conf, directive_param_field_len=1, ) - self.response_required = reponse_required + self.response_required = response_required @property def directive_type(self) -> DirectiveType: @@ -34,7 +34,7 @@ def pdu_header(self) -> PduHeader: @classmethod def __empty(cls) -> PromptPdu: empty_conf = PduConfig.empty() - return cls(reponse_required=ResponseRequired.NAK, pdu_conf=empty_conf) + return cls(response_required=ResponseRequired.NAK, pdu_conf=empty_conf) def pack(self) -> bytearray: prompt_pdu = self.pdu_file_directive.pack() @@ -56,3 +56,9 @@ def unpack(cls, raw_packet: bytearray) -> PromptPdu: (raw_packet[current_idx] & 0x80) >> 7 ) return prompt_pdu + + def __eq__(self, other: PromptPdu): + return ( + self.pdu_file_directive == other.pdu_file_directive + and self.response_required == other.response_required + ) diff --git a/tests/cfdp/pdus/test_directive.py b/tests/cfdp/pdus/test_directive.py index d353285..d71b0e5 100644 --- a/tests/cfdp/pdus/test_directive.py +++ b/tests/cfdp/pdus/test_directive.py @@ -26,12 +26,8 @@ def test_file_directive(self): self.assertFalse(file_directive_header.verify_file_len(file_size=pow(2, 33))) invalid_fss = bytes([0x00, 0x01]) with self.assertRaises(ValueError): - file_directive_header._parse_fss_field( - raw_packet=invalid_fss, current_idx=0 - ) + file_directive_header.parse_fss_field(raw_packet=invalid_fss, current_idx=0) file_directive_header.pdu_header.file_size = LargeFileFlag.LARGE self.assertFalse(file_directive_header.verify_file_len(file_size=pow(2, 65))) with self.assertRaises(ValueError): - file_directive_header._parse_fss_field( - raw_packet=invalid_fss, current_idx=0 - ) + file_directive_header.parse_fss_field(raw_packet=invalid_fss, current_idx=0) diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index abe80e9..7604bdc 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -8,8 +8,13 @@ DirectiveType, TransactionStatus, NakPdu, + PromptPdu, + EofPdu, + FinishedPdu, ) from spacepackets.cfdp.pdu.file_data import FileDataPdu +from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus +from spacepackets.cfdp.pdu.prompt import ResponseRequired from spacepackets.cfdp.pdu.wrapper import PduWrapper @@ -86,3 +91,33 @@ def test_ack_cast(self): def test_nak_cast(self): nak_pdu = NakPdu(start_of_scope=0, end_of_scope=200, pdu_conf=self.pdu_conf) self.pdu_wrapper.base = nak_pdu + nak_pdu_converted = self.pdu_wrapper.to_nak_pdu() + self.assertEqual(nak_pdu_converted, nak_pdu) + + def test_prompt_cast(self): + prompt_pdu = PromptPdu( + pdu_conf=self.pdu_conf, response_required=ResponseRequired.KEEP_ALIVE + ) + self.pdu_wrapper.base = prompt_pdu + prompt_pdu_converted = self.pdu_wrapper.to_prompt_pdu() + self.assertEqual(prompt_pdu_converted, prompt_pdu) + + def test_eof_cast(self): + zero_checksum = bytes([0x00, 0x00, 0x00, 0x00]) + eof_pdu = EofPdu( + file_checksum=zero_checksum, file_size=0, pdu_conf=self.pdu_conf + ) + self.pdu_wrapper.base = eof_pdu + eof_pdu_converted = self.pdu_wrapper.to_eof_pdu() + self.assertEqual(eof_pdu_converted, eof_pdu) + + def test_finished_cast(self): + finish_pdu = FinishedPdu( + delivery_code=DeliveryCode.DATA_COMPLETE, + file_delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, + condition_code=ConditionCode.NO_ERROR, + pdu_conf=self.pdu_conf, + ) + self.pdu_wrapper.base = finish_pdu + finish_pdu_converted = self.pdu_wrapper.to_finished_pdu() + self.assertEqual(finish_pdu_converted, finish_pdu) diff --git a/tests/cfdp/pdus/test_prompt_pdu.py b/tests/cfdp/pdus/test_prompt_pdu.py index 6aa79fb..66677f4 100644 --- a/tests/cfdp/pdus/test_prompt_pdu.py +++ b/tests/cfdp/pdus/test_prompt_pdu.py @@ -9,7 +9,7 @@ class TestPromptPdu(TestCase): def test_prompt_pdu(self): pdu_conf = PduConfig.empty() prompt_pdu = PromptPdu( - pdu_conf=pdu_conf, reponse_required=ResponseRequired.KEEP_ALIVE + pdu_conf=pdu_conf, response_required=ResponseRequired.KEEP_ALIVE ) print(prompt_pdu.pack().hex(sep=",")) prompt_pdu_raw = prompt_pdu.pack() diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index df77e9c..93840f3 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -132,7 +132,7 @@ def test_pdu_header(self): pdu_conf.dest_entity_id = bytes([0]) pdu_conf.transaction_seq_num = bytes([0x00, 0x2C]) prompt_pdu = PromptPdu( - reponse_required=ResponseRequired.KEEP_ALIVE, pdu_conf=pdu_conf + response_required=ResponseRequired.KEEP_ALIVE, pdu_conf=pdu_conf ) self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 9) self.assertEqual(prompt_pdu.packet_len, 10) From 0e79a25a3d2b430ad61a7cf145ececf2f93f4a5f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 17:29:44 +0200 Subject: [PATCH 09/46] wrapper tests done --- spacepackets/cfdp/pdu/keep_alive.py | 6 ++++++ tests/cfdp/pdus/test_pdu_wrapper.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/spacepackets/cfdp/pdu/keep_alive.py b/spacepackets/cfdp/pdu/keep_alive.py index bd747b6..caf3443 100644 --- a/spacepackets/cfdp/pdu/keep_alive.py +++ b/spacepackets/cfdp/pdu/keep_alive.py @@ -86,3 +86,9 @@ def unpack(cls, raw_packet: bytearray) -> KeepAlivePdu: @property def packet_len(self): return self.pdu_file_directive.packet_len + + def __eq__(self, other: KeepAlivePdu): + return ( + self.pdu_file_directive == other.pdu_file_directive + and self.progress == other.progress + ) diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index 7604bdc..3b1064a 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -11,6 +11,7 @@ PromptPdu, EofPdu, FinishedPdu, + KeepAlivePdu, ) from spacepackets.cfdp.pdu.file_data import FileDataPdu from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus @@ -121,3 +122,9 @@ def test_finished_cast(self): self.pdu_wrapper.base = finish_pdu finish_pdu_converted = self.pdu_wrapper.to_finished_pdu() self.assertEqual(finish_pdu_converted, finish_pdu) + + def test_keep_alive_cast(self): + keep_alive_pdu = KeepAlivePdu(progress=0, pdu_conf=self.pdu_conf) + self.pdu_wrapper.base = keep_alive_pdu + keep_alive_pdu_converted = self.pdu_wrapper.to_keep_alive_pdu() + self.assertEqual(keep_alive_pdu_converted, keep_alive_pdu) From f8e7619ccbfbaa434f0ebd6b1a7edd00621b28aa Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Jul 2022 20:08:25 +0200 Subject: [PATCH 10/46] additional re-export, comment --- spacepackets/cfdp/defs.py | 2 ++ spacepackets/cfdp/pdu/__init__.py | 1 + 2 files changed, 3 insertions(+) diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index 23d39bc..8725861 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -8,6 +8,8 @@ class PduType(enum.IntEnum): class Direction(enum.IntEnum): + """This is used for PDU forwarding""" + TOWARDS_RECEIVER = 0 TOWARDS_SENDER = 1 diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 64ba1ae..2e26b28 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -15,3 +15,4 @@ from .metadata import MetadataPdu from .nak import NakPdu from .prompt import PromptPdu +from .wrapper import PduWrapper From 68e4824bcf717c370c8f7113a72cf6e311f1ba6d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 7 Jul 2022 14:18:42 +0200 Subject: [PATCH 11/46] additional comment --- spacepackets/cfdp/defs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index 8725861..50ec6ab 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -107,5 +107,6 @@ class ChecksumTypes(enum.IntEnum): MODULAR = 0 CRC_32_PROXIMITY_1 = 1 CRC_32C = 2 + # Polynomial: 0x4C11DB7. This is the preferred checksum for now. CRC_32 = 3 NULL_CHECKSUM = 15 From 3618d98a43fe9e4b76948f221e6f2142dfc8d225 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 7 Jul 2022 17:30:14 +0200 Subject: [PATCH 12/46] refactored variable byte field handling --- spacepackets/cfdp/conf.py | 33 ++---- spacepackets/cfdp/defs.py | 140 ++++++++++++++++++++---- spacepackets/cfdp/pdu/file_data.py | 5 +- spacepackets/cfdp/pdu/file_directive.py | 6 +- spacepackets/cfdp/pdu/header.py | 110 +++++++------------ spacepackets/ecss/fields.py | 37 +------ spacepackets/util.py | 51 +++++++++ tests/cfdp/pdus/test_ack_pdu.py | 37 ++++--- tests/cfdp/pdus/test_nak_pdu.py | 8 +- tests/cfdp/test_cfdp.py | 6 - tests/cfdp/test_header.py | 82 +++++--------- tests/test_ecss.py | 13 +-- 12 files changed, 286 insertions(+), 242 deletions(-) diff --git a/spacepackets/cfdp/conf.py b/spacepackets/cfdp/conf.py index 89a42ea..251124a 100644 --- a/spacepackets/cfdp/conf.py +++ b/spacepackets/cfdp/conf.py @@ -8,6 +8,8 @@ CrcFlag, Direction, SegmentationControl, + UnsignedByteField, + ByteFieldU8, ) from spacepackets.log import get_console_logger @@ -21,54 +23,39 @@ class PduConfig: specifying parameter which rarely change repeatedly """ - transaction_seq_num: bytes + source_entity_id: UnsignedByteField + dest_entity_id: UnsignedByteField + transaction_seq_num: UnsignedByteField trans_mode: TransmissionModes file_flag: LargeFileFlag = LargeFileFlag.NORMAL - crc_flag: CrcFlag = CrcFlag.GLOBAL_CONFIG + crc_flag: CrcFlag = CrcFlag.NO_CRC direction: Direction = Direction.TOWARDS_RECEIVER seg_ctrl: SegmentationControl = ( SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION ) - source_entity_id: bytes = bytes() - dest_entity_id: bytes = bytes() @classmethod def empty(cls) -> PduConfig: return PduConfig( - transaction_seq_num=bytes([0]), + transaction_seq_num=ByteFieldU8(0), trans_mode=TransmissionModes.ACKNOWLEDGED, - source_entity_id=bytes([0]), - dest_entity_id=bytes([0]), + source_entity_id=ByteFieldU8(0), + dest_entity_id=ByteFieldU8(0), file_flag=LargeFileFlag.NORMAL, - crc_flag=CrcFlag.GLOBAL_CONFIG, + crc_flag=CrcFlag.NO_CRC, ) - def __post_init__(self): - """Ensure that the global configuration is converted to the actual value immediately""" - if self.crc_flag == CrcFlag.GLOBAL_CONFIG: - self.crc_flag = get_default_pdu_crc_mode() - class CfdpDict(TypedDict): source_dest_entity_ids: Tuple[bytes, bytes] - with_crc: CrcFlag # TODO: Protect dict access with a dedicated lock for thread-safety __CFDP_DICT: CfdpDict = { "source_dest_entity_ids": (bytes(), bytes()), - "with_crc": CrcFlag.NO_CRC, } -def set_default_pdu_crc_mode(with_crc: CrcFlag): - __CFDP_DICT["with_crc"] = with_crc - - -def get_default_pdu_crc_mode() -> CrcFlag: - return __CFDP_DICT["with_crc"] - - def set_entity_ids(source_entity_id: bytes, dest_entity_id: bytes): __CFDP_DICT["source_dest_entity_ids"] = (source_entity_id, dest_entity_id) diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index 50ec6ab..b35fc57 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -1,6 +1,9 @@ +from __future__ import annotations import enum import struct +from spacepackets.util import IntByteConversion + class PduType(enum.IntEnum): FILE_DIRECTIVE = 0 @@ -22,7 +25,6 @@ class TransmissionModes(enum.IntEnum): class CrcFlag(enum.IntEnum): NO_CRC = 0 WITH_CRC = 1 - GLOBAL_CONFIG = 2 class SegmentMetadataFlag(enum.IntEnum): @@ -47,12 +49,11 @@ class FaultHandlerCodes(enum.IntEnum): class LenInBytes(enum.IntEnum): + ZERO_OR_NONE = 0 ONE_BYTE = 1 TWO_BYTES = 2 FOUR_BYTES = 4 EIGHT_BYTES = 8 - GLOBAL = 90 - NONE = 99 class ConditionCode(enum.IntEnum): @@ -72,26 +73,6 @@ class ConditionCode(enum.IntEnum): CANCEL_REQUEST_RECEIVED = 0b1111 -def get_transaction_seq_num_as_bytes( - transaction_seq_num: int, byte_length: LenInBytes -) -> bytearray: - """Return the byte representation of the transaction sequece number - :param transaction_seq_num: - :param byte_length: - :raises ValueError: Invalid input - :return: - """ - if byte_length == LenInBytes.ONE_BYTE and transaction_seq_num < 255: - return bytearray([transaction_seq_num]) - if byte_length == LenInBytes.TWO_BYTES and transaction_seq_num < pow(2, 16) - 1: - return bytearray(struct.pack("!H", transaction_seq_num)) - if byte_length == LenInBytes.FOUR_BYTES and transaction_seq_num < pow(2, 32) - 1: - return bytearray(struct.pack("!I", transaction_seq_num)) - if byte_length == LenInBytes.EIGHT_BYTES and transaction_seq_num < pow(2, 64) - 1: - return bytearray(struct.pack("!Q", transaction_seq_num)) - raise ValueError - - # File sizes, determine the field sizes of FSS fields class LargeFileFlag(enum.IntEnum): # 32 bit maximum file size and FSS size @@ -110,3 +91,116 @@ class ChecksumTypes(enum.IntEnum): # Polynomial: 0x4C11DB7. This is the preferred checksum for now. CRC_32 = 3 NULL_CHECKSUM = 15 + + +class UnsignedByteField: + def __init__(self, val: int, byte_len: int): + self.byte_len = byte_len + self.value = val + + @property + def byte_len(self): + return self._byte_len + + @byte_len.setter + def byte_len(self, byte_len: int): + if byte_len not in [1, 2, 4, 8]: + # I really have no idea why anyone would use other values than these + raise ValueError( + "Only 1, 2, 4 and 8 bytes are allowed as an entity ID length" + ) + self._byte_len = byte_len + + @property + def value(self): + return self._val + + @value.setter + def value(self, val: int): + if val > pow(2, self.byte_len * 8) - 1: + raise ValueError( + f"Passed value larger than allowed {pow(2, self.byte_len * 8) - 1}" + ) + self._val = val + + def as_bytes(self) -> bytes: + return IntByteConversion.to_unsigned(self.byte_len, self.value) + + def __repr__(self): + return ( + f"{self.__class__.__name__}(entity_id={self.value!r}, " + f"byte_len={self.byte_len!r})" + ) + + def __int__(self): + return self.value + + def __eq__(self, other: UnsignedByteField): + return self.value == other.value and self.byte_len == other.byte_len + + def __hash__(self): + return hash((self.value, self.byte_len)) + + +class ByteFieldU8(UnsignedByteField): + """Concrete variant of a variable length byte field which has 1 byte""" + + def __init__(self, val: int): + super().__init__(val, 1) + + @classmethod + def from_bytes(cls, stream: bytes) -> ByteFieldU8: + if len(stream) < 1: + raise ValueError( + "Passed stream not large enough, should be at least 1 byte" + ) + return cls(stream[0]) + + +class ByteFieldU16(UnsignedByteField): + """Concrete variant of a variable length byte field which has 2 bytes""" + + def __init__(self, val: int): + super().__init__(val, 2) + + @classmethod + def from_bytes(cls, stream: bytes) -> ByteFieldU16: + if len(stream) < 2: + raise ValueError( + "Passed stream not large enough, should be at least 2 byte" + ) + return cls( + struct.unpack(IntByteConversion.unsigned_struct_specifier(2), stream[0:2])[ + 0 + ] + ) + + +class ByteFieldU32(UnsignedByteField): + """Concrete variant of a variable length byte field which has 4 bytes""" + + def __init__(self, val: int): + super().__init__(val, 4) + + @classmethod + def from_bytes(cls, stream: bytes) -> ByteFieldU32: + if len(stream) < 4: + raise ValueError( + "Passed stream not large enough, should be at least 4 byte" + ) + return cls( + struct.unpack(IntByteConversion.unsigned_struct_specifier(4), stream[0:4])[ + 0 + ] + ) + + +class ByteFieldGenerator: + @staticmethod + def from_bytes(byte_len: int, stream: bytes) -> UnsignedByteField: + if byte_len == 1: + return ByteFieldU8.from_bytes(stream) + elif byte_len == 2: + return ByteFieldU16.from_bytes(stream) + elif byte_len == 4: + return ByteFieldU32.from_bytes(stream) diff --git a/spacepackets/cfdp/pdu/file_data.py b/spacepackets/cfdp/pdu/file_data.py index 490e122..cccf0e1 100644 --- a/spacepackets/cfdp/pdu/file_data.py +++ b/spacepackets/cfdp/pdu/file_data.py @@ -4,6 +4,7 @@ import struct from spacepackets.cfdp import LargeFileFlag +from spacepackets.cfdp.defs import UnsignedByteField from spacepackets.cfdp.pdu.file_directive import SegmentMetadataFlag, PduType from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.pdu.header import PduHeader, AbstractPduBase @@ -80,11 +81,11 @@ def file_flag(self) -> LargeFileFlag: return self.pdu_header.file_flag @property - def source_entity_id(self) -> bytes: + def source_entity_id(self) -> UnsignedByteField: return self.pdu_header.source_entity_id @property - def dest_entity_id(self) -> bytes: + def dest_entity_id(self) -> UnsignedByteField: return self.pdu_header.dest_entity_id @property diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index a092d01..494f056 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -10,7 +10,7 @@ SegmentMetadataFlag, AbstractPduBase, ) -from spacepackets.cfdp.defs import LargeFileFlag, CrcFlag +from spacepackets.cfdp.defs import LargeFileFlag, CrcFlag, UnsignedByteField from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.log import get_console_logger @@ -66,11 +66,11 @@ def pdu_data_field_len(self): return self.pdu_header.pdu_data_field_len @property - def source_entity_id(self) -> bytes: + def source_entity_id(self) -> UnsignedByteField: return self.pdu_header.source_entity_id @property - def dest_entity_id(self) -> bytes: + def dest_entity_id(self) -> UnsignedByteField: return self.pdu_header.dest_entity_id @pdu_data_field_len.setter diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 2cb327e..38c4008 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -4,7 +4,6 @@ from spacepackets.log import get_console_logger from spacepackets.cfdp.defs import ( - LenInBytes, LargeFileFlag, PduType, SegmentMetadataFlag, @@ -12,11 +11,12 @@ TransmissionModes, Direction, SegmentationControl, + UnsignedByteField, + LenInBytes, + ByteFieldGenerator, ) from spacepackets.cfdp.conf import ( PduConfig, - get_default_pdu_crc_mode, - get_entity_ids, ) @@ -55,12 +55,12 @@ def header_len(self) -> int: @property @abc.abstractmethod - def source_entity_id(self) -> bytes: + def source_entity_id(self) -> UnsignedByteField: pass @property @abc.abstractmethod - def dest_entity_id(self) -> bytes: + def dest_entity_id(self) -> UnsignedByteField: pass @property @@ -127,13 +127,10 @@ def __init__( self.pdu_conf = pdu_conf self.pdu_data_field_len = pdu_data_field_len - self.len_entity_id = 0 self.set_entity_ids( source_entity_id=pdu_conf.source_entity_id, dest_entity_id=pdu_conf.dest_entity_id, ) - - self.len_transaction_seq_num = 0 self.transaction_seq_num = pdu_conf.transaction_seq_num self.segment_metadata_flag = segment_metadata_flag @@ -153,50 +150,25 @@ def source_entity_id(self): def dest_entity_id(self): return self.pdu_conf.dest_entity_id - def set_entity_ids(self, source_entity_id: bytes, dest_entity_id: bytes): + def set_entity_ids( + self, source_entity_id: UnsignedByteField, dest_entity_id: UnsignedByteField + ): """Both IDs must be set at once because they must have the same length as well :param source_entity_id: :param dest_entity_id: :return: """ - if source_entity_id == bytes() or dest_entity_id == bytes(): - source_entity_id, dest_entity_id = get_entity_ids() - if source_entity_id == bytes() or dest_entity_id == bytes(): - logger = get_console_logger() - logger.warning( - "Can not set default value for source entity ID or destination entity ID " - "because it has not been set yet" - ) - raise ValueError + if source_entity_id.byte_len != dest_entity_id.byte_len: + raise ValueError("Length of destination ID and source ID are not the same") self.pdu_conf.source_entity_id = source_entity_id self.pdu_conf.dest_entity_id = dest_entity_id - try: - self.len_entity_id = self.check_len_in_bytes(len(source_entity_id)) - dest_id_check = self.check_len_in_bytes(len(dest_entity_id)) - except ValueError: - logger = get_console_logger() - logger.warning("Invalid length of entity IDs passed") - raise ValueError - if dest_id_check != self.len_entity_id: - logger = get_console_logger() - logger.warning("Length of destination ID and source ID are not the same") - raise ValueError @property def transaction_seq_num(self): return self.pdu_conf.transaction_seq_num @transaction_seq_num.setter - def transaction_seq_num(self, transaction_seq_num: bytes): - if transaction_seq_num is not None: - try: - self.len_transaction_seq_num = self.check_len_in_bytes( - len(transaction_seq_num) - ) - except ValueError: - logger = get_console_logger() - logger.warning("Invalid length of transaction sequence number passed") - raise ValueError + def transaction_seq_num(self, transaction_seq_num: UnsignedByteField): self.pdu_conf.transaction_seq_num = transaction_seq_num @property @@ -213,10 +185,7 @@ def crc_flag(self): @crc_flag.setter def crc_flag(self, crc_flag: CrcFlag): - if crc_flag == CrcFlag.GLOBAL_CONFIG: - self.pdu_conf.crc_flag = get_default_pdu_crc_mode() - else: - self.pdu_conf.crc_flag = crc_flag + self.pdu_conf.crc_flag = crc_flag @property def trans_mode(self): @@ -260,7 +229,11 @@ def pdu_data_field_len(self, new_len: int): @property def header_len(self) -> int: """Get length of PDU header when packing it""" - return self.FIXED_LENGTH + 2 * self.len_entity_id + self.len_transaction_seq_num + return ( + self.FIXED_LENGTH + + 2 * self.source_entity_id.byte_len + + self.transaction_seq_num.byte_len + ) def pack(self) -> bytearray: header = bytearray() @@ -276,13 +249,13 @@ def pack(self) -> bytearray: header.append(self.pdu_data_field_len & 0xFF) header.append( self.pdu_conf.seg_ctrl << 7 - | self.len_entity_id << 4 + | self.source_entity_id.byte_len << 4 | self.segment_metadata_flag << 3 - | self.len_transaction_seq_num + | self.transaction_seq_num.byte_len ) - header.extend(self.pdu_conf.source_entity_id) - header.extend(self.pdu_conf.transaction_seq_num) - header.extend(self.pdu_conf.dest_entity_id) + header.extend(self.pdu_conf.source_entity_id.as_bytes()) + header.extend(self.pdu_conf.transaction_seq_num.as_bytes()) + header.extend(self.pdu_conf.dest_entity_id.as_bytes()) return header @classmethod @@ -317,32 +290,29 @@ def unpack(cls, raw_packet: bytes) -> PduHeader: pdu_header.segmentation_control = SegmentationControl( (raw_packet[3] & 0x80) >> 7 ) - pdu_header.len_entity_id = cls.check_len_in_bytes((raw_packet[3] & 0x70) >> 4) + expected_len_entity_ids = cls.check_len_in_bytes((raw_packet[3] & 0x70) >> 4) pdu_header.segment_metadata_flag = SegmentMetadataFlag( (raw_packet[3] & 0x08) >> 3 ) - pdu_header.len_transaction_seq_num = cls.check_len_in_bytes( - raw_packet[3] & 0x07 - ) - expected_remaining_len = ( - 2 * pdu_header.len_entity_id + pdu_header.len_transaction_seq_num - ) + expected_len_seq_num = cls.check_len_in_bytes(raw_packet[3] & 0x07) + expected_remaining_len = 2 * expected_len_entity_ids + expected_len_seq_num if len(raw_packet) - cls.FIXED_LENGTH < expected_remaining_len: - logger = get_console_logger() - logger.warning("Raw packet too small for PDU header") - raise ValueError + raise ValueError("Raw packet too small for PDU header") current_idx = 4 - source_entity_id = raw_packet[ - current_idx : current_idx + pdu_header.len_entity_id - ] - current_idx += pdu_header.len_entity_id - pdu_header.transaction_seq_num = raw_packet[ - current_idx : current_idx + pdu_header.len_transaction_seq_num - ] - current_idx += pdu_header.len_transaction_seq_num - dest_entity_id = raw_packet[ - current_idx : current_idx + pdu_header.len_entity_id - ] + source_entity_id = ByteFieldGenerator.from_bytes( + expected_len_entity_ids, + raw_packet[current_idx : current_idx + expected_len_entity_ids], + ) + current_idx += expected_len_entity_ids + pdu_header.transaction_seq_num = ByteFieldGenerator.from_bytes( + expected_len_seq_num, + raw_packet[current_idx : current_idx + expected_len_seq_num], + ) + current_idx += expected_len_seq_num + dest_entity_id = ByteFieldGenerator.from_bytes( + expected_len_entity_ids, + raw_packet[current_idx : current_idx + expected_len_entity_ids], + ) pdu_header.set_entity_ids( source_entity_id=source_entity_id, dest_entity_id=dest_entity_id ) diff --git a/spacepackets/ecss/fields.py b/spacepackets/ecss/fields.py index f97ee3d..d8bc512 100644 --- a/spacepackets/ecss/fields.py +++ b/spacepackets/ecss/fields.py @@ -3,6 +3,8 @@ import struct from dataclasses import dataclass +from spacepackets.util import IntByteConversion + class Ptc(enum.IntEnum): BOOLEAN = 1 @@ -88,9 +90,7 @@ def with_byte_size(cls, num_bytes: int, val: int): def pack(self) -> bytearray: num_bytes = self.check_pfc(self.pfc) - return bytearray( - struct.pack(byte_num_to_unsigned_struct_specifier(num_bytes), self.val) - ) + return bytearray(IntByteConversion.to_unsigned(num_bytes, self.val)) def len(self): """Return the length in bytes. This will raise a ValueError for non-byte-aligned @@ -103,7 +103,8 @@ def unpack(cls, data: bytes, pfc: int): return cls( pfc, struct.unpack( - byte_num_to_unsigned_struct_specifier(num_bytes), data[0:num_bytes] + IntByteConversion.unsigned_struct_specifier(num_bytes), + data[0:num_bytes], )[0], ) @@ -122,31 +123,3 @@ def __repr__(self): def __eq__(self, other: PacketFieldEnum): return self.pfc == other.pfc and self.val == other.val - - -def byte_num_to_signed_struct_specifier(byte_num: int) -> str: - """Convert number of bytes in a field to the struct API signed format specifier, - assuming network endianness. Raises value error if number is not inside [1, 2, 4, 8]""" - if byte_num == 1: - return "!b" - elif byte_num == 2: - return "!h" - elif byte_num == 4: - return "!i" - elif byte_num == 8: - return "!q" - raise ValueError("Invalid number of bytes specified") - - -def byte_num_to_unsigned_struct_specifier(byte_num: int) -> str: - """Convert number of bytes in a field to the struct API unsigned format specifier, - assuming network endianness. Raises value error if number is not inside [1, 2, 4, 8]""" - if byte_num == 1: - return "!B" - elif byte_num == 2: - return "!H" - elif byte_num == 4: - return "!I" - elif byte_num == 8: - return "!Q" - raise ValueError("Invalid number of bytes specified") diff --git a/spacepackets/util.py b/spacepackets/util.py index 3c0d33e..569bf13 100644 --- a/spacepackets/util.py +++ b/spacepackets/util.py @@ -1,4 +1,5 @@ import enum +import struct class PrintFormats(enum.IntEnum): @@ -40,3 +41,53 @@ def get_printable_data_string( string_to_print += f"{idx}:{data_to_print[idx]:08b}\n" string_to_print += f"]" return string_to_print + + +class IntByteConversion: + @staticmethod + def signed_struct_specifier(byte_num: int) -> str: + if byte_num not in [1, 2, 4, 8]: + raise ValueError("Invalid byte number, must be one of [1, 2, 4, 8]") + if byte_num == 1: + return "!b" + elif byte_num == 2: + return "!h" + elif byte_num == 4: + return "!i" + elif byte_num == 8: + return "!q" + + @staticmethod + def unsigned_struct_specifier(byte_num: int) -> str: + if byte_num not in [1, 2, 4, 8]: + raise ValueError("Invalid byte number, must be one of [1, 2, 4, 8]") + if byte_num == 1: + return "!B" + elif byte_num == 2: + return "!H" + elif byte_num == 4: + return "!I" + elif byte_num == 8: + return "!Q" + + @staticmethod + def to_signed(byte_num: int, val: int) -> bytes: + """Convert number of bytes in a field to the struct API signed format specifier, + assuming network endianness. Raises value error if number is not inside [1, 2, 4, 8]""" + if byte_num not in [1, 2, 4, 8]: + raise ValueError("Invalid byte number, must be one of [1, 2, 4, 8]") + if abs(val) > pow(2, (byte_num * 8) - 1) - 1: + raise ValueError( + f"Passed value larger than allows {pow(2, (byte_num * 8) - 1) - 1}" + ) + return struct.pack(IntByteConversion.signed_struct_specifier(byte_num), val) + + @staticmethod + def to_unsigned(byte_num: int, val: int) -> bytes: + """Convert number of bytes in a field to the struct API unsigned format specifier, + assuming network endianness. Raises value error if number is not inside [1, 2, 4, 8]""" + if byte_num not in [1, 2, 4, 8]: + raise ValueError("Invalid byte number, must be one of [1, 2, 4, 8]") + if val > pow(2, byte_num * 8) - 1: + raise ValueError(f"Passed value larger than allows {pow(2, byte_num) - 1}") + return struct.pack(IntByteConversion.unsigned_struct_specifier(byte_num), val) diff --git a/tests/cfdp/pdus/test_ack_pdu.py b/tests/cfdp/pdus/test_ack_pdu.py index 350431d..8ec5ede 100644 --- a/tests/cfdp/pdus/test_ack_pdu.py +++ b/tests/cfdp/pdus/test_ack_pdu.py @@ -2,15 +2,16 @@ from spacepackets.cfdp import CrcFlag, TransmissionModes, LargeFileFlag, ConditionCode from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.defs import ByteFieldU16, ByteFieldU32 from spacepackets.cfdp.pdu import AckPdu, DirectiveType, TransactionStatus class TestAckPdu(TestCase): def test_ack_pdu(self): pdu_conf = PduConfig( - transaction_seq_num=bytes([0x00, 0x01]), - source_entity_id=bytes([0x00, 0x00]), - dest_entity_id=bytes([0x00, 0x01]), + transaction_seq_num=ByteFieldU16(1), + source_entity_id=ByteFieldU16(2), + dest_entity_id=ByteFieldU16(3), crc_flag=CrcFlag.NO_CRC, trans_mode=TransmissionModes.ACKNOWLEDGED, file_flag=LargeFileFlag.NORMAL, @@ -36,12 +37,12 @@ def test_ack_pdu(self): 0x00, 0x03, 0x22, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, + 0x00, # This and following byte in source ID + 0x02, + 0x00, # This and following byte is seq number 0x01, + 0x00, # This and following byte in dest ID + 0x03, 0x06, 0x51, 0x02, @@ -52,9 +53,11 @@ def test_ack_pdu(self): self.check_fields_packet_0(ack_pdu=ack_pdu_unpacked) pdu_conf = PduConfig( - transaction_seq_num=bytes([0x50, 0x00, 0x10, 0x01]), - source_entity_id=bytes([0x10, 0x00, 0x01, 0x02]), - dest_entity_id=bytes([0x30, 0x00, 0x01, 0x03]), + transaction_seq_num=ByteFieldU32.from_bytes( + bytes([0x50, 0x00, 0x10, 0x01]) + ), + source_entity_id=ByteFieldU32.from_bytes(bytes([0x10, 0x00, 0x01, 0x02])), + dest_entity_id=ByteFieldU32.from_bytes(bytes([0x30, 0x00, 0x01, 0x03])), crc_flag=CrcFlag.WITH_CRC, trans_mode=TransmissionModes.UNACKNOWLEDGED, file_flag=LargeFileFlag.NORMAL, @@ -118,15 +121,15 @@ def check_fields_packet_0(self, ack_pdu: AckPdu): self.assertEqual(ack_pdu.transaction_status, TransactionStatus.TERMINATED) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.transaction_seq_num, - bytes([0x00, 0x01]), + ByteFieldU16(1), ) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.source_entity_id, - bytes([0x00, 0x00]), + ByteFieldU16(2), ) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.dest_entity_id, - bytes([0x00, 0x01]), + ByteFieldU16(3), ) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.trans_mode, @@ -143,15 +146,15 @@ def check_fields_packet_1(self, ack_pdu: AckPdu): self.assertEqual(ack_pdu.transaction_status, TransactionStatus.ACTIVE) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.transaction_seq_num, - bytes([0x50, 0x00, 0x10, 0x01]), + ByteFieldU32.from_bytes(bytes([0x50, 0x00, 0x10, 0x01])), ) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.source_entity_id, - bytes([0x10, 0x00, 0x01, 0x02]), + ByteFieldU32.from_bytes(bytes([0x10, 0x00, 0x01, 0x02])), ) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.dest_entity_id, - bytes([0x30, 0x00, 0x01, 0x03]), + ByteFieldU32.from_bytes(bytes([0x30, 0x00, 0x01, 0x03])), ) self.assertEqual( ack_pdu.pdu_file_directive.pdu_header.pdu_conf.trans_mode, diff --git a/tests/cfdp/pdus/test_nak_pdu.py b/tests/cfdp/pdus/test_nak_pdu.py index 32c44f7..c1517f7 100644 --- a/tests/cfdp/pdus/test_nak_pdu.py +++ b/tests/cfdp/pdus/test_nak_pdu.py @@ -2,7 +2,7 @@ from spacepackets.cfdp import TransmissionModes from spacepackets.cfdp.conf import PduConfig -from spacepackets.cfdp.defs import Direction, LargeFileFlag +from spacepackets.cfdp.defs import Direction, LargeFileFlag, ByteFieldU16 from spacepackets.cfdp.pdu import NakPdu @@ -10,9 +10,9 @@ class TestNakPdu(TestCase): def test_nak_pdu(self): pdu_conf = PduConfig( trans_mode=TransmissionModes.ACKNOWLEDGED, - transaction_seq_num=bytes([0x00, 0x01]), - source_entity_id=bytes([0x00, 0x00]), - dest_entity_id=bytes([0x00, 0x01]), + transaction_seq_num=ByteFieldU16(1), + source_entity_id=ByteFieldU16(0), + dest_entity_id=ByteFieldU16(1), ) nak_pdu = NakPdu(start_of_scope=0, end_of_scope=200, pdu_conf=pdu_conf) self.assertEqual(nak_pdu.segment_requests, []) diff --git a/tests/cfdp/test_cfdp.py b/tests/cfdp/test_cfdp.py index 26a6602..f2d6d8c 100644 --- a/tests/cfdp/test_cfdp.py +++ b/tests/cfdp/test_cfdp.py @@ -2,8 +2,6 @@ from spacepackets.cfdp.conf import ( CrcFlag, - set_default_pdu_crc_mode, - get_default_pdu_crc_mode, set_entity_ids, get_entity_ids, ) @@ -11,9 +9,5 @@ class TestConfig(TestCase): def test_config(self): - set_default_pdu_crc_mode(CrcFlag.WITH_CRC) - self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.WITH_CRC) - set_default_pdu_crc_mode(CrcFlag.NO_CRC) - self.assertEqual(get_default_pdu_crc_mode(), CrcFlag.NO_CRC) set_entity_ids(bytes([0x00, 0x01]), bytes([0x02, 0x03])) self.assertEqual(get_entity_ids(), (bytes([0x00, 0x01]), bytes([0x02, 0x03]))) diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index 93840f3..c85c1e6 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -3,7 +3,6 @@ from spacepackets.cfdp.conf import PduConfig, set_entity_ids from spacepackets.cfdp.defs import ( - get_transaction_seq_num_as_bytes, LenInBytes, TransmissionModes, Direction, @@ -12,6 +11,9 @@ PduType, SegmentMetadataFlag, LargeFileFlag, + ByteFieldU8, + ByteFieldU16, + ByteFieldU32, ) from spacepackets.cfdp.pdu import PduHeader, PromptPdu from spacepackets.cfdp.pdu.prompt import ResponseRequired @@ -21,33 +23,22 @@ class TestHeader(TestCase): # TODO: Split up in smaller test fixtures def test_pdu_header(self): - len_in_bytes = get_transaction_seq_num_as_bytes( - transaction_seq_num=22, byte_length=LenInBytes.ONE_BYTE - ) - self.assertEqual(len_in_bytes[0], 22) - len_in_bytes = get_transaction_seq_num_as_bytes( - transaction_seq_num=5292, byte_length=LenInBytes.TWO_BYTES - ) - self.assertEqual(len_in_bytes[0] << 8 | len_in_bytes[1], 5292) - len_in_bytes = get_transaction_seq_num_as_bytes( - transaction_seq_num=129302, byte_length=LenInBytes.FOUR_BYTES - ) - self.assertEqual(struct.unpack("!I", len_in_bytes[:])[0], 129302) - len_in_bytes = get_transaction_seq_num_as_bytes( - transaction_seq_num=8292392392, byte_length=LenInBytes.EIGHT_BYTES - ) - self.assertEqual(struct.unpack("!Q", len_in_bytes[:])[0], 8292392392) - self.assertRaises( - ValueError, get_transaction_seq_num_as_bytes, 900, LenInBytes.ONE_BYTE - ) + byte_field = ByteFieldU8(22) + self.assertEqual(int(byte_field), 22) + byte_field = ByteFieldU16(5292) + self.assertEqual(int(byte_field), 5292) + byte_field = ByteFieldU32(129302) + self.assertEqual(struct.unpack("!I", byte_field.as_bytes())[0], 129302) + with self.assertRaises(ValueError): + ByteFieldU8(900) pdu_conf = PduConfig( - source_entity_id=bytes([0]), - dest_entity_id=bytes([0]), + source_entity_id=ByteFieldU8(0), + dest_entity_id=ByteFieldU8(0), trans_mode=TransmissionModes.ACKNOWLEDGED, direction=Direction.TOWARDS_RECEIVER, crc_flag=CrcFlag.NO_CRC, seg_ctrl=SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION, - transaction_seq_num=bytes([0]), + transaction_seq_num=ByteFieldU8(0), ) pdu_header = PduHeader( pdu_type=PduType.FILE_DIRECTIVE, @@ -56,18 +47,16 @@ def test_pdu_header(self): pdu_conf=pdu_conf, ) self.assertEqual(pdu_header.pdu_type, PduType.FILE_DIRECTIVE) - self.assertEqual(pdu_header.source_entity_id, bytes([0])) - self.assertEqual(pdu_header.len_entity_id, 1) + self.assertEqual(pdu_header.source_entity_id, ByteFieldU8(0)) + self.assertEqual(pdu_header.source_entity_id.byte_len, 1) self.assertEqual(pdu_header.trans_mode, TransmissionModes.ACKNOWLEDGED) self.assertEqual(pdu_header.direction, Direction.TOWARDS_RECEIVER) self.assertEqual( pdu_header.segment_metadata_flag, SegmentMetadataFlag.NOT_PRESENT ) self.assertFalse(pdu_header.large_file_flag_set) - self.assertEqual(pdu_header.transaction_seq_num, bytes([0])) - self.assertEqual(pdu_header.len_transaction_seq_num, 1) - self.assertEqual(pdu_header.crc_flag, CrcFlag.NO_CRC) - pdu_header.crc_flag = CrcFlag.GLOBAL_CONFIG + self.assertEqual(pdu_header.transaction_seq_num, ByteFieldU8(0)) + self.assertEqual(pdu_header.transaction_seq_num.byte_len, 1) self.assertEqual(pdu_header.crc_flag, CrcFlag.NO_CRC) self.assertEqual( pdu_header.seg_ctrl, SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION @@ -85,11 +74,9 @@ def test_pdu_header(self): pdu_header.pdu_type = PduType.FILE_DATA pdu_header.set_entity_ids( - source_entity_id=bytes([0, 0]), dest_entity_id=bytes([0, 1]) - ) - pdu_header.transaction_seq_num = get_transaction_seq_num_as_bytes( - 300, byte_length=LenInBytes.TWO_BYTES + source_entity_id=ByteFieldU16(0), dest_entity_id=ByteFieldU16(1) ) + pdu_header.transaction_seq_num = ByteFieldU16(300) pdu_header.trans_mode = TransmissionModes.UNACKNOWLEDGED pdu_header.direction = Direction.TOWARDS_SENDER pdu_header.crc_flag = CrcFlag.WITH_CRC @@ -102,43 +89,30 @@ def test_pdu_header(self): pdu_header_packed = pdu_header.pack() self.check_fields_case_two(pdu_header_packed=pdu_header_packed) set_entity_ids(source_entity_id=bytes(), dest_entity_id=bytes()) - self.assertRaises(ValueError, pdu_header.set_entity_ids, bytes(), bytes()) - self.assertRaises( - ValueError, pdu_header.set_entity_ids, bytes([0, 1, 2]), bytes() - ) - self.assertRaises( - ValueError, pdu_header.set_entity_ids, bytes([0, 1, 2]), bytes([2, 3, 4]) - ) - self.assertRaises( - ValueError, pdu_header.set_entity_ids, bytes([0, 1, 2, 8]), bytes([2, 3]) - ) - with self.assertRaises(ValueError): - pdu_header.transaction_seq_num = bytes([0, 1, 2]) with self.assertRaises(ValueError): pdu_header.pdu_data_field_len = 78292 invalid_pdu_header = bytearray([0, 1, 2]) self.assertRaises(ValueError, PduHeader.unpack, invalid_pdu_header) self.assertRaises(ValueError, PduHeader.unpack, pdu_header_packed[0:6]) pdu_header_unpacked = PduHeader.unpack(raw_packet=pdu_header_packed) - self.assertEqual(pdu_header_unpacked.source_entity_id, bytes([0, 0])) - self.assertEqual(pdu_header_unpacked.dest_entity_id, bytes([0, 1])) + self.assertEqual(pdu_header_unpacked.source_entity_id, ByteFieldU16(0)) + self.assertEqual(pdu_header_unpacked.dest_entity_id, ByteFieldU16(1)) self.assertEqual( - pdu_header_unpacked.transaction_seq_num[0] << 8 - | pdu_header_unpacked.transaction_seq_num[1], + int(pdu_header_unpacked.transaction_seq_num), 300, ) - pdu_conf.source_entity_id = bytes([0]) - pdu_conf.dest_entity_id = bytes([0]) - pdu_conf.transaction_seq_num = bytes([0x00, 0x2C]) + pdu_conf.source_entity_id = ByteFieldU8(0) + pdu_conf.dest_entity_id = ByteFieldU8(0) + pdu_conf.transaction_seq_num = ByteFieldU16.from_bytes(bytes([0x00, 0x2C])) prompt_pdu = PromptPdu( response_required=ResponseRequired.KEEP_ALIVE, pdu_conf=pdu_conf ) self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 9) self.assertEqual(prompt_pdu.packet_len, 10) self.assertEqual(prompt_pdu.crc_flag, CrcFlag.WITH_CRC) - self.assertEqual(prompt_pdu.source_entity_id, bytes([0])) - self.assertEqual(prompt_pdu.dest_entity_id, bytes([0])) + self.assertEqual(prompt_pdu.source_entity_id, ByteFieldU8(0)) + self.assertEqual(prompt_pdu.dest_entity_id, ByteFieldU8(0)) self.assertEqual(prompt_pdu.file_flag, LargeFileFlag.LARGE) prompt_pdu.file_flag = LargeFileFlag.NORMAL self.assertEqual(prompt_pdu.file_flag, LargeFileFlag.NORMAL) diff --git a/tests/test_ecss.py b/tests/test_ecss.py index f8962ab..f33ce4c 100644 --- a/tests/test_ecss.py +++ b/tests/test_ecss.py @@ -3,10 +3,7 @@ from unittest import TestCase from spacepackets.ecss import PacketFieldEnum -from spacepackets.ecss.fields import ( - byte_num_to_unsigned_struct_specifier, - byte_num_to_signed_struct_specifier, -) +from spacepackets.util import IntByteConversion class ExampleEnum(enum.IntEnum): @@ -22,13 +19,13 @@ def test_basic(self): self._enum_serialize_deserialize_different_sizes(32) self._enum_serialize_deserialize_different_sizes(64) with self.assertRaises(ValueError): - byte_num_to_unsigned_struct_specifier(12) + IntByteConversion.unsigned_struct_specifier(12) with self.assertRaises(ValueError): - byte_num_to_unsigned_struct_specifier(0) + IntByteConversion.unsigned_struct_specifier(0) with self.assertRaises(ValueError): - byte_num_to_signed_struct_specifier(12) + IntByteConversion.signed_struct_specifier(12) with self.assertRaises(ValueError): - byte_num_to_signed_struct_specifier(0) + IntByteConversion.signed_struct_specifier(0) def _enum_serialize_deserialize_different_sizes(self, pfc: int): for val in ExampleEnum: From 326678020db7a9a8c008a0b11a200916c226010a Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 02:43:24 +0200 Subject: [PATCH 13/46] improve metadata API --- spacepackets/cfdp/defs.py | 2 +- spacepackets/cfdp/pdu/file_directive.py | 4 ++ spacepackets/cfdp/pdu/metadata.py | 67 ++++++++++++++----------- spacepackets/cfdp/pdu/wrapper.py | 3 ++ tests/cfdp/pdus/test_metadata.py | 30 +++++------ tests/cfdp/pdus/test_pdu_wrapper.py | 9 ++-- 6 files changed, 65 insertions(+), 50 deletions(-) diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index b35fc57..ca7b380 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -128,7 +128,7 @@ def as_bytes(self) -> bytes: def __repr__(self): return ( - f"{self.__class__.__name__}(entity_id={self.value!r}, " + f"{self.__class__.__name__}(val={self.value!r}, " f"byte_len={self.byte_len!r})" ) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 494f056..7293345 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -126,6 +126,10 @@ def __init__( ) self._directive_type = directive_code + @property + def pdu_conf(self) -> PduConfig: + return self.pdu_header.pdu_conf + @property def pdu_header(self) -> PduHeader: return self._pdu_header diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 6ab2653..2c91cbe 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -1,6 +1,8 @@ from __future__ import annotations + +import dataclasses import struct -from typing import List, Optional, cast +from typing import List, Optional from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( @@ -15,36 +17,40 @@ from spacepackets.cfdp.conf import check_packet_length +@dataclasses.dataclass +class MetadataParams: + closure_requested: bool + checksum_type: ChecksumTypes + file_size: int + source_file_name: Optional[str] + dest_file_name: Optional[str] + + class MetadataPdu(AbstractFileDirectiveBase): """Encapsulates the Metadata file directive PDU, see CCSDS 727.0-B-5 p.83""" def __init__( self, - closure_requested: bool, - checksum_type: ChecksumTypes, - file_size: int, - source_file_name: Optional[str], - dest_file_name: Optional[str], + params: MetadataParams, pdu_conf: PduConfig, options: Optional[TlvList] = None, ): - self.closure_requested = closure_requested - self.checksum_type = checksum_type - self.file_size = file_size - if source_file_name is None: + self.params = params + if params.source_file_name is None: self._source_file_name_lv = CfdpLv(value=bytes()) else: - source_file_name_as_bytes = source_file_name.encode("utf-8") + source_file_name_as_bytes = params.source_file_name.encode("utf-8") self._source_file_name_lv = CfdpLv(value=source_file_name_as_bytes) - if dest_file_name is None: + if params.dest_file_name is None: self._dest_file_name_lv = CfdpLv(value=bytes()) else: - dest_file_name_as_bytes = dest_file_name.encode("utf-8") + dest_file_name_as_bytes = params.dest_file_name.encode("utf-8") self._dest_file_name_lv = CfdpLv(value=dest_file_name_as_bytes) if options is None: self._options = [] else: self._options = options + self.pdu_conf = pdu_conf self.pdu_file_directive = FileDirectivePduBase( directive_code=DirectiveType.METADATA_PDU, pdu_conf=pdu_conf, @@ -64,11 +70,7 @@ def pdu_header(self) -> PduHeader: def __empty(cls) -> MetadataPdu: empty_conf = PduConfig.empty() return cls( - closure_requested=False, - checksum_type=ChecksumTypes.MODULAR, - file_size=0, - source_file_name="", - dest_file_name="", + params=MetadataParams(False, ChecksumTypes.MODULAR, 0, "", ""), pdu_conf=empty_conf, ) @@ -131,14 +133,14 @@ def packet_len(self) -> int: return self.pdu_file_directive.packet_len def pack(self) -> bytearray: - if not self.pdu_file_directive.verify_file_len(self.file_size): + if not self.pdu_file_directive.verify_file_len(self.params.file_size): raise ValueError packet = self.pdu_file_directive.pack() - packet.append((self.closure_requested << 6) | self.checksum_type) + packet.append((self.params.closure_requested << 6) | self.params.checksum_type) if self.pdu_file_directive.pdu_header.large_file_flag_set: - packet.extend(struct.pack("!Q", self.file_size)) + packet.extend(struct.pack("!Q", self.params.file_size)) else: - packet.extend(struct.pack("!I", self.file_size)) + packet.extend(struct.pack("!I", self.params.file_size)) packet.extend(self._source_file_name_lv.pack()) packet.extend(self._dest_file_name_lv.pack()) for option in self.options: @@ -148,6 +150,7 @@ def pack(self) -> bytearray: @classmethod def unpack(cls, raw_packet: bytearray) -> MetadataPdu: metadata_pdu = cls.__empty() + metadata_pdu.pdu_file_directive = FileDirectivePduBase.unpack( raw_packet=raw_packet ) @@ -155,15 +158,17 @@ def unpack(cls, raw_packet: bytearray) -> MetadataPdu: # Minimal length: 1 byte + FSS (4 byte) + 2 empty LV (1 byte) if not check_packet_length(len(raw_packet), current_idx + 7): raise ValueError - metadata_pdu.closure_requested = raw_packet[current_idx] & 0x40 - metadata_pdu.checksum_type = raw_packet[current_idx] & 0x0F + params = MetadataParams(False, ChecksumTypes.MODULAR, 0, "", "") + params.closure_requested = raw_packet[current_idx] & 0x40 + params.checksum_type = raw_packet[current_idx] & 0x0F current_idx += 1 ( current_idx, - metadata_pdu.file_size, + params.file_size, ) = metadata_pdu.pdu_file_directive.parse_fss_field( raw_packet=raw_packet, current_idx=current_idx ) + metadata_pdu.params = params metadata_pdu._source_file_name_lv = CfdpLv.unpack( raw_bytes=raw_packet[current_idx:] ) @@ -191,12 +196,18 @@ def _parse_options(self, raw_packet: bytearray, start_idx: int): elif current_idx == len(raw_packet): break + def __repr__(self): + return ( + f"{self.__class__.__name__}(params={self.params!r}, options={self.options!r}, " + f"pdu_conf={self.pdu_file_directive.pdu_conf})" + ) + def __eq__(self, other: MetadataPdu): return ( self.pdu_file_directive == other.pdu_file_directive - and self.closure_requested == other.closure_requested - and self.checksum_type == other.checksum_type - and self.file_size == other.file_size + and self.params.closure_requested == other.params.closure_requested + and self.params.checksum_type == other.params.checksum_type + and self.params.file_size == other.params.file_size and self._source_file_name_lv == other._source_file_name_lv and self._dest_file_name_lv == other._dest_file_name_lv and self._options == other._options diff --git a/spacepackets/cfdp/pdu/wrapper.py b/spacepackets/cfdp/pdu/wrapper.py index 71738c7..45e9126 100644 --- a/spacepackets/cfdp/pdu/wrapper.py +++ b/spacepackets/cfdp/pdu/wrapper.py @@ -24,6 +24,9 @@ class PduWrapper: def __init__(self, base: Optional[GenericPduPacket]): self.base = base + def __repr__(self): + return f"{self.__class__.__name__}(base={self.base!r}" + def _raise_not_target_exception(self, pdu_type: Type[any]): raise TypeError(f"Stored PDU is not {pdu_type.__name__!r}: {self.base!r}") diff --git a/tests/cfdp/pdus/test_metadata.py b/tests/cfdp/pdus/test_metadata.py index 071d9ff..b53efb1 100644 --- a/tests/cfdp/pdus/test_metadata.py +++ b/tests/cfdp/pdus/test_metadata.py @@ -9,20 +9,21 @@ from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.defs import FaultHandlerCodes, LargeFileFlag from spacepackets.cfdp.pdu import MetadataPdu +from spacepackets.cfdp.pdu.metadata import MetadataParams from spacepackets.cfdp.tlv import TlvWrapper, FaultHandlerOverrideTlv class TestMetadata(TestCase): def test_metadata_pdu(self): pdu_conf = PduConfig.empty() - metadata_pdu = MetadataPdu( - pdu_conf=pdu_conf, + metadata_params = MetadataParams( closure_requested=False, file_size=2, source_file_name="test.txt", dest_file_name="test2.txt", checksum_type=ChecksumTypes.MODULAR, ) + metadata_pdu = MetadataPdu(pdu_conf=pdu_conf, params=metadata_params) self.check_metadata_fields_0(metadata_pdu=metadata_pdu) header_len = metadata_pdu.pdu_file_directive.header_len self.assertEqual(header_len, 8) @@ -47,11 +48,7 @@ def test_metadata_pdu(self): # Create completey new packet pdu_with_option = MetadataPdu( pdu_conf=pdu_conf, - closure_requested=False, - file_size=2, - source_file_name="test.txt", - dest_file_name="test2.txt", - checksum_type=ChecksumTypes.MODULAR, + params=metadata_params, options=[option_0], ) self.assertEqual(pdu_with_option.options, [option_0]) @@ -76,13 +73,16 @@ def test_metadata_pdu(self): handler_code=FaultHandlerCodes.ABANDON_TRANSACTION, ) self.assertEqual(option_1.packet_len, 3) - pdu_with_two_options = MetadataPdu( - pdu_conf=pdu_conf, + metadata_params = MetadataParams( closure_requested=False, file_size=2, source_file_name=None, dest_file_name=None, checksum_type=ChecksumTypes.MODULAR, + ) + pdu_with_two_options = MetadataPdu( + pdu_conf=pdu_conf, + params=metadata_params, options=[option_0, option_1], ) pdu_with_two_options_raw = pdu_with_two_options.pack() @@ -101,18 +101,14 @@ def test_metadata_pdu(self): self.assertEqual(pdu_with_two_options.packet_len, expected_len) pdu_with_no_options = pdu_with_two_options pdu_with_no_options.options = None - pdu_with_no_options.file_size = pow(2, 32) + 1 + pdu_with_no_options.params.file_size = pow(2, 32) + 1 with self.assertRaises(ValueError): pdu_with_no_options.pack() pdu_conf.file_flag = LargeFileFlag.LARGE pdu_file_size_large = MetadataPdu( pdu_conf=pdu_conf, - closure_requested=False, - file_size=2, - source_file_name=None, - dest_file_name=None, - checksum_type=ChecksumTypes.MODULAR, + params=metadata_params, options=None, ) self.assertEqual(pdu_file_size_large.pdu_file_directive.header_len, header_len) @@ -124,8 +120,8 @@ def test_metadata_pdu(self): MetadataPdu.unpack(raw_packet=pdu_file_size_large_raw) def check_metadata_fields_0(self, metadata_pdu: MetadataPdu): - self.assertEqual(metadata_pdu.closure_requested, False) - self.assertEqual(metadata_pdu.file_size, 2) + self.assertEqual(metadata_pdu.params.closure_requested, False) + self.assertEqual(metadata_pdu.params.file_size, 2) self.assertEqual(metadata_pdu.source_file_name, "test.txt") self.assertEqual(metadata_pdu.dest_file_name, "test2.txt") self.assertEqual(metadata_pdu.pdu_file_directive.header_len, 8) diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index 3b1064a..870dc08 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -15,6 +15,7 @@ ) from spacepackets.cfdp.pdu.file_data import FileDataPdu from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus +from spacepackets.cfdp.pdu.metadata import MetadataParams from spacepackets.cfdp.pdu.prompt import ResponseRequired from spacepackets.cfdp.pdu.wrapper import PduWrapper @@ -38,14 +39,14 @@ def test_file_data(self): self.assertEqual(pdu_casted_back, file_data_pdu) def test_invalid_to_file_data(self): - metadata_pdu = MetadataPdu( - pdu_conf=self.pdu_conf, + params = MetadataParams( closure_requested=False, file_size=2, source_file_name="test.txt", dest_file_name="test2.txt", checksum_type=ChecksumTypes.MODULAR, ) + metadata_pdu = MetadataPdu(pdu_conf=self.pdu_conf, params=params) self.pdu_wrapper.base = metadata_pdu with self.assertRaises(TypeError) as cm: self.pdu_wrapper.to_file_data_pdu() @@ -53,14 +54,14 @@ def test_invalid_to_file_data(self): self.assertIn("Stored PDU is not 'FileDataPdu'", str(exception)) def test_metadata(self): - metadata_pdu = MetadataPdu( - pdu_conf=self.pdu_conf, + params = MetadataParams( closure_requested=False, file_size=2, source_file_name="test.txt", dest_file_name="test2.txt", checksum_type=ChecksumTypes.MODULAR, ) + metadata_pdu = MetadataPdu(pdu_conf=self.pdu_conf, params=params) self.pdu_wrapper.base = metadata_pdu metadata_casted_back = self.pdu_wrapper.to_metadata_pdu() self.assertEqual(metadata_casted_back, metadata_pdu) From 664e4b80d17b165aac3c5cc02aefe9ede9eaedc9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 12:49:52 +0200 Subject: [PATCH 14/46] fixed tests --- spacepackets/cfdp/conf.py | 3 +- spacepackets/cfdp/defs.py | 116 ------------------ spacepackets/cfdp/pdu/ack.py | 1 - spacepackets/cfdp/pdu/file_data.py | 2 +- spacepackets/cfdp/pdu/file_directive.py | 3 +- spacepackets/cfdp/pdu/header.py | 9 +- spacepackets/util.py | 151 ++++++++++++++++++++++++ tests/cfdp/pdus/test_ack_pdu.py | 2 +- tests/cfdp/pdus/test_nak_pdu.py | 3 +- tests/cfdp/test_header.py | 13 +- 10 files changed, 170 insertions(+), 133 deletions(-) diff --git a/spacepackets/cfdp/conf.py b/spacepackets/cfdp/conf.py index 251124a..9407a75 100644 --- a/spacepackets/cfdp/conf.py +++ b/spacepackets/cfdp/conf.py @@ -8,10 +8,9 @@ CrcFlag, Direction, SegmentationControl, - UnsignedByteField, - ByteFieldU8, ) from spacepackets.log import get_console_logger +from spacepackets.util import UnsignedByteField, ByteFieldU8 @dataclass diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index ca7b380..341b1cf 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -1,8 +1,5 @@ from __future__ import annotations import enum -import struct - -from spacepackets.util import IntByteConversion class PduType(enum.IntEnum): @@ -91,116 +88,3 @@ class ChecksumTypes(enum.IntEnum): # Polynomial: 0x4C11DB7. This is the preferred checksum for now. CRC_32 = 3 NULL_CHECKSUM = 15 - - -class UnsignedByteField: - def __init__(self, val: int, byte_len: int): - self.byte_len = byte_len - self.value = val - - @property - def byte_len(self): - return self._byte_len - - @byte_len.setter - def byte_len(self, byte_len: int): - if byte_len not in [1, 2, 4, 8]: - # I really have no idea why anyone would use other values than these - raise ValueError( - "Only 1, 2, 4 and 8 bytes are allowed as an entity ID length" - ) - self._byte_len = byte_len - - @property - def value(self): - return self._val - - @value.setter - def value(self, val: int): - if val > pow(2, self.byte_len * 8) - 1: - raise ValueError( - f"Passed value larger than allowed {pow(2, self.byte_len * 8) - 1}" - ) - self._val = val - - def as_bytes(self) -> bytes: - return IntByteConversion.to_unsigned(self.byte_len, self.value) - - def __repr__(self): - return ( - f"{self.__class__.__name__}(val={self.value!r}, " - f"byte_len={self.byte_len!r})" - ) - - def __int__(self): - return self.value - - def __eq__(self, other: UnsignedByteField): - return self.value == other.value and self.byte_len == other.byte_len - - def __hash__(self): - return hash((self.value, self.byte_len)) - - -class ByteFieldU8(UnsignedByteField): - """Concrete variant of a variable length byte field which has 1 byte""" - - def __init__(self, val: int): - super().__init__(val, 1) - - @classmethod - def from_bytes(cls, stream: bytes) -> ByteFieldU8: - if len(stream) < 1: - raise ValueError( - "Passed stream not large enough, should be at least 1 byte" - ) - return cls(stream[0]) - - -class ByteFieldU16(UnsignedByteField): - """Concrete variant of a variable length byte field which has 2 bytes""" - - def __init__(self, val: int): - super().__init__(val, 2) - - @classmethod - def from_bytes(cls, stream: bytes) -> ByteFieldU16: - if len(stream) < 2: - raise ValueError( - "Passed stream not large enough, should be at least 2 byte" - ) - return cls( - struct.unpack(IntByteConversion.unsigned_struct_specifier(2), stream[0:2])[ - 0 - ] - ) - - -class ByteFieldU32(UnsignedByteField): - """Concrete variant of a variable length byte field which has 4 bytes""" - - def __init__(self, val: int): - super().__init__(val, 4) - - @classmethod - def from_bytes(cls, stream: bytes) -> ByteFieldU32: - if len(stream) < 4: - raise ValueError( - "Passed stream not large enough, should be at least 4 byte" - ) - return cls( - struct.unpack(IntByteConversion.unsigned_struct_specifier(4), stream[0:4])[ - 0 - ] - ) - - -class ByteFieldGenerator: - @staticmethod - def from_bytes(byte_len: int, stream: bytes) -> UnsignedByteField: - if byte_len == 1: - return ByteFieldU8.from_bytes(stream) - elif byte_len == 2: - return ByteFieldU16.from_bytes(stream) - elif byte_len == 4: - return ByteFieldU32.from_bytes(stream) diff --git a/spacepackets/cfdp/pdu/ack.py b/spacepackets/cfdp/pdu/ack.py index 8601661..6f0622e 100644 --- a/spacepackets/cfdp/pdu/ack.py +++ b/spacepackets/cfdp/pdu/ack.py @@ -1,6 +1,5 @@ from __future__ import annotations import enum -from typing import cast from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( diff --git a/spacepackets/cfdp/pdu/file_data.py b/spacepackets/cfdp/pdu/file_data.py index cccf0e1..873ac61 100644 --- a/spacepackets/cfdp/pdu/file_data.py +++ b/spacepackets/cfdp/pdu/file_data.py @@ -4,11 +4,11 @@ import struct from spacepackets.cfdp import LargeFileFlag -from spacepackets.cfdp.defs import UnsignedByteField from spacepackets.cfdp.pdu.file_directive import SegmentMetadataFlag, PduType from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.pdu.header import PduHeader, AbstractPduBase from spacepackets.log import get_console_logger +from spacepackets.util import UnsignedByteField class RecordContinuationState(enum.IntEnum): diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 7293345..0bddc24 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -10,9 +10,10 @@ SegmentMetadataFlag, AbstractPduBase, ) -from spacepackets.cfdp.defs import LargeFileFlag, CrcFlag, UnsignedByteField +from spacepackets.cfdp.defs import LargeFileFlag, CrcFlag from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.log import get_console_logger +from spacepackets.util import UnsignedByteField class DirectiveType(enum.IntEnum): diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 38c4008..0580474 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -11,13 +11,12 @@ TransmissionModes, Direction, SegmentationControl, - UnsignedByteField, LenInBytes, - ByteFieldGenerator, ) from spacepackets.cfdp.conf import ( PduConfig, ) +from spacepackets.util import UnsignedByteField, ByteFieldGenerator class AbstractPduBase(abc.ABC): @@ -253,9 +252,9 @@ def pack(self) -> bytearray: | self.segment_metadata_flag << 3 | self.transaction_seq_num.byte_len ) - header.extend(self.pdu_conf.source_entity_id.as_bytes()) - header.extend(self.pdu_conf.transaction_seq_num.as_bytes()) - header.extend(self.pdu_conf.dest_entity_id.as_bytes()) + header.extend(self.pdu_conf.source_entity_id.as_bytes) + header.extend(self.pdu_conf.transaction_seq_num.as_bytes) + header.extend(self.pdu_conf.dest_entity_id.as_bytes) return header @classmethod diff --git a/spacepackets/util.py b/spacepackets/util.py index 569bf13..748376b 100644 --- a/spacepackets/util.py +++ b/spacepackets/util.py @@ -1,5 +1,7 @@ +from __future__ import annotations import enum import struct +from typing import Union class PrintFormats(enum.IntEnum): @@ -91,3 +93,152 @@ def to_unsigned(byte_num: int, val: int) -> bytes: if val > pow(2, byte_num * 8) - 1: raise ValueError(f"Passed value larger than allows {pow(2, byte_num) - 1}") return struct.pack(IntByteConversion.unsigned_struct_specifier(byte_num), val) + + +class UnsignedByteField: + def __init__(self, val: int, byte_len: int): + self.byte_len = byte_len + self.value = val + self._val_as_bytes = IntByteConversion.to_unsigned(self.byte_len, self.value) + + @property + def byte_len(self): + return self._byte_len + + @byte_len.setter + def byte_len(self, byte_len: int): + UnsignedByteField.verify_byte_len(byte_len) + self._byte_len = byte_len + + @property + def value(self): + return self._val + + @value.setter + def value(self, val: Union[int, bytes]): + if isinstance(val, int): + self._verify_int_value(val) + self._val = val + self._val_as_bytes = IntByteConversion.to_unsigned( + self.byte_len, self.value + ) + elif isinstance(val, bytes): + self._val, self._val_as_bytes = self._verify_bytes_value(val) + + @property + def as_bytes(self) -> bytes: + return self._val_as_bytes + + @staticmethod + def verify_byte_len(byte_len: int): + if byte_len not in [1, 2, 4, 8]: + # I really have no idea why anyone would use other values than these + raise ValueError( + "Only 1, 2, 4 and 8 bytes are allowed as an entity ID length" + ) + + def _verify_int_value(self, val: int): + if val > pow(2, self.byte_len * 8) - 1 or val < 0: + raise ValueError( + f"Passed value {val} larger than allowed {pow(2, self.byte_len * 8) - 1} or negative" + ) + + def _verify_bytes_value(self, val: bytes) -> (int, bytes): + if len(val) < self.byte_len: + raise ValueError( + f"Passed byte object {val} smaller than byte length {self.byte_len}" + ) + int_val = struct.unpack( + IntByteConversion.unsigned_struct_specifier(self.byte_len), + val[0 : self.byte_len], + )[0] + self._verify_int_value(int_val) + return int_val, val[0 : self.byte_len] + + def __repr__(self): + return ( + f"{self.__class__.__name__}(val={self.value!r}, " + f"byte_len={self.byte_len!r})" + ) + + def __int__(self): + return self.value + + def __eq__(self, other: UnsignedByteField): + return self.value == other.value and self.byte_len == other.byte_len + + def __hash__(self): + return hash((self.value, self.byte_len)) + + +class ByteFieldU8(UnsignedByteField): + """Concrete variant of a variable length byte field which has 1 byte""" + + def __init__(self, val: int): + super().__init__(val, 1) + + @classmethod + def from_bytes(cls, stream: bytes) -> ByteFieldU8: + if len(stream) < 1: + raise ValueError( + "Passed stream not large enough, should be at least 1 byte" + ) + return cls(stream[0]) + + +class ByteFieldU16(UnsignedByteField): + """Concrete variant of a variable length byte field which has 2 bytes""" + + def __init__(self, val: int): + super().__init__(val, 2) + + @classmethod + def from_bytes(cls, stream: bytes) -> ByteFieldU16: + if len(stream) < 2: + raise ValueError( + "Passed stream not large enough, should be at least 2 byte" + ) + return cls( + struct.unpack(IntByteConversion.unsigned_struct_specifier(2), stream[0:2])[ + 0 + ] + ) + + +class ByteFieldU32(UnsignedByteField): + """Concrete variant of a variable length byte field which has 4 bytes""" + + def __init__(self, val: int): + super().__init__(val, 4) + + @classmethod + def from_bytes(cls, stream: bytes) -> ByteFieldU32: + if len(stream) < 4: + raise ValueError( + "Passed stream not large enough, should be at least 4 byte" + ) + return cls( + struct.unpack(IntByteConversion.unsigned_struct_specifier(4), stream[0:4])[ + 0 + ] + ) + + +class ByteFieldGenerator: + @staticmethod + def from_int(byte_len: int, val: int) -> UnsignedByteField: + if byte_len == 1: + return ByteFieldU8(val) + elif byte_len == 2: + return ByteFieldU16(val) + elif byte_len == 4: + return ByteFieldU32(val) + + @staticmethod + def from_bytes(byte_len: int, stream: bytes) -> UnsignedByteField: + if byte_len == 1: + return ByteFieldU8.from_bytes(stream) + elif byte_len == 2: + return ByteFieldU16.from_bytes(stream) + elif byte_len == 4: + return ByteFieldU32.from_bytes(stream) diff --git a/tests/cfdp/pdus/test_ack_pdu.py b/tests/cfdp/pdus/test_ack_pdu.py index 8ec5ede..c1d7abc 100644 --- a/tests/cfdp/pdus/test_ack_pdu.py +++ b/tests/cfdp/pdus/test_ack_pdu.py @@ -2,8 +2,8 @@ from spacepackets.cfdp import CrcFlag, TransmissionModes, LargeFileFlag, ConditionCode from spacepackets.cfdp.conf import PduConfig -from spacepackets.cfdp.defs import ByteFieldU16, ByteFieldU32 from spacepackets.cfdp.pdu import AckPdu, DirectiveType, TransactionStatus +from spacepackets.util import ByteFieldU16, ByteFieldU32 class TestAckPdu(TestCase): diff --git a/tests/cfdp/pdus/test_nak_pdu.py b/tests/cfdp/pdus/test_nak_pdu.py index c1517f7..64d4a6a 100644 --- a/tests/cfdp/pdus/test_nak_pdu.py +++ b/tests/cfdp/pdus/test_nak_pdu.py @@ -2,8 +2,9 @@ from spacepackets.cfdp import TransmissionModes from spacepackets.cfdp.conf import PduConfig -from spacepackets.cfdp.defs import Direction, LargeFileFlag, ByteFieldU16 +from spacepackets.cfdp.defs import Direction, LargeFileFlag from spacepackets.cfdp.pdu import NakPdu +from spacepackets.util import ByteFieldU16 class TestNakPdu(TestCase): diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index c85c1e6..a2351fc 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -11,13 +11,16 @@ PduType, SegmentMetadataFlag, LargeFileFlag, - ByteFieldU8, - ByteFieldU16, - ByteFieldU32, ) from spacepackets.cfdp.pdu import PduHeader, PromptPdu from spacepackets.cfdp.pdu.prompt import ResponseRequired -from spacepackets.util import get_printable_data_string, PrintFormats +from spacepackets.util import ( + get_printable_data_string, + PrintFormats, + ByteFieldU8, + ByteFieldU32, + ByteFieldU16, +) class TestHeader(TestCase): @@ -28,7 +31,7 @@ def test_pdu_header(self): byte_field = ByteFieldU16(5292) self.assertEqual(int(byte_field), 5292) byte_field = ByteFieldU32(129302) - self.assertEqual(struct.unpack("!I", byte_field.as_bytes())[0], 129302) + self.assertEqual(struct.unpack("!I", byte_field.as_bytes)[0], 129302) with self.assertRaises(ValueError): ByteFieldU8(900) pdu_conf = PduConfig( From da3346feba34600601ff269c171e2e5641a9632b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 13:13:12 +0200 Subject: [PATCH 15/46] added some doctests --- spacepackets/util.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/spacepackets/util.py b/spacepackets/util.py index 748376b..365a825 100644 --- a/spacepackets/util.py +++ b/spacepackets/util.py @@ -96,6 +96,24 @@ def to_unsigned(byte_num: int, val: int) -> bytes: class UnsignedByteField: + """Generic base class for byte fields containing unsigned values. These are a common + component for packet protocols or packed identifier fields. Each unsigned byte field + has an unsigned value and a corresponding byte length. This base class implements + commonly required boilerplate code to easily work with fields like that and convert + them to the byte and integer representation accordingly. + + >>> field = UnsignedByteField(2, 1) + >>> int(field) + 2 + >>> field.as_bytes.hex(sep=',') + '02' + >>> field = UnsignedByteField(42, 2) + >>> int(field) + 42 + >>> field.as_bytes.hex(sep=',') + '00,2a' + """ + def __init__(self, val: int, byte_len: int): self.byte_len = byte_len self.value = val @@ -168,11 +186,12 @@ def __eq__(self, other: UnsignedByteField): return self.value == other.value and self.byte_len == other.byte_len def __hash__(self): + """Makes all unsigned byte fields usable as dictionary keys""" return hash((self.value, self.byte_len)) class ByteFieldU8(UnsignedByteField): - """Concrete variant of a variable length byte field which has 1 byte""" + """Concrete variant of a variable length byte field which has a length of 1 byte""" def __init__(self, val: int): super().__init__(val, 1) @@ -187,7 +206,7 @@ def from_bytes(cls, stream: bytes) -> ByteFieldU8: class ByteFieldU16(UnsignedByteField): - """Concrete variant of a variable length byte field which has 2 bytes""" + """Concrete variant of a variable length byte field which has a length of 2 bytes""" def __init__(self, val: int): super().__init__(val, 2) @@ -206,7 +225,7 @@ def from_bytes(cls, stream: bytes) -> ByteFieldU16: class ByteFieldU32(UnsignedByteField): - """Concrete variant of a variable length byte field which has 4 bytes""" + """Concrete variant of a variable length byte field which has a length of 4 bytes""" def __init__(self, val: int): super().__init__(val, 4) @@ -225,6 +244,8 @@ def from_bytes(cls, stream: bytes) -> ByteFieldU32: class ByteFieldGenerator: + """Static helpers to create the U8, U16 and U32 byte field variants of unsigned byte fields""" + @staticmethod def from_int(byte_len: int, val: int) -> UnsignedByteField: if byte_len == 1: From 906c2f8ac25d2d096b3a993d91a120a33d101fbc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 13:26:37 +0200 Subject: [PATCH 16/46] doctests for space packet header --- docs/api.rst | 2 +- docs/api/cfdp.rst | 4 ++-- spacepackets/ccsds/spacepacket.py | 26 +++++++++++++++++++++----- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index fa53fb8..976dcef 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,7 +4,7 @@ API Documentation This package is split into three subpackages: - :py:mod:`spacepackets.ccsds`: Contains CCSDS specific code. This includes the space packet - implementation inside the :py:mod:`spacepacket.ccsds.spacepackets` module and time related + implementation inside the :py:mod:`spacepackets.ccsds.spacepacket` module and time related implementations in the :py:mod:`spacepackets.ccsds.time` module - :py:mod:`spacepackets.cfdp`: Contains packet implementations related to the CCSDS File Delivery Protocol diff --git a/docs/api/cfdp.rst b/docs/api/cfdp.rst index b3136a5..d10fca1 100644 --- a/docs/api/cfdp.rst +++ b/docs/api/cfdp.rst @@ -17,10 +17,10 @@ spacepackets.cfdp.conf module :undoc-members: :show-inheritance: -spacepackets.cfdp.defintions module +spacepackets.cfdp.defs module ------------------------------------- -.. automodule:: spacepackets.cfdp.definitions +.. automodule:: spacepackets.cfdp.defs :members: :undoc-members: :show-inheritance: diff --git a/spacepackets/ccsds/spacepacket.py b/spacepackets/ccsds/spacepacket.py index 1fd9101..813d163 100644 --- a/spacepackets/ccsds/spacepacket.py +++ b/spacepackets/ccsds/spacepacket.py @@ -120,7 +120,21 @@ def __init__( seq_flags: SequenceFlags = SequenceFlags.UNSEGMENTED, ccsds_version: int = 0b000, ): - """Create a space packet header with the given field parameters + """Create a space packet header with the given field parameters. + + >>> sph = SpacePacketHeader(packet_type=PacketTypes.TC, apid=0x42, seq_count=0, data_len=12) + >>> hex(sph.apid) + '0x42' + >>> sph.packet_type + + >>> sph.data_len + 12 + >>> sph.packet_len + 19 + >>> sph.packet_id + PacketId(ptype=, sec_header_flag=True, apid=66) + >>> sph.psc + PacketSeqCtrl(seq_flags=, seq_count=0) :param packet_type: 0 for Telemetery, 1 for Telecommands :param apid: Application Process ID, should not be larger @@ -215,16 +229,18 @@ def apid(self, apid): @property def packet_len(self) -> int: - """Retrieve the full space packet size when packed + """Retrieve the full space packet size when packed. + :return: Size of the TM packet based on the space packet header data length field. - The space packet data field is the full length of data field minus one without - the space packet header. + The space packet data field is the full length of data field minus one without + the space packet header. """ return SPACE_PACKET_HEADER_SIZE + self.data_len + 1 @classmethod def unpack(cls, space_packet_raw: bytes) -> SpacePacketHeader: - """Unpack a raw space packet into the space packet header instance + """Unpack a raw space packet into the space packet header instance. + :raise ValueError: Raw packet length invalid """ if len(space_packet_raw) < SPACE_PACKET_HEADER_SIZE: From 248137edadd143b571ea1e4a7f1076fb42dcf083 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 13:31:35 +0200 Subject: [PATCH 17/46] doctest for PUS TC --- spacepackets/ecss/tc.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/spacepackets/ecss/tc.py b/spacepackets/ecss/tc.py index 5b35e2e..e4b0c86 100644 --- a/spacepackets/ecss/tc.py +++ b/spacepackets/ecss/tc.py @@ -100,9 +100,17 @@ def get_header_size(cls): class PusTelecommand: - """Class representation of a PUS telecommand. It can be used to pack a raw telecommand from - input parameters. The structure of a PUS telecommand is specified in ECSS-E-70-41A on p.42 - and is also shown below (bottom) + """Class representation of a PUS telecommand. Can be converted to the raw byte representation + but also unpacked from a raw byte stream. Only PUS C telecommands are supported. + + >>> ping_tc = PusTelecommand(service=17, subservice=1, seq_count=22, apid=0x01) + >>> ping_tc.service + 17 + >>> ping_tc.subservice + 1 + >>> ping_tc.pack().hex(sep=',') + '18,01,c0,16,00,06,2f,11,01,00,00,ab,62' + """ def __init__( From a0db63588cf1bd79a799357a3031fa4d878f0cc0 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 13:34:12 +0200 Subject: [PATCH 18/46] added cross-ref --- docs/packets.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/packets.rst b/docs/packets.rst index 1131ba5..bfc2e4f 100644 --- a/docs/packets.rst +++ b/docs/packets.rst @@ -21,8 +21,9 @@ function from the `crcmod package`_ can be used to calculate this checksum. Telecommands ^^^^^^^^^^^^^^^^^^ -Extended information can be found in `ECSS-E-70-41A`_ on p.42 or in `ECSS-ST-E-70-41C`_ starting at -page 442. +This chapter contains some high level information about the +:py:class:`spacepackets.ecss.tc.PusTelecommand` class. Extended information can be found +in `ECSS-E-70-41A`_ on p.42 or in `ECSS-ST-E-70-41C`_ starting at page 442. The structure is shown as follows for a ping telecommand using the PUS service 17 with the subervice ID 1. This can also be denoted more briefly as TC[17,1]. The first part @@ -80,8 +81,9 @@ PUS A is not supported anymore starting at version ``v0.12.0``. Telemetry ^^^^^^^^^^^^ -Extended information can be found in `ECSS-E-70-41A`_ on p.42 or in `ECSS-ST-E-70-41C`_ starting at -page 442. +This chapter contains some high level information about the +:py:class:`spacepackets.ecss.tm.PusTelemetry` class. Extended information can be found +in `ECSS-E-70-41A`_ on p.42 or in `ECSS-ST-E-70-41C`_ starting at page 442. The structure is shown as follows for a ping reply using the PUS service 17 with the subervice ID 2. This can also be denoted more briefly as TM[17,2]. The first part From a4b8069aae7db706ee6951e3fe9eff0de6b4d3c5 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 13:55:39 +0200 Subject: [PATCH 19/46] util unittests --- spacepackets/util.py | 4 +-- tests/test_util.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/test_util.py diff --git a/spacepackets/util.py b/spacepackets/util.py index 365a825..37c260e 100644 --- a/spacepackets/util.py +++ b/spacepackets/util.py @@ -133,14 +133,14 @@ def value(self): return self._val @value.setter - def value(self, val: Union[int, bytes]): + def value(self, val: Union[int, bytes, bytearray]): if isinstance(val, int): self._verify_int_value(val) self._val = val self._val_as_bytes = IntByteConversion.to_unsigned( self.byte_len, self.value ) - elif isinstance(val, bytes): + elif isinstance(val, bytes) or isinstance(val, bytearray): self._val, self._val_as_bytes = self._verify_bytes_value(val) @property diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..2138dfb --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,80 @@ +import struct +from unittest import TestCase + +from spacepackets.util import ( + ByteFieldGenerator, + ByteFieldU8, + ByteFieldU16, + ByteFieldU32, + UnsignedByteField, + IntByteConversion, +) + + +class TestUtility(TestCase): + def test_one_byte_field_gen(self): + one_byte_test = ByteFieldGenerator.from_int(byte_len=1, val=0x42) + self.assertEqual(ByteFieldU8(0x42), one_byte_test) + one_byte_test = ByteFieldGenerator.from_bytes(1, one_byte_test.as_bytes) + self.assertEqual(ByteFieldU8(0x42), one_byte_test) + + def test_one_byte_invalid_gen(self): + with self.assertRaises(ValueError) as cm: + ByteFieldGenerator.from_int(byte_len=1, val=0x4217) + self.assertEqual( + str(cm.exception), + f"Passed value {0x4217} larger than allowed 255 or negative", + ) + with self.assertRaises(ValueError) as cm: + ByteFieldGenerator.from_int(byte_len=1, val=-1) + self.assertEqual( + str(cm.exception), f"Passed value -1 larger than allowed 255 or negative" + ) + + def test_byte_field_u8_invalid_unpack(self): + with self.assertRaises(ValueError): + ByteFieldU8.from_bytes(bytes()) + + def test_byte_field_u16_invalid_unpack(self): + with self.assertRaises(ValueError): + ByteFieldU16.from_bytes(bytes([1])) + + def test_byte_field_u32_invalid_unpack(self): + with self.assertRaises(ValueError): + ByteFieldU32.from_bytes(bytes([1, 2, 3])) + + def test_two_byte_field_gen(self): + two_byte_test = ByteFieldGenerator.from_int(byte_len=2, val=0x1842) + self.assertEqual(ByteFieldU16(0x1842), two_byte_test) + two_byte_test = ByteFieldGenerator.from_bytes(2, two_byte_test.as_bytes) + self.assertEqual(ByteFieldU16(0x1842), two_byte_test) + + def test_four_byte_field_gen(self): + four_byte_test = ByteFieldGenerator.from_int(byte_len=4, val=0x10101010) + self.assertEqual(ByteFieldU32(0x10101010), four_byte_test) + four_byte_test = ByteFieldGenerator.from_bytes(4, four_byte_test.as_bytes) + self.assertEqual(ByteFieldU32(0x10101010), four_byte_test) + + def test_setting_from_raw(self): + one_byte_test = ByteFieldGenerator.from_int(byte_len=1, val=0x42) + one_byte_test.value = bytes([0x22]) + self.assertEqual(int(one_byte_test), 0x22) + self.assertEqual(one_byte_test.as_bytes, bytes([0x22])) + with self.assertRaises(ValueError): + one_byte_test.value = bytes() + + def invalid_byte_field_len(self): + with self.assertRaises(ValueError): + UnsignedByteField(0x42, 3) + + def test_byte_int_converter_signed_one_byte(self): + minus_two_raw = IntByteConversion.to_signed(byte_num=1, val=-2) + self.assertEqual(struct.unpack("!b", minus_two_raw)[0], -2) + + def test_byte_int_converter_signed_two_byte(self): + raw = IntByteConversion.to_signed(byte_num=2, val=-32084) + self.assertEqual(struct.unpack("!h", raw)[0], -32084) + + def test_byte_int_converter_signed_four_byte(self): + raw = IntByteConversion.to_signed(byte_num=4, val=-7329093) + self.assertEqual(struct.unpack("!i", raw)[0], -7329093) From 582acca7abf2ff21fa5ffcfd11d26aa5ba04ec49 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 14:18:52 +0200 Subject: [PATCH 20/46] additional metadata properties - PDU wrapper helper properties --- spacepackets/cfdp/pdu/metadata.py | 8 ++++++++ spacepackets/cfdp/pdu/wrapper.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 2c91cbe..28d8703 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -66,6 +66,14 @@ def directive_type(self) -> DirectiveType: def pdu_header(self) -> PduHeader: return self.pdu_file_directive.pdu_header + @property + def closure_requested(self) -> bool: + return self.params.closure_requested + + @property + def checksum_type(self) -> ChecksumTypes: + return self.params.checksum_type + @classmethod def __empty(cls) -> MetadataPdu: empty_conf = PduConfig.empty() diff --git a/spacepackets/cfdp/pdu/wrapper.py b/spacepackets/cfdp/pdu/wrapper.py index 45e9126..6b60cde 100644 --- a/spacepackets/cfdp/pdu/wrapper.py +++ b/spacepackets/cfdp/pdu/wrapper.py @@ -24,6 +24,20 @@ class PduWrapper: def __init__(self, base: Optional[GenericPduPacket]): self.base = base + @property + def file_directive(self) -> bool: + return self.base.pdu_header.pdu_type == PduType.FILE_DIRECTIVE + + @property + def pdu_directive_type(self) -> Optional[DirectiveType]: + """If the contained type is not a PDU file directive, returns None. Otherwise, returns + the directive type + """ + if not self.file_directive: + return None + directive_base = cast(AbstractFileDirectiveBase, self.base) + return directive_base.directive_type + def __repr__(self): return f"{self.__class__.__name__}(base={self.base!r}" From d08bd6d4c8068303bb8040eef2c8e85776ffda6b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Jul 2022 14:38:10 +0200 Subject: [PATCH 21/46] helper methods and PduFactory --- docs/api/cfdp_pdu.rst | 15 ++++++- spacepackets/cfdp/pdu/__init__.py | 2 +- spacepackets/cfdp/pdu/header.py | 11 +++-- .../cfdp/pdu/{wrapper.py => helper.py} | 41 +++++++++++++++++-- tests/cfdp/pdus/test_pdu_wrapper.py | 2 +- 5 files changed, 61 insertions(+), 10 deletions(-) rename spacepackets/cfdp/pdu/{wrapper.py => helper.py} (71%) diff --git a/docs/api/cfdp_pdu.rst b/docs/api/cfdp_pdu.rst index 97d309a..0f69b18 100644 --- a/docs/api/cfdp_pdu.rst +++ b/docs/api/cfdp_pdu.rst @@ -23,12 +23,23 @@ Following File Data PDUs are available in the subpackage - File Data: :py:mod:`spacepackets.cfdp.pdu.file_data` Every PDU type has a common PDU header which can be found inside the -:py:mod:`spacepackets.cfdp.pdu.header` module +:py:mod:`spacepackets.cfdp.pdu.header` module. + +The helper module :py:mod:`spacepackets.cfdp.pdu.helper` contains components like the +:py:class:`spacepackets.cfdp.pdu.helper.PduWrapper` class which stores PDUs as a generic base type +and allows typed conversion in to the concrete PDU type + +spacepackets.cfdp.pdu.helper module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: spacepackets.cfdp.pdu.helper + :members: + :undoc-members: + :show-inheritance: File Data Submodules --------------------------- - spacepackets.cfdp.pdu.file_data module ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 2e26b28..09cd241 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -15,4 +15,4 @@ from .metadata import MetadataPdu from .nak import NakPdu from .prompt import PromptPdu -from .wrapper import PduWrapper +from .helper import PduWrapper diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 0580474..38d783d 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -20,6 +20,8 @@ class AbstractPduBase(abc.ABC): + VERSION_BITS = 0b0010_0000 + FIXED_LENGTH = 4 """Encapsulate common functions for PDU. PDU or Packet Data Units are the base data unit which are exchanged for CFDP procedures. Each PDU has a common header and this class provides abstract methods to access fields of that common header. @@ -100,13 +102,16 @@ def __eq__(self, other: AbstractPduBase): and self.packet_len == other.packet_len ) + @staticmethod + def header_len_from_raw(data: bytes): + entity_id_len = (data[3] >> 4) & 0b111 + seq_num_len = data[3] & 0b111 + return AbstractPduBase.FIXED_LENGTH + 2 * entity_id_len + seq_num_len + class PduHeader(AbstractPduBase): """Concrete implementation of the abstract :py:class:`AbstractPduBase` class""" - VERSION_BITS = 0b0010_0000 - FIXED_LENGTH = 4 - def __init__( self, pdu_type: PduType, diff --git a/spacepackets/cfdp/pdu/wrapper.py b/spacepackets/cfdp/pdu/helper.py similarity index 71% rename from spacepackets/cfdp/pdu/wrapper.py rename to spacepackets/cfdp/pdu/helper.py index 6b60cde..0e7d852 100644 --- a/spacepackets/cfdp/pdu/wrapper.py +++ b/spacepackets/cfdp/pdu/helper.py @@ -1,3 +1,4 @@ +from __future__ import annotations from typing import cast, Union, Type, Optional from spacepackets.cfdp import PduType @@ -25,15 +26,19 @@ def __init__(self, base: Optional[GenericPduPacket]): self.base = base @property - def file_directive(self) -> bool: - return self.base.pdu_header.pdu_type == PduType.FILE_DIRECTIVE + def pdu_type(self) -> PduType: + return self.base.pdu_header.pdu_type + + @property + def is_file_directive(self): + return self.pdu_type == PduType.FILE_DIRECTIVE @property def pdu_directive_type(self) -> Optional[DirectiveType]: """If the contained type is not a PDU file directive, returns None. Otherwise, returns the directive type """ - if not self.file_directive: + if not self.is_file_directive: return None directive_base = cast(AbstractFileDirectiveBase, self.base) return directive_base.directive_type @@ -93,3 +98,33 @@ def to_prompt_pdu(self) -> PromptPdu: return self._cast_to_concrete_file_directive( PromptPdu, DirectiveType.PROMPT_PDU ) + + +class PduFactory: + """Helper class to generate PDUs and retrieve PDU information from a raw bytestream""" + + @staticmethod + def from_raw(data: bytes): + pass + + @staticmethod + def pdu_type(data: bytes) -> PduType: + return PduType((data[0] >> 4) & 0x01) + + @staticmethod + def is_file_directive(data: bytes): + return PduFactory.pdu_type(data) == PduType.FILE_DIRECTIVE + + @staticmethod + def pdu_directive_type(data: bytes) -> Optional[DirectiveType]: + """Retrieve the PDU directive type from a raw bytestream. + + :raises ValueError: Invalid directive type + :return None if the PDU in the given bytestream is not a file directive, otherwise the + directive + """ + if not PduFactory.is_file_directive(data): + return None + else: + header_len = AbstractPduBase.header_len_from_raw(data) + return DirectiveType(data[header_len]) diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index 870dc08..a3cb8c0 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -17,7 +17,7 @@ from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus from spacepackets.cfdp.pdu.metadata import MetadataParams from spacepackets.cfdp.pdu.prompt import ResponseRequired -from spacepackets.cfdp.pdu.wrapper import PduWrapper +from spacepackets.cfdp.pdu.helper import PduWrapper class TestPduWrapper(TestCase): From e1de9a009324437ba133906fd2914ec51ca84597 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Jul 2022 20:50:59 +0200 Subject: [PATCH 22/46] rename PduWrapper to PduHolder --- spacepackets/cfdp/pdu/__init__.py | 2 +- spacepackets/cfdp/pdu/helper.py | 2 +- tests/cfdp/pdus/test_pdu_wrapper.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 09cd241..0ce0997 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -15,4 +15,4 @@ from .metadata import MetadataPdu from .nak import NakPdu from .prompt import PromptPdu -from .helper import PduWrapper +from .helper import PduHolder diff --git a/spacepackets/cfdp/pdu/helper.py b/spacepackets/cfdp/pdu/helper.py index 0e7d852..895bdc0 100644 --- a/spacepackets/cfdp/pdu/helper.py +++ b/spacepackets/cfdp/pdu/helper.py @@ -19,7 +19,7 @@ GenericPduPacket = Union[AbstractFileDirectiveBase, AbstractPduBase] -class PduWrapper: +class PduHolder: """Helper type to store arbitrary PDU types and cast them to a concrete PDU type conveniently""" def __init__(self, base: Optional[GenericPduPacket]): diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index a3cb8c0..d56007c 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -17,7 +17,7 @@ from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus from spacepackets.cfdp.pdu.metadata import MetadataParams from spacepackets.cfdp.pdu.prompt import ResponseRequired -from spacepackets.cfdp.pdu.helper import PduWrapper +from spacepackets.cfdp.pdu.helper import PduHolder class TestPduWrapper(TestCase): @@ -25,7 +25,7 @@ def setUp(self) -> None: self.pdu_conf = PduConfig.empty() self.file_data = "hello world" self.file_data_bytes = self.file_data.encode() - self.pdu_wrapper = PduWrapper(None) + self.pdu_wrapper = PduHolder(None) def test_file_data(self): file_data_pdu = FileDataPdu( From 0ef456a2e56ef961a221c5f1ca16e9a034f6b696 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Jul 2022 21:02:22 +0200 Subject: [PATCH 23/46] check generated EOF pdu --- spacepackets/cfdp/defs.py | 3 +++ spacepackets/cfdp/pdu/eof.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/spacepackets/cfdp/defs.py b/spacepackets/cfdp/defs.py index 341b1cf..c6bab57 100644 --- a/spacepackets/cfdp/defs.py +++ b/spacepackets/cfdp/defs.py @@ -88,3 +88,6 @@ class ChecksumTypes(enum.IntEnum): # Polynomial: 0x4C11DB7. This is the preferred checksum for now. CRC_32 = 3 NULL_CHECKSUM = 15 + + +NULL_CHECKSUM_U32 = bytes([0x00, 0x00, 0x00, 0x00]) diff --git a/spacepackets/cfdp/pdu/eof.py b/spacepackets/cfdp/pdu/eof.py index c931561..daf811d 100644 --- a/spacepackets/cfdp/pdu/eof.py +++ b/spacepackets/cfdp/pdu/eof.py @@ -134,3 +134,11 @@ def __eq__(self, other: EofPdu): and self.file_size == other.file_size and self._fault_location == other._fault_location ) + + def __repr__(self): + return ( + f"{self.__class__.__name__}(file_checksum={self.file_checksum!r}," + f"file_size={self.file_size!r}, pdu_conf={self.pdu_file_directive.pdu_conf}," + f"fault_location={self.fault_location!r}," + f"condition_code={self.condition_code})" + ) From 0834889b7f4addeec433595af50c9afb2eb461ee Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 11 Jul 2022 16:10:28 +0200 Subject: [PATCH 24/46] some changes to file_data PDU --- spacepackets/cfdp/pdu/file_data.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/spacepackets/cfdp/pdu/file_data.py b/spacepackets/cfdp/pdu/file_data.py index 873ac61..dbc4d04 100644 --- a/spacepackets/cfdp/pdu/file_data.py +++ b/spacepackets/cfdp/pdu/file_data.py @@ -1,6 +1,6 @@ from __future__ import annotations import enum -from typing import Union +from typing import Union, Optional import struct from spacepackets.cfdp import LargeFileFlag @@ -31,16 +31,23 @@ def __init__( pdu_conf: PduConfig, file_data: bytes, offset: int, - segment_metadata_flag: Union[SegmentMetadataFlag, bool], + segment_metadata_flag: Union[ + SegmentMetadataFlag, bool + ] = SegmentMetadataFlag.NOT_PRESENT, # These fields will only be present if the segment metadata flag is set - record_continuation_state: RecordContinuationState = RecordContinuationState.START_AND_END, - segment_metadata: bytes = bytes(), + record_continuation_state: Optional[RecordContinuationState] = None, + segment_metadata: Optional[bytes] = None, ): - self.record_continuation_state = record_continuation_state if isinstance(segment_metadata_flag, bool): self.segment_metadata_flag = SegmentMetadataFlag(segment_metadata_flag) else: self.segment_metadata_flag = segment_metadata_flag + if ( + self.segment_metadata_flag == SegmentMetadataFlag.PRESENT + and record_continuation_state is None + ): + raise ValueError("Record continuation state must be specified") + self.record_continuation_state = record_continuation_state self._segment_metadata = segment_metadata self.offset = offset self._file_data = file_data @@ -92,6 +99,10 @@ def dest_entity_id(self) -> UnsignedByteField: def crc_flag(self): return self.pdu_header.crc_flag + @property + def has_segment_metadata(self) -> bool: + return self.segment_metadata_flag == SegmentMetadataFlag.PRESENT + @property def segment_metadata(self): return self._segment_metadata From b2d9edf06344af739f19b7fe127510ff4b6d0e4b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 11 Jul 2022 17:48:14 +0200 Subject: [PATCH 25/46] added some re-exports --- spacepackets/cfdp/pdu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 0ce0997..1b6707a 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -10,7 +10,7 @@ DirectiveType, AbstractFileDirectiveBase, ) -from .finished import FinishedPdu +from .finished import FinishedPdu, FileDeliveryStatus, DeliveryCode from .keep_alive import KeepAlivePdu from .metadata import MetadataPdu from .nak import NakPdu From 8f8685162f5c4b647040850c5e9ff2f30e3386fc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 11 Jul 2022 21:47:43 +0200 Subject: [PATCH 26/46] some re-exports --- spacepackets/cfdp/__init__.py | 4 ++++ spacepackets/cfdp/pdu/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spacepackets/cfdp/__init__.py b/spacepackets/cfdp/__init__.py index 12e4296..14fc80e 100644 --- a/spacepackets/cfdp/__init__.py +++ b/spacepackets/cfdp/__init__.py @@ -1,12 +1,15 @@ from .defs import ( PduType, ChecksumTypes, + Direction, CrcFlag, LargeFileFlag, SegmentationControl, SegmentMetadataFlag, TransmissionModes, ConditionCode, + FaultHandlerCodes, + NULL_CHECKSUM_U32, ) from .tlv import ( CfdpTlv, @@ -21,3 +24,4 @@ FilestoreResponseStatusCode, ) from .lv import CfdpLv +from .conf import PduConfig diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 1b6707a..55fda41 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -12,7 +12,8 @@ ) from .finished import FinishedPdu, FileDeliveryStatus, DeliveryCode from .keep_alive import KeepAlivePdu -from .metadata import MetadataPdu +from .metadata import MetadataPdu, MetadataParams from .nak import NakPdu from .prompt import PromptPdu from .helper import PduHolder +from .file_data import FileDataPdu From 34f4b8239dd10b6cd8c2c5ea7e988dda836cf854 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 12:09:07 +0200 Subject: [PATCH 27/46] docstring for request ID --- spacepackets/ccsds/__init__.py | 2 +- spacepackets/ecss/req_id.py | 14 ++++++++++++++ spacepackets/ecss/tm.py | 13 ++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/spacepackets/ccsds/__init__.py b/spacepackets/ccsds/__init__.py index 030e821..874d650 100644 --- a/spacepackets/ccsds/__init__.py +++ b/spacepackets/ccsds/__init__.py @@ -1,3 +1,3 @@ """This package contains all CCSDS related components""" -from .spacepacket import SpacePacketHeader, SpacePacket +from .spacepacket import SpacePacketHeader, SpacePacket, PacketTypes, SequenceFlags from .time import CdsShortTimestamp diff --git a/spacepackets/ecss/req_id.py b/spacepackets/ecss/req_id.py index 9c5aab3..d5a794b 100644 --- a/spacepackets/ecss/req_id.py +++ b/spacepackets/ecss/req_id.py @@ -7,6 +7,20 @@ class RequestId: + """The request ID which is used to identify PUS telecommands. It is primarily used + to verify the execution of sent telecommands + + >>> from spacepackets.ccsds import PacketTypes, SequenceFlags + >>> packet_id = PacketId(ptype=PacketTypes.TC, sec_header_flag=False, apid=0x22) + >>> psc = PacketSeqCtrl(seq_flags=SequenceFlags.UNSEGMENTED, seq_count=17) + >>> req_id = RequestId(packet_id, psc) + >>> req_id + RequestId(tc_packet_id=PacketId(ptype=, sec_header_flag=False, apid=34), \ +tc_psc=PacketSeqCtrl(seq_flags=, seq_count=17), ccsds_version=0) + >>> struct.pack("!I", req_id.as_u32()).hex(sep=",") + '10,22,c0,11' + """ + def __init__( self, tc_packet_id: PacketId, tc_psc: PacketSeqCtrl, ccsds_version: int = 0b000 ): diff --git a/spacepackets/ecss/tm.py b/spacepackets/ecss/tm.py index 4f91791..9bdaadf 100644 --- a/spacepackets/ecss/tm.py +++ b/spacepackets/ecss/tm.py @@ -172,7 +172,18 @@ class PusTelemetry: Can be used to generate TM packets using a high level interface with the default constructor, or to deserialize TM packets from a raw byte stream using the :py:meth:`unpack` method. - This implementation only supports PUS C + This implementation only supports PUS C. + + The following doc example cuts off the timestamp (7 byte CDS Short) and the CRC16 from the ping + packet because those change regularly. + + >>> ping_tm = PusTelemetry(service=17, subservice=2, seq_count=5, apid=0x01) + >>> ping_tm.service + 17 + >>> ping_tm.subservice + 2 + >>> ping_tm.pack()[:-9].hex(sep=',') + '08,01,c0,05,00,0f,20,11,02,00,00,00,00' """ CDS_SHORT_SIZE = 7 From 9f92d45787df33fbf1326f853214abef6947c543 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 12:10:47 +0200 Subject: [PATCH 28/46] some corrections --- spacepackets/ecss/req_id.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spacepackets/ecss/req_id.py b/spacepackets/ecss/req_id.py index d5a794b..ff3170a 100644 --- a/spacepackets/ecss/req_id.py +++ b/spacepackets/ecss/req_id.py @@ -7,8 +7,9 @@ class RequestId: - """The request ID which is used to identify PUS telecommands. It is primarily used - to verify the execution of sent telecommands + """The request ID which is used to identify PUS telecommands. The request ID consists of + the first two bytes of the CCSDS primary header. It is primarily used to verify the execution + of sent telecommands. >>> from spacepackets.ccsds import PacketTypes, SequenceFlags >>> packet_id = PacketId(ptype=PacketTypes.TC, sec_header_flag=False, apid=0x22) From e0b5d8b3641ab03614cac63fbb84015965d11358 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 12:18:45 +0200 Subject: [PATCH 29/46] added some re-exports --- spacepackets/__init__.py | 3 +++ spacepackets/ccsds/__init__.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spacepackets/__init__.py b/spacepackets/__init__.py index 38a2978..468ae12 100644 --- a/spacepackets/__init__.py +++ b/spacepackets/__init__.py @@ -1 +1,4 @@ +from spacepackets.ccsds import SpacePacketHeader, SpacePacket, PacketTypes, SequenceFlags + + __version__ = "0.13.0rc1" diff --git a/spacepackets/ccsds/__init__.py b/spacepackets/ccsds/__init__.py index 874d650..168efa0 100644 --- a/spacepackets/ccsds/__init__.py +++ b/spacepackets/ccsds/__init__.py @@ -1,3 +1,10 @@ """This package contains all CCSDS related components""" -from .spacepacket import SpacePacketHeader, SpacePacket, PacketTypes, SequenceFlags +from .spacepacket import ( + SpacePacketHeader, + SpacePacket, + PacketTypes, + SequenceFlags, + PacketId, + PacketSeqCtrl, +) from .time import CdsShortTimestamp From b1d140e61e1c639db085a78ff812c22e5b44341c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 13:13:56 +0200 Subject: [PATCH 30/46] improve finished PDUs --- spacepackets/__init__.py | 7 ++- spacepackets/cfdp/pdu/finished.py | 77 ++++++++++++++++++------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/spacepackets/__init__.py b/spacepackets/__init__.py index 468ae12..04bef56 100644 --- a/spacepackets/__init__.py +++ b/spacepackets/__init__.py @@ -1,4 +1,9 @@ -from spacepackets.ccsds import SpacePacketHeader, SpacePacket, PacketTypes, SequenceFlags +from spacepackets.ccsds import ( + SpacePacketHeader, + SpacePacket, + PacketTypes, + SequenceFlags, +) __version__ = "0.13.0rc1" diff --git a/spacepackets/cfdp/pdu/finished.py b/spacepackets/cfdp/pdu/finished.py index 6a266b0..6d603ee 100644 --- a/spacepackets/cfdp/pdu/finished.py +++ b/spacepackets/cfdp/pdu/finished.py @@ -1,5 +1,6 @@ from __future__ import annotations import enum +from dataclasses import dataclass from typing import List, Optional, cast from spacepackets.cfdp.pdu import PduHeader @@ -26,17 +27,30 @@ class FileDeliveryStatus(enum.IntEnum): FILE_STATUS_UNREPORTED = 3 +@dataclass +class FinishedParams: + delivery_code: DeliveryCode + delivery_status: FileDeliveryStatus + condition_code: ConditionCode + file_store_responses: Optional[List[FileStoreResponseTlv]] = None + fault_location: Optional[EntityIdTlv] = None + + @classmethod + def empty(cls) -> FinishedParams: + return cls( + delivery_code=DeliveryCode.DATA_COMPLETE, + delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, + condition_code=ConditionCode.NO_ERROR, + ) + + class FinishedPdu(AbstractFileDirectiveBase): """Encapsulates the Finished file directive PDU, see CCSDS 727.0-B-5 p.80""" def __init__( self, - delivery_code: DeliveryCode, - file_delivery_status: FileDeliveryStatus, - condition_code: ConditionCode, + params: FinishedParams, pdu_conf: PduConfig, - file_store_responses: Optional[List[FileStoreResponseTlv]] = None, - fault_location: Optional[EntityIdTlv] = None, ): self.pdu_file_directive = FileDirectivePduBase( directive_code=DirectiveType.FINISHED_PDU, @@ -46,11 +60,7 @@ def __init__( self._fault_location = None self._file_store_responses = [] self._might_have_fault_location = False - self.condition_code = condition_code - self.delivery_code = delivery_code - self.fault_location = fault_location - self.file_store_responses = file_store_responses - self.file_delivery_status = file_delivery_status + self._params = params @property def directive_type(self) -> DirectiveType: @@ -62,7 +72,7 @@ def pdu_header(self) -> PduHeader: @property def condition_code(self) -> ConditionCode: - return self._condition_code + return self._params.condition_code @condition_code.setter def condition_code(self, condition_code: ConditionCode): @@ -73,7 +83,7 @@ def condition_code(self, condition_code: ConditionCode): self._might_have_fault_location = False else: self._might_have_fault_location = True - self._condition_code = condition_code + self._params.condition_code = condition_code @property def packet_len(self) -> int: @@ -81,7 +91,7 @@ def packet_len(self) -> int: @property def file_store_responses(self) -> List[FileStoreResponseTlv]: - return self._file_store_responses + return self._params.file_store_responses @file_store_responses.setter def file_store_responses( @@ -93,29 +103,29 @@ def file_store_responses( :return: """ if file_store_responses is None: - self._file_store_responses = [] + self._params.file_store_responses = [] self.pdu_file_directive.directive_param_field_len = ( 1 + self.fault_location_len ) return - self._file_store_responses = file_store_responses + self._params.file_store_responses = file_store_responses self.pdu_file_directive.directive_param_field_len = ( 1 + self.fault_location_len + self.file_store_responses_len ) @property def file_store_responses_len(self): - if not self._file_store_responses: + if not self._params.file_store_responses: return 0 else: file_store_responses_len = 0 - for file_store_response in self._file_store_responses: + for file_store_response in self._params.file_store_responses: file_store_responses_len += file_store_response.packet_len return file_store_responses_len @property def fault_location(self) -> Optional[EntityIdTlv]: - return self._fault_location + return self._params.fault_location @fault_location.setter def fault_location(self, fault_location: Optional[EntityIdTlv]): @@ -129,31 +139,29 @@ def fault_location(self, fault_location: Optional[EntityIdTlv]): self.pdu_file_directive.directive_param_field_len = ( 1 + fault_loc_len + self.file_store_responses_len ) - self._fault_location = fault_location + self._params.fault_location = fault_location @property def fault_location_len(self): - if self._fault_location is None: + if self._params.fault_location is None: return 0 else: - return self._fault_location.packet_len + return self._params.fault_location.packet_len @classmethod def __empty(cls) -> FinishedPdu: empty_conf = PduConfig.empty() return cls( - delivery_code=DeliveryCode.DATA_INCOMPLETE, - file_delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, - condition_code=ConditionCode.NO_ERROR, + params=FinishedParams.empty(), pdu_conf=empty_conf, ) def pack(self) -> bytearray: packet = self.pdu_file_directive.pack() packet.append( - (self.condition_code << 4) - | (self.delivery_code << 2) - | self.file_delivery_status + (self._params.condition_code << 4) + | (self._params.delivery_code << 2) + | self._params.delivery_status ) for file_store_reponse in self.file_store_responses: packet.extend(file_store_reponse.pack()) @@ -163,7 +171,8 @@ def pack(self) -> bytearray: @classmethod def unpack(cls, raw_packet: bytearray) -> FinishedPdu: - """Unpack a raw packet into a PDU object + """Unpack a raw packet into a PDU object. + :param raw_packet: :raise ValueError: If packet is too short :return: @@ -179,9 +188,9 @@ def unpack(cls, raw_packet: bytearray) -> FinishedPdu: raise ValueError current_idx = finished_pdu.pdu_file_directive.header_len first_param_byte = raw_packet[current_idx] - finished_pdu.condition_code = (first_param_byte & 0xF0) >> 4 - finished_pdu.delivery_code = (first_param_byte & 0x04) >> 2 - finished_pdu.file_delivery_status = first_param_byte & 0b11 + finished_pdu._params.condition_code = (first_param_byte & 0xF0) >> 4 + finished_pdu._params.delivery_code = (first_param_byte & 0x04) >> 2 + finished_pdu._params.file_delivery_status = first_param_byte & 0b11 current_idx += 1 if len(raw_packet) > current_idx: finished_pdu._unpack_tlvs( @@ -217,3 +226,9 @@ def _unpack_tlvs(self, rest_of_packet: bytearray) -> int: raise ValueError if current_idx >= len(rest_of_packet): break + + def __eq__(self, other: FinishedPdu): + pass + + def __repr__(self): + pass From a24f240cfbc08db903c6841d044e409e72c103d1 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 14:32:44 +0200 Subject: [PATCH 31/46] refactored finished PDU --- spacepackets/cfdp/pdu/finished.py | 87 ++++++++++++++++++----------- tests/cfdp/pdus/test_finish_pdu.py | 40 ++++++++----- tests/cfdp/pdus/test_pdu_wrapper.py | 13 ++++- 3 files changed, 90 insertions(+), 50 deletions(-) diff --git a/spacepackets/cfdp/pdu/finished.py b/spacepackets/cfdp/pdu/finished.py index 6d603ee..d5e1a8a 100644 --- a/spacepackets/cfdp/pdu/finished.py +++ b/spacepackets/cfdp/pdu/finished.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum from dataclasses import dataclass -from typing import List, Optional, cast +from typing import List, Optional from spacepackets.cfdp.pdu import PduHeader from spacepackets.cfdp.pdu.file_directive import ( @@ -12,7 +12,6 @@ from spacepackets.cfdp.defs import ConditionCode from spacepackets.cfdp.conf import check_packet_length, PduConfig from spacepackets.cfdp.tlv import TlvTypes, FileStoreResponseTlv, EntityIdTlv -from spacepackets.log import get_console_logger class DeliveryCode(enum.IntEnum): @@ -57,10 +56,11 @@ def __init__( pdu_conf=pdu_conf, directive_param_field_len=1, ) - self._fault_location = None - self._file_store_responses = [] - self._might_have_fault_location = False self._params = params + if params.fault_location is not None: + self.fault_location = self._params.fault_location + if params.file_store_responses is not None: + self.file_store_responses = self._params.file_store_responses @property def directive_type(self) -> DirectiveType: @@ -76,15 +76,16 @@ def condition_code(self) -> ConditionCode: @condition_code.setter def condition_code(self, condition_code: ConditionCode): - if condition_code in [ - ConditionCode.NO_ERROR, - ConditionCode.UNSUPPORTED_CHECKSUM_TYPE, - ]: - self._might_have_fault_location = False - else: - self._might_have_fault_location = True self._params.condition_code = condition_code + @property + def delivery_code(self) -> DeliveryCode: + return self._params.delivery_code + + @property + def delivery_status(self) -> FileDeliveryStatus: + return self._params.delivery_status + @property def packet_len(self) -> int: return self.pdu_file_directive.packet_len @@ -93,6 +94,15 @@ def packet_len(self) -> int: def file_store_responses(self) -> List[FileStoreResponseTlv]: return self._params.file_store_responses + @property + def might_have_fault_location(self): + if self._params.condition_code in [ + ConditionCode.NO_ERROR, + ConditionCode.UNSUPPORTED_CHECKSUM_TYPE, + ]: + return False + return True + @file_store_responses.setter def file_store_responses( self, file_store_responses: Optional[List[FileStoreResponseTlv]] @@ -163,9 +173,10 @@ def pack(self) -> bytearray: | (self._params.delivery_code << 2) | self._params.delivery_status ) - for file_store_reponse in self.file_store_responses: - packet.extend(file_store_reponse.pack()) - if self.fault_location is not None and self._might_have_fault_location: + if self.file_store_responses is not None: + for file_store_reponse in self.file_store_responses: + packet.extend(file_store_reponse.pack()) + if self.fault_location is not None and self.might_have_fault_location: packet.extend(self.fault_location.pack()) return packet @@ -188,9 +199,13 @@ def unpack(cls, raw_packet: bytearray) -> FinishedPdu: raise ValueError current_idx = finished_pdu.pdu_file_directive.header_len first_param_byte = raw_packet[current_idx] - finished_pdu._params.condition_code = (first_param_byte & 0xF0) >> 4 - finished_pdu._params.delivery_code = (first_param_byte & 0x04) >> 2 - finished_pdu._params.file_delivery_status = first_param_byte & 0b11 + params = FinishedParams( + condition_code=ConditionCode((first_param_byte & 0xF0) >> 4), + delivery_code=DeliveryCode((first_param_byte & 0x04) >> 2), + delivery_status=FileDeliveryStatus(first_param_byte & 0b11), + ) + finished_pdu.condition_code = params.condition_code + finished_pdu._params = params current_idx += 1 if len(raw_packet) > current_idx: finished_pdu._unpack_tlvs( @@ -200,6 +215,8 @@ def unpack(cls, raw_packet: bytearray) -> FinishedPdu: def _unpack_tlvs(self, rest_of_packet: bytearray) -> int: current_idx = 0 + fs_responses_list = [] + fault_loc = None while True: next_tlv_code = rest_of_packet[current_idx] if next_tlv_code == TlvTypes.FILESTORE_RESPONSE: @@ -207,28 +224,32 @@ def _unpack_tlvs(self, rest_of_packet: bytearray) -> int: raw_bytes=rest_of_packet[current_idx:] ) current_idx += next_fs_response.packet_len - self._file_store_responses.append(next_fs_response) + fs_responses_list.append(next_fs_response) elif next_tlv_code == TlvTypes.ENTITY_ID: - if not self._might_have_fault_location: - logger = get_console_logger() - logger.warning( + if not self.might_have_fault_location: + raise ValueError( "Entity ID found in Finished PDU but wrong condition code" ) - raise ValueError - self._fault_location = EntityIdTlv.unpack( - raw_bytes=rest_of_packet[current_idx:] - ) - current_idx += self._fault_location.packet_len - return current_idx + fault_loc = EntityIdTlv.unpack(raw_bytes=rest_of_packet[current_idx:]) + current_idx += fault_loc.packet_len else: - logger = get_console_logger() - logger.warning("Invalid TLV ID in Finished PDU detected") - raise ValueError + raise ValueError("Invalid TLV ID in Finished PDU detected") if current_idx >= len(rest_of_packet): break + if fs_responses_list is not None: + self.file_store_responses = fs_responses_list + if fault_loc is not None: + self.fault_location = fault_loc + return current_idx def __eq__(self, other: FinishedPdu): - pass + return ( + self._params == other._params + and self.pdu_file_directive == other.pdu_file_directive + ) def __repr__(self): - pass + return ( + f"{self.__class__.__name__}(params={self._params!r}, " + f"pdu_conf={self.pdu_file_directive.pdu_conf!r})" + ) diff --git a/tests/cfdp/pdus/test_finish_pdu.py b/tests/cfdp/pdus/test_finish_pdu.py index 6842dbb..7720f56 100644 --- a/tests/cfdp/pdus/test_finish_pdu.py +++ b/tests/cfdp/pdus/test_finish_pdu.py @@ -10,21 +10,28 @@ ) from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.pdu import FinishedPdu -from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus +from spacepackets.cfdp.pdu.finished import ( + DeliveryCode, + FileDeliveryStatus, + FinishedParams, +) class TestFinishPdu(TestCase): def test_finished_pdu(self): pdu_conf = PduConfig.empty() - finish_pdu = FinishedPdu( + params = FinishedParams( delivery_code=DeliveryCode.DATA_COMPLETE, - file_delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, + delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, condition_code=ConditionCode.NO_ERROR, + ) + finish_pdu = FinishedPdu( + params=params, pdu_conf=pdu_conf, ) self.assertEqual(finish_pdu.delivery_code, DeliveryCode.DATA_COMPLETE) self.assertEqual( - finish_pdu.file_delivery_status, FileDeliveryStatus.FILE_STATUS_UNREPORTED + finish_pdu.delivery_status, FileDeliveryStatus.FILE_STATUS_UNREPORTED ) self.assertEqual(finish_pdu.pdu_file_directive.packet_len, 9) finish_pdu_raw = finish_pdu.pack() @@ -38,7 +45,7 @@ def test_finished_pdu(self): finish_pdu_unpacked = FinishedPdu.unpack(raw_packet=finish_pdu_raw) self.assertEqual(finish_pdu_unpacked.delivery_code, DeliveryCode.DATA_COMPLETE) self.assertEqual( - finish_pdu_unpacked.file_delivery_status, + finish_pdu_unpacked.delivery_status, FileDeliveryStatus.FILE_STATUS_UNREPORTED, ) self.assertEqual(finish_pdu_unpacked.pdu_file_directive.packet_len, 9) @@ -62,18 +69,21 @@ def test_finished_pdu(self): # Now generate a packet with a fault location fault_location_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x02])) self.assertEqual(fault_location_tlv.packet_len, 4) - finish_pdu_with_fault_loc = FinishedPdu( + params = FinishedParams( delivery_code=DeliveryCode.DATA_INCOMPLETE, - file_delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, + delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, condition_code=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, fault_location=fault_location_tlv, + ) + finish_pdu_with_fault_loc = FinishedPdu( + params=params, pdu_conf=pdu_conf, ) self.assertEqual( finish_pdu_with_fault_loc.delivery_code, DeliveryCode.DATA_INCOMPLETE ) self.assertEqual( - finish_pdu_with_fault_loc.file_delivery_status, + finish_pdu_with_fault_loc.delivery_status, FileDeliveryStatus.DISCARDED_DELIBERATELY, ) self.assertEqual( @@ -114,13 +124,13 @@ def test_finished_pdu(self): ), ) self.assertEqual(filestore_reponse_1.packet_len, 13) - pdu_with_response = FinishedPdu( + params = FinishedParams( delivery_code=DeliveryCode.DATA_INCOMPLETE, - file_delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, + delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, condition_code=ConditionCode.FILESTORE_REJECTION, - pdu_conf=pdu_conf, file_store_responses=[filestore_reponse_1], ) + pdu_with_response = FinishedPdu(params=params, pdu_conf=pdu_conf) self.assertEqual(pdu_with_response.packet_len, 22) pdu_with_response_raw = pdu_with_response.pack() expected_array = bytearray( @@ -153,14 +163,16 @@ def test_finished_pdu(self): expected_reply.append(0) self.assertEqual(filestore_reponse_2.packet_len, 23) self.assertEqual(fs_response_2_raw, expected_reply) - finish_pdu_two_responses_one_fault_loc = FinishedPdu( + params = FinishedParams( delivery_code=DeliveryCode.DATA_COMPLETE, - file_delivery_status=FileDeliveryStatus.FILE_RETAINED, + delivery_status=FileDeliveryStatus.FILE_RETAINED, condition_code=ConditionCode.CHECK_LIMIT_REACHED, - pdu_conf=pdu_conf, file_store_responses=[filestore_reponse_1, filestore_reponse_2], fault_location=fault_location_tlv, ) + finish_pdu_two_responses_one_fault_loc = FinishedPdu( + params=params, pdu_conf=pdu_conf + ) # length should be 13 (response 1) + 23 (response 2) + 4 (fault loc) + 9 (base) self.assertEqual(finish_pdu_two_responses_one_fault_loc.packet_len, 49) fs_responses = finish_pdu_two_responses_one_fault_loc.file_store_responses diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_wrapper.py index d56007c..139bf54 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_wrapper.py @@ -14,7 +14,11 @@ KeepAlivePdu, ) from spacepackets.cfdp.pdu.file_data import FileDataPdu -from spacepackets.cfdp.pdu.finished import DeliveryCode, FileDeliveryStatus +from spacepackets.cfdp.pdu.finished import ( + DeliveryCode, + FileDeliveryStatus, + FinishedParams, +) from spacepackets.cfdp.pdu.metadata import MetadataParams from spacepackets.cfdp.pdu.prompt import ResponseRequired from spacepackets.cfdp.pdu.helper import PduHolder @@ -114,10 +118,13 @@ def test_eof_cast(self): self.assertEqual(eof_pdu_converted, eof_pdu) def test_finished_cast(self): - finish_pdu = FinishedPdu( + params = FinishedParams( delivery_code=DeliveryCode.DATA_COMPLETE, - file_delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, + delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, condition_code=ConditionCode.NO_ERROR, + ) + finish_pdu = FinishedPdu( + params=params, pdu_conf=self.pdu_conf, ) self.pdu_wrapper.base = finish_pdu From 6c688993f703c44326513c8b542b750508d60ac0 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 14:43:24 +0200 Subject: [PATCH 32/46] added repr for PduHeader --- spacepackets/cfdp/pdu/header.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 38d783d..6d37778 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -334,3 +334,11 @@ def check_len_in_bytes(detected_len: int) -> LenInBytes: ) raise ValueError return len_in_bytes + + def __repr__(self): + return ( + f"{self.__class__.__name__}(pdu_type={self.pdu_type!r}," + f"segment_metadata_flag={self.segment_metadata_flag!r}," + f"pdu_data_field_len={self.pdu_data_field_len!r}," + f"pdu_conf={self.pdu_conf!r})" + ) From c2a963e05025d4369fe4e64ad1d5ee148bc0c9b6 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 14:45:01 +0200 Subject: [PATCH 33/46] removed some logger usage --- spacepackets/cfdp/pdu/header.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 6d37778..d81024e 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -2,7 +2,6 @@ import abc -from spacepackets.log import get_console_logger from spacepackets.cfdp.defs import ( LargeFileFlag, PduType, @@ -281,9 +280,7 @@ def unpack(cls, raw_packet: bytes) -> PduHeader: :return: """ if len(raw_packet) < cls.FIXED_LENGTH: - logger = get_console_logger() - logger.warning("Can not unpack less than four bytes into PDU header") - raise ValueError + raise ValueError("Can not unpack less than four bytes into PDU header") pdu_header = cls.__empty() pdu_header._pdu_type = (raw_packet[0] & 0x10) >> 4 pdu_header.direction = (raw_packet[0] & 0x08) >> 3 @@ -327,12 +324,10 @@ def check_len_in_bytes(detected_len: int) -> LenInBytes: try: len_in_bytes = LenInBytes(detected_len) except ValueError: - logger = get_console_logger() - logger.warning( + raise ValueError( "Unsupported length field detected. " "Only 1, 2, 4 and 8 bytes are supported" ) - raise ValueError return len_in_bytes def __repr__(self): From 4756e0d373417da9ec030aa3c579de8c369ea250 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 14:48:51 +0200 Subject: [PATCH 34/46] added some useful dunders --- spacepackets/cfdp/pdu/file_directive.py | 9 ++++++++- spacepackets/cfdp/pdu/keep_alive.py | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 0bddc24..8ef4a65 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -98,10 +98,10 @@ def __eq__(self, other: AbstractFileDirectiveBase): class FileDirectivePduBase(AbstractFileDirectiveBase): - FILE_DIRECTIVE_PDU_LEN = 5 """Base class for file directive PDUs encapsulating all its common components. All other file directive PDU classes implement this class """ + FILE_DIRECTIVE_PDU_LEN = 5 def __init__( self, @@ -215,5 +215,12 @@ def parse_fss_field(self, raw_packet: bytes, current_idx: int) -> (int, int): current_idx += 4 return current_idx, file_size + def __repr__(self): + return ( + f"{self.__class__.__name__}(directive_code={self.directive_type!r}, " + f"directive_param_field_len={self.directive_param_field_len!r}, " + f"pdu_conf={self.pdu_conf!r})" + ) + def __eq__(self, other: FileDirectivePduBase): return AbstractFileDirectiveBase.__eq__(self, other) diff --git a/spacepackets/cfdp/pdu/keep_alive.py b/spacepackets/cfdp/pdu/keep_alive.py index caf3443..3f4e703 100644 --- a/spacepackets/cfdp/pdu/keep_alive.py +++ b/spacepackets/cfdp/pdu/keep_alive.py @@ -92,3 +92,9 @@ def __eq__(self, other: KeepAlivePdu): self.pdu_file_directive == other.pdu_file_directive and self.progress == other.progress ) + + def __repr__(self): + return ( + f"{self.__class__.__name__}(progress={self.progress!r}, " + f"pdu_conf={self.pdu_file_directive.pdu_conf!r})" + ) From 445cd5206ca280db83c69be6cb2d6bb7e9e1022a Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 14:55:57 +0200 Subject: [PATCH 35/46] split up finished unittest --- spacepackets/cfdp/pdu/file_directive.py | 1 + tests/cfdp/pdus/test_finish_pdu.py | 73 ++++++++++++++++--------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/spacepackets/cfdp/pdu/file_directive.py b/spacepackets/cfdp/pdu/file_directive.py index 8ef4a65..848be70 100644 --- a/spacepackets/cfdp/pdu/file_directive.py +++ b/spacepackets/cfdp/pdu/file_directive.py @@ -101,6 +101,7 @@ class FileDirectivePduBase(AbstractFileDirectiveBase): """Base class for file directive PDUs encapsulating all its common components. All other file directive PDU classes implement this class """ + FILE_DIRECTIVE_PDU_LEN = 5 def __init__( diff --git a/tests/cfdp/pdus/test_finish_pdu.py b/tests/cfdp/pdus/test_finish_pdu.py index 7720f56..dd88dbf 100644 --- a/tests/cfdp/pdus/test_finish_pdu.py +++ b/tests/cfdp/pdus/test_finish_pdu.py @@ -18,16 +18,24 @@ class TestFinishPdu(TestCase): - def test_finished_pdu(self): - pdu_conf = PduConfig.empty() - params = FinishedParams( + def setUp(self) -> None: + self.pdu_conf = PduConfig.empty() + self.params = FinishedParams( delivery_code=DeliveryCode.DATA_COMPLETE, delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, condition_code=ConditionCode.NO_ERROR, ) + self.filestore_reponse_1 = FileStoreResponseTlv( + action_code=FilestoreActionCode.REMOVE_DIR_SNN, + first_file_name="test.txt", + status_code=FilestoreResponseStatusCode.REMOVE_DIR_SUCCESS, + ) + self.fault_location_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x02])) + + def test_basic(self): finish_pdu = FinishedPdu( - params=params, - pdu_conf=pdu_conf, + params=self.params, + pdu_conf=self.pdu_conf, ) self.assertEqual(finish_pdu.delivery_code, DeliveryCode.DATA_COMPLETE) self.assertEqual( @@ -42,6 +50,13 @@ def test_finished_pdu(self): finish_pdu_raw, bytes([0x20, 0x00, 0x02, 0x11, 0x00, 0x00, 0x00, 0x05, 0x03]), ) + + def test_unpack_basic(self): + finish_pdu = FinishedPdu( + params=self.params, + pdu_conf=self.pdu_conf, + ) + finish_pdu_raw = finish_pdu.pack() finish_pdu_unpacked = FinishedPdu.unpack(raw_packet=finish_pdu_raw) self.assertEqual(finish_pdu_unpacked.delivery_code, DeliveryCode.DATA_COMPLETE) self.assertEqual( @@ -49,14 +64,23 @@ def test_finished_pdu(self): FileDeliveryStatus.FILE_STATUS_UNREPORTED, ) self.assertEqual(finish_pdu_unpacked.pdu_file_directive.packet_len, 9) + self.assertEqual(finish_pdu_unpacked, finish_pdu) finish_pdu_repacked = finish_pdu_unpacked.pack() self.assertEqual(finish_pdu.pdu_file_directive.packet_len, 9) self.assertEqual(finish_pdu_repacked, finish_pdu_raw) + + def test_unpack_failure(self): + finish_pdu = FinishedPdu( + params=self.params, + pdu_conf=self.pdu_conf, + ) + finish_pdu_raw = finish_pdu.pack() + finish_pdu_unpacked = FinishedPdu.unpack(raw_packet=finish_pdu_raw) + finish_pdu_repacked = finish_pdu_unpacked.pack() finish_pdu_repacked = finish_pdu_repacked[:-1] self.assertRaises( ValueError, FinishedPdu.unpack, raw_packet=finish_pdu_repacked ) - invalid_fault_source = EntityIdTlv(entity_id=bytes([0x0])) finish_pdu_raw.extend(invalid_fault_source.pack()) current_size = finish_pdu_raw[1] << 8 | finish_pdu_raw[2] @@ -66,18 +90,18 @@ def test_finished_pdu(self): with self.assertRaises(ValueError): FinishedPdu.unpack(raw_packet=finish_pdu_raw) + def test_with_fault_location(self): # Now generate a packet with a fault location - fault_location_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x02])) - self.assertEqual(fault_location_tlv.packet_len, 4) + self.assertEqual(self.fault_location_tlv.packet_len, 4) params = FinishedParams( delivery_code=DeliveryCode.DATA_INCOMPLETE, delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, condition_code=ConditionCode.POSITIVE_ACK_LIMIT_REACHED, - fault_location=fault_location_tlv, + fault_location=self.fault_location_tlv, ) finish_pdu_with_fault_loc = FinishedPdu( params=params, - pdu_conf=pdu_conf, + pdu_conf=self.pdu_conf, ) self.assertEqual( finish_pdu_with_fault_loc.delivery_code, DeliveryCode.DATA_INCOMPLETE @@ -90,19 +114,17 @@ def test_finished_pdu(self): finish_pdu_with_fault_loc.condition_code, ConditionCode.POSITIVE_ACK_LIMIT_REACHED, ) - self.assertEqual(finish_pdu_with_fault_loc.fault_location, fault_location_tlv) + self.assertEqual( + finish_pdu_with_fault_loc.fault_location, self.fault_location_tlv + ) # 4 additional bytes because the entity ID in the TLV has 2 bytes self.assertEqual(finish_pdu_with_fault_loc.packet_len, 13) self.assertEqual(len(finish_pdu_with_fault_loc.pack()), 13) self.assertEqual(finish_pdu_with_fault_loc.fault_location_len, 4) + def test_with_fs_response(self): # Now create a packet with filestore responses - filestore_reponse_1 = FileStoreResponseTlv( - action_code=FilestoreActionCode.REMOVE_DIR_SNN, - first_file_name="test.txt", - status_code=FilestoreResponseStatusCode.REMOVE_DIR_SUCCESS, - ) - filestore_response_1_packed = filestore_reponse_1.pack() + filestore_response_1_packed = self.filestore_reponse_1.pack() self.assertEqual( filestore_response_1_packed, bytes( @@ -123,14 +145,14 @@ def test_finished_pdu(self): ] ), ) - self.assertEqual(filestore_reponse_1.packet_len, 13) + self.assertEqual(self.filestore_reponse_1.packet_len, 13) params = FinishedParams( delivery_code=DeliveryCode.DATA_INCOMPLETE, delivery_status=FileDeliveryStatus.DISCARDED_DELIBERATELY, condition_code=ConditionCode.FILESTORE_REJECTION, - file_store_responses=[filestore_reponse_1], + file_store_responses=[self.filestore_reponse_1], ) - pdu_with_response = FinishedPdu(params=params, pdu_conf=pdu_conf) + pdu_with_response = FinishedPdu(params=params, pdu_conf=self.pdu_conf) self.assertEqual(pdu_with_response.packet_len, 22) pdu_with_response_raw = pdu_with_response.pack() expected_array = bytearray( @@ -143,6 +165,7 @@ def test_finished_pdu(self): ) self.assertEqual(len(pdu_with_response_unpacked.file_store_responses), 1) + def test_finished_pdu(self): # Pack with 2 responses and 1 fault location first_file = "test.txt" second_file = "test2.txt" @@ -167,11 +190,11 @@ def test_finished_pdu(self): delivery_code=DeliveryCode.DATA_COMPLETE, delivery_status=FileDeliveryStatus.FILE_RETAINED, condition_code=ConditionCode.CHECK_LIMIT_REACHED, - file_store_responses=[filestore_reponse_1, filestore_reponse_2], - fault_location=fault_location_tlv, + file_store_responses=[self.filestore_reponse_1, filestore_reponse_2], + fault_location=self.fault_location_tlv, ) finish_pdu_two_responses_one_fault_loc = FinishedPdu( - params=params, pdu_conf=pdu_conf + params=params, pdu_conf=self.pdu_conf ) # length should be 13 (response 1) + 23 (response 2) + 4 (fault loc) + 9 (base) self.assertEqual(finish_pdu_two_responses_one_fault_loc.packet_len, 49) @@ -180,9 +203,9 @@ def test_finished_pdu(self): complex_pdu_raw = finish_pdu_two_responses_one_fault_loc.pack() complex_pdu_unpacked = FinishedPdu.unpack(raw_packet=complex_pdu_raw) self.assertEqual( - complex_pdu_unpacked.fault_location.pack(), fault_location_tlv.pack() + complex_pdu_unpacked.fault_location.pack(), self.fault_location_tlv.pack() ) - self.assertEqual(filestore_reponse_1.pack(), fs_responses[0].pack()) + self.assertEqual(self.filestore_reponse_1.pack(), fs_responses[0].pack()) self.assertEqual(filestore_reponse_2.pack(), fs_responses[1].pack()) # Change TLV type to make it invalid From 21b6cf9b276e9770fd608f9dc0e4aa5a1b0eba15 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 17:17:06 +0200 Subject: [PATCH 36/46] create default func for pdu config - Also add ByteFieldEmpty class --- spacepackets/cfdp/conf.py | 17 ++++++++++++++++- spacepackets/cfdp/pdu/metadata.py | 4 ++++ spacepackets/util.py | 19 ++++++++++++++----- tests/cfdp/pdus/test_directive.py | 2 +- tests/cfdp/pdus/test_eof_pdu.py | 13 +++++++------ tests/cfdp/pdus/test_file_data.py | 2 +- tests/cfdp/pdus/test_finish_pdu.py | 2 +- tests/cfdp/pdus/test_keep_alive_pdu.py | 2 +- tests/cfdp/pdus/test_metadata.py | 2 +- tests/cfdp/pdus/test_prompt_pdu.py | 2 +- 10 files changed, 47 insertions(+), 18 deletions(-) diff --git a/spacepackets/cfdp/conf.py b/spacepackets/cfdp/conf.py index 9407a75..33396c9 100644 --- a/spacepackets/cfdp/conf.py +++ b/spacepackets/cfdp/conf.py @@ -10,7 +10,7 @@ SegmentationControl, ) from spacepackets.log import get_console_logger -from spacepackets.util import UnsignedByteField, ByteFieldU8 +from spacepackets.util import UnsignedByteField, ByteFieldU8, ByteFieldEmpty @dataclass @@ -35,6 +35,21 @@ class PduConfig: @classmethod def empty(cls) -> PduConfig: + """Empty PDU configuration which is not valid for usage because the contained unsigned + byte fields are empty (sequence number and both entity IDs) + """ + return PduConfig( + transaction_seq_num=ByteFieldEmpty(), + trans_mode=TransmissionModes.ACKNOWLEDGED, + source_entity_id=ByteFieldEmpty(), + dest_entity_id=ByteFieldEmpty(), + file_flag=LargeFileFlag.NORMAL, + crc_flag=CrcFlag.NO_CRC, + ) + + @classmethod + def default(cls): + """Valid PDU configuration""" return PduConfig( transaction_seq_num=ByteFieldU8(0), trans_mode=TransmissionModes.ACKNOWLEDGED, diff --git a/spacepackets/cfdp/pdu/metadata.py b/spacepackets/cfdp/pdu/metadata.py index 28d8703..9f29183 100644 --- a/spacepackets/cfdp/pdu/metadata.py +++ b/spacepackets/cfdp/pdu/metadata.py @@ -70,6 +70,10 @@ def pdu_header(self) -> PduHeader: def closure_requested(self) -> bool: return self.params.closure_requested + @property + def file_size(self) -> int: + return self.params.file_size + @property def checksum_type(self) -> ChecksumTypes: return self.params.checksum_type diff --git a/spacepackets/util.py b/spacepackets/util.py index 37c260e..241c127 100644 --- a/spacepackets/util.py +++ b/spacepackets/util.py @@ -76,8 +76,10 @@ def unsigned_struct_specifier(byte_num: int) -> str: def to_signed(byte_num: int, val: int) -> bytes: """Convert number of bytes in a field to the struct API signed format specifier, assuming network endianness. Raises value error if number is not inside [1, 2, 4, 8]""" - if byte_num not in [1, 2, 4, 8]: - raise ValueError("Invalid byte number, must be one of [1, 2, 4, 8]") + if byte_num not in [0, 1, 2, 4, 8]: + raise ValueError("Invalid byte number, must be one of [0, 1, 2, 4, 8]") + if byte_num == 0: + return bytes() if abs(val) > pow(2, (byte_num * 8) - 1) - 1: raise ValueError( f"Passed value larger than allows {pow(2, (byte_num * 8) - 1) - 1}" @@ -88,8 +90,10 @@ def to_signed(byte_num: int, val: int) -> bytes: def to_unsigned(byte_num: int, val: int) -> bytes: """Convert number of bytes in a field to the struct API unsigned format specifier, assuming network endianness. Raises value error if number is not inside [1, 2, 4, 8]""" - if byte_num not in [1, 2, 4, 8]: + if byte_num not in [0, 1, 2, 4, 8]: raise ValueError("Invalid byte number, must be one of [1, 2, 4, 8]") + if byte_num == 0: + return bytes() if val > pow(2, byte_num * 8) - 1: raise ValueError(f"Passed value larger than allows {pow(2, byte_num) - 1}") return struct.pack(IntByteConversion.unsigned_struct_specifier(byte_num), val) @@ -149,10 +153,10 @@ def as_bytes(self) -> bytes: @staticmethod def verify_byte_len(byte_len: int): - if byte_len not in [1, 2, 4, 8]: + if byte_len not in [0, 1, 2, 4, 8]: # I really have no idea why anyone would use other values than these raise ValueError( - "Only 1, 2, 4 and 8 bytes are allowed as an entity ID length" + "Only 0, 1, 2, 4 and 8 bytes are allowed as an entity ID length" ) def _verify_int_value(self, val: int): @@ -190,6 +194,11 @@ def __hash__(self): return hash((self.value, self.byte_len)) +class ByteFieldEmpty(UnsignedByteField): + def __init__(self, val: int = 0): + super().__init__(0, val) + + class ByteFieldU8(UnsignedByteField): """Concrete variant of a variable length byte field which has a length of 1 byte""" diff --git a/tests/cfdp/pdus/test_directive.py b/tests/cfdp/pdus/test_directive.py index d71b0e5..ec09265 100644 --- a/tests/cfdp/pdus/test_directive.py +++ b/tests/cfdp/pdus/test_directive.py @@ -7,7 +7,7 @@ class TestDirective(TestCase): def test_file_directive(self): - pdu_conf = PduConfig.empty() + pdu_conf = PduConfig.default() file_directive_header = FileDirectivePduBase( directive_code=DirectiveType.METADATA_PDU, pdu_conf=pdu_conf, diff --git a/tests/cfdp/pdus/test_eof_pdu.py b/tests/cfdp/pdus/test_eof_pdu.py index ceaa981..3d11018 100644 --- a/tests/cfdp/pdus/test_eof_pdu.py +++ b/tests/cfdp/pdus/test_eof_pdu.py @@ -1,22 +1,23 @@ from unittest import TestCase -from spacepackets.cfdp import LargeFileFlag, EntityIdTlv +from spacepackets.cfdp import LargeFileFlag, EntityIdTlv, NULL_CHECKSUM_U32 from spacepackets.cfdp.conf import PduConfig from spacepackets.cfdp.pdu import EofPdu class TestEofPdu(TestCase): def test_eof_pdu(self): - pdu_conf = PduConfig.empty() - zero_checksum = bytes([0x00, 0x00, 0x00, 0x00]) - eof_pdu = EofPdu(file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf) + pdu_conf = PduConfig.default() + eof_pdu = EofPdu( + file_checksum=NULL_CHECKSUM_U32, file_size=0, pdu_conf=pdu_conf + ) self.assertEqual(eof_pdu.pdu_file_directive.header_len, 8) expected_packet_len = 8 + 1 + 4 + 4 self.assertEqual(eof_pdu.packet_len, expected_packet_len) eof_pdu_raw = eof_pdu.pack() expected_header = bytearray([0x20, 0x00, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x04]) expected_header.append(0) - expected_header.extend(zero_checksum) + expected_header.extend(NULL_CHECKSUM_U32) # File size is 0 as 4 bytes expected_header.extend(bytes([0x00, 0x00, 0x00, 0x00])) self.assertEqual(eof_pdu_raw, expected_header) @@ -45,7 +46,7 @@ def test_eof_pdu(self): pdu_conf.file_flag = LargeFileFlag.LARGE eof_pdu_large_file = EofPdu( - file_checksum=zero_checksum, file_size=0, pdu_conf=pdu_conf + file_checksum=NULL_CHECKSUM_U32, file_size=0, pdu_conf=pdu_conf ) self.assertEqual(eof_pdu_large_file.packet_len, expected_packet_len + 4) eof_pdu_large_file_raw = eof_pdu_large_file.pack() diff --git a/tests/cfdp/pdus/test_file_data.py b/tests/cfdp/pdus/test_file_data.py index 0301af3..0fb7881 100644 --- a/tests/cfdp/pdus/test_file_data.py +++ b/tests/cfdp/pdus/test_file_data.py @@ -9,7 +9,7 @@ class TestFileDataPdu(TestCase): def test_file_data_pdu(self): - pdu_conf = PduConfig.empty() + pdu_conf = PduConfig.default() file_data = "hello world" file_data_bytes = file_data.encode() file_data_pdu = FileDataPdu( diff --git a/tests/cfdp/pdus/test_finish_pdu.py b/tests/cfdp/pdus/test_finish_pdu.py index dd88dbf..da842fb 100644 --- a/tests/cfdp/pdus/test_finish_pdu.py +++ b/tests/cfdp/pdus/test_finish_pdu.py @@ -19,7 +19,7 @@ class TestFinishPdu(TestCase): def setUp(self) -> None: - self.pdu_conf = PduConfig.empty() + self.pdu_conf = PduConfig.default() self.params = FinishedParams( delivery_code=DeliveryCode.DATA_COMPLETE, delivery_status=FileDeliveryStatus.FILE_STATUS_UNREPORTED, diff --git a/tests/cfdp/pdus/test_keep_alive_pdu.py b/tests/cfdp/pdus/test_keep_alive_pdu.py index 05eff2f..0f2f924 100644 --- a/tests/cfdp/pdus/test_keep_alive_pdu.py +++ b/tests/cfdp/pdus/test_keep_alive_pdu.py @@ -7,7 +7,7 @@ class TestKeepAlivePdu(TestCase): def test_keep_alive_pdu(self): - pdu_conf = PduConfig.empty() + pdu_conf = PduConfig.default() keep_alive_pdu = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) self.assertEqual(keep_alive_pdu.progress, 0) self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) diff --git a/tests/cfdp/pdus/test_metadata.py b/tests/cfdp/pdus/test_metadata.py index b53efb1..96b47a5 100644 --- a/tests/cfdp/pdus/test_metadata.py +++ b/tests/cfdp/pdus/test_metadata.py @@ -15,7 +15,7 @@ class TestMetadata(TestCase): def test_metadata_pdu(self): - pdu_conf = PduConfig.empty() + pdu_conf = PduConfig.default() metadata_params = MetadataParams( closure_requested=False, file_size=2, diff --git a/tests/cfdp/pdus/test_prompt_pdu.py b/tests/cfdp/pdus/test_prompt_pdu.py index 66677f4..aa91e58 100644 --- a/tests/cfdp/pdus/test_prompt_pdu.py +++ b/tests/cfdp/pdus/test_prompt_pdu.py @@ -7,7 +7,7 @@ class TestPromptPdu(TestCase): def test_prompt_pdu(self): - pdu_conf = PduConfig.empty() + pdu_conf = PduConfig.default() prompt_pdu = PromptPdu( pdu_conf=pdu_conf, response_required=ResponseRequired.KEEP_ALIVE ) From ed56e7a377fae934b7d5f30b82d8964ddd9d47da Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:12:33 +0200 Subject: [PATCH 37/46] various smaller improvements --- spacepackets/cfdp/lv.py | 6 ++++++ spacepackets/cfdp/pdu/header.py | 10 ++++++---- spacepackets/cfdp/tlv.py | 11 ++++++++++- spacepackets/ecss/tc.py | 5 ----- spacepackets/ecss/tm.py | 22 ++++++---------------- tests/cfdp/pdus/test_metadata.py | 4 ++-- tests/cfdp/test_tlvs.py | 14 +++++++------- 7 files changed, 37 insertions(+), 35 deletions(-) diff --git a/spacepackets/cfdp/lv.py b/spacepackets/cfdp/lv.py index c7bbd6d..7c6240e 100644 --- a/spacepackets/cfdp/lv.py +++ b/spacepackets/cfdp/lv.py @@ -43,3 +43,9 @@ def unpack(cls, raw_bytes: bytes) -> CfdpLv: if detected_len == 0: return cls(value=bytes()) return cls(value=raw_bytes[1 : 1 + detected_len]) + + def __repr__(self): + return f"{self.__class__.__name__}(value={self.value!r})" + + def __str__(self): + return f"CFDP LV with data 0x[{self.value.hex(sep=',')} of length {len(self.value)}" diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index d81024e..02919ab 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -19,8 +19,6 @@ class AbstractPduBase(abc.ABC): - VERSION_BITS = 0b0010_0000 - FIXED_LENGTH = 4 """Encapsulate common functions for PDU. PDU or Packet Data Units are the base data unit which are exchanged for CFDP procedures. Each PDU has a common header and this class provides abstract methods to access fields of that common header. @@ -29,6 +27,9 @@ class AbstractPduBase(abc.ABC): :py:class:`PduHeader` class. """ + VERSION_BITS = 0b0010_0000 + FIXED_LENGTH = 4 + @abc.abstractmethod def pack(self) -> bytes: pass @@ -220,7 +221,8 @@ def pdu_data_field_len(self): @pdu_data_field_len.setter def pdu_data_field_len(self, new_len: int): - """Set the PDU data field length + """Set the PDU data field length. + :param new_len: :raises ValueError: Value too large :return: @@ -273,7 +275,7 @@ def __empty(cls) -> PduHeader: @classmethod def unpack(cls, raw_packet: bytes) -> PduHeader: - """Unpack a raw bytearray into the PDU header object representation + """Unpack a raw bytearray into the PDU header object representation. :param raw_packet: :raise ValueError: Passed bytearray is too short. diff --git a/spacepackets/cfdp/tlv.py b/spacepackets/cfdp/tlv.py index a467655..46d1b5c 100644 --- a/spacepackets/cfdp/tlv.py +++ b/spacepackets/cfdp/tlv.py @@ -189,6 +189,15 @@ def unpack(cls, raw_bytes: bytes) -> CfdpTlv: def packet_len(self) -> int: return self.MINIMAL_LEN + len(self.value) + def __repr__(self): + return f"{self.__class__.__name__}(tlv_type={self.tlv_type!r}, value={self.value!r})" + + def __str__(self): + return ( + f"CFDP TLV with type {self.tlv_type} and data 0x[{self.value.hex(sep=',')}] with " + f"length {len(self.value)}" + ) + class ConcreteTlvBase(abc.ABC): def __init__(self, tlv: CfdpTlv): @@ -530,7 +539,7 @@ def from_tlv(cls, cfdp_tlv: CfdpTlv) -> FileStoreResponseTlv: TlvList = List[Union[CfdpTlv, ConcreteTlvBase]] -class TlvWrapper: +class TlvHolder: def __init__(self, tlv_base: Optional[ConcreteTlvBase]): self.base = tlv_base diff --git a/spacepackets/ecss/tc.py b/spacepackets/ecss/tc.py index e4b0c86..348b534 100644 --- a/spacepackets/ecss/tc.py +++ b/spacepackets/ecss/tc.py @@ -22,7 +22,6 @@ from spacepackets.ecss.conf import ( get_default_tc_apid, PusVersion, - get_max_tc_packet_size, FETCH_GLOBAL_APID, ) @@ -141,10 +140,6 @@ def __init__( apid = get_default_tc_apid() secondary_header_flag = 1 logger = get_console_logger() - if len(app_data) > get_max_tc_packet_size(): - logger.warning( - "Application data of PUS packet exceeds maximum allowed size" - ) self.pus_tc_sec_header = PusTcDataFieldHeader( service=service, subservice=subservice, diff --git a/spacepackets/ecss/tm.py b/spacepackets/ecss/tm.py index 9bdaadf..3f66c2f 100644 --- a/spacepackets/ecss/tm.py +++ b/spacepackets/ecss/tm.py @@ -112,20 +112,16 @@ def unpack(cls, header_start: bytes) -> PusTmSecondaryHeader: current_idx = 0 secondary_header.pus_version = (header_start[current_idx] & 0xF0) >> 4 if secondary_header.pus_version != PusVersion.PUS_C: - logger = get_console_logger() - logger.warning( + raise ValueError( f"PUS version field value {secondary_header.pus_version} " - f"found where PUS C {PusVersion.PUS_C} was expected!" + f"found where PUS C {PusVersion.PUS_C} was expected" ) - raise ValueError secondary_header.spacecraft_time_ref = header_start[current_idx] & 0x0F if len(header_start) < secondary_header.header_size: - logger = get_console_logger() - logger.warning( + raise ValueError( f"Invalid PUS data field header size, " f"less than expected {secondary_header.header_size} bytes" ) - raise ValueError current_idx += 1 secondary_header.service = header_start[current_idx] current_idx += 1 @@ -271,13 +267,9 @@ def unpack(cls, raw_telemetry: bytes) -> PusTelemetry: :param raw_telemetry: """ if raw_telemetry is None: - logger = get_console_logger() - logger.warning("Given byte stream invalid!") - raise ValueError + raise ValueError("Given byte stream invalid") elif len(raw_telemetry) == 0: - logger = get_console_logger() - logger.warning("Given byte stream is empty") - raise ValueError + raise ValueError("Given byte stream is empty") pus_tm = cls.__empty() pus_tm._valid = False pus_tm.sp_header = SpacePacketHeader.unpack(space_packet_raw=raw_telemetry) @@ -298,9 +290,7 @@ def unpack(cls, raw_telemetry: bytes) -> PusTelemetry: expected_packet_len < pus_tm.pus_tm_sec_header.header_size + SPACE_PACKET_HEADER_SIZE ): - logger = get_console_logger() - logger.warning("Passed packet too short!") - raise ValueError + raise ValueError("Passed packet too short") if pus_tm.packet_len != len(raw_telemetry): logger = get_console_logger() logger.warning( diff --git a/tests/cfdp/pdus/test_metadata.py b/tests/cfdp/pdus/test_metadata.py index 96b47a5..9f9ee7d 100644 --- a/tests/cfdp/pdus/test_metadata.py +++ b/tests/cfdp/pdus/test_metadata.py @@ -10,7 +10,7 @@ from spacepackets.cfdp.defs import FaultHandlerCodes, LargeFileFlag from spacepackets.cfdp.pdu import MetadataPdu from spacepackets.cfdp.pdu.metadata import MetadataParams -from spacepackets.cfdp.tlv import TlvWrapper, FaultHandlerOverrideTlv +from spacepackets.cfdp.tlv import TlvHolder, FaultHandlerOverrideTlv class TestMetadata(TestCase): @@ -57,7 +57,7 @@ def test_metadata_pdu(self): pdu_with_option_raw = pdu_with_option.pack() self.assertEqual(len(pdu_with_option_raw), expected_len) pdu_with_option_unpacked = MetadataPdu.unpack(raw_packet=pdu_with_option_raw) - tlv_wrapper = TlvWrapper(pdu_with_option_unpacked.options[0]) + tlv_wrapper = TlvHolder(pdu_with_option_unpacked.options[0]) tlv_typed = tlv_wrapper.to_fs_request() self.assertIsNotNone(tlv_typed) self.assertEqual(tlv_typed.pack(), option_0.pack()) diff --git a/tests/cfdp/test_tlvs.py b/tests/cfdp/test_tlvs.py index 1fd40c2..f6890fb 100644 --- a/tests/cfdp/test_tlvs.py +++ b/tests/cfdp/test_tlvs.py @@ -8,7 +8,7 @@ FilestoreActionCode, map_int_status_code_to_enum, EntityIdTlv, - TlvWrapper, + TlvHolder, FileStoreRequestTlv, FileStoreResponseTlv, FaultHandlerOverrideTlv, @@ -75,7 +75,7 @@ def test_tlvs(self): def test_entity_id_tlv(self): entity_id_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x01, 0x02, 0x03])) entity_id_tlv_tlv = entity_id_tlv.tlv - wrapper = TlvWrapper(entity_id_tlv) + wrapper = TlvHolder(entity_id_tlv) entity_id_tlv_from_factory = wrapper.to_entity_id() self.assertEqual(entity_id_tlv_from_factory.pack(), entity_id_tlv.pack()) entity_id_tlv_tlv.tlv_type = TlvTypes.FILESTORE_REQUEST @@ -87,7 +87,7 @@ def test_fs_req_tlv(self): action_code=FilestoreActionCode.APPEND_FILE_SNP, first_file_name="test.txt" ) fs_reqeust_tlv_tlv = fs_reqeust_tlv.tlv - wrapper = TlvWrapper(fs_reqeust_tlv) + wrapper = TlvHolder(fs_reqeust_tlv) fs_req_tlv_from_fac = wrapper.to_fs_request() self.assertEqual(fs_req_tlv_from_fac.pack(), fs_reqeust_tlv.pack()) fs_reqeust_tlv_raw = fs_reqeust_tlv.pack() @@ -110,7 +110,7 @@ def test_fs_response_tlv(self): status_code=FilestoreResponseStatusCode.APPEND_NOT_PERFORMED, ) fs_response_tlv_tlv = fs_response_tlv.tlv - wrapper = TlvWrapper(fs_response_tlv) + wrapper = TlvHolder(fs_response_tlv) fs_reply_tlv_from_fac = wrapper.to_fs_response() self.assertEqual(fs_reply_tlv_from_fac.pack(), fs_response_tlv.pack()) @@ -138,7 +138,7 @@ def test_fault_handler_override_tlv(self): handler_code=FaultHandlerCodes.IGNORE_ERROR, ) fault_handler_ovvrd_tlv_tlv = fault_handler_ovvrd_tlv.tlv - wrapper = TlvWrapper(fault_handler_ovvrd_tlv) + wrapper = TlvHolder(fault_handler_ovvrd_tlv) fault_handler_ovvrd_tlv_from_fac = wrapper.to_fault_handler_override() self.assertEqual( fault_handler_ovvrd_tlv_from_fac.pack(), fault_handler_ovvrd_tlv.pack() @@ -150,7 +150,7 @@ def test_fault_handler_override_tlv(self): def test_msg_to_user_tlv(self): msg_to_usr_tlv = MessageToUserTlv(value=bytes([0x00])) msg_to_usr_tlv_tlv = msg_to_usr_tlv.tlv - wrapper = TlvWrapper(msg_to_usr_tlv) + wrapper = TlvHolder(msg_to_usr_tlv) msg_to_usr_tlv_from_fac = wrapper.to_msg_to_user() self.assertEqual(msg_to_usr_tlv_from_fac.pack(), msg_to_usr_tlv.pack()) msg_to_usr_tlv_tlv.tlv_type = TlvTypes.FILESTORE_REQUEST @@ -168,7 +168,7 @@ def test_msg_to_user_tlv(self): def test_flow_label_tlv(self): flow_label_tlv = FlowLabelTlv(value=bytes([0x00])) flow_label_tlv_tlv = flow_label_tlv.tlv - wrapper = TlvWrapper(flow_label_tlv) + wrapper = TlvHolder(flow_label_tlv) flow_label_tlv_from_fac = wrapper.to_flow_label() self.assertEqual(flow_label_tlv_from_fac.pack(), flow_label_tlv.pack()) flow_label_tlv_raw = flow_label_tlv.pack() From b55dd283aee28af639ad567015ff5b74cf398643 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:26:30 +0200 Subject: [PATCH 38/46] update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad6194..7f5b83d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +## [v0.13.0rc2] 12.07.2022 + +- Improved documentation, first docstrings +- Added more re-exports, for example for the `ccsds` module +- Added several dunder method implementations, especially `__repr__`, `__str__` and `__eq__` +- Improved CFDP packet stack API, several improvements derived from the implementation + of a CFDP handler using it +- Added generic abstraction for CFDP File Data and File Directive PDUs in form of the + `AbstractPduBase` and `AbstractFileDirectiveBase` +- Generic `UnsignedByteField` implementation. This is a data structure which is regularly + used for something like variable sized identifier fields. It provides a lot of boilerplate + code like common dunder implementations +- Split up and improve test structure a bit + ## [v0.13.0rc1] 01.07.2022 - Update `pyproject.toml` file for full support, but still keep `setup.cfg` for now From b226afee6a25973134459951f0afb48f22ca0385 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:26:53 +0200 Subject: [PATCH 39/46] bump revision --- spacepackets/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacepackets/__init__.py b/spacepackets/__init__.py index 04bef56..a0618a8 100644 --- a/spacepackets/__init__.py +++ b/spacepackets/__init__.py @@ -6,4 +6,4 @@ ) -__version__ = "0.13.0rc1" +__version__ = "0.13.0rc2" From cdf0684378222b0b81161496a406e9793a6b4c7e Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:34:40 +0200 Subject: [PATCH 40/46] bump coverage a bit --- spacepackets/cfdp/lv.py | 2 +- tests/cfdp/test_header.py | 18 ++++++++++++++++++ tests/cfdp/test_lvs.py | 7 +++++++ tests/cfdp/test_tlvs.py | 8 ++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/spacepackets/cfdp/lv.py b/spacepackets/cfdp/lv.py index 7c6240e..0aa2bb3 100644 --- a/spacepackets/cfdp/lv.py +++ b/spacepackets/cfdp/lv.py @@ -48,4 +48,4 @@ def __repr__(self): return f"{self.__class__.__name__}(value={self.value!r})" def __str__(self): - return f"CFDP LV with data 0x[{self.value.hex(sep=',')} of length {len(self.value)}" + return f"CFDP LV with data 0x[{self.value.hex(sep=',')}] of length {len(self.value)}" diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index a2351fc..067d32b 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -187,3 +187,21 @@ def check_fields_case_two(self, pdu_header_packed: bytes): self.assertEqual(pdu_header_packed[6] << 8 | pdu_header_packed[7], 300) # Destination ID self.assertEqual(pdu_header_packed[8:10], bytes([0, 1])) + + def test_printout(self): + pdu_conf = PduConfig( + source_entity_id=ByteFieldU8(0), + dest_entity_id=ByteFieldU8(0), + trans_mode=TransmissionModes.ACKNOWLEDGED, + direction=Direction.TOWARDS_RECEIVER, + crc_flag=CrcFlag.NO_CRC, + seg_ctrl=SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION, + transaction_seq_num=ByteFieldU8(0), + ) + pdu_header = PduHeader( + pdu_type=PduType.FILE_DIRECTIVE, + segment_metadata_flag=SegmentMetadataFlag.NOT_PRESENT, + pdu_data_field_len=0, + pdu_conf=pdu_conf, + ) + print(pdu_header) diff --git a/tests/cfdp/test_lvs.py b/tests/cfdp/test_lvs.py index 71d260d..2488542 100644 --- a/tests/cfdp/test_lvs.py +++ b/tests/cfdp/test_lvs.py @@ -30,3 +30,10 @@ def test_lvs(self): # Too short to unpack faulty_lv = bytes([0]) self.assertRaises(ValueError, CfdpTlv.unpack, faulty_lv) + + def test_lv_print(self): + test_lv = CfdpLv( + value=bytes([0, 1, 2, 3, 4]) + ) + print(test_lv) + print(f"{test_lv!r}") diff --git a/tests/cfdp/test_tlvs.py b/tests/cfdp/test_tlvs.py index f6890fb..6767d61 100644 --- a/tests/cfdp/test_tlvs.py +++ b/tests/cfdp/test_tlvs.py @@ -72,10 +72,18 @@ def test_tlvs(self): ) self.assertEqual(invalid_code, FilestoreResponseStatusCode.INVALID) + def test_tlv_print(self): + test_tlv = CfdpTlv( + tlv_type=TlvTypes.FILESTORE_REQUEST, value=bytes([0, 1, 2, 3, 4]) + ) + print(test_tlv) + print(f"{test_tlv!r}") + def test_entity_id_tlv(self): entity_id_tlv = EntityIdTlv(entity_id=bytes([0x00, 0x01, 0x02, 0x03])) entity_id_tlv_tlv = entity_id_tlv.tlv wrapper = TlvHolder(entity_id_tlv) + self.assertEqual(wrapper.tlv_type, TlvTypes.ENTITY_ID) entity_id_tlv_from_factory = wrapper.to_entity_id() self.assertEqual(entity_id_tlv_from_factory.pack(), entity_id_tlv.pack()) entity_id_tlv_tlv.tlv_type = TlvTypes.FILESTORE_REQUEST From 37b24d230c8ece5f845df672a74bdee9254aa316 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:39:26 +0200 Subject: [PATCH 41/46] some more tests added --- tests/cfdp/test_header.py | 102 ++++++++++++++++---------------------- tests/cfdp/test_lvs.py | 4 +- tests/test_util.py | 10 ++++ 3 files changed, 54 insertions(+), 62 deletions(-) diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index 067d32b..7d81af3 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -24,17 +24,8 @@ class TestHeader(TestCase): - # TODO: Split up in smaller test fixtures - def test_pdu_header(self): - byte_field = ByteFieldU8(22) - self.assertEqual(int(byte_field), 22) - byte_field = ByteFieldU16(5292) - self.assertEqual(int(byte_field), 5292) - byte_field = ByteFieldU32(129302) - self.assertEqual(struct.unpack("!I", byte_field.as_bytes)[0], 129302) - with self.assertRaises(ValueError): - ByteFieldU8(900) - pdu_conf = PduConfig( + def setUp(self) -> None: + self.pdu_conf = PduConfig( source_entity_id=ByteFieldU8(0), dest_entity_id=ByteFieldU8(0), trans_mode=TransmissionModes.ACKNOWLEDGED, @@ -43,29 +34,33 @@ def test_pdu_header(self): seg_ctrl=SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION, transaction_seq_num=ByteFieldU8(0), ) - pdu_header = PduHeader( + self.pdu_header = PduHeader( pdu_type=PduType.FILE_DIRECTIVE, segment_metadata_flag=SegmentMetadataFlag.NOT_PRESENT, pdu_data_field_len=0, - pdu_conf=pdu_conf, + pdu_conf=self.pdu_conf, ) - self.assertEqual(pdu_header.pdu_type, PduType.FILE_DIRECTIVE) - self.assertEqual(pdu_header.source_entity_id, ByteFieldU8(0)) - self.assertEqual(pdu_header.source_entity_id.byte_len, 1) - self.assertEqual(pdu_header.trans_mode, TransmissionModes.ACKNOWLEDGED) - self.assertEqual(pdu_header.direction, Direction.TOWARDS_RECEIVER) + + # TODO: Split up in smaller test fixtures + def test_pdu_header(self): + self.assertEqual(self.pdu_header.pdu_type, PduType.FILE_DIRECTIVE) + self.assertEqual(self.pdu_header.source_entity_id, ByteFieldU8(0)) + self.assertEqual(self.pdu_header.source_entity_id.byte_len, 1) + self.assertEqual(self.pdu_header.trans_mode, TransmissionModes.ACKNOWLEDGED) + self.assertEqual(self.pdu_header.direction, Direction.TOWARDS_RECEIVER) self.assertEqual( - pdu_header.segment_metadata_flag, SegmentMetadataFlag.NOT_PRESENT + self.pdu_header.segment_metadata_flag, SegmentMetadataFlag.NOT_PRESENT ) - self.assertFalse(pdu_header.large_file_flag_set) - self.assertEqual(pdu_header.transaction_seq_num, ByteFieldU8(0)) - self.assertEqual(pdu_header.transaction_seq_num.byte_len, 1) - self.assertEqual(pdu_header.crc_flag, CrcFlag.NO_CRC) + self.assertFalse(self.pdu_header.large_file_flag_set) + self.assertEqual(self.pdu_header.transaction_seq_num, ByteFieldU8(0)) + self.assertEqual(self.pdu_header.transaction_seq_num.byte_len, 1) + self.assertEqual(self.pdu_header.crc_flag, CrcFlag.NO_CRC) self.assertEqual( - pdu_header.seg_ctrl, SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION + self.pdu_header.seg_ctrl, + SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION, ) - self.assertEqual(pdu_header.header_len, 7) - pdu_header_packed = pdu_header.pack() + self.assertEqual(self.pdu_header.header_len, 7) + pdu_header_packed = self.pdu_header.pack() string = get_printable_data_string( print_format=PrintFormats.HEX, data=pdu_header_packed ) @@ -75,25 +70,25 @@ def test_pdu_header(self): pdu_header_repacked = pdu_header_unpacked.pack() self.check_fields_case_one(pdu_header_packed=pdu_header_repacked) - pdu_header.pdu_type = PduType.FILE_DATA - pdu_header.set_entity_ids( + self.pdu_header.pdu_type = PduType.FILE_DATA + self.pdu_header.set_entity_ids( source_entity_id=ByteFieldU16(0), dest_entity_id=ByteFieldU16(1) ) - pdu_header.transaction_seq_num = ByteFieldU16(300) - pdu_header.trans_mode = TransmissionModes.UNACKNOWLEDGED - pdu_header.direction = Direction.TOWARDS_SENDER - pdu_header.crc_flag = CrcFlag.WITH_CRC - pdu_header.file_flag = LargeFileFlag.LARGE - pdu_header.pdu_data_field_len = 300 - pdu_header.seg_ctrl = SegmentationControl.RECORD_BOUNDARIES_PRESERVATION - pdu_header.segment_metadata_flag = SegmentMetadataFlag.PRESENT + self.pdu_header.transaction_seq_num = ByteFieldU16(300) + self.pdu_header.trans_mode = TransmissionModes.UNACKNOWLEDGED + self.pdu_header.direction = Direction.TOWARDS_SENDER + self.pdu_header.crc_flag = CrcFlag.WITH_CRC + self.pdu_header.file_flag = LargeFileFlag.LARGE + self.pdu_header.pdu_data_field_len = 300 + self.pdu_header.seg_ctrl = SegmentationControl.RECORD_BOUNDARIES_PRESERVATION + self.pdu_header.segment_metadata_flag = SegmentMetadataFlag.PRESENT - self.assertTrue(pdu_header.large_file_flag_set) - pdu_header_packed = pdu_header.pack() + self.assertTrue(self.pdu_header.large_file_flag_set) + pdu_header_packed = self.pdu_header.pack() self.check_fields_case_two(pdu_header_packed=pdu_header_packed) set_entity_ids(source_entity_id=bytes(), dest_entity_id=bytes()) with self.assertRaises(ValueError): - pdu_header.pdu_data_field_len = 78292 + self.pdu_header.pdu_data_field_len = 78292 invalid_pdu_header = bytearray([0, 1, 2]) self.assertRaises(ValueError, PduHeader.unpack, invalid_pdu_header) self.assertRaises(ValueError, PduHeader.unpack, pdu_header_packed[0:6]) @@ -105,11 +100,11 @@ def test_pdu_header(self): 300, ) - pdu_conf.source_entity_id = ByteFieldU8(0) - pdu_conf.dest_entity_id = ByteFieldU8(0) - pdu_conf.transaction_seq_num = ByteFieldU16.from_bytes(bytes([0x00, 0x2C])) + self.pdu_conf.source_entity_id = ByteFieldU8(0) + self.pdu_conf.dest_entity_id = ByteFieldU8(0) + self.pdu_conf.transaction_seq_num = ByteFieldU16.from_bytes(bytes([0x00, 0x2C])) prompt_pdu = PromptPdu( - response_required=ResponseRequired.KEEP_ALIVE, pdu_conf=pdu_conf + response_required=ResponseRequired.KEEP_ALIVE, pdu_conf=self.pdu_conf ) self.assertEqual(prompt_pdu.pdu_file_directive.header_len, 9) self.assertEqual(prompt_pdu.packet_len, 10) @@ -188,20 +183,9 @@ def check_fields_case_two(self, pdu_header_packed: bytes): # Destination ID self.assertEqual(pdu_header_packed[8:10], bytes([0, 1])) + def test_header_len_raw_func(self): + self.assertEqual(self.pdu_header.header_len, 7) + self.assertEqual(PduHeader.header_len_from_raw(self.pdu_header.pack()), 7) + def test_printout(self): - pdu_conf = PduConfig( - source_entity_id=ByteFieldU8(0), - dest_entity_id=ByteFieldU8(0), - trans_mode=TransmissionModes.ACKNOWLEDGED, - direction=Direction.TOWARDS_RECEIVER, - crc_flag=CrcFlag.NO_CRC, - seg_ctrl=SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION, - transaction_seq_num=ByteFieldU8(0), - ) - pdu_header = PduHeader( - pdu_type=PduType.FILE_DIRECTIVE, - segment_metadata_flag=SegmentMetadataFlag.NOT_PRESENT, - pdu_data_field_len=0, - pdu_conf=pdu_conf, - ) - print(pdu_header) + print(self.pdu_header) diff --git a/tests/cfdp/test_lvs.py b/tests/cfdp/test_lvs.py index 2488542..d242fbc 100644 --- a/tests/cfdp/test_lvs.py +++ b/tests/cfdp/test_lvs.py @@ -32,8 +32,6 @@ def test_lvs(self): self.assertRaises(ValueError, CfdpTlv.unpack, faulty_lv) def test_lv_print(self): - test_lv = CfdpLv( - value=bytes([0, 1, 2, 3, 4]) - ) + test_lv = CfdpLv(value=bytes([0, 1, 2, 3, 4])) print(test_lv) print(f"{test_lv!r}") diff --git a/tests/test_util.py b/tests/test_util.py index 2138dfb..4ba5777 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -12,6 +12,16 @@ class TestUtility(TestCase): + def test_basic_bytefields(self): + byte_field = ByteFieldU8(22) + self.assertEqual(int(byte_field), 22) + byte_field = ByteFieldU16(5292) + self.assertEqual(int(byte_field), 5292) + byte_field = ByteFieldU32(129302) + self.assertEqual(struct.unpack("!I", byte_field.as_bytes)[0], 129302) + with self.assertRaises(ValueError): + ByteFieldU8(900) + def test_one_byte_field_gen(self): one_byte_test = ByteFieldGenerator.from_int(byte_len=1, val=0x42) self.assertEqual(ByteFieldU8(0x42), one_byte_test) From 214cd00dface369bf0b8a596937f3eb7965c5251 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:43:56 +0200 Subject: [PATCH 42/46] some more tests --- spacepackets/cfdp/pdu/nak.py | 7 +++++++ .../cfdp/pdus/{test_pdu_wrapper.py => test_pdu_holder.py} | 6 ++++++ 2 files changed, 13 insertions(+) rename tests/cfdp/pdus/{test_pdu_wrapper.py => test_pdu_holder.py} (95%) diff --git a/spacepackets/cfdp/pdu/nak.py b/spacepackets/cfdp/pdu/nak.py index 7e6d079..12c1848 100644 --- a/spacepackets/cfdp/pdu/nak.py +++ b/spacepackets/cfdp/pdu/nak.py @@ -178,3 +178,10 @@ def __eq__(self, other: NakPdu): and self.start_of_scope == other.start_of_scope and self.end_of_scope == other.end_of_scope ) + + def __repr__(self): + return ( + f"{self.__class__.__name__}(start_of_scope={self.start_of_scope!r}, " + f"end_of_scope={self.end_of_scope!r}, pdu_conf={self.pdu_file_directive.pdu_conf!r}" + f"segment_requests={self.segment_requests!r})" + ) diff --git a/tests/cfdp/pdus/test_pdu_wrapper.py b/tests/cfdp/pdus/test_pdu_holder.py similarity index 95% rename from tests/cfdp/pdus/test_pdu_wrapper.py rename to tests/cfdp/pdus/test_pdu_holder.py index 139bf54..0f490b7 100644 --- a/tests/cfdp/pdus/test_pdu_wrapper.py +++ b/tests/cfdp/pdus/test_pdu_holder.py @@ -41,6 +41,12 @@ def test_file_data(self): self.pdu_wrapper.base = file_data_pdu pdu_casted_back = self.pdu_wrapper.to_file_data_pdu() self.assertEqual(pdu_casted_back, file_data_pdu) + self.assertEqual(self.pdu_wrapper.pdu_directive_type, None) + + def test_holder_print(self): + nak_pdu = NakPdu(start_of_scope=0, end_of_scope=200, pdu_conf=self.pdu_conf) + self.pdu_wrapper.base = nak_pdu + print(self.pdu_wrapper) def test_invalid_to_file_data(self): params = MetadataParams( From 834a94d34c58a45a4db502deb47d64fb806fdee2 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 19:56:57 +0200 Subject: [PATCH 43/46] some pdu factory tests --- spacepackets/cfdp/__init__.py | 1 + spacepackets/cfdp/pdu/__init__.py | 2 +- spacepackets/cfdp/pdu/helper.py | 3 ++- tests/cfdp/pdus/test_factory.py | 27 +++++++++++++++++++++++++++ tests/cfdp/pdus/test_pdu_holder.py | 2 +- 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/cfdp/pdus/test_factory.py diff --git a/spacepackets/cfdp/__init__.py b/spacepackets/cfdp/__init__.py index 14fc80e..11a12cc 100644 --- a/spacepackets/cfdp/__init__.py +++ b/spacepackets/cfdp/__init__.py @@ -25,3 +25,4 @@ ) from .lv import CfdpLv from .conf import PduConfig +from .pdu import DirectiveType diff --git a/spacepackets/cfdp/pdu/__init__.py b/spacepackets/cfdp/pdu/__init__.py index 55fda41..f655b1a 100644 --- a/spacepackets/cfdp/pdu/__init__.py +++ b/spacepackets/cfdp/pdu/__init__.py @@ -15,5 +15,5 @@ from .metadata import MetadataPdu, MetadataParams from .nak import NakPdu from .prompt import PromptPdu -from .helper import PduHolder +from .helper import PduHolder, PduFactory from .file_data import FileDataPdu diff --git a/spacepackets/cfdp/pdu/helper.py b/spacepackets/cfdp/pdu/helper.py index 895bdc0..ae9e0f2 100644 --- a/spacepackets/cfdp/pdu/helper.py +++ b/spacepackets/cfdp/pdu/helper.py @@ -105,7 +105,8 @@ class PduFactory: @staticmethod def from_raw(data: bytes): - pass + # TODO: Implement + raise NotImplementedError() @staticmethod def pdu_type(data: bytes) -> PduType: diff --git a/tests/cfdp/pdus/test_factory.py b/tests/cfdp/pdus/test_factory.py new file mode 100644 index 0000000..a1eecb7 --- /dev/null +++ b/tests/cfdp/pdus/test_factory.py @@ -0,0 +1,27 @@ +from unittest import TestCase + +from spacepackets.cfdp import NULL_CHECKSUM_U32, ConditionCode, PduConfig, DirectiveType +from spacepackets.cfdp.pdu import EofPdu, PduFactory, FileDataPdu + + +class TestPduHolder(TestCase): + def setUp(self) -> None: + self.pdu_conf = PduConfig.default() + self.pdu_factory = PduFactory() + + def test_factory_file_directive_getter(self): + eof_pdu = EofPdu( + file_checksum=NULL_CHECKSUM_U32, + condition_code=ConditionCode.NO_ERROR, + file_size=0, + pdu_conf=self.pdu_conf, + ) + eof_raw = eof_pdu.pack() + self.assertEqual( + self.pdu_factory.pdu_directive_type(eof_raw), DirectiveType.EOF_PDU + ) + + def test_factory_file_directive_on_file_data(self): + file_data_pdu = FileDataPdu(file_data=bytes(), offset=0, pdu_conf=self.pdu_conf) + fd_pdu_raw = file_data_pdu.pack() + self.assertEqual(self.pdu_factory.pdu_directive_type(fd_pdu_raw), None) diff --git a/tests/cfdp/pdus/test_pdu_holder.py b/tests/cfdp/pdus/test_pdu_holder.py index 0f490b7..1437f1d 100644 --- a/tests/cfdp/pdus/test_pdu_holder.py +++ b/tests/cfdp/pdus/test_pdu_holder.py @@ -24,7 +24,7 @@ from spacepackets.cfdp.pdu.helper import PduHolder -class TestPduWrapper(TestCase): +class TestPduHolder(TestCase): def setUp(self) -> None: self.pdu_conf = PduConfig.empty() self.file_data = "hello world" From 6e8ad8eaec82d8104f436fc5a9020e1d548ffb12 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 20:01:47 +0200 Subject: [PATCH 44/46] one more test --- spacepackets/cfdp/pdu/header.py | 9 +++------ tests/cfdp/test_header.py | 6 ++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/spacepackets/cfdp/pdu/header.py b/spacepackets/cfdp/pdu/header.py index 02919ab..ecdde3f 100644 --- a/spacepackets/cfdp/pdu/header.py +++ b/spacepackets/cfdp/pdu/header.py @@ -323,14 +323,11 @@ def unpack(cls, raw_packet: bytes) -> PduHeader: @staticmethod def check_len_in_bytes(detected_len: int) -> LenInBytes: - try: - len_in_bytes = LenInBytes(detected_len) - except ValueError: + if detected_len not in [1, 2, 4, 8]: raise ValueError( - "Unsupported length field detected. " - "Only 1, 2, 4 and 8 bytes are supported" + "Unsupported length field detected. Must be in [1, 2, 4, 8]" ) - return len_in_bytes + return LenInBytes(detected_len) def __repr__(self): return ( diff --git a/tests/cfdp/test_header.py b/tests/cfdp/test_header.py index 7d81af3..a1528c1 100644 --- a/tests/cfdp/test_header.py +++ b/tests/cfdp/test_header.py @@ -187,5 +187,11 @@ def test_header_len_raw_func(self): self.assertEqual(self.pdu_header.header_len, 7) self.assertEqual(PduHeader.header_len_from_raw(self.pdu_header.pack()), 7) + def test_length_field_check(self): + with self.assertRaises(ValueError): + PduHeader.check_len_in_bytes(0) + with self.assertRaises(ValueError): + PduHeader.check_len_in_bytes(5) + def test_printout(self): print(self.pdu_header) From 35f7b73a0e6909d8f4014686ae21284bb1ee809c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 20:04:22 +0200 Subject: [PATCH 45/46] printout test keep alive PDU --- tests/cfdp/pdus/test_keep_alive_pdu.py | 36 +++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/cfdp/pdus/test_keep_alive_pdu.py b/tests/cfdp/pdus/test_keep_alive_pdu.py index 0f2f924..0997239 100644 --- a/tests/cfdp/pdus/test_keep_alive_pdu.py +++ b/tests/cfdp/pdus/test_keep_alive_pdu.py @@ -6,12 +6,15 @@ class TestKeepAlivePdu(TestCase): + def setUp(self) -> None: + self.pdu_conf = PduConfig.default() + self.keep_alive_pdu = KeepAlivePdu(pdu_conf=self.pdu_conf, progress=0) + + # TODO: Split into smaller fixtures def test_keep_alive_pdu(self): - pdu_conf = PduConfig.default() - keep_alive_pdu = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) - self.assertEqual(keep_alive_pdu.progress, 0) - self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) - keep_alive_pdu_raw = keep_alive_pdu.pack() + self.assertEqual(self.keep_alive_pdu.progress, 0) + self.assertEqual(self.keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) + keep_alive_pdu_raw = self.keep_alive_pdu.pack() self.assertEqual( keep_alive_pdu_raw, bytes( @@ -31,24 +34,27 @@ def test_keep_alive_pdu(self): ] ), ) - self.assertEqual(keep_alive_pdu.packet_len, 12) + self.assertEqual(self.keep_alive_pdu.packet_len, 12) keep_alive_unpacked = KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_raw) self.assertEqual(keep_alive_unpacked.packet_len, 12) self.assertEqual(keep_alive_unpacked.progress, 0) - keep_alive_pdu.file_flag = LargeFileFlag.LARGE - self.assertEqual(keep_alive_pdu.packet_len, 16) - keep_alive_pdu_large = keep_alive_pdu.pack() + self.keep_alive_pdu.file_flag = LargeFileFlag.LARGE + self.assertEqual(self.keep_alive_pdu.packet_len, 16) + keep_alive_pdu_large = self.keep_alive_pdu.pack() self.assertEqual(len(keep_alive_pdu_large), 16) - keep_alive_pdu.file_flag = LargeFileFlag.NORMAL - self.assertEqual(keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) + self.keep_alive_pdu.file_flag = LargeFileFlag.NORMAL + self.assertEqual(self.keep_alive_pdu.file_flag, LargeFileFlag.NORMAL) - keep_alive_pdu.progress = pow(2, 32) + 1 + self.keep_alive_pdu.progress = pow(2, 32) + 1 with self.assertRaises(ValueError): - keep_alive_pdu.pack() + self.keep_alive_pdu.pack() - pdu_conf.fss_field_len = LargeFileFlag.LARGE - keep_alive_pdu_large = KeepAlivePdu(pdu_conf=pdu_conf, progress=0) + self.pdu_conf.fss_field_len = LargeFileFlag.LARGE + keep_alive_pdu_large = KeepAlivePdu(pdu_conf=self.pdu_conf, progress=0) keep_alive_pdu_invalid = keep_alive_pdu_large.pack()[:-1] with self.assertRaises(ValueError): KeepAlivePdu.unpack(raw_packet=keep_alive_pdu_invalid) + + def test_print(self): + print(self.keep_alive_pdu) From daf0f3d682611da18c3c7bb5d61882f2a62fa196 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 12 Jul 2022 20:08:17 +0200 Subject: [PATCH 46/46] small fix for doc string --- spacepackets/cfdp/pdu/helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spacepackets/cfdp/pdu/helper.py b/spacepackets/cfdp/pdu/helper.py index ae9e0f2..5c4acb0 100644 --- a/spacepackets/cfdp/pdu/helper.py +++ b/spacepackets/cfdp/pdu/helper.py @@ -120,9 +120,9 @@ def is_file_directive(data: bytes): def pdu_directive_type(data: bytes) -> Optional[DirectiveType]: """Retrieve the PDU directive type from a raw bytestream. - :raises ValueError: Invalid directive type - :return None if the PDU in the given bytestream is not a file directive, otherwise the - directive + :raises ValueError: Invalid directive type. + :returns: None, if the PDU in the given bytestream is not a file directive, otherwise the + directive. """ if not PduFactory.is_file_directive(data): return None