use std::io::{Read, Seek, SeekFrom};
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum LuceneReadError {
Truncated {
file: String,
offset: u64,
needed: u64,
remaining: u64,
},
NotALuceneFile {
file: String,
offset: u64,
found: i32,
},
WrongCodecName {
file: String,
offset: u64,
expected: String,
found: String,
},
UnsupportedFormatVersion {
file: String,
offset: u64,
codec: String,
version: i32,
minimum: i32,
maximum: i32,
},
ImplausibleLength {
file: String,
offset: u64,
length: u64,
bound: u64,
},
MalformedVariableInteger {
file: String,
offset: u64,
},
NotUtf8 {
file: String,
offset: u64,
},
Malformed {
file: String,
offset: u64,
details: String,
},
Source {
file: String,
details: String,
},
}
impl std::fmt::Display for LuceneReadError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated {
file,
offset,
needed,
remaining,
} => write!(
formatter,
"{file} ends early: {needed} bytes were needed at offset {offset} and \
{remaining} remain"
),
Self::NotALuceneFile {
file,
offset,
found,
} => write!(
formatter,
"{file} does not open with Lucene's codec magic at offset {offset}: found \
{found:#010x}, expected {CODEC_MAGIC_TEXT}"
),
Self::WrongCodecName {
file,
offset,
expected,
found,
} => write!(
formatter,
"{file} carries codec {found:?} at offset {offset} where {expected:?} was \
expected"
),
Self::UnsupportedFormatVersion {
file,
offset,
codec,
version,
minimum,
maximum,
} => write!(
formatter,
"{file} carries {codec} format version {version} at offset {offset}, \
outside the {minimum}..={maximum} this froe reads"
),
Self::ImplausibleLength {
file,
offset,
length,
bound,
} => write!(
formatter,
"{file} declares a length of {length} at offset {offset}, above the \
{bound} that could be valid there"
),
Self::MalformedVariableInteger { file, offset } => write!(
formatter,
"{file} holds a variable-length integer at offset {offset} whose \
continuation never ended"
),
Self::NotUtf8 { file, offset } => write!(
formatter,
"{file} holds a string at offset {offset} that is not UTF-8, which \
Lucene's own writer cannot produce"
),
Self::Malformed {
file,
offset,
details,
} => write!(
formatter,
"{file} is malformed at offset {offset}: {details}"
),
Self::Source { file, details } => {
write!(formatter, "reading {file} failed: {details}")
}
}
}
}
const CODEC_MAGIC_TEXT: &str = "0x3fd76c17";
impl std::error::Error for LuceneReadError {}
pub type LuceneResult<T> = std::result::Result<T, LuceneReadError>;
pub struct Reader<Source: Read + Seek> {
source: Source,
file_name: String,
length: u64,
}
impl<Source: Read + Seek> Reader<Source> {
pub fn new(source: Source, file_name: impl Into<String>, length: u64) -> Self {
Self {
source,
file_name: file_name.into(),
length,
}
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub fn length(&self) -> u64 {
self.length
}
pub fn position(&mut self) -> LuceneResult<u64> {
self.source
.stream_position()
.map_err(|error| self.source_error(&error))
}
pub fn remaining(&mut self) -> LuceneResult<u64> {
let position = self.position()?;
Ok(self.length.saturating_sub(position))
}
pub fn seek(&mut self, offset: u64) -> LuceneResult<()> {
self.source
.seek(SeekFrom::Start(offset))
.map(|_| ())
.map_err(|error| self.source_error(&error))
}
pub fn read_exact(&mut self, count: usize) -> LuceneResult<Vec<u8>> {
let offset = self.position()?;
let remaining = self.remaining()?;
let needed = count as u64;
if needed > remaining {
return Err(LuceneReadError::Truncated {
file: self.file_name.clone(),
offset,
needed,
remaining,
});
}
let mut bytes = vec![0u8; count];
self.source
.read_exact(&mut bytes)
.map_err(|error| self.source_error(&error))?;
Ok(bytes)
}
pub fn read_byte(&mut self) -> LuceneResult<u8> {
Ok(self.read_exact(1)?[0])
}
pub fn read_int(&mut self) -> LuceneResult<i32> {
let bytes = self.read_exact(4)?;
Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
pub fn read_long(&mut self) -> LuceneResult<i64> {
let bytes = self.read_exact(8)?;
Ok(i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]))
}
pub fn read_variable_int(&mut self) -> LuceneResult<i32> {
let offset = self.position()?;
let mut value: u32 = 0;
for group in 0..5u32 {
let byte = self.read_byte()?;
value |= u32::from(byte & 0x7f) << (group * 7);
if byte & 0x80 == 0 {
return Ok(value as i32);
}
}
Err(LuceneReadError::MalformedVariableInteger {
file: self.file_name.clone(),
offset,
})
}
pub fn read_string(&mut self, bound: usize) -> LuceneResult<String> {
let offset = self.position()?;
let length = self.read_variable_int()?;
let length = u64::try_from(length).map_err(|_| LuceneReadError::ImplausibleLength {
file: self.file_name.clone(),
offset,
length: 0,
bound: bound as u64,
})?;
let remaining = self.remaining()?;
let effective_bound = (bound as u64).min(remaining);
if length > effective_bound {
return Err(LuceneReadError::ImplausibleLength {
file: self.file_name.clone(),
offset,
length,
bound: effective_bound,
});
}
let bytes = self.read_exact(length as usize)?;
String::from_utf8(bytes).map_err(|_| LuceneReadError::NotUtf8 {
file: self.file_name.clone(),
offset,
})
}
pub fn read_string_set(&mut self, bound: usize) -> LuceneResult<Vec<String>> {
let count = self.read_counted_length()?;
let mut values = Vec::with_capacity(count);
for _ in 0..count {
values.push(self.read_string(bound)?);
}
Ok(values)
}
pub fn read_string_map(&mut self, bound: usize) -> LuceneResult<Vec<(String, String)>> {
let count = self.read_counted_length()?;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
let key = self.read_string(bound)?;
let value = self.read_string(bound)?;
entries.push((key, value));
}
Ok(entries)
}
pub fn read_counted_length(&mut self) -> LuceneResult<usize> {
let offset = self.position()?;
let count = self.read_int()?;
let remaining = self.remaining()?;
let count = u64::try_from(count).map_err(|_| LuceneReadError::ImplausibleLength {
file: self.file_name.clone(),
offset,
length: 0,
bound: remaining,
})?;
if count > remaining {
return Err(LuceneReadError::ImplausibleLength {
file: self.file_name.clone(),
offset,
length: count,
bound: remaining,
});
}
usize::try_from(count).map_err(|_| LuceneReadError::ImplausibleLength {
file: self.file_name.clone(),
offset,
length: count,
bound: remaining,
})
}
fn source_error(&self, error: &std::io::Error) -> LuceneReadError {
LuceneReadError::Source {
file: self.file_name.clone(),
details: error.to_string(),
}
}
}