fit/lib.rs
1#![deny(unsafe_code)]
2
3//! Garmin FIT protocol SDK for Rust — pure-Rust decoder + encoder for v21.200
4//! `.fit` files (workouts, activities, courses, settings, etc.).
5//!
6//! # Quick start
7//!
8//! ```no_run
9//! use fit::{Decoder, Encoder};
10//!
11//! let bytes = std::fs::read("Activity.fit")?;
12//!
13//! // Decode every message with all transforms applied (DateTime, enum
14//! // strings, scale/offset, components, sub-fields, dev fields).
15//! let (messages, errors) = Decoder::builder(&bytes).build().read_all();
16//! assert!(errors.is_empty());
17//!
18//! // Encode the typed messages back into a FIT binary.
19//! let encoded: Vec<u8> = Encoder::new().encode(&messages)?;
20//! fit::check_integrity(&encoded)?;
21//! # Ok::<(), Box<dyn std::error::Error>>(())
22//! ```
23//!
24//! # Module map
25//!
26//! | API | Purpose |
27//! |---|---|
28//! | [`is_fit`] / [`check_integrity`] | Quick file-level validation |
29//! | [`FileHeader`] / [`ByteStream`] | Low-level header + byte cursor |
30//! | [`Decoder`] / [`RawMessage`] | Streaming raw-message iterator |
31//! | [`DecoderBuilder`] / [`TypedDecoder`] / [`Message`] | Profile-aware typed pipeline |
32//! | [`Encoder`] / [`EncoderBuilder`] | Round-trip back to FIT binary |
33//! | [`crc`] / [`base_type`] / `record_header` | Protocol primitives |
34//! | [`profile`] | Generated Profile.xlsx tables (see `MesgNum`, `MesgInfo`, `FieldInfo`) |
35//! | [`transforms`] | Re-exports of the M5 transform helpers (datetime / enum / scale / components) |
36//! | [`merge_heart_rates`] / [`decode_memo_glob`] | M6 post-processing helpers |
37//! | [`FitError`] | Single error type used by every fallible operation |
38//!
39//! See the [repository](https://github.com/Chen-Lim/fit-editor-rust) for the
40//! milestone log and companion tools.
41
42pub mod base_type;
43pub mod crc;
44pub mod datetime;
45mod decoder;
46mod definition;
47pub mod dev_fields;
48mod encoder;
49mod error;
50mod header;
51pub mod output_stream;
52pub mod profile;
53mod raw_value;
54mod record_header;
55mod stream;
56pub mod transforms;
57mod typed_decoder;
58mod value;
59
60pub use base_type::BaseType;
61pub use decoder::{Decoder, RawDevField, RawField, RawMessage};
62pub use definition::{
63 DeveloperFieldDefinition, FieldDefinition, LocalDefinitions, MessageDefinition,
64 LOCAL_DEFINITION_SLOTS,
65};
66pub use dev_fields::{DevFieldInfo, DevFieldRegistry};
67pub use encoder::{Encoder, EncoderBuilder};
68pub use error::{FieldTooLargeKind, FitError};
69pub use header::FileHeader;
70pub use raw_value::RawValue;
71pub use record_header::RecordHeader;
72pub use stream::{ByteStream, Endian};
73pub use transforms::decode_memo_glob;
74#[cfg(feature = "chrono")]
75pub use transforms::merge_heart_rates;
76pub use typed_decoder::{DecoderBuilder, TransformOptions, TypedDecoder};
77pub use value::{Field, FieldKind, Message, Value};
78
79/// Compute the CRC-16 over a byte slice. Thin wrapper around [`crc::calculate`].
80pub fn crc16(data: &[u8]) -> u16 {
81 crc::calculate(data)
82}
83
84/// Quick check: does this byte slice plausibly hold a FIT file?
85///
86/// Returns `true` iff:
87/// 1. Length is at least 12 bytes
88/// 2. Byte 0 (header size) is 12 or 14
89/// 3. The total slice is at least `header_size + 2` bytes long (room for the
90/// trailing file CRC)
91/// 4. Bytes 8..12 equal the ASCII string `".FIT"`
92///
93/// This does **not** verify any CRC; for that use [`check_integrity`].
94pub fn is_fit(bytes: &[u8]) -> bool {
95 let Ok(header) = FileHeader::parse(bytes) else {
96 return false;
97 };
98 bytes.len() >= header.header_size as usize + 2
99}
100
101/// Fully verify a FIT file's CRCs.
102///
103/// Performs three checks in order:
104/// 1. The header parses (size, signature)
105/// 2. If the 14-byte header carries a non-zero CRC, it matches the CRC of
106/// the first 12 bytes (per protocol, a stored value of `0` skips this check)
107/// 3. The two trailing bytes match the CRC of the entire header + data region
108///
109/// The byte slice must be **at least** `header.total_file_size()` bytes long.
110/// Anything beyond that (e.g. additional chained FIT files) is ignored here.
111pub fn check_integrity(bytes: &[u8]) -> Result<(), FitError> {
112 let header = FileHeader::parse(bytes)?;
113 let total = header.total_file_size();
114 if bytes.len() < total {
115 return Err(FitError::TooShort {
116 expected: total,
117 actual: bytes.len(),
118 });
119 }
120
121 // 1. Header CRC (only present in 14-byte headers, only checked when non-zero).
122 if let Some(stored) = header.header_crc {
123 if stored != 0 {
124 let calculated = crc::calculate(&bytes[..12]);
125 if stored != calculated {
126 return Err(FitError::HeaderCrcMismatch { stored, calculated });
127 }
128 }
129 }
130
131 // 2. Trailing file CRC (always little-endian u16).
132 let crc_offset = header.file_crc_offset();
133 let stored_file_crc = u16::from_le_bytes([bytes[crc_offset], bytes[crc_offset + 1]]);
134 let calculated_file_crc = crc::calculate(&bytes[..crc_offset]);
135 if stored_file_crc != calculated_file_crc {
136 return Err(FitError::FileCrcMismatch {
137 stored: stored_file_crc,
138 calculated: calculated_file_crc,
139 });
140 }
141
142 Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn is_fit_rejects_too_short() {
151 assert!(!is_fit(&[]));
152 assert!(!is_fit(&[14u8]));
153 }
154
155 #[test]
156 fn is_fit_rejects_bad_signature() {
157 let mut bytes = [0u8; 16];
158 bytes[0] = 14;
159 bytes[8..12].copy_from_slice(b"NOPE");
160 assert!(!is_fit(&bytes));
161 }
162
163 #[test]
164 fn is_fit_rejects_invalid_header_size_byte() {
165 let mut bytes = [0u8; 16];
166 bytes[0] = 16; // not 12 or 14
167 bytes[8..12].copy_from_slice(b".FIT");
168 assert!(!is_fit(&bytes));
169 }
170
171 #[test]
172 fn is_fit_accepts_well_formed_14_byte() {
173 // 14-byte header + 2 trailing bytes (CRC value doesn't matter for is_fit).
174 let mut bytes = [0u8; 16];
175 bytes[0] = 14;
176 bytes[8..12].copy_from_slice(b".FIT");
177 assert!(is_fit(&bytes));
178 }
179}