From ddd785eaab274cf3af86caf907888398ba61db9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Sj=C3=B6strand?= Date: Thu, 9 Jan 2025 14:35:18 +0100 Subject: [PATCH] Add basic support for REAL using f32/f64 with JER/OER rules --- Cargo.toml | 3 ++ src/ber/de.rs | 9 +++++ src/ber/enc.rs | 10 ++++++ src/de.rs | 31 +++++++++++++++++ src/enc.rs | 36 ++++++++++++++++++++ src/error/decode.rs | 14 ++++++++ src/error/encode.rs | 14 ++++++++ src/jer.rs | 40 ++++++++++++++++++++-- src/jer/de.rs | 38 +++++++++++++++++++++ src/jer/enc.rs | 31 +++++++++++++++++ src/oer.rs | 19 +++++++++++ src/oer/de.rs | 14 ++++++++ src/oer/enc.rs | 17 ++++++++++ src/per/de.rs | 9 +++++ src/per/enc.rs | 10 ++++++ src/types.rs | 17 ++++++++++ src/types/real.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++ 17 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 src/types/real.rs diff --git a/Cargo.toml b/Cargo.toml index 02d690c7..0525fde1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,10 @@ chrono = { version = "0.4.38", default-features = false, features = ["alloc"] } bitvec = { version = "1.0.1", default-features = false, features = ["alloc"] } [features] +default = ["f32", "f64"] std = [] +f32 = [] +f64 = [] backtraces = ["std", "snafu/backtrace"] compiler = ["rasn-compiler"] diff --git a/src/ber/de.rs b/src/ber/de.rs index 25004047..e5f55e0d 100644 --- a/src/ber/de.rs +++ b/src/ber/de.rs @@ -410,6 +410,15 @@ impl<'input> crate::Decoder for Decoder<'input> { } } + #[cfg(any(feature = "f32", feature = "f64"))] + fn decode_real( + &mut self, + _: Tag, + _: Constraints, + ) -> Result { + Err(DecodeError::real_not_supported(self.codec())) + } + fn decode_octet_string<'b, T: From<&'b [u8]> + From>>( &'b mut self, tag: Tag, diff --git a/src/ber/enc.rs b/src/ber/enc.rs index eca6538d..b2aa1f7f 100644 --- a/src/ber/enc.rs +++ b/src/ber/enc.rs @@ -395,6 +395,16 @@ impl crate::Encoder<'_> for Encoder { Ok(()) } + #[cfg(any(feature = "f32", feature = "f64"))] + fn encode_real( + &mut self, + _: Tag, + _: Constraints, + _: &R, + ) -> Result { + Err(EncodeError::real_not_supported(self.codec())) + } + fn encode_null(&mut self, tag: Tag) -> Result { self.encode_primitive(tag, &[]); Ok(()) diff --git a/src/de.rs b/src/de.rs index 5a203f3b..215f6b36 100644 --- a/src/de.rs +++ b/src/de.rs @@ -93,6 +93,15 @@ pub trait Decoder: Sized { tag: Tag, constraints: Constraints, ) -> Result; + + /// Decode a `REAL` identified by `tag` from the available input. + #[cfg(any(feature = "f32", feature = "f64"))] + fn decode_real( + &mut self, + tag: Tag, + constraints: Constraints, + ) -> Result; + /// Decode `NULL` identified by `tag` from the available input. fn decode_null(&mut self, tag: Tag) -> Result<(), Self::Error>; /// Decode a `OBJECT IDENTIFIER` identified by `tag` from the available input. @@ -534,6 +543,28 @@ impl Decode for types::Integer { } } +#[cfg(feature = "f32")] +impl Decode for f32 { + fn decode_with_tag_and_constraints( + decoder: &mut D, + tag: Tag, + _: Constraints, + ) -> Result { + decoder.decode_real::(tag, Constraints::default()) + } +} + +#[cfg(feature = "f64")] +impl Decode for f64 { + fn decode_with_tag_and_constraints( + decoder: &mut D, + tag: Tag, + _: Constraints, + ) -> Result { + decoder.decode_real::(tag, Constraints::default()) + } +} + impl Decode for Box { fn decode(decoder: &mut D) -> Result { T::decode(decoder).map(Box::new) diff --git a/src/enc.rs b/src/enc.rs index e7a99d4b..b9fd676c 100644 --- a/src/enc.rs +++ b/src/enc.rs @@ -2,6 +2,9 @@ use crate::types::{self, AsnType, Constraints, Enumerated, IntegerType, SetOf, Tag}; +#[cfg(any(feature = "f32", feature = "f64"))] +use crate::types::RealType; + use num_bigint::BigInt; pub use rasn_derive::Encode; @@ -117,6 +120,15 @@ pub trait Encoder<'encoder, const RCL: usize = 0, const ECL: usize = 0> { value: &I, ) -> Result; + /// Encode a `REAL` value. + #[cfg(any(feature = "f32", feature = "f64"))] + fn encode_real( + &mut self, + tag: Tag, + constraints: Constraints, + value: &R, + ) -> Result; + /// Encode a `NULL` value. fn encode_null(&mut self, tag: Tag) -> Result; @@ -576,6 +588,30 @@ impl Encode for types::Integer { } } +#[cfg(feature = "f32")] +impl Encode for f32 { + fn encode_with_tag_and_constraints<'b, E: Encoder<'b>>( + &self, + encoder: &mut E, + tag: Tag, + constraints: Constraints, + ) -> Result<(), E::Error> { + encoder.encode_real(tag, constraints, self).map(drop) + } +} + +#[cfg(feature = "f64")] +impl Encode for f64 { + fn encode_with_tag_and_constraints<'b, E: Encoder<'b>>( + &self, + encoder: &mut E, + tag: Tag, + constraints: Constraints, + ) -> Result<(), E::Error> { + encoder.encode_real(tag, constraints, self).map(drop) + } +} + impl Encode for types::OctetString { fn encode_with_tag_and_constraints<'b, E: Encoder<'b>>( &self, diff --git a/src/error/decode.rs b/src/error/decode.rs index 7aef1dd5..07c269f2 100644 --- a/src/error/decode.rs +++ b/src/error/decode.rs @@ -276,6 +276,12 @@ impl DecodeError { DecodeError::from_kind(DecodeErrorKind::Parser { msg }, codec) } + /// Creates a wrapper around using `REAL` with unsupported codecs. + #[must_use] + pub fn real_not_supported(codec: Codec) -> Self { + DecodeError::from_kind(DecodeErrorKind::RealNotSupported, codec) + } + /// Creates a wrapper around a missing required extension error from a given codec. #[must_use] pub fn required_extension_not_present(tag: Tag, codec: Codec) -> Self { @@ -553,6 +559,14 @@ pub enum DecodeErrorKind { msg: alloc::string::String, }, + /// Real conversion failure. + #[snafu(display("Invalid real encoding"))] + InvalidRealEncoding, + + /// Decoder doesn't support REAL + #[snafu(display("Decoder doesn't support REAL types"))] + RealNotSupported, + /// BitString contains an invalid amount of unused bits. #[snafu(display("BitString contains an invalid amount of unused bits: {}", bits))] InvalidBitString { diff --git a/src/error/encode.rs b/src/error/encode.rs index d4704ae7..1a417ef7 100644 --- a/src/error/encode.rs +++ b/src/error/encode.rs @@ -212,6 +212,12 @@ impl EncodeError { Self::from_kind(EncodeErrorKind::VariantNotInChoice, codec) } + /// Returns an encode error when the encoder doesn't support `REAL` type. + #[must_use] + pub fn real_not_supported(codec: crate::Codec) -> Self { + Self::from_kind(EncodeErrorKind::RealNotSuppored, codec) + } + /// A helper function to construct an `EncodeError` from the given `kind` and `codec`. #[must_use] pub fn from_kind(kind: EncodeErrorKind, codec: crate::Codec) -> Self { @@ -328,6 +334,11 @@ pub enum EncodeErrorKind { /// Error when the selected variant is not found in the choice. #[snafu(display("Selected Variant not found from Choice"))] VariantNotInChoice, + + /// Error when we try to encode a `REAL` type with an unspported codec. + #[snafu(display("Encoder doesn't support `REAL` type"))] + RealNotSuppored, + } /// `EncodeError` kinds of `Kind::CodecSpecific` which are specific for BER. #[derive(Snafu, Debug)] @@ -392,6 +403,9 @@ pub enum JerEncodeErrorKind { /// value failed to encode value: BigInt, }, + /// Error to be thrown when encoding real values that exceed the supported range + #[snafu(display("Exceeds supported real value range"))] + ExceedsSupportedRealRange, /// Error to be thrown when some character from the input data is not valid UTF-8 #[snafu(display("Invalid character: {:?}", error))] InvalidCharacter { diff --git a/src/jer.rs b/src/jer.rs index 40c6ee16..106cc2c4 100644 --- a/src/jer.rs +++ b/src/jer.rs @@ -24,6 +24,13 @@ pub fn encode( #[cfg(test)] mod tests { macro_rules! round_trip_jer { + ($typ:ty, $value:expr, $expected:expr) => {{ + let value: $typ = $value; + pretty_assertions::assert_eq!(value, round_trip_value!($typ, $value, $expected)); + }}; + } + + macro_rules! round_trip_value { ($typ:ty, $value:expr, $expected:expr) => {{ let value: $typ = $value; let expected: &'static str = $expected; @@ -32,8 +39,7 @@ mod tests { pretty_assertions::assert_eq!(expected, &*actual_encoding); let decoded_value: $typ = crate::jer::decode(&actual_encoding).unwrap(); - - pretty_assertions::assert_eq!(value, decoded_value); + decoded_value }}; } @@ -187,6 +193,36 @@ mod tests { round_trip_jer!(ConstrainedInt, ConstrainedInt(1.into()), "1"); } + #[test] + #[cfg(feature = "f32")] + fn real_f32() { + round_trip_jer!(f32, 0.0, "0.0"); + round_trip_jer!(f32, -0.0, "\"-0\""); + + round_trip_jer!(f32, f32::INFINITY, "\"INF\""); + round_trip_jer!(f32, f32::NEG_INFINITY, "\"-INF\""); + + assert!(round_trip_value!(f32, f32::NAN, "\"NAN\"").is_nan()); + + round_trip_jer!(f32, 1.0, "1.0"); + round_trip_jer!(f32, -1.0, "-1.0"); + } + + #[test] + #[cfg(feature = "f64")] + fn real_f64() { + round_trip_jer!(f64, 0.0, "0.0"); + round_trip_jer!(f64, -0.0, "\"-0\""); + + round_trip_jer!(f64, f64::INFINITY, "\"INF\""); + round_trip_jer!(f64, f64::NEG_INFINITY, "\"-INF\""); + + assert!(round_trip_value!(f64, f64::NAN, "\"NAN\"").is_nan()); + + round_trip_jer!(f64, 1.0, "1.0"); + round_trip_jer!(f64, -1.0, "-1.0"); + } + #[test] fn bit_string() { round_trip_jer!( diff --git a/src/jer/de.rs b/src/jer/de.rs index 51e775c1..729ee1e0 100644 --- a/src/jer/de.rs +++ b/src/jer/de.rs @@ -142,6 +142,15 @@ impl crate::Decoder for Decoder { decode_jer_value!(Self::integer_from_value::, self.stack) } + #[cfg(any(feature = "f32", feature = "f64"))] + fn decode_real( + &mut self, + _t: Tag, + _c: Constraints, + ) -> Result { + decode_jer_value!(Self::real_from_value::, self.stack) + } + fn decode_null(&mut self, _t: Tag) -> Result<(), Self::Error> { decode_jer_value!(Self::null_from_value, self.stack) } @@ -496,6 +505,35 @@ impl Decoder { .map_err(|_| DecodeError::integer_overflow(I::WIDTH, crate::Codec::Jer)) } + #[cfg(feature = "f64")] + fn real_from_value( + value: Value + ) -> Result { + if let Some(as_f64) = value.as_f64() { + return R::try_from_float(as_f64) + .ok_or_else(|| JerDecodeErrorKind::TypeMismatch { + needed: "number (double precision floating point)", + found: alloc::format!("{value}"), + }.into()); + } + + value + .as_str() + .and_then(|s| + match s { + "-0" => R::try_from_float(-0.0), + "INF" => R::try_from_float(f64::INFINITY), + "-INF" => R::try_from_float(f64::NEG_INFINITY), + "NAN" => R::try_from_float(f64::NAN), + _ => None + } + ) + .ok_or_else(|| JerDecodeErrorKind::TypeMismatch { + needed: "number (double precision floating point)", + found: alloc::format!("{value}"), + }.into()) + } + fn null_from_value(value: Value) -> Result<(), DecodeError> { Ok(value .is_null() diff --git a/src/jer/enc.rs b/src/jer/enc.rs index 8c94514b..8017dba9 100644 --- a/src/jer/enc.rs +++ b/src/jer/enc.rs @@ -11,6 +11,9 @@ use crate::{ types::{variants, Constraints, IntegerType, Tag}, }; +#[cfg(any(feature = "f32", feature = "f64"))] +use crate::types::RealType; + /// Encodes Rust structures into JSON Encoding Rules data. pub struct Encoder { stack: alloc::vec::Vec<&'static str>, @@ -149,6 +152,34 @@ impl crate::Encoder<'_> for Encoder { } } + #[cfg(feature = "f64")] + fn encode_real( + &mut self, + _t: Tag, + _c: Constraints, + value: &R, + ) -> Result { + use num_traits::{Float, ToPrimitive, Zero}; + + let as_float = value.try_to_float().ok_or(JerEncodeErrorKind::ExceedsSupportedRealRange)?; + + if as_float.is_infinite() { + if as_float.is_sign_positive() { + self.update_root_or_constructed(Value::String("INF".into())) + } else { + self.update_root_or_constructed(Value::String("-INF".into())) + } + } else if as_float.is_nan() { + self.update_root_or_constructed(Value::String("NAN".into())) + } else if as_float.is_zero() && as_float.is_sign_negative() { + self.update_root_or_constructed(Value::String("-0".into())) + } else if let Some(number) = as_float.to_f64().and_then(serde_json::Number::from_f64) { + self.update_root_or_constructed(number.into()) + } else { + Err(JerEncodeErrorKind::ExceedsSupportedRealRange.into()) + } + } + fn encode_null(&mut self, _: Tag) -> Result { self.update_root_or_constructed(Value::Null) } diff --git a/src/oer.rs b/src/oer.rs index 374e1e11..67825a94 100644 --- a/src/oer.rs +++ b/src/oer.rs @@ -129,6 +129,7 @@ mod tests { decode_ok!(oer, bool, &bytes, true); } } + #[test] fn test_length_determinant() { // short with leading zeros @@ -143,6 +144,21 @@ mod tests { &[0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x41, 0x41] ); } + + #[test] + #[cfg(feature = "f32")] + fn real_f32() { + round_trip!(oer, f32, 1.0.into(), &[0x3f, 0x80, 0x00, 0x00]); + round_trip!(oer, f32, (-1.0).into(), &[0xbf, 0x80, 0x00, 0x00]); + } + + #[test] + #[cfg(feature = "f64")] + fn real_f64() { + round_trip!(oer, f64, 1.0.into(), &[0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + round_trip!(oer, f64, (-1.0).into(), &[0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + } + #[test] fn test_sequence_of() { #[derive(AsnType, Decode, Encode, Debug, Clone, PartialEq)] @@ -160,6 +176,7 @@ mod tests { let data: [u8; 108] = [30; 108]; decode_error!(oer, TestA, &data); } + #[test] fn test_enumerated() { // short with leading zeros @@ -191,6 +208,7 @@ mod tests { decode_error!(coer, Test, &[0b1000_0001, 0x01]); decode_ok!(oer, Test, &[0b1000_0001, 0x01], Test::A); } + #[test] fn test_seq_preamble_unused_bits() { use crate as rasn; @@ -215,6 +233,7 @@ mod tests { &data ); } + #[test] fn test_explicit_with_optional() { #[derive(AsnType, Decode, Encode, Clone, Debug, PartialEq, Eq)] diff --git a/src/oer/de.rs b/src/oer/de.rs index d54c1f78..f9afb8e3 100644 --- a/src/oer/de.rs +++ b/src/oer/de.rs @@ -554,6 +554,20 @@ impl<'input, const RFC: usize, const EFC: usize> crate::Decoder for Decoder<'inp self.decode_integer_with_constraints::(&constraints) } + #[cfg(any(feature = "f32", feature = "f64"))] + fn decode_real( + &mut self, + _: Tag, + _: Constraints, + ) -> Result { + let octets = self.extract_data_by_length(R::BYTE_WIDTH)?; + R::try_from_ieee754_bytes(octets) + .map_err(|_| DecodeError::from_kind( + DecodeErrorKind::InvalidRealEncoding, + self.codec(), + )) + } + /// Null contains no data, so we just skip fn decode_null(&mut self, _: Tag) -> Result<(), Self::Error> { Ok(()) diff --git a/src/oer/enc.rs b/src/oer/enc.rs index e9883380..f0515d87 100644 --- a/src/oer/enc.rs +++ b/src/oer/enc.rs @@ -14,6 +14,9 @@ use crate::{ Codec, Encode, }; +#[cfg(any(feature = "f32", feature = "f64"))] +use crate::types::RealType; + /// ITU-T X.696 (02/2021) version of (C)OER encoding /// On this crate, only canonical version will be used to provide unique and reproducible encodings. /// Basic-OER is not supported and it might be that never will. @@ -750,6 +753,20 @@ impl<'buffer, const RFC: usize, const EFC: usize> crate::Encoder<'buffer> self.encode_integer_with_constraints(tag, &constraints, value) } + #[cfg(any(feature = "f32", feature = "f64"))] + fn encode_real( + &mut self, + tag: Tag, + _constraints: Constraints, + value: &R, + ) -> Result { + let (bytes, len) = value.to_ieee754_bytes(); + self.output.extend_from_slice(&bytes.as_ref()[..len]); + self.extend(tag)?; + + Ok(()) + } + fn encode_null(&mut self, _tag: Tag) -> Result { Ok(()) } diff --git a/src/per/de.rs b/src/per/de.rs index c7635cc6..950f2b35 100644 --- a/src/per/de.rs +++ b/src/per/de.rs @@ -688,6 +688,15 @@ impl<'input, const RFC: usize, const EFC: usize> crate::Decoder for Decoder<'inp self.parse_integer::(constraints) } + #[cfg(any(feature = "f32", feature = "f64"))] + fn decode_real( + &mut self, + _: Tag, + _: Constraints, + ) -> Result { + Err(DecodeError::real_not_supported(self.codec())) + } + fn decode_octet_string<'b, T: From<&'b [u8]> + From>>( &'b mut self, _: Tag, diff --git a/src/per/enc.rs b/src/per/enc.rs index 15bfdec0..39e72318 100644 --- a/src/per/enc.rs +++ b/src/per/enc.rs @@ -893,6 +893,16 @@ impl crate::Encoder<'_> for Encoder( + &mut self, + _: Tag, + _: Constraints, + _: &R, + ) -> Result { + Err(Error::real_not_supported(self.codec())) + } + fn encode_null(&mut self, _tag: Tag) -> Result { Ok(()) } diff --git a/src/types.rs b/src/types.rs index 7616bc15..252b2a5b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -17,6 +17,10 @@ pub(crate) mod constructed; pub(crate) mod date; pub(crate) mod integer; pub(crate) mod oid; + +#[cfg(any(feature = "f32", feature = "f64"))] +pub(crate) mod real; + pub(crate) mod strings; use crate::macros::{constraints, size_constraint, value_constraint}; @@ -42,6 +46,9 @@ pub use { rasn_derive::AsnType, }; +#[cfg(any(feature = "f32", feature = "f64"))] +pub use self::real::RealType; + /// The `UniversalString` type. pub type UniversalString = Implicit; /// The `UTCTime` type. @@ -320,3 +327,13 @@ impl AsnType for Any { const TAG: Tag = Tag::EOC; const TAG_TREE: TagTree = TagTree::Choice(&[]); } + +#[cfg(feature = "f32")] +impl AsnType for f32 { + const TAG: Tag = Tag::REAL; +} + +#[cfg(feature = "f64")] +impl AsnType for f64 { + const TAG: Tag = Tag::REAL; +} diff --git a/src/types/real.rs b/src/types/real.rs new file mode 100644 index 00000000..5b07e613 --- /dev/null +++ b/src/types/real.rs @@ -0,0 +1,81 @@ +/// Represents a real type in Rust that can be decoded or encoded into any +/// ASN.1 codec. +pub trait RealType: + Sized + + core::fmt::Debug + + core::fmt::Display +{ + /// The byte level width of the floating point type. + const BYTE_WIDTH: usize; + + /// Returns the IEEE 754 encoded bytes of the real type, byte count defined in `usize` + fn to_ieee754_bytes(&self) -> (impl AsRef<[u8]>, usize); + + /// Attempts to decode the IEEE 754 encoded bytes into `Self` + fn try_from_ieee754_bytes(bytes: &[u8]) -> Result; + + /// Attempts to convert a generic floating point type into `Self` + fn try_from_float(value: T) -> Option; + + /// Attempts to convert `Self` into a generic floating point type + fn try_to_float(&self) -> Option; +} + +#[cfg(feature = "f64")] +impl RealType for f64 { + const BYTE_WIDTH: usize = core::mem::size_of::(); + + #[inline] + fn to_ieee754_bytes(&self) -> (impl AsRef<[u8]>, usize) { + let bytes = self.to_be_bytes(); + (bytes, bytes.len()) + } + + #[inline] + fn try_from_ieee754_bytes(bytes: &[u8]) -> Result { + let bytes = bytes.try_into() + .map_err(|_| TryFromRealError::InvalidEncoding)?; + + Ok(f64::from_be_bytes(bytes)) + } + + fn try_from_float(value: T) -> Option { + value.to_f64() + } + + fn try_to_float(&self) -> Option { + Some(*self) + } +} + +#[cfg(feature = "f32")] +impl RealType for f32 { + const BYTE_WIDTH: usize = core::mem::size_of::(); + + #[inline] + fn to_ieee754_bytes(&self) -> (impl AsRef<[u8]>, usize) { + let bytes = self.to_be_bytes(); + (bytes, bytes.len()) + } + + #[inline] + fn try_from_ieee754_bytes(bytes: &[u8]) -> Result { + let bytes = bytes.try_into() + .map_err(|_| TryFromRealError::InvalidEncoding)?; + + Ok(f32::from_be_bytes(bytes)) + } + + fn try_from_float(value: T) -> Option { + value.to_f32() + } + + fn try_to_float(&self) -> Option { + Some(*self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TryFromRealError { + InvalidEncoding, +}