Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions arma-file-formats/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ pub enum PaaError {

#[error("unknown decoding error")]
Unknown,

#[error("PNG image, not a PAA")]
Png,

#[error("Unknown PAA type `{0:#06x}`")]
UnknownType(u16),

/// The file is cut off, like the decoys of obfuscated PBOs: copies of real textures that
/// end before their mipmaps.
#[error("Mipmap {index} ends at byte {end}, past the end of the file ({len} bytes)")]
MipmapPastEnd { index: usize, end: u64, len: u64 },
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -51,6 +62,11 @@ pub enum OdolError {

#[error("Lod not found")]
LodNotFound,

/// The file is cut off, like the decoys of obfuscated PBOs: copies of real models that end
/// before their LODs.
#[error("LOD {index} starts at byte {start}, past the end of the file ({len} bytes)")]
LodPastEnd { index: usize, start: u64, len: u64 },
}

#[derive(Debug, PartialEq, Eq, Clone, Error)]
Expand Down
20 changes: 20 additions & 0 deletions arma-file-formats/src/real_virtuality/p3d/odol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ pub fn read_lods(
start_address_of_lods: &[u32],
args: ODOLArgs,
) -> BinResult<Vec<Lod>> {
// A file that ends before its LODs fails as that, also when they're read later. Only the
// start: end addresses of small models that load fine can be past the end of the file.
let pos = reader.stream_position()?;
let len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(pos))?;
if let Some((index, &start)) = start_address_of_lods
.iter()
.enumerate()
.find(|(_, start)| u64::from(**start) >= len)
{
return Err(binrw::Error::Custom {
pos,
err: Box::new(OdolError::LodPastEnd {
index,
start: start.into(),
len,
}),
});
}

if args.skip_lods {
return Ok(Vec::new());
}
Expand Down
44 changes: 33 additions & 11 deletions arma-file-formats/src/real_virtuality/paa/paa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use crate::{

use super::{Mipmap, Tagg};

/// Some mods ship PNG images with a .paa name.
const PNG_SIGNATURE: [u8; 8] = *b"\x89PNG\r\n\x1a\n";

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Paa {
pub pixel_type: PixelType,
Expand Down Expand Up @@ -95,8 +98,23 @@ impl Paa {
{
let mut paa = Self::new();

let len = reader.seek(SeekFrom::End(0))?;
reader.rewind()?;
paa.magic_number = PaaType::try_from(reader.read_u16()?).unwrap_or(PaaType::UNKNOWN);
let magic = reader.read_u16()?;
paa.magic_number = match PaaType::try_from(magic) {
Ok(PaaType::UNKNOWN) | Err(_) => {
let mut signature = [0; 8];
reader.rewind()?;
return Err(
if reader.read_exact(&mut signature).is_ok() && signature == PNG_SIGNATURE {
PaaError::Png
} else {
PaaError::UnknownType(magic)
},
);
}
Ok(paa_type) => paa_type,
};

paa.pixel_type = match paa.magic_number {
PaaType::DXT1 | PaaType::DXT5 | PaaType::RGBA4444 | PaaType::ARGB1555 => {
Expand All @@ -117,22 +135,26 @@ impl Paa {
paa.palette = reader.read_bytes(palette_length)?;
}

let mut index_counter: u32 = 0;
// All headers first: a file that ends before its mipmaps fails as that, not on the
// garbage in the mipmaps it does have.
while reader.peek_u16()? != 0 {
let mut mipmap = Mipmap::new();

mipmap.read_header(reader, paa.magic_number)?;
let end = reader.stream_position()?;
if end > len {
return Err(PaaError::MipmapPastEnd {
index: paa.mipmaps.len(),
end,
len,
});
}
paa.mipmaps.push(mipmap);
}

// TODO: refactor
if indicies_to_load.is_none()
|| indicies_to_load.unwrap().is_empty()
|| indicies_to_load.unwrap().contains(&index_counter)
{
for (index, mipmap) in (0u32..).zip(&mut paa.mipmaps) {
if indicies_to_load.is_none_or(|i| i.is_empty() || i.contains(&index)) {
mipmap.read_with(reader, paa.magic_number, keep_blocks)?;
}

paa.mipmaps.push(mipmap);
index_counter += 1;
}

Ok(paa)
Expand Down
8 changes: 6 additions & 2 deletions arma-file-formats/src/real_virtuality/paa/tagg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ impl Tagg {
T: Read + Seek,
{
self.signature = stream.read_string(Self::TAGG_SIG_SIZE)?;
let size = stream.read_u32()? as usize;
self.data = stream.read_bytes(size)?;
let size = stream.read_u32()?;
// Grows with the data, a garbage size doesn't allocate up to 4 GB.
self.data.clear();
if stream.take(size.into()).read_to_end(&mut self.data)? != size as usize {
return Err(io::ErrorKind::UnexpectedEof.into());
}

Ok(())
}
Expand Down
16 changes: 16 additions & 0 deletions arma-file-formats/tests/real_virtuality/p3d_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,19 @@ fn garbage_input() {
}
}
}

/// A model that ends before its LODs, like the decoys of obfuscated PBOs, fails as that.
#[test]
fn truncated() {
let data = fs::read(format!("{INPUT_PATH_PREFIX}Crash_barrier_F.p3d")).unwrap();
let odol = ODOL::from_stream(&mut std::io::Cursor::new(&data)).unwrap();
let start = *odol.start_address_of_lods.iter().max().unwrap() as usize;
let err = ODOL::from_stream(&mut std::io::Cursor::new(&data[..start])).unwrap_err();
let message = err.to_string();
assert!(
message.contains(&format!(
"starts at byte {start}, past the end of the file ({start} bytes)"
)),
"{message}"
);
}
28 changes: 28 additions & 0 deletions arma-file-formats/tests/real_virtuality/paa_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
use arma_file_formats::{
self,
core::types::PixelType,
errors::PaaError,
real_virtuality::paa::{Paa, Tagg},
};
use image::ImageBuffer;
Expand Down Expand Up @@ -280,3 +281,30 @@ fn uncompressed_4096_dxt5() {
assert_eq!(paa.mipmaps[0].data, big);
assert_eq!(paa.mipmaps[1].data, small);
}

#[test]
fn not_paa() {
let png = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR";
assert!(matches!(
Paa::from_reader(&mut Cursor::new(png), None),
Err(PaaError::Png)
));
assert!(matches!(
Paa::from_reader(&mut Cursor::new([0x34, 0x12, 0, 0]), None),
Err(PaaError::UnknownType(0x1234))
));
}

/// A texture that ends before its mipmaps, like the decoys of obfuscated PBOs, fails as that.
#[test]
fn truncated() {
let data = fs::read(format!("{}logo_dxt5_128.paa", INPUT_PATH_PREFIX)).unwrap();
let paa = Paa::from_reader(&mut Cursor::new(&data), None).unwrap();
// Cut into the last mipmap, without its end marker.
let len = data.len() - 7;
let err = Paa::from_reader_blocks(&mut Cursor::new(&data[..len]), None).unwrap_err();
assert!(
matches!(err, PaaError::MipmapPastEnd { index, len: l, .. } if index == paa.mipmaps.len() - 1 && l == len as u64),
"{err:?}"
);
}
Loading