Skip to main content

fit/
base_type.rs

1//! FIT base types — the 17 primitive wire types referenced by every Definition
2//! message's field-type byte.
3//!
4//! The field-type byte layout is:
5//!
6//! ```text
7//!   Bit 7    Bits [6:5]   Bits [4:0]
8//!   ┌─────┬─────────────┬──────────────┐
9//!   │ EE  │ reserved=00 │  type code   │
10//!   └─────┴─────────────┴──────────────┘
11//! ```
12//!
13//! - **EE**: endian flag — set when multi-byte values are big-endian. In
14//!   practice the architecture byte (offset 2 of the Definition) is the
15//!   source of truth for endianness; `EE` is largely redundant and we don't
16//!   honor it at runtime.
17//! - **type code**: one of 17 values (`0x00..=0x10`).
18//!
19//! Reference: `guide/fit_binary_learning_notes.md` §3.1.
20
21use crate::error::FitError;
22
23/// The 17 FIT base types, indexed by their type-code byte.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25#[repr(u8)]
26pub enum BaseType {
27    /// `0x00` — 1-byte enumeration. Invalid: `0xFF`.
28    Enum = 0x00,
29    /// `0x01` — signed 8-bit. Invalid: `0x7F`.
30    SInt8 = 0x01,
31    /// `0x02` — unsigned 8-bit. Invalid: `0xFF`.
32    UInt8 = 0x02,
33    /// `0x03` — signed 16-bit. Invalid: `0x7FFF`.
34    SInt16 = 0x03,
35    /// `0x04` — unsigned 16-bit. Invalid: `0xFFFF`.
36    UInt16 = 0x04,
37    /// `0x05` — signed 32-bit. Invalid: `0x7FFFFFFF`.
38    SInt32 = 0x05,
39    /// `0x06` — unsigned 32-bit. Invalid: `0xFFFFFFFF`.
40    UInt32 = 0x06,
41    /// `0x07` — null-terminated UTF-8. Invalid: empty / first byte `0x00`.
42    String = 0x07,
43    /// `0x08` — IEEE 754 single. Invalid: bit-pattern `0xFFFFFFFF`.
44    Float32 = 0x08,
45    /// `0x09` — IEEE 754 double. Invalid: bit-pattern `0xFFFFFFFFFFFFFFFF`.
46    Float64 = 0x09,
47    /// `0x0A` — unsigned 8-bit (Z series). Invalid: **`0x00`**.
48    UInt8z = 0x0A,
49    /// `0x0B` — unsigned 16-bit (Z series). Invalid: **`0x0000`**.
50    UInt16z = 0x0B,
51    /// `0x0C` — unsigned 32-bit (Z series). Invalid: **`0x00000000`**.
52    UInt32z = 0x0C,
53    /// `0x0D` — opaque byte array. Invalid: **all** elements `0xFF` (special
54    /// rule: a single `0xFF` element is *not* invalid in a multi-byte array).
55    Byte = 0x0D,
56    /// `0x0E` — signed 64-bit. Invalid: `0x7FFFFFFFFFFFFFFF`.
57    SInt64 = 0x0E,
58    /// `0x0F` — unsigned 64-bit. Invalid: `0xFFFFFFFFFFFFFFFF`.
59    UInt64 = 0x0F,
60    /// `0x10` — unsigned 64-bit (Z series). Invalid: **`0x0000000000000000`**.
61    ///
62    /// The 17th and largest valid type code; missing from many older
63    /// references but defined in current FIT SDKs.
64    UInt64z = 0x10,
65}
66
67impl BaseType {
68    /// Mask to extract the type code from a field-type byte (drops endian flag).
69    pub const TYPE_CODE_MASK: u8 = 0x1F;
70    /// Endian flag bit (bit 7 of a field-type byte).
71    pub const ENDIAN_FLAG: u8 = 0x80;
72
73    /// Decode a raw field-type byte.
74    ///
75    /// Bit 7 (the endian flag) is masked off; only the low 5 bits are
76    /// consulted. Returns [`FitError::UnknownBaseType`] if the type code
77    /// is outside the valid range `0x00..=0x10`.
78    pub fn from_byte(byte: u8) -> Result<Self, FitError> {
79        let code = byte & Self::TYPE_CODE_MASK;
80        let bt = match code {
81            0x00 => Self::Enum,
82            0x01 => Self::SInt8,
83            0x02 => Self::UInt8,
84            0x03 => Self::SInt16,
85            0x04 => Self::UInt16,
86            0x05 => Self::SInt32,
87            0x06 => Self::UInt32,
88            0x07 => Self::String,
89            0x08 => Self::Float32,
90            0x09 => Self::Float64,
91            0x0A => Self::UInt8z,
92            0x0B => Self::UInt16z,
93            0x0C => Self::UInt32z,
94            0x0D => Self::Byte,
95            0x0E => Self::SInt64,
96            0x0F => Self::UInt64,
97            0x10 => Self::UInt64z,
98            _ => return Err(FitError::UnknownBaseType(byte, code)),
99        };
100        Ok(bt)
101    }
102
103    /// True iff bit 7 of the raw field-type byte indicates big-endian.
104    pub fn endian_flag_set(byte: u8) -> bool {
105        byte & Self::ENDIAN_FLAG != 0
106    }
107
108    /// Size in bytes of a **single element** of this type. For the variable-
109    /// length types ([`BaseType::String`] and [`BaseType::Byte`]) this is the
110    /// stride per element (1 byte); the actual payload size comes from the
111    /// Field Size byte in the Definition message.
112    pub fn element_size(&self) -> usize {
113        match self {
114            Self::Enum | Self::SInt8 | Self::UInt8 | Self::UInt8z | Self::Byte | Self::String => 1,
115            Self::SInt16 | Self::UInt16 | Self::UInt16z => 2,
116            Self::SInt32 | Self::UInt32 | Self::UInt32z | Self::Float32 => 4,
117            Self::SInt64 | Self::UInt64 | Self::UInt64z | Self::Float64 => 8,
118        }
119    }
120
121    /// True for the Z series — types whose invalid sentinel is **all zero**
122    /// rather than all ones. Important for invalid-value detection in M4.
123    pub fn is_z_type(&self) -> bool {
124        matches!(
125            self,
126            Self::UInt8z | Self::UInt16z | Self::UInt32z | Self::UInt64z
127        )
128    }
129
130    /// True for [`BaseType::Byte`]. Required because the Byte type has a
131    /// special invalid-value rule (only invalid when *every* element is
132    /// `0xFF`).
133    pub fn is_byte(&self) -> bool {
134        matches!(self, Self::Byte)
135    }
136
137    /// True for [`BaseType::String`].
138    pub fn is_string(&self) -> bool {
139        matches!(self, Self::String)
140    }
141
142    /// Raw type code byte (0x00..=0x10). Since BaseType is `#[repr(u8)]`,
143    /// this is the discriminant value — suitable for writing in a Definition
144    /// message's field definition.
145    pub fn type_code(&self) -> u8 {
146        *self as u8
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn all_17_codes_round_trip() {
156        for code in 0x00..=0x10 {
157            let bt = BaseType::from_byte(code).expect("code in range must decode");
158            assert_eq!(bt as u8, code, "round-trip for 0x{code:02X}");
159        }
160    }
161
162    #[test]
163    fn endian_flag_is_masked() {
164        // Same type code, endian flag set vs unset must yield the same BaseType.
165        let with_flag = BaseType::from_byte(0x80 | 0x04).unwrap();
166        let without = BaseType::from_byte(0x04).unwrap();
167        assert_eq!(with_flag, BaseType::UInt16);
168        assert_eq!(without, BaseType::UInt16);
169        assert!(BaseType::endian_flag_set(0x80 | 0x04));
170        assert!(!BaseType::endian_flag_set(0x04));
171    }
172
173    #[test]
174    fn invalid_type_code_returns_error() {
175        // 0x11..0x1F are reserved/invalid; 0x1F is the largest masked value.
176        for bad in 0x11..=0x1F {
177            assert!(matches!(
178                BaseType::from_byte(bad),
179                Err(FitError::UnknownBaseType(_, _))
180            ));
181        }
182    }
183
184    #[test]
185    fn element_sizes_are_correct() {
186        assert_eq!(BaseType::Enum.element_size(), 1);
187        assert_eq!(BaseType::UInt16.element_size(), 2);
188        assert_eq!(BaseType::UInt32.element_size(), 4);
189        assert_eq!(BaseType::Float32.element_size(), 4);
190        assert_eq!(BaseType::UInt64.element_size(), 8);
191        assert_eq!(BaseType::UInt64z.element_size(), 8);
192        assert_eq!(BaseType::String.element_size(), 1);
193        assert_eq!(BaseType::Byte.element_size(), 1);
194    }
195
196    #[test]
197    fn z_type_classification() {
198        assert!(BaseType::UInt8z.is_z_type());
199        assert!(BaseType::UInt16z.is_z_type());
200        assert!(BaseType::UInt32z.is_z_type());
201        assert!(BaseType::UInt64z.is_z_type());
202        assert!(!BaseType::UInt8.is_z_type());
203        assert!(!BaseType::Byte.is_z_type());
204    }
205
206    #[test]
207    fn uint64z_is_present_at_0x10() {
208        // Regression check for the gap originally identified in the learning notes.
209        assert_eq!(BaseType::from_byte(0x10).unwrap(), BaseType::UInt64z);
210    }
211}