1use crate::error::FitError;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct FileHeader {
21 pub header_size: u8,
23 pub protocol_version: u8,
25 pub profile_version: u16,
27 pub data_size: u32,
29 pub header_crc: Option<u16>,
32}
33
34impl FileHeader {
35 pub const SIGNATURE: [u8; 4] = *b".FIT";
37 pub const MIN_SIZE: u8 = 12;
39 pub const MAX_SIZE: u8 = 14;
41
42 pub fn parse(bytes: &[u8]) -> Result<Self, FitError> {
48 if bytes.len() < Self::MIN_SIZE as usize {
49 return Err(FitError::TooShort {
50 expected: Self::MIN_SIZE as usize,
51 actual: bytes.len(),
52 });
53 }
54
55 let header_size = bytes[0];
56 if header_size != 12 && header_size != 14 {
57 return Err(FitError::InvalidHeaderSize(header_size));
58 }
59 if bytes.len() < header_size as usize {
60 return Err(FitError::TooShort {
61 expected: header_size as usize,
62 actual: bytes.len(),
63 });
64 }
65
66 let protocol_version = bytes[1];
67 let profile_version = u16::from_le_bytes([bytes[2], bytes[3]]);
68 let data_size = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
69
70 let signature: [u8; 4] = [bytes[8], bytes[9], bytes[10], bytes[11]];
72 if signature != Self::SIGNATURE {
73 return Err(FitError::InvalidSignature(signature));
74 }
75
76 let header_crc = if header_size == 14 {
77 Some(u16::from_le_bytes([bytes[12], bytes[13]]))
78 } else {
79 None
80 };
81
82 Ok(Self {
83 header_size,
84 protocol_version,
85 profile_version,
86 data_size,
87 header_crc,
88 })
89 }
90
91 #[inline]
93 pub fn total_file_size(&self) -> usize {
94 self.header_size as usize + self.data_size as usize + 2
95 }
96
97 #[inline]
99 pub fn file_crc_offset(&self) -> usize {
100 self.header_size as usize + self.data_size as usize
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn make_14_byte_header(data_size: u32, header_crc: u16) -> [u8; 14] {
111 let ds = data_size.to_le_bytes();
112 let hc = header_crc.to_le_bytes();
113 [
114 14, 0x20, 0xD0, 0x52, ds[0], ds[1], ds[2], ds[3], 0x2E, 0x46, 0x49, 0x54, hc[0], hc[1],
117 ]
118 }
119
120 #[test]
121 fn parses_14_byte_header() {
122 let bytes = make_14_byte_header(94070, 0xABCD);
123 let h = FileHeader::parse(&bytes).unwrap();
124 assert_eq!(h.header_size, 14);
125 assert_eq!(h.protocol_version, 0x20);
126 assert_eq!(h.profile_version, 21200);
127 assert_eq!(h.data_size, 94070);
128 assert_eq!(h.header_crc, Some(0xABCD));
129 assert_eq!(h.total_file_size(), 14 + 94070 + 2);
130 }
131
132 #[test]
133 fn parses_12_byte_header() {
134 let bytes: [u8; 12] = [12, 0x10, 0x10, 0x00, 0x40, 0, 0, 0, 0x2E, 0x46, 0x49, 0x54];
136 let h = FileHeader::parse(&bytes).unwrap();
137 assert_eq!(h.header_size, 12);
138 assert_eq!(h.protocol_version, 0x10);
139 assert_eq!(h.data_size, 0x40);
140 assert_eq!(h.header_crc, None);
141 }
142
143 #[test]
144 fn rejects_invalid_header_size() {
145 let mut bytes = make_14_byte_header(0, 0);
146 bytes[0] = 13; assert_eq!(
148 FileHeader::parse(&bytes),
149 Err(FitError::InvalidHeaderSize(13))
150 );
151 }
152
153 #[test]
154 fn rejects_invalid_signature() {
155 let mut bytes = make_14_byte_header(0, 0);
156 bytes[8] = b'X'; let err = FileHeader::parse(&bytes).unwrap_err();
158 assert!(matches!(err, FitError::InvalidSignature(sig) if sig[0] == b'X'));
159 }
160
161 #[test]
162 fn rejects_too_short_for_minimum() {
163 let bytes = [14u8; 11];
164 assert_eq!(
165 FileHeader::parse(&bytes),
166 Err(FitError::TooShort {
167 expected: 12,
168 actual: 11,
169 }),
170 );
171 }
172
173 #[test]
174 fn rejects_too_short_for_declared_14_byte() {
175 let bytes: [u8; 12] = [14, 0x20, 0, 0, 0, 0, 0, 0, 0x2E, 0x46, 0x49, 0x54];
177 assert_eq!(
178 FileHeader::parse(&bytes),
179 Err(FitError::TooShort {
180 expected: 14,
181 actual: 12,
182 }),
183 );
184 }
185}