use crate::read::error::{GvdbReaderError, GvdbReaderResult};
use crate::read::hash_item::{GvdbHashItem, GvdbHashItemType};
use crate::read::header::GvdbHeader;
use crate::read::pointer::GvdbPointer;
use crate::read::GvdbHashTable;
use memmap2::Mmap;
use safe_transmute::transmute_one_pedantic;
use std::borrow::Cow;
use std::fs::File;
use std::io::Read;
use std::mem::size_of;
use std::path::Path;
#[derive(Debug)]
pub(crate) enum GvdbData {
Cow(Cow<'static, [u8]>),
Mmap(Mmap),
}
impl AsRef<[u8]> for GvdbData {
fn as_ref(&self) -> &[u8] {
match self {
GvdbData::Cow(cow) => cow.as_ref(),
GvdbData::Mmap(mmap) => mmap.as_ref(),
}
}
}
#[derive(Debug)]
pub struct GvdbFile {
pub(crate) data: GvdbData,
pub(crate) byteswapped: bool,
}
impl GvdbFile {
pub(crate) fn get_header(&self) -> GvdbReaderResult<GvdbHeader> {
let header_data = self
.data
.as_ref()
.get(0..size_of::<GvdbHeader>())
.ok_or(GvdbReaderError::DataOffset)?;
Ok(transmute_one_pedantic(header_data)?)
}
pub fn hash_table(&self) -> GvdbReaderResult<GvdbHashTable> {
let header = self.get_header()?;
let root_ptr = header.root();
GvdbHashTable::for_bytes(self.dereference(root_ptr, 4)?, self)
}
pub(crate) fn dereference(
&self,
pointer: &GvdbPointer,
alignment: u32,
) -> GvdbReaderResult<&[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(GvdbReaderError::DataOffset)
} else if start & (alignment - 1) != 0 {
Err(GvdbReaderError::DataAlignment)
} else {
self.data
.as_ref()
.get(start..end)
.ok_or(GvdbReaderError::DataOffset)
}
}
fn read_header(&mut self) -> GvdbReaderResult<()> {
let header = self.get_header()?;
if !header.header_valid() {
return Err(GvdbReaderError::DataError(
"Invalid GVDB header. Is this a GVDB file?".to_string(),
));
}
self.byteswapped = header.is_byteswap()?;
if header.version() != 0 {
return Err(GvdbReaderError::DataError(format!(
"Unknown GVDB file format version: {}",
header.version()
)));
}
Ok(())
}
pub fn from_bytes(bytes: Cow<'static, [u8]>) -> GvdbReaderResult<GvdbFile> {
let mut this = Self {
data: GvdbData::Cow(bytes),
byteswapped: false,
};
this.read_header()?;
Ok(this)
}
pub fn from_file(filename: &Path) -> GvdbReaderResult<Self> {
let mut file =
File::open(filename).map_err(GvdbReaderError::from_io_with_filename(filename))?;
let mut data = Vec::with_capacity(
file.metadata()
.map_err(GvdbReaderError::from_io_with_filename(filename))?
.len() as usize,
);
file.read_to_end(&mut data)
.map_err(GvdbReaderError::from_io_with_filename(filename))?;
Self::from_bytes(Cow::Owned(data))
}
pub unsafe fn from_file_mmap(filename: &Path) -> GvdbReaderResult<Self> {
let file =
File::open(filename).map_err(GvdbReaderError::from_io_with_filename(filename))?;
let mmap = Mmap::map(&file).map_err(GvdbReaderError::from_io_with_filename(filename))?;
let mut this = Self {
data: GvdbData::Mmap(mmap),
byteswapped: false,
};
this.read_header()?;
Ok(this)
}
pub(crate) fn get_key(&self, item: &GvdbHashItem) -> GvdbReaderResult<String> {
let data = self.dereference(&item.key_ptr(), 1)?;
Ok(String::from_utf8(data.to_vec())?)
}
fn get_bytes_for_item(&self, item: &GvdbHashItem) -> GvdbReaderResult<&[u8]> {
let typ = item.typ()?;
if typ == GvdbHashItemType::Value {
Ok(self.dereference(item.value_ptr(), 8)?)
} else {
Err(GvdbReaderError::DataError(format!(
"Unable to parse item for key '{}' as GVariant: Expected type 'v', got type {}",
self.get_key(item)?,
typ
)))
}
}
#[cfg(feature = "glib")]
pub(crate) fn get_gvariant_for_item(
&self,
item: &GvdbHashItem,
) -> GvdbReaderResult<glib::Variant> {
let data = self.get_bytes_for_item(item)?;
let variant = glib::Variant::from_data_with_type(data, glib::VariantTy::VARIANT);
if self.byteswapped {
Ok(variant.byteswap())
} else {
Ok(variant)
}
}
pub(crate) fn get_value_for_item(
&self,
item: &GvdbHashItem,
) -> GvdbReaderResult<zvariant::Value> {
let data = self.get_bytes_for_item(item)?;
#[cfg(target_endian = "little")]
let le = true;
#[cfg(target_endian = "big")]
let le = false;
if le && !self.byteswapped || !le && self.byteswapped {
let context = zvariant::EncodingContext::<byteorder::LE>::new_gvariant(0);
Ok(zvariant::from_slice(data, context)?)
} else {
let context = zvariant::EncodingContext::<byteorder::BE>::new_gvariant(0);
Ok(zvariant::from_slice(data, context)?)
}
}
pub(crate) fn get_hash_table_for_item(
&self,
item: &GvdbHashItem,
) -> GvdbReaderResult<GvdbHashTable> {
let typ = item.typ()?;
if typ == GvdbHashItemType::HashTable {
GvdbHashTable::for_bytes(self.dereference(item.value_ptr(), 4)?, self)
} else {
Err(GvdbReaderError::DataError(format!(
"Unable to parse item for key '{}' as hash table: Expected type 'H', got type '{}'",
self.get_key(item)?,
typ
)))
}
}
}
#[cfg(test)]
mod test {
use crate::read::file::GvdbFile;
use std::borrow::Cow;
use std::mem::size_of;
use std::path::PathBuf;
use crate::read::{GvdbHeader, GvdbPointer, GvdbReaderError};
use crate::test::*;
use crate::write::{GvdbFileWriter, GvdbHashTableBuilder};
use matches::assert_matches;
#[allow(unused_imports)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use safe_transmute::transmute_one_to_bytes;
#[test]
fn test_file_1() {
let file = GvdbFile::from_file(&TEST_FILE_1).unwrap();
assert_is_file_1(&file);
}
#[test]
fn test_file_1_mmap() {
let file = unsafe { GvdbFile::from_file_mmap(&TEST_FILE_1).unwrap() };
assert_is_file_1(&file);
}
#[test]
fn test_file_2() {
let file = GvdbFile::from_file(&TEST_FILE_2).unwrap();
assert_is_file_2(&file);
}
#[test]
fn test_file_3() {
let file = GvdbFile::from_file(&TEST_FILE_3).unwrap();
assert_is_file_3(&file);
}
#[test]
fn invalid_header() {
let header = GvdbHeader::new(false, 0, GvdbPointer::new(0, 0));
let mut data = transmute_one_to_bytes(&header).to_vec();
data[0] = 0;
assert_matches!(
GvdbFile::from_bytes(Cow::Owned(data)),
Err(GvdbReaderError::DataError(_))
);
}
#[test]
fn invalid_version() {
let header = GvdbHeader::new(false, 1, GvdbPointer::new(0, 0));
let data = transmute_one_to_bytes(&header).to_vec();
assert_matches!(
GvdbFile::from_bytes(Cow::Owned(data)),
Err(GvdbReaderError::DataError(_))
);
}
#[test]
fn file_does_not_exist() {
let res = GvdbFile::from_file(&PathBuf::from("this_file_does_not_exist"));
assert_matches!(res, Err(GvdbReaderError::Io(_, _)));
println!("{}", res.unwrap_err());
}
#[test]
fn file_error_mmap() {
unsafe {
assert_matches!(
GvdbFile::from_file_mmap(&PathBuf::from("this_file_does_not_exist")),
Err(GvdbReaderError::Io(_, _))
);
}
}
fn create_minimal_file() -> GvdbFile {
let header = GvdbHeader::new(false, 0, GvdbPointer::new(0, 0));
let data = transmute_one_to_bytes(&header).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",
);
GvdbFile::from_bytes(Cow::Owned(data)).unwrap()
}
#[test]
fn test_minimal_file() {
let _ = create_minimal_file();
}
#[test]
fn broken_hash_table() {
let writer = GvdbFileWriter::new();
let mut table = GvdbHashTableBuilder::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] = data[root_ptr_end] - 25;
let file = GvdbFile::from_bytes(Cow::Owned(data)).unwrap();
let err = file.hash_table().unwrap_err();
assert_matches!(err, GvdbReaderError::DataError(_));
assert!(format!("{}", err).contains("Not enough bytes to fit hash table"));
}
#[test]
fn broken_hash_table2() {
let writer = GvdbFileWriter::new();
let mut table = GvdbHashTableBuilder::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] = data[root_ptr_end] - 23;
let file = GvdbFile::from_bytes(Cow::Owned(data)).unwrap();
let err = file.hash_table().unwrap_err();
assert_matches!(err, GvdbReaderError::DataError(_));
assert!(format!("{}", err).contains("Remaining size invalid"));
}
#[test]
fn test_dereference_offset1() {
let file = create_minimal_file();
let res = file.dereference(&GvdbPointer::new(40, 42), 2);
assert_matches!(res, Err(GvdbReaderError::DataOffset));
println!("{}", res.unwrap_err());
}
#[test]
fn test_dereference_offset2() {
let file = create_minimal_file();
let res = file.dereference(&GvdbPointer::new(10, 0), 2);
assert_matches!(res, Err(GvdbReaderError::DataOffset));
println!("{}", res.unwrap_err());
}
#[test]
fn test_dereference_offset3() {
let file = create_minimal_file();
let res = file.dereference(&GvdbPointer::new(10, 0), 2);
assert_matches!(res, Err(GvdbReaderError::DataOffset));
println!("{}", res.unwrap_err());
}
#[test]
fn test_dereference_alignment() {
let file = create_minimal_file();
let res = file.dereference(&GvdbPointer::new(1, 2), 2);
assert_matches!(res, Err(GvdbReaderError::DataAlignment));
println!("{}", res.unwrap_err());
}
#[test]
fn test_nested_dict() {
let file = GvdbFile::from_file(&TEST_FILE_2).unwrap();
let table = file.hash_table().unwrap();
let table_res = table.get_value("table");
assert_matches!(table_res, Err(GvdbReaderError::DataError(_)));
}
#[test]
fn test_nested_dict_fail() {
let file = GvdbFile::from_file(&TEST_FILE_2).unwrap();
let table = file.hash_table().unwrap();
let res = table.get_hash_table("string");
assert_matches!(res, Err(GvdbReaderError::DataError(_)));
}
}