use crate::error::Result;
use crate::iso2709::{
self, is_control_field_tag, parse_4digits, parse_5digits, parse_data_field, read_leader_bytes,
read_record_data, DataFieldParseConfig, ParseContext, FIELD_TERMINATOR, LEADER_LEN,
};
use crate::leader::Leader;
use crate::record::Field;
use crate::recovery::{RecoveryCap, RecoveryMode};
use std::io::Read;
pub trait Iso2709Builder: Sized {
type Output;
fn parse_config() -> DataFieldParseConfig;
fn validate_record_type(leader: &Leader, ctx: &ParseContext) -> Result<()> {
let _ = (leader, ctx);
Ok(())
}
fn new_for(leader: Leader) -> Self;
fn add_control_field(&mut self, tag: String, value: String);
fn add_data_field(&mut self, tag: String, field: Field);
#[inline]
fn decode_control_field_value(
field_bytes: &[u8],
tag: &str,
ctx: &ParseContext,
) -> Result<String> {
let _ = (tag, ctx);
Ok(
String::from_utf8_lossy(&field_bytes[..field_bytes.len().saturating_sub(1)])
.to_string(),
)
}
#[inline]
fn validate_data_field_bytes(field_bytes: &[u8], tag: &str, ctx: &ParseContext) -> Result<()> {
let _ = (field_bytes, tag, ctx);
Ok(())
}
#[must_use]
fn try_recover_truncated(
leader: Leader,
partial_data: &[u8],
base_address: usize,
mode: RecoveryMode,
ctx: &ParseContext,
) -> Option<Result<Self::Output>> {
let _ = (leader, partial_data, base_address, mode, ctx);
None
}
fn finalize(self) -> Self::Output;
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn parse_iso2709_record<R, B>(
reader: &mut R,
ctx: &mut ParseContext,
cap: &mut RecoveryCap,
recovery_mode: RecoveryMode,
) -> Result<Option<B::Output>>
where
R: Read,
B: Iso2709Builder,
{
if cap.is_exhausted() {
return Ok(None);
}
let Some(leader_bytes) = read_leader_bytes(reader)? else {
return Ok(None);
};
ctx.begin_record();
let leader_offset = ctx.stream_byte_offset;
let leader = Leader::from_bytes(&leader_bytes)
.map_err(|e| e.with_bytes_near(&leader_bytes, leader_offset))?;
leader
.validate_for_reading()
.map_err(|e| e.with_bytes_near(&leader_bytes, leader_offset))?;
B::validate_record_type(&leader, ctx)?;
let record_length = leader.record_length as usize;
let base_address = leader.data_base_address as usize;
let directory_size = base_address - 24;
ctx.advance(LEADER_LEN);
let (record_data, _was_truncated) =
read_record_data(reader, record_length, recovery_mode, ctx)?;
let record_data_offset = ctx.stream_byte_offset;
ctx.set_parse_buffer(&record_data, record_data_offset);
if record_data.len() < (record_length - 24) {
if recovery_mode == RecoveryMode::Strict {
return Err(ctx.err_truncated_record(
Some(record_length.saturating_sub(LEADER_LEN)),
Some(record_data.len()),
));
}
cap.note(ctx)?;
if let Some(result) = B::try_recover_truncated(
leader.clone(),
&record_data,
base_address,
recovery_mode,
ctx,
) {
return result.map(Some);
}
}
let directory_end = std::cmp::min(directory_size, record_data.len());
let directory: &[u8] = if directory_end > 0 {
&record_data[..directory_end]
} else {
&[]
};
let data_start = std::cmp::min(base_address - 24, record_data.len());
let data: &[u8] = if data_start < record_data.len() {
&record_data[data_start..]
} else {
&[]
};
let mut builder = B::new_for(leader);
let mut pos = 0;
while pos < directory.len() {
ctx.stream_byte_offset = record_data_offset + pos;
if directory[pos] == FIELD_TERMINATOR {
break;
}
if pos + 12 > directory.len() {
if recovery_mode == RecoveryMode::Strict {
return Err(ctx.err_directory_invalid(
Some(&directory[pos..]),
"complete 12-byte directory entry",
));
}
cap.note(ctx)?;
break;
}
let entry_chunk = &directory[pos..pos + 12];
let tag = String::from_utf8_lossy(&entry_chunk[0..3]).to_string();
let field_length = match parse_4digits(&entry_chunk[3..7]) {
Ok(len) => len,
Err(e) => {
if recovery_mode == RecoveryMode::Strict {
return Err(e);
}
cap.note(ctx)?;
pos += 12;
continue;
},
};
let start_position = match parse_5digits(&entry_chunk[7..12]) {
Ok(s) => s,
Err(e) => {
if recovery_mode == RecoveryMode::Strict {
return Err(e);
}
cap.note(ctx)?;
pos += 12;
continue;
},
};
pos += 12;
let end_position = start_position + field_length;
if end_position > data.len() {
if recovery_mode == RecoveryMode::Strict {
ctx.current_field_tag = tag.as_bytes().try_into().ok();
return Err(ctx.err_invalid_field(format!(
"Field {tag} exceeds data area (end {end_position} > {})",
data.len()
)));
}
cap.note(ctx)?;
let available_end = std::cmp::min(end_position, data.len());
if available_end > start_position {
let field_data = &data[start_position..available_end];
if tag != "LDR" {
if is_control_field_tag(&tag) {
if let Ok(value) = B::decode_control_field_value(field_data, &tag, ctx) {
if tag == "001" {
ctx.record_control_number = Some(value.clone());
}
builder.add_control_field(tag.clone(), value);
}
} else if B::validate_data_field_bytes(field_data, &tag, ctx).is_ok() {
ctx.current_field_tag = tag.as_bytes().try_into().ok();
ctx.stream_byte_offset = record_data_offset + data_start + start_position;
if let Ok(field) =
parse_data_field(field_data, &tag, B::parse_config(), ctx)
{
builder.add_data_field(tag, field);
}
ctx.current_field_tag = None;
}
}
}
continue;
}
let field_data = &data[start_position..end_position];
if tag == "LDR" {
continue;
}
if is_control_field_tag(&tag) {
let value = match B::decode_control_field_value(field_data, &tag, ctx) {
Ok(v) => v,
Err(e) => {
if recovery_mode == RecoveryMode::Strict {
return Err(e);
}
cap.note(ctx)?;
continue;
},
};
if tag == "001" {
ctx.record_control_number = Some(value.clone());
}
builder.add_control_field(tag, value);
continue;
}
if let Err(e) = B::validate_data_field_bytes(field_data, &tag, ctx) {
if recovery_mode == RecoveryMode::Strict {
return Err(e);
}
cap.note(ctx)?;
continue;
}
ctx.current_field_tag = tag.as_bytes().try_into().ok();
ctx.stream_byte_offset = record_data_offset + data_start + start_position;
let parsed = parse_data_field(field_data, &tag, B::parse_config(), ctx);
ctx.current_field_tag = None;
match parsed {
Ok(field) => builder.add_data_field(tag, field),
Err(e) => {
if recovery_mode == RecoveryMode::Strict {
return Err(e);
}
cap.note(ctx)?;
},
}
}
ctx.stream_byte_offset = record_data_offset + record_length.saturating_sub(LEADER_LEN);
Ok(Some(builder.finalize()))
}
pub use iso2709::{DataFieldParseConfig as ParseConfig, FIELD_TERMINATOR as DIRECTORY_TERMINATOR};