fwob-v1 1.5.2

Reader, writer, verifier, and editor for the FWOB v1 binary format
Documentation
use std::io::{Read, Seek, SeekFrom, Write};

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use fwob_core::{Field, FieldType, Schema};

use crate::{Result, V1Error};

pub const SIGNATURE: &[u8; 4] = b"FWOB";
pub const VERSION: u8 = 1;
pub const HEADER_LEN: u64 = 214;
pub const DEFAULT_PREFIX_LEN: u32 = 2048;
pub const DEFAULT_STRING_TABLE_PRESERVED_LEN: u32 = DEFAULT_PREFIX_LEN - HEADER_LEN as u32;
pub const MAX_FIELDS: usize = 16;
pub const MAX_FIELD_NAME_LEN: usize = 8;
pub const MAX_FRAME_TYPE_LEN: usize = 16;
pub const MAX_TITLE_LEN: usize = 16;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
    pub version: u8,
    pub field_count: u8,
    pub field_lengths: Vec<u8>,
    pub field_types: u64,
    pub field_names: Vec<String>,
    pub string_count: u32,
    pub string_table_length: u32,
    pub string_table_preserved_length: u32,
    pub frame_count: u64,
    pub frame_length: u32,
    pub frame_type: String,
    pub title: String,
}

impl Header {
    pub fn first_frame_position(&self) -> u64 {
        HEADER_LEN + u64::from(self.string_table_preserved_length)
    }

    pub fn string_table_position(&self) -> u64 {
        HEADER_LEN
    }

    pub fn string_table_ending(&self) -> u64 {
        HEADER_LEN + u64::from(self.string_table_length)
    }

    pub fn file_length(&self) -> u64 {
        self.first_frame_position() + u64::from(self.frame_length) * self.frame_count
    }

    pub fn schema(&self, key_field_index: usize) -> Result<Schema> {
        if key_field_index >= self.field_count as usize {
            return Err(V1Error::KeyFieldIndexOutOfRange(key_field_index));
        }

        let mut offset = 0u32;
        let mut fields = Vec::with_capacity(self.field_count as usize);
        for i in 0..self.field_count as usize {
            let field_type_id = ((self.field_types >> (i * 4)) & 0x0f) as u8;
            let field_type = FieldType::from_v1_id(field_type_id)?;
            let length = u16::from(self.field_lengths[i]);
            fields.push(Field::new(
                self.field_names[i].clone(),
                field_type,
                length,
                offset,
            ));
            offset += u32::from(length);
        }
        Schema::new(self.frame_type.clone(), fields, key_field_index).map_err(Into::into)
    }
}

pub fn read_header<R: Read>(reader: &mut R) -> Result<Header> {
    let mut sig = [0u8; 4];
    reader.read_exact(&mut sig)?;
    if &sig != SIGNATURE {
        return Err(V1Error::CorruptedHeader);
    }

    let version = reader.read_u8()?;
    if version != VERSION {
        return Err(V1Error::CorruptedHeader);
    }

    let field_count = reader.read_u8()?;
    if field_count as usize > MAX_FIELDS {
        return Err(V1Error::CorruptedHeader);
    }

    let mut all_lengths = [0u8; MAX_FIELDS];
    reader.read_exact(&mut all_lengths)?;
    let field_lengths = all_lengths[..field_count as usize].to_vec();
    let field_types = reader.read_u64::<LittleEndian>()?;

    let mut all_names = Vec::with_capacity(MAX_FIELDS);
    for _ in 0..MAX_FIELDS {
        let mut raw = [0u8; MAX_FIELD_NAME_LEN];
        reader.read_exact(&mut raw)?;
        all_names.push(trim_ascii_field(&raw));
    }
    let field_names = all_names[..field_count as usize].to_vec();
    if field_names.iter().any(|name| name.is_empty()) {
        return Err(V1Error::CorruptedHeader);
    }

    let string_count = reader.read_i32::<LittleEndian>()?;
    let string_table_length = reader.read_i32::<LittleEndian>()?;
    let string_table_preserved_length = reader.read_i32::<LittleEndian>()?;
    if string_count < 0
        || string_table_length < 0
        || string_table_preserved_length < string_table_length
    {
        return Err(V1Error::CorruptedHeader);
    }

    let frame_count = reader.read_i64::<LittleEndian>()?;
    let frame_length = reader.read_i32::<LittleEndian>()?;
    if frame_count < 0 || frame_length < 0 {
        return Err(V1Error::CorruptedHeader);
    }
    let expected_frame_length: u32 = field_lengths.iter().map(|&v| u32::from(v)).sum();
    if frame_length as u32 != expected_frame_length {
        return Err(V1Error::CorruptedHeader);
    }

    let mut frame_type = [0u8; MAX_FRAME_TYPE_LEN];
    reader.read_exact(&mut frame_type)?;
    let frame_type = trim_ascii_field(&frame_type);
    if frame_type.is_empty() {
        return Err(V1Error::CorruptedHeader);
    }

    let mut title = [0u8; MAX_TITLE_LEN];
    reader.read_exact(&mut title)?;
    let title = trim_ascii_field(&title);
    if title.is_empty() {
        return Err(V1Error::CorruptedHeader);
    }

    Ok(Header {
        version,
        field_count,
        field_lengths,
        field_types,
        field_names,
        string_count: string_count as u32,
        string_table_length: string_table_length as u32,
        string_table_preserved_length: string_table_preserved_length as u32,
        frame_count: frame_count as u64,
        frame_length: frame_length as u32,
        frame_type,
        title,
    })
}

