use std::io::{Read, Seek};
use crate::index::lucene::codec_header::{
CODEC_MAGIC, read_codec_header, read_codec_header_without_magic,
};
use crate::index::lucene::read::{LuceneReadError, LuceneResult, Reader};
pub const SEGMENTS_GEN_FORMAT: i32 = -2;
pub const SEGMENTS_CODEC_NAME: &str = "segments";
pub const SEGMENTS_VERSION_40: i32 = 0;
pub const SEGMENTS_VERSION_46: i32 = 1;
pub const SEGMENT_INFO_CODEC_NAME: &str = "Lucene46SegmentInfo";
pub const SEGMENT_INFO_VERSION: i32 = 0;
pub const SEGMENTS_GEN_FILE_NAME: &str = "segments.gen";
pub const SEGMENTS_FILE_PREFIX: &str = "segments";
const STRING_BOUND: usize = 32 * 1024;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SegmentEntry {
pub name: String,
pub codec_name: String,
pub deletion_generation: i64,
pub deletion_count: i32,
pub field_infos_generation: i64,
pub info: SegmentInfo,
}
impl SegmentEntry {
#[must_use]
pub fn live_document_count(&self) -> i64 {
i64::from(self.info.document_count) - i64::from(self.deletion_count)
}
#[must_use]
pub fn deletions_file_name(&self) -> Option<String> {
match self.deletion_generation {
-1 => None,
0 => Some(format!("{}.{DELETIONS_EXTENSION}", self.name)),
generation if generation > 0 => Some(format!(
"{}_{}.{DELETIONS_EXTENSION}",
self.name,
to_base_36(generation)
)),
_ => None,
}
}
}
const DELETIONS_EXTENSION: &str = "del";
fn to_base_36(mut value: i64) -> String {
const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let mut rendered = Vec::new();
while value > 0 {
rendered.push(DIGITS[(value % 36) as usize]);
value /= 36;
}
rendered.reverse();
String::from_utf8(rendered).expect("every digit is ASCII")
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SegmentInfo {
pub lucene_version: String,
pub document_count: i32,
pub compound: bool,
pub diagnostics: Vec<(String, String)>,
pub files: Vec<String>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CommitFile {
pub file_name: String,
pub generation: i64,
pub format_version: i32,
pub version: i64,
pub counter: i32,
pub segments: Vec<SegmentEntry>,
pub user_data: Vec<(String, String)>,
pub generation_files: Vec<String>,
}
impl CommitFile {
#[must_use]
pub fn referenced_files(&self) -> Vec<String> {
let mut files = vec![self.file_name.clone()];
for segment in &self.segments {
files.extend(segment.info.files.iter().cloned());
if let Some(deletions) = segment.deletions_file_name() {
files.push(deletions);
}
}
files.extend(self.generation_files.iter().cloned());
files.sort();
files.dedup();
files
}
#[must_use]
pub fn live_document_count(&self) -> i64 {
self.segments
.iter()
.map(SegmentEntry::live_document_count)
.sum()
}
}
#[must_use]
pub fn generation_from_commit_file_name(name: &str) -> Option<i64> {
if name == SEGMENTS_FILE_PREFIX {
return Some(0);
}
let suffix = name.strip_prefix("segments_")?;
i64::from_str_radix(suffix, 36).ok()
}
#[must_use]
pub fn is_commit_file_name(name: &str) -> bool {
name != SEGMENTS_GEN_FILE_NAME && generation_from_commit_file_name(name).is_some()
}
#[must_use]
pub fn commit_generation(listing: &[String], hint: Option<i64>) -> Option<i64> {
let from_listing = listing
.iter()
.filter(|name| is_commit_file_name(name))
.filter_map(|name| generation_from_commit_file_name(name))
.max();
match (from_listing, hint) {
(Some(listed), Some(hinted)) => Some(listed.max(hinted)),
(Some(listed), None) => Some(listed),
(None, hinted) => hinted,
}
}
pub fn read_segments_gen<Source: Read + Seek>(reader: &mut Reader<Source>) -> Option<i64> {
let format = reader.read_int().ok()?;
if format != SEGMENTS_GEN_FORMAT {
return None;
}
let first = reader.read_long().ok()?;
let second = reader.read_long().ok()?;
(first == second).then_some(first)
}
pub fn read_segment_info<Source: Read + Seek>(
reader: &mut Reader<Source>,
) -> LuceneResult<SegmentInfo> {
read_codec_header(
reader,
SEGMENT_INFO_CODEC_NAME,
SEGMENT_INFO_VERSION,
SEGMENT_INFO_VERSION,
)?;
let lucene_version = reader.read_string(STRING_BOUND)?;
let offset = reader.position()?;
let document_count = reader.read_int()?;
if document_count < 0 {
return Err(LuceneReadError::Malformed {
file: reader.file_name().to_owned(),
offset,
details: format!("document count {document_count} is negative"),
});
}
let compound = reader.read_byte()? == 1;
let diagnostics = reader.read_string_map(STRING_BOUND)?;
let files = reader.read_string_set(STRING_BOUND)?;
let position = reader.position()?;
if position != reader.length() {
return Err(LuceneReadError::Malformed {
file: reader.file_name().to_owned(),
offset: position,
details: format!(
"{} trailing bytes; Lucene's own reader requires the file to be consumed \
exactly",
reader.length().saturating_sub(position)
),
});
}
Ok(SegmentInfo {
lucene_version,
document_count,
compound,
diagnostics,
files,
})
}
pub fn read_commit_file<Source, Open, InfoSource>(
reader: &mut Reader<Source>,
file_name: &str,
mut open_info: Open,
) -> LuceneResult<CommitFile>
where
Source: Read + Seek,
Open: FnMut(&str) -> LuceneResult<Reader<InfoSource>>,
InfoSource: Read + Seek,
{
let generation =
generation_from_commit_file_name(file_name).ok_or_else(|| LuceneReadError::Malformed {
file: file_name.to_owned(),
offset: 0,
details: "the name carries no commit generation".to_owned(),
})?;
let offset = reader.position()?;
let magic = reader.read_int()?;
if magic != CODEC_MAGIC {
return Err(LuceneReadError::Malformed {
file: file_name.to_owned(),
offset,
details: format!(
"first word {magic:#010x} is not Lucene's codec magic, so this is a \
Lucene 3.x commit file this froe does not read"
),
});
}
let header = read_codec_header_without_magic(
reader,
SEGMENTS_CODEC_NAME,
SEGMENTS_VERSION_40,
SEGMENTS_VERSION_46,
)?;
let version = reader.read_long()?;
let counter = reader.read_int()?;
let count_offset = reader.position()?;
let segment_count = reader.read_int()?;
if segment_count < 0 {
return Err(LuceneReadError::Malformed {
file: file_name.to_owned(),
offset: count_offset,
details: format!("segment count {segment_count} is negative"),
});
}
let mut segments = Vec::new();
let mut generation_files = Vec::new();
for _ in 0..segment_count {
let name = reader.read_string(STRING_BOUND)?;
let codec_name = reader.read_string(STRING_BOUND)?;
let info_name = format!("{name}.si");
let mut info_reader = open_info(&info_name)?;
let info = read_segment_info(&mut info_reader)?;
let deletion_generation = reader.read_long()?;
let deletion_offset = reader.position()?;
let deletion_count = reader.read_int()?;
if deletion_count < 0 || deletion_count > info.document_count {
return Err(LuceneReadError::Malformed {
file: file_name.to_owned(),
offset: deletion_offset,
details: format!(
"deletion count {deletion_count} is outside 0..={} for segment {name}",
info.document_count
),
});
}
let mut field_infos_generation = -1;
if header.version >= SEGMENTS_VERSION_46 {
field_infos_generation = reader.read_long()?;
let update_count = reader.read_counted_length()?;
for _ in 0..update_count {
let _generation = reader.read_long()?;
generation_files.extend(reader.read_string_set(STRING_BOUND)?);
}
}
segments.push(SegmentEntry {
name,
codec_name,
deletion_generation,
deletion_count,
field_infos_generation,
info,
});
}
let user_data = reader.read_string_map(STRING_BOUND)?;
let _checksum = reader.read_long()?;
Ok(CommitFile {
file_name: file_name.to_owned(),
generation,
format_version: header.version,
version,
counter,
segments,
user_data,
generation_files,
})
}