use crate::{
metadata::streams::StreamHeader,
utils::{read_le, read_le_at},
Error, ParseFailure, ParseStage, Result,
};
use std::io::Write;
pub const CIL_HEADER_MAGIC: u32 = 0x424A_5342;
const MIN_ROOT_HEADER_SIZE: usize = 36;
const VERSION_LENGTH_OFFSET: usize = 12;
const VERSION_STRING_OFFSET: u16 = 16;
const FLAGS_FIELD_SIZE: usize = 2;
const STREAM_COUNT_FIELD_SIZE: usize = 2;
const MAX_VERSION_STRING_LENGTH: usize = 255;
const MAX_STREAM_COUNT: u16 = 6;
const MIN_STREAM_HEADER_SIZE: usize = 12;
const STREAM_HEADER_FIXED_SIZE: usize = 8;
const MAX_STREAM_NAME_LENGTH: usize = 32;
const FIELD_OFFSET_MAJOR_VERSION: usize = 4;
const FIELD_OFFSET_MINOR_VERSION: usize = 6;
const FIELD_OFFSET_RESERVED: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Root {
pub signature: u32,
pub major_version: u16,
pub minor_version: u16,
pub reserved: u32,
pub length: u32,
pub version: String,
pub flags: u16,
pub stream_number: u16,
pub stream_headers: Vec<StreamHeader>,
}
impl Root {
pub fn read(data: &[u8]) -> Result<Root> {
let invalid = |field: &'static str, reason: String| ParseFailure::InvalidField {
stage: ParseStage::MetadataRoot,
field,
reason,
};
let oob = || ParseFailure::OutOfBounds {
stage: ParseStage::MetadataRoot,
};
if data.len() < MIN_ROOT_HEADER_SIZE {
return Err(ParseFailure::Truncated {
stage: ParseStage::MetadataRoot,
expected: MIN_ROOT_HEADER_SIZE,
found: data.len(),
}
.into());
}
let signature = read_le::<u32>(data)?;
if signature != CIL_HEADER_MAGIC {
return Err(ParseFailure::BadMagic {
stage: ParseStage::MetadataRoot,
expected: CIL_HEADER_MAGIC,
found: signature,
}
.into());
}
let version_string_length = read_le_at::<u32>(data, &mut { VERSION_LENGTH_OFFSET })?;
match version_string_length.checked_add(u32::from(VERSION_STRING_OFFSET)) {
Some(str_end) => {
let data_len = u32::try_from(data.len()).map_err(|_| {
invalid("data_length", "metadata root data length too large".into())
})?;
if str_end > data_len {
return Err(oob().into());
}
}
None => {
return Err(invalid(
"version_string_length",
format!(
"{version_string_length} causes integer overflow [ECMA-335 §II.24.2.1]",
),
)
.into())
}
}
let mut version_string: String = String::with_capacity(version_string_length as usize);
for counter in 0..version_string_length {
let mut pos = usize::from(VERSION_STRING_OFFSET)
.checked_add(counter as usize)
.ok_or_else(|| invalid("version_string_offset", "offset overflow".into()))?;
version_string.push(char::from(read_le_at::<u8>(data, &mut pos)?));
}
if version_string.is_empty() {
return Err(invalid(
"version_string",
"version string cannot be empty [ECMA-335 §II.24.2.1]".into(),
)
.into());
}
if !version_string.starts_with('v') {
return Err(invalid(
"version_string",
format!("'{version_string}' must start with 'v' [ECMA-335 §II.24.2.1]",),
)
.into());
}
if version_string.len() > MAX_VERSION_STRING_LENGTH {
return Err(invalid(
"version_string",
format!(
"length {} exceeds reasonable limit ({}) [ECMA-335 §II.24.2.1]",
version_string.len(),
MAX_VERSION_STRING_LENGTH
),
)
.into());
}
let mut stream_count_offset = version_string
.len()
.checked_add(usize::from(VERSION_STRING_OFFSET))
.and_then(|v| v.checked_add(FLAGS_FIELD_SIZE))
.ok_or_else(|| invalid("stream_count_offset", "offset overflow".into()))?;
let stream_count = read_le_at::<u16>(data, &mut stream_count_offset)?;
let stream_count_size = (stream_count as usize)
.checked_mul(MIN_STREAM_HEADER_SIZE)
.ok_or_else(|| invalid("stream_count", "size overflow".into()))?;
if stream_count == 0 || stream_count > MAX_STREAM_COUNT || stream_count_size > data.len() {
return Err(invalid(
"stream_count",
format!("{stream_count} (must be 1-{MAX_STREAM_COUNT}) [ECMA-335 §II.24.2.1]",),
)
.into());
}
let mut streams = Vec::with_capacity(stream_count as usize);
let mut stream_offset = version_string
.len()
.checked_add(usize::from(VERSION_STRING_OFFSET))
.and_then(|v| v.checked_add(FLAGS_FIELD_SIZE))
.and_then(|v| v.checked_add(STREAM_COUNT_FIELD_SIZE))
.ok_or_else(|| invalid("stream_directory_offset", "offset overflow".into()))?;
let mut streams_seen = [false; MAX_STREAM_COUNT as usize];
for _i in 0..stream_count {
if stream_offset > data.len() {
return Err(oob().into());
}
let stream_data = data
.get(stream_offset..)
.ok_or_else(|| Error::from(oob()))?;
let new_stream = StreamHeader::from(stream_data)?;
if new_stream.offset as usize > data.len()
|| new_stream.size as usize > data.len()
|| new_stream.name.len() > MAX_STREAM_NAME_LENGTH
{
return Err(oob().into());
}
match u32::checked_add(new_stream.offset, new_stream.size) {
Some(range) => {
if range as usize > data.len() {
return Err(oob().into());
}
}
None => {
return Err(invalid(
"stream_extent",
format!(
"stream '{}' offset {} + size {} causes integer overflow [ECMA-335 §II.24.2.2]",
new_stream.name, new_stream.offset, new_stream.size
),
)
.into())
}
}
let stream_index = match new_stream.name.as_str() {
"#Strings" => 0usize,
"#US" => 1,
"#Blob" => 2,
"#GUID" => 3,
"#~" => 4,
"#-" => 5,
_ => {
return Err(invalid(
"stream_name",
format!(
"unrecognized stream '{}' [ECMA-335 §II.24.2.2]",
new_stream.name
),
)
.into())
}
};
if *streams_seen
.get(stream_index)
.ok_or_else(|| Error::from(oob()))?
{
return Err(invalid(
"stream_name",
format!(
"duplicate stream '{}' [ECMA-335 §II.24.2.2]",
new_stream.name
),
)
.into());
}
*streams_seen
.get_mut(stream_index)
.ok_or_else(|| Error::from(oob()))? = true;
let name_aligned = new_stream
.name
.len()
.checked_add(1)
.and_then(|v| v.checked_add(3))
.ok_or_else(|| invalid("stream_name", "alignment overflow".into()))?
& !3usize;
stream_offset = stream_offset
.checked_add(STREAM_HEADER_FIXED_SIZE)
.and_then(|v| v.checked_add(name_aligned))
.ok_or_else(|| invalid("stream_offset", "offset overflow".into()))?;
streams.push(new_stream);
}
if streams.is_empty() {
return Err(invalid(
"streams",
"no valid streams found [ECMA-335 §II.24.2.1]".into(),
)
.into());
}
let flags_offset = usize::from(VERSION_STRING_OFFSET)
.checked_add(version_string.len())
.ok_or_else(|| invalid("flags_offset", "offset overflow".into()))?;
Ok(Root {
signature,
major_version: read_le::<u16>(
data.get(FIELD_OFFSET_MAJOR_VERSION..)
.ok_or_else(|| Error::from(oob()))?,
)?,
minor_version: read_le::<u16>(
data.get(FIELD_OFFSET_MINOR_VERSION..)
.ok_or_else(|| Error::from(oob()))?,
)?,
reserved: read_le::<u32>(
data.get(FIELD_OFFSET_RESERVED..)
.ok_or_else(|| Error::from(oob()))?,
)?,
length: u32::try_from(version_string.len()).map_err(|_| {
invalid(
"version_string_length",
"string length too large [ECMA-335 §II.24.2.1]".into(),
)
})?,
flags: read_le::<u16>(data.get(flags_offset..).ok_or_else(|| Error::from(oob()))?)?,
stream_number: u16::try_from(streams.len()).map_err(|_| {
invalid(
"stream_count",
"too many streams [ECMA-335 §II.24.2.1]".into(),
)
})?,
stream_headers: streams,
version: version_string,
})
}
pub fn validate_stream_layout(
&self,
meta_root_offset: usize,
total_metadata_size: u32,
) -> Result<()> {
let invalid = |field: &'static str, reason: String| ParseFailure::InvalidField {
stage: ParseStage::MetadataRoot,
field,
reason,
};
let mut stream_ranges: Vec<(u32, u32, &str)> = Vec::new();
let metadata_end = meta_root_offset
.checked_add(total_metadata_size as usize)
.ok_or_else(|| {
invalid(
"metadata_size",
format!("size causes overflow: {meta_root_offset} + {total_metadata_size}",),
)
})?;
for stream in &self.stream_headers {
let absolute_start = meta_root_offset
.checked_add(stream.offset as usize)
.ok_or_else(|| {
invalid(
"stream_offset",
format!(
"stream '{}' offset causes overflow: {} + {}",
stream.name, meta_root_offset, stream.offset
),
)
})?;
let absolute_end = absolute_start
.checked_add(stream.size as usize)
.ok_or_else(|| {
invalid(
"stream_size",
format!(
"stream '{}' size causes overflow: {} + {}",
stream.name, absolute_start, stream.size
),
)
})?;
if absolute_end > metadata_end {
return Err(invalid(
"stream_extent",
format!(
"stream '{}' extends beyond metadata bounds (end {} > metadata end {})",
stream.name, absolute_end, metadata_end
),
)
.into());
}
stream_ranges.push((
u32::try_from(absolute_start).map_err(|_| {
invalid(
"stream_start",
format!(
"stream '{}' start position {} exceeds u32 range",
stream.name, absolute_start
),
)
})?,
u32::try_from(absolute_end).map_err(|_| {
invalid(
"stream_end",
format!(
"stream '{}' end position {} exceeds u32 range",
stream.name, absolute_end
),
)
})?,
&stream.name,
));
}
for (i, &(start1, end1, name1)) in stream_ranges.iter().enumerate() {
let skip = i.saturating_add(1);
for &(start2, end2, name2) in stream_ranges.iter().skip(skip) {
if start1 < end2 && start2 < end1 {
return Err(invalid(
"stream_overlap",
format!(
"stream '{name1}' ({start1}..{end1}) overlaps with stream '{name2}' ({start2}..{end2})",
),
)
.into());
}
}
}
Ok(())
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_all(&self.signature.to_le_bytes())?;
writer.write_all(&self.major_version.to_le_bytes())?;
writer.write_all(&self.minor_version.to_le_bytes())?;
writer.write_all(&self.reserved.to_le_bytes())?;
let version_bytes = self.version.as_bytes();
let padded_len =
version_bytes
.len()
.checked_add(3)
.ok_or_else(|| ParseFailure::InvalidField {
stage: ParseStage::MetadataRoot,
field: "version_string_length",
reason: "padded length overflow".into(),
})?
& !3usize;
let padded_len_u32 = u32::try_from(padded_len).map_err(|_| ParseFailure::InvalidField {
stage: ParseStage::MetadataRoot,
field: "version_string_length",
reason: format!("padded length {padded_len} exceeds u32 range"),
})?;
writer.write_all(&padded_len_u32.to_le_bytes())?;
writer.write_all(version_bytes)?;
let padding = padded_len.saturating_sub(version_bytes.len());
if padding > 0 {
writer.write_all(&vec![0u8; padding])?;
}
writer.write_all(&self.flags.to_le_bytes())?;
writer.write_all(&self.stream_number.to_le_bytes())?;
for stream in &self.stream_headers {
stream.write_to(writer)?;
}
Ok(())
}
#[must_use]
pub fn serialized_size(&self) -> usize {
let fixed_size = 20usize;
let version_padded = self.version.len().saturating_add(3) & !3usize;
let streams_size: usize = self
.stream_headers
.iter()
.map(crate::StreamHeader::serialized_size)
.fold(0usize, usize::saturating_add);
fixed_size
.saturating_add(version_padded)
.saturating_add(streams_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crafted() {
#[rustfmt::skip]
let header_bytes = [
0x42, 0x53, 0x4A, 0x42,
0x00, 0x20,
0x00, 0x30,
0x00, 0x00, 0x00, 0x40,
0x06, 0x00, 0x00, 0x00, b'v', b'4', b'.', b'0', b'.', b'0',
0x00, 0x60,
0x01, 0x00,
0x1, 0x00, 0x00, 0x00, 0x8, 0x00, 0x00, 0x00,
0x23, 0x7E, 0x00,
];
let parsed_header = Root::read(&header_bytes).unwrap();
assert_eq!(parsed_header.signature, CIL_HEADER_MAGIC);
assert_eq!(parsed_header.major_version, 0x2000);
assert_eq!(parsed_header.minor_version, 0x3000);
assert_eq!(parsed_header.reserved, 0x40000000);
assert_eq!(parsed_header.length, 6);
assert_eq!(parsed_header.version, "v4.0.0");
assert_eq!(parsed_header.flags, 0x6000);
assert_eq!(parsed_header.stream_number, 1);
assert_eq!(parsed_header.stream_headers.len(), 1);
assert_eq!(parsed_header.stream_headers[0].offset, 0x1);
assert_eq!(parsed_header.stream_headers[0].size, 0x8);
assert_eq!(parsed_header.stream_headers[0].name, "#~");
}
#[test]
fn duplicate_stream_names_should_fail() {
#[rustfmt::skip]
let mut header_bytes = vec![
0x42, 0x53, 0x4A, 0x42, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, 0x00, b'v', b'4', b'.', b'0', b'.', b'0', 0x00, 0x60, 0x02, 0x00,
0x52, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x23, 0x7E, 0x00, 0x00,
0x5A, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x23, 0x7E, 0x00, 0x00, ];
header_bytes.resize(98, 0x00);
let result = Root::read(&header_bytes);
let err = result.expect_err("Expected error for duplicate stream names");
let error_string = err.to_string();
assert!(
error_string.contains("duplicate stream") && error_string.contains("#~"),
"Expected error about duplicate stream '#~', got: {}",
error_string
);
}
#[test]
fn test_write_to_basic() {
let root = Root {
signature: CIL_HEADER_MAGIC,
major_version: 1,
minor_version: 1,
reserved: 0,
length: 12, version: "v4.0.30319".to_string(),
flags: 0,
stream_number: 1,
stream_headers: vec![StreamHeader {
offset: 0x6C,
size: 0x1000,
name: "#~".to_string(),
}],
};
let mut buffer = Vec::new();
root.write_to(&mut buffer).unwrap();
assert_eq!(&buffer[0..4], &[0x42, 0x53, 0x4A, 0x42]);
assert_eq!(&buffer[4..6], &[0x01, 0x00]);
assert_eq!(&buffer[6..8], &[0x01, 0x00]);
assert_eq!(&buffer[8..12], &[0x00, 0x00, 0x00, 0x00]);
assert_eq!(&buffer[12..16], &[0x0C, 0x00, 0x00, 0x00]);
assert_eq!(&buffer[16..20], b"v4.0");
}
#[test]
fn test_serialized_size() {
let root = Root {
signature: CIL_HEADER_MAGIC,
major_version: 1,
minor_version: 1,
reserved: 0,
length: 12,
version: "v4.0.30319".to_string(), flags: 0,
stream_number: 1,
stream_headers: vec![StreamHeader {
offset: 0x6C,
size: 0x1000,
name: "#~".to_string(), }],
};
assert_eq!(root.serialized_size(), 44);
}
#[test]
fn test_serialized_size_multiple_streams() {
let root = Root {
signature: CIL_HEADER_MAGIC,
major_version: 1,
minor_version: 1,
reserved: 0,
length: 12,
version: "v4.0.30319".to_string(),
flags: 0,
stream_number: 3,
stream_headers: vec![
StreamHeader {
offset: 0x6C,
size: 0x1000,
name: "#~".to_string(),
}, StreamHeader {
offset: 0x106C,
size: 0x500,
name: "#Strings".to_string(),
}, StreamHeader {
offset: 0x156C,
size: 0x100,
name: "#Blob".to_string(),
}, ],
};
assert_eq!(root.serialized_size(), 80);
}
}