use std::collections::BTreeSet;
use std::ops::Range;
use crate::endian::{Cursor, Encoder, Endian};
use crate::error::{Error, Result};
use crate::format::{GSYM_MAGIC, align_up};
use super::v1::detect_endian;
pub(crate) const VERSION: u16 = 2;
pub(crate) const HEADER_SIZE: usize = 20;
pub(crate) const GLOBAL_DATA_SIZE: usize = 20;
pub(crate) const ADDRESS_INFO_OFFSET_SIZE: u8 = 8;
pub(crate) const STRING_OFFSET_SIZE: u8 = 8;
pub(crate) const FILE_ENTRY_SIZE: usize = 16;
pub(crate) const STRING_TABLE_ENCODING_DEFAULT: u8 = 0;
pub(crate) const GLOBAL_END: u32 = 0;
pub(crate) const GLOBAL_ADDRESS_OFFSETS: u32 = 1;
pub(crate) const GLOBAL_ADDRESS_INFO_OFFSETS: u32 = 2;
pub(crate) const GLOBAL_STRING_TABLE: u32 = 3;
pub(crate) const GLOBAL_FILE_TABLE: u32 = 4;
pub(crate) const GLOBAL_FUNCTION_INFO: u32 = 5;
pub(crate) const GLOBAL_UUID: u32 = 6;
const REQUIRED_TYPES: [u32; 5] = [
GLOBAL_ADDRESS_OFFSETS,
GLOBAL_ADDRESS_INFO_OFFSETS,
GLOBAL_STRING_TABLE,
GLOBAL_FILE_TABLE,
GLOBAL_FUNCTION_INFO,
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Header {
pub(crate) address_offset_size: u8,
pub(crate) string_table_encoding: u8,
pub(crate) base_address: u64,
pub(crate) address_count: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct GlobalData {
pub(crate) section_type: u32,
pub(crate) file_offset: u64,
pub(crate) file_size: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Directory {
pub(crate) entries: Box<[GlobalData]>,
pub(crate) end_offset: usize,
}
impl Directory {
pub(crate) fn get(&self, section_type: u32) -> Option<GlobalData> {
self.entries
.iter()
.copied()
.find(|entry| entry.section_type == section_type)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct WriteLayout {
pub(crate) directory: Box<[GlobalData]>,
pub(crate) directory_end: usize,
pub(crate) uuid: Option<Range<usize>>,
pub(crate) address_offsets: Range<usize>,
pub(crate) address_info_offsets: Range<usize>,
pub(crate) file_table: Range<usize>,
pub(crate) string_table: Range<usize>,
pub(crate) function_info: Range<usize>,
pub(crate) file_size: usize,
}
impl Header {
pub(crate) fn decode(bytes: &[u8]) -> Result<(Self, Endian)> {
let endian = detect_endian(bytes)?;
let mut cursor = Cursor::new(bytes, endian);
let magic = cursor.read_u32()?;
if magic != GSYM_MAGIC {
return Err(Error::InvalidMagic(magic));
}
let version = cursor.read_u16()?;
if version != VERSION {
return Err(Error::UnsupportedVersion(version));
}
let address_offset_size = cursor.read_u8()?;
validate_address_offset_size(address_offset_size)?;
let string_table_encoding = cursor.read_u8()?;
if string_table_encoding != STRING_TABLE_ENCODING_DEFAULT {
return Err(Error::UnsupportedStringTableEncoding(string_table_encoding));
}
let base_address = cursor.read_u64()?;
let address_count = cursor.read_u32()?;
Ok((
Self {
address_offset_size,
string_table_encoding,
base_address,
address_count,
},
endian,
))
}
pub(crate) fn encode(&self, endian: Endian) -> Result<Vec<u8>> {
validate_address_offset_size(self.address_offset_size)?;
if self.string_table_encoding != STRING_TABLE_ENCODING_DEFAULT {
return Err(Error::UnsupportedStringTableEncoding(
self.string_table_encoding,
));
}
let mut output = Encoder::with_capacity(endian, HEADER_SIZE);
output.write_u32(GSYM_MAGIC);
output.write_u16(VERSION);
output.write_u8(self.address_offset_size);
output.write_u8(self.string_table_encoding);
output.write_u64(self.base_address);
output.write_u32(self.address_count);
debug_assert_eq!(output.len(), HEADER_SIZE);
Ok(output.into_inner())
}
pub(crate) fn write_layout(
&self,
uuid_size: usize,
file_count: u32,
string_table_size: usize,
function_info_size: usize,
) -> Result<WriteLayout> {
let has_uuid = uuid_size != 0;
let directory_entry_count = 5_usize
.checked_add(usize::from(has_uuid))
.and_then(|count| count.checked_add(1))
.ok_or(Error::Overflow("v2 directory entry count"))?;
let directory_end = HEADER_SIZE
.checked_add(
directory_entry_count
.checked_mul(GLOBAL_DATA_SIZE)
.ok_or(Error::Overflow("v2 directory size"))?,
)
.ok_or(Error::Overflow("v2 directory end"))?;
let mut current = directory_end;
let uuid = if has_uuid {
let end = current
.checked_add(uuid_size)
.ok_or(Error::Overflow("v2 UUID section end"))?;
let range = current..end;
current = end;
Some(range)
} else {
None
};
current = align_up(current, usize::from(self.address_offset_size))
.ok_or(Error::Overflow("v2 address-table alignment"))?;
let address_size = usize::try_from(self.address_count)
.map_err(|_| Error::Overflow("v2 address count"))?
.checked_mul(usize::from(self.address_offset_size))
.ok_or(Error::Overflow("v2 address-table size"))?;
let address_offsets = checked_range(current, address_size, "v2 address table")?;
current = address_offsets.end;
current = align_up(current, usize::from(ADDRESS_INFO_OFFSET_SIZE))
.ok_or(Error::Overflow("v2 address-info alignment"))?;
let address_info_size = usize::try_from(self.address_count)
.map_err(|_| Error::Overflow("v2 address count"))?
.checked_mul(usize::from(ADDRESS_INFO_OFFSET_SIZE))
.ok_or(Error::Overflow("v2 address-info size"))?;
let address_info_offsets =
checked_range(current, address_info_size, "v2 address-info table")?;
current = address_info_offsets.end;
current = align_up(current, 4).ok_or(Error::Overflow("v2 file-table alignment"))?;
let file_size = file_table_size(file_count)?;
let file_table = checked_range(current, file_size, "v2 file table")?;
current = file_table.end;
let string_table = checked_range(current, string_table_size, "v2 string table")?;
current = string_table.end;
current = align_up(current, 4).ok_or(Error::Overflow("v2 FunctionInfo alignment"))?;
let function_info = checked_range(current, function_info_size, "v2 FunctionInfo section")?;
current = function_info.end;
let mut directory = Vec::with_capacity(directory_entry_count.saturating_sub(1));
if let Some(range) = &uuid {
directory.push(global_from_range(GLOBAL_UUID, range)?);
}
directory.push(global_from_range(GLOBAL_ADDRESS_OFFSETS, &address_offsets)?);
directory.push(global_from_range(
GLOBAL_ADDRESS_INFO_OFFSETS,
&address_info_offsets,
)?);
directory.push(global_from_range(GLOBAL_FILE_TABLE, &file_table)?);
directory.push(global_from_range(GLOBAL_STRING_TABLE, &string_table)?);
directory.push(global_from_range(GLOBAL_FUNCTION_INFO, &function_info)?);
Ok(WriteLayout {
directory: directory.into_boxed_slice(),
directory_end,
uuid,
address_offsets,
address_info_offsets,
file_table,
string_table,
function_info,
file_size: current,
})
}
}
fn validate_address_offset_size(size: u8) -> Result<()> {
if (1..=8).contains(&size) {
Ok(())
} else {
Err(Error::InvalidAddressOffsetSize {
version: VERSION,
size,
})
}
}
impl GlobalData {
pub(crate) fn decode_from(cursor: &mut Cursor<'_>) -> Result<Self> {
Ok(Self {
section_type: cursor.read_u32()?,
file_offset: cursor.read_u64()?,
file_size: cursor.read_u64()?,
})
}
pub(crate) fn encode_into(self, output: &mut Encoder) {
output.write_u32(self.section_type);
output.write_u64(self.file_offset);
output.write_u64(self.file_size);
}
}
pub(crate) fn parse_directory(bytes: &[u8], endian: Endian, header: &Header) -> Result<Directory> {
let mut cursor = Cursor::at(bytes, endian, HEADER_SIZE)?;
let mut entries = Vec::new();
let mut seen = BTreeSet::new();
loop {
let entry = GlobalData::decode_from(&mut cursor)?;
if entry.section_type == GLOBAL_END {
if entry.file_offset != 0 || entry.file_size != 0 {
return Err(Error::InvalidFormat(
"v2 directory terminator is not all zero",
));
}
break;
}
if !seen.insert(entry.section_type) {
return Err(Error::DuplicateSection(entry.section_type));
}
if entry.file_size == 0 {
return Err(Error::ZeroSizedSection {
section_type: entry.section_type,
});
}
let end = entry
.file_offset
.checked_add(entry.file_size)
.ok_or(Error::Overflow("v2 section end"))?;
if end > bytes.len() as u64 {
return Err(Error::SectionOutOfBounds {
section_type: entry.section_type,
offset: entry.file_offset,
size: entry.file_size,
input_len: bytes.len(),
});
}
entries.push(entry);
}
let end_offset = cursor.position();
for required in REQUIRED_TYPES {
if !seen.contains(&required) {
return Err(Error::MissingSection(required));
}
}
validate_sections(bytes, end_offset, header, &entries)?;
Ok(Directory {
entries: entries.into_boxed_slice(),
end_offset,
})
}
fn validate_sections(
bytes: &[u8],
directory_end: usize,
header: &Header,
entries: &[GlobalData],
) -> Result<()> {
let address_size = u64::from(header.address_count)
.checked_mul(u64::from(header.address_offset_size))
.ok_or(Error::Overflow("v2 address-table size"))?;
let address_info_size = u64::from(header.address_count)
.checked_mul(u64::from(ADDRESS_INFO_OFFSET_SIZE))
.ok_or(Error::Overflow("v2 address-info size"))?;
for entry in entries {
match entry.section_type {
GLOBAL_ADDRESS_OFFSETS if entry.file_size != address_size => {
return Err(Error::InvalidFormat("v2 address-table size mismatch"));
}
GLOBAL_ADDRESS_OFFSETS
if entry
.file_offset
.checked_rem(u64::from(header.address_offset_size))
.is_none_or(|remainder| remainder != 0) =>
{
return Err(Error::InvalidFormat("v2 address table is misaligned"));
}
GLOBAL_ADDRESS_INFO_OFFSETS if entry.file_size != address_info_size => {
return Err(Error::InvalidFormat("v2 address-info table size mismatch"));
}
GLOBAL_ADDRESS_INFO_OFFSETS
if entry
.file_offset
.checked_rem(u64::from(ADDRESS_INFO_OFFSET_SIZE))
.is_none_or(|remainder| remainder != 0) =>
{
return Err(Error::InvalidFormat("v2 address-info table is misaligned"));
}
GLOBAL_FILE_TABLE
if entry.file_size < 4
|| entry
.file_size
.saturating_sub(4)
.checked_rem(FILE_ENTRY_SIZE as u64)
.is_none_or(|remainder| remainder != 0) =>
{
return Err(Error::InvalidFormat("invalid v2 file-table size"));
}
GLOBAL_FILE_TABLE if entry.file_offset % 4 != 0 => {
return Err(Error::InvalidFormat("v2 file table is misaligned"));
}
GLOBAL_STRING_TABLE => {
let offset = usize::try_from(entry.file_offset)
.map_err(|_| Error::Overflow("v2 string-table offset"))?;
if bytes.get(offset).copied() != Some(0) {
return Err(Error::InvalidFormat(
"GSYM string table does not begin with an empty string",
));
}
}
GLOBAL_FUNCTION_INFO if entry.file_offset % 4 != 0 => {
return Err(Error::InvalidFormat(
"v2 FunctionInfo section is misaligned",
));
}
_ => {}
}
}
let mut ranges: Vec<(u64, u64, u32)> = entries
.iter()
.map(|entry| {
Ok((
entry.file_offset,
entry
.file_offset
.checked_add(entry.file_size)
.ok_or(Error::Overflow("v2 section end"))?,
entry.section_type,
))
})
.collect::<Result<_>>()?;
ranges.sort_unstable_by_key(|range| range.0);
let mut previous_end = directory_end as u64;
for (start, end, _) in ranges {
if start < previous_end {
return Err(Error::InvalidFormat(
"v2 sections overlap each other or the directory",
));
}
previous_end = end;
}
Ok(())
}
pub(crate) fn file_table_size(file_count: u32) -> Result<usize> {
super::file_table_size(file_count, FILE_ENTRY_SIZE, "v2 file-table size")
}
fn checked_range(start: usize, size: usize, context: &'static str) -> Result<Range<usize>> {
let end = start.checked_add(size).ok_or(Error::Overflow(context))?;
Ok(start..end)
}
fn global_from_range(section_type: u32, range: &Range<usize>) -> Result<GlobalData> {
Ok(GlobalData {
section_type,
file_offset: u64::try_from(range.start).map_err(|_| Error::Overflow("file offset"))?,
file_size: u64::try_from(range.len()).map_err(|_| Error::Overflow("file size"))?,
})
}