diff --git a/.idea/runConfigurations/Check.xml b/.idea/runConfigurations/Check.xml
new file mode 100644
index 0000000..502aa84
--- /dev/null
+++ b/.idea/runConfigurations/Check.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Test.xml b/.idea/runConfigurations/Test.xml
new file mode 100644
index 0000000..d051ff1
--- /dev/null
+++ b/.idea/runConfigurations/Test.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84a2f15..50a2301 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
+## Added
+
+- Added new `util` module which contains the following (new) helper modules:
+ - `UnsignedEnum` trait as an abstraction for unsigned byte fields with variable lengths. It is
+ not tied to the ECSS PFC value like the `EcssEnumeration` trait.
+ - `GenericUnsignedByteField` and helper typedefs `UnsignedU8`, `UnsignedU16`, `UnsignedU32`
+ and `UnsignedU64` as helper types implementing `UnsignedEnum`
+ - `UnsignedByteField` as a type-erased helper.
+- Initial CFDP support: Added PDU packet implementation.
+
+## Changed
+
+- The `EcssEnumeration` now requires the `UnsignedEnum` trait and only adds the `pfc` method to it.
+- Renamed `byte_width` usages to `len` (part of new `UnsignedEnum` trait)
+- Moved `ecss::CRC_CCITT_FALSE` CRC constant to the root module. This CRC type is not just used by
+ the PUS standard, but by the CCSDS Telecommand standard and the CFDP standard as well.
+
# [v0.5.4] 2023-02-12
## Added
diff --git a/Cargo.toml b/Cargo.toml
index debc410..ec45f26 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,7 +18,7 @@ crc = "3"
delegate = ">=0.8, <0.10"
[dependencies.num_enum]
-version = "0.5"
+version = "0.6"
default-features = false
[dependencies.serde]
diff --git a/README.md b/README.md
index ca85e6c..a440074 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,8 @@ Currently, this includes the following components:
- Space Packet implementation according to
[CCSDS Blue Book 133.0-B-2](https://public.ccsds.org/Pubs/133x0b2e1.pdf)
+- CCSDS File Delivery Protocol (CFDP) packet implementations according to
+ [CCSDS Blue Book 727.0-B-5](https://public.ccsds.org/Pubs/727x0b5.pdf)
- PUS Telecommand and PUS Telemetry implementation according to the
[ECSS-E-ST-70-41C standard](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
- CUC (CCSDS Unsegmented Time Code) implementation according to
diff --git a/src/cfdp/lv.rs b/src/cfdp/lv.rs
new file mode 100644
index 0000000..96f932d
--- /dev/null
+++ b/src/cfdp/lv.rs
@@ -0,0 +1,308 @@
+//! Generic CFDP length-value (LV) abstraction as specified in CFDP 5.1.8.
+use crate::cfdp::TlvLvError;
+use crate::{ByteConversionError, SizeMissmatch};
+use core::str::Utf8Error;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "std")]
+use std::string::String;
+
+pub const MIN_LV_LEN: usize = 1;
+
+/// Generic CFDP length-value (LV) abstraction as specified in CFDP 5.1.8.
+///
+/// # Lifetimes
+/// * `data`: If the LV is generated from a raw bytestream, this will be the lifetime of
+/// the raw bytestream. If the LV is generated from a raw slice or a similar data reference,
+/// this will be the lifetime of that data reference.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Lv<'data> {
+ data: Option<&'data [u8]>,
+}
+
+pub(crate) fn generic_len_check_data_serialization(
+ buf: &[u8],
+ data_len: usize,
+ min_overhead: usize,
+) -> Result<(), ByteConversionError> {
+ if buf.len() < data_len + min_overhead {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: data_len + min_overhead,
+ }));
+ }
+ Ok(())
+}
+
+pub(crate) fn generic_len_check_deserialization(
+ buf: &[u8],
+ min_overhead: usize,
+) -> Result<(), ByteConversionError> {
+ if buf.len() < min_overhead {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: min_overhead,
+ }));
+ }
+ Ok(())
+}
+
+impl<'data> Lv<'data> {
+ pub fn new(data: &[u8]) -> Result {
+ if data.len() > u8::MAX as usize {
+ return Err(TlvLvError::DataTooLarge(data.len()));
+ }
+ Ok(Lv { data: Some(data) })
+ }
+
+ /// Creates a LV with an empty value field.
+ pub fn new_empty() -> Lv<'data> {
+ Lv { data: None }
+ }
+
+ /// Helper function to build a string LV. This is especially useful for the file or directory
+ /// path LVs
+ pub fn new_from_str(str_slice: &str) -> Result {
+ Self::new(str_slice.as_bytes())
+ }
+
+ /// Helper function to build a string LV. This is especially useful for the file or directory
+ /// path LVs
+ #[cfg(feature = "std")]
+ pub fn new_from_string(string: &'data String) -> Result, TlvLvError> {
+ Self::new(string.as_bytes())
+ }
+
+ /// Returns the length of the value part, not including the length byte.
+ pub fn len_value(&self) -> usize {
+ if self.data.is_none() {
+ return 0;
+ }
+ self.data.unwrap().len()
+ }
+
+ /// Returns the full raw length, including the length byte.
+ pub fn len_full(&self) -> usize {
+ self.len_value() + 1
+ }
+
+ /// Checks whether the value field is empty.
+ pub fn is_empty(&self) -> bool {
+ self.data.is_none()
+ }
+
+ pub fn value(&self) -> Option<&[u8]> {
+ self.data
+ }
+
+ /// Convenience function to extract the value as a [str]. This is useful if the LV is
+ /// known to contain a [str], for example being a file name.
+ pub fn value_as_str(&self) -> Option> {
+ self.data?;
+ Some(core::str::from_utf8(self.data.unwrap()))
+ }
+
+ /// Writes the LV to a raw buffer. Please note that the first byte will contain the length
+ /// of the value, but the values may not exceed a length of [u8::MAX].
+ pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result {
+ generic_len_check_data_serialization(buf, self.len_value(), MIN_LV_LEN)?;
+ Ok(self.write_to_be_bytes_no_len_check(buf))
+ }
+
+ /// Reads a LV from a raw buffer.
+ pub fn from_bytes(buf: &'data [u8]) -> Result, ByteConversionError> {
+ generic_len_check_deserialization(buf, MIN_LV_LEN)?;
+ Self::from_be_bytes_no_len_check(buf)
+ }
+
+ pub(crate) fn write_to_be_bytes_no_len_check(&self, buf: &mut [u8]) -> usize {
+ if self.data.is_none() {
+ buf[0] = 0;
+ return MIN_LV_LEN;
+ }
+ let data = self.data.unwrap();
+ // Length check in constructor ensures the length always has a valid value.
+ buf[0] = data.len() as u8;
+ buf[MIN_LV_LEN..data.len() + MIN_LV_LEN].copy_from_slice(data);
+ MIN_LV_LEN + data.len()
+ }
+
+ pub(crate) fn from_be_bytes_no_len_check(
+ buf: &'data [u8],
+ ) -> Result, ByteConversionError> {
+ let value_len = buf[0] as usize;
+ generic_len_check_deserialization(buf, value_len + MIN_LV_LEN)?;
+ let mut data = None;
+ if value_len > 0 {
+ data = Some(&buf[MIN_LV_LEN..MIN_LV_LEN + value_len])
+ }
+ Ok(Self { data })
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use crate::cfdp::lv::Lv;
+ use crate::cfdp::TlvLvError;
+ use crate::ByteConversionError;
+ use std::string::String;
+
+ #[test]
+ fn test_basic() {
+ let lv_data: [u8; 4] = [1, 2, 3, 4];
+ let lv_res = Lv::new(&lv_data);
+ assert!(lv_res.is_ok());
+ let lv = lv_res.unwrap();
+ assert!(lv.value().is_some());
+ let val = lv.value().unwrap();
+ assert_eq!(val[0], 1);
+ assert_eq!(val[1], 2);
+ assert_eq!(val[2], 3);
+ assert_eq!(val[3], 4);
+ assert!(!lv.is_empty());
+ assert_eq!(lv.len_full(), 5);
+ assert_eq!(lv.len_value(), 4);
+ }
+
+ #[test]
+ fn test_empty() {
+ let lv_empty = Lv::new_empty();
+ assert_eq!(lv_empty.len_value(), 0);
+ assert_eq!(lv_empty.len_full(), 1);
+ assert!(lv_empty.is_empty());
+ assert_eq!(lv_empty.value(), None);
+ let mut buf: [u8; 4] = [0xff; 4];
+ let res = lv_empty.write_to_be_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(written, 1);
+ assert_eq!(buf[0], 0);
+ }
+
+ #[test]
+ fn test_serialization() {
+ let lv_data: [u8; 4] = [1, 2, 3, 4];
+ let lv_res = Lv::new(&lv_data);
+ assert!(lv_res.is_ok());
+ let lv = lv_res.unwrap();
+ let mut buf: [u8; 16] = [0; 16];
+ let res = lv.write_to_be_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(written, 5);
+ assert_eq!(buf[0], 4);
+ assert_eq!(buf[1], 1);
+ assert_eq!(buf[2], 2);
+ assert_eq!(buf[3], 3);
+ assert_eq!(buf[4], 4);
+ }
+
+ #[test]
+ fn test_deserialization() {
+ let mut buf: [u8; 16] = [0; 16];
+ buf[0] = 4;
+ buf[1] = 1;
+ buf[2] = 2;
+ buf[3] = 3;
+ buf[4] = 4;
+ let lv = Lv::from_bytes(&buf);
+ assert!(lv.is_ok());
+ let lv = lv.unwrap();
+ assert!(!lv.is_empty());
+ assert!(lv.value().is_some());
+ assert_eq!(lv.len_value(), 4);
+ assert_eq!(lv.len_full(), 5);
+ let val = lv.value().unwrap();
+ assert_eq!(val[0], 1);
+ assert_eq!(val[1], 2);
+ assert_eq!(val[2], 3);
+ assert_eq!(val[3], 4);
+ }
+
+ #[test]
+ fn test_deserialization_empty() {
+ let buf: [u8; 2] = [0; 2];
+ let lv_empty = Lv::from_bytes(&buf);
+ assert!(lv_empty.is_ok());
+ let lv_empty = lv_empty.unwrap();
+ assert!(lv_empty.is_empty());
+ assert!(lv_empty.value().is_none());
+ }
+
+ #[test]
+ fn test_data_too_large() {
+ let data_big: [u8; u8::MAX as usize + 1] = [0; u8::MAX as usize + 1];
+ let lv = Lv::new(&data_big);
+ assert!(lv.is_err());
+ let error = lv.unwrap_err();
+ if let TlvLvError::DataTooLarge(size) = error {
+ assert_eq!(size, u8::MAX as usize + 1);
+ } else {
+ panic!("invalid exception {:?}", error)
+ }
+ }
+
+ #[test]
+ fn test_serialization_buf_too_small() {
+ let mut buf: [u8; 3] = [0; 3];
+ let lv_data: [u8; 4] = [1, 2, 3, 4];
+ let lv = Lv::new(&lv_data).unwrap();
+ let res = lv.write_to_be_bytes(&mut buf);
+ assert!(res.is_err());
+ let error = res.unwrap_err();
+ if let ByteConversionError::ToSliceTooSmall(missmatch) = error {
+ assert_eq!(missmatch.expected, 5);
+ assert_eq!(missmatch.found, 3);
+ } else {
+ panic!("invalid error {}", error);
+ }
+ }
+
+ #[test]
+ fn test_deserialization_buf_too_small() {
+ let mut buf: [u8; 3] = [0; 3];
+ buf[0] = 4;
+ let res = Lv::from_bytes(&buf);
+ assert!(res.is_err());
+ let error = res.unwrap_err();
+ if let ByteConversionError::FromSliceTooSmall(missmatch) = error {
+ assert_eq!(missmatch.found, 3);
+ assert_eq!(missmatch.expected, 5);
+ } else {
+ panic!("invalid error {}", error);
+ }
+ }
+
+ fn verify_test_str_lv(lv: Lv) {
+ let mut buf: [u8; 16] = [0; 16];
+ let res = lv.write_to_be_bytes(&mut buf);
+ assert!(res.is_ok());
+ let res = res.unwrap();
+ assert_eq!(res, 8 + 1);
+ assert_eq!(buf[0], 8);
+ assert_eq!(buf[1], 't' as u8);
+ assert_eq!(buf[2], 'e' as u8);
+ assert_eq!(buf[3], 's' as u8);
+ assert_eq!(buf[4], 't' as u8);
+ assert_eq!(buf[5], '.' as u8);
+ assert_eq!(buf[6], 'b' as u8);
+ assert_eq!(buf[7], 'i' as u8);
+ assert_eq!(buf[8], 'n' as u8);
+ }
+ #[test]
+ fn test_str_helper() {
+ let test_str = "test.bin";
+ let str_lv = Lv::new_from_str(test_str);
+ assert!(str_lv.is_ok());
+ verify_test_str_lv(str_lv.unwrap());
+ }
+
+ #[test]
+ fn test_string_helper() {
+ let string = String::from("test.bin");
+ let str_lv = Lv::new_from_string(&string);
+ assert!(str_lv.is_ok());
+ verify_test_str_lv(str_lv.unwrap());
+ }
+}
diff --git a/src/cfdp/mod.rs b/src/cfdp/mod.rs
new file mode 100644
index 0000000..b6c082f
--- /dev/null
+++ b/src/cfdp/mod.rs
@@ -0,0 +1,203 @@
+//! Low-level CCSDS File Delivery Protocol (CFDP) support according to [CCSDS 727.0-B-5](https://public.ccsds.org/Pubs/727x0b5.pdf).
+use crate::ByteConversionError;
+use core::fmt::{Display, Formatter};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "std")]
+use std::error::Error;
+
+pub mod lv;
+pub mod pdu;
+pub mod tlv;
+
+/// This is the name of the standard this module is based on.
+pub const CFDP_VERSION_2_NAME: &str = "CCSDS 727.0-B-5";
+/// Currently, only this version is supported.
+pub const CFDP_VERSION_2: u8 = 0b001;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum PduType {
+ FileDirective = 0,
+ FileData = 1,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum Direction {
+ TowardsReceiver = 0,
+ TowardsSender = 1,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum TransmissionMode {
+ Acknowledged = 0,
+ Unacknowledged = 1,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum CrcFlag {
+ NoCrc = 0,
+ WithCrc = 1,
+}
+
+/// Always 0 and ignored for File Directive PDUs (CCSDS 727.0-B-5 P.75)
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum SegmentMetadataFlag {
+ NotPresent = 0,
+ Present = 1,
+}
+
+/// Always 0 and ignored for File Directive PDUs (CCSDS 727.0-B-5 P.75)
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum SegmentationControl {
+ NoRecordBoundaryPreservation = 0,
+ WithRecordBoundaryPreservation = 1,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum FaultHandlerCode {
+ NoticeOfCancellation = 0b0001,
+ NoticeOfSuspension = 0b0010,
+ IgnoreError = 0b0011,
+ AbandonTransaction = 0b0100,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum LenInBytes {
+ ZeroOrNone = 0,
+ OneByte = 1,
+ TwoBytes = 2,
+ ThreeBytes = 4,
+ FourBytes = 8,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum ConditionCode {
+ /// This is not an error condition for which a faulty handler override can be specified
+ NoError = 0b0000,
+ PositiveAckLimitReached = 0b0001,
+ KeepAliveLimitReached = 0b0010,
+ InvalidTransmissionMode = 0b0011,
+ FilestoreRejection = 0b0100,
+ FileChecksumFailure = 0b0101,
+ FileSizeError = 0b0110,
+ NakLimitReached = 0b0111,
+ InactivityDetected = 0b1000,
+ CheckLimitReached = 0b1001,
+ UnsupportedChecksumType = 0b1011,
+ /// Not an actual fault condition for which fault handler overrides can be specified
+ SuspendRequestReceived = 0b1110,
+ /// Not an actual fault condition for which fault handler overrides can be specified
+ CancelRequestReceived = 0b1111,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum LargeFileFlag {
+ /// 32 bit maximum file size and FSS size
+ Normal = 0,
+ /// 64 bit maximum file size and FSS size
+ Large = 1,
+}
+
+/// Checksum types according to the
+/// [SANA Checksum Types registry](https://sanaregistry.org/r/checksum_identifiers/)
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum ChecksumType {
+ /// Modular legacy checksum
+ Modular = 0,
+ Crc32Proximity1 = 1,
+ Crc32C = 2,
+ /// Polynomial: 0x4C11DB7. Preferred checksum for now.
+ Crc32 = 3,
+ NullChecksum = 15,
+}
+
+pub const NULL_CHECKSUM_U32: [u8; 4] = [0; 4];
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum TlvLvError {
+ DataTooLarge(usize),
+ ByteConversionError(ByteConversionError),
+ /// First value: Found value. Second value: Expected value if there is one.
+ InvalidTlvTypeField((u8, Option)),
+ /// Logically invalid value length detected. The value length may not exceed 255 bytes.
+ /// Depending on the concrete TLV type, the value length may also be logically invalid.
+ InvalidValueLength(usize),
+ /// Only applies to filestore requests and responses. Second name was missing where one is
+ /// expected.
+ SecondNameMissing,
+ /// Invalid action code for filestore requests or responses.
+ InvalidFilestoreActionCode(u8),
+}
+
+impl From for TlvLvError {
+ fn from(value: ByteConversionError) -> Self {
+ Self::ByteConversionError(value)
+ }
+}
+
+impl Display for TlvLvError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+ match self {
+ TlvLvError::DataTooLarge(data_len) => {
+ write!(
+ f,
+ "data with size {} larger than allowed {} bytes",
+ data_len,
+ u8::MAX
+ )
+ }
+ TlvLvError::ByteConversionError(e) => {
+ write!(f, "{}", e)
+ }
+ TlvLvError::InvalidTlvTypeField((found, expected)) => {
+ write!(
+ f,
+ "invalid TLV type field, found {found}, possibly expected {expected:?}"
+ )
+ }
+ TlvLvError::InvalidValueLength(len) => {
+ write!(f, "invalid value length {len} detected")
+ }
+ TlvLvError::SecondNameMissing => {
+ write!(f, "second name missing for filestore request or response")
+ }
+ TlvLvError::InvalidFilestoreActionCode(raw) => {
+ write!(f, "invalid filestore action code with raw value {raw}")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl Error for TlvLvError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ match self {
+ TlvLvError::ByteConversionError(e) => Some(e),
+ _ => None,
+ }
+ }
+}
diff --git a/src/cfdp/pdu/eof.rs b/src/cfdp/pdu/eof.rs
new file mode 100644
index 0000000..71f977b
--- /dev/null
+++ b/src/cfdp/pdu/eof.rs
@@ -0,0 +1,206 @@
+use crate::cfdp::pdu::{
+ add_pdu_crc, generic_length_checks_pdu_deserialization, read_fss_field, write_fss_field,
+ FileDirectiveType, PduError, PduHeader,
+};
+use crate::cfdp::tlv::EntityIdTlv;
+use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag};
+use crate::{ByteConversionError, SizeMissmatch};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+/// Finished PDU abstraction.
+///
+/// For more information, refer to CFDP chapter 5.2.2.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct EofPdu {
+ pdu_header: PduHeader,
+ condition_code: ConditionCode,
+ file_checksum: u32,
+ file_size: u64,
+ fault_location: Option,
+}
+
+impl EofPdu {
+ pub fn new_no_error(pdu_header: PduHeader, file_checksum: u32, file_size: u64) -> Self {
+ let mut eof_pdu = Self {
+ pdu_header,
+ condition_code: ConditionCode::NoError,
+ file_checksum,
+ file_size,
+ fault_location: None,
+ };
+ eof_pdu.pdu_header.pdu_datafield_len = eof_pdu.calc_pdu_datafield_len() as u16;
+ eof_pdu
+ }
+
+ pub fn pdu_header(&self) -> &PduHeader {
+ &self.pdu_header
+ }
+
+ pub fn written_len(&self) -> usize {
+ self.pdu_header.header_len() + self.calc_pdu_datafield_len()
+ }
+
+ pub fn condition_code(&self) -> ConditionCode {
+ self.condition_code
+ }
+
+ pub fn file_checksum(&self) -> u32 {
+ self.file_checksum
+ }
+
+ pub fn file_size(&self) -> u64 {
+ self.file_size
+ }
+
+ fn calc_pdu_datafield_len(&self) -> usize {
+ // One directive type octet, 4 bits condition code, 4 spare bits.
+ let mut len = 2 + core::mem::size_of::() + 4;
+ if self.pdu_header.pdu_conf.file_flag == LargeFileFlag::Large {
+ len += 4;
+ }
+ if let Some(fault_location) = self.fault_location {
+ len += fault_location.len_full();
+ }
+ len
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ let expected_len = self.written_len();
+ if buf.len() < expected_len {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: expected_len,
+ })
+ .into());
+ }
+ let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
+ buf[current_idx] = FileDirectiveType::EofPdu as u8;
+ current_idx += 1;
+ buf[current_idx] = (self.condition_code as u8) << 4;
+ current_idx += 1;
+ buf[current_idx..current_idx + 4].copy_from_slice(&self.file_checksum.to_be_bytes());
+ current_idx += 4;
+ current_idx += write_fss_field(
+ self.pdu_header.pdu_conf.file_flag,
+ self.file_size,
+ &mut buf[current_idx..],
+ )?;
+ if let Some(fault_location) = self.fault_location {
+ current_idx += fault_location.write_to_be_bytes(buf)?;
+ }
+ if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ current_idx = add_pdu_crc(buf, current_idx);
+ }
+ Ok(current_idx)
+ }
+
+ pub fn from_bytes(buf: &[u8]) -> Result {
+ let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
+ let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
+ let is_large_file = pdu_header.pdu_conf.file_flag == LargeFileFlag::Large;
+ let mut min_expected_len = 2 + 4 + 4;
+ if is_large_file {
+ min_expected_len += 4;
+ }
+ generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
+ let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
+ PduError::InvalidDirectiveType((buf[current_idx], FileDirectiveType::EofPdu))
+ })?;
+ if directive_type != FileDirectiveType::EofPdu {
+ return Err(PduError::WrongDirectiveType((
+ directive_type,
+ FileDirectiveType::EofPdu,
+ )));
+ }
+ current_idx += 1;
+ let condition_code = ConditionCode::try_from((buf[current_idx] >> 4) & 0b1111)
+ .map_err(|_| PduError::InvalidConditionCode((buf[current_idx] >> 4) & 0b1111))?;
+ current_idx += 1;
+ let file_checksum =
+ u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
+ current_idx += 4;
+ let (fss_field_len, file_size) =
+ read_fss_field(pdu_header.pdu_conf.file_flag, &buf[current_idx..]);
+ current_idx += fss_field_len;
+ let mut fault_location = None;
+ if condition_code != ConditionCode::NoError && current_idx < full_len_without_crc {
+ fault_location = Some(EntityIdTlv::from_bytes(&buf[current_idx..])?);
+ }
+ Ok(Self {
+ pdu_header,
+ condition_code,
+ file_checksum,
+ file_size,
+ fault_location,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::cfdp::pdu::eof::EofPdu;
+ use crate::cfdp::pdu::tests::{common_pdu_conf, verify_raw_header};
+ use crate::cfdp::pdu::{FileDirectiveType, PduHeader};
+ use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag};
+
+ #[test]
+ fn test_basic() {
+ let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
+ let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
+ let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
+ assert_eq!(eof_pdu.written_len(), pdu_header.header_len() + 2 + 4 + 4);
+ assert_eq!(eof_pdu.file_checksum(), 0x01020304);
+ assert_eq!(eof_pdu.file_size(), 12);
+ assert_eq!(eof_pdu.condition_code(), ConditionCode::NoError);
+ }
+
+ #[test]
+ fn test_serialization() {
+ let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
+ let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
+ let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
+ let mut buf: [u8; 64] = [0; 64];
+ let res = eof_pdu.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(written, eof_pdu.written_len());
+ verify_raw_header(eof_pdu.pdu_header(), &buf);
+ let mut current_idx = eof_pdu.pdu_header().header_len();
+ buf[current_idx] = FileDirectiveType::EofPdu as u8;
+ current_idx += 1;
+ assert_eq!(
+ (buf[current_idx] >> 4) & 0b1111,
+ ConditionCode::NoError as u8
+ );
+ current_idx += 1;
+ assert_eq!(
+ u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()),
+ 0x01020304
+ );
+ current_idx += 4;
+ assert_eq!(
+ u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()),
+ 12
+ );
+ current_idx += 4;
+ assert_eq!(current_idx, written);
+ }
+
+ #[test]
+ fn test_deserialization() {
+ let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
+ let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
+ let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
+ let mut buf: [u8; 64] = [0; 64];
+ eof_pdu.write_to_bytes(&mut buf).unwrap();
+ let eof_read_back = EofPdu::from_bytes(&buf);
+ if !eof_read_back.is_ok() {
+ let e = eof_read_back.unwrap_err();
+ panic!("deserialization failed with: {e}")
+ }
+ let eof_read_back = eof_read_back.unwrap();
+ assert_eq!(eof_read_back, eof_pdu);
+ }
+}
diff --git a/src/cfdp/pdu/file_data.rs b/src/cfdp/pdu/file_data.rs
new file mode 100644
index 0000000..ee6846c
--- /dev/null
+++ b/src/cfdp/pdu/file_data.rs
@@ -0,0 +1,397 @@
+use crate::cfdp::pdu::{
+ add_pdu_crc, generic_length_checks_pdu_deserialization, read_fss_field, write_fss_field,
+ PduError, PduHeader,
+};
+use crate::cfdp::{CrcFlag, LargeFileFlag, PduType, SegmentMetadataFlag};
+use crate::{ByteConversionError, SizeMissmatch};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum RecordContinuationState {
+ NoStartNoEnd = 0b00,
+ StartWithoutEnd = 0b01,
+ EndWithoutStart = 0b10,
+ StartAndEnd = 0b11,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct SegmentMetadata<'seg_meta> {
+ record_continuation_state: RecordContinuationState,
+ metadata: Option<&'seg_meta [u8]>,
+}
+
+impl<'seg_meta> SegmentMetadata<'seg_meta> {
+ pub fn new(
+ record_continuation_state: RecordContinuationState,
+ metadata: Option<&'seg_meta [u8]>,
+ ) -> Option {
+ if let Some(metadata) = metadata {
+ if metadata.len() > 2_usize.pow(6) - 1 {
+ return None;
+ }
+ }
+ Some(Self {
+ record_continuation_state,
+ metadata,
+ })
+ }
+
+ pub fn written_len(&self) -> usize {
+ // Map empty metadata to 0 and slice to its length.
+ 1 + self.metadata.map_or(0, |meta| meta.len())
+ }
+
+ pub(crate) fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ if buf.len() < self.written_len() {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: self.written_len(),
+ }));
+ }
+ buf[0] = ((self.record_continuation_state as u8) << 6)
+ | self.metadata.map_or(0, |meta| meta.len() as u8);
+ if let Some(metadata) = self.metadata {
+ buf[1..1 + metadata.len()].copy_from_slice(metadata)
+ }
+ Ok(self.written_len())
+ }
+
+ pub(crate) fn from_bytes(buf: &'seg_meta [u8]) -> Result {
+ if buf.is_empty() {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: 2,
+ }));
+ }
+ let mut metadata = None;
+ let seg_metadata_len = (buf[0] & 0b111111) as usize;
+ if seg_metadata_len > 0 {
+ metadata = Some(&buf[1..1 + seg_metadata_len]);
+ }
+ Ok(Self {
+ // Can't fail, only 2 bits
+ record_continuation_state: RecordContinuationState::try_from((buf[0] >> 6) & 0b11)
+ .unwrap(),
+ metadata,
+ })
+ }
+}
+
+/// File Data PDU abstraction.
+///
+/// For more information, refer to CFDP chapter 5.3.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct FileDataPdu<'seg_meta, 'file_data> {
+ pdu_header: PduHeader,
+ #[cfg_attr(feature = "serde", serde(borrow))]
+ segment_metadata: Option>,
+ offset: u64,
+ file_data: &'file_data [u8],
+}
+
+impl<'seg_meta, 'file_data> FileDataPdu<'seg_meta, 'file_data> {
+ pub fn new_with_seg_metadata(
+ pdu_header: PduHeader,
+ segment_metadata: SegmentMetadata<'seg_meta>,
+ offset: u64,
+ file_data: &'file_data [u8],
+ ) -> Self {
+ Self::new_generic(pdu_header, Some(segment_metadata), offset, file_data)
+ }
+
+ pub fn new_no_seg_metadata(
+ pdu_header: PduHeader,
+ offset: u64,
+ file_data: &'file_data [u8],
+ ) -> Self {
+ Self::new_generic(pdu_header, None, offset, file_data)
+ }
+
+ pub fn new_generic(
+ mut pdu_header: PduHeader,
+ segment_metadata: Option>,
+ offset: u64,
+ file_data: &'file_data [u8],
+ ) -> Self {
+ pdu_header.pdu_type = PduType::FileData;
+ if segment_metadata.is_some() {
+ pdu_header.seg_metadata_flag = SegmentMetadataFlag::Present;
+ }
+ let mut pdu = Self {
+ pdu_header,
+ segment_metadata,
+ offset,
+ file_data,
+ };
+ pdu.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
+ pdu
+ }
+
+ fn calc_pdu_datafield_len(&self) -> usize {
+ let mut len = core::mem::size_of::();
+ if self.pdu_header.pdu_conf.file_flag == LargeFileFlag::Large {
+ len += 4;
+ }
+ if self.segment_metadata.is_some() {
+ len += self.segment_metadata.as_ref().unwrap().written_len()
+ }
+ len += self.file_data.len();
+ if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ len += 2;
+ }
+ len
+ }
+ pub fn written_len(&self) -> usize {
+ self.pdu_header.header_len() + self.calc_pdu_datafield_len()
+ }
+
+ pub fn offset(&self) -> u64 {
+ self.offset
+ }
+
+ pub fn file_data(&self) -> &'file_data [u8] {
+ self.file_data
+ }
+
+ pub fn segment_metadata(&self) -> Option<&SegmentMetadata> {
+ self.segment_metadata.as_ref()
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ if buf.len() < self.written_len() {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: self.written_len(),
+ })
+ .into());
+ }
+ let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
+ if self.segment_metadata.is_some() {
+ current_idx += self
+ .segment_metadata
+ .as_ref()
+ .unwrap()
+ .write_to_bytes(&mut buf[current_idx..])?;
+ }
+ current_idx += write_fss_field(
+ self.pdu_header.common_pdu_conf().file_flag,
+ self.offset,
+ &mut buf[current_idx..],
+ )?;
+ buf[current_idx..current_idx + self.file_data.len()].copy_from_slice(self.file_data);
+ current_idx += self.file_data.len();
+ if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ current_idx = add_pdu_crc(buf, current_idx);
+ }
+ Ok(current_idx)
+ }
+
+ pub fn from_bytes<'longest: 'seg_meta + 'file_data>(
+ buf: &'longest [u8],
+ ) -> Result {
+ let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
+ let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
+ let min_expected_len = current_idx + core::mem::size_of::();
+ generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
+ let mut segment_metadata = None;
+ if pdu_header.seg_metadata_flag == SegmentMetadataFlag::Present {
+ segment_metadata = Some(SegmentMetadata::from_bytes(&buf[current_idx..])?);
+ current_idx += segment_metadata.as_ref().unwrap().written_len();
+ }
+ let (fss, offset) = read_fss_field(pdu_header.pdu_conf.file_flag, &buf[current_idx..]);
+ current_idx += fss;
+ if current_idx > full_len_without_crc {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: current_idx,
+ expected: full_len_without_crc,
+ })
+ .into());
+ }
+ Ok(Self {
+ pdu_header,
+ segment_metadata,
+ offset,
+ file_data: &buf[current_idx..full_len_without_crc],
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::cfdp::pdu::file_data::{FileDataPdu, RecordContinuationState, SegmentMetadata};
+ use crate::cfdp::pdu::{CommonPduConfig, PduHeader};
+ use crate::cfdp::{SegmentMetadataFlag, SegmentationControl};
+ use crate::util::UbfU8;
+
+ #[test]
+ fn test_basic() {
+ let src_id = UbfU8::new(1);
+ let dest_id = UbfU8::new(2);
+ let transaction_seq_num = UbfU8::new(3);
+ let common_conf =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_num).unwrap();
+ let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
+ let file_data: [u8; 4] = [1, 2, 3, 4];
+ let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
+ assert_eq!(fd_pdu.file_data(), file_data);
+ assert_eq!(fd_pdu.offset(), 10);
+ assert!(fd_pdu.segment_metadata().is_none());
+ assert_eq!(
+ fd_pdu.written_len(),
+ fd_pdu.pdu_header.header_len() + core::mem::size_of::() + 4
+ );
+ }
+
+ #[test]
+ fn test_serialization() {
+ let src_id = UbfU8::new(1);
+ let dest_id = UbfU8::new(2);
+ let transaction_seq_num = UbfU8::new(3);
+ let common_conf =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_num).unwrap();
+ let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
+ let file_data: [u8; 4] = [1, 2, 3, 4];
+ let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
+ let mut buf: [u8; 32] = [0; 32];
+ let res = fd_pdu.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(
+ written,
+ fd_pdu.pdu_header.header_len() + core::mem::size_of::() + 4
+ );
+ let mut current_idx = fd_pdu.pdu_header.header_len();
+ let file_size = u32::from_be_bytes(
+ buf[fd_pdu.pdu_header.header_len()..fd_pdu.pdu_header.header_len() + 4]
+ .try_into()
+ .unwrap(),
+ );
+ current_idx += 4;
+ assert_eq!(file_size, 10);
+ assert_eq!(buf[current_idx], 1);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 2);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 3);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 4);
+ }
+
+ #[test]
+ fn test_deserialization() {
+ let src_id = UbfU8::new(1);
+ let dest_id = UbfU8::new(2);
+ let transaction_seq_num = UbfU8::new(3);
+ let common_conf =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_num).unwrap();
+ let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
+ let file_data: [u8; 4] = [1, 2, 3, 4];
+ let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
+ let mut buf: [u8; 32] = [0; 32];
+ fd_pdu.write_to_bytes(&mut buf).unwrap();
+ let fd_pdu_read_back = FileDataPdu::from_bytes(&buf);
+ assert!(fd_pdu_read_back.is_ok());
+ let fd_pdu_read_back = fd_pdu_read_back.unwrap();
+ assert_eq!(fd_pdu_read_back, fd_pdu);
+ }
+
+ #[test]
+ fn test_with_seg_metadata_serialization() {
+ let src_id = UbfU8::new(1);
+ let dest_id = UbfU8::new(2);
+ let transaction_seq_num = UbfU8::new(3);
+ let common_conf =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_num).unwrap();
+ let pdu_header = PduHeader::new_for_file_data(
+ common_conf,
+ 0,
+ SegmentMetadataFlag::Present,
+ SegmentationControl::WithRecordBoundaryPreservation,
+ );
+ let file_data: [u8; 4] = [1, 2, 3, 4];
+ let seg_metadata: [u8; 4] = [4, 3, 2, 1];
+ let segment_meta =
+ SegmentMetadata::new(RecordContinuationState::StartAndEnd, Some(&seg_metadata))
+ .unwrap();
+ let fd_pdu = FileDataPdu::new_with_seg_metadata(pdu_header, segment_meta, 10, &file_data);
+ assert!(fd_pdu.segment_metadata().is_some());
+ assert_eq!(*fd_pdu.segment_metadata().unwrap(), segment_meta);
+ assert_eq!(
+ fd_pdu.written_len(),
+ fd_pdu.pdu_header.header_len()
+ + 1
+ + seg_metadata.len()
+ + core::mem::size_of::()
+ + 4
+ );
+ let mut buf: [u8; 32] = [0; 32];
+ fd_pdu
+ .write_to_bytes(&mut buf)
+ .expect("writing FD PDU failed");
+ let mut current_idx = fd_pdu.pdu_header.header_len();
+ assert_eq!(
+ RecordContinuationState::try_from((buf[current_idx] >> 6) & 0b11).unwrap(),
+ RecordContinuationState::StartAndEnd
+ );
+ assert_eq!((buf[current_idx] & 0b111111) as usize, seg_metadata.len());
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 4);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 3);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 2);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 1);
+ current_idx += 1;
+ // Still verify that the rest is written correctly.
+ assert_eq!(
+ u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()),
+ 10
+ );
+ current_idx += 4;
+ assert_eq!(buf[current_idx], 1);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 2);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 3);
+ current_idx += 1;
+ assert_eq!(buf[current_idx], 4);
+ current_idx += 1;
+ assert_eq!(current_idx, fd_pdu.written_len());
+ }
+
+ #[test]
+ fn test_with_seg_metadata_deserialization() {
+ let src_id = UbfU8::new(1);
+ let dest_id = UbfU8::new(2);
+ let transaction_seq_num = UbfU8::new(3);
+ let common_conf =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_num).unwrap();
+ let pdu_header = PduHeader::new_for_file_data(
+ common_conf,
+ 0,
+ SegmentMetadataFlag::Present,
+ SegmentationControl::WithRecordBoundaryPreservation,
+ );
+ let file_data: [u8; 4] = [1, 2, 3, 4];
+ let seg_metadata: [u8; 4] = [4, 3, 2, 1];
+ let segment_meta =
+ SegmentMetadata::new(RecordContinuationState::StartAndEnd, Some(&seg_metadata))
+ .unwrap();
+ let fd_pdu = FileDataPdu::new_with_seg_metadata(pdu_header, segment_meta, 10, &file_data);
+ let mut buf: [u8; 32] = [0; 32];
+ fd_pdu
+ .write_to_bytes(&mut buf)
+ .expect("writing FD PDU failed");
+ let fd_pdu_read_back = FileDataPdu::from_bytes(&buf);
+ assert!(fd_pdu_read_back.is_ok());
+ let fd_pdu_read_back = fd_pdu_read_back.unwrap();
+ assert_eq!(fd_pdu_read_back, fd_pdu);
+ }
+}
diff --git a/src/cfdp/pdu/finished.rs b/src/cfdp/pdu/finished.rs
new file mode 100644
index 0000000..29cf747
--- /dev/null
+++ b/src/cfdp/pdu/finished.rs
@@ -0,0 +1,336 @@
+use crate::cfdp::pdu::{
+ add_pdu_crc, generic_length_checks_pdu_deserialization, FileDirectiveType, PduError, PduHeader,
+};
+use crate::cfdp::tlv::{EntityIdTlv, Tlv, TlvType, TlvTypeField};
+use crate::cfdp::{ConditionCode, CrcFlag, PduType, TlvLvError};
+use crate::{ByteConversionError, SizeMissmatch};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum DeliveryCode {
+ Complete = 0,
+ Incomplete = 1,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum FileStatus {
+ DiscardDeliberately = 0b00,
+ DiscardedFsRejection = 0b01,
+ Retained = 0b10,
+ Unreported = 0b11,
+}
+
+/// Finished PDU abstraction.
+///
+/// For more information, refer to CFDP chapter 5.2.3.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct FinishedPdu<'fs_responses> {
+ pdu_header: PduHeader,
+ condition_code: ConditionCode,
+ delivery_code: DeliveryCode,
+ file_status: FileStatus,
+ fs_responses: Option<&'fs_responses [u8]>,
+ fault_location: Option,
+}
+
+impl<'fs_responses> FinishedPdu<'fs_responses> {
+ /// Default finished PDU: No error (no fault location field) and no filestore responses.
+ pub fn new_default(
+ pdu_header: PduHeader,
+ delivery_code: DeliveryCode,
+ file_status: FileStatus,
+ ) -> Self {
+ Self::new_generic(
+ pdu_header,
+ ConditionCode::NoError,
+ delivery_code,
+ file_status,
+ None,
+ None,
+ )
+ }
+
+ pub fn new_with_error(
+ pdu_header: PduHeader,
+ condition_code: ConditionCode,
+ delivery_code: DeliveryCode,
+ file_status: FileStatus,
+ fault_location: EntityIdTlv,
+ ) -> Self {
+ Self::new_generic(
+ pdu_header,
+ condition_code,
+ delivery_code,
+ file_status,
+ None,
+ Some(fault_location),
+ )
+ }
+
+ pub fn new_generic(
+ mut pdu_header: PduHeader,
+ condition_code: ConditionCode,
+ delivery_code: DeliveryCode,
+ file_status: FileStatus,
+ fs_responses: Option<&'fs_responses [u8]>,
+ fault_location: Option,
+ ) -> Self {
+ pdu_header.pdu_type = PduType::FileDirective;
+ let mut finished_pdu = Self {
+ pdu_header,
+ condition_code,
+ delivery_code,
+ file_status,
+ fs_responses,
+ fault_location,
+ };
+ finished_pdu.pdu_header.pdu_datafield_len = finished_pdu.calc_pdu_datafield_len() as u16;
+ finished_pdu
+ }
+ pub fn pdu_header(&self) -> &PduHeader {
+ &self.pdu_header
+ }
+
+ pub fn written_len(&self) -> usize {
+ self.pdu_header.header_len() + self.calc_pdu_datafield_len()
+ }
+
+ pub fn condition_code(&self) -> ConditionCode {
+ self.condition_code
+ }
+
+ pub fn delivery_code(&self) -> DeliveryCode {
+ self.delivery_code
+ }
+
+ pub fn file_status(&self) -> FileStatus {
+ self.file_status
+ }
+
+ pub fn filestore_responses(&self) -> Option<&'fs_responses [u8]> {
+ self.fs_responses
+ }
+
+ pub fn fault_location(&self) -> Option {
+ self.fault_location
+ }
+
+ fn calc_pdu_datafield_len(&self) -> usize {
+ let mut base_len = 2;
+ if let Some(fs_responses) = self.fs_responses {
+ base_len += fs_responses.len();
+ }
+ if let Some(fault_location) = self.fault_location {
+ base_len += fault_location.len_full();
+ }
+ base_len
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ let expected_len = self.written_len();
+ if buf.len() < expected_len {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: expected_len,
+ })
+ .into());
+ }
+
+ let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
+ buf[current_idx] = FileDirectiveType::FinishedPdu as u8;
+ current_idx += 1;
+ buf[current_idx] = ((self.condition_code as u8) << 4)
+ | ((self.delivery_code as u8) << 2)
+ | self.file_status as u8;
+ current_idx += 1;
+ if let Some(fs_responses) = self.fs_responses {
+ buf[current_idx..current_idx + fs_responses.len()].copy_from_slice(fs_responses);
+ current_idx += fs_responses.len();
+ }
+ if let Some(fault_location) = self.fault_location {
+ current_idx += fault_location.write_to_be_bytes(&mut buf[current_idx..])?;
+ }
+ if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ current_idx = add_pdu_crc(buf, current_idx);
+ }
+ Ok(current_idx)
+ }
+
+ /// Generates [Self] from a raw bytestream.
+ pub fn from_bytes(buf: &'fs_responses [u8]) -> Result {
+ let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
+ let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
+ let min_expected_len = current_idx + 2;
+ generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
+ let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
+ PduError::InvalidDirectiveType((buf[current_idx], FileDirectiveType::FinishedPdu))
+ })?;
+ if directive_type != FileDirectiveType::FinishedPdu {
+ return Err(PduError::WrongDirectiveType((
+ directive_type,
+ FileDirectiveType::FinishedPdu,
+ )));
+ }
+ current_idx += 1;
+ let condition_code = ConditionCode::try_from((buf[current_idx] >> 4) & 0b1111)
+ .map_err(|_| PduError::InvalidConditionCode((buf[current_idx] >> 4) & 0b1111))?;
+ // Unwrap is okay here for both of the following operations which can not fail.
+ let delivery_code = DeliveryCode::try_from((buf[current_idx] >> 2) & 0b1).unwrap();
+ let file_status = FileStatus::try_from(buf[current_idx] & 0b11).unwrap();
+ current_idx += 1;
+ let (fs_responses, fault_location) =
+ Self::parse_tlv_fields(current_idx, full_len_without_crc, buf)?;
+ Ok(Self {
+ pdu_header,
+ condition_code,
+ delivery_code,
+ file_status,
+ fs_responses,
+ fault_location,
+ })
+ }
+
+ fn parse_tlv_fields(
+ mut current_idx: usize,
+ full_len_without_crc: usize,
+ buf: &'fs_responses [u8],
+ ) -> Result<(Option<&'fs_responses [u8]>, Option), PduError> {
+ let mut fs_responses = None;
+ let mut fault_location = None;
+ let start_of_fs_responses = current_idx;
+ // There are leftover filestore response(s) and/or a fault location field.
+ while current_idx < full_len_without_crc {
+ let next_tlv = Tlv::from_bytes(&buf[current_idx..])?;
+ match next_tlv.tlv_type_field() {
+ TlvTypeField::Standard(tlv_type) => {
+ if tlv_type == TlvType::FilestoreResponse {
+ current_idx += next_tlv.len_full();
+ if current_idx == full_len_without_crc {
+ fs_responses = Some(&buf[start_of_fs_responses..current_idx]);
+ }
+ } else if tlv_type == TlvType::EntityId {
+ // At least one FS response is included.
+ if current_idx > full_len_without_crc {
+ fs_responses = Some(&buf[start_of_fs_responses..current_idx]);
+ }
+ fault_location = Some(EntityIdTlv::from_bytes(&buf[current_idx..])?);
+ current_idx += fault_location.as_ref().unwrap().len_full();
+ // This is considered a configuration error: The entity ID has to be the
+ // last TLV, everything else would break the whole handling of the packet
+ // TLVs.
+ if current_idx != full_len_without_crc {
+ return Err(PduError::FormatError);
+ }
+ } else {
+ return Err(TlvLvError::InvalidTlvTypeField((tlv_type as u8, None)).into());
+ }
+ }
+ TlvTypeField::Custom(raw) => {
+ return Err(TlvLvError::InvalidTlvTypeField((raw, None)).into());
+ }
+ }
+ }
+ Ok((fs_responses, fault_location))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::cfdp::pdu::finished::{DeliveryCode, FileStatus, FinishedPdu};
+ use crate::cfdp::pdu::tests::{common_pdu_conf, verify_raw_header};
+ use crate::cfdp::pdu::{FileDirectiveType, PduHeader};
+ use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag};
+
+ fn generic_finished_pdu(
+ crc_flag: CrcFlag,
+ fss: LargeFileFlag,
+ delivery_code: DeliveryCode,
+ file_status: FileStatus,
+ ) -> FinishedPdu<'static> {
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_conf(crc_flag, fss), 0);
+ FinishedPdu::new_default(pdu_header, delivery_code, file_status)
+ }
+
+ #[test]
+ fn test_basic() {
+ let finished_pdu = generic_finished_pdu(
+ CrcFlag::NoCrc,
+ LargeFileFlag::Normal,
+ DeliveryCode::Complete,
+ FileStatus::Retained,
+ );
+ assert_eq!(finished_pdu.condition_code(), ConditionCode::NoError);
+ assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete);
+ assert_eq!(finished_pdu.file_status(), FileStatus::Retained);
+ assert_eq!(finished_pdu.filestore_responses(), None);
+ assert_eq!(finished_pdu.fault_location(), None);
+ assert_eq!(finished_pdu.pdu_header().pdu_datafield_len, 2);
+ }
+
+ fn generic_serialization_test_no_error(delivery_code: DeliveryCode, file_status: FileStatus) {
+ let finished_pdu = generic_finished_pdu(
+ CrcFlag::NoCrc,
+ LargeFileFlag::Normal,
+ delivery_code,
+ file_status,
+ );
+ let mut buf: [u8; 64] = [0; 64];
+ let written = finished_pdu.write_to_bytes(&mut buf);
+ assert!(written.is_ok());
+ let written = written.unwrap();
+ assert_eq!(written, finished_pdu.written_len());
+ assert_eq!(written, finished_pdu.pdu_header().header_len() + 2);
+ verify_raw_header(finished_pdu.pdu_header(), &buf);
+ let mut current_idx = finished_pdu.pdu_header().header_len();
+ assert_eq!(buf[current_idx], FileDirectiveType::FinishedPdu as u8);
+ current_idx += 1;
+ assert_eq!(
+ (buf[current_idx] >> 4) & 0b1111,
+ ConditionCode::NoError as u8
+ );
+ assert_eq!((buf[current_idx] >> 2) & 0b1, delivery_code as u8);
+ assert_eq!(buf[current_idx] & 0b11, file_status as u8);
+ assert_eq!(current_idx + 1, written);
+ }
+
+ #[test]
+ fn test_serialization_simple() {
+ generic_serialization_test_no_error(DeliveryCode::Complete, FileStatus::Retained);
+ }
+
+ #[test]
+ fn test_serialization_simple_2() {
+ generic_serialization_test_no_error(
+ DeliveryCode::Incomplete,
+ FileStatus::DiscardDeliberately,
+ );
+ }
+
+ #[test]
+ fn test_serialization_simple_3() {
+ generic_serialization_test_no_error(DeliveryCode::Incomplete, FileStatus::Unreported);
+ }
+
+ #[test]
+ fn test_deserialization_simple() {
+ let finished_pdu = generic_finished_pdu(
+ CrcFlag::NoCrc,
+ LargeFileFlag::Normal,
+ DeliveryCode::Complete,
+ FileStatus::Retained,
+ );
+ let mut buf: [u8; 64] = [0; 64];
+ finished_pdu.write_to_bytes(&mut buf).unwrap();
+ let read_back = FinishedPdu::from_bytes(&buf);
+ assert!(read_back.is_ok());
+ let read_back = read_back.unwrap();
+ assert_eq!(finished_pdu, read_back);
+ }
+}
diff --git a/src/cfdp/pdu/metadata.rs b/src/cfdp/pdu/metadata.rs
new file mode 100644
index 0000000..fbdb982
--- /dev/null
+++ b/src/cfdp/pdu/metadata.rs
@@ -0,0 +1,527 @@
+use crate::cfdp::lv::Lv;
+use crate::cfdp::pdu::{
+ add_pdu_crc, generic_length_checks_pdu_deserialization, read_fss_field, write_fss_field,
+ FileDirectiveType, PduError, PduHeader,
+};
+use crate::cfdp::tlv::Tlv;
+use crate::cfdp::{ChecksumType, CrcFlag, LargeFileFlag, PduType};
+use crate::{ByteConversionError, SizeMissmatch};
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct MetadataGenericParams {
+ closure_requested: bool,
+ checksum_type: ChecksumType,
+ file_size: u64,
+}
+
+impl MetadataGenericParams {
+ pub fn new(closure_requested: bool, checksum_type: ChecksumType, file_size: u64) -> Self {
+ Self {
+ closure_requested,
+ checksum_type,
+ file_size,
+ }
+ }
+}
+
+pub fn build_metadata_opts_from_slice(
+ buf: &mut [u8],
+ tlvs: &[Tlv],
+) -> Result {
+ let mut written = 0;
+ for tlv in tlvs {
+ written += tlv.write_to_bytes(&mut buf[written..])?;
+ }
+ Ok(written)
+}
+
+#[cfg(feature = "alloc")]
+pub fn build_metadata_opts_from_vec(
+ buf: &mut [u8],
+ tlvs: &Vec,
+) -> Result {
+ build_metadata_opts_from_slice(buf, tlvs.as_slice())
+}
+
+/// Helper structure to loop through all options of a metadata PDU. It should be noted that
+/// iterators in Rust are not fallible, but the TLV creation can fail, for example if the raw TLV
+/// data is invalid for some reason. In that case, the iterator will yield [None] because there
+/// is no way to recover from this.
+///
+/// The user can accumulate the length of all TLVs yielded by the iterator and compare it against
+/// the full length of the options to check whether the iterator was able to parse all TLVs
+/// successfully.
+pub struct OptionsIter<'opts> {
+ opt_buf: &'opts [u8],
+ current_idx: usize,
+}
+
+impl<'opts> Iterator for OptionsIter<'opts> {
+ type Item = Tlv<'opts>;
+
+ fn next(&mut self) -> Option {
+ if self.current_idx == self.opt_buf.len() {
+ return None;
+ }
+ let tlv = Tlv::from_bytes(&self.opt_buf[self.current_idx..]);
+ // There are not really fallible iterators so we can't continue here..
+ if tlv.is_err() {
+ return None;
+ }
+ let tlv = tlv.unwrap();
+ self.current_idx += tlv.len_full();
+ Some(tlv)
+ }
+}
+
+/// Metadata PDU abstraction.
+///
+/// For more information, refer to CFDP chapter 5.2.5.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct MetadataPdu<'src_name, 'dest_name, 'opts> {
+ pdu_header: PduHeader,
+ metadata_params: MetadataGenericParams,
+ #[cfg_attr(feature = "serde", serde(borrow))]
+ src_file_name: Lv<'src_name>,
+ #[cfg_attr(feature = "serde", serde(borrow))]
+ dest_file_name: Lv<'dest_name>,
+ options: Option<&'opts [u8]>,
+}
+
+impl<'src_name, 'dest_name, 'opts> MetadataPdu<'src_name, 'dest_name, 'opts> {
+ pub fn new_no_opts(
+ pdu_header: PduHeader,
+ metadata_params: MetadataGenericParams,
+ src_file_name: Lv<'src_name>,
+ dest_file_name: Lv<'dest_name>,
+ ) -> Self {
+ Self::new(
+ pdu_header,
+ metadata_params,
+ src_file_name,
+ dest_file_name,
+ None,
+ )
+ }
+
+ pub fn new_with_opts(
+ pdu_header: PduHeader,
+ metadata_params: MetadataGenericParams,
+ src_file_name: Lv<'src_name>,
+ dest_file_name: Lv<'dest_name>,
+ options: &'opts [u8],
+ ) -> Self {
+ Self::new(
+ pdu_header,
+ metadata_params,
+ src_file_name,
+ dest_file_name,
+ Some(options),
+ )
+ }
+
+ pub fn new(
+ mut pdu_header: PduHeader,
+ metadata_params: MetadataGenericParams,
+ src_file_name: Lv<'src_name>,
+ dest_file_name: Lv<'dest_name>,
+ options: Option<&'opts [u8]>,
+ ) -> Self {
+ pdu_header.pdu_type = PduType::FileDirective;
+ let mut pdu = Self {
+ pdu_header,
+ metadata_params,
+ src_file_name,
+ dest_file_name,
+ options,
+ };
+ pdu.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
+ pdu
+ }
+
+ pub fn src_file_name(&self) -> Lv<'src_name> {
+ self.src_file_name
+ }
+
+ pub fn dest_file_name(&self) -> Lv<'dest_name> {
+ self.dest_file_name
+ }
+
+ pub fn options(&self) -> Option<&'opts [u8]> {
+ self.options
+ }
+
+ /// Yield an iterator which can be used to loop through all options. Returns [None] if the
+ /// options field is empty.
+ pub fn options_iter(&self) -> Option> {
+ self.options?;
+ Some(OptionsIter {
+ opt_buf: self.options.unwrap(),
+ current_idx: 0,
+ })
+ }
+
+ pub fn written_len(&self) -> usize {
+ self.pdu_header.header_len() + self.calc_pdu_datafield_len()
+ }
+
+ fn calc_pdu_datafield_len(&self) -> usize {
+ // One directve type octet and one byte of the directive parameter field.
+ let mut len = 2;
+ if self.pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large {
+ len += 8;
+ } else {
+ len += 4;
+ }
+ len += self.src_file_name.len_full();
+ len += self.dest_file_name.len_full();
+ if let Some(opts) = self.options {
+ len += opts.len();
+ }
+ if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ len += 2;
+ }
+ len
+ }
+
+ pub fn pdu_header(&self) -> &PduHeader {
+ &self.pdu_header
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ let expected_len = self.written_len();
+ if buf.len() < expected_len {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: expected_len,
+ })
+ .into());
+ }
+
+ let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
+ buf[current_idx] = FileDirectiveType::MetadataPdu as u8;
+ current_idx += 1;
+ buf[current_idx] = ((self.metadata_params.closure_requested as u8) << 7)
+ | (self.metadata_params.checksum_type as u8);
+ current_idx += 1;
+ current_idx += write_fss_field(
+ self.pdu_header.common_pdu_conf().file_flag,
+ self.metadata_params.file_size,
+ &mut buf[current_idx..],
+ )?;
+ current_idx += self
+ .src_file_name
+ .write_to_be_bytes(&mut buf[current_idx..])?;
+ current_idx += self
+ .dest_file_name
+ .write_to_be_bytes(&mut buf[current_idx..])?;
+ if let Some(opts) = self.options {
+ buf[current_idx..current_idx + opts.len()].copy_from_slice(opts);
+ current_idx += opts.len();
+ }
+ if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ current_idx = add_pdu_crc(buf, current_idx);
+ }
+ Ok(current_idx)
+ }
+
+ pub fn from_bytes<'longest: 'src_name + 'dest_name + 'opts>(
+ buf: &'longest [u8],
+ ) -> Result {
+ let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
+ let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
+ let is_large_file = pdu_header.pdu_conf.file_flag == LargeFileFlag::Large;
+ // Minimal length: 1 byte + FSS (4 byte) + 2 empty LV (1 byte)
+ let mut min_expected_len = current_idx + 7;
+ if is_large_file {
+ min_expected_len += 4;
+ }
+ generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
+ let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
+ PduError::InvalidDirectiveType((buf[current_idx], FileDirectiveType::MetadataPdu))
+ })?;
+ if directive_type != FileDirectiveType::MetadataPdu {
+ return Err(PduError::WrongDirectiveType((
+ directive_type,
+ FileDirectiveType::MetadataPdu,
+ )));
+ }
+ current_idx += 1;
+ let (fss_len, file_size) =
+ read_fss_field(pdu_header.pdu_conf.file_flag, &buf[current_idx + 1..]);
+ let metadata_params = MetadataGenericParams {
+ closure_requested: ((buf[current_idx] >> 6) & 0b1) != 0,
+ checksum_type: ChecksumType::try_from(buf[current_idx] & 0b1111)
+ .map_err(|_| PduError::InvalidChecksumType(buf[current_idx] & 0b1111))?,
+ file_size,
+ };
+ current_idx += 1 + fss_len;
+ let src_file_name = Lv::from_bytes(&buf[current_idx..])?;
+ current_idx += src_file_name.len_full();
+ let dest_file_name = Lv::from_bytes(&buf[current_idx..])?;
+ current_idx += dest_file_name.len_full();
+ // All left-over bytes are options.
+ let mut options = None;
+ if current_idx < full_len_without_crc {
+ options = Some(&buf[current_idx..full_len_without_crc]);
+ }
+ Ok(Self {
+ pdu_header,
+ metadata_params,
+ src_file_name,
+ dest_file_name,
+ options,
+ })
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use crate::cfdp::lv::Lv;
+ use crate::cfdp::pdu::metadata::{
+ build_metadata_opts_from_slice, build_metadata_opts_from_vec, MetadataGenericParams,
+ MetadataPdu,
+ };
+ use crate::cfdp::pdu::tests::{common_pdu_conf, verify_raw_header};
+ use crate::cfdp::pdu::{FileDirectiveType, PduHeader};
+ use crate::cfdp::tlv::{Tlv, TlvType};
+ use crate::cfdp::{
+ ChecksumType, CrcFlag, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl,
+ };
+ use std::vec;
+
+ const SRC_FILENAME: &'static str = "hello-world.txt";
+ const DEST_FILENAME: &'static str = "hello-world2.txt";
+
+ fn generic_metadata_pdu<'opts>(
+ crc_flag: CrcFlag,
+ fss: LargeFileFlag,
+ opts: Option<&'opts [u8]>,
+ ) -> (
+ Lv<'static>,
+ Lv<'static>,
+ MetadataPdu<'static, 'static, 'opts>,
+ ) {
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_conf(crc_flag, fss), 0);
+ let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, 0x1010);
+ let src_filename = Lv::new_from_str(SRC_FILENAME).expect("Generating string LV failed");
+ let dest_filename =
+ Lv::new_from_str(DEST_FILENAME).expect("Generating destination LV failed");
+ (
+ src_filename,
+ dest_filename,
+ MetadataPdu::new(
+ pdu_header,
+ metadata_params,
+ src_filename,
+ dest_filename,
+ opts,
+ ),
+ )
+ }
+
+ #[test]
+ fn test_basic() {
+ let (src_filename, dest_filename, metadata_pdu) =
+ generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Normal, None);
+ assert_eq!(
+ metadata_pdu.written_len(),
+ metadata_pdu.pdu_header().header_len()
+ + 1
+ + 1
+ + 4
+ + src_filename.len_full()
+ + dest_filename.len_full()
+ );
+ assert_eq!(metadata_pdu.src_file_name(), src_filename);
+ assert_eq!(metadata_pdu.dest_file_name(), dest_filename);
+ assert_eq!(metadata_pdu.options(), None);
+ }
+
+ #[test]
+ fn test_serialization() {
+ let (src_filename, dest_filename, metadata_pdu) =
+ generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Normal, None);
+ let mut buf: [u8; 64] = [0; 64];
+ let res = metadata_pdu.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(
+ written,
+ metadata_pdu.pdu_header.header_len()
+ + 1
+ + 1
+ + 4
+ + src_filename.len_full()
+ + dest_filename.len_full()
+ );
+ verify_raw_header(metadata_pdu.pdu_header(), &buf);
+ assert_eq!(buf[7], FileDirectiveType::MetadataPdu as u8);
+ assert_eq!(buf[8] >> 6, false as u8);
+ assert_eq!(buf[8] & 0b1111, ChecksumType::Crc32 as u8);
+ assert_eq!(u32::from_be_bytes(buf[9..13].try_into().unwrap()), 0x1010);
+ let mut current_idx = 13;
+ let src_name_from_raw =
+ Lv::from_bytes(&buf[current_idx..]).expect("Creating source name LV failed");
+ assert_eq!(src_name_from_raw, src_filename);
+ current_idx += src_name_from_raw.len_full();
+ let dest_name_from_raw =
+ Lv::from_bytes(&buf[current_idx..]).expect("Creating dest name LV failed");
+ assert_eq!(dest_name_from_raw, dest_filename);
+ current_idx += dest_name_from_raw.len_full();
+ // No options, so no additional data here.
+ assert_eq!(current_idx, written);
+ }
+
+ #[test]
+ fn test_deserialization() {
+ let (_, _, metadata_pdu) =
+ generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Normal, None);
+ let mut buf: [u8; 64] = [0; 64];
+ metadata_pdu.write_to_bytes(&mut buf).unwrap();
+ let pdu_read_back = MetadataPdu::from_bytes(&buf);
+ assert!(pdu_read_back.is_ok());
+ let pdu_read_back = pdu_read_back.unwrap();
+ assert_eq!(pdu_read_back, metadata_pdu);
+ }
+
+ #[test]
+ fn test_with_crc_flag() {
+ let (src_filename, dest_filename, metadata_pdu) =
+ generic_metadata_pdu(CrcFlag::WithCrc, LargeFileFlag::Normal, None);
+ let mut buf: [u8; 64] = [0; 64];
+ let write_res = metadata_pdu.write_to_bytes(&mut buf);
+ assert!(write_res.is_ok());
+ let written = write_res.unwrap();
+ assert_eq!(
+ written,
+ metadata_pdu.pdu_header().header_len()
+ + 1
+ + 1
+ + core::mem::size_of::()
+ + src_filename.len_full()
+ + dest_filename.len_full()
+ + 2
+ );
+ let pdu_read_back = MetadataPdu::from_bytes(&buf).unwrap();
+ assert_eq!(pdu_read_back, metadata_pdu);
+ }
+
+ #[test]
+ fn test_with_large_file_flag() {
+ let (src_filename, dest_filename, metadata_pdu) =
+ generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Large, None);
+ let mut buf: [u8; 64] = [0; 64];
+ let write_res = metadata_pdu.write_to_bytes(&mut buf);
+ assert!(write_res.is_ok());
+ let written = write_res.unwrap();
+ assert_eq!(
+ written,
+ metadata_pdu.pdu_header().header_len()
+ + 1
+ + 1
+ + core::mem::size_of::()
+ + src_filename.len_full()
+ + dest_filename.len_full()
+ );
+ let pdu_read_back = MetadataPdu::from_bytes(&buf).unwrap();
+ assert_eq!(pdu_read_back, metadata_pdu);
+ }
+
+ #[test]
+ fn test_opts_builders() {
+ let tlv1 = Tlv::new_empty(TlvType::FlowLabel);
+ let msg_to_user: [u8; 4] = [1, 2, 3, 4];
+ let tlv2 = Tlv::new(TlvType::MsgToUser, &msg_to_user).unwrap();
+ let tlv_slice = [tlv1, tlv2];
+ let mut buf: [u8; 32] = [0; 32];
+ let opts = build_metadata_opts_from_slice(&mut buf, &tlv_slice);
+ assert!(opts.is_ok());
+ let opts_len = opts.unwrap();
+ assert_eq!(opts_len, tlv1.len_full() + tlv2.len_full());
+ let tlv1_conv_back = Tlv::from_bytes(&buf).unwrap();
+ assert_eq!(tlv1_conv_back, tlv1);
+ let tlv2_conv_back = Tlv::from_bytes(&buf[tlv1_conv_back.len_full()..]).unwrap();
+ assert_eq!(tlv2_conv_back, tlv2);
+ }
+
+ #[test]
+ fn test_opts_builders_from_vec() {
+ let tlv1 = Tlv::new_empty(TlvType::FlowLabel);
+ let msg_to_user: [u8; 4] = [1, 2, 3, 4];
+ let tlv2 = Tlv::new(TlvType::MsgToUser, &msg_to_user).unwrap();
+ let tlv_vec = vec![tlv1, tlv2];
+ let mut buf: [u8; 32] = [0; 32];
+ let opts = build_metadata_opts_from_vec(&mut buf, &tlv_vec);
+ assert!(opts.is_ok());
+ let opts_len = opts.unwrap();
+ assert_eq!(opts_len, tlv1.len_full() + tlv2.len_full());
+ let tlv1_conv_back = Tlv::from_bytes(&buf).unwrap();
+ assert_eq!(tlv1_conv_back, tlv1);
+ let tlv2_conv_back = Tlv::from_bytes(&buf[tlv1_conv_back.len_full()..]).unwrap();
+ assert_eq!(tlv2_conv_back, tlv2);
+ }
+
+ #[test]
+ fn test_with_opts() {
+ let tlv1 = Tlv::new_empty(TlvType::FlowLabel);
+ let msg_to_user: [u8; 4] = [1, 2, 3, 4];
+ let tlv2 = Tlv::new(TlvType::MsgToUser, &msg_to_user).unwrap();
+ let tlv_vec = vec![tlv1, tlv2];
+ let mut opts_buf: [u8; 32] = [0; 32];
+ let opts_len = build_metadata_opts_from_vec(&mut opts_buf, &tlv_vec).unwrap();
+ let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
+ CrcFlag::NoCrc,
+ LargeFileFlag::Normal,
+ Some(&opts_buf[..opts_len]),
+ );
+ let mut buf: [u8; 128] = [0; 128];
+ let write_res = metadata_pdu.write_to_bytes(&mut buf);
+ assert!(write_res.is_ok());
+ let written = write_res.unwrap();
+ assert_eq!(
+ written,
+ metadata_pdu.pdu_header.header_len()
+ + 1
+ + 1
+ + 4
+ + src_filename.len_full()
+ + dest_filename.len_full()
+ + opts_len
+ );
+ let pdu_read_back = MetadataPdu::from_bytes(&buf).unwrap();
+ assert_eq!(pdu_read_back, metadata_pdu);
+ let opts_iter = pdu_read_back.options_iter();
+ assert!(opts_iter.is_some());
+ let opts_iter = opts_iter.unwrap();
+ let mut accumulated_len = 0;
+ for (idx, opt) in opts_iter.enumerate() {
+ assert_eq!(tlv_vec[idx], opt);
+ accumulated_len += opt.len_full();
+ }
+ assert_eq!(accumulated_len, pdu_read_back.options().unwrap().len());
+ }
+
+ #[test]
+ fn test_corrects_pdu_header() {
+ let pdu_header = PduHeader::new_for_file_data(
+ common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal),
+ 0,
+ SegmentMetadataFlag::NotPresent,
+ SegmentationControl::NoRecordBoundaryPreservation,
+ );
+ let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, 10);
+ let src_filename = Lv::new_from_str(SRC_FILENAME).expect("Generating string LV failed");
+ let dest_filename =
+ Lv::new_from_str(DEST_FILENAME).expect("Generating destination LV failed");
+ let metadata_pdu =
+ MetadataPdu::new_no_opts(pdu_header, metadata_params, src_filename, dest_filename);
+ assert_eq!(metadata_pdu.pdu_header().pdu_type(), PduType::FileDirective);
+ }
+}
diff --git a/src/cfdp/pdu/mod.rs b/src/cfdp/pdu/mod.rs
new file mode 100644
index 0000000..2ef5771
--- /dev/null
+++ b/src/cfdp/pdu/mod.rs
@@ -0,0 +1,907 @@
+//! CFDP Packet Data Unit (PDU) support.
+use crate::cfdp::*;
+use crate::util::{UnsignedByteField, UnsignedEnum};
+use crate::CRC_CCITT_FALSE;
+use crate::{ByteConversionError, SizeMissmatch};
+use core::fmt::{Display, Formatter};
+#[cfg(feature = "std")]
+use std::error::Error;
+
+pub mod eof;
+pub mod file_data;
+pub mod finished;
+pub mod metadata;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum FileDirectiveType {
+ EofPdu = 0x04,
+ FinishedPdu = 0x05,
+ AckPdu = 0x06,
+ MetadataPdu = 0x07,
+ NakPdu = 0x08,
+ PromptPdu = 0x09,
+ KeepAlivePdu = 0x0c,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum PduError {
+ ByteConversionError(ByteConversionError),
+ /// Found version ID invalid, not equal to [CFDP_VERSION_2].
+ CfdpVersionMissmatch(u8),
+ /// Invalid length for the entity ID detected. Only the values 1, 2, 4 and 8 are supported.
+ InvalidEntityLen(u8),
+ /// Invalid length for the entity ID detected. Only the values 1, 2, 4 and 8 are supported.
+ InvalidTransactionSeqNumLen(u8),
+ /// The first entry will be the source entity ID length, the second one the destination entity
+ /// ID length.
+ SourceDestIdLenMissmatch((usize, usize)),
+ /// The first tuple entry will be the found directive type, the second entry the expected entry
+ /// type.
+ WrongDirectiveType((FileDirectiveType, FileDirectiveType)),
+ /// The directive type field contained a value not in the range of permitted values.
+ /// The first tuple entry will be the found raw number, the second entry the expected entry
+ /// type.
+ InvalidDirectiveType((u8, FileDirectiveType)),
+ /// Invalid condition code. Contains the raw detected value.
+ InvalidConditionCode(u8),
+ /// Invalid checksum type which is not part of the checksums listed in the
+ /// [SANA Checksum Types registry](https://sanaregistry.org/r/checksum_identifiers/).
+ InvalidChecksumType(u8),
+ FileSizeTooLarge(u64),
+ /// If the CRC flag for a PDU is enabled and the checksum check fails. Contains raw 16-bit CRC.
+ ChecksumError(u16),
+ /// Generic error for invalid PDU formats.
+ FormatError,
+ /// Error handling a TLV field.
+ TlvLvError(TlvLvError),
+}
+
+impl Display for PduError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+ match self {
+ PduError::InvalidEntityLen(raw_id) => {
+ write!(
+ f,
+ "Invalid PDU entity ID length {raw_id}, only [1, 2, 4, 8] are allowed"
+ )
+ }
+ PduError::InvalidTransactionSeqNumLen(raw_id) => {
+ write!(
+ f,
+ "invalid PDUtransaction seq num length {raw_id}, only [1, 2, 4, 8] are allowed"
+ )
+ }
+ PduError::CfdpVersionMissmatch(raw) => {
+ write!(
+ f,
+ "cfdp version missmatch, found {raw}, expected {CFDP_VERSION_2}"
+ )
+ }
+ PduError::SourceDestIdLenMissmatch((src_len, dest_len)) => {
+ write!(
+ f,
+ "missmatch of PDU source length {src_len} and destination length {dest_len}"
+ )
+ }
+ PduError::ByteConversionError(e) => {
+ write!(f, "{}", e)
+ }
+ PduError::FileSizeTooLarge(value) => {
+ write!(f, "file size value {value} exceeds allowed 32 bit width")
+ }
+ PduError::WrongDirectiveType((found, expected)) => {
+ write!(f, "found directive type {found:?}, expected {expected:?}")
+ }
+ PduError::InvalidConditionCode(raw_code) => {
+ write!(f, "found invalid condition code with raw value {raw_code}")
+ }
+ PduError::InvalidDirectiveType((found, expected)) => {
+ write!(
+ f,
+ "invalid directive type value {found}, expected {expected:?} ({})",
+ *expected as u8
+ )
+ }
+ PduError::InvalidChecksumType(checksum_type) => {
+ write!(f, "invalid checksum type {checksum_type}")
+ }
+ PduError::ChecksumError(checksum) => {
+ write!(f, "checksum error for CRC {checksum:#04x}")
+ }
+ PduError::TlvLvError(error) => {
+ write!(f, "pdu tlv error: {error}")
+ }
+ PduError::FormatError => {
+ write!(f, "generic PDU format error")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl Error for PduError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ match self {
+ PduError::ByteConversionError(e) => Some(e),
+ PduError::TlvLvError(e) => Some(e),
+ _ => None,
+ }
+ }
+}
+
+impl From for PduError {
+ fn from(value: ByteConversionError) -> Self {
+ Self::ByteConversionError(value)
+ }
+}
+
+impl From for PduError {
+ fn from(e: TlvLvError) -> Self {
+ Self::TlvLvError(e)
+ }
+}
+
+/// Common configuration fields for a PDU.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct CommonPduConfig {
+ source_entity_id: UnsignedByteField,
+ dest_entity_id: UnsignedByteField,
+ pub transaction_seq_num: UnsignedByteField,
+ pub trans_mode: TransmissionMode,
+ pub file_flag: LargeFileFlag,
+ pub crc_flag: CrcFlag,
+ pub direction: Direction,
+}
+
+// TODO: Builder pattern might be applicable here..
+impl CommonPduConfig {
+ pub fn new(
+ source_id: impl Into,
+ dest_id: impl Into,
+ transaction_seq_num: impl Into,
+ trans_mode: TransmissionMode,
+ file_flag: LargeFileFlag,
+ crc_flag: CrcFlag,
+ direction: Direction,
+ ) -> Result {
+ let source_id = source_id.into();
+ let dest_id = dest_id.into();
+ let transaction_seq_num = transaction_seq_num.into();
+ if source_id.len() != dest_id.len() {
+ return Err(PduError::SourceDestIdLenMissmatch((
+ source_id.len(),
+ dest_id.len(),
+ )));
+ }
+ if source_id.len() != 1
+ && source_id.len() != 2
+ && source_id.len() != 4
+ && source_id.len() != 8
+ {
+ return Err(PduError::InvalidEntityLen(source_id.len() as u8));
+ }
+ if transaction_seq_num.len() != 1
+ && transaction_seq_num.len() != 2
+ && transaction_seq_num.len() != 4
+ && transaction_seq_num.len() != 8
+ {
+ return Err(PduError::InvalidTransactionSeqNumLen(
+ transaction_seq_num.len() as u8,
+ ));
+ }
+ Ok(Self {
+ source_entity_id: source_id,
+ dest_entity_id: dest_id,
+ transaction_seq_num,
+ trans_mode,
+ file_flag,
+ crc_flag,
+ direction,
+ })
+ }
+
+ pub fn new_with_defaults(
+ source_id: impl Into,
+ dest_id: impl Into,
+ transaction_seq_num: impl Into,
+ ) -> Result {
+ Self::new(
+ source_id,
+ dest_id,
+ transaction_seq_num,
+ TransmissionMode::Acknowledged,
+ LargeFileFlag::Normal,
+ CrcFlag::NoCrc,
+ Direction::TowardsReceiver,
+ )
+ }
+
+ pub fn source_id(&self) -> UnsignedByteField {
+ self.source_entity_id
+ }
+
+ pub fn dest_id(&self) -> UnsignedByteField {
+ self.dest_entity_id
+ }
+}
+
+pub const FIXED_HEADER_LEN: usize = 4;
+
+/// Abstraction for the PDU header common to all CFDP PDUs.
+///
+/// For detailed information, refer to chapter 5.1 of the CFDP standard.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct PduHeader {
+ pdu_type: PduType,
+ pdu_conf: CommonPduConfig,
+ seg_metadata_flag: SegmentMetadataFlag,
+ seg_ctrl: SegmentationControl,
+ pdu_datafield_len: u16,
+}
+
+impl PduHeader {
+ pub fn new_for_file_data(
+ pdu_conf: CommonPduConfig,
+ pdu_datafield_len: u16,
+ seg_metadata_flag: SegmentMetadataFlag,
+ seg_ctrl: SegmentationControl,
+ ) -> Self {
+ Self::new_generic(
+ PduType::FileData,
+ pdu_conf,
+ pdu_datafield_len,
+ seg_metadata_flag,
+ seg_ctrl,
+ )
+ }
+
+ pub fn new_for_file_data_default(pdu_conf: CommonPduConfig, pdu_datafield_len: u16) -> Self {
+ Self::new_generic(
+ PduType::FileData,
+ pdu_conf,
+ pdu_datafield_len,
+ SegmentMetadataFlag::NotPresent,
+ SegmentationControl::NoRecordBoundaryPreservation,
+ )
+ }
+ pub fn new_no_file_data(pdu_conf: CommonPduConfig, pdu_datafield_len: u16) -> Self {
+ Self::new_generic(
+ PduType::FileDirective,
+ pdu_conf,
+ pdu_datafield_len,
+ SegmentMetadataFlag::NotPresent,
+ SegmentationControl::NoRecordBoundaryPreservation,
+ )
+ }
+
+ pub fn new_generic(
+ pdu_type: PduType,
+ pdu_conf: CommonPduConfig,
+ pdu_datafield_len: u16,
+ seg_metadata_flag: SegmentMetadataFlag,
+ seg_ctrl: SegmentationControl,
+ ) -> Self {
+ Self {
+ pdu_type,
+ pdu_conf,
+ seg_metadata_flag,
+ seg_ctrl,
+ pdu_datafield_len,
+ }
+ }
+
+ /// Returns only the length of the PDU header when written to a raw buffer.
+ pub fn header_len(&self) -> usize {
+ FIXED_HEADER_LEN
+ + self.pdu_conf.source_entity_id.len()
+ + self.pdu_conf.transaction_seq_num.len()
+ + self.pdu_conf.dest_entity_id.len()
+ }
+
+ /// Returns the full length of the PDU when written to a raw buffer, which is the header length
+ /// plus the PDU datafield length.
+ pub fn pdu_len(&self) -> usize {
+ self.header_len() + self.pdu_datafield_len as usize
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ // Internal note: There is currently no way to pass a PDU configuration like this, but
+ // this check is still kept for defensive programming.
+ if self.pdu_conf.source_entity_id.len() != self.pdu_conf.dest_entity_id.len() {
+ return Err(PduError::SourceDestIdLenMissmatch((
+ self.pdu_conf.source_entity_id.len(),
+ self.pdu_conf.dest_entity_id.len(),
+ )));
+ }
+ if buf.len()
+ < FIXED_HEADER_LEN
+ + self.pdu_conf.source_entity_id.len()
+ + self.pdu_conf.transaction_seq_num.len()
+ {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: FIXED_HEADER_LEN,
+ })
+ .into());
+ }
+ let mut current_idx = 0;
+ buf[current_idx] = (CFDP_VERSION_2 << 5)
+ | ((self.pdu_type as u8) << 4)
+ | ((self.pdu_conf.direction as u8) << 3)
+ | ((self.pdu_conf.trans_mode as u8) << 2)
+ | ((self.pdu_conf.crc_flag as u8) << 1)
+ | (self.pdu_conf.file_flag as u8);
+ current_idx += 1;
+ buf[current_idx..current_idx + 2].copy_from_slice(&self.pdu_datafield_len.to_be_bytes());
+ current_idx += 2;
+ buf[current_idx] = ((self.seg_ctrl as u8) << 7)
+ | (((self.pdu_conf.source_entity_id.len() - 1) as u8) << 4)
+ | ((self.seg_metadata_flag as u8) << 3)
+ | ((self.pdu_conf.transaction_seq_num.len() - 1) as u8);
+ current_idx += 1;
+ self.pdu_conf.source_entity_id.write_to_be_bytes(
+ &mut buf[current_idx..current_idx + self.pdu_conf.source_entity_id.len()],
+ )?;
+ current_idx += self.pdu_conf.source_entity_id.len();
+ self.pdu_conf.transaction_seq_num.write_to_be_bytes(
+ &mut buf[current_idx..current_idx + self.pdu_conf.transaction_seq_num.len()],
+ )?;
+ current_idx += self.pdu_conf.transaction_seq_num.len();
+ self.pdu_conf.dest_entity_id.write_to_be_bytes(
+ &mut buf[current_idx..current_idx + self.pdu_conf.dest_entity_id.len()],
+ )?;
+ current_idx += self.pdu_conf.dest_entity_id.len();
+ Ok(current_idx)
+ }
+
+ /// This function first verifies that the buffer can hold the full length of the PDU parsed from
+ /// the header. Then, it verifies the checksum as specified in the standard if the CRC flag
+ /// of the PDU header is set.
+ ///
+ /// This function will return the PDU length excluding the 2 CRC bytes on success. If the CRC
+ /// flag is not set, it will simply return the PDU length.
+ pub fn verify_length_and_checksum(&self, buf: &[u8]) -> Result {
+ if buf.len() < self.pdu_len() {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: self.pdu_len(),
+ })
+ .into());
+ }
+ if self.pdu_conf.crc_flag == CrcFlag::WithCrc {
+ let mut digest = CRC_CCITT_FALSE.digest();
+ digest.update(&buf[..self.pdu_len()]);
+ if digest.finalize() != 0 {
+ return Err(PduError::ChecksumError(u16::from_be_bytes(
+ buf[self.pdu_len() - 2..self.pdu_len()].try_into().unwrap(),
+ )));
+ }
+ return Ok(self.pdu_len() - 2);
+ }
+ Ok(self.pdu_len())
+ }
+
+ /// Please note that this function will not verify that the passed buffer can hold the full
+ /// PDU length. This allows recovering the header portion even if the data field length is
+ /// invalid. This function will also not do the CRC procedure specified in chapter 4.1.1
+ /// and 4.1.2 because performing the CRC procedure requires the buffer to be large enough
+ /// to hold the full PDU.
+ ///
+ /// Both functions can however be performed with the [Self::verify_length_and_checksum]
+ /// function.
+ pub fn from_bytes(buf: &[u8]) -> Result<(Self, usize), PduError> {
+ if buf.len() < FIXED_HEADER_LEN {
+ return Err(PduError::ByteConversionError(
+ ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: FIXED_HEADER_LEN,
+ }),
+ ));
+ }
+ let cfdp_version_raw = (buf[0] >> 5) & 0b111;
+ if cfdp_version_raw != CFDP_VERSION_2 {
+ return Err(PduError::CfdpVersionMissmatch(cfdp_version_raw));
+ }
+ // unwrap for single bit fields: This operation will always succeed.
+ let pdu_type = PduType::try_from((buf[0] >> 4) & 0b1).unwrap();
+ let direction = Direction::try_from((buf[0] >> 3) & 0b1).unwrap();
+ let trans_mode = TransmissionMode::try_from((buf[0] >> 2) & 0b1).unwrap();
+ let crc_flag = CrcFlag::try_from((buf[0] >> 1) & 0b1).unwrap();
+ let file_flag = LargeFileFlag::try_from(buf[0] & 0b1).unwrap();
+ let pdu_datafield_len = u16::from_be_bytes(buf[1..3].try_into().unwrap());
+ let seg_ctrl = SegmentationControl::try_from((buf[3] >> 7) & 0b1).unwrap();
+ let expected_len_entity_ids = (((buf[3] >> 4) & 0b111) + 1) as usize;
+ if (expected_len_entity_ids != 1)
+ && (expected_len_entity_ids != 2)
+ && (expected_len_entity_ids != 4)
+ && (expected_len_entity_ids != 8)
+ {
+ return Err(PduError::InvalidEntityLen(expected_len_entity_ids as u8));
+ }
+ let seg_metadata_flag = SegmentMetadataFlag::try_from((buf[3] >> 3) & 0b1).unwrap();
+ let expected_len_seq_num = ((buf[3] & 0b111) + 1) as usize;
+ if (expected_len_seq_num != 1)
+ && (expected_len_seq_num != 2)
+ && (expected_len_seq_num != 4)
+ && (expected_len_seq_num != 8)
+ {
+ return Err(PduError::InvalidTransactionSeqNumLen(
+ expected_len_seq_num as u8,
+ ));
+ }
+ if buf.len() < (4 + 2 * expected_len_entity_ids + expected_len_seq_num) {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: 4 + 2 * expected_len_entity_ids + expected_len_seq_num,
+ })
+ .into());
+ }
+ let mut current_idx = 4;
+ // It is okay to unwrap here because we checked the validity of the expected length and of
+ // the remaining buffer length.
+ let source_id =
+ UnsignedByteField::new_from_be_bytes(expected_len_entity_ids, &buf[current_idx..])
+ .unwrap();
+ current_idx += expected_len_entity_ids;
+ let transaction_seq_num =
+ UnsignedByteField::new_from_be_bytes(expected_len_seq_num, &buf[current_idx..])
+ .unwrap();
+ current_idx += expected_len_seq_num;
+ let dest_id =
+ UnsignedByteField::new_from_be_bytes(expected_len_entity_ids, &buf[current_idx..])
+ .unwrap();
+ current_idx += expected_len_entity_ids;
+ let common_pdu_conf = CommonPduConfig::new(
+ source_id,
+ dest_id,
+ transaction_seq_num,
+ trans_mode,
+ file_flag,
+ crc_flag,
+ direction,
+ )
+ .unwrap();
+ Ok((
+ PduHeader {
+ pdu_type,
+ pdu_conf: common_pdu_conf,
+ seg_metadata_flag,
+ seg_ctrl,
+ pdu_datafield_len,
+ },
+ current_idx,
+ ))
+ }
+ pub fn pdu_type(&self) -> PduType {
+ self.pdu_type
+ }
+
+ pub fn common_pdu_conf(&self) -> &CommonPduConfig {
+ &self.pdu_conf
+ }
+
+ pub fn seg_metadata_flag(&self) -> SegmentMetadataFlag {
+ self.seg_metadata_flag
+ }
+ pub fn seg_ctrl(&self) -> SegmentationControl {
+ self.seg_ctrl
+ }
+}
+
+pub(crate) fn write_fss_field(
+ file_flag: LargeFileFlag,
+ file_size: u64,
+ buf: &mut [u8],
+) -> Result {
+ Ok(if file_flag == LargeFileFlag::Large {
+ buf[..core::mem::size_of::()].copy_from_slice(&file_size.to_be_bytes());
+ core::mem::size_of::()
+ } else {
+ if file_size > u32::MAX as u64 {
+ return Err(PduError::FileSizeTooLarge(file_size));
+ }
+ buf[..core::mem::size_of::()].copy_from_slice(&(file_size as u32).to_be_bytes());
+ core::mem::size_of::()
+ })
+}
+
+pub(crate) fn read_fss_field(file_flag: LargeFileFlag, buf: &[u8]) -> (usize, u64) {
+ if file_flag == LargeFileFlag::Large {
+ (
+ core::mem::size_of::(),
+ u64::from_be_bytes(buf[..core::mem::size_of::()].try_into().unwrap()),
+ )
+ } else {
+ (
+ core::mem::size_of::(),
+ u32::from_be_bytes(buf[..core::mem::size_of::()].try_into().unwrap()).into(),
+ )
+ }
+}
+
+// This is a generic length check applicable to most PDU deserializations. It first checks whether
+// a given buffer can hold an expected minimum size, and then it checks whether the PDU datafield
+// length is larger than that expected minimum size.
+pub(crate) fn generic_length_checks_pdu_deserialization(
+ buf: &[u8],
+ min_expected_len: usize,
+ full_len_without_crc: usize,
+) -> Result<(), ByteConversionError> {
+ // Buffer too short to hold additional expected minimum datasize.
+ if buf.len() < min_expected_len {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: min_expected_len,
+ }));
+ }
+ // This can happen if the PDU datafield length value is invalid.
+ if full_len_without_crc < min_expected_len {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: full_len_without_crc,
+ expected: min_expected_len,
+ }));
+ }
+ Ok(())
+}
+
+pub(crate) fn add_pdu_crc(buf: &mut [u8], mut current_idx: usize) -> usize {
+ let mut digest = CRC_CCITT_FALSE.digest();
+ digest.update(&buf[..current_idx]);
+ buf[current_idx..current_idx + 2].copy_from_slice(&digest.finalize().to_be_bytes());
+ current_idx += 2;
+ current_idx
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::cfdp::pdu::{CommonPduConfig, PduError, PduHeader, FIXED_HEADER_LEN};
+ use crate::cfdp::{
+ CrcFlag, Direction, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl,
+ TransmissionMode, CFDP_VERSION_2,
+ };
+ use crate::util::{
+ UbfU8, UnsignedByteField, UnsignedByteFieldU16, UnsignedByteFieldU8, UnsignedEnum,
+ };
+ use crate::ByteConversionError;
+ use std::format;
+
+ pub(crate) fn common_pdu_conf(crc_flag: CrcFlag, fss: LargeFileFlag) -> CommonPduConfig {
+ let src_id = UbfU8::new(5);
+ let dest_id = UbfU8::new(10);
+ let transaction_seq_num = UbfU8::new(20);
+ let mut pdu_conf = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_num)
+ .expect("Generating common PDU config");
+ pdu_conf.crc_flag = crc_flag;
+ pdu_conf.file_flag = fss;
+ pdu_conf
+ }
+
+ pub(crate) fn verify_raw_header(pdu_conf: &PduHeader, buf: &[u8]) {
+ assert_eq!((buf[0] >> 5) & 0b111, CFDP_VERSION_2);
+ // File directive
+ assert_eq!((buf[0] >> 4) & 1, pdu_conf.pdu_type as u8);
+ // Towards receiver
+ assert_eq!((buf[0] >> 3) & 1, pdu_conf.pdu_conf.direction as u8);
+ // Acknowledged
+ assert_eq!((buf[0] >> 2) & 1, pdu_conf.pdu_conf.trans_mode as u8);
+ // No CRC
+ assert_eq!((buf[0] >> 1) & 1, pdu_conf.pdu_conf.crc_flag as u8);
+ // Regular file size
+ assert_eq!(buf[0] & 1, pdu_conf.pdu_conf.file_flag as u8);
+ let pdu_datafield_len = u16::from_be_bytes(buf[1..3].try_into().unwrap());
+ assert_eq!(pdu_datafield_len, pdu_conf.pdu_datafield_len);
+ // No record boundary preservation
+ assert_eq!((buf[3] >> 7) & 1, pdu_conf.seg_ctrl as u8);
+ // Entity ID length raw value is actual number of octets - 1 => 0
+ let entity_id_len = pdu_conf.pdu_conf.source_entity_id.len();
+ assert_eq!((buf[3] >> 4) & 0b111, entity_id_len as u8 - 1);
+ // No segment metadata
+ assert_eq!((buf[3] >> 3) & 0b1, pdu_conf.seg_metadata_flag as u8);
+ // Transaction Sequence ID length raw value is actual number of octets - 1 => 0
+ let seq_num_len = pdu_conf.pdu_conf.transaction_seq_num.len();
+ assert_eq!(buf[3] & 0b111, seq_num_len as u8 - 1);
+ let mut current_idx = 4;
+ let mut byte_field_check = |field_len: usize, ubf: &UnsignedByteField| {
+ match field_len {
+ 1 => assert_eq!(buf[current_idx], ubf.value() as u8),
+ 2 => assert_eq!(
+ u16::from_be_bytes(
+ buf[current_idx..current_idx + field_len]
+ .try_into()
+ .unwrap()
+ ),
+ ubf.value() as u16
+ ),
+ 4 => assert_eq!(
+ u32::from_be_bytes(
+ buf[current_idx..current_idx + field_len]
+ .try_into()
+ .unwrap()
+ ),
+ ubf.value() as u32
+ ),
+ 8 => assert_eq!(
+ u64::from_be_bytes(
+ buf[current_idx..current_idx + field_len]
+ .try_into()
+ .unwrap()
+ ),
+ ubf.value() as u64
+ ),
+ _ => panic!("invalid entity ID length"),
+ }
+ current_idx += field_len
+ };
+ byte_field_check(entity_id_len, &pdu_conf.pdu_conf.source_entity_id);
+ byte_field_check(seq_num_len, &pdu_conf.pdu_conf.transaction_seq_num);
+ byte_field_check(entity_id_len, &pdu_conf.pdu_conf.dest_entity_id);
+ }
+
+ #[test]
+ fn test_basic_state() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ assert_eq!(pdu_header.pdu_type(), PduType::FileDirective);
+ let common_conf_ref = pdu_header.common_pdu_conf();
+ assert_eq!(*common_conf_ref, common_pdu_cfg);
+ // These should be 0 and ignored for non-filedata PDUs
+ assert_eq!(
+ pdu_header.seg_metadata_flag(),
+ SegmentMetadataFlag::NotPresent
+ );
+ assert_eq!(
+ pdu_header.seg_ctrl(),
+ SegmentationControl::NoRecordBoundaryPreservation
+ );
+ assert_eq!(pdu_header.pdu_datafield_len, 5);
+ assert_eq!(pdu_header.header_len(), 7);
+ }
+
+ #[test]
+ fn test_serialization_1() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ let mut buf: [u8; 7] = [0; 7];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ // 4 byte fixed header plus three bytes src, dest ID and transaction ID
+ assert_eq!(res.unwrap(), 7);
+ verify_raw_header(&pdu_header, &buf);
+ }
+
+ #[test]
+ fn test_deserialization_1() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ let mut buf: [u8; 7] = [0; 7];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let deser_res = PduHeader::from_bytes(&buf);
+ assert!(deser_res.is_ok());
+ let (header_read_back, read_size) = deser_res.unwrap();
+ assert_eq!(read_size, 7);
+ assert_eq!(header_read_back, pdu_header);
+ }
+
+ #[test]
+ fn test_serialization_2() {
+ let src_id = UnsignedByteFieldU16::new(0x0001);
+ let dest_id = UnsignedByteFieldU16::new(0x0203);
+ let transaction_id = UnsignedByteFieldU16::new(0x0405);
+ let mut common_pdu_cfg =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ common_pdu_cfg.crc_flag = CrcFlag::WithCrc;
+ common_pdu_cfg.direction = Direction::TowardsSender;
+ common_pdu_cfg.trans_mode = TransmissionMode::Unacknowledged;
+ common_pdu_cfg.file_flag = LargeFileFlag::Large;
+ let pdu_header = PduHeader::new_for_file_data(
+ common_pdu_cfg,
+ 5,
+ SegmentMetadataFlag::Present,
+ SegmentationControl::WithRecordBoundaryPreservation,
+ );
+ assert_eq!(pdu_header.header_len(), 10);
+ let mut buf: [u8; 16] = [0; 16];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok(), "{}", format!("Result {res:?} not okay"));
+ // 4 byte fixed header, 6 bytes additional fields
+ assert_eq!(res.unwrap(), 10);
+ verify_raw_header(&pdu_header, &buf);
+ }
+
+ #[test]
+ fn test_deserialization_2() {
+ let src_id = UnsignedByteFieldU16::new(0x0001);
+ let dest_id = UnsignedByteFieldU16::new(0x0203);
+ let transaction_id = UnsignedByteFieldU16::new(0x0405);
+ let mut common_pdu_cfg =
+ CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ common_pdu_cfg.crc_flag = CrcFlag::WithCrc;
+ common_pdu_cfg.direction = Direction::TowardsSender;
+ common_pdu_cfg.trans_mode = TransmissionMode::Unacknowledged;
+ common_pdu_cfg.file_flag = LargeFileFlag::Large;
+ let pdu_header = PduHeader::new_for_file_data(
+ common_pdu_cfg,
+ 5,
+ SegmentMetadataFlag::Present,
+ SegmentationControl::WithRecordBoundaryPreservation,
+ );
+ let mut buf: [u8; 16] = [0; 16];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let deser_res = PduHeader::from_bytes(&buf);
+ assert!(deser_res.is_ok());
+ let (header_read_back, read_size) = deser_res.unwrap();
+ assert_eq!(read_size, 10);
+ assert_eq!(header_read_back, pdu_header);
+ }
+
+ #[test]
+ fn test_invalid_raw_version() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ let mut buf: [u8; 7] = [0; 7];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ buf[0] &= !0b1110_0000;
+ buf[0] |= (CFDP_VERSION_2 + 1) << 5;
+ let res = PduHeader::from_bytes(&buf);
+ assert!(res.is_err());
+ let error = res.unwrap_err();
+ if let PduError::CfdpVersionMissmatch(raw_version) = error {
+ assert_eq!(raw_version, CFDP_VERSION_2 + 1);
+ } else {
+ panic!("invalid exception: {}", error);
+ }
+ }
+
+ #[test]
+ fn test_buf_too_small_1() {
+ let buf: [u8; 3] = [0; 3];
+ let res = PduHeader::from_bytes(&buf);
+ assert!(res.is_err());
+ let error = res.unwrap_err();
+ if let PduError::ByteConversionError(ByteConversionError::FromSliceTooSmall(missmatch)) =
+ error
+ {
+ assert_eq!(missmatch.found, 3);
+ assert_eq!(missmatch.expected, FIXED_HEADER_LEN);
+ } else {
+ panic!("invalid exception: {}", error);
+ }
+ }
+
+ #[test]
+ fn test_buf_too_small_2() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ let mut buf: [u8; 7] = [0; 7];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let header = PduHeader::from_bytes(&buf[0..6]);
+ assert!(header.is_err());
+ let error = header.unwrap_err();
+ if let PduError::ByteConversionError(ByteConversionError::FromSliceTooSmall(missmatch)) =
+ error
+ {
+ assert_eq!(missmatch.found, 6);
+ assert_eq!(missmatch.expected, 7);
+ }
+ }
+
+ #[test]
+ fn test_invalid_seq_len() {
+ let src_id = UbfU8::new(1);
+ let dest_id = UbfU8::new(2);
+ let transaction_seq_id = UbfU8::new(3);
+ let invalid_byte_field = UnsignedByteField::new(3, 5);
+ let pdu_conf_res = CommonPduConfig::new_with_defaults(src_id, dest_id, invalid_byte_field);
+ assert!(pdu_conf_res.is_err());
+ let error = pdu_conf_res.unwrap_err();
+ if let PduError::InvalidTransactionSeqNumLen(len) = error {
+ assert_eq!(len, 3);
+ } else {
+ panic!("Invalid exception: {}", error)
+ }
+ let pdu_conf_res = CommonPduConfig::new_with_defaults(
+ invalid_byte_field,
+ invalid_byte_field,
+ transaction_seq_id,
+ );
+ assert!(pdu_conf_res.is_err());
+ let error = pdu_conf_res.unwrap_err();
+ if let PduError::InvalidEntityLen(len) = error {
+ assert_eq!(len, 3);
+ } else {
+ panic!("Invalid exception: {}", error)
+ }
+ }
+ #[test]
+ fn test_missmatch_src_dest_id() {
+ let src_id = UnsignedByteField::new(1, 5);
+ let dest_id = UnsignedByteField::new(2, 5);
+ let transaction_seq_id = UbfU8::new(3);
+ let pdu_conf_res = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_seq_id);
+ assert!(pdu_conf_res.is_err());
+ let error = pdu_conf_res.unwrap_err();
+ if let PduError::SourceDestIdLenMissmatch((src_len, dest_len)) = error {
+ assert_eq!(src_len, 1);
+ assert_eq!(dest_len, 2);
+ }
+ }
+
+ #[test]
+ fn test_invalid_raw_src_id_len() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ let mut buf: [u8; 7] = [0; 7];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ buf[3] &= !0b0111_0000;
+ // Equivalent to the length of three
+ buf[3] |= 0b10 << 4;
+ let header_res = PduHeader::from_bytes(&buf);
+ assert!(header_res.is_err());
+ let error = header_res.unwrap_err();
+ if let PduError::InvalidEntityLen(len) = error {
+ assert_eq!(len, 3);
+ } else {
+ panic!("invalid exception {:?}", error)
+ }
+ }
+
+ #[test]
+ fn test_invalid_transaction_seq_id_len() {
+ let src_id = UnsignedByteFieldU8::new(1);
+ let dest_id = UnsignedByteFieldU8::new(2);
+ let transaction_id = UnsignedByteFieldU8::new(3);
+ let common_pdu_cfg = CommonPduConfig::new_with_defaults(src_id, dest_id, transaction_id)
+ .expect("common config creation failed");
+ let pdu_header = PduHeader::new_no_file_data(common_pdu_cfg, 5);
+ let mut buf: [u8; 7] = [0; 7];
+ let res = pdu_header.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ buf[3] &= !0b0000_0111;
+ // Equivalent to the length of three
+ buf[3] |= 0b10;
+ let header_res = PduHeader::from_bytes(&buf);
+ assert!(header_res.is_err());
+ let error = header_res.unwrap_err();
+ if let PduError::InvalidTransactionSeqNumLen(len) = error {
+ assert_eq!(len, 3);
+ } else {
+ panic!("invalid exception {:?}", error)
+ }
+ }
+}
diff --git a/src/cfdp/tlv.rs b/src/cfdp/tlv.rs
new file mode 100644
index 0000000..2bb3e9d
--- /dev/null
+++ b/src/cfdp/tlv.rs
@@ -0,0 +1,765 @@
+//! Generic CFDP type-length-value (TLV) abstraction as specified in CFDP 5.1.9.
+use crate::cfdp::lv::{
+ generic_len_check_data_serialization, generic_len_check_deserialization, Lv, MIN_LV_LEN,
+};
+use crate::cfdp::TlvLvError;
+use crate::util::{UnsignedByteField, UnsignedByteFieldError, UnsignedEnum};
+use crate::{ByteConversionError, SizeMissmatch};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+pub const MIN_TLV_LEN: usize = 2;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum TlvType {
+ FilestoreRequest = 0x00,
+ FilestoreResponse = 0x01,
+ MsgToUser = 0x02,
+ FaultHandler = 0x04,
+ FlowLabel = 0x05,
+ EntityId = 0x06,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum TlvTypeField {
+ Standard(TlvType),
+ Custom(u8),
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[repr(u8)]
+pub enum FilestoreActionCode {
+ CreateFile = 0b0000,
+ DeleteFile = 0b0001,
+ RenameFile = 0b0010,
+ /// This operation appends one file to another. The first specified name will form the first
+ /// part of the new file and the name of the new file. This function can be used to get
+ /// similar functionality to the UNIX cat utility (albeit for only two files).
+ AppendFile = 0b0011,
+ /// This operation replaces the content of the first specified file with the content of
+ /// the secondly specified file.
+ ReplaceFile = 0b0100,
+ CreateDirectory = 0b0101,
+ RemoveDirectory = 0b0110,
+ DenyFile = 0b0111,
+ DenyDirectory = 0b1000,
+}
+
+impl From for TlvTypeField {
+ fn from(value: u8) -> Self {
+ match TlvType::try_from(value) {
+ Ok(tlv_type) => TlvTypeField::Standard(tlv_type),
+ Err(_) => TlvTypeField::Custom(value),
+ }
+ }
+}
+
+impl From for u8 {
+ fn from(value: TlvTypeField) -> Self {
+ match value {
+ TlvTypeField::Standard(std) => std as u8,
+ TlvTypeField::Custom(custom) => custom,
+ }
+ }
+}
+
+/// Generic CFDP type-length-value (TLV) abstraction as specified in CFDP 5.1.9.
+///
+/// # Lifetimes
+/// * `data`: If the TLV is generated from a raw bytestream, this will be the lifetime of
+/// the raw bytestream. If the TLV is generated from a raw slice or a similar data reference,
+/// this will be the lifetime of that data reference.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Tlv<'data> {
+ tlv_type_field: TlvTypeField,
+ #[cfg_attr(feature = "serde", serde(borrow))]
+ lv: Lv<'data>,
+}
+
+impl<'data> Tlv<'data> {
+ pub fn new(tlv_type: TlvType, data: &[u8]) -> Result {
+ Ok(Tlv {
+ tlv_type_field: TlvTypeField::Standard(tlv_type),
+ lv: Lv::new(data)?,
+ })
+ }
+
+ /// Creates a TLV with an empty value field.
+ pub fn new_empty(tlv_type: TlvType) -> Tlv<'data> {
+ Tlv {
+ tlv_type_field: TlvTypeField::Standard(tlv_type),
+ lv: Lv::new_empty(),
+ }
+ }
+
+ pub fn tlv_type_field(&self) -> TlvTypeField {
+ self.tlv_type_field
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ generic_len_check_data_serialization(buf, self.len_value(), MIN_TLV_LEN)?;
+ buf[0] = self.tlv_type_field.into();
+ self.lv.write_to_be_bytes_no_len_check(&mut buf[1..]);
+ Ok(self.len_full())
+ }
+
+ pub fn value(&self) -> Option<&[u8]> {
+ self.lv.value()
+ }
+
+ /// Returns the length of the value part, not including the length byte.
+ pub fn len_value(&self) -> usize {
+ self.lv.len_value()
+ }
+
+ /// Returns the full raw length, including the length byte.
+ pub fn len_full(&self) -> usize {
+ self.lv.len_full() + 1
+ }
+
+ /// Checks whether the value field is empty.
+ pub fn is_empty(&self) -> bool {
+ self.lv.is_empty()
+ }
+
+ /// Creates a TLV give a raw bytestream. Please note that is is not necessary to pass the
+ /// bytestream with the exact size of the expected TLV. This function will take care
+ /// of parsing the length byte, and the length of the parsed TLV can be retrieved using
+ /// [Self::len_full].
+ pub fn from_bytes(buf: &'data [u8]) -> Result, TlvLvError> {
+ generic_len_check_deserialization(buf, MIN_TLV_LEN)?;
+ Ok(Self {
+ tlv_type_field: TlvTypeField::from(buf[0]),
+ lv: Lv::from_bytes(&buf[MIN_LV_LEN..])?,
+ })
+ }
+}
+
+pub(crate) fn verify_tlv_type(raw_type: u8, expected_tlv_type: TlvType) -> Result<(), TlvLvError> {
+ let tlv_type = TlvType::try_from(raw_type)
+ .map_err(|_| TlvLvError::InvalidTlvTypeField((raw_type, Some(expected_tlv_type as u8))))?;
+ if tlv_type != expected_tlv_type {
+ return Err(TlvLvError::InvalidTlvTypeField((
+ tlv_type as u8,
+ Some(expected_tlv_type as u8),
+ )));
+ }
+ Ok(())
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct EntityIdTlv {
+ entity_id: UnsignedByteField,
+}
+
+impl EntityIdTlv {
+ pub fn new(entity_id: UnsignedByteField) -> Self {
+ Self { entity_id }
+ }
+
+ fn len_check(buf: &[u8]) -> Result<(), ByteConversionError> {
+ if buf.len() < 2 {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: 2,
+ }));
+ }
+ Ok(())
+ }
+
+ pub fn len_value(&self) -> usize {
+ self.entity_id.len()
+ }
+
+ pub fn len_full(&self) -> usize {
+ 2 + self.entity_id.len()
+ }
+
+ pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result {
+ Self::len_check(buf)?;
+ buf[0] = TlvType::EntityId as u8;
+ buf[1] = self.entity_id.len() as u8;
+ self.entity_id.write_to_be_bytes(&mut buf[2..])
+ }
+
+ pub fn from_bytes(buf: &[u8]) -> Result {
+ Self::len_check(buf)?;
+ verify_tlv_type(buf[0], TlvType::EntityId)?;
+ let len = buf[1];
+ if len != 1 && len != 2 && len != 4 && len != 8 {
+ return Err(TlvLvError::InvalidValueLength(len as usize));
+ }
+ // Okay to unwrap here. The checks before make sure that the deserialization never fails
+ let entity_id = UnsignedByteField::new_from_be_bytes(len as usize, &buf[2..]).unwrap();
+ Ok(Self { entity_id })
+ }
+
+ /// Convert to a generic [Tlv], which also erases the programmatic type information.
+ pub fn to_tlv(self, buf: &mut [u8]) -> Result {
+ Self::len_check(buf)?;
+ self.entity_id
+ .write_to_be_bytes(&mut buf[2..2 + self.entity_id.len()])?;
+ Tlv::new(TlvType::EntityId, &buf[2..2 + self.entity_id.len()]).map_err(|e| match e {
+ TlvLvError::ByteConversionError(e) => e,
+ // All other errors are impossible.
+ _ => panic!("unexpected TLV error"),
+ })
+ }
+}
+
+impl<'data> TryFrom> for EntityIdTlv {
+ type Error = TlvLvError;
+
+ fn try_from(value: Tlv) -> Result {
+ match value.tlv_type_field {
+ TlvTypeField::Standard(tlv_type) => {
+ if tlv_type != TlvType::EntityId {
+ return Err(TlvLvError::InvalidTlvTypeField((
+ tlv_type as u8,
+ Some(TlvType::EntityId as u8),
+ )));
+ }
+ }
+ TlvTypeField::Custom(val) => {
+ return Err(TlvLvError::InvalidTlvTypeField((
+ val,
+ Some(TlvType::EntityId as u8),
+ )));
+ }
+ }
+ if value.len_value() != 1
+ && value.len_value() != 2
+ && value.len_value() != 4
+ && value.len_value() != 8
+ {
+ return Err(TlvLvError::InvalidValueLength(value.len_value()));
+ }
+ Ok(Self::new(
+ UnsignedByteField::new_from_be_bytes(value.len_value(), value.value().unwrap())
+ .map_err(|e| match e {
+ UnsignedByteFieldError::ByteConversionError(e) => e,
+ // This can not happen, we checked for the length validity, and the data is always smaller than
+ // 255 bytes.
+ _ => panic!("unexpected error"),
+ })?,
+ ))
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct FilestoreRequestTlv<'first_name, 'second_name> {
+ action_code: FilestoreActionCode,
+ #[cfg_attr(feature = "serde", serde(borrow))]
+ first_name: Lv<'first_name>,
+ #[cfg_attr(feature = "serde", serde(borrow))]
+ second_name: Option>,
+}
+
+impl<'first_name, 'second_name> FilestoreRequestTlv<'first_name, 'second_name> {
+ pub fn new_create_file(first_name: Lv<'first_name>) -> Result {
+ Self::new(FilestoreActionCode::CreateFile, first_name, None)
+ }
+
+ pub fn new_delete_file(first_name: Lv<'first_name>) -> Result {
+ Self::new(FilestoreActionCode::DeleteFile, first_name, None)
+ }
+
+ pub fn new_rename_file(
+ source_name: Lv<'first_name>,
+ target_name: Lv<'second_name>,
+ ) -> Result {
+ Self::new(
+ FilestoreActionCode::RenameFile,
+ source_name,
+ Some(target_name),
+ )
+ }
+
+ /// This operation appends one file to another. The first specified name will form the first
+ /// part of the new file and the name of the new file. This function can be used to get
+ /// similar functionality to the UNIX cat utility (albeit for only two files).
+ pub fn new_append_file(
+ first_file: Lv<'first_name>,
+ second_file: Lv<'second_name>,
+ ) -> Result {
+ Self::new(
+ FilestoreActionCode::AppendFile,
+ first_file,
+ Some(second_file),
+ )
+ }
+
+ /// This operation replaces the content of the first specified file with the content of
+ /// the secondly specified file. This function can be used to get similar functionality to
+ /// the UNIX copy (cp) utility if the target file already exists.
+ pub fn new_replace_file(
+ replaced_file: Lv<'first_name>,
+ new_file: Lv<'second_name>,
+ ) -> Result {
+ Self::new(
+ FilestoreActionCode::ReplaceFile,
+ replaced_file,
+ Some(new_file),
+ )
+ }
+
+ pub fn new_create_directory(dir_name: Lv<'first_name>) -> Result {
+ Self::new(FilestoreActionCode::CreateDirectory, dir_name, None)
+ }
+
+ pub fn new_remove_directory(dir_name: Lv<'first_name>) -> Result {
+ Self::new(FilestoreActionCode::RemoveDirectory, dir_name, None)
+ }
+
+ pub fn new_deny_file(file_name: Lv<'first_name>) -> Result {
+ Self::new(FilestoreActionCode::DenyFile, file_name, None)
+ }
+
+ pub fn new_deny_directory(dir_name: Lv<'first_name>) -> Result {
+ Self::new(FilestoreActionCode::DenyDirectory, dir_name, None)
+ }
+
+ /// This function will return [None] if the respective action code requires two names but
+ /// only one is passed. It will also returns [None] if the cumulative length of the first
+ /// name and the second name exceeds 255 bytes.
+ ///
+ /// This is the case for the rename, append and replace filestore request.
+ pub fn new(
+ action_code: FilestoreActionCode,
+ first_name: Lv<'first_name>,
+ second_name: Option>,
+ ) -> Result {
+ let mut base_value_len = first_name.len_full();
+ if Self::has_second_filename(action_code) {
+ if second_name.is_none() {
+ return Err(TlvLvError::SecondNameMissing);
+ }
+ base_value_len += second_name.as_ref().unwrap().len_full();
+ }
+ if base_value_len > u8::MAX as usize {
+ return Err(TlvLvError::InvalidValueLength(base_value_len));
+ }
+ Ok(Self {
+ action_code,
+ first_name,
+ second_name,
+ })
+ }
+
+ pub fn has_second_filename(action_code: FilestoreActionCode) -> bool {
+ if action_code == FilestoreActionCode::RenameFile
+ || action_code == FilestoreActionCode::AppendFile
+ || action_code == FilestoreActionCode::ReplaceFile
+ {
+ return true;
+ }
+ false
+ }
+
+ pub fn action_code(&self) -> FilestoreActionCode {
+ self.action_code
+ }
+
+ pub fn first_name(&self) -> Lv<'first_name> {
+ self.first_name
+ }
+
+ pub fn second_name(&self) -> Option> {
+ self.second_name
+ }
+
+ pub fn len_value(&self) -> usize {
+ let mut len = 1 + self.first_name.len_full();
+ if let Some(second_name) = self.second_name {
+ len += second_name.len_full();
+ }
+ len
+ }
+
+ pub fn len_full(&self) -> usize {
+ 2 + self.len_value()
+ }
+
+ pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result {
+ if buf.len() < self.len_full() {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: self.len_full(),
+ }));
+ }
+ buf[0] = TlvType::FilestoreRequest as u8;
+ buf[1] = self.len_value() as u8;
+ buf[2] = (self.action_code as u8) << 4;
+ let mut current_idx = 3;
+ // Length checks were already performed.
+ self.first_name.write_to_be_bytes_no_len_check(
+ &mut buf[current_idx..current_idx + self.first_name.len_full()],
+ );
+ current_idx += self.first_name.len_full();
+ if let Some(second_name) = self.second_name {
+ second_name.write_to_be_bytes_no_len_check(
+ &mut buf[current_idx..current_idx + second_name.len_full()],
+ );
+ current_idx += second_name.len_full();
+ }
+ Ok(current_idx)
+ }
+
+ pub fn from_bytes<'longest: 'first_name + 'second_name>(
+ buf: &'longest [u8],
+ ) -> Result {
+ if buf.len() < 2 {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: 2,
+ })
+ .into());
+ }
+ verify_tlv_type(buf[0], TlvType::FilestoreRequest)?;
+ let len = buf[1] as usize;
+ let mut current_idx = 2;
+ let action_code = FilestoreActionCode::try_from((buf[2] >> 4) & 0b1111)
+ .map_err(|_| TlvLvError::InvalidFilestoreActionCode((buf[2] >> 4) & 0b1111))?;
+ current_idx += 1;
+ let first_name = Lv::from_bytes(&buf[current_idx..])?;
+ let mut second_name = None;
+
+ current_idx += first_name.len_full();
+ if Self::has_second_filename(action_code) {
+ if current_idx >= 2 + len {
+ return Err(TlvLvError::SecondNameMissing);
+ }
+ second_name = Some(Lv::from_bytes(&buf[current_idx..])?);
+ }
+ Ok(Self {
+ action_code,
+ first_name,
+ second_name,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::cfdp::lv::Lv;
+ use crate::cfdp::tlv::{FilestoreActionCode, FilestoreRequestTlv, Tlv, TlvType, TlvTypeField};
+ use crate::cfdp::TlvLvError;
+ use crate::util::{UbfU8, UnsignedEnum};
+
+ const TLV_TEST_STR_0: &'static str = "hello.txt";
+ const TLV_TEST_STR_1: &'static str = "hello2.txt";
+
+ #[test]
+ fn test_basic() {
+ let entity_id = UbfU8::new(5);
+ let mut buf: [u8; 4] = [0; 4];
+ assert!(entity_id.write_to_be_bytes(&mut buf).is_ok());
+ let tlv_res = Tlv::new(TlvType::EntityId, &buf[0..1]);
+ assert!(tlv_res.is_ok());
+ let tlv_res = tlv_res.unwrap();
+ assert_eq!(
+ tlv_res.tlv_type_field(),
+ TlvTypeField::Standard(TlvType::EntityId)
+ );
+ assert_eq!(tlv_res.len_full(), 3);
+ assert_eq!(tlv_res.len_value(), 1);
+ assert!(!tlv_res.is_empty());
+ assert!(tlv_res.value().is_some());
+ assert_eq!(tlv_res.value().unwrap()[0], 5);
+ }
+
+ #[test]
+ fn test_serialization() {
+ let entity_id = UbfU8::new(5);
+ let mut buf: [u8; 4] = [0; 4];
+ assert!(entity_id.write_to_be_bytes(&mut buf).is_ok());
+ let tlv_res = Tlv::new(TlvType::EntityId, &buf[0..1]);
+ assert!(tlv_res.is_ok());
+ let tlv_res = tlv_res.unwrap();
+ let mut ser_buf: [u8; 4] = [0; 4];
+ assert!(tlv_res.write_to_bytes(&mut ser_buf).is_ok());
+ assert_eq!(ser_buf[0], TlvType::EntityId as u8);
+ assert_eq!(ser_buf[1], 1);
+ assert_eq!(ser_buf[2], 5);
+ }
+
+ #[test]
+ fn test_deserialization() {
+ let entity_id = UbfU8::new(5);
+ let mut buf: [u8; 4] = [0; 4];
+ assert!(entity_id.write_to_be_bytes(&mut buf[2..]).is_ok());
+ buf[0] = TlvType::EntityId as u8;
+ buf[1] = 1;
+ let tlv_from_raw = Tlv::from_bytes(&mut buf);
+ assert!(tlv_from_raw.is_ok());
+ let tlv_from_raw = tlv_from_raw.unwrap();
+ assert_eq!(
+ tlv_from_raw.tlv_type_field(),
+ TlvTypeField::Standard(TlvType::EntityId)
+ );
+ assert_eq!(tlv_from_raw.len_value(), 1);
+ assert_eq!(tlv_from_raw.len_full(), 3);
+ assert!(tlv_from_raw.value().is_some());
+ assert_eq!(tlv_from_raw.value().unwrap()[0], 5);
+ }
+
+ #[test]
+ fn test_empty() {
+ let tlv_empty = Tlv::new_empty(TlvType::MsgToUser);
+ assert!(tlv_empty.value().is_none());
+ assert!(tlv_empty.is_empty());
+ assert_eq!(tlv_empty.len_full(), 2);
+ assert_eq!(tlv_empty.len_value(), 0);
+ assert_eq!(
+ tlv_empty.tlv_type_field(),
+ TlvTypeField::Standard(TlvType::MsgToUser)
+ );
+ }
+
+ #[test]
+ fn test_empty_serialization() {
+ let tlv_empty = Tlv::new_empty(TlvType::MsgToUser);
+ let mut buf: [u8; 4] = [0; 4];
+ assert!(tlv_empty.write_to_bytes(&mut buf).is_ok());
+ assert_eq!(buf[0], TlvType::MsgToUser as u8);
+ assert_eq!(buf[1], 0);
+ }
+
+ #[test]
+ fn test_empty_deserialization() {
+ let mut buf: [u8; 4] = [0; 4];
+ buf[0] = TlvType::MsgToUser as u8;
+ buf[1] = 0;
+ let tlv_empty = Tlv::from_bytes(&mut buf);
+ assert!(tlv_empty.is_ok());
+ let tlv_empty = tlv_empty.unwrap();
+ assert!(tlv_empty.is_empty());
+ assert!(tlv_empty.value().is_none());
+ assert_eq!(
+ tlv_empty.tlv_type_field(),
+ TlvTypeField::Standard(TlvType::MsgToUser)
+ );
+ assert_eq!(tlv_empty.len_full(), 2);
+ assert_eq!(tlv_empty.len_value(), 0);
+ }
+
+ #[test]
+ fn test_buf_too_large() {
+ let buf_too_large: [u8; u8::MAX as usize + 1] = [0; u8::MAX as usize + 1];
+ let tlv_res = Tlv::new(TlvType::MsgToUser, &buf_too_large);
+ assert!(tlv_res.is_err());
+ let error = tlv_res.unwrap_err();
+ if let TlvLvError::DataTooLarge(size) = error {
+ assert_eq!(size, u8::MAX as usize + 1);
+ } else {
+ panic!("unexpected error {:?}", error);
+ }
+ }
+
+ #[test]
+ fn test_deserialization_custom_tlv_type() {
+ let mut buf: [u8; 4] = [0; 4];
+ buf[0] = 3;
+ buf[1] = 1;
+ buf[2] = 5;
+ let tlv = Tlv::from_bytes(&mut buf);
+ assert!(tlv.is_ok());
+ let tlv = tlv.unwrap();
+ assert_eq!(tlv.tlv_type_field(), TlvTypeField::Custom(3));
+ assert_eq!(tlv.len_value(), 1);
+ assert_eq!(tlv.len_full(), 3);
+ }
+
+ fn generic_fs_request_test_one_file(
+ action_code: FilestoreActionCode,
+ ) -> FilestoreRequestTlv<'static, 'static> {
+ assert!(!FilestoreRequestTlv::has_second_filename(action_code));
+ let first_name = Lv::new_from_str(TLV_TEST_STR_0).unwrap();
+ let fs_request = match action_code {
+ FilestoreActionCode::CreateFile => FilestoreRequestTlv::new_create_file(first_name),
+ FilestoreActionCode::DeleteFile => FilestoreRequestTlv::new_delete_file(first_name),
+ FilestoreActionCode::CreateDirectory => {
+ FilestoreRequestTlv::new_create_directory(first_name)
+ }
+ FilestoreActionCode::RemoveDirectory => {
+ FilestoreRequestTlv::new_remove_directory(first_name)
+ }
+ FilestoreActionCode::DenyFile => FilestoreRequestTlv::new_deny_file(first_name),
+ FilestoreActionCode::DenyDirectory => {
+ FilestoreRequestTlv::new_deny_directory(first_name)
+ }
+ _ => panic!("invalid action code"),
+ };
+ assert!(fs_request.is_ok());
+ let fs_request = fs_request.unwrap();
+ assert_eq!(fs_request.len_value(), 1 + first_name.len_full());
+ assert_eq!(fs_request.len_full(), fs_request.len_value() + 2);
+ assert_eq!(fs_request.action_code(), action_code);
+ assert_eq!(fs_request.first_name(), first_name);
+ assert_eq!(fs_request.second_name(), None);
+ fs_request
+ }
+
+ fn generic_fs_request_test_two_files(
+ action_code: FilestoreActionCode,
+ ) -> FilestoreRequestTlv<'static, 'static> {
+ assert!(FilestoreRequestTlv::has_second_filename(action_code));
+ let first_name = Lv::new_from_str(TLV_TEST_STR_0).unwrap();
+ let second_name = Lv::new_from_str(TLV_TEST_STR_1).unwrap();
+ let fs_request = match action_code {
+ FilestoreActionCode::ReplaceFile => {
+ FilestoreRequestTlv::new_replace_file(first_name, second_name)
+ }
+ FilestoreActionCode::AppendFile => {
+ FilestoreRequestTlv::new_append_file(first_name, second_name)
+ }
+ FilestoreActionCode::RenameFile => {
+ FilestoreRequestTlv::new_rename_file(first_name, second_name)
+ }
+ _ => panic!("invalid action code"),
+ };
+ assert!(fs_request.is_ok());
+ let fs_request = fs_request.unwrap();
+ assert_eq!(
+ fs_request.len_value(),
+ 1 + first_name.len_full() + second_name.len_full()
+ );
+ assert_eq!(fs_request.len_full(), fs_request.len_value() + 2);
+ assert_eq!(fs_request.action_code(), action_code);
+ assert_eq!(fs_request.first_name(), first_name);
+ assert!(fs_request.second_name().is_some());
+ assert_eq!(fs_request.second_name().unwrap(), second_name);
+ fs_request
+ }
+
+ #[test]
+ fn test_fs_request_basic_create_file() {
+ generic_fs_request_test_one_file(FilestoreActionCode::CreateFile);
+ }
+
+ #[test]
+ fn test_fs_request_basic_delete() {
+ generic_fs_request_test_one_file(FilestoreActionCode::DeleteFile);
+ }
+
+ #[test]
+ fn test_fs_request_basic_create_dir() {
+ generic_fs_request_test_one_file(FilestoreActionCode::CreateDirectory);
+ }
+
+ #[test]
+ fn test_fs_request_basic_remove_dir() {
+ generic_fs_request_test_one_file(FilestoreActionCode::RemoveDirectory);
+ }
+
+ #[test]
+ fn test_fs_request_basic_deny_file() {
+ generic_fs_request_test_one_file(FilestoreActionCode::DenyFile);
+ }
+
+ #[test]
+ fn test_fs_request_basic_deny_dir() {
+ generic_fs_request_test_one_file(FilestoreActionCode::DenyDirectory);
+ }
+
+ #[test]
+ fn test_fs_request_basic_append_file() {
+ generic_fs_request_test_two_files(FilestoreActionCode::AppendFile);
+ }
+
+ #[test]
+ fn test_fs_request_basic_rename_file() {
+ generic_fs_request_test_two_files(FilestoreActionCode::RenameFile);
+ }
+
+ #[test]
+ fn test_fs_request_basic_replace_file() {
+ generic_fs_request_test_two_files(FilestoreActionCode::ReplaceFile);
+ }
+
+ fn check_fs_request_first_part(
+ buf: &[u8],
+ action_code: FilestoreActionCode,
+ expected_val_len: u8,
+ ) -> usize {
+ assert_eq!(buf[0], TlvType::FilestoreRequest as u8);
+ assert_eq!(buf[1], expected_val_len);
+ assert_eq!((buf[2] >> 4) & 0b1111, action_code as u8);
+ let lv = Lv::from_bytes(&buf[3..]);
+ assert!(lv.is_ok());
+ let lv = lv.unwrap();
+ assert_eq!(lv.value_as_str().unwrap().unwrap(), TLV_TEST_STR_0);
+ 3 + lv.len_full()
+ }
+
+ #[test]
+ fn test_fs_request_serialization_one_file() {
+ let req = generic_fs_request_test_one_file(FilestoreActionCode::CreateFile);
+ let mut buf: [u8; 64] = [0; 64];
+ let res = req.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(written, 3 + 1 + TLV_TEST_STR_0.len());
+ assert_eq!(written, req.len_full());
+ check_fs_request_first_part(
+ &buf,
+ FilestoreActionCode::CreateFile,
+ 1 + 1 + TLV_TEST_STR_0.len() as u8,
+ );
+ }
+
+ #[test]
+ fn test_fs_request_deserialization_one_file() {
+ let req = generic_fs_request_test_one_file(FilestoreActionCode::CreateFile);
+ let mut buf: [u8; 64] = [0; 64];
+ let res = req.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let req_conv_back = FilestoreRequestTlv::from_bytes(&buf);
+ assert!(req_conv_back.is_ok());
+ let req_conv_back = req_conv_back.unwrap();
+ assert_eq!(req_conv_back, req);
+ }
+
+ #[test]
+ fn test_fs_request_serialization_two_files() {
+ let req = generic_fs_request_test_two_files(FilestoreActionCode::RenameFile);
+ let mut buf: [u8; 64] = [0; 64];
+ let res = req.write_to_bytes(&mut buf);
+ assert!(res.is_ok());
+ let written = res.unwrap();
+ assert_eq!(written, req.len_full());
+ assert_eq!(
+ written,
+ 3 + 1 + TLV_TEST_STR_0.len() + 1 + TLV_TEST_STR_1.len()
+ );
+ let current_idx = check_fs_request_first_part(
+ &buf,
+ FilestoreActionCode::RenameFile,
+ 1 + 1 + TLV_TEST_STR_0.len() as u8 + 1 + TLV_TEST_STR_1.len() as u8,
+ );
+ let second_lv = Lv::from_bytes(&buf[current_idx..]);
+ assert!(second_lv.is_ok());
+ let second_lv = second_lv.unwrap();
+ assert_eq!(second_lv.value_as_str().unwrap().unwrap(), TLV_TEST_STR_1);
+ assert_eq!(current_idx + second_lv.len_full(), req.len_full());
+ }
+
+ #[test]
+ fn test_fs_request_deserialization_two_files() {
+ let req = generic_fs_request_test_two_files(FilestoreActionCode::RenameFile);
+ let mut buf: [u8; 64] = [0; 64];
+ req.write_to_bytes(&mut buf).unwrap();
+ let req_conv_back = FilestoreRequestTlv::from_bytes(&buf);
+ assert!(req_conv_back.is_ok());
+ let req_conv_back = req_conv_back.unwrap();
+ assert_eq!(req_conv_back, req);
+ }
+}
diff --git a/src/ecss/mod.rs b/src/ecss/mod.rs
index 8f5cd74..920632f 100644
--- a/src/ecss/mod.rs
+++ b/src/ecss/mod.rs
@@ -3,10 +3,9 @@
//!
//! You can find the PUS telecommand definitions in the [crate::tc] module and ithe PUS telemetry definitions
//! inside the [crate::tm] module.
-use crate::{ByteConversionError, CcsdsPacket, SizeMissmatch};
+use crate::{ByteConversionError, CcsdsPacket, CRC_CCITT_FALSE};
use core::fmt::{Debug, Display, Formatter};
use core::mem::size_of;
-use crc::{Crc, CRC_16_IBM_3740};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@@ -19,9 +18,6 @@ pub mod scheduling;
pub mod verification;
pub type CrcType = u16;
-
-/// CRC algorithm used by the PUS standard.
-pub const CRC_CCITT_FALSE: Crc = Crc::::new(&CRC_16_IBM_3740);
pub const CCSDS_HEADER_LEN: usize = size_of::();
#[derive(Debug, Copy, Clone, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
@@ -261,7 +257,10 @@ pub(crate) fn user_data_from_raw(
}
}
-pub(crate) fn verify_crc16_from_raw(raw_data: &[u8], crc16: u16) -> Result<(), PusError> {
+pub(crate) fn verify_crc16_ccitt_false_from_raw(
+ raw_data: &[u8],
+ crc16: u16,
+) -> Result<(), PusError> {
let mut digest = CRC_CCITT_FALSE.digest();
digest.update(raw_data);
if digest.finalize() == 0 {
@@ -291,6 +290,7 @@ macro_rules! sp_header_impls {
}
}
+use crate::util::{GenericUnsignedByteField, ToBeBytes, UnsignedEnum};
pub(crate) use ccsds_impl;
pub(crate) use sp_header_impls;
@@ -298,66 +298,17 @@ pub(crate) use sp_header_impls;
/// and an unsigned value. The trait makes no assumptions about the actual type of the unsigned
/// value and only requires implementors to implement a function which writes the enumeration into
/// a raw byte format.
-pub trait EcssEnumeration {
+pub trait EcssEnumeration: UnsignedEnum {
/// Packet Format Code, which denotes the number of bits of the enumeration
fn pfc(&self) -> u8;
- fn byte_width(&self) -> usize {
- (self.pfc() / 8) as usize
- }
- fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<(), ByteConversionError>;
}
pub trait EcssEnumerationExt: EcssEnumeration + Debug + Copy + Clone + PartialEq + Eq {}
-pub trait ToBeBytes {
- type ByteArray: AsRef<[u8]>;
- fn to_be_bytes(&self) -> Self::ByteArray;
-}
-
-impl ToBeBytes for () {
- type ByteArray = [u8; 0];
-
- fn to_be_bytes(&self) -> Self::ByteArray {
- []
- }
-}
-
-impl ToBeBytes for u8 {
- type ByteArray = [u8; 1];
-
- fn to_be_bytes(&self) -> Self::ByteArray {
- u8::to_be_bytes(*self)
- }
-}
-
-impl ToBeBytes for u16 {
- type ByteArray = [u8; 2];
-
- fn to_be_bytes(&self) -> Self::ByteArray {
- u16::to_be_bytes(*self)
- }
-}
-
-impl ToBeBytes for u32 {
- type ByteArray = [u8; 4];
-
- fn to_be_bytes(&self) -> Self::ByteArray {
- u32::to_be_bytes(*self)
- }
-}
-
-impl ToBeBytes for u64 {
- type ByteArray = [u8; 8];
-
- fn to_be_bytes(&self) -> Self::ByteArray {
- u64::to_be_bytes(*self)
- }
-}
-
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GenericEcssEnumWrapper {
- val: TYPE,
+ field: GenericUnsignedByteField,
}
impl GenericEcssEnumWrapper {
@@ -366,7 +317,19 @@ impl GenericEcssEnumWrapper {
}
pub fn new(val: TYPE) -> Self {
- Self { val }
+ Self {
+ field: GenericUnsignedByteField::new(val),
+ }
+ }
+}
+
+impl UnsignedEnum for GenericEcssEnumWrapper {
+ fn len(&self) -> usize {
+ (self.pfc() / 8) as usize
+ }
+
+ fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result {
+ self.field.write_to_be_bytes(buf)
}
}
@@ -374,17 +337,6 @@ impl EcssEnumeration for GenericEcssEnumWrapper {
fn pfc(&self) -> u8 {
size_of::() as u8 * 8_u8
}
-
- fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<(), ByteConversionError> {
- if buf.len() < self.byte_width() {
- return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
- found: buf.len(),
- expected: self.byte_width(),
- }));
- }
- buf[0..self.byte_width()].copy_from_slice(self.val.to_be_bytes().as_ref());
- Ok(())
- }
}
impl EcssEnumerationExt
@@ -399,7 +351,7 @@ pub type EcssEnumU64 = GenericEcssEnumWrapper;
#[cfg(test)]
mod tests {
- use crate::ecss::{EcssEnumU16, EcssEnumU32, EcssEnumU8, EcssEnumeration};
+ use crate::ecss::{EcssEnumU16, EcssEnumU32, EcssEnumU8, UnsignedEnum};
use crate::ByteConversionError;
#[test]
diff --git a/src/lib.rs b/src/lib.rs
index a3b02e0..094322d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,8 @@
//!
//! - Space Packet implementation according to
//! [CCSDS Blue Book 133.0-B-2](https://public.ccsds.org/Pubs/133x0b2e1.pdf)
+//! - CCSDS File Delivery Protocol (CFDP) packet implementations according to
+//! [CCSDS Blue Book 727.0-B-5](https://public.ccsds.org/Pubs/727x0b5.pdf)
//! - PUS Telecommand and PUS Telemetry implementation according to the
//! [ECSS-E-ST-70-41C standard](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
//! - CUC (CCSDS Unsegmented Time Code) implementation according to
@@ -60,23 +62,31 @@ extern crate alloc;
extern crate std;
use crate::ecss::CCSDS_HEADER_LEN;
-use core::fmt::{Display, Formatter};
+use core::fmt::{Debug, Display, Formatter};
+use crc::{Crc, CRC_16_IBM_3740};
use delegate::delegate;
+#[cfg(not(feature = "std"))]
+use num_traits::Unsigned;
#[cfg(feature = "std")]
use std::error::Error;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
+pub mod cfdp;
pub mod ecss;
pub mod tc;
pub mod time;
pub mod tm;
+pub mod util;
mod private {
pub trait Sealed {}
}
+/// CRC algorithm used by the PUS standard, the CCSDS TC standard and the CFDP standard.
+pub const CRC_CCITT_FALSE: Crc = Crc::::new(&CRC_16_IBM_3740);
+
pub const MAX_APID: u16 = 2u16.pow(11) - 1;
pub const MAX_SEQ_COUNT: u16 = 2u16.pow(14) - 1;
diff --git a/src/tc.rs b/src/tc.rs
index 184e799..8b75f1c 100644
--- a/src/tc.rs
+++ b/src/tc.rs
@@ -33,12 +33,12 @@
//! ```
use crate::ecss::{
ccsds_impl, crc_from_raw_data, crc_procedure, sp_header_impls, user_data_from_raw,
- verify_crc16_from_raw, CrcType, PusError, PusPacket, PusVersion, CRC_CCITT_FALSE,
+ verify_crc16_ccitt_false_from_raw, CrcType, PusError, PusPacket, PusVersion,
};
-use crate::SpHeader;
use crate::{
ByteConversionError, CcsdsPacket, PacketType, SequenceFlags, SizeMissmatch, CCSDS_HEADER_LEN,
};
+use crate::{SpHeader, CRC_CCITT_FALSE};
use core::mem::size_of;
use delegate::delegate;
#[cfg(feature = "serde")]
@@ -437,7 +437,7 @@ impl<'raw_data> PusTc<'raw_data> {
calc_crc_on_serialization: false,
crc16: Some(crc_from_raw_data(raw_data)?),
};
- verify_crc16_from_raw(raw_data, pus_tc.crc16.expect("CRC16 invalid"))?;
+ verify_crc16_ccitt_false_from_raw(raw_data, pus_tc.crc16.expect("CRC16 invalid"))?;
Ok((pus_tc, total_len))
}
diff --git a/src/tm.rs b/src/tm.rs
index 5813ccc..ed84a2e 100644
--- a/src/tm.rs
+++ b/src/tm.rs
@@ -2,11 +2,11 @@
//! to [ECSS-E-ST-70-41C](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
use crate::ecss::{
ccsds_impl, crc_from_raw_data, crc_procedure, sp_header_impls, user_data_from_raw,
- verify_crc16_from_raw, CrcType, PusError, PusPacket, PusVersion, CRC_CCITT_FALSE,
+ verify_crc16_ccitt_false_from_raw, CrcType, PusError, PusPacket, PusVersion,
};
use crate::{
ByteConversionError, CcsdsPacket, PacketType, SequenceFlags, SizeMissmatch, SpHeader,
- CCSDS_HEADER_LEN,
+ CCSDS_HEADER_LEN, CRC_CCITT_FALSE,
};
use core::mem::size_of;
#[cfg(feature = "serde")]
@@ -439,7 +439,7 @@ impl<'raw_data> PusTm<'raw_data> {
calc_crc_on_serialization: false,
crc16: Some(crc_from_raw_data(raw_data)?),
};
- verify_crc16_from_raw(raw_data, pus_tm.crc16.expect("CRC16 invalid"))?;
+ verify_crc16_ccitt_false_from_raw(raw_data, pus_tm.crc16.expect("CRC16 invalid"))?;
Ok((pus_tm, total_len))
}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..b5d34b1
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,642 @@
+use crate::{ByteConversionError, SizeMissmatch};
+use core::fmt::{Debug, Display, Formatter};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "std")]
+use std::error::Error;
+
+pub trait ToBeBytes {
+ type ByteArray: AsRef<[u8]>;
+ /// Length when written to big endian bytes.
+ fn written_len(&self) -> usize;
+ fn to_be_bytes(&self) -> Self::ByteArray;
+}
+
+impl ToBeBytes for () {
+ type ByteArray = [u8; 0];
+
+ fn written_len(&self) -> usize {
+ 0
+ }
+
+ fn to_be_bytes(&self) -> Self::ByteArray {
+ []
+ }
+}
+
+impl ToBeBytes for u8 {
+ type ByteArray = [u8; 1];
+
+ fn written_len(&self) -> usize {
+ 1
+ }
+ fn to_be_bytes(&self) -> Self::ByteArray {
+ u8::to_be_bytes(*self)
+ }
+}
+
+impl ToBeBytes for u16 {
+ type ByteArray = [u8; 2];
+
+ fn written_len(&self) -> usize {
+ 2
+ }
+ fn to_be_bytes(&self) -> Self::ByteArray {
+ u16::to_be_bytes(*self)
+ }
+}
+
+impl ToBeBytes for u32 {
+ type ByteArray = [u8; 4];
+
+ fn written_len(&self) -> usize {
+ 4
+ }
+ fn to_be_bytes(&self) -> Self::ByteArray {
+ u32::to_be_bytes(*self)
+ }
+}
+
+impl ToBeBytes for u64 {
+ type ByteArray = [u8; 8];
+
+ fn written_len(&self) -> usize {
+ 8
+ }
+ fn to_be_bytes(&self) -> Self::ByteArray {
+ u64::to_be_bytes(*self)
+ }
+}
+
+#[allow(clippy::len_without_is_empty)]
+pub trait UnsignedEnum {
+ fn len(&self) -> usize;
+ /// Write the unsigned enumeration to a raw buffer. Returns the written size on success.
+ fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result;
+}
+
+pub trait UnsignedEnumExt: UnsignedEnum + Debug + Copy + Clone + PartialEq + Eq {}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum UnsignedByteFieldError {
+ /// Value is too large for specified width of byte field. The first value contains the width,
+ /// the second value contains the detected value.
+ ValueTooLargeForWidth((usize, u64)),
+ /// Only 1, 2, 4 and 8 are allow width values. Optionally contains the expected width if
+ /// applicable, for example for conversions.
+ InvalidWidth(usize, Option),
+ ByteConversionError(ByteConversionError),
+}
+
+impl From for UnsignedByteFieldError {
+ fn from(value: ByteConversionError) -> Self {
+ Self::ByteConversionError(value)
+ }
+}
+
+impl Display for UnsignedByteFieldError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Self::ByteConversionError(e) => {
+ write!(f, "low level byte conversion error: {e}")
+ }
+ Self::InvalidWidth(val, _) => {
+ write!(f, "invalid width {val}, only 1, 2, 4 and 8 are allowed.")
+ }
+ Self::ValueTooLargeForWidth((width, value)) => {
+ write!(f, "value {value} too large for width {width}")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl Error for UnsignedByteFieldError {}
+
+/// Type erased variant.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct UnsignedByteField {
+ width: usize,
+ value: u64,
+}
+
+impl UnsignedByteField {
+ pub fn new(width: usize, value: u64) -> Self {
+ Self { width, value }
+ }
+
+ pub fn value(&self) -> u64 {
+ self.value
+ }
+
+ pub fn new_from_be_bytes(width: usize, buf: &[u8]) -> Result {
+ if width > buf.len() {
+ return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
+ expected: width,
+ found: buf.len(),
+ })
+ .into());
+ }
+ match width {
+ 0 => Ok(Self::new(width, 0)),
+ 1 => Ok(Self::new(width, buf[0] as u64)),
+ 2 => Ok(Self::new(
+ width,
+ u16::from_be_bytes(buf[0..2].try_into().unwrap()) as u64,
+ )),
+ 4 => Ok(Self::new(
+ width,
+ u32::from_be_bytes(buf[0..4].try_into().unwrap()) as u64,
+ )),
+ 8 => Ok(Self::new(
+ width,
+ u64::from_be_bytes(buf[0..8].try_into().unwrap()),
+ )),
+ _ => Err(UnsignedByteFieldError::InvalidWidth(width, None)),
+ }
+ }
+}
+
+impl UnsignedEnum for UnsignedByteField {
+ fn len(&self) -> usize {
+ self.width
+ }
+
+ fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result {
+ if buf.len() < self.len() {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ expected: self.len(),
+ found: buf.len(),
+ }));
+ }
+ match self.len() {
+ 0 => Ok(0),
+ 1 => {
+ let u8 = UnsignedByteFieldU8::try_from(*self).unwrap();
+ u8.write_to_be_bytes(buf)
+ }
+ 2 => {
+ let u16 = UnsignedByteFieldU16::try_from(*self).unwrap();
+ u16.write_to_be_bytes(buf)
+ }
+ 4 => {
+ let u32 = UnsignedByteFieldU32::try_from(*self).unwrap();
+ u32.write_to_be_bytes(buf)
+ }
+ 8 => {
+ let u64 = UnsignedByteFieldU64::try_from(*self).unwrap();
+ u64.write_to_be_bytes(buf)
+ }
+ _ => {
+ // The API does not allow this.
+ panic!("unexpected written length");
+ }
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct GenericUnsignedByteField {
+ value: TYPE,
+}
+
+impl GenericUnsignedByteField {
+ pub fn new(val: TYPE) -> Self {
+ Self { value: val }
+ }
+}
+
+impl UnsignedEnum for GenericUnsignedByteField {
+ fn len(&self) -> usize {
+ self.value.written_len()
+ }
+
+ fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result {
+ if buf.len() < self.len() {
+ return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
+ found: buf.len(),
+ expected: self.len(),
+ }));
+ }
+ buf[0..self.len()].copy_from_slice(self.value.to_be_bytes().as_ref());
+ Ok(self.value.written_len())
+ }
+}
+
+pub type UnsignedByteFieldEmpty = GenericUnsignedByteField<()>;
+pub type UnsignedByteFieldU8 = GenericUnsignedByteField;
+pub type UnsignedByteFieldU16 = GenericUnsignedByteField;
+pub type UnsignedByteFieldU32 = GenericUnsignedByteField;
+pub type UnsignedByteFieldU64 = GenericUnsignedByteField;
+
+pub type UbfU8 = UnsignedByteFieldU8;
+pub type UbfU16 = UnsignedByteFieldU16;
+pub type UbfU32 = UnsignedByteFieldU32;
+pub type UbfU64 = UnsignedByteFieldU64;
+
+impl From for UnsignedByteField {
+ fn from(value: UnsignedByteFieldU8) -> Self {
+ Self::new(1, value.value as u64)
+ }
+}
+
+impl TryFrom for UnsignedByteFieldU8 {
+ type Error = UnsignedByteFieldError;
+
+ fn try_from(value: UnsignedByteField) -> Result {
+ if value.width != 1 {
+ return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(1)));
+ }
+ Ok(Self::new(value.value as u8))
+ }
+}
+
+impl From for UnsignedByteField {
+ fn from(value: UnsignedByteFieldU16) -> Self {
+ Self::new(2, value.value as u64)
+ }
+}
+
+impl TryFrom for UnsignedByteFieldU16 {
+ type Error = UnsignedByteFieldError;
+
+ fn try_from(value: UnsignedByteField) -> Result {
+ if value.width != 2 {
+ return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(2)));
+ }
+ Ok(Self::new(value.value as u16))
+ }
+}
+
+impl From for UnsignedByteField {
+ fn from(value: UnsignedByteFieldU32) -> Self {
+ Self::new(4, value.value as u64)
+ }
+}
+
+impl TryFrom for UnsignedByteFieldU32 {
+ type Error = UnsignedByteFieldError;
+
+ fn try_from(value: UnsignedByteField) -> Result {
+ if value.width != 4 {
+ return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(4)));
+ }
+ Ok(Self::new(value.value as u32))
+ }
+}
+
+impl From for UnsignedByteField {
+ fn from(value: UnsignedByteFieldU64) -> Self {
+ Self::new(8, value.value)
+ }
+}
+
+impl TryFrom for UnsignedByteFieldU64 {
+ type Error = UnsignedByteFieldError;
+
+ fn try_from(value: UnsignedByteField) -> Result {
+ if value.width != 8 {
+ return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(8)));
+ }
+ Ok(Self::new(value.value))
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use crate::util::{
+ UnsignedByteField, UnsignedByteFieldError, UnsignedByteFieldU16, UnsignedByteFieldU32,
+ UnsignedByteFieldU64, UnsignedByteFieldU8, UnsignedEnum,
+ };
+ use crate::ByteConversionError;
+ use std::format;
+
+ #[test]
+ fn test_simple_u8() {
+ let u8 = UnsignedByteFieldU8::new(5);
+ assert_eq!(u8.len(), 1);
+ let mut buf: [u8; 8] = [0; 8];
+ let len = u8
+ .write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ assert_eq!(len, 1);
+ assert_eq!(buf[0], 5);
+ for i in 1..8 {
+ assert_eq!(buf[i], 0);
+ }
+ }
+
+ #[test]
+ fn test_simple_u16() {
+ let u16 = UnsignedByteFieldU16::new(3823);
+ assert_eq!(u16.len(), 2);
+ let mut buf: [u8; 8] = [0; 8];
+ let len = u16
+ .write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ assert_eq!(len, 2);
+ let raw_val = u16::from_be_bytes(buf[0..2].try_into().unwrap());
+ assert_eq!(raw_val, 3823);
+ for i in 2..8 {
+ assert_eq!(buf[i], 0);
+ }
+ }
+
+ #[test]
+ fn test_simple_u32() {
+ let u32 = UnsignedByteFieldU32::new(80932);
+ assert_eq!(u32.len(), 4);
+ let mut buf: [u8; 8] = [0; 8];
+ let len = u32
+ .write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ assert_eq!(len, 4);
+ let raw_val = u32::from_be_bytes(buf[0..4].try_into().unwrap());
+ assert_eq!(raw_val, 80932);
+ for i in 4..8 {
+ assert_eq!(buf[i], 0);
+ }
+ }
+
+ #[test]
+ fn test_simple_u64() {
+ let u64 = UnsignedByteFieldU64::new(5999999);
+ assert_eq!(u64.len(), 8);
+ let mut buf: [u8; 8] = [0; 8];
+ let len = u64
+ .write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ assert_eq!(len, 8);
+ let raw_val = u64::from_be_bytes(buf[0..8].try_into().unwrap());
+ assert_eq!(raw_val, 5999999);
+ }
+
+ #[test]
+ fn conversions_u8() {
+ let u8 = UnsignedByteFieldU8::new(5);
+ let u8_type_erased = UnsignedByteField::from(u8);
+ assert_eq!(u8_type_erased.width, 1);
+ assert_eq!(u8_type_erased.value, 5);
+ let u8_conv_back =
+ UnsignedByteFieldU8::try_from(u8_type_erased).expect("conversion failed for u8");
+ assert_eq!(u8, u8_conv_back);
+ assert_eq!(u8_conv_back.value, 5);
+ }
+
+ #[test]
+ fn conversion_u8_fails() {
+ let field = UnsignedByteField::new(2, 60000);
+ let conv_fails = UnsignedByteFieldU8::try_from(field);
+ assert!(conv_fails.is_err());
+ let err = conv_fails.unwrap_err();
+ match err {
+ UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
+ assert_eq!(width, 2);
+ assert_eq!(expected, 1);
+ }
+ _ => {
+ panic!("{}", format!("invalid error {err}"))
+ }
+ }
+ }
+
+ #[test]
+ fn conversions_u16() {
+ let u16 = UnsignedByteFieldU16::new(64444);
+ let u16_type_erased = UnsignedByteField::from(u16);
+ assert_eq!(u16_type_erased.width, 2);
+ assert_eq!(u16_type_erased.value, 64444);
+ let u16_conv_back =
+ UnsignedByteFieldU16::try_from(u16_type_erased).expect("conversion failed for u16");
+ assert_eq!(u16, u16_conv_back);
+ assert_eq!(u16_conv_back.value, 64444);
+ }
+
+ #[test]
+ fn conversion_u16_fails() {
+ let field = UnsignedByteField::new(4, 75000);
+ let conv_fails = UnsignedByteFieldU16::try_from(field);
+ assert!(conv_fails.is_err());
+ let err = conv_fails.unwrap_err();
+ match err {
+ UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
+ assert_eq!(width, 4);
+ assert_eq!(expected, 2);
+ }
+ _ => {
+ panic!("{}", format!("invalid error {err}"))
+ }
+ }
+ }
+
+ #[test]
+ fn conversions_u32() {
+ let u32 = UnsignedByteFieldU32::new(75000);
+ let u32_type_erased = UnsignedByteField::from(u32);
+ assert_eq!(u32_type_erased.width, 4);
+ assert_eq!(u32_type_erased.value, 75000);
+ let u32_conv_back =
+ UnsignedByteFieldU32::try_from(u32_type_erased).expect("conversion failed for u32");
+ assert_eq!(u32, u32_conv_back);
+ assert_eq!(u32_conv_back.value, 75000);
+ }
+
+ #[test]
+ fn conversion_u32_fails() {
+ let field = UnsignedByteField::new(8, 75000);
+ let conv_fails = UnsignedByteFieldU32::try_from(field);
+ assert!(conv_fails.is_err());
+ let err = conv_fails.unwrap_err();
+ match err {
+ UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
+ assert_eq!(width, 8);
+ assert_eq!(expected, 4);
+ }
+ _ => {
+ panic!("{}", format!("invalid error {err}"))
+ }
+ }
+ }
+
+ #[test]
+ fn conversions_u64() {
+ let u64 = UnsignedByteFieldU64::new(5999999);
+ let u64_type_erased = UnsignedByteField::from(u64);
+ assert_eq!(u64_type_erased.width, 8);
+ assert_eq!(u64_type_erased.value, 5999999);
+ let u64_conv_back =
+ UnsignedByteFieldU64::try_from(u64_type_erased).expect("conversion failed for u64");
+ assert_eq!(u64, u64_conv_back);
+ assert_eq!(u64_conv_back.value, 5999999);
+ }
+
+ #[test]
+ fn conversion_u64_fails() {
+ let field = UnsignedByteField::new(4, 60000);
+ let conv_fails = UnsignedByteFieldU64::try_from(field);
+ assert!(conv_fails.is_err());
+ let err = conv_fails.unwrap_err();
+ match err {
+ UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
+ assert_eq!(width, 4);
+ assert_eq!(expected, 8);
+ }
+ _ => {
+ panic!("{}", format!("invalid error {err}"))
+ }
+ }
+ }
+
+ #[test]
+ fn type_erased_u8_write() {
+ let u8 = UnsignedByteField::new(1, 5);
+ assert_eq!(u8.len(), 1);
+ let mut buf: [u8; 8] = [0; 8];
+ u8.write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ assert_eq!(buf[0], 5);
+ for i in 1..8 {
+ assert_eq!(buf[i], 0);
+ }
+ }
+
+ #[test]
+ fn type_erased_u16_write() {
+ let u16 = UnsignedByteField::new(2, 3823);
+ assert_eq!(u16.len(), 2);
+ let mut buf: [u8; 8] = [0; 8];
+ u16.write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ let raw_val = u16::from_be_bytes(buf[0..2].try_into().unwrap());
+ assert_eq!(raw_val, 3823);
+ for i in 2..8 {
+ assert_eq!(buf[i], 0);
+ }
+ }
+
+ #[test]
+ fn type_erased_u32_write() {
+ let u32 = UnsignedByteField::new(4, 80932);
+ assert_eq!(u32.len(), 4);
+ let mut buf: [u8; 8] = [0; 8];
+ u32.write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ let raw_val = u32::from_be_bytes(buf[0..4].try_into().unwrap());
+ assert_eq!(raw_val, 80932);
+ for i in 4..8 {
+ assert_eq!(buf[i], 0);
+ }
+ }
+
+ #[test]
+ fn type_erased_u64_write() {
+ let u64 = UnsignedByteField::new(8, 5999999);
+ assert_eq!(u64.len(), 8);
+ let mut buf: [u8; 8] = [0; 8];
+ u64.write_to_be_bytes(&mut buf)
+ .expect("writing to raw buffer failed");
+ let raw_val = u64::from_be_bytes(buf[0..8].try_into().unwrap());
+ assert_eq!(raw_val, 5999999);
+ }
+
+ #[test]
+ fn type_erased_u8_construction() {
+ let buf: [u8; 2] = [5, 10];
+ let u8 = UnsignedByteField::new_from_be_bytes(1, &buf).expect("construction failed");
+ assert_eq!(u8.width, 1);
+ assert_eq!(u8.value, 5);
+ }
+
+ #[test]
+ fn type_erased_u16_construction() {
+ let buf: [u8; 2] = [0x10, 0x15];
+ let u16 = UnsignedByteField::new_from_be_bytes(2, &buf).expect("construction failed");
+ assert_eq!(u16.width, 2);
+ assert_eq!(u16.value, 0x1015);
+ }
+
+ #[test]
+ fn type_erased_u32_construction() {
+ let buf: [u8; 4] = [0x01, 0x02, 0x03, 0x04];
+ let u32 = UnsignedByteField::new_from_be_bytes(4, &buf).expect("construction failed");
+ assert_eq!(u32.width, 4);
+ assert_eq!(u32.value, 0x01020304);
+ }
+
+ #[test]
+ fn type_erased_u64_construction() {
+ let buf: [u8; 8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
+ let u64 = UnsignedByteField::new_from_be_bytes(8, &buf).expect("construction failed");
+ assert_eq!(u64.width, 8);
+ assert_eq!(u64.value, 0x0102030405060708);
+ }
+
+ #[test]
+ fn type_u16_target_buf_too_small() {
+ let u16 = UnsignedByteFieldU16::new(500);
+ let mut buf: [u8; 1] = [0; 1];
+ let res = u16.write_to_be_bytes(&mut buf);
+ assert!(res.is_err());
+ let err = res.unwrap_err();
+ match err {
+ ByteConversionError::ToSliceTooSmall(missmatch) => {
+ assert_eq!(missmatch.found, 1);
+ assert_eq!(missmatch.expected, 2);
+ }
+ _ => {
+ panic!("invalid exception")
+ }
+ }
+ }
+
+ #[test]
+ fn type_erased_u16_target_buf_too_small() {
+ let u16 = UnsignedByteField::new(2, 500);
+ let mut buf: [u8; 1] = [0; 1];
+ let res = u16.write_to_be_bytes(&mut buf);
+ assert!(res.is_err());
+ let err = res.unwrap_err();
+ match err {
+ ByteConversionError::ToSliceTooSmall(missmatch) => {
+ assert_eq!(missmatch.found, 1);
+ assert_eq!(missmatch.expected, 2);
+ }
+ _ => {
+ panic!("invalid exception {}", err)
+ }
+ }
+ let u16 = UnsignedByteField::new_from_be_bytes(2, &buf);
+ assert!(u16.is_err());
+ let err = u16.unwrap_err();
+ if let UnsignedByteFieldError::ByteConversionError(
+ ByteConversionError::FromSliceTooSmall(missmatch),
+ ) = err
+ {
+ assert_eq!(missmatch.expected, 2);
+ assert_eq!(missmatch.found, 1);
+ } else {
+ panic!("unexpected exception {}", err);
+ }
+ }
+
+ #[test]
+ fn type_u32_target_buf_too_small() {
+ let u16 = UnsignedByteFieldU32::new(500);
+ let mut buf: [u8; 3] = [0; 3];
+ let res = u16.write_to_be_bytes(&mut buf);
+ assert!(res.is_err());
+ let err = res.unwrap_err();
+ match err {
+ ByteConversionError::ToSliceTooSmall(missmatch) => {
+ assert_eq!(missmatch.found, 3);
+ assert_eq!(missmatch.expected, 4);
+ }
+ _ => {
+ panic!("invalid exception")
+ }
+ }
+ }
+}