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