pub fn write_header<W: Write>(writer: &mut W, header: &Header) -> Result<()> {
    writer.write_all(SIGNATURE)?;
    writer.write_u8(header.version)?;
    writer.write_u8(header.field_count)?;

    writer.write_all(&header.field_lengths)?;
    if header.field_lengths.len() < MAX_FIELDS {
        writer.write_all(&vec![0; MAX_FIELDS - header.field_lengths.len()])?;
    }

    writer.write_u64::<LittleEndian>(header.field_types)?;

    for name in &header.field_names {
        write_fixed_ascii(writer, name, MAX_FIELD_NAME_LEN)?;
    }
    for _ in header.field_names.len()..MAX_FIELDS {
        writer.write_all(&[0u8; MAX_FIELD_NAME_LEN])?;
    }

    writer.write_i32::<LittleEndian>(header.string_count as i32)?;
    writer.write_i32::<LittleEndian>(header.string_table_length as i32)?;
    writer.write_i32::<LittleEndian>(header.string_table_preserved_length as i32)?;
    writer.write_i64::<LittleEndian>(header.frame_count as i64)?;
    writer.write_i32::<LittleEndian>(header.frame_length as i32)?;
    write_fixed_ascii(writer, &header.frame_type, MAX_FRAME_TYPE_LEN)?;
    write_fixed_ascii(writer, &header.title, MAX_TITLE_LEN)?;
    Ok(())
}

pub fn update_frame_count<W: Write + Seek>(writer: &mut W, frame_count: u64) -> Result<()> {
    writer.seek(SeekFrom::Start(170))?;
    writer.write_i64::<LittleEndian>(frame_count as i64)?;
    Ok(())
}

pub fn update_string_table_len<W: Write + Seek>(
    writer: &mut W,
    string_count: u32,
    string_table_length: u32,
) -> Result<()> {
    writer.seek(SeekFrom::Start(158))?;
    writer.write_i32::<LittleEndian>(string_count as i32)?;
    writer.write_i32::<LittleEndian>(string_table_length as i32)?;
    Ok(())
}

fn trim_ascii_field(bytes: &[u8]) -> String {
    let end = bytes
        .iter()
        .rposition(|&b| b != 0 && b != b' ')
        .map(|idx| idx + 1)
        .unwrap_or(0);
    String::from_utf8_lossy(&bytes[..end]).into_owned()
}

fn write_fixed_ascii<W: Write>(writer: &mut W, value: &str, len: usize) -> Result<()> {
    let bytes = value.as_bytes();
    if bytes.len() > len {
        return Err(V1Error::CorruptedHeader);
    }
    writer.write_all(bytes)?;
    if bytes.len() < len {
        writer.write_all(&vec![b' '; len - bytes.len()])?;
    }
    Ok(())
}