mdf4_rs/
index.rs

1//! MDF File Indexing System
2//!
3//! This module provides a lightweight indexing system for MDF4 files, enabling
4//! efficient random access to channel data without loading the entire file into
5//! memory. Indexes can be serialized to JSON for caching and reuse.
6//!
7//! # Overview
8//!
9//! The MDF index system addresses a key challenge with large measurement files:
10//! reading specific channel data efficiently. Instead of parsing the entire file
11//! structure each time, you can:
12//!
13//! 1. **Build an index** that captures channel metadata and data locations
14//! 2. **Save the index** to disk for reuse across sessions
15//! 3. **Read channel data** by seeking directly to the relevant byte ranges
16//!
17//! This approach is particularly valuable for:
18//! - Large files (hundreds of MB to GB)
19//! - Remote files accessed via HTTP range requests
20//! - Applications that need to read specific channels repeatedly
21//!
22//! # Index Contents
23//!
24//! An [`MdfIndex`] contains:
25//! - Channel group metadata (names, record sizes, record counts)
26//! - Channel metadata (names, data types, byte offsets, conversions)
27//! - Data block locations (file offsets and sizes)
28//!
29//! # Performance Comparison
30//!
31//! | Operation | Full Parse | With Index |
32//! |-----------|-----------|------------|
33//! | Open 1GB file | 5-10 sec | <100ms |
34//! | Read 1 channel | Full parse | ~50ms |
35//! | Second channel | Full parse | ~50ms |
36//!
37//! # Example: Building and Using an Index
38//!
39//! ```no_run
40//! use mdf4_rs::{MdfIndex, FileRangeReader, Result};
41//!
42//! fn read_efficiently() -> Result<()> {
43//!     // Option 1: Create index with streaming (minimal memory)
44//!     let index = MdfIndex::from_file_streaming("large_file.mf4")?;
45//!
46//!     // Save for later use (requires serde_json feature)
47//!     index.save_to_file("large_file.index")?;
48//!
49//!     // Option 2: Load pre-built index (instant)
50//!     let index = MdfIndex::load_from_file("large_file.index")?;
51//!
52//!     // Read only the channel you need
53//!     let mut reader = FileRangeReader::new("large_file.mf4")?;
54//!     let values = index.read_channel_values_by_name("Temperature", &mut reader)?;
55//!
56//!     Ok(())
57//! }
58//! ```
59//!
60//! # Reader Types
61//!
62//! The index system supports multiple reader implementations:
63//!
64//! - [`FileRangeReader`]: Direct file access (simple, low memory)
65//! - [`BufferedRangeReader`]: Buffered file access (better for sequential reads)
66//! - Custom implementations: HTTP range requests, cloud storage, etc.
67//!
68//! # Feature Flags
69//!
70//! - `serde`: Enables index serialization/deserialization
71//! - `serde_json`: Enables JSON file save/load methods
72
73use crate::{
74    Error, MDF, Result,
75    blocks::{
76        BlockHeader, BlockParse, ChannelBlock, ChannelGroupBlock, ConversionBlock, ConversionType,
77        DataGroupBlock, DataListBlock, DataType, HeaderBlock, IdentificationBlock, TextBlock,
78    },
79    parsing::decoder::{DecodedValue, decode_channel_value_with_validity},
80};
81use std::collections::BTreeMap;
82use std::io::{Read, Seek, SeekFrom};
83
84/// Location and metadata for a data block within the MDF file.
85///
86/// Each channel group can have multiple data blocks, especially in files
87/// created with streaming writes. This struct stores the information needed
88/// to locate and read a specific data block.
89///
90/// # Data Block Types
91///
92/// - **DT blocks**: Uncompressed raw data (most common)
93/// - **DZ blocks**: Zlib-compressed data (requires decompression)
94/// - **DL blocks**: Data lists pointing to multiple blocks
95#[derive(Debug, Clone)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97pub struct DataBlockInfo {
98    /// Absolute file offset where the block header starts.
99    /// The actual data begins 24 bytes after this offset (after the block header).
100    pub file_offset: u64,
101    /// Total size of the block including the 24-byte header.
102    pub size: u64,
103    /// Whether this block contains compressed data (DZ block).
104    /// Compressed blocks require decompression before reading values.
105    pub is_compressed: bool,
106}
107
108/// Metadata for a single channel, containing all information needed to decode values.
109///
110/// This struct captures the essential channel properties from the MDF file's
111/// CN blocks, including data type, bit layout, and conversion formula. It enables
112/// decoding channel values without re-parsing the original MDF structure.
113///
114/// # Bit Layout
115///
116/// Values are extracted using `byte_offset`, `bit_offset`, and `bit_count`:
117/// - `byte_offset`: Starting byte within the record (after record ID)
118/// - `bit_offset`: Starting bit within that byte (0-7)
119/// - `bit_count`: Total number of bits to read
120///
121/// # Channel Types
122///
123/// - **Type 0**: Regular data channel
124/// - **Type 1**: Variable Length Signal Data (VLSD)
125/// - **Type 2**: Master channel (time, angle, etc.)
126/// - **Type 3**: Virtual master channel
127/// - **Type 4**: Synchronization channel
128/// - **Type 5**: Maximum length channel
129/// - **Type 6**: Virtual data channel
130#[derive(Debug, Clone)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct IndexedChannel {
133    /// Channel name (e.g., "EngineRPM", "Temperature")
134    pub name: Option<String>,
135    /// Physical unit (e.g., "rpm", "°C", "m/s")
136    pub unit: Option<String>,
137    /// Data type determining how raw bytes are interpreted
138    pub data_type: DataType,
139    /// Byte offset within each record (after record ID bytes)
140    pub byte_offset: u32,
141    /// Bit offset within the starting byte (0-7)
142    pub bit_offset: u8,
143    /// Number of bits for this channel's raw value
144    pub bit_count: u32,
145    /// Channel type (0=data, 1=VLSD, 2=master, etc.)
146    pub channel_type: u8,
147    /// Channel flags indicating invalidation bit presence and other properties
148    pub flags: u32,
149    /// Position of invalidation bit within invalidation bytes (if used)
150    pub pos_invalidation_bit: u32,
151    /// Conversion formula to transform raw values to physical units.
152    /// If `None`, raw values are used directly.
153    pub conversion: Option<ConversionBlock>,
154    /// For VLSD channels: file address of signal data blocks
155    pub vlsd_data_address: Option<u64>,
156}
157
158/// Metadata and layout for a channel group (measurement data collection).
159///
160/// A channel group represents a collection of channels that share the same
161/// time base and record structure. All channels in a group have synchronized
162/// samples stored together in fixed-size records.
163///
164/// # Record Structure
165///
166/// Each record has the following layout:
167/// ```text
168/// [Record ID (0-8 bytes)] [Channel Data (record_size bytes)] [Invalidation (invalidation_bytes bytes)]
169/// ```
170///
171/// The total record size is: `record_id_size + record_size + invalidation_bytes`
172#[derive(Debug, Clone)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct IndexedChannelGroup {
175    /// Group name (e.g., "CAN1", "EngineData", "GPS")
176    pub name: Option<String>,
177    /// Group description or comment
178    pub comment: Option<String>,
179    /// Size of record ID prefix in bytes (0, 1, 2, 4, or 8)
180    pub record_id_size: u8,
181    /// Size of channel data portion in each record (bytes)
182    pub record_size: u32,
183    /// Size of invalidation bytes at end of each record
184    pub invalidation_bytes: u32,
185    /// Total number of records (samples) in this group
186    pub record_count: u64,
187    /// Channels belonging to this group
188    pub channels: Vec<IndexedChannel>,
189    /// Data block locations containing this group's records
190    pub data_blocks: Vec<DataBlockInfo>,
191}
192
193/// Complete index of an MDF file for efficient random access.
194///
195/// The index captures all structural information needed to read channel
196/// data without parsing the entire MDF file. It can be serialized to JSON
197/// for caching across sessions.
198///
199/// # Creating an Index
200///
201/// ```no_run
202/// use mdf4_rs::MdfIndex;
203///
204/// // From file (loads entire structure into memory)
205/// let index = MdfIndex::from_file("data.mf4")?;
206///
207/// // From file with streaming (minimal memory)
208/// let index = MdfIndex::from_file_streaming("large_file.mf4")?;
209/// # Ok::<(), mdf4_rs::Error>(())
210/// ```
211///
212/// # Reading Channel Data
213///
214/// ```no_run
215/// use mdf4_rs::{MdfIndex, FileRangeReader};
216///
217/// let index = MdfIndex::from_file_streaming("data.mf4")?;
218/// let mut reader = FileRangeReader::new("data.mf4")?;
219///
220/// // By name (searches all groups)
221/// let values = index.read_channel_values_by_name("Temperature", &mut reader)?;
222///
223/// // By index (faster, no search)
224/// let values = index.read_channel_values(0, 1, &mut reader)?;
225/// # Ok::<(), mdf4_rs::Error>(())
226/// ```
227#[derive(Debug, Clone)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
229pub struct MdfIndex {
230    /// Original file size in bytes (for validation)
231    pub file_size: u64,
232    /// All channel groups in the file
233    pub channel_groups: Vec<IndexedChannelGroup>,
234}
235
236/// Trait for reading arbitrary byte ranges from a data source.
237///
238/// This trait abstracts the data source, allowing the index system to work
239/// with local files, HTTP resources, cloud storage, or any other source
240/// that supports random access.
241///
242/// # Implementing Custom Readers
243///
244/// ```ignore
245/// use mdf4_rs::index::ByteRangeReader;
246///
247/// struct HttpRangeReader {
248///     url: String,
249///     client: reqwest::blocking::Client,
250/// }
251///
252/// impl ByteRangeReader for HttpRangeReader {
253///     type Error = mdf4_rs::Error;
254///
255///     fn read_range(&mut self, offset: u64, length: u64) -> Result<Vec<u8>, Self::Error> {
256///         let end = offset + length - 1;
257///         let response = self.client
258///             .get(&self.url)
259///             .header("Range", format!("bytes={}-{}", offset, end))
260///             .send()
261///             .map_err(|e| mdf4_rs::Error::BlockSerializationError(e.to_string()))?;
262///         response.bytes()
263///             .map(|b| b.to_vec())
264///             .map_err(|e| mdf4_rs::Error::BlockSerializationError(e.to_string()))
265///     }
266/// }
267/// ```
268pub trait ByteRangeReader {
269    /// Error type returned by read operations
270    type Error;
271
272    /// Read `length` bytes starting at `offset`.
273    ///
274    /// # Arguments
275    /// * `offset` - Byte offset from the start of the data source
276    /// * `length` - Number of bytes to read
277    ///
278    /// # Returns
279    /// The requested bytes, or an error if the read fails.
280    fn read_range(
281        &mut self,
282        offset: u64,
283        length: u64,
284    ) -> core::result::Result<Vec<u8>, Self::Error>;
285}
286
287/// Simple file reader that seeks and reads for each request.
288///
289/// This reader has minimal memory overhead but may have higher I/O latency
290/// when reading many small ranges. For sequential access patterns, consider
291/// using [`BufferedRangeReader`] instead.
292///
293/// # Example
294///
295/// ```no_run
296/// use mdf4_rs::{MdfIndex, FileRangeReader};
297///
298/// let index = MdfIndex::from_file_streaming("data.mf4")?;
299/// let mut reader = FileRangeReader::new("data.mf4")?;
300/// let values = index.read_channel_values(0, 0, &mut reader)?;
301/// # Ok::<(), mdf4_rs::Error>(())
302/// ```
303pub struct FileRangeReader {
304    file: std::fs::File,
305}
306
307impl FileRangeReader {
308    /// Open a file for range reading.
309    ///
310    /// # Arguments
311    /// * `file_path` - Path to the file
312    ///
313    /// # Errors
314    /// Returns an error if the file cannot be opened.
315    pub fn new(file_path: &str) -> Result<Self> {
316        let file = std::fs::File::open(file_path).map_err(Error::IOError)?;
317        Ok(Self { file })
318    }
319}
320
321impl ByteRangeReader for FileRangeReader {
322    type Error = Error;
323
324    fn read_range(
325        &mut self,
326        offset: u64,
327        length: u64,
328    ) -> core::result::Result<Vec<u8>, Self::Error> {
329        self.file
330            .seek(SeekFrom::Start(offset))
331            .map_err(Error::IOError)?;
332
333        let mut buffer = vec![0u8; length as usize];
334        self.file.read_exact(&mut buffer).map_err(Error::IOError)?;
335
336        Ok(buffer)
337    }
338}
339
340/// Buffered file reader with read-ahead caching for better I/O performance.
341///
342/// This reader maintains an internal buffer and prefetches data to minimize
343/// system calls when reading many small ranges sequentially.
344pub struct BufferedRangeReader {
345    file: std::fs::File,
346    buffer: Vec<u8>,
347    buffer_start: u64,
348    buffer_end: u64,
349    buffer_capacity: usize,
350}
351
352impl BufferedRangeReader {
353    /// Create a new buffered reader with the default buffer size (64 KB).
354    pub fn new(file_path: &str) -> Result<Self> {
355        Self::with_capacity(file_path, 64 * 1024)
356    }
357
358    /// Create a new buffered reader with a custom buffer size.
359    pub fn with_capacity(file_path: &str, capacity: usize) -> Result<Self> {
360        let file = std::fs::File::open(file_path).map_err(Error::IOError)?;
361        Ok(Self {
362            file,
363            buffer: Vec::with_capacity(capacity),
364            buffer_start: 0,
365            buffer_end: 0,
366            buffer_capacity: capacity,
367        })
368    }
369
370    /// Fill the internal buffer starting at the given offset.
371    fn fill_buffer(&mut self, offset: u64) -> Result<()> {
372        self.file
373            .seek(SeekFrom::Start(offset))
374            .map_err(Error::IOError)?;
375
376        self.buffer.clear();
377        self.buffer.resize(self.buffer_capacity, 0);
378
379        let bytes_read = self.file.read(&mut self.buffer).map_err(Error::IOError)?;
380        self.buffer.truncate(bytes_read);
381        self.buffer_start = offset;
382        self.buffer_end = offset + bytes_read as u64;
383
384        Ok(())
385    }
386}
387
388impl ByteRangeReader for BufferedRangeReader {
389    type Error = Error;
390
391    fn read_range(
392        &mut self,
393        offset: u64,
394        length: u64,
395    ) -> core::result::Result<Vec<u8>, Self::Error> {
396        let end = offset + length;
397
398        // Check if the requested range is fully within the buffer
399        if offset >= self.buffer_start && end <= self.buffer_end {
400            let start_idx = (offset - self.buffer_start) as usize;
401            let end_idx = start_idx + length as usize;
402            return Ok(self.buffer[start_idx..end_idx].to_vec());
403        }
404
405        // If the request is larger than our buffer, read directly
406        if length as usize > self.buffer_capacity {
407            self.file
408                .seek(SeekFrom::Start(offset))
409                .map_err(Error::IOError)?;
410            let mut buffer = vec![0u8; length as usize];
411            self.file.read_exact(&mut buffer).map_err(Error::IOError)?;
412            return Ok(buffer);
413        }
414
415        // Fill buffer starting at the requested offset
416        self.fill_buffer(offset)?;
417
418        // Now read from buffer
419        if end <= self.buffer_end {
420            let start_idx = (offset - self.buffer_start) as usize;
421            let end_idx = start_idx + length as usize;
422            Ok(self.buffer[start_idx..end_idx].to_vec())
423        } else {
424            // Buffer didn't have enough data (near end of file)
425            Err(Error::TooShortBuffer {
426                actual: (self.buffer_end - offset) as usize,
427                expected: length as usize,
428                file: file!(),
429                line: line!(),
430            })
431        }
432    }
433}
434
435/// Example HTTP range reader (would be implemented in production)
436/// ```rust,ignore
437/// use mdf4_rs::index::ByteRangeReader;
438/// use mdf4_rs::error::MdfError;
439///
440/// pub struct HttpRangeReader {
441///     client: reqwest::blocking::Client,
442///     url: String,
443/// }
444///
445/// impl HttpRangeReader {
446///     pub fn new(url: String) -> Self {
447///         Self {
448///             client: reqwest::blocking::Client::new(),
449///             url,
450///         }
451///     }
452/// }
453///
454/// impl ByteRangeReader for HttpRangeReader {
455///     type Error = MdfError;
456///     
457///     fn read_range(&mut self, offset: u64, length: u64) -> Result<Vec<u8>, Self::Error> {
458///         let range_header = format!("bytes={}-{}", offset, offset + length - 1);
459///         
460///         let response = self.client
461///             .get(&self.url)
462///             .header("Range", range_header)
463///             .send()
464///             .map_err(|e| MdfError::BlockSerializationError(format!("HTTP error: {}", e)))?;
465///         
466///         if !response.status().is_success() {
467///             return Err(MdfError::BlockSerializationError(
468///                 format!("HTTP error: {}", response.status())
469///             ));
470///         }
471///         
472///         let bytes = response.bytes()
473///             .map_err(|e| MdfError::BlockSerializationError(format!("Response error: {}", e)))?;
474///         
475///         Ok(bytes.to_vec())
476///     }
477/// }
478/// ```
479pub struct _HttpRangeReaderExample;
480
481impl MdfIndex {
482    /// Create an index from an MDF file
483    pub fn from_file(file_path: &str) -> Result<Self> {
484        let mdf = MDF::from_file(file_path)?;
485        let file_size = std::fs::metadata(file_path).map_err(Error::IOError)?.len();
486
487        let mut indexed_groups = Vec::new();
488
489        for group in mdf.channel_groups() {
490            let mut indexed_channels = Vec::new();
491            let mmap = group.mmap(); // Get memory mapped file data for resolving conversions
492
493            // Index each channel in the group
494            for channel in group.channels() {
495                let block = channel.block();
496
497                // Clone and resolve conversion dependencies if present
498                let resolved_conversion = if let Some(mut conversion) = block.conversion.clone() {
499                    // Resolve all dependencies for this conversion block
500                    if let Err(e) = conversion.resolve_all_dependencies(mmap) {
501                        eprintln!(
502                            "Warning: Failed to resolve conversion dependencies for channel '{}': {}",
503                            block.name.as_deref().unwrap_or("<unnamed>"),
504                            e
505                        );
506                    }
507                    Some(conversion)
508                } else {
509                    None
510                };
511
512                let indexed_channel = IndexedChannel {
513                    name: channel.name()?,
514                    unit: channel.unit()?,
515                    data_type: block.data_type,
516                    byte_offset: block.byte_offset,
517                    bit_offset: block.bit_offset,
518                    bit_count: block.bit_count,
519                    channel_type: block.channel_type,
520                    flags: block.flags,
521                    pos_invalidation_bit: block.pos_invalidation_bit,
522                    conversion: resolved_conversion,
523                    vlsd_data_address: if block.channel_type == 1 && block.data_addr != 0 {
524                        Some(block.data_addr)
525                    } else {
526                        None
527                    },
528                };
529                indexed_channels.push(indexed_channel);
530            }
531
532            // Get data block information
533            let data_blocks = Self::extract_data_blocks(&group)?;
534
535            let indexed_group = IndexedChannelGroup {
536                name: group.name()?,
537                comment: group.comment()?,
538                record_id_size: group.raw_data_group().block.record_id_size,
539                record_size: group.raw_channel_group().block.record_size,
540                invalidation_bytes: group.raw_channel_group().block.invalidation_size,
541                record_count: group.raw_channel_group().block.cycle_count,
542                channels: indexed_channels,
543                data_blocks,
544            };
545            indexed_groups.push(indexed_group);
546        }
547
548        Ok(MdfIndex {
549            file_size,
550            channel_groups: indexed_groups,
551        })
552    }
553
554    /// Create an index from a file using streaming reads (minimal memory usage).
555    ///
556    /// This method reads only the metadata blocks needed to build the index,
557    /// without loading the entire file into memory. Ideal for large files.
558    ///
559    /// # Arguments
560    /// * `file_path` - Path to the MDF file
561    ///
562    /// # Example
563    /// ```no_run
564    /// use mdf4_rs::MdfIndex;
565    ///
566    /// let index = MdfIndex::from_file_streaming("large_recording.mf4")?;
567    /// # Ok::<(), mdf4_rs::Error>(())
568    /// ```
569    pub fn from_file_streaming(file_path: &str) -> Result<Self> {
570        let file_size = std::fs::metadata(file_path).map_err(Error::IOError)?.len();
571        let mut reader = BufferedRangeReader::new(file_path)?;
572        Self::from_reader(&mut reader, file_size)
573    }
574
575    /// Create an index from any byte range reader.
576    ///
577    /// This is the most flexible method, allowing index creation from files,
578    /// HTTP sources, or any other data source implementing `ByteRangeReader`.
579    ///
580    /// # Arguments
581    /// * `reader` - Any implementation of `ByteRangeReader`
582    /// * `file_size` - Total size of the file in bytes
583    pub fn from_reader<R: ByteRangeReader<Error = Error>>(
584        reader: &mut R,
585        file_size: u64,
586    ) -> Result<Self> {
587        // Read and validate ID block (64 bytes at offset 0)
588        let id_bytes = reader.read_range(0, 64)?;
589        let _id_block = IdentificationBlock::from_bytes(&id_bytes)?;
590
591        // Read HD block (104 bytes at offset 64)
592        let hd_bytes = reader.read_range(64, 104)?;
593        let header = HeaderBlock::from_bytes(&hd_bytes)?;
594
595        let mut indexed_groups = Vec::new();
596
597        // Follow the DG chain
598        let mut dg_addr = header.first_dg_addr;
599        while dg_addr != 0 {
600            // Read DG block (64 bytes)
601            let dg_bytes = reader.read_range(dg_addr, 64)?;
602            let dg_block = DataGroupBlock::from_bytes(&dg_bytes)?;
603
604            // Follow the CG chain within this DG
605            let mut cg_addr = dg_block.first_cg_addr;
606            while cg_addr != 0 {
607                // Read CG block (104 bytes)
608                let cg_bytes = reader.read_range(cg_addr, 104)?;
609                let cg_block = ChannelGroupBlock::from_bytes(&cg_bytes)?;
610
611                // Read CG name if present
612                let cg_name = Self::read_text_block(reader, cg_block.acq_name_addr)?;
613                let cg_comment = Self::read_text_block(reader, cg_block.comment_addr)?;
614
615                // Follow the CN chain within this CG
616                let mut indexed_channels = Vec::new();
617                let mut cn_addr = cg_block.first_ch_addr;
618                while cn_addr != 0 {
619                    // Read CN block (160 bytes)
620                    let cn_bytes = reader.read_range(cn_addr, 160)?;
621                    let cn_block = ChannelBlock::from_bytes(&cn_bytes)?;
622
623                    // Read channel name
624                    let ch_name = Self::read_text_block(reader, cn_block.name_addr)?;
625
626                    // Read unit
627                    let ch_unit = Self::read_text_block(reader, cn_block.unit_addr)?;
628
629                    // Read and resolve conversion block if present
630                    let conversion =
631                        Self::read_conversion_block_streaming(reader, cn_block.conversion_addr)?;
632
633                    let indexed_channel = IndexedChannel {
634                        name: ch_name,
635                        unit: ch_unit,
636                        data_type: cn_block.data_type,
637                        byte_offset: cn_block.byte_offset,
638                        bit_offset: cn_block.bit_offset,
639                        bit_count: cn_block.bit_count,
640                        channel_type: cn_block.channel_type,
641                        flags: cn_block.flags,
642                        pos_invalidation_bit: cn_block.pos_invalidation_bit,
643                        conversion,
644                        vlsd_data_address: if cn_block.channel_type == 1 && cn_block.data_addr != 0
645                        {
646                            Some(cn_block.data_addr)
647                        } else {
648                            None
649                        },
650                    };
651                    indexed_channels.push(indexed_channel);
652
653                    cn_addr = cn_block.next_ch_addr;
654                }
655
656                // Extract data block info for this CG
657                let data_blocks =
658                    Self::extract_data_blocks_streaming(reader, dg_block.data_block_addr)?;
659
660                let indexed_group = IndexedChannelGroup {
661                    name: cg_name,
662                    comment: cg_comment,
663                    record_id_size: dg_block.record_id_size,
664                    record_size: cg_block.record_size,
665                    invalidation_bytes: cg_block.invalidation_size,
666                    record_count: cg_block.cycle_count,
667                    channels: indexed_channels,
668                    data_blocks,
669                };
670                indexed_groups.push(indexed_group);
671
672                cg_addr = cg_block.next_cg_addr;
673            }
674
675            dg_addr = dg_block.next_dg_addr;
676        }
677
678        Ok(MdfIndex {
679            file_size,
680            channel_groups: indexed_groups,
681        })
682    }
683
684    /// Read a text block at the given address, returning None if address is 0.
685    fn read_text_block<R: ByteRangeReader<Error = Error>>(
686        reader: &mut R,
687        addr: u64,
688    ) -> Result<Option<String>> {
689        if addr == 0 {
690            return Ok(None);
691        }
692
693        // First read the header to get block length (24 bytes)
694        let header_bytes = reader.read_range(addr, 24)?;
695        let header = BlockHeader::from_bytes(&header_bytes)?;
696
697        // Now read the full block
698        let block_bytes = reader.read_range(addr, header.length)?;
699        let text_block = TextBlock::from_bytes(&block_bytes)?;
700
701        Ok(Some(text_block.text))
702    }
703
704    /// Read and parse a conversion block at the given address.
705    fn read_conversion_block_streaming<R: ByteRangeReader<Error = Error>>(
706        reader: &mut R,
707        addr: u64,
708    ) -> Result<Option<ConversionBlock>> {
709        if addr == 0 {
710            return Ok(None);
711        }
712
713        // First read the header to get block length
714        let header_bytes = reader.read_range(addr, 24)?;
715        let header = BlockHeader::from_bytes(&header_bytes)?;
716
717        // Read the full conversion block
718        let block_bytes = reader.read_range(addr, header.length)?;
719        let mut conv_block = ConversionBlock::from_bytes(&block_bytes)?;
720
721        // Resolve references based on conversion type
722        Self::resolve_conversion_refs(reader, &mut conv_block)?;
723
724        Ok(Some(conv_block))
725    }
726
727    /// Resolve references in a conversion block based on its type.
728    fn resolve_conversion_refs<R: ByteRangeReader<Error = Error>>(
729        reader: &mut R,
730        conv: &mut ConversionBlock,
731    ) -> Result<()> {
732        match conv.conversion_type {
733            // Algebraic conversion - first cc_ref is formula text
734            ConversionType::Algebraic => {
735                if let Some(&formula_addr) = conv.refs.first() {
736                    if formula_addr != 0 {
737                        conv.formula = Self::read_text_block(reader, formula_addr)?;
738                    }
739                }
740            }
741            // Text-based conversions - resolve text references
742            ConversionType::ValueToText
743            | ConversionType::RangeToText
744            | ConversionType::TextToValue
745            | ConversionType::TextToText
746            | ConversionType::BitfieldText => {
747                let mut resolved = BTreeMap::new();
748                for (idx, &ref_addr) in conv.refs.iter().enumerate() {
749                    if ref_addr != 0 {
750                        // Check if this is a text block or nested conversion
751                        let header_bytes = reader.read_range(ref_addr, 24)?;
752                        let header = BlockHeader::from_bytes(&header_bytes)?;
753
754                        if header.id == "##TX" || header.id == "##MD" {
755                            if let Ok(Some(text)) = Self::read_text_block(reader, ref_addr) {
756                                resolved.insert(idx, text);
757                            }
758                        }
759                        // Skip nested conversions for now - they're complex
760                    }
761                }
762                if !resolved.is_empty() {
763                    conv.resolved_texts = Some(resolved);
764                }
765            }
766            // Linear and other numeric conversions don't need text resolution
767            _ => {}
768        }
769
770        Ok(())
771    }
772
773    /// Extract data block information using streaming reads.
774    fn extract_data_blocks_streaming<R: ByteRangeReader<Error = Error>>(
775        reader: &mut R,
776        data_addr: u64,
777    ) -> Result<Vec<DataBlockInfo>> {
778        let mut data_blocks = Vec::new();
779        let mut current_addr = data_addr;
780
781        while current_addr != 0 {
782            // Read block header (24 bytes)
783            let header_bytes = reader.read_range(current_addr, 24)?;
784            let header = BlockHeader::from_bytes(&header_bytes)?;
785
786            match header.id.as_str() {
787                "##DT" | "##DV" => {
788                    data_blocks.push(DataBlockInfo {
789                        file_offset: current_addr,
790                        size: header.length,
791                        is_compressed: false,
792                    });
793                    current_addr = 0;
794                }
795                "##DZ" => {
796                    data_blocks.push(DataBlockInfo {
797                        file_offset: current_addr,
798                        size: header.length,
799                        is_compressed: true,
800                    });
801                    current_addr = 0;
802                }
803                "##DL" => {
804                    // Read the full DL block
805                    let dl_bytes = reader.read_range(current_addr, header.length)?;
806                    let dl_block = DataListBlock::from_bytes(&dl_bytes)?;
807
808                    // Process each fragment
809                    for &fragment_addr in &dl_block.data_block_addrs {
810                        if fragment_addr != 0 {
811                            let frag_header_bytes = reader.read_range(fragment_addr, 24)?;
812                            let frag_header = BlockHeader::from_bytes(&frag_header_bytes)?;
813
814                            data_blocks.push(DataBlockInfo {
815                                file_offset: fragment_addr,
816                                size: frag_header.length,
817                                is_compressed: frag_header.id == "##DZ",
818                            });
819                        }
820                    }
821
822                    current_addr = dl_block.next_dl_addr;
823                }
824                _ => {
825                    // Unknown block type, stop
826                    current_addr = 0;
827                }
828            }
829        }
830
831        Ok(data_blocks)
832    }
833
834    /// Extract data block information from a channel group
835    fn extract_data_blocks(
836        group: &crate::channel_group::ChannelGroup,
837    ) -> Result<Vec<DataBlockInfo>> {
838        let mut data_blocks = Vec::new();
839        let raw_data_group = group.raw_data_group();
840        let mmap = group.mmap();
841
842        // Start at the group's primary data pointer
843        let mut current_block_address = raw_data_group.block.data_block_addr;
844        while current_block_address != 0 {
845            let byte_offset = current_block_address as usize;
846
847            // Read the block header
848            let block_header = BlockHeader::from_bytes(&mmap[byte_offset..byte_offset + 24])?;
849
850            match block_header.id.as_str() {
851                "##DT" | "##DV" => {
852                    // Single contiguous DataBlock
853                    let data_block_info = DataBlockInfo {
854                        file_offset: current_block_address,
855                        size: block_header.length,
856                        is_compressed: false,
857                    };
858                    data_blocks.push(data_block_info);
859                    // No list to follow, we're done
860                    current_block_address = 0;
861                }
862                "##DZ" => {
863                    // Compressed data block
864                    let data_block_info = DataBlockInfo {
865                        file_offset: current_block_address,
866                        size: block_header.length,
867                        is_compressed: true,
868                    };
869                    data_blocks.push(data_block_info);
870                    current_block_address = 0;
871                }
872                "##DL" => {
873                    // Fragmented list of data blocks
874                    let data_list_block = DataListBlock::from_bytes(&mmap[byte_offset..])?;
875
876                    // Parse each fragment in this list
877                    for &fragment_address in &data_list_block.data_block_addrs {
878                        let fragment_offset = fragment_address as usize;
879                        let fragment_header =
880                            BlockHeader::from_bytes(&mmap[fragment_offset..fragment_offset + 24])?;
881
882                        let is_compressed = fragment_header.id == "##DZ";
883                        let data_block_info = DataBlockInfo {
884                            file_offset: fragment_address,
885                            size: fragment_header.length,
886                            is_compressed,
887                        };
888                        data_blocks.push(data_block_info);
889                    }
890
891                    // Move to the next DLBLOCK in the chain (0 = end)
892                    current_block_address = data_list_block.next_dl_addr;
893                }
894
895                unexpected_id => {
896                    return Err(Error::BlockIDError {
897                        actual: unexpected_id.to_string(),
898                        expected: "##DT / ##DV / ##DL / ##DZ".to_string(),
899                    });
900                }
901            }
902        }
903
904        Ok(data_blocks)
905    }
906
907    /// Save the index to a JSON file.
908    ///
909    /// Requires the `serde` and `serde_json` features.
910    #[cfg(feature = "serde_json")]
911    pub fn save_to_file(&self, index_path: &str) -> Result<()> {
912        let json = serde_json::to_string_pretty(self).map_err(|e| {
913            Error::BlockSerializationError(format!("JSON serialization failed: {}", e))
914        })?;
915
916        std::fs::write(index_path, json).map_err(Error::IOError)?;
917
918        Ok(())
919    }
920
921    /// Load an index from a JSON file.
922    ///
923    /// Requires the `serde` and `serde_json` features.
924    #[cfg(feature = "serde_json")]
925    pub fn load_from_file(index_path: &str) -> Result<Self> {
926        let json = std::fs::read_to_string(index_path).map_err(Error::IOError)?;
927
928        let index: MdfIndex = serde_json::from_str(&json).map_err(|e| {
929            Error::BlockSerializationError(format!("JSON deserialization failed: {}", e))
930        })?;
931
932        Ok(index)
933    }
934
935    /// Read channel values using the index and a byte range reader
936    ///
937    /// # Returns
938    /// A vector of `Option<DecodedValue>` where:
939    /// - `Some(value)` represents a valid decoded value
940    /// - `None` represents an invalid value (invalidation bit set or decoding failed)
941    pub fn read_channel_values<R: ByteRangeReader<Error = Error>>(
942        &self,
943        group_index: usize,
944        channel_index: usize,
945        reader: &mut R,
946    ) -> Result<Vec<Option<DecodedValue>>> {
947        let group = self
948            .channel_groups
949            .get(group_index)
950            .ok_or_else(|| Error::BlockSerializationError("Invalid group index".to_string()))?;
951
952        let channel = group
953            .channels
954            .get(channel_index)
955            .ok_or_else(|| Error::BlockSerializationError("Invalid channel index".to_string()))?;
956
957        // Handle VLSD channels differently
958        if channel.channel_type == 1 && channel.vlsd_data_address.is_some() {
959            return self.read_vlsd_channel_values(group, channel, reader);
960        }
961
962        // For regular channels, read from data blocks
963        self.read_regular_channel_values(group, channel, reader)
964    }
965
966    /// Read values for a regular (non-VLSD) channel using byte range reader
967    fn read_regular_channel_values<R: ByteRangeReader<Error = Error>>(
968        &self,
969        group: &IndexedChannelGroup,
970        channel: &IndexedChannel,
971        reader: &mut R,
972    ) -> Result<Vec<Option<DecodedValue>>> {
973        // Record structure: record_id + data_bytes + invalidation_bytes
974        let record_size = group.record_id_size as usize
975            + group.record_size as usize
976            + group.invalidation_bytes as usize;
977        let mut values = Vec::new();
978
979        // Read from each data block
980        for data_block in &group.data_blocks {
981            // Handle compression if needed
982            if data_block.is_compressed {
983                // TODO: Implement decompression for DZ blocks
984                return Err(Error::BlockSerializationError(
985                    "Compressed blocks not yet supported in index reader".to_string(),
986                ));
987            }
988
989            // Read the block data (skip 24-byte block header)
990            let block_data =
991                reader.read_range(data_block.file_offset + 24, data_block.size - 24)?;
992
993            // Process records in this block
994            let record_count = block_data.len() / record_size;
995            for i in 0..record_count {
996                let record_start = i * record_size;
997                let record_end = record_start + record_size;
998                let record = &block_data[record_start..record_end];
999
1000                // Create a ChannelBlock for decoding
1001                let temp_channel_block = ChannelBlock {
1002                    header: BlockHeader {
1003                        id: "##CN".to_string(),
1004                        reserved: 0,
1005                        length: 160,
1006                        link_count: 8,
1007                    },
1008                    next_ch_addr: 0,
1009                    component_addr: 0,
1010                    name_addr: 0,
1011                    source_addr: 0,
1012                    conversion_addr: 0,
1013                    data_addr: 0,
1014                    unit_addr: 0,
1015                    comment_addr: 0,
1016                    channel_type: channel.channel_type,
1017                    sync_type: 0,
1018                    data_type: channel.data_type,
1019                    bit_offset: channel.bit_offset,
1020                    byte_offset: channel.byte_offset,
1021                    bit_count: channel.bit_count,
1022                    flags: channel.flags,
1023                    pos_invalidation_bit: channel.pos_invalidation_bit,
1024                    precision: 0,
1025                    reserved1: 0,
1026                    attachment_count: 0,
1027                    min_raw_value: 0.0,
1028                    max_raw_value: 0.0,
1029                    lower_limit: 0.0,
1030                    upper_limit: 0.0,
1031                    lower_ext_limit: 0.0,
1032                    upper_ext_limit: 0.0,
1033                    name: channel.name.clone(),
1034                    conversion: channel.conversion.clone(),
1035                };
1036
1037                // Decode with validity checking
1038                if let Some(decoded) = decode_channel_value_with_validity(
1039                    record,
1040                    group.record_id_size as usize,
1041                    group.record_size,
1042                    &temp_channel_block,
1043                ) {
1044                    if decoded.is_valid {
1045                        // Apply conversion if present
1046                        let final_value = if let Some(conversion) = &channel.conversion {
1047                            conversion.apply_decoded(decoded.value, &[])?
1048                        } else {
1049                            decoded.value
1050                        };
1051                        values.push(Some(final_value));
1052                    } else {
1053                        // Invalid sample
1054                        values.push(None);
1055                    }
1056                } else {
1057                    // Decoding failed
1058                    values.push(None);
1059                }
1060            }
1061        }
1062
1063        Ok(values)
1064    }
1065
1066    /// Read values for a VLSD channel
1067    fn read_vlsd_channel_values<R: ByteRangeReader<Error = Error>>(
1068        &self,
1069        _group: &IndexedChannelGroup,
1070        _channel: &IndexedChannel,
1071        _reader: &mut R,
1072    ) -> Result<Vec<Option<DecodedValue>>> {
1073        // TODO: Implement VLSD channel reading
1074        Err(Error::BlockSerializationError(
1075            "VLSD channels not yet supported in index reader".to_string(),
1076        ))
1077    }
1078
1079    /// Get channel information for a specific group and channel
1080    pub fn get_channel_info(
1081        &self,
1082        group_index: usize,
1083        channel_index: usize,
1084    ) -> Option<&IndexedChannel> {
1085        self.channel_groups
1086            .get(group_index)?
1087            .channels
1088            .get(channel_index)
1089    }
1090
1091    /// List all channel groups with their basic information
1092    pub fn list_channel_groups(&self) -> Vec<(usize, &str, usize)> {
1093        self.channel_groups
1094            .iter()
1095            .enumerate()
1096            .map(|(i, group)| {
1097                (
1098                    i,
1099                    group.name.as_deref().unwrap_or("<unnamed>"),
1100                    group.channels.len(),
1101                )
1102            })
1103            .collect()
1104    }
1105
1106    /// List all channels in a specific group
1107    pub fn list_channels(&self, group_index: usize) -> Option<Vec<(usize, &str, &DataType)>> {
1108        let group = self.channel_groups.get(group_index)?;
1109        Some(
1110            group
1111                .channels
1112                .iter()
1113                .enumerate()
1114                .map(|(i, ch)| (i, ch.name.as_deref().unwrap_or("<unnamed>"), &ch.data_type))
1115                .collect(),
1116        )
1117    }
1118
1119    /// Get the exact byte ranges needed to read all data for a specific channel
1120    ///
1121    /// Returns a vector of (file_offset, length) tuples representing the byte ranges
1122    /// that need to be read from the file to get all data for the specified channel.
1123    ///
1124    /// # Arguments
1125    /// * `group_index` - Index of the channel group
1126    /// * `channel_index` - Index of the channel within the group
1127    ///
1128    /// # Returns
1129    /// * `Ok(Vec<(u64, u64)>)` - Vector of (offset, length) byte ranges
1130    /// * `Err(MdfError)` - If indices are invalid or channel type not supported
1131    pub fn get_channel_byte_ranges(
1132        &self,
1133        group_index: usize,
1134        channel_index: usize,
1135    ) -> Result<Vec<(u64, u64)>> {
1136        let group = self
1137            .channel_groups
1138            .get(group_index)
1139            .ok_or_else(|| Error::BlockSerializationError("Invalid group index".to_string()))?;
1140
1141        let channel = group
1142            .channels
1143            .get(channel_index)
1144            .ok_or_else(|| Error::BlockSerializationError("Invalid channel index".to_string()))?;
1145
1146        // Handle VLSD channels differently
1147        if channel.channel_type == 1 && channel.vlsd_data_address.is_some() {
1148            return Err(Error::BlockSerializationError(
1149                "VLSD channels not yet supported for byte range calculation".to_string(),
1150            ));
1151        }
1152
1153        // For regular channels, calculate byte ranges from data blocks
1154        self.calculate_regular_channel_byte_ranges(group, channel)
1155    }
1156
1157    /// Get the exact byte ranges for a specific record range of a channel
1158    ///
1159    /// This is useful when you only want to read a subset of records rather than all data.
1160    ///
1161    /// # Arguments
1162    /// * `group_index` - Index of the channel group
1163    /// * `channel_index` - Index of the channel within the group
1164    /// * `start_record` - Starting record index (0-based)
1165    /// * `record_count` - Number of records to read
1166    ///
1167    /// # Returns
1168    /// * `Ok(Vec<(u64, u64)>)` - Vector of (offset, length) byte ranges
1169    /// * `Err(MdfError)` - If indices are invalid, range is out of bounds, or channel type not supported
1170    pub fn get_channel_byte_ranges_for_records(
1171        &self,
1172        group_index: usize,
1173        channel_index: usize,
1174        start_record: u64,
1175        record_count: u64,
1176    ) -> Result<Vec<(u64, u64)>> {
1177        let group = self
1178            .channel_groups
1179            .get(group_index)
1180            .ok_or_else(|| Error::BlockSerializationError("Invalid group index".to_string()))?;
1181
1182        let channel = group
1183            .channels
1184            .get(channel_index)
1185            .ok_or_else(|| Error::BlockSerializationError("Invalid channel index".to_string()))?;
1186
1187        // Validate record range
1188        if start_record + record_count > group.record_count {
1189            return Err(Error::BlockSerializationError(format!(
1190                "Record range {}-{} exceeds total records {}",
1191                start_record,
1192                start_record + record_count - 1,
1193                group.record_count
1194            )));
1195        }
1196
1197        // Handle VLSD channels differently
1198        if channel.channel_type == 1 && channel.vlsd_data_address.is_some() {
1199            return Err(Error::BlockSerializationError(
1200                "VLSD channels not yet supported for byte range calculation".to_string(),
1201            ));
1202        }
1203
1204        self.calculate_channel_byte_ranges_for_records(group, channel, start_record, record_count)
1205    }
1206
1207    /// Calculate byte ranges for a regular (non-VLSD) channel for all records
1208    fn calculate_regular_channel_byte_ranges(
1209        &self,
1210        group: &IndexedChannelGroup,
1211        channel: &IndexedChannel,
1212    ) -> Result<Vec<(u64, u64)>> {
1213        self.calculate_channel_byte_ranges_for_records(group, channel, 0, group.record_count)
1214    }
1215
1216    /// Calculate byte ranges for a regular channel for a specific record range
1217    fn calculate_channel_byte_ranges_for_records(
1218        &self,
1219        group: &IndexedChannelGroup,
1220        channel: &IndexedChannel,
1221        start_record: u64,
1222        record_count: u64,
1223    ) -> Result<Vec<(u64, u64)>> {
1224        // Record structure: record_id + data_bytes + invalidation_bytes
1225        let record_size = group.record_id_size as usize
1226            + group.record_size as usize
1227            + group.invalidation_bytes as usize;
1228        let channel_offset_in_record = group.record_id_size as usize + channel.byte_offset as usize;
1229
1230        // Calculate how many bytes this channel needs per record
1231        let channel_bytes_per_record = if matches!(
1232            channel.data_type,
1233            DataType::StringLatin1
1234                | DataType::StringUtf8
1235                | DataType::StringUtf16LE
1236                | DataType::StringUtf16BE
1237                | DataType::ByteArray
1238                | DataType::MimeSample
1239                | DataType::MimeStream
1240        ) {
1241            channel.bit_count as usize / 8
1242        } else {
1243            (channel.bit_offset as usize + channel.bit_count as usize)
1244                .div_ceil(8)
1245                .max(1)
1246        };
1247
1248        let mut byte_ranges = Vec::new();
1249        let mut records_processed = 0u64;
1250
1251        for data_block in &group.data_blocks {
1252            if data_block.is_compressed {
1253                return Err(Error::BlockSerializationError(
1254                    "Compressed blocks not supported for byte range calculation".to_string(),
1255                ));
1256            }
1257
1258            let block_data_start = data_block.file_offset + 24; // Skip block header
1259            let block_data_size = data_block.size - 24;
1260            let records_in_block = block_data_size / record_size as u64;
1261
1262            // Determine which records from this block we need
1263            let block_start_record = records_processed;
1264            let block_end_record = records_processed + records_in_block;
1265
1266            let need_start = start_record.max(block_start_record);
1267            let need_end = (start_record + record_count).min(block_end_record);
1268
1269            if need_start < need_end {
1270                // We need some records from this block
1271                let first_record_in_block = need_start - block_start_record;
1272                let last_record_in_block = need_end - block_start_record - 1;
1273
1274                // Calculate byte range for the channel data in these records
1275                let first_channel_byte = block_data_start
1276                    + first_record_in_block * record_size as u64
1277                    + channel_offset_in_record as u64;
1278
1279                let last_channel_byte = block_data_start
1280                    + last_record_in_block * record_size as u64
1281                    + channel_offset_in_record as u64
1282                    + channel_bytes_per_record as u64
1283                    - 1;
1284
1285                let range_length = last_channel_byte - first_channel_byte + 1;
1286                byte_ranges.push((first_channel_byte, range_length));
1287            }
1288
1289            records_processed = block_end_record;
1290
1291            // Early exit if we've processed all needed records
1292            if records_processed >= start_record + record_count {
1293                break;
1294            }
1295        }
1296
1297        Ok(byte_ranges)
1298    }
1299
1300    /// Get a summary of byte ranges for a channel (total bytes, number of ranges)
1301    ///
1302    /// This is useful for understanding the I/O pattern before actually reading.
1303    ///
1304    /// # Returns
1305    /// * `(total_bytes, number_of_ranges)` - Total bytes to read and number of separate ranges
1306    pub fn get_channel_byte_summary(
1307        &self,
1308        group_index: usize,
1309        channel_index: usize,
1310    ) -> Result<(u64, usize)> {
1311        let ranges = self.get_channel_byte_ranges(group_index, channel_index)?;
1312        let total_bytes: u64 = ranges.iter().map(|(_, len)| len).sum();
1313        Ok((total_bytes, ranges.len()))
1314    }
1315
1316    /// Find a channel group index by name
1317    ///
1318    /// # Arguments
1319    /// * `group_name` - Name of the channel group to find
1320    ///
1321    /// # Returns
1322    /// * `Some(group_index)` if found
1323    /// * `None` if not found
1324    pub fn find_channel_group_by_name(&self, group_name: &str) -> Option<usize> {
1325        self.channel_groups
1326            .iter()
1327            .enumerate()
1328            .find(|(_, group)| group.name.as_deref() == Some(group_name))
1329            .map(|(index, _)| index)
1330    }
1331
1332    /// Find a channel index by name within a specific group
1333    ///
1334    /// # Arguments
1335    /// * `group_index` - Index of the channel group to search in
1336    /// * `channel_name` - Name of the channel to find
1337    ///
1338    /// # Returns
1339    /// * `Some(channel_index)` if found
1340    /// * `None` if group doesn't exist or channel not found
1341    pub fn find_channel_by_name(&self, group_index: usize, channel_name: &str) -> Option<usize> {
1342        let group = self.channel_groups.get(group_index)?;
1343
1344        group
1345            .channels
1346            .iter()
1347            .enumerate()
1348            .find(|(_, channel)| channel.name.as_deref() == Some(channel_name))
1349            .map(|(index, _)| index)
1350    }
1351
1352    /// Find a channel by name across all groups
1353    ///
1354    /// # Arguments
1355    /// * `channel_name` - Name of the channel to find
1356    ///
1357    /// # Returns
1358    /// * `Some((group_index, channel_index))` if found
1359    /// * `None` if not found
1360    pub fn find_channel_by_name_global(&self, channel_name: &str) -> Option<(usize, usize)> {
1361        for (group_index, group) in self.channel_groups.iter().enumerate() {
1362            for (channel_index, channel) in group.channels.iter().enumerate() {
1363                if channel.name.as_deref() == Some(channel_name) {
1364                    return Some((group_index, channel_index));
1365                }
1366            }
1367        }
1368        None
1369    }
1370
1371    /// Find all channels with a given name across all groups
1372    ///
1373    /// This is useful when the same channel name appears in multiple groups.
1374    ///
1375    /// # Arguments
1376    /// * `channel_name` - Name of the channels to find
1377    ///
1378    /// # Returns
1379    /// * `Vec<(group_index, channel_index)>` - All matching channels
1380    pub fn find_all_channels_by_name(&self, channel_name: &str) -> Vec<(usize, usize)> {
1381        let mut matches = Vec::new();
1382
1383        for (group_index, group) in self.channel_groups.iter().enumerate() {
1384            for (channel_index, channel) in group.channels.iter().enumerate() {
1385                if channel.name.as_deref() == Some(channel_name) {
1386                    matches.push((group_index, channel_index));
1387                }
1388            }
1389        }
1390
1391        matches
1392    }
1393
1394    /// Read channel values by name using a byte range reader
1395    ///
1396    /// Convenience method that finds the channel by name and reads its values.
1397    /// If multiple channels have the same name, uses the first one found.
1398    ///
1399    /// # Arguments
1400    /// * `channel_name` - Name of the channel to read
1401    /// * `reader` - Byte range reader implementation
1402    ///
1403    /// # Returns
1404    /// * `Ok(Vec<Option<DecodedValue>>)` - Channel values (None for invalid samples)
1405    /// * `Err(MdfError)` - If channel not found or reading fails
1406    pub fn read_channel_values_by_name<R: ByteRangeReader<Error = Error>>(
1407        &self,
1408        channel_name: &str,
1409        reader: &mut R,
1410    ) -> Result<Vec<Option<DecodedValue>>> {
1411        let (group_index, channel_index) = self
1412            .find_channel_by_name_global(channel_name)
1413            .ok_or_else(|| {
1414                Error::BlockSerializationError(format!("Channel '{}' not found", channel_name))
1415            })?;
1416
1417        self.read_channel_values(group_index, channel_index, reader)
1418    }
1419
1420    /// Get byte ranges for a channel by name
1421    ///
1422    /// # Arguments
1423    /// * `channel_name` - Name of the channel
1424    ///
1425    /// # Returns
1426    /// * `Ok(Vec<(u64, u64)>)` - Byte ranges as (offset, length) tuples
1427    /// * `Err(MdfError)` - If channel not found or calculation fails
1428    pub fn get_channel_byte_ranges_by_name(&self, channel_name: &str) -> Result<Vec<(u64, u64)>> {
1429        let (group_index, channel_index) = self
1430            .find_channel_by_name_global(channel_name)
1431            .ok_or_else(|| {
1432                Error::BlockSerializationError(format!("Channel '{}' not found", channel_name))
1433            })?;
1434
1435        self.get_channel_byte_ranges(group_index, channel_index)
1436    }
1437
1438    /// Get channel information by name
1439    ///
1440    /// # Arguments
1441    /// * `channel_name` - Name of the channel
1442    ///
1443    /// # Returns
1444    /// * `Some((group_index, channel_index, &IndexedChannel))` - Channel info if found
1445    /// * `None` - If channel not found
1446    pub fn get_channel_info_by_name(
1447        &self,
1448        channel_name: &str,
1449    ) -> Option<(usize, usize, &IndexedChannel)> {
1450        let (group_index, channel_index) = self.find_channel_by_name_global(channel_name)?;
1451        let channel = self.get_channel_info(group_index, channel_index)?;
1452        Some((group_index, channel_index, channel))
1453    }
1454}