use std::fs::File;
use std::io::{Read as _, Seek as _, SeekFrom};
use crate::rrd::{CodecError, Decodable as _, StreamFooter, StreamHeader};
use crate::{RrdFooter, ToApplication as _};
pub fn read_rrd_footer(file: &mut File) -> Result<Option<RrdFooter>, CodecError> {
let file_len = file.metadata()?.len();
if file_len < StreamHeader::ENCODED_SIZE_BYTES as u64 {
return Err(CodecError::FrameDecoding(
"file too small to be an RRD".to_owned(),
));
}
file.seek(SeekFrom::Start(0))?;
let mut header_buf = [0u8; StreamHeader::ENCODED_SIZE_BYTES];
file.read_exact(&mut header_buf)?;
StreamHeader::from_rrd_bytes(&header_buf)?;
if file_len < StreamFooter::ENCODED_SIZE_BYTES as u64 {
return Ok(None); }
#[expect(clippy::cast_possible_wrap)]
file.seek(SeekFrom::End(-(StreamFooter::ENCODED_SIZE_BYTES as i64)))?;
let mut footer_buf = [0u8; StreamFooter::ENCODED_SIZE_BYTES];
file.read_exact(&mut footer_buf)?;
let Ok(stream_footer) = StreamFooter::from_rrd_bytes(&footer_buf) else {
return Ok(None); };
let Some(entry) = stream_footer.entries.first() else {
return Ok(None);
};
let span = &entry.rrd_footer_byte_span_from_start_excluding_header;
let payload_len = usize::try_from(span.len)?;
if span.start + span.len > file_len {
return Err(CodecError::FrameDecoding(format!(
"RrdFooter payload span ({start}..{end}) exceeds file size ({file_len})",
start = span.start,
end = span.start + span.len,
)));
}
file.seek(SeekFrom::Start(span.start))?;
let mut payload_buf = vec![0u8; payload_len];
file.read_exact(&mut payload_buf)?;
let actual_crc = StreamFooter::compute_crc(&payload_buf);
if actual_crc != entry.crc_excluding_header {
return Err(CodecError::CrcMismatch {
expected: entry.crc_excluding_header,
got: actual_crc,
});
}
let transport_footer = re_protos::log_msg::v1alpha1::RrdFooter::from_rrd_bytes(&payload_buf)?;
let rrd_footer = transport_footer.to_application(())?;
Ok(Some(rrd_footer))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rrd::test_util::{encode_test_rrd, encode_test_rrd_to_file, make_test_chunks};
#[test]
fn test_read_footer_roundtrip() {
let chunks = make_test_chunks(5);
let (file, _store_id) = encode_test_rrd(&chunks);
let footer = read_rrd_footer(&mut File::open(file.path()).unwrap()).unwrap();
assert!(footer.is_some(), "Footer should be present");
let footer = footer.unwrap();
assert!(
!footer.manifests.is_empty(),
"Should have at least one manifest"
);
}
#[test]
fn test_read_footer_no_footer() {
let file = tempfile::NamedTempFile::new().unwrap();
let chunks = make_test_chunks(3);
encode_test_rrd_to_file(file.path(), &chunks, false);
let footer = read_rrd_footer(&mut File::open(file.path()).unwrap()).unwrap();
assert!(footer.is_none(), "Legacy RRD should have no footer");
}
#[test]
fn test_read_footer_not_an_rrd() {
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(file.path(), b"this is not an rrd file at all").unwrap();
let result = read_rrd_footer(&mut File::open(file.path()).unwrap());
assert!(result.is_err(), "Non-RRD file should return an error");
}
#[test]
fn test_read_footer_too_small() {
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(file.path(), b"tiny").unwrap();
let result = read_rrd_footer(&mut File::open(file.path()).unwrap());
assert!(
result.is_err(),
"File too small for StreamHeader should error"
);
}
#[test]
fn test_read_footer_corrupted_crc() {
let chunks = make_test_chunks(3);
let (file, _store_id) = encode_test_rrd(&chunks);
let mut data = std::fs::read(file.path()).unwrap();
let file_len = data.len();
let footer_bytes = &data[file_len - StreamFooter::ENCODED_SIZE_BYTES..];
let stream_footer = StreamFooter::from_rrd_bytes(footer_bytes).unwrap();
let entry = &stream_footer.entries[0];
let payload_start = entry.rrd_footer_byte_span_from_start_excluding_header.start as usize;
data[payload_start] ^= 0xFF;
std::fs::write(file.path(), &data).unwrap();
let result = read_rrd_footer(&mut File::open(file.path()).unwrap());
assert!(
matches!(result, Err(CodecError::CrcMismatch { .. })),
"Expected CRC mismatch, got: {result:?}"
);
}
}