Skip to main content

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}