use std::io::{Read, Seek};
use crate::index::lucene::codec_header::read_codec_header;
use crate::index::lucene::read::{LuceneReadError, LuceneResult, Reader};
pub const COMPOUND_DATA_CODEC_NAME: &str = "CompoundFileWriterData";
pub const COMPOUND_ENTRY_CODEC_NAME: &str = "CompoundFileWriterEntries";
pub const COMPOUND_VERSION: i32 = 0;
const NAME_BOUND: usize = 4 * 1024;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CompoundEntry {
pub name: String,
pub offset: i64,
pub length: i64,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CompoundTableOfContents {
pub entries: Vec<CompoundEntry>,
}
impl CompoundTableOfContents {
#[must_use]
pub fn entry(&self, name: &str) -> Option<&CompoundEntry> {
let stripped = strip_segment_name(name);
self.entries.iter().find(|entry| entry.name == stripped)
}
}
#[must_use]
pub fn strip_segment_name(name: &str) -> &str {
if !name.starts_with('_') {
return name;
}
let rest = &name[1..];
match rest.find(['.', '_']) {
Some(position) => &rest[position..],
None => name,
}
}
pub fn read_table_of_contents<Source: Read + Seek>(
reader: &mut Reader<Source>,
data_length: i64,
) -> LuceneResult<CompoundTableOfContents> {
read_codec_header(
reader,
COMPOUND_ENTRY_CODEC_NAME,
COMPOUND_VERSION,
COMPOUND_VERSION,
)?;
let count_offset = reader.position()?;
let count = reader.read_variable_int()?;
if count < 0 {
return Err(LuceneReadError::Malformed {
file: reader.file_name().to_owned(),
offset: count_offset,
details: format!("entry count {count} is negative"),
});
}
let remaining = reader.remaining()?;
let count = u64::from(count.unsigned_abs());
if count.saturating_mul(17) > remaining {
return Err(LuceneReadError::ImplausibleLength {
file: reader.file_name().to_owned(),
offset: count_offset,
length: count,
bound: remaining / 17,
});
}
let mut entries: Vec<CompoundEntry> = Vec::with_capacity(count as usize);
for _ in 0..count {
let name_offset = reader.position()?;
let name = reader.read_string(NAME_BOUND)?;
if name.starts_with('_') {
return Err(LuceneReadError::Malformed {
file: reader.file_name().to_owned(),
offset: name_offset,
details: format!(
"entry name {name:?} carries a segment prefix; Lucene's own writer \
strips it, and two names for one file is not a namespace froe reads"
),
});
}
if entries.iter().any(|entry| entry.name == name) {
return Err(LuceneReadError::Malformed {
file: reader.file_name().to_owned(),
offset: name_offset,
details: format!("duplicate entry {name:?}"),
});
}
let bounds_offset = reader.position()?;
let offset = reader.read_long()?;
let length = reader.read_long()?;
if offset < 0 || length < 0 || offset.saturating_add(length) > data_length {
return Err(LuceneReadError::Malformed {
file: reader.file_name().to_owned(),
offset: bounds_offset,
details: format!(
"entry {name:?} spans {offset}..{} of a compound file {data_length} \
bytes long",
offset.saturating_add(length)
),
});
}
entries.push(CompoundEntry {
name,
offset,
length,
});
}
Ok(CompoundTableOfContents { entries })
}
pub fn read_compound_data_header<Source: Read + Seek>(
reader: &mut Reader<Source>,
) -> LuceneResult<()> {
read_codec_header(
reader,
COMPOUND_DATA_CODEC_NAME,
COMPOUND_VERSION,
COMPOUND_VERSION,
)
.map(|_| ())
}