Skip to main content

fit/
encoder.rs

1//! FIT encoder — converts [`Message`]s back to protocol-compliant binary.
2//!
3//! The encoder reverses the typed-decoder's transforms:
4//! - `DateTime` → `datetime_to_fit()` → u32 wire bytes
5//! - `Enum(name)` → `enum_value_by_str(type_name, name)` → base-type int bytes
6//! - `Float(physical)` → `(v + offset) * scale` → integer bytes
7//! - `String` / `Bytes` / `UInt` / `SInt` / `Bool` → direct wire encoding
8//!
9//! M9 capabilities: per-message Definition re-emission with 16-slot LRU
10//! eviction, [`EncoderBuilder`] chaining, developer-field encoding (driven by
11//! a [`DevFieldRegistry`] pre-built from `field_description` messages in the
12//! input), and multi-segment chains via [`Encoder::encode_chain`].
13//!
14//! # Limitations
15//!
16//! - **Compressed-timestamp records are not re-emitted.** The decoder fully
17//!   supports them (per protocol §2.3), but the encoder always writes regular
18//!   Data records with an explicit `timestamp` field. Round-tripping a file
19//!   that contained compressed-timestamp records produces a semantically
20//!   identical, slightly larger file — not a byte-for-byte copy.
21
22use std::collections::HashMap;
23
24use crate::base_type::BaseType;
25use crate::crc;
26use crate::dev_fields::{base_type_to_type_name, DevFieldInfo, DevFieldRegistry};
27use crate::error::{FieldTooLargeKind, FitError};
28use crate::output_stream::OutputStream;
29use crate::profile;
30use crate::value::{FieldKind, Message, Value};
31
32// ────────────────────────────────────────────────────────────────────
33// Public API: Encoder + EncoderBuilder
34// ────────────────────────────────────────────────────────────────────
35
36/// FIT file encoder.
37#[derive(Debug, Clone)]
38pub struct Encoder {
39    protocol_version: u8,
40    profile_version: u16,
41}
42
43impl Encoder {
44    /// Build an encoder with the default protocol (0x20) and profile (21200) versions.
45    pub fn new() -> Self {
46        Self {
47            protocol_version: 0x20,
48            profile_version: 21200,
49        }
50    }
51
52    /// Begin a chainable builder for non-default settings.
53    pub fn builder() -> EncoderBuilder {
54        EncoderBuilder::default()
55    }
56
57    /// Encode a single FIT segment (header + records + CRC).
58    pub fn encode(&self, messages: &[Message]) -> Result<Vec<u8>, FitError> {
59        let mut out = OutputStream::with_capacity(14 + messages.len() * 32);
60        self.write_segment(&mut out, messages)?;
61        Ok(out.into_bytes())
62    }
63
64    /// Encode multiple FIT segments back-to-back. Each segment gets its own
65    /// header, definitions, and trailing CRC; the local-definition table is
66    /// reset between segments (matching how chained `.fit` files are decoded).
67    pub fn encode_chain(&self, segments: &[&[Message]]) -> Result<Vec<u8>, FitError> {
68        if segments.is_empty() {
69            return Ok(self.encode_empty());
70        }
71        let mut out = OutputStream::with_capacity(segments.iter().map(|s| s.len() * 32).sum());
72        for seg in segments {
73            self.write_segment(&mut out, seg)?;
74        }
75        Ok(out.into_bytes())
76    }
77
78    // ─── internals ─────────────────────────────────────────────────────
79
80    fn write_segment(&self, out: &mut OutputStream, messages: &[Message]) -> Result<(), FitError> {
81        if messages.is_empty() {
82            self.write_empty_segment(out);
83            return Ok(());
84        }
85
86        // Pre-pass: harvest developer-field schemas from any field_description
87        // (mesg 206) messages in the input so we can size FieldKind::Developer
88        // fields when we encounter them.
89        let dev_registry = collect_dev_registry(messages);
90
91        let segment_start = out.position();
92
93        // 14-byte header with placeholders for data_size and header_crc.
94        out.write_u8(14);
95        out.write_u8(self.protocol_version);
96        out.write_u16(self.profile_version);
97        out.write_u32(0); // data_size placeholder (offset +4..+8)
98        out.write_bytes(b".FIT");
99        out.write_u16(0); // header CRC placeholder (offset +12..+14)
100
101        let mut registry = LocalDefRegistry::new();
102        let mut clock: u64 = 0;
103
104        for msg in messages {
105            clock += 1;
106            let new_def = build_wire_def(msg, &dev_registry)?;
107            let local = registry.acquire(msg.global_mesg_num, &new_def, clock)?;
108
109            if registry.needs_redefinition(msg.global_mesg_num, &new_def) {
110                write_definition_record(out, local, &new_def)?;
111                registry.commit(msg.global_mesg_num, local, new_def.clone(), clock);
112            }
113
114            write_data_record(out, local, msg, &new_def, &dev_registry)?;
115        }
116
117        // Backpatch the segment header.
118        let header_off = segment_start;
119        let data_size = u32::try_from(out.position() - header_off - 14)
120            .expect("FIT segment data exceeds u32::MAX bytes");
121        out.patch(header_off + 4, &data_size.to_le_bytes());
122
123        let header_crc = crc::calculate(&out.as_slice()[header_off..header_off + 12]);
124        out.patch(header_off + 12, &header_crc.to_le_bytes());
125
126        let file_crc = crc::calculate(&out.as_slice()[header_off..]);
127        out.write_u16(file_crc);
128        Ok(())
129    }
130
131    fn write_empty_segment(&self, out: &mut OutputStream) {
132        let segment_start = out.position();
133        out.write_u8(14);
134        out.write_u8(self.protocol_version);
135        out.write_u16(self.profile_version);
136        out.write_u32(0);
137        out.write_bytes(b".FIT");
138        let header_crc = crc::calculate(&out.as_slice()[segment_start..segment_start + 12]);
139        out.write_u16(header_crc);
140        let file_crc = crc::calculate(&out.as_slice()[segment_start..]);
141        out.write_u16(file_crc);
142    }
143
144    fn encode_empty(&self) -> Vec<u8> {
145        let mut out = OutputStream::with_capacity(16);
146        self.write_empty_segment(&mut out);
147        out.into_bytes()
148    }
149}
150
151impl Default for Encoder {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157/// Chainable builder for [`Encoder`]. Defaults match [`Encoder::new`].
158#[derive(Debug, Clone)]
159pub struct EncoderBuilder {
160    protocol_version: u8,
161    profile_version: u16,
162}
163
164impl Default for EncoderBuilder {
165    fn default() -> Self {
166        Self {
167            protocol_version: 0x20,
168            profile_version: 21200,
169        }
170    }
171}
172
173impl EncoderBuilder {
174    /// Set the protocol version byte written at offset 1 of each segment header.
175    pub fn protocol_version(mut self, v: u8) -> Self {
176        self.protocol_version = v;
177        self
178    }
179
180    /// Set the profile version word written at offsets 2..4 of each segment header.
181    pub fn profile_version(mut self, v: u16) -> Self {
182        self.profile_version = v;
183        self
184    }
185
186    /// Finalise the configuration into an [`Encoder`].
187    pub fn build(self) -> Encoder {
188        Encoder {
189            protocol_version: self.protocol_version,
190            profile_version: self.profile_version,
191        }
192    }
193}
194
195// ────────────────────────────────────────────────────────────────────
196// Wire-level intermediate representation
197// ────────────────────────────────────────────────────────────────────
198
199/// Wire-level field definition used during encoding.
200#[derive(Debug, Clone, PartialEq)]
201struct WireField {
202    field_def_num: u8,
203    /// Byte width of this field on the wire (= element_size × count).
204    size: u8,
205    base_type: BaseType,
206    base_type_byte: u8,
207    /// Profile type name (e.g. `"manufacturer"`, `"file"`, `"sport"`,
208    /// `"date_time"`, or `"uint16"`). Used to reverse `Value::Enum` lookups
209    /// and to size enum-typed fields. Empty when no FieldInfo was found.
210    type_name: &'static str,
211    /// Profile scale factor (`physical = raw / scale - offset`). `None` ⇒ 1.0.
212    scale: Option<f64>,
213    /// Profile offset. `None` ⇒ 0.0.
214    offset: Option<f64>,
215}
216
217/// Wire-level developer-field definition.
218#[derive(Debug, Clone, PartialEq)]
219struct WireDevField {
220    field_def_num: u8,
221    size: u8,
222    developer_data_index: u8,
223    base_type: BaseType,
224    scale: Option<f64>,
225    offset: Option<f64>,
226}
227
228/// Wire-level message definition used during encoding.
229#[derive(Debug, Clone, PartialEq)]
230struct WireDef {
231    global_mesg_num: u16,
232    fields: Vec<WireField>,
233    dev_fields: Vec<WireDevField>,
234}
235
236/// Returns `true` if a [`Field`] with the given `(name, field_def_num)` is a
237/// real wire field — its name must match the canonical `FieldInfo.name` or one
238/// of its `SubField.name`s. Components-synthesised fields (e.g. `enhanced_speed`
239/// derived from `speed`'s low 16 bits) carry a different name and must be
240/// skipped: they're rebuilt on decode and re-emitting them would duplicate
241/// `field_def_num` slots in the Definition record.
242///
243/// When `mesg_info` is `None` (unknown message), we accept all fields.
244fn is_real_wire_field(
245    field_name: &str,
246    field_def_num: u8,
247    mesg_info: Option<&profile::MesgInfo>,
248) -> bool {
249    let Some(info) = mesg_info else {
250        return true;
251    };
252    let Some(real) = info.field(field_def_num) else {
253        return false;
254    };
255    real.name == field_name || real.sub_fields.iter().any(|sf| sf.name == field_name)
256}
257
258/// Build a `WireDef` from a single [`Message`]'s fields, consulting both the
259/// generated Profile metadata (for Standard fields) and the supplied developer
260/// registry (for Developer fields).
261fn build_wire_def(msg: &Message, dev_registry: &DevFieldRegistry) -> Result<WireDef, FitError> {
262    let mesg_info = profile::mesg_info_by_num(msg.global_mesg_num);
263
264    let mut fields = Vec::with_capacity(msg.fields.len());
265    let mut dev_fields = Vec::new();
266    let mut seen_fdns = std::collections::HashSet::<u8>::new();
267
268    for field in &msg.fields {
269        match field.kind {
270            FieldKind::Standard { field_def_num } => {
271                let fi = mesg_info.and_then(|m| m.field(field_def_num));
272
273                if !is_real_wire_field(&field.name, field_def_num, mesg_info) {
274                    continue;
275                }
276                if !seen_fdns.insert(field_def_num) {
277                    continue;
278                }
279
280                let base_type = resolve_base_type(fi, &field.value);
281                let base_type_byte = base_type.type_code();
282                let size = compute_wire_size(fi, &field.value, base_type)?;
283                let (type_name, scale, offset) = match fi {
284                    Some(f) => (f.type_name, f.scale, f.offset),
285                    None => ("", None, None),
286                };
287
288                fields.push(WireField {
289                    field_def_num,
290                    size,
291                    base_type,
292                    base_type_byte,
293                    type_name,
294                    scale,
295                    offset,
296                });
297            }
298            FieldKind::Developer {
299                field_def_num,
300                developer_data_index,
301            } => {
302                // Resolve schema from the registry. Without it we cannot size
303                // the field, so it has to be skipped.
304                let Some(info) = dev_registry.get(developer_data_index, field_def_num) else {
305                    continue;
306                };
307                let size = compute_dev_wire_size(info, &field.value)?;
308                dev_fields.push(WireDevField {
309                    field_def_num,
310                    size,
311                    developer_data_index,
312                    base_type: info.base_type,
313                    scale: info.scale,
314                    offset: info.offset,
315                });
316            }
317        }
318    }
319
320    Ok(WireDef {
321        global_mesg_num: msg.global_mesg_num,
322        fields,
323        dev_fields,
324    })
325}
326
327/// Resolve a field's wire base type. Order:
328/// 1. Codegen dispatcher (Profile enum types like `manufacturer`, `sport`).
329/// 2. Direct base-type name (`uint16`, `string`, `byte`, ...).
330/// 3. Inference from the value variant (last resort).
331fn resolve_base_type(fi: Option<&profile::FieldInfo>, value: &Value) -> BaseType {
332    if let Some(fi) = fi {
333        if let Some(bt) = crate::transforms::enum_strings::base_type_for_type_name(fi.type_name) {
334            return bt;
335        }
336        if let Some(bt) = base_type_from_name(fi.type_name) {
337            return bt;
338        }
339    }
340    infer_base_type(value)
341}
342
343fn base_type_from_name(type_name: &str) -> Option<BaseType> {
344    match type_name {
345        "enum" => Some(BaseType::Enum),
346        "sint8" => Some(BaseType::SInt8),
347        "uint8" => Some(BaseType::UInt8),
348        "sint16" => Some(BaseType::SInt16),
349        "uint16" => Some(BaseType::UInt16),
350        "sint32" => Some(BaseType::SInt32),
351        "uint32" => Some(BaseType::UInt32),
352        "sint64" => Some(BaseType::SInt64),
353        "uint64" => Some(BaseType::UInt64),
354        "float32" => Some(BaseType::Float32),
355        "float64" => Some(BaseType::Float64),
356        "uint8z" => Some(BaseType::UInt8z),
357        "uint16z" => Some(BaseType::UInt16z),
358        "uint32z" => Some(BaseType::UInt32z),
359        "uint64z" => Some(BaseType::UInt64z),
360        "string" => Some(BaseType::String),
361        "byte" => Some(BaseType::Byte),
362        "bool" => Some(BaseType::UInt8),
363        "date_time" | "local_date_time" => Some(BaseType::UInt32),
364        _ => None,
365    }
366}
367
368fn infer_base_type(value: &Value) -> BaseType {
369    match value {
370        Value::UInt(_) => BaseType::UInt32,
371        Value::SInt(_) => BaseType::SInt32,
372        Value::Float(_) => BaseType::Float64,
373        Value::String(_) => BaseType::String,
374        Value::Bytes(_) => BaseType::Byte,
375        Value::Bool(_) => BaseType::UInt8,
376        Value::Enum(_) => BaseType::Enum,
377        Value::DateTime(_) => BaseType::UInt32,
378        Value::Invalid => BaseType::UInt32,
379        Value::Array(items) if !items.is_empty() => infer_base_type(&items[0]),
380        Value::Array(_) => BaseType::UInt32,
381    }
382}
383
384fn compute_wire_size(
385    fi: Option<&profile::FieldInfo>,
386    value: &Value,
387    base_type: BaseType,
388) -> Result<u8, FitError> {
389    if base_type == BaseType::String {
390        return match value {
391            Value::String(s) => u8::try_from(s.len() + 1).map_err(|_| FitError::FieldTooLarge {
392                kind: FieldTooLargeKind::String,
393                size: s.len(),
394            }),
395            _ => Ok(1),
396        };
397    }
398    if base_type == BaseType::Byte {
399        return match value {
400            Value::Bytes(b) => u8::try_from(b.len().max(1)).map_err(|_| FitError::FieldTooLarge {
401                kind: FieldTooLargeKind::ByteArray,
402                size: b.len(),
403            }),
404            _ => Ok(1),
405        };
406    }
407    let element_size = base_type.element_size() as u8;
408    let count = element_count(fi, value)?;
409    Ok(element_size * count.max(1))
410}
411
412fn compute_dev_wire_size(info: &DevFieldInfo, value: &Value) -> Result<u8, FitError> {
413    if info.base_type == BaseType::String {
414        return match value {
415            Value::String(s) => u8::try_from(s.len() + 1).map_err(|_| FitError::FieldTooLarge {
416                kind: FieldTooLargeKind::String,
417                size: s.len(),
418            }),
419            _ => Ok(1),
420        };
421    }
422    if info.base_type == BaseType::Byte {
423        return match value {
424            Value::Bytes(b) => u8::try_from(b.len().max(1)).map_err(|_| FitError::FieldTooLarge {
425                kind: FieldTooLargeKind::ByteArray,
426                size: b.len(),
427            }),
428            _ => Ok(1),
429        };
430    }
431    let element_size = info.base_type.element_size() as u8;
432    if let Value::Array(a) = value {
433        let len = u8::try_from(a.len()).map_err(|_| FitError::FieldTooLarge {
434            kind: FieldTooLargeKind::Array,
435            size: a.len(),
436        })?;
437        return Ok(element_size * len.max(1));
438    }
439    Ok(element_size)
440}
441
442fn element_count(fi: Option<&profile::FieldInfo>, value: &Value) -> Result<u8, FitError> {
443    if let Value::Array(a) = value {
444        return u8::try_from(a.len()).map_err(|_| FitError::FieldTooLarge {
445            kind: FieldTooLargeKind::Array,
446            size: a.len(),
447        });
448    }
449    if let Some(fi) = fi {
450        if let Some(spec) = fi.array {
451            let inner = spec.trim_matches(|c| c == '[' || c == ']');
452            if let Some((first, _rest)) = inner.split_once('x') {
453                if let Ok(n) = first.parse::<u8>() {
454                    return Ok(n);
455                }
456            } else if inner != "N" {
457                if let Ok(n) = inner.parse::<u8>() {
458                    return Ok(n);
459                }
460            }
461        }
462    }
463    Ok(1)
464}
465
466// ────────────────────────────────────────────────────────────────────
467// Local-definition registry with 16-slot LRU eviction
468// ────────────────────────────────────────────────────────────────────
469
470#[derive(Debug, Clone)]
471struct RegisteredDef {
472    mesg_num: u16,
473    wire_def: WireDef,
474    last_used: u64,
475}
476
477/// Tracks the protocol-mandated 4-bit local-message-number table. Up to 16
478/// distinct definitions can be live at once; when a 17th unique mesg_num
479/// arrives, the least-recently-used slot is evicted and reused.
480#[derive(Debug, Default)]
481struct LocalDefRegistry {
482    /// Slot table: indexed by local_mesg_num (0..=15). `None` means free.
483    slots: [Option<RegisteredDef>; 16],
484    /// Mesg-num → local-num index for fast lookup.
485    by_mesg_num: HashMap<u16, u8>,
486    /// `true` after `acquire` if the matching slot already holds an equivalent
487    /// WireDef; `false` if the caller should re-emit a Definition record.
488    last_acquire_was_match: bool,
489}
490
491impl LocalDefRegistry {
492    fn new() -> Self {
493        Self::default()
494    }
495
496    /// Reserve a local_mesg_num for `(mesg_num, wire_def)`, allocating a fresh
497    /// slot or evicting the LRU entry as needed. Caller must follow with
498    /// [`Self::commit`] *only* when [`Self::needs_redefinition`] reports `true`.
499    fn acquire(&mut self, mesg_num: u16, wire_def: &WireDef, clock: u64) -> Result<u8, FitError> {
500        if let Some(&local) = self.by_mesg_num.get(&mesg_num) {
501            let slot = self.slots[local as usize]
502                .as_mut()
503                .expect("by_mesg_num invariant");
504            self.last_acquire_was_match = slot.wire_def == *wire_def;
505            slot.last_used = clock;
506            return Ok(local);
507        }
508        // Fresh registration — find a free slot first.
509        if let Some(free) = self.slots.iter().position(Option::is_none) {
510            self.last_acquire_was_match = false;
511            return Ok(free as u8);
512        }
513        // All 16 slots in use — evict LRU.
514        let (lru_local, lru_mesg) = self
515            .slots
516            .iter()
517            .enumerate()
518            .min_by_key(|(_, s)| s.as_ref().map(|d| d.last_used).unwrap_or(u64::MAX))
519            .map(|(i, s)| (i as u8, s.as_ref().expect("all slots occupied").mesg_num))
520            .ok_or(FitError::TooManyLocalDefinitions(17))?;
521        self.by_mesg_num.remove(&lru_mesg);
522        self.slots[lru_local as usize] = None;
523        self.last_acquire_was_match = false;
524        Ok(lru_local)
525    }
526
527    /// `true` iff the caller must emit a fresh Definition record before the
528    /// upcoming Data record (either the slot is fresh, or the WireDef changed).
529    fn needs_redefinition(&self, mesg_num: u16, wire_def: &WireDef) -> bool {
530        match self.by_mesg_num.get(&mesg_num) {
531            Some(&local) => self.slots[local as usize]
532                .as_ref()
533                .map(|d| d.wire_def != *wire_def)
534                .unwrap_or(true),
535            None => true,
536        }
537    }
538
539    /// Record that a Definition record has been emitted for `(mesg_num, local)`
540    /// with the supplied `wire_def`. Future identical messages will reuse the
541    /// slot without re-emission.
542    fn commit(&mut self, mesg_num: u16, local: u8, wire_def: WireDef, clock: u64) {
543        self.slots[local as usize] = Some(RegisteredDef {
544            mesg_num,
545            wire_def,
546            last_used: clock,
547        });
548        self.by_mesg_num.insert(mesg_num, local);
549    }
550}
551
552// ────────────────────────────────────────────────────────────────────
553// Developer-field registry harvesting
554// ────────────────────────────────────────────────────────────────────
555
556/// Walk `messages` once and harvest a [`DevFieldRegistry`] from any
557/// `field_description` (mesg 206) messages. Mirrors what the typed decoder
558/// does inline; needed up-front by the encoder so that subsequent messages
559/// with `FieldKind::Developer` fields can be sized.
560fn collect_dev_registry(messages: &[Message]) -> DevFieldRegistry {
561    let mut reg = DevFieldRegistry::new();
562    for m in messages {
563        if m.global_mesg_num != 206 {
564            continue;
565        }
566        let dev_idx = m.field("developer_data_index").and_then(value_as_u8);
567        let fdn = m.field("field_definition_number").and_then(value_as_u8);
568        // `fit_base_type_id` is enum-typed in Profile; the typed decoder may
569        // surface it as either `Value::UInt(_)` or `Value::Enum("uint8")` /
570        // `Value::Enum("float32")` etc. depending on toggles.
571        let bt_id = m.field("fit_base_type_id").and_then(|f| match &f.value {
572            Value::UInt(v) => Some(*v as u8),
573            Value::SInt(v) => Some(*v as u8),
574            Value::Enum(name) => {
575                crate::transforms::enum_strings::enum_value_by_str("fit_base_type", name)
576                    .map(|v| v as u8)
577            }
578            _ => None,
579        });
580        let name = m
581            .field("field_name")
582            .and_then(|f| match &f.value {
583                Value::String(s) => Some(s.clone()),
584                _ => None,
585            })
586            .unwrap_or_default();
587        let scale = m.field("scale").and_then(|f| match &f.value {
588            Value::Float(v) => Some(*v),
589            Value::UInt(v) => Some(*v as f64),
590            _ => None,
591        });
592        let offset = m.field("offset").and_then(|f| match &f.value {
593            Value::Float(v) => Some(*v),
594            Value::SInt(v) => Some(*v as f64),
595            _ => None,
596        });
597        let units = m.field("units").and_then(|f| match &f.value {
598            Value::String(s) => Some(s.clone()),
599            _ => None,
600        });
601        if let (Some(dev_idx), Some(fdn), Some(bt_id)) = (dev_idx, fdn, bt_id) {
602            reg.register_field(dev_idx, fdn, name, bt_id, scale, offset, units);
603        }
604    }
605    reg
606}
607
608fn value_as_u8(f: &crate::value::Field) -> Option<u8> {
609    match &f.value {
610        Value::UInt(v) => Some(*v as u8),
611        Value::SInt(v) => Some(*v as u8),
612        _ => None,
613    }
614}
615
616// ────────────────────────────────────────────────────────────────────
617// Wire emission
618// ────────────────────────────────────────────────────────────────────
619
620fn write_definition_record(
621    out: &mut OutputStream,
622    local_mesg_num: u8,
623    def: &WireDef,
624) -> Result<(), FitError> {
625    let header = if def.dev_fields.is_empty() {
626        0x40 | (local_mesg_num & 0x0F)
627    } else {
628        // Bit 5 signals developer-field definitions follow.
629        0x40 | 0x20 | (local_mesg_num & 0x0F)
630    };
631    out.write_u8(header);
632    out.write_u8(0x00); // reserved
633    out.write_u8(0x00); // architecture: little-endian
634    out.write_u16(def.global_mesg_num);
635    let field_count = u8::try_from(def.fields.len()).map_err(|_| FitError::FieldTooLarge {
636        kind: FieldTooLargeKind::FieldList,
637        size: def.fields.len(),
638    })?;
639    out.write_u8(field_count);
640    for f in &def.fields {
641        out.write_u8(f.field_def_num);
642        out.write_u8(f.size);
643        out.write_u8(f.base_type_byte);
644    }
645    if !def.dev_fields.is_empty() {
646        let dev_count =
647            u8::try_from(def.dev_fields.len()).map_err(|_| FitError::FieldTooLarge {
648                kind: FieldTooLargeKind::DevFieldList,
649                size: def.dev_fields.len(),
650            })?;
651        out.write_u8(dev_count);
652        for d in &def.dev_fields {
653            out.write_u8(d.field_def_num);
654            out.write_u8(d.size);
655            out.write_u8(d.developer_data_index);
656        }
657    }
658    Ok(())
659}
660
661fn write_data_record(
662    out: &mut OutputStream,
663    local_mesg_num: u8,
664    msg: &Message,
665    def: &WireDef,
666    dev_registry: &DevFieldRegistry,
667) -> Result<(), FitError> {
668    out.write_u8(local_mesg_num & 0x0F); // data record header
669    let mesg_info = profile::mesg_info_by_num(msg.global_mesg_num);
670
671    // Standard fields, in Definition order.
672    for wf in &def.fields {
673        let value = msg
674            .fields
675            .iter()
676            .find(|f| {
677                let FieldKind::Standard { field_def_num } = f.kind else {
678                    return false;
679                };
680                field_def_num == wf.field_def_num
681                    && is_real_wire_field(&f.name, field_def_num, mesg_info)
682            })
683            .map(|f| &f.value);
684        encode_field_value(out, value, wf)?;
685    }
686
687    // Developer fields, in Definition order.
688    for wd in &def.dev_fields {
689        let value = msg
690            .fields
691            .iter()
692            .find(|f| {
693                matches!(f.kind, FieldKind::Developer { field_def_num, developer_data_index }
694                if field_def_num == wd.field_def_num
695                    && developer_data_index == wd.developer_data_index)
696            })
697            .map(|f| &f.value);
698        encode_dev_field_value(out, value, wd, dev_registry)?;
699    }
700    Ok(())
701}
702
703fn encode_field_value(
704    out: &mut OutputStream,
705    value: Option<&Value>,
706    wf: &WireField,
707) -> Result<(), FitError> {
708    let Some(value) = value else {
709        write_invalid(out, wf.base_type, wf.size);
710        return Ok(());
711    };
712    encode_value_inner(
713        out,
714        value,
715        wf.base_type,
716        wf.size,
717        wf.type_name,
718        wf.scale,
719        wf.offset,
720    )
721}
722
723fn encode_dev_field_value(
724    out: &mut OutputStream,
725    value: Option<&Value>,
726    wd: &WireDevField,
727    _registry: &DevFieldRegistry,
728) -> Result<(), FitError> {
729    let Some(value) = value else {
730        write_invalid(out, wd.base_type, wd.size);
731        return Ok(());
732    };
733    encode_value_inner(
734        out,
735        value,
736        wd.base_type,
737        wd.size,
738        base_type_to_type_name(wd.base_type),
739        wd.scale,
740        wd.offset,
741    )
742}
743
744/// Common encoder for both standard and developer fields. `type_name` is used
745/// only for `Value::Enum` reverse lookups; `scale`/`offset` flip the M5 forward
746/// transform for `Value::Float` on integer base types.
747fn encode_value_inner(
748    out: &mut OutputStream,
749    value: &Value,
750    base_type: BaseType,
751    size: u8,
752    type_name: &str,
753    scale: Option<f64>,
754    offset: Option<f64>,
755) -> Result<(), FitError> {
756    match value {
757        Value::Invalid => write_invalid(out, base_type, size),
758        Value::Bool(b) => out.write_u8(if *b { 1 } else { 0 }),
759        Value::UInt(v) => encode_int(out, *v as i128, base_type),
760        Value::SInt(v) => encode_int(out, *v as i128, base_type),
761        Value::Float(v) => encode_float(out, *v, base_type, scale, offset),
762        Value::String(s) => {
763            out.write_bytes(s.as_bytes());
764            out.write_u8(0x00);
765            for _ in s.len() + 1..size as usize {
766                out.write_u8(0x00);
767            }
768        }
769        Value::Bytes(b) => {
770            out.write_bytes(b);
771            for _ in b.len()..size as usize {
772                out.write_u8(0xFF);
773            }
774        }
775        Value::Enum(name) => {
776            let lookup_name = if type_name.is_empty() {
777                "enum"
778            } else {
779                type_name
780            };
781            if let Some(v) = crate::transforms::enum_strings::enum_value_by_str(lookup_name, name) {
782                encode_int(out, v as i128, base_type);
783            } else {
784                write_invalid(out, base_type, size);
785            }
786        }
787        Value::DateTime(dt) => {
788            #[cfg(feature = "chrono")]
789            let secs = crate::datetime::datetime_to_fit(*dt);
790            #[cfg(not(feature = "chrono"))]
791            let secs = Some(*dt);
792            if let Some(secs) = secs {
793                encode_int(out, secs as i128, base_type);
794            } else {
795                write_invalid(out, base_type, size);
796            }
797        }
798        Value::Array(items) => {
799            let element_size = base_type.element_size() as u8;
800            for item in items {
801                encode_value_inner(out, item, base_type, element_size, type_name, scale, offset)?;
802            }
803            let written = items.len() * base_type.element_size();
804            for _ in written..size as usize {
805                out.write_u8(invalid_byte_for_base(base_type));
806            }
807        }
808    }
809    Ok(())
810}
811
812fn encode_float(
813    out: &mut OutputStream,
814    v: f64,
815    base_type: BaseType,
816    scale: Option<f64>,
817    offset: Option<f64>,
818) {
819    match base_type {
820        BaseType::Float32 => out.write_u32((v as f32).to_bits()),
821        BaseType::Float64 => out.write_u64(v.to_bits()),
822        _ => {
823            let offset = offset.unwrap_or(0.0);
824            let scale = scale.unwrap_or(1.0);
825            let scaled = (v + offset) * scale;
826            let size = base_type.element_size() as u8;
827            // NaN/Infinity → invalid sentinel (saturating `as i128` would
828            // map NaN → 0, silently corrupting output).
829            if !scaled.is_finite() {
830                write_invalid(out, base_type, size);
831                return;
832            }
833            let as_i128 = scaled.round() as i128;
834            let (lo, hi) = base_type_int_range(base_type);
835            if as_i128 < lo || as_i128 > hi {
836                write_invalid(out, base_type, size);
837                return;
838            }
839            encode_int(out, as_i128, base_type);
840        }
841    }
842}
843
844/// Inclusive `[min, max]` range of an integer base type, in `i128`. Used by
845/// [`encode_float`] to reject scaled values that would silently wrap on cast.
846/// Float / String bases are not integer-shaped; we return a degenerate range
847/// (they are routed through other paths and never reach this helper in practice).
848fn base_type_int_range(base_type: BaseType) -> (i128, i128) {
849    match base_type {
850        BaseType::Enum | BaseType::UInt8 | BaseType::UInt8z | BaseType::Byte => {
851            (0, u8::MAX as i128)
852        }
853        BaseType::UInt16 | BaseType::UInt16z => (0, u16::MAX as i128),
854        BaseType::UInt32 | BaseType::UInt32z => (0, u32::MAX as i128),
855        BaseType::UInt64 | BaseType::UInt64z => (0, u64::MAX as i128),
856        BaseType::SInt8 => (i8::MIN as i128, i8::MAX as i128),
857        BaseType::SInt16 => (i16::MIN as i128, i16::MAX as i128),
858        BaseType::SInt32 => (i32::MIN as i128, i32::MAX as i128),
859        BaseType::SInt64 => (i64::MIN as i128, i64::MAX as i128),
860        BaseType::Float32 | BaseType::Float64 | BaseType::String => (0, 0),
861    }
862}
863
864fn encode_int(out: &mut OutputStream, v: i128, base_type: BaseType) {
865    match base_type {
866        BaseType::Enum | BaseType::UInt8 | BaseType::UInt8z | BaseType::Byte => {
867            out.write_u8(v as u8);
868        }
869        BaseType::UInt16 | BaseType::UInt16z => out.write_u16(v as u16),
870        BaseType::UInt32 | BaseType::UInt32z => out.write_u32(v as u32),
871        BaseType::UInt64 | BaseType::UInt64z => out.write_u64(v as u64),
872        BaseType::SInt8 => out.write_u8(v as i8 as u8),
873        BaseType::SInt16 => out.write_i16(v as i16),
874        BaseType::SInt32 => out.write_i32(v as i32),
875        BaseType::SInt64 => out.write_i64(v as i64),
876        BaseType::Float32 => out.write_u32((v as f32).to_bits()),
877        BaseType::Float64 => out.write_u64((v as f64).to_bits()),
878        BaseType::String => {} // strings handled separately
879    }
880}
881
882fn write_invalid(out: &mut OutputStream, base_type: BaseType, size: u8) {
883    let element_size = base_type.element_size().max(1);
884    let count = (size as usize / element_size).max(1);
885    match base_type {
886        BaseType::Enum | BaseType::UInt8 | BaseType::Byte => {
887            for _ in 0..count {
888                out.write_u8(0xFF);
889            }
890        }
891        BaseType::UInt8z => {
892            for _ in 0..count {
893                out.write_u8(0x00);
894            }
895        }
896        BaseType::SInt8 => {
897            for _ in 0..count {
898                out.write_u8(0x7F);
899            }
900        }
901        BaseType::UInt16 => {
902            for _ in 0..count {
903                out.write_u16(0xFFFF);
904            }
905        }
906        BaseType::UInt16z => {
907            for _ in 0..count {
908                out.write_u16(0x0000);
909            }
910        }
911        BaseType::SInt16 => {
912            for _ in 0..count {
913                out.write_i16(i16::MAX);
914            }
915        }
916        BaseType::UInt32 | BaseType::Float32 => {
917            for _ in 0..count {
918                out.write_u32(0xFFFF_FFFF);
919            }
920        }
921        BaseType::UInt32z => {
922            for _ in 0..count {
923                out.write_u32(0);
924            }
925        }
926        BaseType::SInt32 => {
927            for _ in 0..count {
928                out.write_i32(i32::MAX);
929            }
930        }
931        BaseType::UInt64 | BaseType::Float64 => {
932            for _ in 0..count {
933                out.write_u64(0xFFFF_FFFF_FFFF_FFFF);
934            }
935        }
936        BaseType::UInt64z => {
937            for _ in 0..count {
938                out.write_u64(0);
939            }
940        }
941        BaseType::SInt64 => {
942            for _ in 0..count {
943                out.write_i64(i64::MAX);
944            }
945        }
946        BaseType::String => out.write_u8(0x00),
947    }
948}
949
950fn invalid_byte_for_base(base_type: BaseType) -> u8 {
951    match base_type {
952        BaseType::UInt8z | BaseType::UInt16z | BaseType::UInt32z | BaseType::UInt64z => 0x00,
953        BaseType::String => 0x00,
954        _ => 0xFF,
955    }
956}
957
958#[cfg(test)]
959mod tests {
960    use super::*;
961
962    #[test]
963    fn encode_empty_produces_valid_header() {
964        let bytes = Encoder::new().encode(&[]).unwrap();
965        assert_eq!(bytes.len(), 16);
966        assert_eq!(&bytes[8..12], b".FIT");
967        assert_eq!(bytes[0], 14);
968        crate::check_integrity(&bytes).unwrap();
969    }
970
971    #[test]
972    fn encoder_builder_overrides_versions() {
973        let enc = Encoder::builder()
974            .protocol_version(0x21)
975            .profile_version(21300)
976            .build();
977        let bytes = enc.encode(&[]).unwrap();
978        assert_eq!(bytes[1], 0x21);
979        assert_eq!(u16::from_le_bytes([bytes[2], bytes[3]]), 21300);
980        crate::check_integrity(&bytes).unwrap();
981    }
982
983    #[test]
984    fn lru_registry_evicts_least_recently_used() {
985        let mut reg = LocalDefRegistry::new();
986        // Fill all 16 slots.
987        for i in 0..16u16 {
988            let def = WireDef {
989                global_mesg_num: i,
990                fields: vec![],
991                dev_fields: vec![],
992            };
993            let local = reg.acquire(i, &def, i as u64 + 1).unwrap();
994            reg.commit(i, local, def, i as u64 + 1);
995        }
996        // Touch mesg_nums 1..16 with later clock values; mesg_num 0 stays the
997        // oldest.
998        for i in 1..16u16 {
999            let def = WireDef {
1000                global_mesg_num: i,
1001                fields: vec![],
1002                dev_fields: vec![],
1003            };
1004            reg.acquire(i, &def, 100 + i as u64).unwrap();
1005        }
1006        // Adding a 17th must evict mesg_num 0 and re-use slot 0.
1007        let new_def = WireDef {
1008            global_mesg_num: 999,
1009            fields: vec![],
1010            dev_fields: vec![],
1011        };
1012        let local = reg.acquire(999, &new_def, 200).unwrap();
1013        assert_eq!(local, 0);
1014        reg.commit(999, local, new_def, 200);
1015        assert!(reg.by_mesg_num.contains_key(&999));
1016        assert!(!reg.by_mesg_num.contains_key(&0));
1017    }
1018}