use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Display;
use std::path::Path;
use crate::errors::{ErrorDetails, ErrorKind, ResourceError};
use crate::ParseResult;
#[derive(Debug, Clone, Serialize)]
pub struct FormResourceFile {
#[serde(skip)]
buffer: Vec<u8>,
file_name: Box<str>,
entries: HashMap<usize, ResourceEntry>,
}
impl Display for FormResourceFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FormResourceFile {{ file_name: {:?}, size: {} bytes, entries: {} }}",
self.file_name,
self.buffer.len(),
self.entries.len()
)
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum ResourceEntry {
Record12ByteHeader {
data: Vec<u8>,
},
Record3ByteHeader {
data: Vec<u8>,
},
ListItems {
items: Vec<String>,
},
Record4ByteHeader {
data: Vec<u8>,
},
Record1ByteHeader {
data: Vec<u8>,
},
Empty {
offset: usize,
},
}
impl ResourceEntry {
#[must_use]
pub fn as_text(&self) -> Option<String> {
let bytes = match self {
ResourceEntry::Record12ByteHeader { data }
| ResourceEntry::Record4ByteHeader { data } => data.as_slice(),
_ => return None,
};
encoding_rs::WINDOWS_1252
.decode_without_bom_handling_and_without_replacement(bytes)
.map(|s| s.to_string())
}
#[must_use]
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
ResourceEntry::Record12ByteHeader { data }
| ResourceEntry::Record4ByteHeader { data }
| ResourceEntry::Record3ByteHeader { data }
| ResourceEntry::Record1ByteHeader { data } => Some(data.as_slice()),
ResourceEntry::ListItems { .. } | ResourceEntry::Empty { .. } => None,
}
}
}
#[derive(Debug, Clone)]
struct ResourceEntryMetadata {
offset: usize,
total_size: usize,
entry_type: ResourceEntryType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ResourceEntryType {
Record12ByteHeader,
Record3ByteHeader,
ListItems,
Record4ByteHeader,
Record1ByteHeader,
Empty,
}
impl FormResourceFile {
#[must_use]
pub fn parse(file_name: &str, buffer: Vec<u8>) -> ParseResult<'static, Self> {
let mut failures = Vec::new();
let mut entries = HashMap::new();
let file_name_box = file_name.to_string().into_boxed_str();
let entry_offsets = Self::scan_entries(&buffer, &mut failures, &file_name_box);
for metadata in entry_offsets {
match Self::parse_entry(&buffer, &metadata) {
Ok(entry) => {
entries.insert(metadata.offset, entry);
}
Err(err) => {
failures.push(ErrorDetails {
source_name: file_name_box.clone(),
source_content: "",
error_offset: u32::try_from(metadata.offset).unwrap_or(0),
line_start: 0,
line_end: 0,
kind: Box::new(err),
severity: crate::errors::Severity::Error,
labels: vec![],
notes: vec![],
});
}
}
}
ParseResult::new(
Some(FormResourceFile {
file_name: file_name_box,
buffer, entries,
}),
failures,
)
}
pub fn from_file<P: AsRef<Path>>(file_path: P) -> std::io::Result<ParseResult<'static, Self>> {
let path = file_path.as_ref();
let bytes = std::fs::read(path)?;
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.frx");
Ok(Self::parse(file_name, bytes))
}
fn scan_entries(
buffer: &[u8],
failures: &mut Vec<ErrorDetails<'static>>,
file_name: &str,
) -> Vec<ResourceEntryMetadata> {
let mut entries = Vec::new();
let mut offset = 0;
while offset < buffer.len() {
match Self::identify_entry(buffer, offset) {
Ok(metadata) => {
offset = metadata.offset + metadata.total_size;
entries.push(metadata);
}
Err(err) => {
failures.push(ErrorDetails {
source_name: file_name.into(),
source_content: "",
error_offset: u32::try_from(offset).unwrap_or(0),
line_start: 0,
line_end: 0,
kind: Box::new(err),
severity: crate::errors::Severity::Error,
labels: vec![],
notes: vec![],
});
offset += 1;
}
}
}
entries
}
fn identify_entry(buffer: &[u8], offset: usize) -> Result<ResourceEntryMetadata, ErrorKind> {
if offset >= buffer.len() {
return Err(ErrorKind::Resource(ResourceError::OffsetOutOfBounds {
offset,
file_length: buffer.len(),
}));
}
if offset + 12 <= buffer.len() && &buffer[offset + 4..offset + 8] == b"lt\0\0" {
let size_buffer_1 = buffer[offset..offset + 4].try_into().map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError { offset })
})?;
let buffer_size_1 = u32::from_le_bytes(size_buffer_1) as usize;
let size_buffer_2 = buffer[offset + 8..offset + 12].try_into().map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError { offset })
})?;
let buffer_size_2 = u32::from_le_bytes(size_buffer_2) as usize;
if buffer_size_1 == 8 && buffer_size_2 == 0 {
return Ok(ResourceEntryMetadata {
offset,
total_size: 12,
entry_type: ResourceEntryType::Empty,
});
}
let total_size = 12 + buffer_size_2;
return Ok(ResourceEntryMetadata {
offset,
total_size,
entry_type: ResourceEntryType::Record12ByteHeader,
});
}
if buffer[offset] == 0xFF && offset + 3 <= buffer.len() {
let size_buffer = buffer[offset + 1..offset + 3].try_into().map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError { offset })
})?;
let mut record_size = u16::from_le_bytes(size_buffer) as usize;
if offset + 3 + record_size > buffer.len() {
record_size -= 1;
}
let total_size = 3 + record_size;
return Ok(ResourceEntryMetadata {
offset,
total_size,
entry_type: ResourceEntryType::Record3ByteHeader,
});
}
if offset + 4 <= buffer.len() {
let signature = &buffer[offset + 2..offset + 4];
if signature == [0x03, 0x00] || signature == [0x07, 0x00] {
let count_buffer = buffer[offset..offset + 2].try_into().map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError { offset })
})?;
let item_count = u16::from_le_bytes(count_buffer) as usize;
let mut current_offset = offset + 4;
for _ in 0..item_count {
if current_offset + 2 > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::CorruptedListItems {
offset,
details: "Item header out of bounds".to_string(),
}));
}
let item_size_buffer = buffer[current_offset..current_offset + 2]
.try_into()
.map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError {
offset: current_offset,
})
})?;
let item_size = u16::from_le_bytes(item_size_buffer) as usize;
current_offset += 2 + item_size;
if current_offset > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::CorruptedListItems {
offset,
details: "Item data out of bounds".to_string(),
}));
}
}
let total_size = current_offset - offset;
return Ok(ResourceEntryMetadata {
offset,
total_size,
entry_type: ResourceEntryType::ListItems,
});
}
}
if offset + 4 <= buffer.len() && buffer[offset..offset + 4].contains(&0u8) {
let size_buffer = buffer[offset..offset + 4].try_into().map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError { offset })
})?;
let record_size = u32::from_le_bytes(size_buffer) as usize;
let total_size = 4 + record_size;
return Ok(ResourceEntryMetadata {
offset,
total_size,
entry_type: ResourceEntryType::Record4ByteHeader,
});
}
let mut record_size = buffer[offset] as usize;
if offset + 1 + record_size > buffer.len() {
record_size = record_size.saturating_sub(1);
}
let total_size = 1 + record_size;
Ok(ResourceEntryMetadata {
offset,
total_size,
entry_type: ResourceEntryType::Record1ByteHeader,
})
}
fn parse_entry(
buffer: &[u8],
metadata: &ResourceEntryMetadata,
) -> Result<ResourceEntry, ErrorKind> {
match metadata.entry_type {
ResourceEntryType::Record12ByteHeader => Self::parse_binary_blob(buffer, metadata),
ResourceEntryType::Record3ByteHeader => Self::parse_16bit_record(buffer, metadata),
ResourceEntryType::ListItems => Self::parse_list_items(buffer, metadata),
ResourceEntryType::Record4ByteHeader => Self::parse_text_data(buffer, metadata),
ResourceEntryType::Record1ByteHeader => Self::parse_8bit_record(buffer, metadata),
ResourceEntryType::Empty => Ok(ResourceEntry::Empty {
offset: metadata.offset,
}),
}
}
fn parse_binary_blob(
buffer: &[u8],
metadata: &ResourceEntryMetadata,
) -> Result<ResourceEntry, ErrorKind> {
let offset = metadata.offset;
if offset + 12 > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::HeaderReadError {
offset,
reason: "Not enough bytes for 12-byte header".to_string(),
}));
}
let signature = &buffer[offset + 4..offset + 8];
if signature != b"lt\0\0" {
return Err(ErrorKind::Resource(ResourceError::InvalidData {
offset,
details: format!("Invalid signature: {signature:?}"),
}));
}
let size_buffer_1 = buffer[offset..offset + 4]
.try_into()
.map_err(|_| ErrorKind::Resource(ResourceError::BufferConversionError { offset }))?;
let buffer_size_1 = u32::from_le_bytes(size_buffer_1) as usize;
let size_buffer_2 = buffer[offset + 8..offset + 12]
.try_into()
.map_err(|_| ErrorKind::Resource(ResourceError::BufferConversionError { offset }))?;
let buffer_size_2 = u32::from_le_bytes(size_buffer_2) as usize;
if buffer_size_1 == 8 && buffer_size_2 == 0 {
return Ok(ResourceEntry::Empty { offset });
}
if buffer_size_2 != buffer_size_1 - 8 {
return Err(ErrorKind::Resource(ResourceError::SizeMismatch {
offset,
expected: buffer_size_1 - 8,
actual: buffer_size_2,
}));
}
let data_start = offset + 12;
let data_end = data_start + buffer_size_2;
if data_end > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::OffsetOutOfBounds {
offset: data_end,
file_length: buffer.len(),
}));
}
Ok(ResourceEntry::Record12ByteHeader {
data: buffer[data_start..data_end].to_vec(),
})
}
fn parse_16bit_record(
buffer: &[u8],
metadata: &ResourceEntryMetadata,
) -> Result<ResourceEntry, ErrorKind> {
let offset = metadata.offset;
if offset + 3 > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::HeaderReadError {
offset,
reason: "Not enough bytes for 16-bit header".to_string(),
}));
}
if buffer[offset] != 0xFF {
return Err(ErrorKind::Resource(ResourceError::InvalidData {
offset,
details: format!("Expected 0xFF marker, got 0x{:02X}", buffer[offset]),
}));
}
let size_buffer = buffer[offset + 1..offset + 3]
.try_into()
.map_err(|_| ErrorKind::Resource(ResourceError::BufferConversionError { offset }))?;
let mut record_size = u16::from_le_bytes(size_buffer) as usize;
if offset + 3 + record_size > buffer.len() {
record_size -= 1;
}
let data_start = offset + 3;
let data_end = data_start + record_size;
if data_end > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::OffsetOutOfBounds {
offset: data_end,
file_length: buffer.len(),
}));
}
Ok(ResourceEntry::Record3ByteHeader {
data: buffer[data_start..data_end].to_vec(),
})
}
fn parse_list_items(
buffer: &[u8],
metadata: &ResourceEntryMetadata,
) -> Result<ResourceEntry, ErrorKind> {
let offset = metadata.offset;
if offset + 4 > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::HeaderReadError {
offset,
reason: "Not enough bytes for list header".to_string(),
}));
}
let count_buffer = buffer[offset..offset + 2]
.try_into()
.map_err(|_| ErrorKind::Resource(ResourceError::BufferConversionError { offset }))?;
let item_count = u16::from_le_bytes(count_buffer) as usize;
let signature = &buffer[offset + 2..offset + 4];
if signature != [0x03, 0x00] && signature != [0x07, 0x00] {
return Err(ErrorKind::Resource(ResourceError::InvalidData {
offset,
details: format!("Invalid list signature: {signature:?}"),
}));
}
let mut items = Vec::with_capacity(item_count);
let mut current_offset = offset + 4;
for item_idx in 0..item_count {
if current_offset + 2 > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::CorruptedListItems {
offset,
details: format!("Item {item_idx} header out of bounds"),
}));
}
let item_size_buffer = buffer[current_offset..current_offset + 2]
.try_into()
.map_err(|_| {
ErrorKind::Resource(ResourceError::BufferConversionError {
offset: current_offset,
})
})?;
let item_size = u16::from_le_bytes(item_size_buffer) as usize;
let item_start = current_offset + 2;
let item_end = item_start + item_size;
if item_end > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::CorruptedListItems {
offset,
details: format!("Item {item_idx} data out of bounds"),
}));
}
let item_bytes = &buffer[item_start..item_end];
let item_string = String::from_utf8_lossy(item_bytes).to_string();
items.push(item_string);
current_offset = item_end;
}
Ok(ResourceEntry::ListItems { items })
}
fn parse_text_data(
buffer: &[u8],
metadata: &ResourceEntryMetadata,
) -> Result<ResourceEntry, ErrorKind> {
let offset = metadata.offset;
if offset + 4 > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::HeaderReadError {
offset,
reason: "Not enough bytes for 4-byte header".to_string(),
}));
}
let size_buffer = buffer[offset..offset + 4]
.try_into()
.map_err(|_| ErrorKind::Resource(ResourceError::BufferConversionError { offset }))?;
let record_size = u32::from_le_bytes(size_buffer) as usize;
let data_start = offset + 4;
let data_end = data_start + record_size;
if data_end > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::OffsetOutOfBounds {
offset: data_end,
file_length: buffer.len(),
}));
}
let data = buffer[data_start..data_end].to_vec();
Ok(ResourceEntry::Record4ByteHeader { data })
}
fn parse_8bit_record(
buffer: &[u8],
metadata: &ResourceEntryMetadata,
) -> Result<ResourceEntry, ErrorKind> {
let offset = metadata.offset;
if offset >= buffer.len() {
return Err(ErrorKind::Resource(ResourceError::HeaderReadError {
offset,
reason: "Offset at end of file".to_string(),
}));
}
let mut record_size = buffer[offset] as usize;
if offset + 1 + record_size > buffer.len() {
record_size -= 1;
}
let data_start = offset + 1;
let data_end = data_start + record_size;
if data_end > buffer.len() {
return Err(ErrorKind::Resource(ResourceError::OffsetOutOfBounds {
offset: data_end,
file_length: buffer.len(),
}));
}
Ok(ResourceEntry::Record1ByteHeader {
data: buffer[data_start..data_end].to_vec(),
})
}
#[must_use]
pub fn get_entry(&self, offset: usize) -> Option<&ResourceEntry> {
self.entries.get(&offset)
}
#[must_use]
pub fn get_binary_blob(&self, offset: usize) -> Option<&[u8]> {
self.entries.get(&offset).and_then(|entry| match entry {
ResourceEntry::Record12ByteHeader { data }
| ResourceEntry::Record3ByteHeader { data }
| ResourceEntry::Record4ByteHeader { data }
| ResourceEntry::Record1ByteHeader { data } => Some(data.as_slice()),
_ => None,
})
}
#[must_use]
pub fn get_list_items(&self, offset: usize) -> Option<&[String]> {
self.entries.get(&offset).and_then(|entry| match entry {
ResourceEntry::ListItems { items } => Some(items.as_slice()),
_ => None,
})
}
#[must_use]
pub fn get_text_data(&self, offset: usize) -> Option<&[u8]> {
self.entries.get(&offset).and_then(|entry| match entry {
ResourceEntry::Record4ByteHeader { data } => Some(data.as_slice()),
_ => None,
})
}
pub fn iter_entries(&self) -> impl Iterator<Item = (usize, &ResourceEntry)> {
self.entries.iter().map(|(&offset, entry)| (offset, entry))
}
#[must_use]
pub fn entry_count(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn file_size(&self) -> usize {
self.buffer.len()
}
}