Skip to main content

fit/
raw_value.rs

1//! Raw (untransformed) field values produced by the M4 decoder.
2//!
3//! Each base type has **two** variants: a `…Scalar(T)` for the common case of
4//! a length-1 field (~95% of FIT fields) and a `…Array(Box<[T]>)` for the rare
5//! multi-element case. Scalar variants are stack-only — no heap allocation per
6//! decoded message — while Array variants box the slice so the enum payload
7//! stays compact (16B + tag) regardless of cardinality.
8//!
9//! Invalid detection still applies the rule "every element matches the
10//! sentinel" (per protocol §3.1) but is checked before construction so the
11//! caller never sees an `Invalid` array.
12//!
13//! Notable per-type rules:
14//! - **Z series** (`U8z` / `U16z` / `U32z` / `U64z`): invalid sentinel is `0`.
15//! - **`Byte`**: invalid only when *every* byte is `0xFF`.
16//! - **`String`**: invalid if first byte is `0x00` or empty after
17//!   null-stripping. Decoded with `from_utf8_lossy`.
18//! - **`Float32/64`**: invalid is the all-ones bit pattern.
19
20use crate::base_type::BaseType;
21use crate::error::FitError;
22use crate::stream::Endian;
23
24/// A decoded field value.
25#[derive(Debug, Clone, PartialEq)]
26pub enum RawValue {
27    /// All elements matched the type's invalid sentinel.
28    Invalid,
29
30    // ── Scalars (stack-only) ───────────────────────────────────────────
31    EnumScalar(u8),
32    U8Scalar(u8),
33    U8zScalar(u8),
34    I8Scalar(i8),
35    U16Scalar(u16),
36    U16zScalar(u16),
37    I16Scalar(i16),
38    U32Scalar(u32),
39    U32zScalar(u32),
40    I32Scalar(i32),
41    U64Scalar(u64),
42    U64zScalar(u64),
43    I64Scalar(i64),
44    F32Scalar(f32),
45    F64Scalar(f64),
46
47    // ── Arrays (heap-boxed, length ≥ 2) ────────────────────────────────
48    EnumArray(Box<[u8]>),
49    U8Array(Box<[u8]>),
50    U8zArray(Box<[u8]>),
51    I8Array(Box<[i8]>),
52    U16Array(Box<[u16]>),
53    U16zArray(Box<[u16]>),
54    I16Array(Box<[i16]>),
55    U32Array(Box<[u32]>),
56    U32zArray(Box<[u32]>),
57    I32Array(Box<[i32]>),
58    U64Array(Box<[u64]>),
59    U64zArray(Box<[u64]>),
60    I64Array(Box<[i64]>),
61    F32Array(Box<[f32]>),
62    F64Array(Box<[f64]>),
63
64    /// UTF-8 string with the trailing `0x00` (and any padding) stripped.
65    String(Box<str>),
66    /// Opaque byte array (single bytes that are not invalid sentinels also
67    /// land here — there is no `ByteScalar` variant since `Byte` is by
68    /// definition an array type on the wire).
69    Byte(Box<[u8]>),
70}
71
72impl RawValue {
73    /// True iff the field collapsed to the invalid sentinel.
74    #[inline]
75    pub fn is_invalid(&self) -> bool {
76        matches!(self, RawValue::Invalid)
77    }
78
79    /// Borrow the underlying string.
80    pub fn as_str(&self) -> Option<&str> {
81        match self {
82            RawValue::String(s) => Some(s.as_ref()),
83            _ => None,
84        }
85    }
86
87    /// Get a scalar `u8` for length-1 single-byte fields.
88    pub fn as_u8(&self) -> Option<u8> {
89        match *self {
90            RawValue::U8Scalar(v) | RawValue::U8zScalar(v) | RawValue::EnumScalar(v) => Some(v),
91            RawValue::Byte(ref b) if b.len() == 1 => Some(b[0]),
92            _ => None,
93        }
94    }
95
96    /// Get a scalar `u16` (also widens single-byte fields).
97    pub fn as_u16(&self) -> Option<u16> {
98        match *self {
99            RawValue::U16Scalar(v) | RawValue::U16zScalar(v) => Some(v),
100            RawValue::U8Scalar(v) | RawValue::U8zScalar(v) | RawValue::EnumScalar(v) => {
101                Some(v as u16)
102            }
103            _ => None,
104        }
105    }
106
107    /// Get a scalar `u32` (also accepts `U32z`).
108    pub fn as_u32(&self) -> Option<u32> {
109        match *self {
110            RawValue::U32Scalar(v) | RawValue::U32zScalar(v) => Some(v),
111            _ => None,
112        }
113    }
114
115    /// Best-effort cast of any single-element numeric value to `u64`.
116    /// Signed types are widened via two's complement (matches the prior
117    /// `components::scalar_as_u64` behavior).
118    pub fn scalar_u64(&self) -> Option<u64> {
119        match *self {
120            RawValue::EnumScalar(v) | RawValue::U8Scalar(v) | RawValue::U8zScalar(v) => {
121                Some(v as u64)
122            }
123            RawValue::U16Scalar(v) | RawValue::U16zScalar(v) => Some(v as u64),
124            RawValue::U32Scalar(v) | RawValue::U32zScalar(v) => Some(v as u64),
125            RawValue::U64Scalar(v) | RawValue::U64zScalar(v) => Some(v),
126            RawValue::I8Scalar(v) => Some(v as u8 as u64),
127            RawValue::I16Scalar(v) => Some(v as u16 as u64),
128            RawValue::I32Scalar(v) => Some(v as u32 as u64),
129            RawValue::I64Scalar(v) => Some(v as u64),
130            RawValue::Byte(ref b) if b.len() == 1 => Some(b[0] as u64),
131            _ => None,
132        }
133    }
134
135    /// Best-effort cast of any single-element numeric value to `f64`.
136    pub fn scalar_f64(&self) -> Option<f64> {
137        if let Some(u) = self.scalar_u64() {
138            return Some(u as f64);
139        }
140        match *self {
141            RawValue::F32Scalar(v) => Some(v as f64),
142            RawValue::F64Scalar(v) => Some(v),
143            _ => None,
144        }
145    }
146
147    /// Iterate any numeric variant (scalar or array) as `f64`s. Returns
148    /// `None` for non-numeric types (`String`, `Invalid`).
149    pub fn to_f64s(&self) -> Option<Vec<f64>> {
150        use RawValue::*;
151        Some(match self {
152            EnumScalar(v) | U8Scalar(v) | U8zScalar(v) => vec![*v as f64],
153            U16Scalar(v) | U16zScalar(v) => vec![*v as f64],
154            U32Scalar(v) | U32zScalar(v) => vec![*v as f64],
155            U64Scalar(v) | U64zScalar(v) => vec![*v as f64],
156            I8Scalar(v) => vec![*v as f64],
157            I16Scalar(v) => vec![*v as f64],
158            I32Scalar(v) => vec![*v as f64],
159            I64Scalar(v) => vec![*v as f64],
160            F32Scalar(v) => vec![*v as f64],
161            F64Scalar(v) => vec![*v],
162            EnumArray(a) | U8Array(a) | U8zArray(a) | Byte(a) => {
163                a.iter().map(|&x| x as f64).collect()
164            }
165            U16Array(a) | U16zArray(a) => a.iter().map(|&x| x as f64).collect(),
166            U32Array(a) | U32zArray(a) => a.iter().map(|&x| x as f64).collect(),
167            U64Array(a) | U64zArray(a) => a.iter().map(|&x| x as f64).collect(),
168            I8Array(a) => a.iter().map(|&x| x as f64).collect(),
169            I16Array(a) => a.iter().map(|&x| x as f64).collect(),
170            I32Array(a) => a.iter().map(|&x| x as f64).collect(),
171            I64Array(a) => a.iter().map(|&x| x as f64).collect(),
172            F32Array(a) => a.iter().map(|&x| x as f64).collect(),
173            F64Array(a) => a.to_vec(),
174            _ => return None,
175        })
176    }
177}
178
179// ───────────────────────────────────────────────────────────────────
180// Decode entry point.
181// ───────────────────────────────────────────────────────────────────
182
183/// Decode a single field's bytes into a [`RawValue`].
184///
185/// `raw` must be exactly the field's declared wire size. Returns
186/// [`FitError::MalformedField`] when a multi-byte type's size is not a
187/// multiple of its element stride.
188pub(crate) fn decode_value(
189    base_type: BaseType,
190    raw: &[u8],
191    endian: Endian,
192    field_def_num: u8,
193) -> Result<RawValue, FitError> {
194    let stride = base_type.element_size();
195    if !base_type.is_string() && !base_type.is_byte() && (raw.is_empty() || raw.len() % stride != 0)
196    {
197        return Err(FitError::MalformedField {
198            field_def_num,
199            size: raw.len() as u8,
200            element_size: stride,
201        });
202    }
203
204    Ok(match base_type {
205        BaseType::Enum => collapse(
206            decode_u8_iter(raw),
207            |&v| v == 0xFF,
208            RawValue::EnumScalar,
209            RawValue::EnumArray,
210        ),
211        BaseType::UInt8 => collapse(
212            decode_u8_iter(raw),
213            |&v| v == 0xFF,
214            RawValue::U8Scalar,
215            RawValue::U8Array,
216        ),
217        BaseType::UInt8z => collapse(
218            decode_u8_iter(raw),
219            |&v| v == 0x00,
220            RawValue::U8zScalar,
221            RawValue::U8zArray,
222        ),
223        BaseType::SInt8 => collapse(
224            raw.iter().map(|&b| b as i8),
225            |&v| v == i8::MAX,
226            RawValue::I8Scalar,
227            RawValue::I8Array,
228        ),
229        BaseType::Byte => decode_byte(raw),
230        BaseType::String => decode_string(raw),
231
232        BaseType::UInt16 => collapse(
233            decode_u16_iter(raw, endian),
234            |&v| v == u16::MAX,
235            RawValue::U16Scalar,
236            RawValue::U16Array,
237        ),
238        BaseType::UInt16z => collapse(
239            decode_u16_iter(raw, endian),
240            |&v| v == 0,
241            RawValue::U16zScalar,
242            RawValue::U16zArray,
243        ),
244        BaseType::SInt16 => collapse(
245            decode_i16_iter(raw, endian),
246            |&v| v == i16::MAX,
247            RawValue::I16Scalar,
248            RawValue::I16Array,
249        ),
250
251        BaseType::UInt32 => collapse(
252            decode_u32_iter(raw, endian),
253            |&v| v == u32::MAX,
254            RawValue::U32Scalar,
255            RawValue::U32Array,
256        ),
257        BaseType::UInt32z => collapse(
258            decode_u32_iter(raw, endian),
259            |&v| v == 0,
260            RawValue::U32zScalar,
261            RawValue::U32zArray,
262        ),
263        BaseType::SInt32 => collapse(
264            decode_i32_iter(raw, endian),
265            |&v| v == i32::MAX,
266            RawValue::I32Scalar,
267            RawValue::I32Array,
268        ),
269
270        BaseType::UInt64 => collapse(
271            decode_u64_iter(raw, endian),
272            |&v| v == u64::MAX,
273            RawValue::U64Scalar,
274            RawValue::U64Array,
275        ),
276        BaseType::UInt64z => collapse(
277            decode_u64_iter(raw, endian),
278            |&v| v == 0,
279            RawValue::U64zScalar,
280            RawValue::U64zArray,
281        ),
282        BaseType::SInt64 => collapse(
283            decode_i64_iter(raw, endian),
284            |&v| v == i64::MAX,
285            RawValue::I64Scalar,
286            RawValue::I64Array,
287        ),
288
289        BaseType::Float32 => collapse(
290            decode_f32_iter(raw, endian),
291            |v| v.to_bits() == 0xFFFF_FFFF,
292            RawValue::F32Scalar,
293            RawValue::F32Array,
294        ),
295        BaseType::Float64 => collapse(
296            decode_f64_iter(raw, endian),
297            |v| v.to_bits() == 0xFFFF_FFFF_FFFF_FFFF,
298            RawValue::F64Scalar,
299            RawValue::F64Array,
300        ),
301    })
302}
303
304// ───────────────────────────────────────────────────────────────────
305// Per-type element iterators. Returning iterators (not Vec) lets
306// `collapse` pick Scalar without ever allocating in the hot path.
307// ───────────────────────────────────────────────────────────────────
308
309fn decode_u8_iter(raw: &[u8]) -> impl Iterator<Item = u8> + '_ {
310    raw.iter().copied()
311}
312
313fn decode_u16_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = u16> + '_ {
314    raw.chunks_exact(2).map(move |c| {
315        let arr = [c[0], c[1]];
316        match endian {
317            Endian::Little => u16::from_le_bytes(arr),
318            Endian::Big => u16::from_be_bytes(arr),
319        }
320    })
321}
322
323fn decode_i16_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = i16> + '_ {
324    raw.chunks_exact(2).map(move |c| {
325        let arr = [c[0], c[1]];
326        match endian {
327            Endian::Little => i16::from_le_bytes(arr),
328            Endian::Big => i16::from_be_bytes(arr),
329        }
330    })
331}
332
333fn decode_u32_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = u32> + '_ {
334    raw.chunks_exact(4).map(move |c| {
335        let arr = [c[0], c[1], c[2], c[3]];
336        match endian {
337            Endian::Little => u32::from_le_bytes(arr),
338            Endian::Big => u32::from_be_bytes(arr),
339        }
340    })
341}
342
343fn decode_i32_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = i32> + '_ {
344    raw.chunks_exact(4).map(move |c| {
345        let arr = [c[0], c[1], c[2], c[3]];
346        match endian {
347            Endian::Little => i32::from_le_bytes(arr),
348            Endian::Big => i32::from_be_bytes(arr),
349        }
350    })
351}
352
353fn decode_u64_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = u64> + '_ {
354    raw.chunks_exact(8).map(move |c| {
355        let arr = [c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]];
356        match endian {
357            Endian::Little => u64::from_le_bytes(arr),
358            Endian::Big => u64::from_be_bytes(arr),
359        }
360    })
361}
362
363fn decode_i64_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = i64> + '_ {
364    raw.chunks_exact(8).map(move |c| {
365        let arr = [c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]];
366        match endian {
367            Endian::Little => i64::from_le_bytes(arr),
368            Endian::Big => i64::from_be_bytes(arr),
369        }
370    })
371}
372
373fn decode_f32_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = f32> + '_ {
374    raw.chunks_exact(4).map(move |c| {
375        let arr = [c[0], c[1], c[2], c[3]];
376        match endian {
377            Endian::Little => f32::from_le_bytes(arr),
378            Endian::Big => f32::from_be_bytes(arr),
379        }
380    })
381}
382
383fn decode_f64_iter(raw: &[u8], endian: Endian) -> impl Iterator<Item = f64> + '_ {
384    raw.chunks_exact(8).map(move |c| {
385        let arr = [c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]];
386        match endian {
387            Endian::Little => f64::from_le_bytes(arr),
388            Endian::Big => f64::from_be_bytes(arr),
389        }
390    })
391}
392
393/// Drive the decoded element iterator and pick the right enum shape:
394/// - all elements invalid → `RawValue::Invalid`
395/// - exactly one element → `scalar(value)`
396/// - many elements → `array(Box<[T]>)`
397fn collapse<T, I, F, S, A>(iter: I, is_invalid: F, scalar: S, array: A) -> RawValue
398where
399    T: Copy,
400    I: Iterator<Item = T>,
401    F: Fn(&T) -> bool,
402    S: FnOnce(T) -> RawValue,
403    A: FnOnce(Box<[T]>) -> RawValue,
404{
405    let collected: Vec<T> = iter.collect();
406    if collected.is_empty() || collected.iter().all(is_invalid) {
407        return RawValue::Invalid;
408    }
409    if collected.len() == 1 {
410        scalar(collected[0])
411    } else {
412        array(collected.into_boxed_slice())
413    }
414}
415
416fn decode_byte(raw: &[u8]) -> RawValue {
417    if !raw.is_empty() && raw.iter().all(|&b| b == 0xFF) {
418        RawValue::Invalid
419    } else {
420        RawValue::Byte(raw.to_vec().into_boxed_slice())
421    }
422}
423
424fn decode_string(raw: &[u8]) -> RawValue {
425    let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
426    if end == 0 {
427        return RawValue::Invalid;
428    }
429    let s = String::from_utf8_lossy(&raw[..end]).into_owned();
430    RawValue::String(s.into_boxed_str())
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    fn dec(bt: BaseType, raw: &[u8], endian: Endian) -> RawValue {
438        decode_value(bt, raw, endian, 0).unwrap()
439    }
440
441    #[test]
442    fn enum_invalid_when_all_ff() {
443        assert_eq!(
444            dec(BaseType::Enum, &[0xFF], Endian::Little),
445            RawValue::Invalid
446        );
447        assert_eq!(
448            dec(BaseType::Enum, &[0xFF, 0xFF], Endian::Little),
449            RawValue::Invalid
450        );
451    }
452
453    #[test]
454    fn enum_valid_keeps_all_elements() {
455        assert_eq!(
456            dec(BaseType::Enum, &[1, 0xFF, 4], Endian::Little),
457            RawValue::EnumArray(vec![1u8, 0xFF, 4].into_boxed_slice())
458        );
459    }
460
461    #[test]
462    fn uint8z_invalid_is_zero_not_ff() {
463        assert_eq!(
464            dec(BaseType::UInt8z, &[0], Endian::Little),
465            RawValue::Invalid
466        );
467        assert_eq!(
468            dec(BaseType::UInt8z, &[0xFF], Endian::Little),
469            RawValue::U8zScalar(0xFF)
470        );
471    }
472
473    #[test]
474    fn byte_invalid_only_when_all_ff() {
475        assert_eq!(
476            dec(BaseType::Byte, &[0xFF], Endian::Little),
477            RawValue::Invalid
478        );
479        assert_eq!(
480            dec(BaseType::Byte, &[0xFF, 0x01, 0xFF], Endian::Little),
481            RawValue::Byte(vec![0xFFu8, 0x01, 0xFF].into_boxed_slice())
482        );
483        assert_eq!(
484            dec(BaseType::Byte, &[0xFF, 0xFF, 0xFF], Endian::Little),
485            RawValue::Invalid
486        );
487    }
488
489    #[test]
490    fn uint16_endianness_and_invalid() {
491        assert_eq!(
492            dec(BaseType::UInt16, &[0x34, 0x12], Endian::Little),
493            RawValue::U16Scalar(0x1234)
494        );
495        assert_eq!(
496            dec(BaseType::UInt16, &[0x12, 0x34], Endian::Big),
497            RawValue::U16Scalar(0x1234)
498        );
499        assert_eq!(
500            dec(BaseType::UInt16, &[0xFF, 0xFF], Endian::Little),
501            RawValue::Invalid
502        );
503    }
504
505    #[test]
506    fn uint32_le_decodes_known_timestamp() {
507        assert_eq!(
508            dec(BaseType::UInt32, &[0xF8, 0xEF, 0x59, 0x3B], Endian::Little),
509            RawValue::U32Scalar(995749880)
510        );
511    }
512
513    #[test]
514    fn float32_invalid_is_all_ones_bit_pattern() {
515        assert_eq!(
516            dec(BaseType::Float32, &[0xFF, 0xFF, 0xFF, 0xFF], Endian::Little),
517            RawValue::Invalid
518        );
519        let v = dec(BaseType::Float32, &[0x00, 0x00, 0x80, 0x3F], Endian::Little);
520        match v {
521            RawValue::F32Scalar(x) => assert_eq!(x, 1.0),
522            _ => panic!("expected F32Scalar"),
523        }
524    }
525
526    #[test]
527    fn string_strips_null_and_lossy_decodes() {
528        assert_eq!(
529            dec(BaseType::String, b"FIT Cookbook\0\0\0", Endian::Little),
530            RawValue::String("FIT Cookbook".into())
531        );
532        assert_eq!(
533            dec(BaseType::String, b"\0", Endian::Little),
534            RawValue::Invalid
535        );
536        assert_eq!(
537            dec(BaseType::String, b"", Endian::Little),
538            RawValue::Invalid
539        );
540    }
541
542    #[test]
543    fn malformed_size_returns_error() {
544        let err = decode_value(BaseType::UInt32, &[0, 0, 0], Endian::Little, 42).unwrap_err();
545        assert!(matches!(
546            err,
547            FitError::MalformedField {
548                field_def_num: 42,
549                ..
550            }
551        ));
552    }
553
554    #[test]
555    fn helpers_extract_scalars() {
556        let v = RawValue::U32Scalar(995749880);
557        assert_eq!(v.as_u32(), Some(995749880));
558        assert!(!v.is_invalid());
559
560        let v = RawValue::EnumScalar(4);
561        assert_eq!(v.as_u8(), Some(4));
562        assert_eq!(v.as_u16(), Some(4));
563
564        assert!(RawValue::Invalid.is_invalid());
565        assert_eq!(RawValue::Invalid.as_u32(), None);
566    }
567
568    #[test]
569    fn sint8_invalid_at_max() {
570        assert_eq!(
571            dec(BaseType::SInt8, &[0x7F], Endian::Little),
572            RawValue::Invalid
573        );
574        assert_eq!(
575            dec(BaseType::SInt8, &[0x7E], Endian::Little),
576            RawValue::I8Scalar(126)
577        );
578    }
579
580    #[test]
581    fn sint16_array_partial_invalid_keeps_field() {
582        assert_eq!(
583            dec(BaseType::SInt16, &[0xFF, 0x7F, 0x05, 0x00], Endian::Little),
584            RawValue::I16Array(vec![i16::MAX, 5].into_boxed_slice())
585        );
586    }
587}