Skip to main content

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_str = std::str::from_utf8(&mmap[start..=end])
104            .map_err(|_| anyhow!("Invalid UTF-8 in header segment"))?;
105        Ok(offset_str.trim().parse::<usize>()?)
106    }
107    /// Parse bytes 10-17 from the memory map as the ASCII-encoded offset (in usize bytes) to the first byte of the TEXT segment:
108    fn get_text_offset_start(mmap: &Mmap) -> Result<usize> {
109        Self::get_offset_from_header(mmap, 10, 17)
110    }
111    /// Parse bytes 18-25 as the ASCII-encoded offset (in usize bytes) to the last byte of the TEXT segment:
112    fn get_text_offset_end(mmap: &Mmap) -> Result<usize> {
113        Self::get_offset_from_header(mmap, 18, 25)
114    }
115    /// Parse bytes 26-33 as the ASCII-encoded offset to the first byte of the DATA segment:
116    fn get_data_offset_start(mmap: &Mmap) -> Result<usize> {
117        Self::get_offset_from_header(mmap, 26, 33)
118    }
119    /// Parse bytes 34-41 as the ASCII-encoded offset to the last byte of the DATA segment:
120    fn get_data_offset_end(mmap: &Mmap) -> Result<usize> {
121        Self::get_offset_from_header(mmap, 34, 41)
122    }
123    /// Parse bytes 42-49 as the ASCII-encoded offset to the first byte of the ANALYSIS segment:
124    fn get_analysis_offset_start(mmap: &Mmap) -> Result<usize> {
125        Self::get_offset_from_header(mmap, 42, 49)
126    }
127    /// Parse bytes 50-57 as the ASCII-encoded offset to the last byte of the ANALYSIS segment:
128    fn get_analysis_offset_end(mmap: &Mmap) -> Result<usize> {
129        Self::get_offset_from_header(mmap, 50, 57)
130    }
131    /// Returns the byte offsets for the TEXT segment
132    fn get_text_offsets(mmap: &Mmap) -> Result<RangeInclusive<usize>> {
133        let text_offset_start = Self::get_text_offset_start(mmap)?;
134        let text_offset_end = Self::get_text_offset_end(mmap)?;
135        Ok(text_offset_start..=text_offset_end)
136    }
137    /// Returns the byte offsets for the DATA segment
138    fn get_data_offsets(mmap: &Mmap) -> Result<RangeInclusive<usize>> {
139        let data_offset_start = Self::get_data_offset_start(mmap)?;
140        let data_offset_end = Self::get_data_offset_end(mmap)?;
141        Ok(data_offset_start..=data_offset_end)
142    }
143    /// Returns the byte offsets for the ANALYSIS segment
144    fn get_analysis_offsets(mmap: &Mmap) -> Result<RangeInclusive<usize>> {
145        let analysis_offset_start = Self::get_analysis_offset_start(mmap)?;
146        let analysis_offset_end = Self::get_analysis_offset_end(mmap)?;
147        Ok(analysis_offset_start..=analysis_offset_end)
148    }
149    /// Debug utility to print FCS file segment offsets
150    ///
151    /// This function prints detailed information about the header segment
152    /// and the byte offsets for TEXT, DATA, and ANALYSIS segments.
153    /// Useful for debugging file parsing issues.
154    ///
155    /// # Arguments
156    /// * `mmap` - Memory-mapped view of the FCS file
157    ///
158    /// # Errors
159    /// Will return `Err` if offsets cannot be read from the header
160    pub fn check_fcs_offsets(mmap: &Mmap) -> Result<()> {
161        println!(
162            "HEADER (first 58 bytes): {:?}",
163            std::str::from_utf8(&mmap[0..58]).unwrap_or("<invalid utf-8>")
164        );
165        println!(
166            "TEXT segment start offset: {:?}",
167            Self::get_text_offset_start(mmap)?
168        );
169        println!(
170            "TEXT segment end offset: {:?}",
171            Self::get_text_offset_end(mmap)?
172        );
173        println!(
174            "DATA segment start offset: {:?}",
175            Self::get_data_offset_start(mmap)?
176        );
177        println!(
178            "DATA segment end offset: {:?}",
179            Self::get_data_offset_end(mmap)?
180        );
181        println!(
182            "ANALYSIS segment start offset (optional): {:?}",
183            Self::get_analysis_offset_start(mmap)
184        );
185        println!(
186            "ANALYSIS segment end offset (optional): {:?}",
187            Self::get_analysis_offset_end(mmap)
188        );
189        // print from byte 4700 to 5210 (end of text, beginning of data)
190        println!(
191            "header range of TEXT: {:?}",
192            std::str::from_utf8(&mmap[4700..=5216]).unwrap_or("<invalid utf-8>")
193        );
194        Ok(())
195    }
196}
197impl Default for Header {
198    fn default() -> Self {
199        Self::new()
200    }
201}