use crate::error::{MarcError, Result};
use crate::leader::Leader;
use crate::record::Record;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RecoveryMode {
#[default]
Strict,
Lenient,
Permissive,
}
#[derive(Debug)]
pub struct RecoveryContext {
pub mode: RecoveryMode,
pub has_errors: bool,
pub recovery_messages: Vec<String>,
}
impl Default for RecoveryContext {
fn default() -> Self {
RecoveryContext {
mode: RecoveryMode::Strict,
has_errors: false,
recovery_messages: Vec::new(),
}
}
}
impl RecoveryContext {
#[must_use]
pub fn new(mode: RecoveryMode) -> Self {
RecoveryContext {
mode,
has_errors: false,
recovery_messages: Vec::new(),
}
}
fn add_message(&mut self, message: String) {
self.has_errors = true;
self.recovery_messages.push(message);
}
pub fn recover<T>(&mut self, error: MarcError, context: &str) -> Result<Option<T>> {
match self.mode {
RecoveryMode::Strict => Err(error),
RecoveryMode::Lenient | RecoveryMode::Permissive => {
self.add_message(format!("{context}: {error}"));
Ok(None)
},
}
}
}
const FIELD_TERMINATOR: u8 = 0x1E;
const SUBFIELD_DELIMITER: u8 = 0x1F;
#[allow(clippy::too_many_lines)]
pub fn try_recover_record(
leader: Leader,
partial_data: &[u8],
base_address: usize,
mode: RecoveryMode,
) -> Result<Record> {
let mut context = RecoveryContext::new(mode);
let mut record = Record::new(leader);
let directory_size = base_address.saturating_sub(24);
if directory_size == 0 {
return Err(MarcError::TruncatedRecord(
"No directory found in record".to_string(),
));
}
let directory_end = std::cmp::min(directory_size, partial_data.len());
let directory = &partial_data[..directory_end];
let mut pos = 0;
while pos < directory.len() {
if directory[pos] == FIELD_TERMINATOR {
break;
}
if pos + 12 > directory.len() {
if mode != RecoveryMode::Strict {
context.add_message("Incomplete directory entry at end of record".to_string());
}
break;
}
let entry_chunk = &directory[pos..pos + 12];
let tag = String::from_utf8_lossy(&entry_chunk[0..3]).to_string();
let field_length = if mode == RecoveryMode::Strict {
parse_4digits(&entry_chunk[3..7])?
} else if let Ok(len) = parse_4digits(&entry_chunk[3..7]) {
len
} else {
context.add_message(format!("Invalid field length for tag {tag}"));
pos += 12;
continue;
};
let start_position = if mode == RecoveryMode::Strict {
parse_digits(&entry_chunk[7..12])?
} else if let Ok(p) = parse_digits(&entry_chunk[7..12]) {
p
} else {
context.add_message(format!("Invalid start position for tag {tag}"));
pos += 12;
continue;
};
pos += 12;
let end_position = start_position + field_length;
let data_start = directory_size;
if start_position < data_start || end_position > partial_data.len() {
if mode == RecoveryMode::Strict {
return Err(MarcError::TruncatedRecord(format!(
"Field {tag} data not available"
)));
}
context.add_message(format!("Field {tag} data truncated"));
let available_end = std::cmp::min(end_position, partial_data.len());
if available_end > data_start {
if let Ok(field) = try_parse_field(
&partial_data[start_position..available_end],
&tag,
SUBFIELD_DELIMITER,
FIELD_TERMINATOR,
) {
record.add_field(field);
}
}
continue;
}
if tag != "LDR" {
if tag.starts_with('0') && tag.chars().all(char::is_numeric) && tag.as_str() < "010" {
let value = String::from_utf8_lossy(
&partial_data[start_position..end_position.saturating_sub(1)],
)
.to_string();
record.add_control_field(tag, value);
} else if let Ok(field) = try_parse_field(
&partial_data[start_position..end_position],
&tag,
SUBFIELD_DELIMITER,
FIELD_TERMINATOR,
) {
record.add_field(field);
} else if mode != RecoveryMode::Strict {
context.add_message(format!("Failed to parse field {tag}"));
}
}
}
Ok(record)
}
fn try_parse_field(
data: &[u8],
tag: &str,
subfield_delim: u8,
field_term: u8,
) -> Result<crate::record::Field> {
use crate::record::Field;
if data.is_empty() {
return Err(MarcError::InvalidField("Empty field data".to_string()));
}
if data.len() < 2 {
return Err(MarcError::InvalidField(
"Data field too short (needs indicators)".to_string(),
));
}
let indicator1 = data[0] as char;
let indicator2 = data[1] as char;
let mut field = Field::new(tag.to_string(), indicator1, indicator2);
let subfield_data = &data[2..];
let mut current_position = 0;
while current_position < subfield_data.len() {
if subfield_data[current_position] == field_term {
break;
}
if subfield_data[current_position] == subfield_delim {
current_position += 1;
if current_position >= subfield_data.len() {
break;
}
let code = subfield_data[current_position] as char;
current_position += 1;
let mut end = current_position;
while end < subfield_data.len()
&& subfield_data[end] != subfield_delim
&& subfield_data[end] != field_term
{
end += 1;
}
let value = String::from_utf8_lossy(&subfield_data[current_position..end]).to_string();
field.add_subfield(code, value);
current_position = end;
} else {
return Err(MarcError::InvalidField(
"Expected subfield delimiter".to_string(),
));
}
}
Ok(field)
}
fn parse_4digits(bytes: &[u8]) -> Result<usize> {
if bytes.len() != 4 {
return Err(MarcError::InvalidRecord(format!(
"Expected 4-digit field, got {} bytes",
bytes.len()
)));
}
let s = String::from_utf8_lossy(bytes);
s.parse::<usize>()
.map_err(|_| MarcError::InvalidRecord(format!("Invalid numeric field: '{s}'")))
}
fn parse_digits(bytes: &[u8]) -> Result<usize> {
if bytes.len() != 5 {
return Err(MarcError::InvalidRecord(format!(
"Expected 5-digit field, got {} bytes",
bytes.len()
)));
}
let s = String::from_utf8_lossy(bytes);
s.parse::<usize>()
.map_err(|_| MarcError::InvalidRecord(format!("Invalid numeric field: '{s}'")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recovery_context_default() {
let ctx = RecoveryContext::default();
assert_eq!(ctx.mode, RecoveryMode::Strict);
assert!(!ctx.has_errors);
assert!(ctx.recovery_messages.is_empty());
}
#[test]
fn test_recovery_mode_lenient() {
let mut ctx = RecoveryContext::new(RecoveryMode::Lenient);
let error = MarcError::InvalidField("test".to_string());
let result: Result<Option<()>> = ctx.recover(error, "test context");
assert!(result.is_ok());
assert!(ctx.has_errors);
assert!(!ctx.recovery_messages.is_empty());
}
#[test]
fn test_recovery_mode_strict() {
let mut ctx = RecoveryContext::new(RecoveryMode::Strict);
let error = MarcError::InvalidField("test".to_string());
let result: Result<Option<()>> = ctx.recover(error, "test context");
assert!(result.is_err());
}
}