Skip to main content

mint_core/layout/
conversions.rs

1use super::error::LayoutError;
2use super::scalar_type::{FixedPointType, ScalarType};
3use super::settings::{EndianBytes, Endianness};
4use super::value::DataValue;
5use std::fmt;
6
7macro_rules! impl_saturating_unsigned_try_from_data_value {
8    ($($t:ty),* $(,)?) => {$(
9        impl TryFrom<&DataValue> for $t {
10            type Error = LayoutError;
11            fn try_from(value: &DataValue) -> Result<Self, LayoutError> {
12                match value {
13                    DataValue::Bool(val) => {
14                        let n: u8 = if *val { 1 } else { 0 };
15                        Ok(n as $t)
16                    }
17                    DataValue::U64(val) => Ok((*val).min(<$t>::MAX as u64) as $t),
18                    DataValue::I64(val) => {
19                        if *val < 0 {
20                            Ok(<$t>::MIN)
21                        } else {
22                            Ok((*val as u64).min(<$t>::MAX as u64) as $t)
23                        }
24                    }
25                    DataValue::F64(val) => Ok(*val as $t),
26                    DataValue::Str(_) => {
27                        return Err(LayoutError::DataValueExportFailed(
28                            "Cannot convert string to scalar type.".to_owned(),
29                        ));
30                    }
31                }
32            }
33        }
34    )* }; }
35
36macro_rules! impl_saturating_signed_try_from_data_value {
37    ($($t:ty),* $(,)?) => {$(
38        impl TryFrom<&DataValue> for $t {
39            type Error = LayoutError;
40            fn try_from(value: &DataValue) -> Result<Self, LayoutError> {
41                match value {
42                    DataValue::Bool(val) => {
43                        let n: u8 = if *val { 1 } else { 0 };
44                        Ok(n as $t)
45                    }
46                    DataValue::U64(val) => Ok((*val).min(<$t>::MAX as u64) as $t),
47                    DataValue::I64(val) => {
48                        let clamped = i128::from(*val).clamp(<$t>::MIN as i128, <$t>::MAX as i128);
49                        Ok(clamped as $t)
50                    }
51                    DataValue::F64(val) => Ok(*val as $t),
52                    DataValue::Str(_) => {
53                        return Err(LayoutError::DataValueExportFailed(
54                            "Cannot convert string to scalar type.".to_owned(),
55                        ));
56                    }
57                }
58            }
59        }
60    )* }; }
61
62macro_rules! impl_float_try_from_data_value {
63    ($($t:ty),* $(,)?) => {$(
64        impl TryFrom<&DataValue> for $t {
65            type Error = LayoutError;
66            fn try_from(value: &DataValue) -> Result<Self, LayoutError> {
67                match value {
68                    DataValue::Bool(val) => {
69                        let n: u8 = if *val { 1 } else { 0 };
70                        Ok(n as $t)
71                    }
72                    DataValue::U64(val) => Ok(*val as $t),
73                    DataValue::I64(val) => Ok(*val as $t),
74                    DataValue::F64(val) => Ok(*val as $t),
75                    DataValue::Str(_) => {
76                        return Err(LayoutError::DataValueExportFailed(
77                            "Cannot convert string to scalar type.".to_owned(),
78                        ));
79                    }
80                }
81            }
82        }
83    )* }; }
84
85impl_saturating_unsigned_try_from_data_value!(u8, u16, u32, u64);
86impl_saturating_signed_try_from_data_value!(i8, i16, i32, i64, i128);
87impl_float_try_from_data_value!(f32, f64);
88
89pub trait TryFromStrict<T>: Sized {
90    fn try_from_strict(value: T) -> Result<Self, LayoutError>;
91}
92
93macro_rules! err {
94    ($msg:expr) => {
95        LayoutError::DataValueExportFailed($msg.to_owned())
96    };
97}
98
99macro_rules! impl_try_from_strict_unsigned {
100    ($($t:ty),* $(,)?) => {$(
101        impl TryFromStrict<&DataValue> for $t {
102            fn try_from_strict(value: &DataValue) -> Result<Self, LayoutError> {
103                match value {
104                    DataValue::U64(v) => <Self as TryFrom<u64>>::try_from(*v)
105                        .map_err(|_| err!(format!("u64 value {} out of range for {}", v, stringify!($t)))),
106                    DataValue::I64(v) => {
107                        if *v < 0 { return Err(err!("negative integer cannot convert to unsigned in strict mode")); }
108                        <Self as TryFrom<u64>>::try_from(*v as u64)
109                            .map_err(|_| err!(format!("i64 value {} out of range for {}", v, stringify!($t))))
110                    }
111                    DataValue::F64(v) => {
112                        if !v.is_finite() { return Err(err!("non-finite float cannot convert to integer in strict mode")); }
113                        if v.fract() != 0.0 { return Err(err!("float to integer conversion not allowed unless value is an exact integer")); }
114                        if *v < 0.0 || *v > (<$t>::MAX as f64) { return Err(err!(format!("float value {} out of range for {}", v, stringify!($t)))); }
115                        Ok(*v as $t)
116                    }
117                    DataValue::Bool(b) => {
118                        let n: u8 = if *b { 1 } else { 0 };
119                        Ok(n as $t)
120                    }
121                    DataValue::Str(_) => Err(err!("Cannot convert string to scalar type.")),
122                }
123            }
124        }
125    )*};
126}
127
128macro_rules! impl_try_from_strict_signed {
129    ($($t:ty),* $(,)?) => {$(
130        impl TryFromStrict<&DataValue> for $t {
131            fn try_from_strict(value: &DataValue) -> Result<Self, LayoutError> {
132                match value {
133                    DataValue::U64(v) => {
134                        <Self as TryFrom<i128>>::try_from(*v as i128)
135                            .map_err(|_| err!(format!("u64 value {} out of range for {}", v, stringify!($t))))
136                    }
137                    DataValue::I64(v) => <Self as TryFrom<i64>>::try_from(*v)
138                        .map_err(|_| err!(format!("i64 value {} out of range for {}", v, stringify!($t)))),
139                    DataValue::F64(v) => {
140                        if !v.is_finite() { return Err(err!("non-finite float cannot convert to integer in strict mode")); }
141                        if v.fract() != 0.0 { return Err(err!("float to integer conversion not allowed unless value is an exact integer")); }
142                        if *v < (<$t>::MIN as f64) || *v > (<$t>::MAX as f64) { return Err(err!(format!("float value {} out of range for {}", v, stringify!($t)))); }
143                        Ok(*v as $t)
144                    }
145                    DataValue::Bool(b) => {
146                        let n: u8 = if *b { 1 } else { 0 };
147                        Ok(n as $t)
148                    }
149                    DataValue::Str(_) => Err(err!("Cannot convert string to scalar type.")),
150                }
151            }
152        }
153    )*};
154}
155
156macro_rules! impl_try_from_strict_float_targets {
157    ($t:ty) => {
158        impl TryFromStrict<&DataValue> for $t {
159            fn try_from_strict(value: &DataValue) -> Result<Self, LayoutError> {
160                match value {
161                    DataValue::F64(v) => {
162                        if !v.is_finite() {
163                            return Err(err!("non-finite float not allowed in strict mode"));
164                        }
165                        let out = *v as $t;
166                        if out.is_finite() {
167                            Ok(out)
168                        } else {
169                            Err(err!(format!(
170                                "float value {} out of range for {}",
171                                v,
172                                stringify!($t)
173                            )))
174                        }
175                    }
176                    DataValue::U64(v) => {
177                        let out = (*v as $t);
178                        if !out.is_finite() {
179                            return Err(err!("integer to float produced non-finite value"));
180                        }
181                        // exactness check via round-trip
182                        if (out as u64) == *v {
183                            Ok(out)
184                        } else {
185                            Err(err!(
186                                "lossy integer to float conversion not allowed in strict mode"
187                            ))
188                        }
189                    }
190                    DataValue::I64(v) => {
191                        let out = (*v as $t);
192                        if !out.is_finite() {
193                            return Err(err!("integer to float produced non-finite value"));
194                        }
195                        if (out as i64) == *v {
196                            Ok(out)
197                        } else {
198                            Err(err!(
199                                "lossy integer to float conversion not allowed in strict mode"
200                            ))
201                        }
202                    }
203                    DataValue::Bool(b) => {
204                        let out: $t = if *b { 1.0 } else { 0.0 };
205                        Ok(out)
206                    }
207                    DataValue::Str(_) => Err(err!("Cannot convert string to scalar type.")),
208                }
209            }
210        }
211    };
212}
213
214impl_try_from_strict_unsigned!(u8, u16, u32, u64);
215impl_try_from_strict_signed!(i8, i16, i32, i64, i128);
216impl_try_from_strict_float_targets!(f32);
217impl TryFromStrict<&DataValue> for f64 {
218    fn try_from_strict(value: &DataValue) -> Result<Self, LayoutError> {
219        match value {
220            DataValue::F64(v) => Ok(*v),
221            DataValue::U64(v) => {
222                let out = *v as f64;
223                if (out as u64) == *v {
224                    Ok(out)
225                } else {
226                    Err(err!(
227                        "lossy integer to float conversion not allowed in strict mode"
228                    ))
229                }
230            }
231            DataValue::I64(v) => {
232                let out = *v as f64;
233                if (out as i64) == *v {
234                    Ok(out)
235                } else {
236                    Err(err!(
237                        "lossy integer to float conversion not allowed in strict mode"
238                    ))
239                }
240            }
241            DataValue::Bool(b) => {
242                let out = if *b { 1.0 } else { 0.0 };
243                Ok(out)
244            }
245            DataValue::Str(_) => Err(err!("Cannot convert string to scalar type.")),
246        }
247    }
248}
249
250/// Converts a DataValue to an i128 for bitfield packing, with range clamping/checking.
251///
252/// - `bits`: field width in bits (must be > 0)
253/// - `signed`: whether to interpret as two's complement signed field
254/// - `strict`: if true, out-of-range or non-integer floats produce errors; otherwise saturate
255pub fn clamp_bitfield_value(
256    value: &DataValue,
257    bits: usize,
258    signed: bool,
259    strict: bool,
260) -> Result<i128, LayoutError> {
261    let raw: i128 = if strict {
262        i128::try_from_strict(value)?
263    } else {
264        i128::try_from(value)?
265    };
266
267    let (min, max) = if signed {
268        let half = 1i128 << (bits - 1);
269        (-half, half - 1)
270    } else {
271        (0, (1i128 << bits) - 1)
272    };
273
274    if strict && (raw < min || raw > max) {
275        return Err(LayoutError::BitfieldOutOfRange {
276            value: raw,
277            bits,
278            signedness: if signed { "signed" } else { "unsigned" },
279            min,
280            max,
281        });
282    }
283    Ok(raw.clamp(min, max))
284}
285
286fn data_value_display(value: &DataValue) -> String {
287    match value {
288        DataValue::Bool(value) => value.to_string(),
289        DataValue::U64(value) => value.to_string(),
290        DataValue::I64(value) => value.to_string(),
291        DataValue::F64(value) => value.to_string(),
292        DataValue::Str(value) => format!("{value:?}"),
293    }
294}
295
296fn fixed_point_overflow_error(
297    fixed: FixedPointType,
298    value: &DataValue,
299    scaled: impl fmt::Display,
300) -> LayoutError {
301    LayoutError::DataValueExportFailed(format!(
302        "fixed-point type '{}' overflows {} for value {} (scaled to {})",
303        fixed,
304        fixed.storage_label(),
305        data_value_display(value),
306        scaled
307    ))
308}
309
310fn fixed_point_non_finite_error(fixed: FixedPointType, value: &DataValue) -> LayoutError {
311    LayoutError::DataValueExportFailed(format!(
312        "fixed-point type '{}' cannot encode non-finite value {}",
313        fixed,
314        data_value_display(value)
315    ))
316}
317
318fn encode_fixed_point_bytes(
319    value: &DataValue,
320    fixed: FixedPointType,
321    endianness: &Endianness,
322    strict: bool,
323) -> Result<Vec<u8>, LayoutError> {
324    let encoded = encode_fixed_point_value(value, fixed, strict)?;
325    encode_integer_bytes(encoded, fixed, endianness)
326}
327
328fn encode_fixed_point_value(
329    value: &DataValue,
330    fixed: FixedPointType,
331    strict: bool,
332) -> Result<i128, LayoutError> {
333    let (min, max) = fixed.encoded_bounds();
334    let scale = 1i128 << fixed.fractional_bits;
335
336    let encoded = match value {
337        DataValue::Bool(raw) => clamp_fixed_point_integer(
338            if *raw { 1 } else { 0 },
339            scale,
340            min,
341            max,
342            fixed,
343            value,
344            strict,
345        )?,
346        DataValue::U64(raw) => {
347            clamp_fixed_point_integer(i128::from(*raw), scale, min, max, fixed, value, strict)?
348        }
349        DataValue::I64(raw) => {
350            clamp_fixed_point_integer(i128::from(*raw), scale, min, max, fixed, value, strict)?
351        }
352        DataValue::F64(raw) => clamp_fixed_point_float(*raw, min, max, fixed, strict, value)?,
353        DataValue::Str(_) => {
354            return Err(LayoutError::DataValueExportFailed(
355                "Cannot convert string to scalar type.".to_owned(),
356            ));
357        }
358    };
359
360    Ok(encoded)
361}
362
363fn clamp_fixed_point_integer(
364    raw: i128,
365    scale: i128,
366    min: i128,
367    max: i128,
368    fixed: FixedPointType,
369    original: &DataValue,
370    strict: bool,
371) -> Result<i128, LayoutError> {
372    let Some(scaled) = raw.checked_mul(scale) else {
373        if strict {
374            return Err(fixed_point_overflow_error(
375                fixed,
376                original,
377                "integer scaling overflow",
378            ));
379        }
380        return Ok(if raw.is_negative() { min } else { max });
381    };
382
383    if strict && (scaled < min || scaled > max) {
384        return Err(fixed_point_overflow_error(fixed, original, scaled));
385    }
386
387    Ok(scaled.clamp(min, max))
388}
389
390fn clamp_fixed_point_float(
391    raw: f64,
392    min: i128,
393    max: i128,
394    fixed: FixedPointType,
395    strict: bool,
396    original: &DataValue,
397) -> Result<i128, LayoutError> {
398    if !raw.is_finite() {
399        return Err(fixed_point_non_finite_error(fixed, original));
400    }
401
402    let scaled = raw * (2f64).powi(i32::from(fixed.fractional_bits));
403    if !scaled.is_finite() {
404        if strict {
405            return Err(fixed_point_overflow_error(fixed, original, scaled));
406        }
407        return Ok(if scaled.is_sign_negative() { min } else { max });
408    }
409
410    let rounded = scaled.round_ties_even();
411    let rounded_int = rounded as i128;
412    if rounded_int < min {
413        if strict {
414            return Err(fixed_point_overflow_error(fixed, original, rounded));
415        }
416        return Ok(min);
417    }
418    if rounded_int > max {
419        if strict {
420            return Err(fixed_point_overflow_error(fixed, original, rounded));
421        }
422        return Ok(max);
423    }
424
425    Ok(rounded_int)
426}
427
428fn encode_integer_bytes(
429    encoded: i128,
430    fixed: FixedPointType,
431    endianness: &Endianness,
432) -> Result<Vec<u8>, LayoutError> {
433    match (fixed.signed, fixed.total_bits) {
434        (false, 8) => Ok((encoded as u8).to_endian_bytes(endianness)),
435        (false, 16) => Ok((encoded as u16).to_endian_bytes(endianness)),
436        (false, 32) => Ok((encoded as u32).to_endian_bytes(endianness)),
437        (false, 64) => Ok(u64::try_from(encoded)
438            .map_err(|_| {
439                LayoutError::DataValueExportFailed(format!(
440                    "fixed-point type '{}' encoded value {} could not be written as u64",
441                    fixed, encoded
442                ))
443            })?
444            .to_endian_bytes(endianness)),
445        (true, 8) => Ok((encoded as i8).to_endian_bytes(endianness)),
446        (true, 16) => Ok((encoded as i16).to_endian_bytes(endianness)),
447        (true, 32) => Ok((encoded as i32).to_endian_bytes(endianness)),
448        (true, 64) => Ok(i64::try_from(encoded)
449            .map_err(|_| {
450                LayoutError::DataValueExportFailed(format!(
451                    "fixed-point type '{}' encoded value {} could not be written as i64",
452                    fixed, encoded
453                ))
454            })?
455            .to_endian_bytes(endianness)),
456        _ => Err(LayoutError::DataValueExportFailed(format!(
457            "unsupported fixed-point width '{}'",
458            fixed
459        ))),
460    }
461}
462
463pub fn convert_value_to_bytes(
464    value: &DataValue,
465    scalar_type: ScalarType,
466    endianness: &Endianness,
467    strict: bool,
468) -> Result<Vec<u8>, LayoutError> {
469    macro_rules! to_bytes {
470        ($t:ty) => {{
471            let val: $t = if strict {
472                <$t as TryFromStrict<&DataValue>>::try_from_strict(value)?
473            } else {
474                <$t as TryFrom<&DataValue>>::try_from(value)?
475            };
476            Ok(val.to_endian_bytes(endianness))
477        }};
478    }
479
480    match scalar_type {
481        ScalarType::U8 => to_bytes!(u8),
482        ScalarType::I8 => to_bytes!(i8),
483        ScalarType::U16 => to_bytes!(u16),
484        ScalarType::I16 => to_bytes!(i16),
485        ScalarType::U32 => to_bytes!(u32),
486        ScalarType::I32 => to_bytes!(i32),
487        ScalarType::U64 => to_bytes!(u64),
488        ScalarType::I64 => to_bytes!(i64),
489        ScalarType::F32 => to_bytes!(f32),
490        ScalarType::F64 => to_bytes!(f64),
491        ScalarType::Fixed(fixed) => encode_fixed_point_bytes(value, fixed, endianness, strict),
492    }
493}