flow_fcs/
header.rs

1#[allow(clippy::module_name_repetitions)]
2use super::version::Version;
3use anyhow::{Result, anyhow};
4use core::str;
5// use image::EncodableLayout;
6use memmap3::Mmap;
7use serde::{Serialize, Serializer, ser::SerializeMap};
8use std::ops::RangeInclusive;
9
10/// Contains FCS version and byte offsets to text, data, and analysis segments
11///
12/// The header is the first segment of an FCS file (first 58 bytes) and contains:
13/// - The FCS version string (e.g., "FCS3.1")
14/// - Byte offsets to the TEXT segment (contains metadata/keywords)
15/// - Byte offsets to the DATA segment (contains event data)
16/// - Byte offsets to the ANALYSIS segment (optional, contains analysis results)
17#[derive(Clone, Debug, Hash)]
18pub struct Header {
19    pub version: Version,
20    pub text_offset: RangeInclusive<usize>,
21    pub data_offset: RangeInclusive<usize>,
22    pub analysis_offset: RangeInclusive<usize>,
23}
24impl Serialize for Header {
25    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26    where
27        S: Serializer,
28    {
29        let mut state = serializer.serialize_map(Some(2))?;
30        state.serialize_entry("version", &self.version)?;
31        state.serialize_entry("text_offset", &self.text_offset)?;
32        state.serialize_entry("data_offset", &self.data_offset)?;
33        state.serialize_entry("analysis_offset", &self.analysis_offset)?;
34        state.end()
35    }
36}
37
38impl Header {
39    #[must_use]
40    pub const fn new() -> Self {
41        Self {
42            version: Version::V3_1,
43            text_offset: 0..=0,
44            data_offset: 0..=0,
45            analysis_offset: 0..=0,
46        }
47    }
48    /// Returns a new Header struct from a memory map of an FCS file
49    /// # Errors
50    /// Will return `Err` if:
51    /// - the FCS version is not valid
52    /// - the number of spaces in the header segment is not 4
53    /// - the byte offsets for the TEXT, DATA, or ANALYSIS segments are not valid
54    pub fn from_mmap(mmap: &Mmap) -> Result<Self> {
55        // Check that bytes 6-9 are spaces:
56        Self::check_header_spaces(&mmap[6..=9])?;
57        // View the header segment and print the offsets to the console
58        // Self::check_fcs_offsets(mmap);
59
60        Ok(Self {
61            version: Self::get_version(mmap)?,
62            text_offset: Self::get_text_offsets(mmap)?,
63            data_offset: Self::get_data_offsets(mmap)?,
64            analysis_offset: Self::get_analysis_offsets(mmap)?,
65        })
66    }
67
68    /// Returns the FCS version from the first 6 bytes of the file
69    /// # Errors
70    /// Will return `Err` if the version is not valid
71    pub fn get_version(mmap: &Mmap) -> Result<Version> {
72        let version = String::from_utf8(mmap[..6].to_vec())?;
73        Self::check_fcs_version(&version)
74    }
75
76    /// Returns a valid FCS version enum after checking that the parsed string from the header is valid
77    /// # Errors
78    /// Will return `Err` if the version is not valid
79    pub fn check_fcs_version(version: &str) -> Result<Version> {
80        match version {
81            "FCS1.0" => Ok(Version::V1_0),
82            "FCS2.0" => Ok(Version::V2_0),
83            "FCS3.0" => Ok(Version::V3_0),
84            "FCS3.1" => Ok(Version::V3_1),
85            "FCS3.2" => Ok(Version::V3_2),
86            "FCS4.0" => Ok(Version::V4_0),
87            _ => Err(anyhow!("Invalid FCS version: {}", version)),
88        }
89    }
90    /// Check for valid number of spaces (4) in the HEADER segment
91    /// # Errors
92    /// Will return `Err` if the number of spaces is not 4
93    pub fn check_header_spaces(buffer: &[u8]) -> Result<()> {
94        if bytecount::count(buffer, b' ') != 4 {
95            return Err(anyhow!(
96                "Invalid number of spaces in header segment.  File may be corrupted."
97            ));
98        }
99        Ok(())
100    }
101    /// Parse an inclusive range of bytes from the memory map as an ASCII-encoded offset (in usize bytes)
102    fn get_offset_from_header(mmap: &Mmap, start: usize, end: usize) -> Result<usize> {
103        let offset_char = mmap[start..=end].as_ascii().expect("ascii not found");
104        // println!("Offset bytes {:?}-{:?}: {:?}", &start, &end, &offset_char);
105        // println!(
106        //     "returned: {:?}",
107        //     &offset_char.as_str().trim_ascii().parse::<usize>()?
108        // );
109        Ok(offset_char.as_str().trim_ascii().parse::<usize>()?)
110    }
111    /// Parse bytes 10-17 from the memory map as the ASCII-encoded offset (in usize bytes) to the first byte of the TEXT segment:
112    fn get_text_offset_start(mmap: &Mmap) -> Result<usize> {
113        Self::get_offset_from_header(mmap, 10, 17)
114    }
115    /// Parse bytes 18-25 as the ASCII-encoded offset (in usize bytes) to the last byte of the TEXT segment:
116    fn get_text_offset_end(mmap: &Mmap) -> Result<usize> {
117        Self::get_offset_from_header(mmap, 18, 25)
118    }
119    /// Parse bytes 26-33 as the ASCII-encoded offset to the first byte of the DATA segment:
120    fn get_data_offset_start(mmap: &Mmap) -> Result<usize> {
121        Self::get_offset_from_header(mmap, 26, 33)
122    }
123    /// Parse bytes 34-41 as the ASCII-encoded offset to the last byte of the DATA segment:
124    fn get_data_offset_end(mmap: &Mmap) -> Result<usize> {
125        Self::get_offset_from_header(mmap, 34, 41)
126    }
127    /// Parse bytes 42-49 as the ASCII-encoded offset to the first byte of the ANALYSIS segment:
128    fn get_analysis_offset_start(mmap: &Mmap) -> Result<usize> {
129        Self::get_offset_from_header(mmap, 42, 49)
130    }
131    /// Parse bytes 50-57 as the ASCII-encoded offset to the last byte of the ANALYSIS segment:
132    fn get_analysis_offset_end(mmap: &Mmap) -> Result<usize> {
133        Self::get_offset_from_header(mmap, 50, 57)
134    }
135    /// Returns the byte offsets for the TEXT segment
136    fn get_text_offsets(mmap: &Mmap) -> Result<RangeInclusive<usize>> {
137        let text_offset_start = Self::get_text_offset_start(mmap)?;
138        let text_offset_end = Self::get_text_offset_end(mmap)?;
139        Ok(text_offset_start..=text_offset_end)
140    }
141    /// Returns the byte offsets for the DATA segment
142    fn get_data_offsets(mmap: &Mmap) -> Result<RangeInclusive<usize>> {
143        let data_offset_start = Self::get_data_offset_start(mmap)?;
144        let data_offset_end = Self::get_data_offset_end(mmap)?;
145        Ok(data_offset_start..=data_offset_end)
146    }
147    /// Returns the byte offsets for the ANALYSIS segment
148    fn get_analysis_offsets(mmap: &Mmap) -> Result<RangeInclusive<usize>> {
149        let analysis_offset_start = Self::get_analysis_offset_start(mmap)?;
150        let analysis_offset_end = Self::get_analysis_offset_end(mmap)?;
151        Ok(analysis_offset_start..=analysis_offset_end)
152    }
153    /// Debug utility to print FCS file segment offsets
154    ///
155    /// This function prints detailed information about the header segment
156    /// and the byte offsets for TEXT, DATA, and ANALYSIS segments.
157    /// Useful for debugging file parsing issues.
158    ///
159    /// # Arguments
160    /// * `mmap` - Memory-mapped view of the FCS file
161    ///
162    /// # Errors
163    /// Will return `Err` if offsets cannot be read from the header
164    pub fn check_fcs_offsets(mmap: &Mmap) -> Result<()> {
165        println!("HEADER (first 58 bytes): {:?}", &mmap[0..58].as_ascii());
166        println!(
167            "TEXT segment start offset: {:?}",
168            Self::get_text_offset_start(mmap)?
169        );
170        println!(
171            "TEXT segment end offset: {:?}",
172            Self::get_text_offset_end(mmap)?
173        );
174        println!(
175            "DATA segment start offset: {:?}",
176            Self::get_data_offset_start(mmap)?
177        );
178        println!(
179            "DATA segment end offset: {:?}",
180            Self::get_data_offset_end(mmap)?
181        );
182        println!(
183            "ANALYSIS segment start offset (optional): {:?}",
184            Self::get_analysis_offset_start(mmap)
185        );
186        println!(
187            "ANALYSIS segment end offset (optional): {:?}",
188            Self::get_analysis_offset_end(mmap)
189        );
190        // print from byte 4700 to 5210 (end of text, beginning of data)
191        println!("header range of TEXT: {:?}", &mmap[4700..=5216].as_ascii());
192        Ok(())
193    }
194}
195impl Default for Header {
196    fn default() -> Self {
197        Self::new()
198    }
199}