diff --git a/CHANGELOG.md b/CHANGELOG.md index 4114731..cb356e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] -# [v0.11.3] 2024-06-25 +# [v0.12.0] - Minor documentation build updates. +## Added + +- Added new `cfdp::tlv::TlvOwned` type which erases the lifetime and is clonable. +- Dedicated `cfdp::tlv::TlvLvDataTooLarge` error struct for APIs where this is the only possible + API error. + +## Added and Changed + +- Added new `ReadableTlv` to avoid some boilerplate code and have a common abstraction implemented + for both `Tlv` and `TlvOwned` to read the raw TLV data field and its length. +- Replaced `cfdp::tlv::TlvLvError` by `cfdp::tlv::TlvLvDataTooLarge` where applicable. + # [v0.11.2] 2024-05-19 - Bumped MSRV to 1.68.2 diff --git a/Cargo.toml b/Cargo.toml index f069764..11a6ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spacepackets" -version = "0.11.3" +version = "0.12.0" edition = "2021" rust-version = "1.68.2" authors = ["Robin Mueller "] @@ -60,7 +60,7 @@ chrono = "0.4" default = ["std"] std = ["chrono/std", "chrono/clock", "alloc", "thiserror"] serde = ["dep:serde", "chrono/serde"] -alloc = ["postcard/alloc", "chrono/alloc"] +alloc = ["postcard/alloc", "chrono/alloc", "defmt/alloc", "serde/alloc"] chrono = ["dep:chrono"] timelib = ["dep:time"] defmt = ["dep:defmt"] diff --git a/src/cfdp/lv.rs b/src/cfdp/lv.rs index 638200a..dc12a66 100644 --- a/src/cfdp/lv.rs +++ b/src/cfdp/lv.rs @@ -1,5 +1,4 @@ //! Generic CFDP length-value (LV) abstraction as specified in CFDP 5.1.8. -use crate::cfdp::TlvLvError; use crate::ByteConversionError; use core::str::Utf8Error; #[cfg(feature = "serde")] @@ -7,6 +6,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] use std::string::String; +use super::TlvLvDataTooLarge; + pub const MIN_LV_LEN: usize = 1; /// Generic CFDP length-value (LV) abstraction as specified in CFDP 5.1.8. @@ -63,9 +64,9 @@ pub(crate) fn generic_len_check_deserialization( impl<'data> Lv<'data> { #[inline] - pub fn new(data: &[u8]) -> Result { + pub fn new(data: &[u8]) -> Result { if data.len() > u8::MAX as usize { - return Err(TlvLvError::DataTooLarge(data.len())); + return Err(TlvLvDataTooLarge(data.len())); } Ok(Lv { data, @@ -85,7 +86,7 @@ impl<'data> Lv<'data> { /// Helper function to build a string LV. This is especially useful for the file or directory /// path LVs #[inline] - pub fn new_from_str(str_slice: &str) -> Result { + pub fn new_from_str(str_slice: &str) -> Result { Self::new(str_slice.as_bytes()) } @@ -93,7 +94,7 @@ impl<'data> Lv<'data> { /// path LVs #[cfg(feature = "std")] #[inline] - pub fn new_from_string(string: &'data String) -> Result, TlvLvError> { + pub fn new_from_string(string: &'data String) -> Result, TlvLvDataTooLarge> { Self::new(string.as_bytes()) } @@ -177,10 +178,10 @@ impl<'data> Lv<'data> { #[cfg(test)] pub mod tests { - use super::*; use alloc::string::ToString; - use crate::cfdp::TlvLvError; + use super::*; + use crate::ByteConversionError; use std::string::String; @@ -271,15 +272,11 @@ pub mod tests { 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); - assert_eq!( - error.to_string(), - "data with size 256 larger than allowed 255 bytes" - ); - } else { - panic!("invalid exception {:?}", error) - } + assert_eq!(error.0, u8::MAX as usize + 1); + assert_eq!( + error.to_string(), + "data with size 256 larger than allowed 255 bytes" + ); } #[test] diff --git a/src/cfdp/mod.rs b/src/cfdp/mod.rs index aa9da8a..193c93f 100644 --- a/src/cfdp/mod.rs +++ b/src/cfdp/mod.rs @@ -176,11 +176,30 @@ impl Default for ChecksumType { pub const NULL_CHECKSUM_U32: [u8; 4] = [0; 4]; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TlvLvDataTooLarge(pub usize); + +impl Display for TlvLvDataTooLarge { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!( + f, + "data with size {} larger than allowed {} bytes", + self.0, + u8::MAX + ) + } +} + +#[cfg(feature = "std")] +impl Error for TlvLvDataTooLarge {} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum TlvLvError { - DataTooLarge(usize), + DataTooLarge(TlvLvDataTooLarge), ByteConversion(ByteConversionError), /// First value: Found value. Second value: Expected value if there is one. InvalidTlvTypeField { @@ -197,6 +216,12 @@ pub enum TlvLvError { InvalidFilestoreActionCode(u8), } +impl From for TlvLvError { + fn from(value: TlvLvDataTooLarge) -> Self { + Self::DataTooLarge(value) + } +} + impl From for TlvLvError { fn from(value: ByteConversionError) -> Self { Self::ByteConversion(value) @@ -206,13 +231,8 @@ impl From for TlvLvError { 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::DataTooLarge(e) => { + write!(f, "{}", e) } TlvLvError::ByteConversion(e) => { write!(f, "tlv or lv byte conversion: {}", e) @@ -240,6 +260,7 @@ impl Display for TlvLvError { impl Error for TlvLvError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { + TlvLvError::DataTooLarge(e) => Some(e), TlvLvError::ByteConversion(e) => Some(e), _ => None, } diff --git a/src/cfdp/pdu/finished.rs b/src/cfdp/pdu/finished.rs index ec0124f..742c449 100644 --- a/src/cfdp/pdu/finished.rs +++ b/src/cfdp/pdu/finished.rs @@ -10,6 +10,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use super::tlv::ReadableTlv; use super::{CfdpPdu, WritablePduPacket}; #[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] diff --git a/src/cfdp/pdu/metadata.rs b/src/cfdp/pdu/metadata.rs index 9c4f2fd..637e19f 100644 --- a/src/cfdp/pdu/metadata.rs +++ b/src/cfdp/pdu/metadata.rs @@ -11,6 +11,7 @@ use alloc::vec::Vec; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use super::tlv::ReadableTlv; use super::{CfdpPdu, WritablePduPacket}; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] @@ -354,7 +355,7 @@ pub mod tests { }; use crate::cfdp::pdu::{CfdpPdu, PduError, WritablePduPacket}; use crate::cfdp::pdu::{FileDirectiveType, PduHeader}; - use crate::cfdp::tlv::{Tlv, TlvType}; + use crate::cfdp::tlv::{ReadableTlv, Tlv, TlvType}; use crate::cfdp::{ ChecksumType, CrcFlag, Direction, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl, TransmissionMode, diff --git a/src/cfdp/tlv/mod.rs b/src/cfdp/tlv/mod.rs index b71927d..71154f3 100644 --- a/src/cfdp/tlv/mod.rs +++ b/src/cfdp/tlv/mod.rs @@ -9,10 +9,14 @@ use crate::ByteConversionError; use alloc::vec; #[cfg(feature = "alloc")] use alloc::vec::Vec; +#[cfg(feature = "alloc")] +pub use alloc_mod::*; use num_enum::{IntoPrimitive, TryFromPrimitive}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use super::TlvLvDataTooLarge; + pub mod msg_to_user; pub const MIN_TLV_LEN: usize = 2; @@ -39,6 +43,26 @@ pub trait GenericTlv { } } +pub trait ReadableTlv { + fn value(&self) -> &[u8]; + + /// Checks whether the value field is empty. + fn is_empty(&self) -> bool { + self.value().is_empty() + } + + /// Helper method to retrieve the length of the value. Simply calls the [slice::len] method of + /// [Self::value] + fn len_value(&self) -> usize { + self.value().len() + } + + /// Returns the full raw length, including the length byte. + fn len_full(&self) -> usize { + self.len_value() + 2 + } +} + pub trait WritableTlv { fn write_to_bytes(&self, buf: &mut [u8]) -> Result; fn len_written(&self) -> usize; @@ -129,14 +153,14 @@ pub struct Tlv<'data> { } impl<'data> Tlv<'data> { - pub fn new(tlv_type: TlvType, data: &[u8]) -> Result { + pub fn new(tlv_type: TlvType, data: &[u8]) -> Result { Ok(Tlv { tlv_type_field: TlvTypeField::Standard(tlv_type), lv: Lv::new(data)?, }) } - pub fn new_with_custom_type(tlv_type: u8, data: &[u8]) -> Result { + pub fn new_with_custom_type(tlv_type: u8, data: &[u8]) -> Result { Ok(Tlv { tlv_type_field: TlvTypeField::Custom(tlv_type), lv: Lv::new(data)?, @@ -151,26 +175,6 @@ impl<'data> Tlv<'data> { } } - pub fn value(&self) -> &[u8] { - self.lv.value() - } - - /// Checks whether the value field is empty. - pub fn is_empty(&self) -> bool { - self.value().is_empty() - } - - /// Helper method to retrieve the length of the value. Simply calls the [slice::len] method of - /// [Self::value] - pub fn len_value(&self) -> usize { - self.value().len() - } - - /// Returns the full raw length, including the length byte. - pub fn len_full(&self) -> usize { - self.len_value() + 2 - } - /// 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 @@ -192,6 +196,27 @@ impl<'data> Tlv<'data> { pub fn raw_data(&self) -> Option<&[u8]> { self.lv.raw_data() } + + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> TlvOwned { + TlvOwned { + tlv_type_field: self.tlv_type_field, + data: self.value().to_vec(), + } + } +} + +#[cfg(feature = "alloc")] +impl PartialEq for Tlv<'_> { + fn eq(&self, other: &TlvOwned) -> bool { + self.tlv_type_field == other.tlv_type_field && self.value() == other.value() + } +} + +impl ReadableTlv for Tlv<'_> { + fn value(&self) -> &[u8] { + self.lv.value() + } } impl WritableTlv for Tlv<'_> { @@ -212,18 +237,98 @@ impl GenericTlv for Tlv<'_> { } } -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 { - found: raw_type, - expected: Some(expected_tlv_type.into()), - })?; - if tlv_type != expected_tlv_type { - return Err(TlvLvError::InvalidTlvTypeField { - found: tlv_type as u8, - expected: Some(expected_tlv_type as u8), - }); +#[cfg(feature = "alloc")] +pub mod alloc_mod { + use crate::cfdp::TlvLvDataTooLarge; + + use super::*; + + /// Owned variant of [Tlv] which is consequently [Clone]able and does not have a lifetime + /// associated to a data slice. + #[derive(Debug, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct TlvOwned { + pub(crate) tlv_type_field: TlvTypeField, + pub(crate) data: Vec, + } + + impl TlvOwned { + pub fn new(tlv_type: TlvType, data: &[u8]) -> Result { + if data.len() > u8::MAX as usize { + return Err(TlvLvDataTooLarge(data.len())); + } + Ok(Self { + tlv_type_field: TlvTypeField::Standard(tlv_type), + data: data.to_vec(), + }) + } + + pub fn new_with_custom_type(tlv_type: u8, data: &[u8]) -> Result { + if data.len() > u8::MAX as usize { + return Err(TlvLvDataTooLarge(data.len())); + } + Ok(Self { + tlv_type_field: TlvTypeField::Custom(tlv_type), + data: data.to_vec(), + }) + } + + /// Creates a TLV with an empty value field. + pub fn new_empty(tlv_type: TlvType) -> Self { + Self { + tlv_type_field: TlvTypeField::Standard(tlv_type), + data: Vec::new(), + } + } + + pub fn as_tlv(&self) -> Tlv<'_> { + Tlv { + tlv_type_field: self.tlv_type_field, + // The API should ensure that the data length is never to large, so the unwrap for the + // LV creation should never be an issue. + lv: Lv::new(&self.data).expect("lv creation failed unexpectedly"), + } + } + } + + impl ReadableTlv for TlvOwned { + fn value(&self) -> &[u8] { + &self.data + } + } + + impl WritableTlv for TlvOwned { + fn write_to_bytes(&self, buf: &mut [u8]) -> Result { + generic_len_check_data_serialization(buf, self.data.len(), MIN_TLV_LEN)?; + buf[0] = self.tlv_type_field.into(); + buf[1] = self.data.len() as u8; + buf[2..2 + self.data.len()].copy_from_slice(&self.data); + Ok(self.len_written()) + } + + fn len_written(&self) -> usize { + self.data.len() + 2 + } + } + + impl GenericTlv for TlvOwned { + fn tlv_type_field(&self) -> TlvTypeField { + self.tlv_type_field + } + } + + impl From> for TlvOwned { + fn from(value: Tlv<'_>) -> Self { + value.to_owned() + } + } + + impl PartialEq> for TlvOwned { + fn eq(&self, other: &Tlv) -> bool { + self.tlv_type_field == other.tlv_type_field && self.data == other.value() + } } - Ok(()) } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -238,7 +343,7 @@ impl EntityIdTlv { Self { entity_id } } - fn len_check(buf: &[u8]) -> Result<(), ByteConversionError> { + fn check_min_len(buf: &[u8]) -> Result<(), ByteConversionError> { if buf.len() < 2 { return Err(ByteConversionError::ToSliceTooSmall { found: buf.len(), @@ -261,7 +366,7 @@ impl EntityIdTlv { } pub fn from_bytes(buf: &[u8]) -> Result { - Self::len_check(buf)?; + Self::check_min_len(buf)?; verify_tlv_type(buf[0], TlvType::EntityId)?; let len = buf[1]; if len != 1 && len != 2 && len != 4 && len != 8 { @@ -272,22 +377,31 @@ impl EntityIdTlv { Ok(Self { entity_id }) } - /// Convert to a generic [Tlv], which also erases the programmatic type information. + /// Convert to a generic [Tlv], which also erases the type information. pub fn to_tlv(self, buf: &mut [u8]) -> Result { - Self::len_check(buf)?; + Self::check_min_len(buf)?; self.entity_id .write_to_be_bytes(&mut buf[2..2 + self.entity_id.size()])?; - Tlv::new(TlvType::EntityId, &buf[2..2 + self.entity_id.size()]).map_err(|e| match e { - TlvLvError::ByteConversion(e) => e, - // All other errors are impossible. - _ => panic!("unexpected TLV error"), - }) + if buf.len() < self.len_value() { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: self.len_value(), + }); + } + // We performed all checks necessary to ensure this call never panics. + Ok(Tlv::new(TlvType::EntityId, &buf[2..2 + self.entity_id.size()]).unwrap()) + } + + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> TlvOwned { + // Unwrap is okay here, entity ID should never be larger than maximum allowed size. + TlvOwned::new(TlvType::EntityId, &self.entity_id.to_vec()).unwrap() } } impl WritableTlv for EntityIdTlv { fn write_to_bytes(&self, buf: &mut [u8]) -> Result { - Self::len_check(buf)?; + Self::check_min_len(buf)?; buf[0] = TlvType::EntityId as u8; buf[1] = self.entity_id.size() as u8; Ok(2 + self.entity_id.write_to_be_bytes(&mut buf[2..])?) @@ -526,6 +640,12 @@ impl<'first_name, 'second_name> FilestoreRequestTlv<'first_name, 'second_name> { }, }) } + + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> TlvOwned { + // The API should ensure the data field is never too large, so unwrapping here is okay. + TlvOwned::new(TlvType::FilestoreRequest, &self.to_vec()[2..]).unwrap() + } } impl WritableTlv for FilestoreRequestTlv<'_, '_> { @@ -711,6 +831,12 @@ impl<'first_name, 'second_name, 'fs_msg> FilestoreResponseTlv<'first_name, 'seco filestore_message, }) } + + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> TlvOwned { + // The API should ensure the data field is never too large, so unwrap is okay here. + TlvOwned::new(TlvType::FilestoreResponse, &self.to_vec()[2..]).unwrap() + } } impl WritableTlv for FilestoreResponseTlv<'_, '_, '_> { @@ -752,6 +878,20 @@ impl GenericTlv for FilestoreResponseTlv<'_, '_, '_> { } } +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 { + found: raw_type, + expected: Some(expected_tlv_type.into()), + })?; + if tlv_type != expected_tlv_type { + return Err(TlvLvError::InvalidTlvTypeField { + found: tlv_type as u8, + expected: Some(expected_tlv_type as u8), + }); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -939,15 +1079,11 @@ mod tests { 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); - assert_eq!( - error.to_string(), - "data with size 256 larger than allowed 255 bytes" - ); - } else { - panic!("unexpected error {:?}", error); - } + assert_eq!(error.0, u8::MAX as usize + 1); + assert_eq!( + error.to_string(), + "data with size 256 larger than allowed 255 bytes" + ); } #[test] @@ -1300,4 +1436,71 @@ mod tests { assert_eq!(tlv_as_vec[0], 20); assert_eq!(tlv_as_vec[1], 0); } + + #[test] + fn test_tlv_to_owned() { + 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 tlv_owned = tlv_res.to_owned(); + assert_eq!(tlv_res, tlv_owned); + let tlv_owned_from_conversion: TlvOwned = tlv_res.into(); + assert_eq!(tlv_owned_from_conversion, tlv_owned); + assert_eq!(tlv_owned_from_conversion, tlv_res); + } + + #[test] + fn test_owned_tlv() { + 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 = TlvOwned::new(TlvType::EntityId, &buf[0..1]).expect("creating TLV failed"); + assert_eq!( + tlv_res.tlv_type_field(), + TlvTypeField::Standard(TlvType::EntityId) + ); + assert_eq!(tlv_res.len_full(), 3); + assert_eq!(tlv_res.value().len(), 1); + assert_eq!(tlv_res.len_value(), 1); + assert!(!tlv_res.is_empty()); + assert_eq!(tlv_res.value()[0], 5); + } + + #[test] + fn test_owned_tlv_empty() { + let tlv_res = TlvOwned::new_empty(TlvType::FlowLabel); + assert_eq!( + tlv_res.tlv_type_field(), + TlvTypeField::Standard(TlvType::FlowLabel) + ); + assert_eq!(tlv_res.len_full(), 2); + assert_eq!(tlv_res.value().len(), 0); + assert_eq!(tlv_res.len_value(), 0); + assert!(tlv_res.is_empty()); + } + + #[test] + fn test_owned_tlv_custom_type() { + let tlv_res = TlvOwned::new_with_custom_type(32, &[]).unwrap(); + assert_eq!(tlv_res.tlv_type_field(), TlvTypeField::Custom(32)); + assert_eq!(tlv_res.len_full(), 2); + assert_eq!(tlv_res.value().len(), 0); + assert_eq!(tlv_res.len_value(), 0); + assert!(tlv_res.is_empty()); + } + + #[test] + fn test_owned_tlv_conversion_to_bytes() { + 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 tlv_owned_from_conversion: TlvOwned = tlv_res.into(); + assert_eq!(tlv_res.to_vec(), tlv_owned_from_conversion.to_vec()); + } } diff --git a/src/cfdp/tlv/msg_to_user.rs b/src/cfdp/tlv/msg_to_user.rs index 17cdb0f..c6bb673 100644 --- a/src/cfdp/tlv/msg_to_user.rs +++ b/src/cfdp/tlv/msg_to_user.rs @@ -1,6 +1,8 @@ //! Abstractions for the Message to User CFDP TLV subtype. -use super::{GenericTlv, Tlv, TlvLvError, TlvType, TlvTypeField, WritableTlv}; -use crate::ByteConversionError; +#[cfg(feature = "alloc")] +use super::TlvOwned; +use super::{GenericTlv, ReadableTlv, Tlv, TlvLvError, TlvType, TlvTypeField, WritableTlv}; +use crate::{cfdp::TlvLvDataTooLarge, ByteConversionError}; use delegate::delegate; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -10,7 +12,7 @@ pub struct MsgToUserTlv<'data> { impl<'data> MsgToUserTlv<'data> { /// Create a new message to user TLV where the type field is set correctly. - pub fn new(value: &'data [u8]) -> Result, TlvLvError> { + pub fn new(value: &'data [u8]) -> Result, TlvLvDataTooLarge> { Ok(Self { tlv: Tlv::new(TlvType::MsgToUser, value)?, }) @@ -75,6 +77,11 @@ impl<'data> MsgToUserTlv<'data> { } Ok(msg_to_user) } + + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> TlvOwned { + self.tlv.to_owned() + } } impl WritableTlv for MsgToUserTlv<'_> {