use std::fmt::Display;
use std::ops::RangeInclusive;
use crate::error::DxfError;
pub(crate) const MAX_SYSTEM_PAGE_SIZE: usize = 16 * 1024 * 1024;
pub(crate) const MAX_DATA_PAGE_SIZE: usize = 4 * 1024 * 1024;
pub(crate) const MAX_SECTION_SIZE: usize = 1024 * 1024 * 1024;
pub(crate) fn size_from_file(
field: impl Display,
value: impl Into<i128>,
accepted: RangeInclusive<usize>,
) -> Result<usize, DxfError> {
let value = value.into();
match usize::try_from(value) {
Ok(size) if accepted.contains(&size) => Ok(size),
_ => Err(DxfError::InvalidFormat(format!(
"{field} {value} is outside the accepted range {}..={} bytes",
accepted.start(),
accepted.end()
))),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::io::Cursor;
use super::*;
use crate::io::dwg::decompressor_ac18::decompress_ac18;
use crate::io::dwg::dwg_reader::{DwgFileHeaderInfo, DwgPageCrcInfo, DwgSectionInfo};
use crate::io::dwg::{DwgReadOptions, DwgReader, DwgVersion};
#[test]
fn sizes_in_range_are_converted() {
assert_eq!(size_from_file("size", 0i32, 0..=8).unwrap(), 0);
assert_eq!(size_from_file("size", 8u64, 0..=8).unwrap(), 8);
assert_eq!(size_from_file("size", 1i64, 1..=8).unwrap(), 1);
}
#[test]
fn negative_zero_and_oversized_values_are_rejected() {
let cases: [(i128, RangeInclusive<usize>); 5] = [
(-1, 0..=8),
(i128::from(i64::MIN), 0..=usize::MAX),
(0, 1..=8),
(9, 0..=8),
(i128::from(u64::MAX) + 1, 0..=usize::MAX),
];
for (value, accepted) in cases {
assert!(
size_from_file("size", value, accepted.clone()).is_err(),
"{value} in {accepted:?}"
);
}
let message = size_from_file("AC18 page map decompressed size", -1i32, 1..=16)
.unwrap_err()
.to_string();
assert!(
message.contains(
"AC18 page map decompressed size -1 is outside the accepted range 1..=16 bytes"
),
"{message}"
);
}
#[test]
fn decompress_ac18_does_not_reserve_the_declared_size() {
let out = decompress_ac18(&[0x01, 1, 2, 3, 4, 0x11], usize::MAX);
assert_eq!(out, [1, 2, 3, 4]);
assert!(out.capacity() <= MAX_DATA_PAGE_SIZE);
}
fn header_info(
ac18: bool,
section: DwgSectionInfo,
page_records: HashMap<i32, (i64, i64)>,
) -> DwgFileHeaderInfo {
let (version_string, version) = if ac18 {
("AC1018", DwgVersion::AC18)
} else {
("AC1021", DwgVersion::AC21)
};
DwgFileHeaderInfo {
version_string: version_string.to_owned(),
version,
acad_maintenance_version: 0,
preview_address: 0,
dwg_version: 0,
app_release_version: 0,
code_page: 0,
security_type: 0,
summary_info_addr: 0,
vba_project_addr: 0,
ac21_metadata: None,
ac21_header_crc: None,
ac21_unknown_key: None,
ac21_compressed_data_crc: None,
page_records,
section_descriptors: vec![section],
section_locators: HashMap::new(),
objects_base_offset: 0,
is_ac18_format: ac18,
}
}
fn section(
data_size: u64,
page_size: u64,
page_count: u64,
pages: Vec<DwgPageCrcInfo>,
) -> DwgSectionInfo {
DwgSectionInfo {
name: "AcDb:Header".to_owned(),
compressed_size: data_size,
decompressed_size: page_size,
encrypted: 0,
hash_code: 0,
encoding: 4,
page_count,
pages,
}
}
fn page(page_number: i64, decompressed_size: u64, compressed_size: u64) -> DwgPageCrcInfo {
DwgPageCrcInfo {
page_number,
offset: 0,
size: 0,
decompressed_size,
compressed_size,
checksum: 0,
crc: 0,
}
}
fn rejected(info: &DwgFileHeaderInfo, expected: &str) {
for options in [DwgReadOptions::default(), DwgReadOptions::failsafe()] {
let failsafe = options.failsafe;
let stream = Cursor::new(vec![0u8; 64]);
let mut reader = DwgReader::from_stream_with_options(stream, options);
let message = match reader.get_section_buffer("AcDb:Header", info) {
Err(error) => error.to_string(),
Ok(bytes) => panic!("failsafe={failsafe}: read {} bytes", bytes.len()),
};
assert!(message.contains(expected), "failsafe={failsafe}: {message}");
}
}
#[test]
fn ac18_section_is_bounded_by_the_pages_actually_parsed() {
let info = header_info(
true,
section(512 << 20, 0x7400, u64::MAX, vec![]),
HashMap::new(),
);
rejected(&info, "size 536870912 exceeds its 0 page(s)");
let info = header_info(
true,
section(1 << 40, 0x7400, u64::MAX, vec![]),
HashMap::new(),
);
rejected(
&info,
"size 1099511627776 is outside the accepted range 0..=1073741824",
);
}
#[test]
fn ac18_page_size_is_limited() {
let too_big = MAX_DATA_PAGE_SIZE as u64 + 1;
let info = header_info(
true,
section(8, too_big, 1, vec![page(1, 0, 0)]),
HashMap::new(),
);
rejected(&info, "page size 4194305 is outside");
let info = header_info(
true,
section(8, u64::MAX, 1, vec![page(1, 0, 0)]),
HashMap::new(),
);
rejected(&info, "page size 18446744073709551615 is outside");
}
#[test]
fn ac21_page_sizes_are_checked_before_any_page_is_read() {
let records = HashMap::from([(1, (0i64, 64i64))]);
let info = header_info(
false,
section(8, 0xF800, 1, vec![page(1, 1 << 40, 8)]),
records.clone(),
);
rejected(&info, "page 1 decompressed size 1099511627776 is outside");
let info = header_info(
false,
section(8, 0xF800, 1, vec![page(1, 8, 1 << 40)]),
records.clone(),
);
rejected(&info, "page 1 compressed size 1099511627776 is outside");
let info = header_info(
false,
section(8, 0xF800, 1, vec![page(7, u64::MAX, 8)]),
records,
);
rejected(
&info,
"page 7 decompressed size 18446744073709551615 is outside",
);
}
#[test]
fn ac21_section_size_and_page_total_are_limited() {
let info = header_info(false, section(1 << 40, 0xF800, 0, vec![]), HashMap::new());
rejected(
&info,
"size 1099511627776 is outside the accepted range 0..=1073741824",
);
let pages = (1..=300)
.map(|n| page(n, MAX_DATA_PAGE_SIZE as u64, 8))
.collect();
let info = header_info(false, section(8, 0xF800, 300, pages), HashMap::new());
rejected(&info, "page total 1258291200 is outside");
}
}