use crate::error::{EngramError, Result};
use std::io::{Read, Write};
pub const END_RECORD_SIGNATURE: [u8; 4] = [0x45, 0x4E, 0x44, 0x52];
pub const END_RECORD_SIZE: usize = 64;
pub const ENDR_FLAG_SIGNED: u8 = 0x01;
#[derive(Debug, Clone)]
pub struct EndRecord {
pub version_major: u16,
pub version_minor: u16,
pub central_directory_offset: u64,
pub central_directory_size: u64,
pub entry_count: u32,
pub archive_crc32: u32,
pub is_signed: bool,
}
impl EndRecord {
pub fn new(
version_major: u16,
version_minor: u16,
central_directory_offset: u64,
central_directory_size: u64,
entry_count: u32,
archive_crc32: u32,
) -> Self {
Self {
version_major,
version_minor,
central_directory_offset,
central_directory_size,
entry_count,
archive_crc32,
is_signed: false,
}
}
pub fn write_to<W: Write>(&self, mut writer: W) -> Result<usize> {
self.write_to_with_flags(&mut writer, self.is_signed)
}
pub fn write_to_with_flags<W: Write>(&self, mut writer: W, is_signed: bool) -> Result<usize> {
let mut bytes_written = 0;
writer.write_all(&END_RECORD_SIGNATURE)?;
bytes_written += 4;
writer.write_all(&self.version_major.to_le_bytes())?;
bytes_written += 2;
writer.write_all(&self.version_minor.to_le_bytes())?;
bytes_written += 2;
writer.write_all(&self.central_directory_offset.to_le_bytes())?;
bytes_written += 8;
writer.write_all(&self.central_directory_size.to_le_bytes())?;
bytes_written += 8;
writer.write_all(&self.entry_count.to_le_bytes())?;
bytes_written += 4;
writer.write_all(&self.archive_crc32.to_le_bytes())?;
bytes_written += 4;
let mut reserved = [0u8; 32];
if is_signed {
reserved[0] = ENDR_FLAG_SIGNED;
}
writer.write_all(&reserved)?;
bytes_written += 32;
Ok(bytes_written)
}
pub fn read_from<R: Read>(mut reader: R) -> Result<Self> {
let mut sig = [0u8; 4];
reader.read_exact(&mut sig)?;
if sig != END_RECORD_SIGNATURE {
return Err(EngramError::InvalidFormat(
"Invalid end record signature (expected ENDR)".to_string(),
));
}
let version_major = read_u16(&mut reader)?;
let version_minor = read_u16(&mut reader)?;
let central_directory_offset = read_u64(&mut reader)?;
let central_directory_size = read_u64(&mut reader)?;
let entry_count = read_u32(&mut reader)?;
let archive_crc32 = read_u32(&mut reader)?;
let mut reserved = [0u8; 32];
reader.read_exact(&mut reserved)?;
let is_signed = reserved[0] & ENDR_FLAG_SIGNED != 0;
Ok(Self {
version_major,
version_minor,
central_directory_offset,
central_directory_size,
entry_count,
archive_crc32,
is_signed,
})
}
pub fn validate_against_header(
&self,
header_version_major: u16,
header_version_minor: u16,
header_cd_offset: u64,
header_cd_size: u64,
header_entry_count: u32,
) -> Result<()> {
if self.version_major != header_version_major || self.version_minor != header_version_minor
{
return Err(EngramError::InvalidFormat(format!(
"ENDR version mismatch: header v{}.{}, ENDR v{}.{}",
header_version_major, header_version_minor, self.version_major, self.version_minor
)));
}
if self.central_directory_offset != header_cd_offset {
return Err(EngramError::InvalidFormat(format!(
"ENDR central directory offset mismatch: header {}, ENDR {}",
header_cd_offset, self.central_directory_offset
)));
}
if self.central_directory_size != header_cd_size {
return Err(EngramError::InvalidFormat(format!(
"ENDR central directory size mismatch: header {}, ENDR {}",
header_cd_size, self.central_directory_size
)));
}
if self.entry_count != header_entry_count {
return Err(EngramError::InvalidFormat(format!(
"ENDR entry count mismatch: header {}, ENDR {}",
header_entry_count, self.entry_count
)));
}
Ok(())
}
}
fn read_u16<R: Read>(mut reader: R) -> Result<u16> {
let mut buf = [0u8; 2];
reader.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
fn read_u32<R: Read>(mut reader: R) -> Result<u32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_u64<R: Read>(mut reader: R) -> Result<u64> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(u64::from_le_bytes(buf))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_end_record_roundtrip() {
let record = EndRecord::new(
1, 0, 1024, 3200, 10, 0xDEADBEEF, );
let mut buf = Vec::new();
let written = record.write_to(&mut buf).unwrap();
assert_eq!(written, END_RECORD_SIZE);
assert_eq!(buf.len(), END_RECORD_SIZE);
let parsed = EndRecord::read_from(&buf[..]).unwrap();
assert_eq!(parsed.version_major, record.version_major);
assert_eq!(parsed.version_minor, record.version_minor);
assert_eq!(
parsed.central_directory_offset,
record.central_directory_offset
);
assert_eq!(parsed.central_directory_size, record.central_directory_size);
assert_eq!(parsed.entry_count, record.entry_count);
assert_eq!(parsed.archive_crc32, record.archive_crc32);
assert!(!parsed.is_signed);
}
#[test]
fn test_signed_flag_roundtrip() {
let record = EndRecord::new(1, 0, 1024, 3200, 10, 0xDEADBEEF);
let mut buf = Vec::new();
record.write_to_with_flags(&mut buf, true).unwrap();
let parsed = EndRecord::read_from(&buf[..]).unwrap();
assert!(parsed.is_signed, "Signed flag should be preserved");
}
#[test]
fn test_unsigned_flag_roundtrip() {
let record = EndRecord::new(1, 0, 1024, 3200, 10, 0);
let mut buf = Vec::new();
record.write_to_with_flags(&mut buf, false).unwrap();
let parsed = EndRecord::read_from(&buf[..]).unwrap();
assert!(!parsed.is_signed, "Unsigned flag should be preserved");
}
#[test]
fn test_signature_validation() {
let mut buf = vec![0xFF, 0xFF, 0xFF, 0xFF]; buf.extend_from_slice(&[0u8; 60]);
let result = EndRecord::read_from(&buf[..]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid end record signature"));
}
#[test]
fn test_validate_against_header() {
let record = EndRecord::new(1, 0, 1024, 3200, 10, 0);
assert!(record.validate_against_header(1, 0, 1024, 3200, 10).is_ok());
assert!(record
.validate_against_header(0, 4, 1024, 3200, 10)
.is_err());
assert!(record
.validate_against_header(1, 0, 2048, 3200, 10)
.is_err());
assert!(record
.validate_against_header(1, 0, 1024, 6400, 10)
.is_err());
assert!(record
.validate_against_header(1, 0, 1024, 3200, 20)
.is_err());
}
}