use super::super::package::{DocError, Result};
use zerocopy::{FromBytes, LE, U16, U32};
const FIB_BASE_SIZE: usize = 32;
#[derive(Debug, Clone)]
pub struct FileInformationBlock {
nfib: u16,
flags: u16,
which_table_stream: bool,
lid: u16,
data: Vec<u8>,
}
impl FileInformationBlock {
pub fn parse(word_document: &[u8]) -> Result<Self> {
if word_document.len() < FIB_BASE_SIZE {
return Err(DocError::Corrupted(
"WordDocument stream too short for FIB".to_string(),
));
}
let magic = U16::<LE>::read_from_bytes(&word_document[0..2])
.map(|v| v.get())
.unwrap_or(0);
let nfib = U16::<LE>::read_from_bytes(&word_document[2..4])
.map(|v| v.get())
.unwrap_or(0);
let lid = U16::<LE>::read_from_bytes(&word_document[6..8])
.map(|v| v.get())
.unwrap_or(0);
let flags = U16::<LE>::read_from_bytes(&word_document[10..12])
.map(|v| v.get())
.unwrap_or(0);
if magic != 0xA5EC && magic != 0xA5DC {
return Err(DocError::InvalidFormat(format!(
"Invalid FIB magic number: 0x{:04X}",
magic
)));
}
let which_table_stream = (flags & 0x0200) != 0;
let data = word_document.to_vec();
Ok(Self {
nfib,
flags,
which_table_stream,
lid,
data,
})
}
#[inline]
pub fn version(&self) -> u16 {
self.nfib
}
pub fn version_name(&self) -> &'static str {
match self.nfib {
0x0021 => "Word 1.0",
0x0045 => "Word 2.0",
0x0065 => "Word 6.0",
0x0067 => "Word 95 (7.0)",
0x00C1 => "Word 97",
0x00D9 => "Word 2000",
0x0101 => "Word 2002/2003",
0x010C => "Word 2007",
0x0112 => "Word 2010",
0x0113 => "Word 2013",
_ if self.nfib >= 0x00C1 => "Word 97+",
_ => "Unknown",
}
}
#[inline]
pub fn which_table_stream(&self) -> bool {
self.which_table_stream
}
#[inline]
pub fn is_encrypted(&self) -> bool {
(self.flags & 0x0100) != 0
}
#[inline]
pub fn language_id(&self) -> u16 {
self.lid
}
pub fn get_table_pointer(&self, index: usize) -> Option<(u32, u32)> {
let base_offset = 154;
let entry_offset = base_offset + (index * 8);
if entry_offset + 8 > self.data.len() {
return None;
}
let offset = U32::<LE>::read_from_bytes(&self.data[entry_offset..entry_offset + 4])
.map(|v| v.get())
.unwrap_or(0);
let length = U32::<LE>::read_from_bytes(&self.data[entry_offset + 4..entry_offset + 8])
.map(|v| v.get())
.unwrap_or(0);
Some((offset, length))
}
#[inline]
pub fn raw_data(&self) -> &[u8] {
&self.data
}
fn get_character_count(&self, index: usize) -> u32 {
let offset = 64 + 0xC + (index * 4);
if offset + 4 > self.data.len() {
return 0;
}
U32::<LE>::read_from_bytes(&self.data[offset..offset + 4])
.map(|v| v.get())
.unwrap_or(0)
}
pub fn get_main_doc_range(&self) -> (u32, u32) {
let ccp_text = self.get_character_count(0);
(0, ccp_text)
}
pub fn get_footnote_range(&self) -> Option<(u32, u32)> {
let base = self.get_character_count(0);
let ccp_ftn = self.get_character_count(1);
if ccp_ftn > 0 {
Some((base, base + ccp_ftn))
} else {
None
}
}
pub fn get_header_range(&self) -> Option<(u32, u32)> {
let base = self.get_character_count(0) + self.get_character_count(1);
let ccp_hdd = self.get_character_count(2);
if ccp_hdd > 0 {
Some((base, base + ccp_hdd))
} else {
None
}
}
pub fn get_comment_range(&self) -> Option<(u32, u32)> {
let base = self.get_character_count(0)
+ self.get_character_count(1)
+ self.get_character_count(2)
+ self.get_character_count(3); let ccp_atn = self.get_character_count(4);
if ccp_atn > 0 {
Some((base, base + ccp_atn))
} else {
None
}
}
pub fn get_endnote_range(&self) -> Option<(u32, u32)> {
let base = self.get_character_count(0)
+ self.get_character_count(1)
+ self.get_character_count(2)
+ self.get_character_count(3)
+ self.get_character_count(4);
let ccp_edn = self.get_character_count(5);
if ccp_edn > 0 {
Some((base, base + ccp_edn))
} else {
None
}
}
pub fn get_textbox_range(&self) -> Option<(u32, u32)> {
let base = self.get_character_count(0)
+ self.get_character_count(1)
+ self.get_character_count(2)
+ self.get_character_count(3)
+ self.get_character_count(4)
+ self.get_character_count(5);
let ccp_txbx = self.get_character_count(6);
if ccp_txbx > 0 {
Some((base, base + ccp_txbx))
} else {
None
}
}
pub fn get_header_textbox_range(&self) -> Option<(u32, u32)> {
let base = self.get_character_count(0)
+ self.get_character_count(1)
+ self.get_character_count(2)
+ self.get_character_count(3)
+ self.get_character_count(4)
+ self.get_character_count(5)
+ self.get_character_count(6);
let ccp_hdr_txbx = self.get_character_count(7);
if ccp_hdr_txbx > 0 {
Some((base, base + ccp_hdr_txbx))
} else {
None
}
}
pub fn get_all_subdoc_ranges(&self) -> Vec<(&'static str, u32, u32)> {
let mut ranges = Vec::new();
let (start, end) = self.get_main_doc_range();
if end > start {
ranges.push(("Main Document", start, end));
}
if let Some((start, end)) = self.get_footnote_range() {
ranges.push(("Footnotes", start, end));
}
if let Some((start, end)) = self.get_header_range() {
ranges.push(("Headers/Footers", start, end));
}
if let Some((start, end)) = self.get_comment_range() {
ranges.push(("Comments", start, end));
}
if let Some((start, end)) = self.get_endnote_range() {
ranges.push(("Endnotes", start, end));
}
if let Some((start, end)) = self.get_textbox_range() {
ranges.push(("Text Boxes", start, end));
}
if let Some((start, end)) = self.get_header_textbox_range() {
ranges.push(("Header Text Boxes", start, end));
}
ranges
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fib_min_size() {
let short_data = vec![0u8; 16];
let result = FileInformationBlock::parse(&short_data);
assert!(result.is_err());
}
#[test]
fn test_fib_magic_validation() {
let mut data = vec![0u8; 512];
data[0] = 0xFF;
data[1] = 0xFF;
let result = FileInformationBlock::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_fib_valid() {
let mut data = vec![0u8; 512];
data[0] = 0xEC;
data[1] = 0xA5;
data[2] = 0xC1;
data[3] = 0x00;
let result = FileInformationBlock::parse(&data);
assert!(result.is_ok());
let fib = result.unwrap();
assert_eq!(fib.version(), 0x00C1);
assert!(!fib.is_encrypted());
}
#[test]
fn test_fib_table_stream_flag() {
let mut data = vec![0u8; 512];
data[0] = 0xEC;
data[1] = 0xA5;
data[10] = 0x00;
data[11] = 0x02;
let fib = FileInformationBlock::parse(&data).unwrap();
assert!(fib.which_table_stream());
}
}