mdf4_rs/blocks/conversion/
base.rs

1use super::types::ConversionType;
2use crate::blocks::common::{BlockHeader, BlockParse, read_u8, read_u16, validate_buffer_size};
3use crate::{Error, Result};
4
5use alloc::boxed::Box;
6use alloc::format;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10#[cfg(feature = "std")]
11use alloc::collections::BTreeMap;
12#[cfg(feature = "std")]
13use alloc::collections::BTreeSet;
14
15#[derive(Debug, Clone)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct ConversionBlock {
18    pub header: BlockHeader,
19
20    // Link section
21    pub name_addr: Option<u64>,
22    pub unit_addr: Option<u64>,
23    pub comment_addr: Option<u64>,
24    pub inverse_addr: Option<u64>,
25    pub refs: Vec<u64>,
26
27    // Data
28    pub conversion_type: ConversionType,
29    pub precision: u8,
30    pub flags: u16,
31    pub ref_count: u16,
32    pub value_count: u16,
33    pub phys_range_min: Option<f64>,
34    pub phys_range_max: Option<f64>,
35    pub values: Vec<f64>,
36
37    pub formula: Option<String>,
38
39    // Resolved data for self-contained conversions (populated during index creation)
40    /// Pre-resolved text strings for text-based conversions (ValueToText, RangeToText, etc.)
41    /// Maps refs indices to their resolved text content
42    #[cfg(feature = "std")]
43    pub resolved_texts: Option<BTreeMap<usize, String>>,
44    #[cfg(not(feature = "std"))]
45    pub resolved_texts: Option<()>,
46
47    /// Pre-resolved nested conversion blocks for chained conversions
48    /// Maps refs indices to their resolved ConversionBlock content
49    #[cfg(feature = "std")]
50    pub resolved_conversions: Option<BTreeMap<usize, Box<ConversionBlock>>>,
51    #[cfg(not(feature = "std"))]
52    pub resolved_conversions: Option<()>,
53
54    /// Default conversion for fallback cases (similar to asammdf's "default_addr")
55    /// This is typically the last reference in refs for some conversion types
56    pub default_conversion: Option<Box<ConversionBlock>>,
57}
58
59impl BlockParse<'_> for ConversionBlock {
60    const ID: &'static str = "##CC";
61    fn from_bytes(bytes: &[u8]) -> Result<Self> {
62        let header = Self::parse_header(bytes)?;
63
64        let mut offset = 24;
65
66        // Fixed links
67        let name_addr = read_link(bytes, &mut offset);
68        let unit_addr = read_link(bytes, &mut offset);
69        let comment_addr = read_link(bytes, &mut offset);
70        let inverse_addr = read_link(bytes, &mut offset);
71
72        let fixed_links = 4;
73        let additional_links = header.link_count.saturating_sub(fixed_links);
74        let mut refs = Vec::with_capacity(additional_links as usize);
75        for _ in 0..additional_links {
76            refs.push(read_u64_checked(bytes, &mut offset)?);
77        }
78
79        // Basic fields
80        let conversion_type = ConversionType::from_u8(read_u8(bytes, offset));
81        offset += 1;
82        let precision = read_u8(bytes, offset);
83        offset += 1;
84        let flags = read_u16(bytes, offset);
85        offset += 2;
86        let ref_count = read_u16(bytes, offset);
87        offset += 2;
88        let value_count = read_u16(bytes, offset);
89        offset += 2;
90
91        // IMPORTANT: Some vendors (like dSPACE) always write the physical range fields
92        // even when flags bit 1 is not set. We need to detect this by checking if
93        // there's enough data in the block for the range fields.
94        // Calculate expected sizes:
95        let size_without_range =
96            24 + (header.link_count as usize * 8) + 8 + (value_count as usize * 8);
97        let size_with_range = size_without_range + 16;
98        let has_range_data = header.length as usize >= size_with_range;
99
100        let phys_range_min = if has_range_data {
101            let val = f64::from_bits(read_u64_checked(bytes, &mut offset)?);
102            Some(val)
103        } else {
104            None
105        };
106
107        let phys_range_max = if has_range_data {
108            let val = f64::from_bits(read_u64_checked(bytes, &mut offset)?);
109            Some(val)
110        } else {
111            None
112        };
113
114        let mut values = Vec::with_capacity(value_count as usize);
115        for _ in 0..value_count {
116            let val = f64::from_bits(read_u64_checked(bytes, &mut offset)?);
117            values.push(val);
118        }
119
120        Ok(Self {
121            header,
122            name_addr,
123            unit_addr,
124            comment_addr,
125            inverse_addr,
126            refs,
127            conversion_type,
128            precision,
129            flags,
130            ref_count,
131            value_count,
132            phys_range_min,
133            phys_range_max,
134            values,
135            formula: None,
136            resolved_texts: None,
137            resolved_conversions: None,
138            default_conversion: None,
139        })
140    }
141}
142
143/// Read an optional link from bytes, advancing the offset.
144fn read_link(bytes: &[u8], offset: &mut usize) -> Option<u64> {
145    let link = u64::from_le_bytes([
146        bytes[*offset],
147        bytes[*offset + 1],
148        bytes[*offset + 2],
149        bytes[*offset + 3],
150        bytes[*offset + 4],
151        bytes[*offset + 5],
152        bytes[*offset + 6],
153        bytes[*offset + 7],
154    ]);
155    *offset += 8;
156    if link == 0 { None } else { Some(link) }
157}
158
159/// Read a u64 from bytes, advancing the offset and validating bounds.
160fn read_u64_checked(bytes: &[u8], offset: &mut usize) -> Result<u64> {
161    validate_buffer_size(bytes, *offset + 8)?;
162    let val = u64::from_le_bytes([
163        bytes[*offset],
164        bytes[*offset + 1],
165        bytes[*offset + 2],
166        bytes[*offset + 3],
167        bytes[*offset + 4],
168        bytes[*offset + 5],
169        bytes[*offset + 6],
170        bytes[*offset + 7],
171    ]);
172    *offset += 8;
173    Ok(val)
174}
175
176impl ConversionBlock {
177    /// Resolve all dependencies for this conversion block to make it self-contained.
178    /// This reads referenced text blocks and nested conversions from the file data
179    /// and stores them in the resolved_texts and resolved_conversions fields.
180    ///
181    /// Supports arbitrary depth conversion chains with cycle detection.
182    ///
183    /// # Arguments
184    /// * `file_data` - Memory mapped MDF bytes used to read referenced data
185    ///
186    /// # Returns
187    /// `Ok(())` on success or an [`Error`] if resolution fails
188    #[cfg(feature = "std")]
189    pub fn resolve_all_dependencies(&mut self, file_data: &[u8]) -> Result<()> {
190        self.resolve_all_dependencies_with_address(file_data, 0)
191    }
192
193    /// Resolve all dependencies with a known current block address (used internally)
194    #[cfg(feature = "std")]
195    pub fn resolve_all_dependencies_with_address(
196        &mut self,
197        file_data: &[u8],
198        current_address: u64,
199    ) -> Result<()> {
200        // Start resolution with empty visited set to detect cycles
201        let mut visited = BTreeSet::new();
202        self.resolve_all_dependencies_recursive(file_data, 0, &mut visited, current_address)
203    }
204
205    /// Internal recursive method for resolving conversion dependencies.
206    ///
207    /// # Arguments
208    /// * `file_data` - Memory mapped MDF bytes used to read referenced data
209    /// * `depth` - Current recursion depth (for cycle detection)
210    /// * `visited` - Set of visited block addresses (for cycle detection)
211    /// * `current_address` - Address of the current conversion block being resolved
212    ///
213    /// # Returns
214    /// `Ok(())` on success or an [`Error`] if resolution fails
215    #[cfg(feature = "std")]
216    fn resolve_all_dependencies_recursive(
217        &mut self,
218        file_data: &[u8],
219        depth: usize,
220        visited: &mut BTreeSet<u64>,
221        current_address: u64,
222    ) -> Result<()> {
223        use crate::blocks::common::{BlockHeader, read_string_block};
224
225        const MAX_DEPTH: usize = 20; // Reasonable depth limit
226
227        // Prevent infinite recursion
228        if depth > MAX_DEPTH {
229            return Err(Error::ConversionChainTooDeep {
230                max_depth: MAX_DEPTH,
231            });
232        }
233
234        // Add current address to visited set
235        visited.insert(current_address);
236
237        // First resolve the formula if this is an algebraic conversion
238        self.resolve_formula(file_data)?;
239
240        // Initialize resolved data containers
241        let mut resolved_texts = BTreeMap::new();
242        let mut resolved_conversions = BTreeMap::new();
243        let mut default_conversion = None;
244
245        // Re-enable default conversion logic for specific types that need it
246        let has_default_conversion = matches!(
247            self.conversion_type,
248            crate::blocks::conversion::types::ConversionType::RangeToText // Add other types here as needed based on MDF specification
249        );
250
251        // For some conversion types, the last reference might be the default conversion
252        let default_ref_index = if has_default_conversion && self.refs.len() > 2 {
253            // Only treat as default if there are more than 2 references
254            // This avoids incorrectly treating simple cases as having defaults
255            Some(self.refs.len() - 1)
256        } else {
257            None
258        };
259
260        // Resolve each reference in refs
261        for (i, &link_addr) in self.refs.iter().enumerate() {
262            // Skip null links (address 0 typically means null in MDF format)
263            if link_addr == 0 {
264                continue; // Skip null links
265            }
266
267            // Check for cycles
268            if visited.contains(&link_addr) {
269                return Err(Error::ConversionChainCycle { address: link_addr });
270            }
271
272            let offset = link_addr as usize;
273            if offset + 24 > file_data.len() {
274                continue; // Skip invalid offsets
275            }
276
277            // Read the block header to determine the type
278            let header = BlockHeader::from_bytes(&file_data[offset..offset + 24])?;
279
280            match header.id.as_str() {
281                "##TX" => {
282                    // Text block - resolve the string content
283                    if let Some(text) = read_string_block(file_data, link_addr)? {
284                        resolved_texts.insert(i, text);
285                    }
286                }
287                "##CC" => {
288                    // Nested conversion block - resolve recursively
289                    let mut nested_conversion = ConversionBlock::from_bytes(&file_data[offset..])?;
290                    nested_conversion.resolve_all_dependencies_recursive(
291                        file_data,
292                        depth + 1,
293                        visited,
294                        link_addr,
295                    )?;
296
297                    // Check if this should be stored as default conversion
298                    if Some(i) == default_ref_index {
299                        default_conversion = Some(Box::new(nested_conversion));
300                    } else {
301                        resolved_conversions.insert(i, Box::new(nested_conversion));
302                    }
303                }
304                _ => {
305                    // Other block types - ignore for now but could be extended
306                    // to support metadata blocks, source information, etc.
307                }
308            }
309        }
310
311        // Store resolved data if any was found
312        if !resolved_texts.is_empty() {
313            self.resolved_texts = Some(resolved_texts);
314        }
315        if !resolved_conversions.is_empty() {
316            self.resolved_conversions = Some(resolved_conversions);
317        }
318        if default_conversion.is_some() {
319            self.default_conversion = default_conversion;
320        }
321
322        // Remove current address from visited set before returning
323        visited.remove(&current_address);
324
325        Ok(())
326    }
327
328    /// Get a resolved text string for a given refs index.
329    /// Returns the text if it was resolved during dependency resolution.
330    #[cfg(feature = "std")]
331    pub fn get_resolved_text(&self, ref_index: usize) -> Option<&String> {
332        self.resolved_texts.as_ref()?.get(&ref_index)
333    }
334
335    /// Get a resolved nested conversion for a given refs index.
336    /// Returns the conversion block if it was resolved during dependency resolution.
337    #[cfg(feature = "std")]
338    pub fn get_resolved_conversion(&self, ref_index: usize) -> Option<&ConversionBlock> {
339        self.resolved_conversions
340            .as_ref()?
341            .get(&ref_index)
342            .map(|boxed| boxed.as_ref())
343    }
344
345    /// Get the default conversion for fallback cases.
346    /// Returns the default conversion if it was resolved during dependency resolution.
347    pub fn get_default_conversion(&self) -> Option<&ConversionBlock> {
348        self.default_conversion.as_ref().map(|boxed| boxed.as_ref())
349    }
350
351    /// Serialize this conversion block back to bytes.
352    ///
353    /// # Returns
354    /// A byte vector containing the encoded block or an [`Error`] if
355    /// serialization fails.
356    pub fn to_bytes(&self) -> Result<Vec<u8>> {
357        let links = 4 + self.refs.len();
358
359        let mut header = self.header.clone();
360        header.link_count = links as u64;
361
362        let mut size = 24 + links * 8 + 1 + 1 + 2 + 2 + 2;
363        // Include range fields if they exist (regardless of flag)
364        if self.phys_range_min.is_some() || self.phys_range_max.is_some() {
365            size += 16;
366        }
367        size += self.values.len() * 8;
368        header.length = size as u64;
369
370        let mut buf = Vec::with_capacity(size);
371        buf.extend_from_slice(&header.to_bytes()?);
372        for link in [
373            self.name_addr,
374            self.unit_addr,
375            self.comment_addr,
376            self.inverse_addr,
377        ] {
378            buf.extend_from_slice(&link.unwrap_or(0).to_le_bytes());
379        }
380        for l in &self.refs {
381            buf.extend_from_slice(&l.to_le_bytes());
382        }
383        buf.push(self.conversion_type.to_u8());
384        buf.push(self.precision);
385        buf.extend_from_slice(&self.flags.to_le_bytes());
386        buf.extend_from_slice(&(self.ref_count).to_le_bytes());
387        buf.extend_from_slice(&(self.value_count).to_le_bytes());
388        // Write range fields if they exist (regardless of flag, for vendor compatibility)
389        if self.phys_range_min.is_some() || self.phys_range_max.is_some() {
390            buf.extend_from_slice(&self.phys_range_min.unwrap_or(0.0).to_le_bytes());
391            buf.extend_from_slice(&self.phys_range_max.unwrap_or(0.0).to_le_bytes());
392        }
393        for v in &self.values {
394            buf.extend_from_slice(&v.to_le_bytes());
395        }
396        if buf.len() != size {
397            return Err(Error::BlockSerializationError(format!(
398                "ConversionBlock expected size {size} but wrote {}",
399                buf.len()
400            )));
401        }
402        Ok(buf)
403    }
404
405    /// Creates an identity conversion (1:1, no change).
406    ///
407    /// This is useful when you want to explicitly indicate that no conversion
408    /// is applied, while still having a conversion block for consistency.
409    ///
410    /// # Example
411    /// ```
412    /// use mdf4_rs::blocks::ConversionBlock;
413    ///
414    /// let conv = ConversionBlock::identity();
415    /// ```
416    pub fn identity() -> Self {
417        Self {
418            header: BlockHeader {
419                id: String::from("##CC"),
420                reserved: 0,
421                length: 0, // Will be calculated during to_bytes()
422                link_count: 4,
423            },
424            name_addr: None,
425            unit_addr: None,
426            comment_addr: None,
427            inverse_addr: None,
428            refs: Vec::new(),
429            conversion_type: ConversionType::Identity,
430            precision: 0,
431            flags: 0,
432            ref_count: 0,
433            value_count: 0,
434            phys_range_min: None,
435            phys_range_max: None,
436            values: Vec::new(),
437            formula: None,
438            resolved_texts: None,
439            resolved_conversions: None,
440            default_conversion: None,
441        }
442    }
443
444    /// Creates a linear conversion: `physical = offset + factor * raw`.
445    ///
446    /// This is the most common conversion type, used for scaling and offset
447    /// adjustments. The MDF 4.1 specification defines linear conversion as:
448    /// `y = P1 + P2 * x` where P1 is the offset and P2 is the factor.
449    ///
450    /// # Arguments
451    /// * `offset` - The offset value (P1 in the MDF spec)
452    /// * `factor` - The scaling factor (P2 in the MDF spec)
453    ///
454    /// # Example
455    /// ```
456    /// use mdf4_rs::blocks::ConversionBlock;
457    ///
458    /// // Convert raw temperature: physical = -40.0 + 0.1 * raw
459    /// let temp_conv = ConversionBlock::linear(-40.0, 0.1);
460    ///
461    /// // Convert RPM: physical = 0.0 + 0.25 * raw
462    /// let rpm_conv = ConversionBlock::linear(0.0, 0.25);
463    /// ```
464    pub fn linear(offset: f64, factor: f64) -> Self {
465        Self {
466            header: BlockHeader {
467                id: String::from("##CC"),
468                reserved: 0,
469                length: 0, // Will be calculated during to_bytes()
470                link_count: 4,
471            },
472            name_addr: None,
473            unit_addr: None,
474            comment_addr: None,
475            inverse_addr: None,
476            refs: Vec::new(),
477            conversion_type: ConversionType::Linear,
478            precision: 0,
479            flags: 0,
480            ref_count: 0,
481            value_count: 2,
482            phys_range_min: None,
483            phys_range_max: None,
484            values: alloc::vec![offset, factor],
485            formula: None,
486            resolved_texts: None,
487            resolved_conversions: None,
488            default_conversion: None,
489        }
490    }
491
492    /// Creates a rational conversion: `physical = (P1 + P2*x + P3*x²) / (P4 + P5*x + P6*x²)`.
493    ///
494    /// Rational conversions are used for more complex non-linear transformations.
495    ///
496    /// # Arguments
497    /// * `p1` - Numerator constant term
498    /// * `p2` - Numerator linear coefficient
499    /// * `p3` - Numerator quadratic coefficient
500    /// * `p4` - Denominator constant term
501    /// * `p5` - Denominator linear coefficient
502    /// * `p6` - Denominator quadratic coefficient
503    ///
504    /// # Example
505    /// ```
506    /// use mdf4_rs::blocks::ConversionBlock;
507    ///
508    /// // Simple linear via rational: physical = (0 + 2*x + 0*x²) / (1 + 0*x + 0*x²) = 2*x
509    /// let conv = ConversionBlock::rational(0.0, 2.0, 0.0, 1.0, 0.0, 0.0);
510    /// ```
511    pub fn rational(p1: f64, p2: f64, p3: f64, p4: f64, p5: f64, p6: f64) -> Self {
512        Self {
513            header: BlockHeader {
514                id: String::from("##CC"),
515                reserved: 0,
516                length: 0,
517                link_count: 4,
518            },
519            name_addr: None,
520            unit_addr: None,
521            comment_addr: None,
522            inverse_addr: None,
523            refs: Vec::new(),
524            conversion_type: ConversionType::Rational,
525            precision: 0,
526            flags: 0,
527            ref_count: 0,
528            value_count: 6,
529            phys_range_min: None,
530            phys_range_max: None,
531            values: alloc::vec![p1, p2, p3, p4, p5, p6],
532            formula: None,
533            resolved_texts: None,
534            resolved_conversions: None,
535            default_conversion: None,
536        }
537    }
538
539    /// Check if this is a trivial identity conversion that can be omitted.
540    ///
541    /// Returns `true` if:
542    /// - The conversion type is Identity, OR
543    /// - The conversion type is Linear with offset=0 and factor=1
544    pub fn is_identity(&self) -> bool {
545        match self.conversion_type {
546            ConversionType::Identity => true,
547            ConversionType::Linear => {
548                self.values.len() >= 2 && self.values[0] == 0.0 && self.values[1] == 1.0
549            }
550            _ => false,
551        }
552    }
553
554    /// Set the physical range limits for this conversion.
555    ///
556    /// # Arguments
557    /// * `min` - Minimum physical value
558    /// * `max` - Maximum physical value
559    pub fn with_physical_range(mut self, min: f64, max: f64) -> Self {
560        self.phys_range_min = Some(min);
561        self.phys_range_max = Some(max);
562        self.flags |= 0b10; // Set physical range valid flag
563        self
564    }
565}