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}