use crate::read::HashTable;
use crate::read::error::{Error, Result};
use crate::read::header::Header;
use crate::read::pointer::Pointer;
use std::borrow::Cow;
use std::io::Read;
use std::path::Path;
#[derive(Debug)]
pub(crate) enum Data<'a> {
Cow(Cow<'a, [u8]>),
#[cfg(feature = "mmap")]
Mmap(memmap2::Mmap),
}
impl AsRef<[u8]> for Data<'_> {
fn as_ref(&self) -> &[u8] {
match self {
Data::Cow(cow) => cow.as_ref(),
#[cfg(feature = "mmap")]
Data::Mmap(mmap) => mmap.as_ref(),
}
}
}
impl<'a> Data<'a> {
pub fn dereference(&'a self, pointer: &Pointer, alignment: u32) -> Result<&'a [u8]> {
let start: usize = pointer.start() as usize;
let end: usize = pointer.end() as usize;
let alignment: usize = alignment as usize;
if start > end {
Err(Error::DataOffset)
} else if start & (alignment - 1) != 0 {
Err(Error::DataAlignment)
} else {
self.as_ref().get(start..end).ok_or(Error::DataOffset)
}
}
}
pub struct File<'a> {
pub(crate) data: Data<'a>,
pub(crate) endianness: crate::Endian,
pub(crate) header: Header,
}
impl<'a> File<'a> {
pub fn hash_table(&self) -> Result<HashTable<'_, '_>> {
let header = self.header;
let root_ptr = header.root();
self.read_hash_table(root_ptr)
}
pub(crate) fn read_hash_table(&self, pointer: &Pointer) -> Result<HashTable<'_, '_>> {
let data = self.data.dereference(pointer, 4)?;
HashTable::for_bytes(data, self)
}
pub(crate) fn dereference(&self, pointer: &Pointer, alignment: u32) -> Result<&[u8]> {
self.data.dereference(pointer, alignment)
}
fn from_data(data: Data<'a>) -> Result<Self> {
let header = Header::try_from_bytes(data.as_ref())?;
let byteswapped = header.is_byteswap()?;
let endianness = if cfg!(target_endian = "little") && !byteswapped
|| cfg!(target_endian = "big") && byteswapped
{
crate::Endian::Little
} else {
crate::Endian::Big
};
Ok(Self {
data,
endianness,
header,
})
}
pub fn from_bytes(bytes: Cow<'a, [u8]>) -> Result<Self> {
Self::from_data(Data::Cow(bytes))
}
pub fn from_file(filename: &Path) -> Result<Self> {
let mut file =
std::fs::File::open(filename).map_err(Error::from_io_with_filename(filename))?;
let mut data = Vec::with_capacity(
file.metadata()
.map_err(Error::from_io_with_filename(filename))?
.len() as usize,
);
file.read_to_end(&mut data)
.map_err(Error::from_io_with_filename(filename))?;
Self::from_bytes(Cow::Owned(data))
}
#[cfg(feature = "mmap")]
pub unsafe fn from_file_mmap(filename: &Path) -> Result<Self> {
let file = std::fs::File::open(filename).map_err(Error::from_io_with_filename(filename))?;
let mmap =
unsafe { memmap2::Mmap::map(&file) }.map_err(Error::from_io_with_filename(filename))?;
Self::from_data(Data::Mmap(mmap))
}
pub(crate) fn endianness(&self) -> crate::Endian {
self.endianness
}
}
impl std::fmt::Debug for File<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Ok(hash_table) = self.hash_table() {
f.debug_struct("File")
.field("endianness", &self.endianness)
.field("header", &self.header)
.field("hash_table", &hash_table)
.finish()
} else {
f.debug_struct("File")
.field("endianness", &self.endianness)
.field("header", &self.header)
.finish_non_exhaustive()
}
}
}
#[cfg(test)]
mod test {
use crate::read::file::File;
use std::borrow::Cow;
use std::mem::size_of;
use std::path::PathBuf;
use crate::read::{Error, HashItem, Header, Pointer};
use crate::test::*;
use crate::write::{FileWriter, HashTableBuilder};
use matches::assert_matches;
#[allow(unused_imports)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use zerocopy::IntoBytes;
#[test]
fn test_file_1() {
let file = File::from_file(&TEST_FILE_1).unwrap();
assert_is_file_1(&file);
}
#[cfg(feature = "mmap")]
#[test]
fn test_file_1_mmap() {
let file = unsafe { File::from_file_mmap(&TEST_FILE_1).unwrap() };
assert_is_file_1(&file);
}
#[test]
fn test_file_2() {
let file = File::from_file(&TEST_FILE_2).unwrap();
assert_is_file_2(&file);
}
#[test]
fn test_file_3() {
let file = File::from_file(&TEST_FILE_3).unwrap();
assert_is_file_3(&file);
}
#[test]
fn invalid_header() {
let header = Header::new_be(0, Pointer::new(0, 0));
let mut data = header.as_bytes().to_vec();
data[0] = 0;
assert_matches!(File::from_bytes(Cow::Owned(data)), Err(Error::Data(_)));
}
#[test]
fn invalid_version() {
let header = Header::new_le(1, Pointer::new(0, 0));
let data = header.as_bytes().to_vec();
assert_matches!(File::from_bytes(Cow::Owned(data)), Err(Error::Data(_)));
}
#[test]
fn file_does_not_exist() {
let res = File::from_file(&PathBuf::from("this_file_does_not_exist"));
assert_matches!(res, Err(Error::Io(_, _)));
println!("{}", res.unwrap_err());
}
#[cfg(feature = "mmap")]
#[test]
fn file_error_mmap() {
unsafe {
assert_matches!(
File::from_file_mmap(&PathBuf::from("this_file_does_not_exist")),
Err(Error::Io(_, _))
);
}
}
fn create_minimal_file() -> File<'static> {
let header = Header::new_le(0, Pointer::new(0, 0));
let data = header.as_bytes().to_vec();
assert_bytes_eq(
&data,
&[
71, 86, 97, 114, 105, 97, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
"GVDB header",
);
File::from_bytes(Cow::Owned(data)).unwrap()
}
#[test]
fn test_minimal_file() {
let file = create_minimal_file();
assert!(!format!("{file:?}").is_empty());
}
#[test]
fn broken_hash_table() {
let writer = FileWriter::new();
let mut table = HashTableBuilder::new();
table.insert_string("test", "test").unwrap();
let mut data = writer.write_to_vec_with_table(table).unwrap();
data.remove(data.len() - 24);
let root_ptr_end = size_of::<u32>() * 5;
data[root_ptr_end] -= 25;
let file = File::from_bytes(Cow::Owned(data)).unwrap();
let err = file.hash_table().unwrap_err();
assert_matches!(err, Error::Data(_));
assert!(format!("{err}").contains("Not enough bytes to fit hash table"));
}
#[test]
fn broken_hash_table2() {
let writer = FileWriter::new();
let mut table = HashTableBuilder::new();
table.insert_string("test", "test").unwrap();
let mut data = writer.write_to_vec_with_table(table).unwrap();
let root_ptr_end = size_of::<u32>() * 5;
data[root_ptr_end] -= 23;
let file = File::from_bytes(Cow::Owned(data)).unwrap();
let err = file.hash_table().unwrap_err();
assert_matches!(err, Error::Data(_));
assert!(format!("{err}").contains("Hash item size invalid"));
}
#[test]
fn invalid_gvariant() {
let writer = FileWriter::new();
let mut table = HashTableBuilder::new();
table.insert_string("test", "test").unwrap();
let mut data = writer.write_to_vec_with_table(table).unwrap();
let file = File::from_bytes(Cow::Borrowed(&data[..])).unwrap();
let table = file.hash_table().unwrap();
let item = table.get_hash_item("test").unwrap();
let value_ptr = item.value_ptr();
drop(file);
for i in value_ptr.start()..value_ptr.end() {
data[i as usize] = u8::MAX;
}
let file = File::from_bytes(Cow::Owned(data)).unwrap();
let table = file.hash_table().unwrap();
assert_matches!(table.get::<String>("test"), Err(Error::Data(msg)) if msg.contains("gvariant"));
assert_matches!(table.get_value("test"), Err(Error::Data(msg)) if msg.contains("gvariant"));
for value in table.values() {
assert_matches!(value, Err(Error::Data(msg)) if msg.contains("gvariant"));
}
}
#[test]
fn parent_invalid_offset() {
let writer = FileWriter::new();
let mut table = HashTableBuilder::new();
table.insert_string("parent/test", "test").unwrap();
let mut data = writer.write_to_vec_with_table(table).unwrap();
let file = File::from_bytes(Cow::Owned(data.clone())).unwrap();
let hash_item_size = size_of::<HashItem>();
let start = file.hash_table().unwrap().header.items_offset() + hash_item_size * 2;
let parent_field = start + 4;
data[parent_field..parent_field + size_of::<u32>()]
.copy_from_slice(10u32.to_le().as_bytes());
println!("{:?}", File::from_bytes(Cow::Owned(data.clone())).unwrap());
let file = File::from_bytes(Cow::Owned(data)).unwrap();
let err = file
.hash_table()
.unwrap()
.keys()
.collect::<Result<Vec<_>, _>>()
.unwrap_err();
assert_matches!(err, Error::Data(_));
assert!(format!("{err}").contains("Parent with invalid offset"));
assert!(format!("{err}").contains("10"));
}
#[test]
fn parent_loop() {
let writer = FileWriter::new();
let mut table = HashTableBuilder::new();
table.insert_string("parent/test", "test").unwrap();
let mut data = writer.write_to_vec_with_table(table).unwrap();
let file = File::from_bytes(Cow::Owned(data.clone())).unwrap();
let hash_item_size = size_of::<HashItem>();
let start = file.hash_table().unwrap().header.items_offset() + hash_item_size * 2;
let parent_field = start + 4;
data[parent_field..parent_field + size_of::<u32>()]
.copy_from_slice(1u32.to_le().as_bytes());
println!("{:?}", File::from_bytes(Cow::Owned(data.clone())).unwrap());
let file = File::from_bytes(Cow::Owned(data)).unwrap();
let err = file
.hash_table()
.unwrap()
.keys()
.collect::<Result<Vec<_>, _>>()
.unwrap_err();
assert_matches!(err, Error::Data(_));
assert!(format!("{err}").contains("loop"));
}
#[test]
fn iter() {
let writer = FileWriter::new();
let mut table = HashTableBuilder::new();
table.insert_string("iter/test", "test").unwrap();
table.insert("iter/test2", "test2").unwrap();
let data = writer.write_to_vec_with_table(table).unwrap();
let file = File::from_bytes(Cow::Owned(data.clone())).unwrap();
let table = file.hash_table().unwrap();
let values = table.values().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(String::try_from(&values[0]).unwrap(), "test");
assert_eq!(String::try_from(&values[1]).unwrap(), "test2");
}
#[test]
fn test_dereference_offset1() {
let file = create_minimal_file();
let res = file.data.dereference(&Pointer::new(40, 42), 2);
assert_matches!(res, Err(Error::DataOffset));
println!("{}", res.unwrap_err());
}
#[test]
fn test_dereference_offset2() {
let file = create_minimal_file();
let res = file.data.dereference(&Pointer::new(10, 0), 2);
assert_matches!(res, Err(Error::DataOffset));
println!("{}", res.unwrap_err());
}
#[test]
fn test_dereference_offset3() {
let file = create_minimal_file();
let res = file.data.dereference(&Pointer::new(10, 0), 2);
assert_matches!(res, Err(Error::DataOffset));
println!("{}", res.unwrap_err());
}
#[test]
fn test_dereference_alignment() {
let file = create_minimal_file();
let res = file.data.dereference(&Pointer::new(1, 2), 2);
assert_matches!(res, Err(Error::DataAlignment));
println!("{}", res.unwrap_err());
}
#[test]
fn test_nested_dict() {
let file = File::from_file(&TEST_FILE_2).unwrap();
let table = file.hash_table().unwrap();
let table_res = table.get_value("table");
assert_matches!(table_res, Err(Error::Data(_)));
}
#[test]
fn test_nested_dict_fail() {
let file = File::from_file(&TEST_FILE_2).unwrap();
let table = file.hash_table().unwrap();
let res = table.get_hash_table("string");
assert_matches!(res, Err(Error::Data(_)));
}
#[test]
fn test_from_file_lifetime() {
let filename = TEST_FILE_2.clone();
let file = File::from_file(&filename).unwrap();
drop(filename);
let table = file.hash_table().unwrap();
let table2 = file.hash_table().unwrap();
table2.keys().collect::<Result<Vec<_>, _>>().unwrap();
table.keys().collect::<Result<Vec<_>, _>>().unwrap();
}
}