gvdb-0.6.1/.cargo_vcs_info.json0000644000000001420000000000100117570ustar { "git": { "sha1": "2aff9e8498c171dad34766804d1d8325f1ac4af0" }, "path_in_vcs": "gvdb" }gvdb-0.6.1/Cargo.toml0000644000000036730000000000100077710ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.75" name = "gvdb" version = "0.6.1" exclude = ["test/c"] description = "Read and write GLib GVariant database files" readme = "README.md" keywords = [ "gvdb", "glib", "gresource", "compile-resources", ] categories = [ "gui", "data-structures", "encoding", ] license = "MIT" repository = "https://github.com/felinira/gvdb-rs" [package.metadata.docs.rs] all-features = true [dependencies.byteorder] version = "1.4" [dependencies.flate2] version = "1.0" optional = true [dependencies.glib] version = "0.19" optional = true [dependencies.memmap2] version = "0.9" optional = true [dependencies.quick-xml] version = "0.31" features = ["serialize"] optional = true [dependencies.safe-transmute] version = "0.11" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" optional = true [dependencies.walkdir] version = "2.3" optional = true [dependencies.zvariant] version = "4.0" features = ["gvariant"] default-features = false [dev-dependencies.flate2] version = "1.0" features = ["zlib"] [dev-dependencies.glib] version = "0.19" [dev-dependencies.lazy_static] version = "1.4" [dev-dependencies.matches] version = "0.1" [dev-dependencies.pretty_assertions] version = "1.2" [dev-dependencies.serde_json] version = "1.0" [features] default = [] glib = ["dep:glib"] gresource = [ "dep:quick-xml", "dep:serde_json", "dep:flate2", "dep:walkdir", ] mmap = ["dep:memmap2"] gvdb-0.6.1/Cargo.toml.orig000064400000000000000000000023501046102023000134410ustar 00000000000000[package] name = "gvdb" version = "0.6.1" edition = "2021" description = "Read and write GLib GVariant database files" repository = "https://github.com/felinira/gvdb-rs" license = "MIT" keywords = ["gvdb", "glib", "gresource", "compile-resources"] categories = ["gui", "data-structures", "encoding"] exclude = ["test/c"] rust-version = "1.75" [package.metadata.docs.rs] all-features = true [dependencies] safe-transmute = "0.11" byteorder = "1.4" serde = { version = "1.0", features = ["derive"] } zvariant = { version = "4.0", default-features = false, features = [ "gvariant", ] } flate2 = { version = "1.0", optional = true } glib = { version = "0.19", optional = true } quick-xml = { version = "0.31", optional = true, features = ["serialize"] } memmap2 = { version = "0.9", optional = true } serde_json = { version = "1.0", optional = true } walkdir = { version = "2.3", optional = true } [dev-dependencies] # Use zlib for binary compatibility in tests flate2 = { version = "1.0", features = ["zlib"] } glib = "0.19" lazy_static = "1.4" matches = "0.1" pretty_assertions = "1.2" serde_json = "1.0" [features] mmap = ["dep:memmap2"] gresource = ["dep:quick-xml", "dep:serde_json", "dep:flate2", "dep:walkdir"] glib = ["dep:glib"] default = [] gvdb-0.6.1/LICENSE.Icons.md000064400000000000000000000154671046102023000132450ustar 00000000000000## creative commons # CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. ### Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. __Copyright and Related Rights.__ A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. __Waiver.__ To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. __Public License Fallback.__ Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. __Limitations and Disclaimers.__ a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. gvdb-0.6.1/LICENSE.md000064400000000000000000000017771046102023000121720ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gvdb-0.6.1/README.md000064400000000000000000000065361046102023000120430ustar 00000000000000# About this crate This is an implementation of the glib GVariant database file format in Rust. It includes a GResource XML parser and the ability to create compatible GResource files. [![Crates.io](https://img.shields.io/crates/v/gvdb)](https://crates.io/crates/gvdb) ## MSRV The minimum supported rust version of this crate is 1.75. ## Breaking changes ### 0.6 This crate now uses zvariant 4.0 and glib 0.19. The MSRV has been increased accordingly. ### 0.5 Added the `mmap` feature, disabled by default. ## Example ### Create a GResource file Create a GResource file from XML with `GResourceXMLDocument` and `GResourceBuilder`. Requires the `gresource` feature to be enabled. ```rust #[cfg(feature = "gresource")] mod gresource { use std::borrow::Cow; use std::path::PathBuf; use gvdb::gresource::GResourceBuilder; use gvdb::gresource::GResourceXMLDocument; use gvdb::read::GvdbFile; const GRESOURCE_XML: &str = "test-data/gresource/test3.gresource.xml"; fn create_gresource() { let doc = GResourceXMLDocument::from_file(&PathBuf::from(GRESOURCE_XML)).unwrap(); let builder = GResourceBuilder::from_xml(doc).unwrap(); let data = builder.build().unwrap(); // To immediately read this data again, we can create a file reader from the data let root = GvdbFile::from_bytes(Cow::Owned(data)).unwrap(); } } ``` Create a simple GVDB file with `GvdbFileWriter` ```rust use gvdb::write::{GvdbFileWriter, GvdbHashTableBuilder}; fn create_gvdb_file() { let mut file_writer = GvdbFileWriter::new(); let mut table_builder = GvdbHashTableBuilder::new(); table_builder .insert_string("string", "test string") .unwrap(); let mut table_builder_2 = GvdbHashTableBuilder::new(); table_builder_2 .insert("int", 42u32) .unwrap(); table_builder .insert_table("table", table_builder_2) .unwrap(); let file_data = file_writer.write_to_vec_with_table(table_builder).unwrap(); } ``` ### Read a GVDB file The stored data at `/gvdb/rs/test/online-symbolic.svg` corresponds to the `(uuay)` GVariant type signature. ```rust use gvdb::read::GvdbFile; use std::path::PathBuf; pub fn main() { let path = PathBuf::from("test-data/test3.gresource"); let file = GvdbFile::from_file(&path).unwrap(); let table = file.hash_table().unwrap(); #[derive(zvariant::OwnedValue)] struct GResourceData { size: u32, flags: u32, content: Vec, } let svg1: GResourceData = table.get("/gvdb/rs/test/online-symbolic.svg").unwrap(); assert_eq!(svg1.size, 1390); assert_eq!(svg1.flags, 0); assert_eq!(svg1.size as usize, svg1.content.len() - 1); // Ensure the last byte is zero because of zero-padding defined in the format assert_eq!(svg1.content[svg1.content.len() - 1], 0); let svg1_str = std::str::from_utf8(&svg1.content[0..svg1.content.len() - 1]).unwrap(); println!("{}", svg1_str); } ``` ## License `gvdb` and `gvdb-macros` are available under the MIT license. See the [LICENSE.md](./LICENSE.md) file for more info. SVG icon files included in `test-data/gresource/icons/` are available under the CC0 license and redistributed from [Icon Development Kit](https://gitlab.gnome.org/Teams/Design/icon-development-kit). See the [LICENSE.Icons.md](./LICENSE.Icons.md) and file for more info. gvdb-0.6.1/src/gresource/builder.rs000064400000000000000000000626331046102023000153450ustar 00000000000000use crate::gresource::error::{GResourceBuilderError, GResourceBuilderResult}; use crate::gresource::xml::PreprocessOptions; use crate::write::{GvdbFileWriter, GvdbHashTableBuilder}; use flate2::write::ZlibEncoder; use std::borrow::Cow; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use walkdir::WalkDir; const FLAG_COMPRESSED: u32 = 1 << 0; static SKIPPED_FILE_NAMES_DEFAULT: &[&str] = &["meson.build", "gresource.xml", ".gitignore"]; static COMPRESS_EXTENSIONS_DEFAULT: &[&str] = &[".ui", ".css"]; /// A container for a GResource data object /// /// Allows to read a file from the filesystem. The file is then preprocessed and compressed. /// /// ``` /// # use std::path::PathBuf; /// use gvdb::gresource::{PreprocessOptions, GResourceFileData}; /// /// let mut key = "/my/app/id/icons/scalable/actions/send-symbolic.svg".to_string(); /// let mut filename = PathBuf::from("test-data/gresource/icons/scalable/actions/send-symbolic.svg"); /// /// let preprocess_options = PreprocessOptions::empty(); /// let file_data = /// GResourceFileData::from_file(key, &filename, true, &preprocess_options).unwrap(); /// ``` #[derive(Debug)] pub struct GResourceFileData<'a> { key: String, data: Cow<'a, [u8]>, flags: u32, /// uncompressed data is zero-terminated /// compressed data is not size: u32, } impl<'a> GResourceFileData<'a> { /// Create a new `GResourceFileData` from raw bytes /// /// The `path` parameter is used for error output, and should be set to a valid filesystem path /// if possible or `None` if not applicable. /// /// Preprocessing will be applied based on the `preprocess` parameter. /// Will compress the data if `compressed` is set. /// /// ``` /// # use std::borrow::Cow; /// use std::path::PathBuf; /// use gvdb::gresource::{GResourceFileData, PreprocessOptions}; /// /// let mut key = "/my/app/id/style.css".to_string(); /// let mut filename = PathBuf::from("path/to/style.css"); /// /// let preprocess_options = PreprocessOptions::empty(); /// let data: Vec = vec![1, 2, 3, 4]; /// let file_data = /// GResourceFileData::new(key, Cow::Owned(data), None, true, &preprocess_options).unwrap(); /// ``` pub fn new( key: String, data: Cow<'a, [u8]>, path: Option, compressed: bool, preprocess: &PreprocessOptions, ) -> GResourceBuilderResult { let mut flags = 0; let mut data = Self::preprocess(data, preprocess, path.clone())?; let size = data.len() as u32; if compressed { data = Self::compress(data, path)?; flags |= FLAG_COMPRESSED; } else { data.to_mut().push(0); } Ok(Self { key, data, flags, size, }) } /// Read the data from a file /// /// Preprocessing will be applied based on the `preprocess` parameter. /// Will compress the data if `compressed` is set. /// /// ``` /// # use std::path::PathBuf; /// use gvdb::gresource::{GResourceFileData, PreprocessOptions}; /// /// let mut key = "/my/app/id/icons/scalable/actions/send-symbolic.svg".to_string(); /// let mut filename = PathBuf::from("test-data/gresource/icons/scalable/actions/send-symbolic.svg"); /// /// let preprocess_options = PreprocessOptions::empty(); /// let file_data = /// GResourceFileData::from_file(key, &filename, true, &preprocess_options).unwrap(); /// ``` pub fn from_file( key: String, file_path: &Path, compressed: bool, preprocess: &PreprocessOptions, ) -> GResourceBuilderResult { let mut open_file = std::fs::File::open(file_path).map_err( GResourceBuilderError::from_io_with_filename(Some(file_path)), )?; let mut data = Vec::new(); open_file .read_to_end(&mut data) .map_err(GResourceBuilderError::from_io_with_filename(Some( file_path, )))?; GResourceFileData::new( key, Cow::Owned(data), Some(file_path.to_path_buf()), compressed, preprocess, ) } fn xml_stripblanks( data: Cow<'a, [u8]>, path: Option, ) -> GResourceBuilderResult> { let output = Vec::new(); let mut reader = quick_xml::Reader::from_str( std::str::from_utf8(&data) .map_err(|err| GResourceBuilderError::Utf8(err, path.clone()))?, ); reader.trim_text(true); let mut writer = quick_xml::Writer::new(std::io::Cursor::new(output)); loop { match reader .read_event() .map_err(|err| GResourceBuilderError::Xml(err, path.clone()))? { quick_xml::events::Event::Eof => break, event => writer .write_event(event) .map_err(|err| GResourceBuilderError::Xml(err, path.clone()))?, } } Ok(Cow::Owned(writer.into_inner().into_inner())) } fn json_stripblanks( data: Cow<'a, [u8]>, path: Option, ) -> GResourceBuilderResult> { let string = std::str::from_utf8(&data) .map_err(|err| GResourceBuilderError::Utf8(err, path.clone()))?; let json: serde_json::Value = serde_json::from_str(string) .map_err(|err| GResourceBuilderError::Json(err, path.clone()))?; let mut output = json.to_string().as_bytes().to_vec(); output.push(b'\n'); Ok(Cow::Owned(output)) } fn preprocess( mut data: Cow<'a, [u8]>, options: &PreprocessOptions, path: Option, ) -> GResourceBuilderResult> { if options.xml_stripblanks { data = Self::xml_stripblanks(data, path.clone())?; } if options.json_stripblanks { data = Self::json_stripblanks(data, path)?; } if options.to_pixdata { return Err(GResourceBuilderError::Unimplemented( "to-pixdata is deprecated since gdk-pixbuf 2.32 and not supported by gvdb-rs" .to_string(), )); } Ok(data) } fn compress( data: Cow<'a, [u8]>, path: Option, ) -> GResourceBuilderResult> { let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::best()); encoder .write_all(&data) .map_err(GResourceBuilderError::from_io_with_filename(path.clone()))?; Ok(Cow::Owned(encoder.finish().map_err( GResourceBuilderError::from_io_with_filename(path), )?)) } /// Return the `key` of this `FileData` pub fn key(&self) -> &str { &self.key } } /// GResource data value /// /// This is the format in which all GResource files are stored in the GVDB file. /// /// The size is the *uncompressed* size and can be used for verification purposes. /// The flags only indicate whether a file is compressed or not. (Compressed = 1) #[derive(zvariant::Type, zvariant::Value, zvariant::OwnedValue)] pub struct GResourceData { size: u32, flags: u32, data: Vec, } /// Create a GResource binary file /// /// # Example /// /// Create a GResource XML file with [`GResourceXMLDocument`][crate::gresource::GResourceXMLDocument] and /// [`GResourceBuilder`](crate::gresource::GResourceBuilder) /// ``` /// use std::borrow::Cow; /// use std::path::PathBuf; /// use gvdb::gresource::GResourceBuilder; /// use gvdb::gresource::GResourceXMLDocument; /// use gvdb::read::GvdbFile; /// /// const GRESOURCE_XML: &str = "test/data/gresource/test3.gresource.xml"; /// /// fn create_gresource() { /// let doc = GResourceXMLDocument::from_file(&PathBuf::from(GRESOURCE_XML)).unwrap(); /// let builder = GResourceBuilder::from_xml(doc).unwrap(); /// let data = builder.build().unwrap(); /// let root = GvdbFile::from_bytes(Cow::Owned(data)).unwrap(); /// } /// ``` #[derive(Debug)] pub struct GResourceBuilder<'a> { files: Vec>, } impl<'a> GResourceBuilder<'a> { /// Create this builder from a GResource XML file pub fn from_xml(xml: super::xml::GResourceXMLDocument) -> GResourceBuilderResult { let mut files = Vec::new(); for gresource in &xml.gresources { for file in &gresource.files { let mut key = gresource.prefix.clone(); if !key.ends_with('/') { key.push('/'); } if let Some(alias) = &file.alias { key.push_str(alias); } else { key.push_str(&file.filename); } let mut filename = xml.dir.clone(); filename.push(PathBuf::from(&file.filename)); let file_data = GResourceFileData::from_file( key, &filename, file.compressed, &file.preprocess, )?; files.push(file_data); } } Ok(Self { files }) } /// Scan a directory and create a GResource file with all the contents of the directory. /// /// This will ignore any files that end with gresource.xml and meson.build, as /// those are most likely not needed inside the GResource. /// /// This is equivalent to the following XML: /// /// ```xml /// /// /// /// /// /// ``` /// /// ## `prefix` /// /// The prefix for the gresource section /// /// ## `directory` /// /// The root directory of the included files /// /// ## `strip_blanks` /// /// Acts as if every xml file uses the option `xml-stripblanks` in the GResource XML and every /// JSON file uses `json-stripblanks`. /// /// JSON files are all files with the extension '.json'. /// XML files are all files with the extensions '.xml', '.ui', '.svg' /// /// ## `compress` /// /// Compresses all files that end with the preconfigured patterns. /// Compressed files are currently: ".ui", ".css" pub fn from_directory( prefix: &str, directory: &Path, strip_blanks: bool, compress: bool, ) -> GResourceBuilderResult { let compress_extensions = if compress { COMPRESS_EXTENSIONS_DEFAULT } else { &[] }; Self::from_directory_with_extensions( prefix, directory, strip_blanks, compress_extensions, SKIPPED_FILE_NAMES_DEFAULT, ) } /// Like `from_directory` but allows you to specify the extensions directories yourself /// /// ## `compress_extensions` /// /// All files that end with these strings will get compressed /// /// ## `skipped_file_names` /// /// Skip all files that end with this string pub fn from_directory_with_extensions( prefix: &str, directory: &Path, strip_blanks: bool, compress_extensions: &[&str], skipped_file_names: &[&str], ) -> GResourceBuilderResult { let mut prefix = prefix.to_string(); if !prefix.ends_with('/') { prefix.push('/'); } let mut files = Vec::new(); 'outer: for res in WalkDir::new(directory).into_iter() { let entry = match res { Ok(entry) => entry, Err(err) => { let path = err.path().map(|p| p.to_path_buf()); return if err.io_error().is_some() { Err(GResourceBuilderError::Io( err.into_io_error().unwrap(), path, )) } else { Err(GResourceBuilderError::Generic(err.to_string())) }; } }; if entry.path().is_file() { let Some(filename) = entry.file_name().to_str() else { return Err(GResourceBuilderError::Generic(format!( "Filename '{}' contains invalid UTF-8 characters", entry.file_name().to_string_lossy() ))); }; for name in skipped_file_names { if filename.ends_with(name) { continue 'outer; } } let mut compress_this = false; for name in compress_extensions { if filename.ends_with(name) { compress_this = true; break; } } let file_abs_path = entry.path(); let Ok(file_path_relative) = file_abs_path.strip_prefix(directory) else { return Err(GResourceBuilderError::Generic( "Strip prefix error".to_string(), )); }; let Some(file_path_str_relative) = file_path_relative.to_str() else { return Err(GResourceBuilderError::Generic(format!( "Filename '{}' contains invalid UTF-8 characters", file_path_relative.display() ))); }; let options = if strip_blanks && file_path_str_relative.ends_with(".json") { PreprocessOptions::json_stripblanks() } else if strip_blanks && file_path_str_relative.ends_with(".xml") || file_path_str_relative.ends_with(".ui") || file_path_str_relative.ends_with(".svg") { PreprocessOptions::xml_stripblanks() } else { PreprocessOptions::empty() }; let key = format!("{}{}", prefix, file_path_str_relative); let file_data = GResourceFileData::from_file(key, file_abs_path, compress_this, &options)?; files.push(file_data); } } Ok(Self { files }) } /// Create a new Builder from a `Vec`. /// /// This is the most flexible way to create a GResource file, but also the most hands-on. pub fn from_file_data(files: Vec>) -> Self { Self { files } } /// Build the binary GResource data pub fn build(self) -> GResourceBuilderResult> { let builder = GvdbFileWriter::new(); let mut table_builder = GvdbHashTableBuilder::new(); for file_data in self.files.into_iter() { let data = GResourceData { size: file_data.size, flags: file_data.flags, data: file_data.data.to_vec(), }; table_builder.insert_value(file_data.key(), zvariant::Value::from(data))?; } Ok(builder.write_to_vec_with_table(table_builder)?) } } #[cfg(test)] mod test { use super::*; use crate::gresource::xml::GResourceXMLDocument; use crate::read::GvdbFile; use crate::test::{assert_is_file_3, byte_compare_file_3, GRESOURCE_DIR, GRESOURCE_XML}; use matches::assert_matches; use std::ffi::OsStr; use zvariant::Type; #[test] fn file_data() { let doc = GResourceXMLDocument::from_file(&GRESOURCE_XML).unwrap(); let builder = GResourceBuilder::from_xml(doc).unwrap(); for file in builder.files { assert!(file.key().starts_with("/gvdb/rs/test")); assert!( vec![ "/gvdb/rs/test/online-symbolic.svg", "/gvdb/rs/test/icons/scalable/actions/send-symbolic.svg", "/gvdb/rs/test/json/test.json", "/gvdb/rs/test/test.css", ] .contains(&&*file.key()), "Unknown file with key: {}", file.key() ) } } #[test] fn from_dir_file_data() { for preprocess in [true, false] { let builder = GResourceBuilder::from_directory( "/gvdb/rs/test", &GRESOURCE_DIR, preprocess, preprocess, ) .unwrap(); for file in builder.files { assert!(file.key().starts_with("/gvdb/rs/test")); assert!( vec![ "/gvdb/rs/test/icons/scalable/actions/online-symbolic.svg", "/gvdb/rs/test/icons/scalable/actions/send-symbolic.svg", "/gvdb/rs/test/json/test.json", "/gvdb/rs/test/test.css", "/gvdb/rs/test/test3.gresource.xml", ] .contains(&&*file.key()), "Unknown file with key: {}", file.key() ); } } } #[test] fn from_dir_invalid() { let res = GResourceBuilder::from_directory( "/gvdb/rs/test", &PathBuf::from("INVALID_DIR"), false, false, ); assert!(res.is_err()); let err = res.unwrap_err(); assert_matches!(err, GResourceBuilderError::Io(..)); } #[test] fn test_file_3() { let doc = GResourceXMLDocument::from_file(&GRESOURCE_XML).unwrap(); let builder = GResourceBuilder::from_xml(doc).unwrap(); let data = builder.build().unwrap(); let root = GvdbFile::from_bytes(Cow::Owned(data)).unwrap(); assert_is_file_3(&root); byte_compare_file_3(&root); } #[test] fn test_file_from_dir() { let builder = GResourceBuilder::from_directory("/gvdb/rs/test", &GRESOURCE_DIR, true, true).unwrap(); let data = builder.build().unwrap(); let root = GvdbFile::from_bytes(Cow::Owned(data)).unwrap(); let table = root.hash_table().unwrap(); let mut names = table.get_names().unwrap(); names.sort(); let reference_names = vec![ "/", "/gvdb/", "/gvdb/rs/", "/gvdb/rs/test/", "/gvdb/rs/test/icons/", "/gvdb/rs/test/icons/scalable/", "/gvdb/rs/test/icons/scalable/actions/", "/gvdb/rs/test/icons/scalable/actions/online-symbolic.svg", "/gvdb/rs/test/icons/scalable/actions/send-symbolic.svg", "/gvdb/rs/test/json/", "/gvdb/rs/test/json/test.json", "/gvdb/rs/test/test.css", ]; assert_eq!(names, reference_names); let svg2 = zvariant::Structure::try_from( table .get_value("/gvdb/rs/test/icons/scalable/actions/send-symbolic.svg") .unwrap(), ) .unwrap() .into_fields(); let svg2_size = u32::try_from(&svg2[0]).unwrap(); let svg2_flags = u32::try_from(&svg2[1]).unwrap(); let svg2_data = >::try_from(svg2[2].try_clone().unwrap()).unwrap(); assert_eq!(svg2_size, 339); assert_eq!(svg2_flags, 0); // Check for null byte assert_eq!(svg2_data[svg2_data.len() - 1], 0); assert_eq!(svg2_size as usize, svg2_data.len() - 1); } #[test] #[cfg(unix)] fn test_from_dir_invalid() { use std::os::unix::ffi::OsStrExt; let invalid_utf8 = OsStr::from_bytes(&[0xC3, 0x28]); let mut dir: PathBuf = ["test-data", "temp2"].iter().collect(); dir.push(invalid_utf8); std::fs::create_dir_all(&dir).unwrap(); std::fs::File::create(dir.join("test.xml")).unwrap(); let res = GResourceBuilder::from_directory("test", &dir.parent().unwrap(), false, false); let _ = std::fs::remove_file(dir.join("test.xml")); let _ = std::fs::remove_dir(&dir); std::fs::remove_dir(dir.parent().unwrap()).unwrap(); let err = res.unwrap_err(); println!("{}", err); assert_matches!(err, GResourceBuilderError::Generic(_)); assert!(format!("{}", err).contains("UTF-8")); } #[test] fn test_invalid_utf8_json() { use std::os::unix::ffi::OsStrExt; let invalid_utf8 = OsStr::from_bytes(&[0xC3, 0x28]); let dir: PathBuf = ["test-data", "temp3"].iter().collect(); std::fs::create_dir_all(&dir).unwrap(); let mut file = std::fs::File::create(dir.join("test.json")).unwrap(); let _ = file.write(invalid_utf8.as_bytes()); let res = GResourceBuilder::from_directory("test", &dir, true, true); let _ = std::fs::remove_file(dir.join("test.json")); let _ = std::fs::remove_dir(&dir); let err = res.unwrap_err(); println!("{}", err); assert_matches!(err, GResourceBuilderError::Utf8(..)); assert!(format!("{}", err).contains("UTF-8")); } #[test] fn test_from_file_data() { let path = GRESOURCE_DIR.join("json").join("test.json"); let file_data = GResourceFileData::from_file( "test.json".to_string(), &path, false, &PreprocessOptions::empty(), ) .unwrap(); println!("{:?}", file_data); let builder = GResourceBuilder::from_file_data(vec![file_data]); println!("{:?}", builder); let _ = builder.build().unwrap(); } #[test] fn to_pixdata() { let path = GRESOURCE_DIR.join("json").join("test.json"); let mut options = PreprocessOptions::empty(); options.to_pixdata = true; let err = GResourceFileData::from_file("test.json".to_string(), &path, false, &options) .unwrap_err(); assert_matches!(err, GResourceBuilderError::Unimplemented(_)); assert!(format!("{}", err).contains("to-pixdata is deprecated")); } #[test] fn xml_stripblanks() { for path in [Some(PathBuf::from("test")), None] { let xml = "), /// Generic I/O error occurred when handling XML file Io(std::io::Error, Option), /// A file needs to be interpreted as UTF-8 (for stripping whitespace etc.) but it is invalid Utf8(std::str::Utf8Error, Option), } impl GResourceXMLError { pub(crate) fn from_io_with_filename( filename: &Path, ) -> impl FnOnce(std::io::Error) -> GResourceXMLError { let path = filename.to_path_buf(); move |err| GResourceXMLError::Io(err, Some(path)) } } impl std::error::Error for GResourceXMLError {} impl Display for GResourceXMLError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { GResourceXMLError::Serde(err, path) => { if let Some(path) = path { write!(f, "Error parsing XML file '{}': {}", path.display(), err) } else { write!(f, "Error parsing XML file: {}", err) } } GResourceXMLError::Io(err, path) => { if let Some(path) = path { write!(f, "I/O error for file '{}': {}", path.display(), err) } else { write!(f, "I/O error: {}", err) } } GResourceXMLError::Utf8(err, path) => { if let Some(path) = path { write!( f, "Error converting file '{}' to UTF-8: {}", path.display(), err ) } else { write!(f, "Error converting data to UTF-8: {}", err) } } } } } impl Debug for GResourceXMLError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } /// Result type for GResourceXMLError pub type GResourceXMLResult = Result; /// Error type for creating a GResource XML file pub enum GResourceBuilderError { /// An internal error occurred during creation of the GVDB file Gvdb(GvdbWriterError), /// I/O error Io(std::io::Error, Option), /// This error can occur when using xml-stripblanks and the provided XML file is invalid Xml(quick_xml::Error, Option), /// A file needs to be interpreted as UTF-8 (for stripping whitespace etc.) but it is invalid Utf8(std::str::Utf8Error, Option), /// This error can occur when using json-stripblanks and the provided JSON file is invalid Json(serde_json::Error, Option), /// This feature is not implemented in gvdb-rs Unimplemented(String), /// A generic error with a text description Generic(String), } impl GResourceBuilderError { pub(crate) fn from_io_with_filename

( filename: Option

, ) -> impl FnOnce(std::io::Error) -> GResourceBuilderError where P: Into, { let path = filename.map(|p| p.into()); move |err| GResourceBuilderError::Io(err, path) } } impl std::error::Error for GResourceBuilderError {} impl From for GResourceBuilderError { fn from(err: GvdbWriterError) -> Self { Self::Gvdb(err) } } impl Display for GResourceBuilderError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { GResourceBuilderError::Xml(err, path) => { if let Some(path) = path { write!( f, "Error processing XML data for file '{}': {}", path.display(), err ) } else { write!(f, "Error processing XML data: {}", err) } } GResourceBuilderError::Io(err, path) => { if let Some(path) = path { write!(f, "I/O error for file '{}': {}", path.display(), err) } else { write!(f, "I/O error: {}", err) } } GResourceBuilderError::Json(err, path) => { if let Some(path) = path { write!( f, "Error parsing JSON from file: '{}': {}", path.display(), err ) } else { write!(f, "Error reading/writing JSON data: {}", err) } } GResourceBuilderError::Utf8(err, path) => { if let Some(path) = path { write!( f, "Error converting file '{}' to UTF-8: {}", path.display(), err ) } else { write!(f, "Error converting data to UTF-8: {}", err) } } GResourceBuilderError::Unimplemented(err) => { write!(f, "{}", err) } GResourceBuilderError::Gvdb(err) => { write!(f, "Error while creating GVDB file: {:?}", err) } GResourceBuilderError::Generic(err) => { write!(f, "Error while creating GResource file: {}", err) } } } } impl Debug for GResourceBuilderError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } /// Result type for [`GResourceBuilderError`] pub type GResourceBuilderResult = Result; #[cfg(test)] mod test { use crate::gresource::{GResourceBuilderError, GResourceXMLError}; use crate::write::GvdbWriterError; use std::path::PathBuf; #[test] fn from() { let io_res = std::fs::File::open("test/invalid_file_name"); let err = GResourceXMLError::Io(io_res.unwrap_err(), None); assert!(format!("{}", err).contains("I/O")); let io_res = std::fs::File::open("test/invalid_file_name"); let err = GResourceBuilderError::Io(io_res.unwrap_err(), None); assert!(format!("{}", err).contains("I/O")); let io_res = std::fs::File::open("test/invalid_file_name"); let err = GResourceBuilderError::from_io_with_filename(Some("test"))(io_res.unwrap_err()); assert!(format!("{}", err).contains("test")); let writer_error = GvdbWriterError::Consistency("test".to_string()); let err = GResourceBuilderError::from(writer_error); assert!(format!("{}", err).contains("test")); let err = GResourceBuilderError::Xml( quick_xml::Error::TextNotFound, Some(PathBuf::from("test_file")), ); assert!(format!("{}", err).contains("test_file")); let err = GResourceBuilderError::Xml(quick_xml::Error::TextNotFound, None); assert!(format!("{}", err).contains("XML")); } } gvdb-0.6.1/src/gresource/xml.rs000064400000000000000000000250021046102023000145040ustar 00000000000000use crate::gresource::error::{GResourceXMLError, GResourceXMLResult}; use serde::de::Error; use serde::Deserialize; use std::borrow::Cow; use std::io::Read; use std::path::{Path, PathBuf}; /// A GResource XML document #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] #[non_exhaustive] pub struct GResourceXMLDocument { /// The list of GResource sections #[serde(rename = "gresource")] pub gresources: Vec, /// The directory of the XML file #[serde(default)] pub dir: PathBuf, } /// A GResource section inside a GResource XML document #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] #[non_exhaustive] pub struct GResource { /// The files for this GResource section #[serde(rename = "file", default)] pub files: Vec, /// An optional prefix to prepend to the containing file keys #[serde(default, rename = "@prefix")] pub prefix: String, } /// A file within a GResource section #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] #[non_exhaustive] pub struct File { /// The on-disk file name of the file #[serde(rename = "$value")] pub filename: String, /// The alias for this file if it should be named differently inside the GResource file #[serde(rename = "@alias")] pub alias: Option, /// Whether the file should be compressed using zlib #[serde(deserialize_with = "parse_bool_value", default, rename = "@compressed")] pub compressed: bool, /// A list of preprocessing options #[serde( deserialize_with = "parse_preprocess_options", default, rename = "@preprocess" )] pub preprocess: PreprocessOptions, } /// Preprocessing options for files that will be put in a GResource #[derive(Debug, Default, PartialEq, Eq)] #[non_exhaustive] pub struct PreprocessOptions { /// Strip whitespace from XML file pub xml_stripblanks: bool, /// Unimplemented pub to_pixdata: bool, /// Strip whitespace from JSON file pub json_stripblanks: bool, } impl PreprocessOptions { /// An empty set of preprocessing options /// /// No preprocessing will be done pub fn empty() -> Self { Self { xml_stripblanks: false, to_pixdata: false, json_stripblanks: false, } } /// XML strip blanks preprocessing will be applied pub fn xml_stripblanks() -> Self { Self { xml_stripblanks: true, to_pixdata: false, json_stripblanks: false, } } /// JSON strip blanks preprocessing will be applied pub fn json_stripblanks() -> Self { Self { xml_stripblanks: false, to_pixdata: false, json_stripblanks: true, } } } fn parse_bool_value<'de, D>(d: D) -> Result where D: serde::Deserializer<'de>, { match &*String::deserialize(d)? { "true" | "t" | "yes" | "y" | "1" => Ok(true), "false" | "f" | "no" | "n" | "0" => Ok(false), other => Err(D::Error::custom(format!("got '{}', but expected any of 'true', 't', 'yes', 'y', '1' / 'false', 'f', 'no', 'n', '0'", other))), } } fn parse_preprocess_options<'de, D>(d: D) -> Result where D: serde::Deserializer<'de>, { let mut this = PreprocessOptions::default(); for item in String::deserialize(d)?.split(',') { match item { "json-stripblanks" => this.json_stripblanks = true, "xml-stripblanks" => this.xml_stripblanks = true, "to-pixdata" => this.to_pixdata = true, other => { return Err(D::Error::custom(format!( "got '{}' but expected any of 'json-stripblanks', 'xml-stripblanks'", other ))) } } } Ok(this) } impl GResourceXMLDocument { /// Load a GResource XML file from disk using `path` pub fn from_file(path: &Path) -> GResourceXMLResult { let mut file = std::fs::File::open(path).map_err(GResourceXMLError::from_io_with_filename(path))?; let mut data = Vec::with_capacity( file.metadata() .map_err(GResourceXMLError::from_io_with_filename(path))? .len() as usize, ); file.read_to_end(&mut data) .map_err(GResourceXMLError::from_io_with_filename(path))?; let dir = path.parent().unwrap(); Self::from_bytes_with_filename(dir, Some(path.to_path_buf()), Cow::Owned(data)) } /// Load a GResource XML file from the provided `Cow<[u8]>` bytes. A filename is provided for /// error context fn from_bytes_with_filename( dir: &Path, filename: Option, data: Cow<'_, [u8]>, ) -> GResourceXMLResult { let mut this: Self = quick_xml::de::from_str( std::str::from_utf8(&data) .map_err(|err| GResourceXMLError::Utf8(err, filename.clone()))?, ) .map_err(|err| GResourceXMLError::Serde(err, filename))?; this.dir = dir.to_path_buf(); Ok(this) } /// Load a GResource XML file from the provided `Cow<[u8]>` bytes pub fn from_bytes(dir: &Path, data: Cow<'_, [u8]>) -> GResourceXMLResult { Self::from_bytes_with_filename(dir, None, data) } /// Load a GResource XML file from a `&str` or `String` pub fn from_string(dir: &Path, str: impl ToString) -> GResourceXMLResult { Self::from_bytes(dir, Cow::Borrowed(str.to_string().as_bytes())) } } #[cfg(test)] mod test { use super::super::error::GResourceXMLError; use super::*; use matches::assert_matches; use pretty_assertions::assert_eq; #[test] fn deserialize_simple() { let test_path = PathBuf::from("/TEST"); let data = r#"test"#; let doc = GResourceXMLDocument::from_bytes(&test_path, Cow::Borrowed(data.as_bytes())).unwrap(); println!("{:?}", doc); assert_eq!(doc, doc); assert_eq!(doc.gresources.len(), 1); assert_eq!(doc.gresources[0].files.len(), 1); assert_eq!(doc.gresources[0].files[0].filename, "test"); assert_eq!(doc.gresources[0].files[0].preprocess.xml_stripblanks, true); assert_eq!( doc.gresources[0].files[0].preprocess.json_stripblanks, false ); assert_eq!(doc.gresources[0].files[0].preprocess.to_pixdata, false); assert_eq!(doc.gresources[0].files[0].compressed, false); } #[test] fn deserialize_complex() { let test_path = PathBuf::from("/TEST"); let data = r#"test.json"#; let doc = GResourceXMLDocument::from_bytes(&test_path, Cow::Borrowed(data.as_bytes())).unwrap(); assert_eq!(doc.gresources.len(), 1); assert_eq!(doc.gresources[0].files.len(), 1); assert_eq!(doc.gresources[0].files[0].filename, "test.json"); assert_eq!(doc.gresources[0].files[0].compressed, true); assert_eq!(doc.gresources[0].files[0].preprocess.json_stripblanks, true); assert_eq!(doc.gresources[0].files[0].preprocess.to_pixdata, true); assert_eq!(doc.gresources[0].files[0].preprocess.xml_stripblanks, false); assert_eq!(doc.gresources[0].prefix, "/bla/blub") } #[test] fn deserialize_fail() { let test_path = PathBuf::from("/TEST"); let res = GResourceXMLDocument::from_string(&test_path, r#""#); assert!(format!("{:?}", res).contains("parsing XML")); assert_matches!( res, Err(GResourceXMLError::Serde(quick_xml::DeError::Custom(field), _)) if field == "missing field `gresource`" ); let string = r#""#.to_string(); let res = GResourceXMLDocument::from_bytes_with_filename( &test_path, Some(PathBuf::from("test_filename")), Cow::Borrowed(string.as_bytes()), ); assert!(format!("{:?}", res).contains("test_filename")); assert_matches!( res, Err(GResourceXMLError::Serde(quick_xml::de::DeError::Custom(field), _)) if field == "missing field `$value`" ); assert_matches!( GResourceXMLDocument::from_string(&test_path, r#"filename"#), Err(GResourceXMLError::Serde(quick_xml::de::DeError::Custom(field), _)) if field.starts_with("got 'nobool', but expected any of") ); assert_matches!( GResourceXMLDocument::from_string(&test_path, r#""#), Err(GResourceXMLError::Serde(quick_xml::de::DeError::Custom(field), _))if field.starts_with("unknown field `wrong`, expected `gresource`") ); assert_matches!( GResourceXMLDocument::from_string(&test_path, r#"filename"#), Err(GResourceXMLError::Serde(quick_xml::de::DeError::Custom(field), _)) if field.starts_with("unknown field `wrong`, expected `file` or `@prefix`") ); assert_matches!( GResourceXMLDocument::from_string(&test_path, r#"filename"#), Err(GResourceXMLError::Serde(quick_xml::de::DeError::Custom(field), _)) if field.starts_with("unknown field `@wrong`, expected one of") ); assert_matches!( GResourceXMLDocument::from_string(&test_path, r#"filename"#), Err(GResourceXMLError::Serde(quick_xml::de::DeError::Custom(field), _)) if field.starts_with("got 'fail' but expected any of") ); let res = GResourceXMLDocument::from_bytes(&test_path, Cow::Borrowed(&[0x80, 0x81])).unwrap_err(); println!("{}", res); assert_matches!(res, GResourceXMLError::Utf8(..)); } #[test] fn io_error() { let test_path = PathBuf::from("invalid_file_name.xml"); let res = GResourceXMLDocument::from_file(&test_path); assert_matches!(res, Err(GResourceXMLError::Io(_, _))); assert!(format!("{:?}", res).contains("invalid_file_name.xml")); } } gvdb-0.6.1/src/gresource.rs000064400000000000000000000004061046102023000137050ustar 00000000000000mod builder; mod error; mod xml; pub use self::xml::{GResourceXMLDocument, PreprocessOptions}; pub use builder::{GResourceBuilder, GResourceFileData}; pub use error::{ GResourceBuilderError, GResourceBuilderResult, GResourceXMLError, GResourceXMLResult, }; gvdb-0.6.1/src/lib.rs000064400000000000000000000066211046102023000124620ustar 00000000000000//! # Read and write GVDB files //! //! This crate allows you to read and write GVDB (GLib GVariant database) files. //! It can also parse GResource XML files and create the corresponding GResource binary //! //! ## Examples //! //! Load a GResource file from disk with [`GvdbFile`](crate::read::GvdbFile) //! //! ``` //! use std::path::PathBuf; //! use gvdb::read::GvdbFile; //! //! pub fn read_gresource_file() { //! let path = PathBuf::from("test-data/test3.gresource"); //! let file = GvdbFile::from_file(&path).unwrap(); //! let table = file.hash_table().unwrap(); //! //! #[derive(serde::Deserialize, zvariant::Type)] //! struct SvgData { //! size: u32, //! flags: u32, //! content: Vec //! } //! //! let value = table //! .get_value("/gvdb/rs/test/online-symbolic.svg") //! .unwrap(); //! let structure = zvariant::Structure::try_from(value).unwrap(); //! let svg = structure.fields(); //! let svg1_size = u32::try_from(&svg[0]).unwrap(); //! let svg1_flags = u32::try_from(&svg[1]).unwrap(); //! let svg1_content = >::try_from(svg[2].try_clone().unwrap()).unwrap(); //! let svg1_str = std::str::from_utf8(&svg1_content[0..svg1_content.len() - 1]).unwrap(); //! //! println!("{}", svg1_str); //! } //! ``` //! //! Create a simple GVDB file with [`GvdbFileWriter`](crate::write::GvdbFileWriter) //! //! ``` //! use gvdb::write::{GvdbFileWriter, GvdbHashTableBuilder}; //! //! fn create_gvdb_file() { //! let mut file_writer = GvdbFileWriter::new(); //! let mut table_builder = GvdbHashTableBuilder::new(); //! table_builder //! .insert_string("string", "test string") //! .unwrap(); //! //! let mut table_builder_2 = GvdbHashTableBuilder::new(); //! table_builder_2 //! .insert("int", 42u32) //! .unwrap(); //! //! table_builder //! .insert_table("table", table_builder_2) //! .unwrap(); //! let file_data = file_writer.write_to_vec_with_table(table_builder).unwrap(); //! } //! ``` //! //! ## Features //! //! By default, no features are enabled. //! //! ### `mmap` //! //! Use the memmap2 crate to read memory-mapped GVDB files. //! //! ### `glib` //! //! By default this crate uses the [glib](https://crates.io/crates/zvariant) crate to allow reading //! and writing `GVariant` data to the gvdb files. By enabling this feature you can pass GVariants //! directly from the glib crate as well. //! //! ### `gresource` //! //! To be able to compile GResource files, the `gresource` feature must be enabled. //! //! ## Macros //! //! The [gvdb-macros](https://crates.io/crates/gvdb-macros) crate provides useful macros for //! GResource file creation. #![warn(missing_docs)] #![doc = include_str!("../README.md")] extern crate core; /// Read GResource XML files and compile a GResource file /// /// Use [`GResourceXMLDoc`](crate::gresource::GResourceXMLDocument) for XML file reading and /// [`GResourceBuilder`](crate::gresource::GResourceBuilder) to create the GResource binary /// file #[cfg(feature = "gresource")] pub mod gresource; /// Read GVDB files from a file or from a byte slice /// /// See the documentation of [`GvdbFile`](crate::read::GvdbFile) to get started pub mod read; /// Create GVDB files /// /// See the documentation of [`GvdbFileWriter`](crate::write::GvdbFileWriter) to get started pub mod write; #[cfg(test)] pub(crate) mod test; mod util; gvdb-0.6.1/src/read/error.rs000064400000000000000000000163121046102023000137560ustar 00000000000000use std::error::Error; use std::fmt::{Display, Formatter}; use std::num::TryFromIntError; use std::path::{Path, PathBuf}; use std::string::FromUtf8Error; /// An error that can occur during GVDB file reading #[derive(Debug)] pub enum GvdbReaderError { /// Error converting a string to UTF-8 Utf8(FromUtf8Error), /// Generic I/O error. Path contains an optional filename if applicable Io(std::io::Error, Option), /// An error occured when deserializing variant data with zvariant ZVariant(zvariant::Error), /// Tried to access an invalid data offset DataOffset, /// Tried to read unaligned data DataAlignment, /// Unexpected data InvalidData, /// Like InvalidData but with context information in the provided string DataError(String), /// The item with the specified key does not exist in the hash table KeyError(String), } impl GvdbReaderError { pub(crate) fn from_io_with_filename( filename: &Path, ) -> impl FnOnce(std::io::Error) -> GvdbReaderError { let path = filename.to_path_buf(); move |err| GvdbReaderError::Io(err, Some(path)) } } impl Error for GvdbReaderError {} impl From for GvdbReaderError { fn from(err: FromUtf8Error) -> Self { Self::Utf8(err) } } impl From for GvdbReaderError { fn from(err: zvariant::Error) -> Self { Self::ZVariant(err) } } impl From for GvdbReaderError { fn from(_err: TryFromIntError) -> Self { Self::DataOffset } } impl From> for GvdbReaderError { fn from(err: safe_transmute::Error) -> Self { match err { safe_transmute::Error::Guard(guard_err) => { if guard_err.actual > guard_err.required { Self::DataError(format!( "Found {} unexpected trailing bytes at the end while reading data", guard_err.actual - guard_err.required )) } else { Self::DataError(format!( "Missing {} bytes to read data", guard_err.required - guard_err.actual )) } } safe_transmute::Error::Unaligned(..) => { Self::DataError("Unaligned data read".to_string()) } _ => Self::InvalidData, } } } impl Display for GvdbReaderError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { GvdbReaderError::Utf8(err) => write!(f, "Error converting string to UTF-8: {}", err), GvdbReaderError::Io(err, path) => { if let Some(path) = path { write!( f, "I/O error while reading file '{}': {}", path.display(), err ) } else { write!(f, "I/O error: {}", err) } } GvdbReaderError::ZVariant(err) => write!(f, "Error parsing ZVariant data: {}", err), GvdbReaderError::DataOffset => { write!(f, "Tried to access an invalid data offset. Most likely reason is a corrupted GVDB file") } GvdbReaderError::DataAlignment => { write!( f, "Tried to read unaligned data. Most likely reason is a corrupted GVDB file" ) } GvdbReaderError::InvalidData => { write!( f, "Unexpected data. Most likely reason is a corrupted GVDB file" ) } GvdbReaderError::DataError(msg) => { write!( f, "A data inconsistency error occured while reading gvdb file: {}", msg ) } GvdbReaderError::KeyError(key) => { write!(f, "The item with the key '{}' does not exist", key) } } } } /// The Result type for [`GvdbReaderError`] pub type GvdbReaderResult = Result; #[cfg(test)] mod test { use crate::read::{GvdbHeader, GvdbPointer, GvdbReaderError}; use matches::assert_matches; use safe_transmute::{transmute_one_pedantic, transmute_one_to_bytes, transmute_vec}; use std::num::TryFromIntError; #[test] fn derives() { let err = GvdbReaderError::InvalidData; assert!(format!("{:?}", err).contains("InvalidData")); assert!(format!("{}", err).contains("Unexpected data")); } #[test] fn from() { let io_res = std::fs::File::open("test/invalid_file_name"); let err = GvdbReaderError::Io(io_res.unwrap_err(), None); assert!(format!("{}", err).contains("I/O")); let utf8_err = String::from_utf8([0xC3, 0x28].to_vec()).unwrap_err(); let err = GvdbReaderError::from(utf8_err); assert!(format!("{}", err).contains("UTF-8")); let res: Result = u32::MAX.try_into(); let err = GvdbReaderError::from(res.unwrap_err()); assert_matches!(err, GvdbReaderError::DataOffset); assert!(format!("{}", err).contains("data offset")); let err = GvdbReaderError::DataError("my data error".to_string()); assert!(format!("{}", err).contains("my data error")); let err = GvdbReaderError::KeyError("test".to_string()); assert!(format!("{}", err).contains("test")); let err = GvdbReaderError::from(zvariant::Error::Message("test".to_string())); assert!(format!("{}", err).contains("test")); let to_transmute = GvdbHeader::new(false, 0, GvdbPointer::NULL); let mut bytes = transmute_one_to_bytes(&to_transmute).to_vec(); bytes.extend_from_slice(b"fail"); let res = transmute_one_pedantic::(&bytes); let err = GvdbReaderError::from(res.unwrap_err()); assert_matches!(err, GvdbReaderError::DataError(_)); assert!(format!("{}", err).contains("unexpected trailing bytes")); let to_transmute = GvdbHeader::new(false, 0, GvdbPointer::NULL); let mut bytes = transmute_one_to_bytes(&to_transmute).to_vec(); bytes.remove(bytes.len() - 1); let res = transmute_one_pedantic::(&bytes); let err = GvdbReaderError::from(res.unwrap_err()); assert_matches!(err, GvdbReaderError::DataError(_)); assert!(format!("{}", err).contains("Missing 1 bytes")); let to_transmute = GvdbHeader::new(false, 0, GvdbPointer::NULL); let mut bytes = b"unalign".to_vec(); bytes.extend_from_slice(transmute_one_to_bytes(&to_transmute)); let res = transmute_one_pedantic::(&bytes[7..]); let err = GvdbReaderError::from(res.unwrap_err()); assert_matches!(err, GvdbReaderError::DataError(_)); assert!(format!("{}", err).contains("Unaligned")); let bytes = vec![0u8; 5]; let res = transmute_vec::(bytes); let err = GvdbReaderError::from(res.unwrap_err()); assert_matches!(err, GvdbReaderError::InvalidData); } } gvdb-0.6.1/src/read/file.rs000064400000000000000000000452671046102023000135570ustar 00000000000000use 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 safe_transmute::transmute_one_pedantic; use serde::Deserialize; use std::borrow::Cow; use std::fs::File; use std::io::Read; use std::mem::size_of; use std::path::Path; use zvariant::{gvariant, Type}; #[derive(Debug)] pub(crate) enum GvdbData { Cow(Cow<'static, [u8]>), #[cfg(feature = "mmap")] Mmap(memmap2::Mmap), } impl AsRef<[u8]> for GvdbData { fn as_ref(&self) -> &[u8] { match self { GvdbData::Cow(cow) => cow.as_ref(), #[cfg(feature = "mmap")] GvdbData::Mmap(mmap) => mmap.as_ref(), } } } /// The root of a GVDB file /// /// # Examples /// /// Load a GResource file from disk /// /// ``` /// use std::path::PathBuf; /// use serde::Deserialize; /// use gvdb::read::GvdbFile; /// /// let path = PathBuf::from("test-data/test3.gresource"); /// let file = GvdbFile::from_file(&path).unwrap(); /// let table = file.hash_table().unwrap(); /// /// #[derive(serde::Deserialize, zvariant::Type)] /// struct SvgData { /// size: u32, /// flags: u32, /// content: Vec /// } /// /// let value = table /// .get_value("/gvdb/rs/test/online-symbolic.svg") /// .unwrap(); /// let structure = zvariant::Structure::try_from(value).unwrap(); /// let svg = structure.fields(); /// let svg1_size = u32::try_from(&svg[0]).unwrap(); /// let svg1_flags = u32::try_from(&svg[1]).unwrap(); /// let svg1_content = >::try_from(svg[2].try_clone().unwrap()).unwrap(); /// let svg1_str = std::str::from_utf8(&svg1_content[0..svg1_content.len() - 1]).unwrap(); /// /// println!("{}", svg1_str); /// ``` /// /// Query the root hash table /// /// ``` /// use gvdb::read::GvdbFile; /// /// fn query_hash_table(file: GvdbFile) { /// let table = file.hash_table().unwrap(); /// let names = table.get_names().unwrap(); /// assert_eq!(names.len(), 2); /// assert_eq!(names[0], "string"); /// assert_eq!(names[1], "table"); /// /// let str_value: String = table.get("string").unwrap(); /// assert_eq!(str_value, "test string"); /// /// let sub_table = table.get_hash_table("table").unwrap(); /// let sub_table_names = sub_table.get_names().unwrap(); /// assert_eq!(sub_table_names.len(), 1); /// assert_eq!(sub_table_names[0], "int"); /// /// let int_value: u32 = sub_table.get("int").unwrap(); /// assert_eq!(int_value, 42); /// } /// ``` pub struct GvdbFile { pub(crate) data: GvdbData, pub(crate) byteswapped: bool, } impl GvdbFile { /// Get the GVDB file header. Will err with GvdbError::DataOffset if the header doesn't fit pub(crate) fn get_header(&self) -> GvdbReaderResult { let header_data = self .data .as_ref() .get(0..size_of::()) .ok_or(GvdbReaderError::DataOffset)?; Ok(transmute_one_pedantic(header_data)?) } /// Returns the root hash table of the file pub fn hash_table(&self) -> GvdbReaderResult { let header = self.get_header()?; let root_ptr = header.root(); GvdbHashTable::for_bytes(self.dereference(root_ptr, 4)?, self) } /// Dereference a pointer 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(()) } /// Interpret a slice of bytes as a GVDB file pub fn from_bytes(bytes: Cow<'static, [u8]>) -> GvdbReaderResult { let mut this = Self { data: GvdbData::Cow(bytes), byteswapped: false, }; this.read_header()?; Ok(this) } /// Open a file and interpret the data as GVDB /// ``` /// let path = std::path::PathBuf::from("test-data/test3.gresource"); /// let file = gvdb::read::GvdbFile::from_file(&path).unwrap(); /// ``` pub fn from_file(filename: &Path) -> GvdbReaderResult { 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)) } /// Open a file and `mmap` it into memory. /// /// # Safety /// /// This is marked unsafe as the file could be modified on-disk while the mmap is active. /// This will cause undefined behavior. You must make sure to employ your own locking and to /// reload the file yourself when any modification occurs. #[cfg(feature = "mmap")] pub unsafe fn from_file_mmap(filename: &Path) -> GvdbReaderResult { let file = File::open(filename).map_err(GvdbReaderError::from_io_with_filename(filename))?; let mmap = memmap2::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) } /// gvdb_table_item_get_key pub(crate) fn get_key(&self, item: &GvdbHashItem) -> GvdbReaderResult { 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 ))) } } /// Get a glib::Variant from the [`GvdbHashItem`] #[cfg(feature = "glib")] pub(crate) fn get_gvariant_for_item( &self, item: &GvdbHashItem, ) -> GvdbReaderResult { 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) } } /// Determine the endianess to use for zvariant pub(crate) fn zvariant_endianess(&self) -> zvariant::Endian { if cfg!(target_endian = "little") && !self.byteswapped || cfg!(target_endian = "big") && self.byteswapped { zvariant::LE } else { zvariant::BE } } /// Get a zvariant::Value from the [`GvdbHashItem`] pub(crate) fn get_value_for_item( &self, item: &GvdbHashItem, ) -> GvdbReaderResult { let data = self.get_bytes_for_item(item)?; // Create a new zvariant context based our endianess and the byteswapped property let context = zvariant::serialized::Context::new_gvariant(self.zvariant_endianess(), 0); // On non-unix systems this function lacks the FD argument #[cfg(unix)] let mut de: gvariant::Deserializer<_> = gvariant::Deserializer::new( data, None::<&[zvariant::Fd]>, zvariant::Value::signature(), context, )?; #[cfg(not(unix))] let mut de: gvariant::Deserializer<()> = gvariant::Deserializer::new(data, zvariant::Value::signature(), context)?; Ok(zvariant::Value::deserialize(&mut de)?) } pub(crate) fn get_hash_table_for_item( &self, item: &GvdbHashItem, ) -> GvdbReaderResult { 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 ))) } } } impl std::fmt::Debug for GvdbFile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Ok(hash_table) = self.hash_table() { f.debug_struct("GvdbFile") .field("byteswapped", &self.byteswapped) .field("header", &self.get_header()) .field("hash_table", &hash_table) .finish() } else { f.debug_struct("GvdbFile") .field("byteswapped", &self.byteswapped) .field("header", &self.get_header()) .finish_non_exhaustive() } } } #[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::{GvdbHashItem, 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); } #[cfg(feature = "mmap")] #[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_be(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_le(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()); } #[cfg(feature = "mmap")] #[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_le(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 file = create_minimal_file(); format!("{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(); // Remove data to see if this will throw an error data.remove(data.len() - 24); // We change the root pointer end to be shorter. Otherwise we will trigger // a data offset error when dereferencing. This is a bit hacky. // The root pointer end is always at position sizeof(u32 * 5). // As this is little endian, we can just modify the first byte. let root_ptr_end = size_of::() * 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(); // We change the root pointer end to be shorter. // The root pointer end is always at position sizeof(u32 * 5). // As this is little endian, we can just modify the first byte. let root_ptr_end = size_of::() * 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 parent_invalid_offset() { let writer = GvdbFileWriter::new(); let mut table = GvdbHashTableBuilder::new(); table.insert_string("parent/test", "test").unwrap(); let mut data = writer.write_to_vec_with_table(table).unwrap(); let file = GvdbFile::from_bytes(Cow::Owned(data.clone())).unwrap(); // We change the parent offset to be bigger than the item size in the hash table. // 'test' will always end up being item 2. // The parent field is at +4. let hash_item_size = size_of::(); let start = file.hash_table().unwrap().hash_items_offset() + hash_item_size * 2; let parent_field = start + 4; data[parent_field..parent_field + size_of::()] .copy_from_slice(safe_transmute::transmute_one_to_bytes(&10u32.to_le())); println!( "{:?}", GvdbFile::from_bytes(Cow::Owned(data.clone())).unwrap() ); let file = GvdbFile::from_bytes(Cow::Owned(data)).unwrap(); let err = file.hash_table().unwrap().get_names().unwrap_err(); assert_matches!(err, GvdbReaderError::DataError(_)); assert!(format!("{}", err).contains("Parent with invalid offset")); assert!(format!("{}", err).contains("10")); } #[test] fn parent_loop() { let writer = GvdbFileWriter::new(); let mut table = GvdbHashTableBuilder::new(); table.insert_string("parent/test", "test").unwrap(); let mut data = writer.write_to_vec_with_table(table).unwrap(); let file = GvdbFile::from_bytes(Cow::Owned(data.clone())).unwrap(); // We change the parent offset to be pointing to itself. // 'test' will always end up being item 2. // The parent field is at +4. let hash_item_size = size_of::(); let start = file.hash_table().unwrap().hash_items_offset() + hash_item_size * 2; let parent_field = start + 4; data[parent_field..parent_field + size_of::()] .copy_from_slice(safe_transmute::transmute_one_to_bytes(&1u32.to_le())); println!( "{:?}", GvdbFile::from_bytes(Cow::Owned(data.clone())).unwrap() ); let file = GvdbFile::from_bytes(Cow::Owned(data)).unwrap(); let err = file.hash_table().unwrap().get_names().unwrap_err(); assert_matches!(err, GvdbReaderError::DataError(_)); assert!(format!("{}", err).contains("loop")); } #[test] fn test_dereference_offset1() { // Pointer start > EOF 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() { // Pointer start > end 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() { // Pointer end > EOF 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() { // Pointer end > EOF 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() { // test file 2 has a nested dictionary let file = GvdbFile::from_file(&TEST_FILE_2).unwrap(); let table = file.hash_table().unwrap(); // A table isn't a value 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(_))); } } gvdb-0.6.1/src/read/hash.rs000064400000000000000000000525541046102023000135600ustar 00000000000000use crate::read::error::{GvdbReaderError, GvdbReaderResult}; use crate::read::file::GvdbFile; use crate::read::hash_item::GvdbHashItem; use crate::util::djb_hash; use safe_transmute::{ transmute_many_pedantic, transmute_one, transmute_one_pedantic, TriviallyTransmutable, }; use std::borrow::Cow; use std::cmp::{max, min}; use std::fmt::{Debug, Formatter}; use std::mem::size_of; /// The header of a GVDB hash table #[repr(C)] #[derive(Copy, Clone, PartialEq, Eq)] pub struct GvdbHashHeader { n_bloom_words: u32, n_buckets: u32, } unsafe impl TriviallyTransmutable for GvdbHashHeader {} impl GvdbHashHeader { /// Create a new GvdbHashHeader using the provided `bloom_shift`, `n_bloom_words` and /// `n_buckets` pub fn new(bloom_shift: u32, n_bloom_words: u32, n_buckets: u32) -> Self { assert!(n_bloom_words < (1 << 27)); let n_bloom_words = bloom_shift << 27 | n_bloom_words; Self { n_bloom_words: n_bloom_words.to_le(), n_buckets: n_buckets.to_le(), } } /// Number of bloom words in the hash table header pub fn n_bloom_words(&self) -> u32 { u32::from_le(self.n_bloom_words) & ((1 << 27) - 1) } /// Size of the bloom words section in the header pub fn bloom_words_len(&self) -> usize { self.n_bloom_words() as usize * size_of::() } /// Number of hash buckets in the hash table header pub fn n_buckets(&self) -> u32 { u32::from_le(self.n_buckets) } /// Length of the hash buckets section in the header pub fn buckets_len(&self) -> usize { self.n_buckets() as usize * size_of::() } } impl Debug for GvdbHashHeader { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("GvdbHashHeader") .field("n_bloom_words", &self.n_bloom_words()) .field("n_buckets", &self.n_buckets()) .field("data", &safe_transmute::transmute_one_to_bytes(self)) .finish() } } /// A hash table inside a GVDB file /// /// #[repr(C)] #[derive(Clone)] pub struct GvdbHashTable<'a> { pub(crate) root: &'a GvdbFile, data: Cow<'a, [u8]>, header: GvdbHashHeader, } impl<'a> GvdbHashTable<'a> { /// Interpret a chunk of bytes as a HashTable. The table_ptr should point to the hash table. /// Data has to be the complete GVDB file, as hash table items are stored somewhere else. pub fn for_bytes(data: &'a [u8], root: &'a GvdbFile) -> GvdbReaderResult { let header = Self::hash_header(data)?; let data = Cow::Borrowed(data); let this = Self { root, data, header }; let header_len = size_of::(); let bloom_words_len = this.bloom_words_end() - this.bloom_words_offset(); let hash_buckets_len = this.hash_buckets_end() - this.hash_buckets_offset(); // we use max() here to prevent possible underflow let hash_items_len = max(this.hash_items_end(), this.hash_items_offset()) - this.hash_items_offset(); let required_len = header_len + bloom_words_len + hash_buckets_len + hash_items_len; if required_len > this.data.len() { Err(GvdbReaderError::DataError(format!( "Not enough bytes to fit hash table: Expected at least {} bytes, got {}", required_len, this.data.len() ))) } else if hash_items_len % size_of::() != 0 { // Wrong data length Err(GvdbReaderError::DataError(format!( "Remaining size invalid: Expected a multiple of {}, got {}", size_of::(), this.data.len() ))) } else { Ok(this) } } /// Read the hash table header fn hash_header(data: &'a [u8]) -> GvdbReaderResult { let bytes: &[u8] = data .get(0..size_of::()) .ok_or(GvdbReaderError::DataOffset)?; Ok(transmute_one(bytes)?) } /// Returns the header for this hash table pub fn get_header(&self) -> GvdbHashHeader { self.header } fn get_u32(&self, offset: usize) -> GvdbReaderResult { let bytes = self .data .get(offset..offset + size_of::()) .ok_or(GvdbReaderError::DataOffset)?; Ok(u32::from_le_bytes(bytes.try_into().unwrap())) } fn bloom_words_offset(&self) -> usize { size_of::() } fn bloom_words_end(&self) -> usize { self.bloom_words_offset() + self.header.bloom_words_len() } /// Returns the bloom words for this hash table #[allow(dead_code)] fn bloom_words(&self) -> Option<&[u32]> { // This indexing operation is safe as data is guaranteed to be larger than // bloom_words_offset and this will just return an empty slice if end == offset transmute_many_pedantic(&self.data[self.bloom_words_offset()..self.bloom_words_end()]).ok() } fn get_bloom_word(&self, index: usize) -> GvdbReaderResult { if index >= self.header.n_bloom_words() as usize { return Err(GvdbReaderError::DataOffset); } let start = self.bloom_words_offset() + index * size_of::(); self.get_u32(start) } // TODO: Calculate proper bloom shift fn bloom_shift(&self) -> usize { 0 } /// Check whether the hash value corresponds to the bloom filter fn bloom_filter(&self, hash_value: u32) -> bool { if self.header.n_bloom_words() == 0 { return true; } let word = (hash_value / 32) % self.header.n_bloom_words(); let mut mask = 1 << (hash_value & 31); mask |= 1 << ((hash_value >> self.bloom_shift()) & 31); // We know this index is < n_bloom_words let bloom_word = self.get_bloom_word(word as usize).unwrap(); bloom_word & mask == mask } fn hash_buckets_offset(&self) -> usize { self.bloom_words_end() } fn hash_buckets_end(&self) -> usize { self.hash_buckets_offset() + self.header.buckets_len() } fn get_hash(&self, index: usize) -> GvdbReaderResult { let start = self.hash_buckets_offset() + index * size_of::(); self.get_u32(start) } pub(crate) fn hash_items_offset(&self) -> usize { self.hash_buckets_end() } fn n_hash_items(&self) -> usize { let len = self.hash_items_end() - self.hash_items_offset(); len / size_of::() } fn hash_items_end(&self) -> usize { self.data.len() } /// Get the hash item at hash item index fn get_hash_item_for_index(&self, index: usize) -> GvdbReaderResult { let size = size_of::(); let start = self.hash_items_offset() + size * index; let end = start + size; let data = self .data .get(start..end) .ok_or(GvdbReaderError::DataOffset)?; Ok(transmute_one_pedantic(data)?) } /// Gets a list of keys contained in the hash table pub fn get_names(&self) -> GvdbReaderResult> { let count = self.n_hash_items(); let mut names = vec![None; count]; let mut inserted = 0; while inserted < count { let last_inserted = inserted; for index in 0..count { let item = self.get_hash_item_for_index(index)?; let parent: usize = item.parent().try_into()?; if names[index].is_none() { // Only process items not already processed if parent == 0xffffffff { // root item let name = self.get_key(&item)?; let _ = std::mem::replace(&mut names[index], Some(name)); inserted += 1; } else if parent < count && names[parent].is_some() { // We already came across this item let name = self.get_key(&item)?; let parent_name = names.get(parent).unwrap().as_ref().unwrap(); let full_name = parent_name.to_string() + &name; let _ = std::mem::replace(&mut names[index], Some(full_name)); inserted += 1; } else if parent > count { return Err(GvdbReaderError::DataError(format!( "Parent with invalid offset encountered: {}", parent ))); } } } if last_inserted == inserted { // No insertion took place this round, there must be a parent loop // We fail instead of infinitely looping return Err(GvdbReaderError::DataError( "Error finding all parent items. The file appears to have a loop".to_string(), )); } } let names = names.into_iter().map(|s| s.unwrap()).collect(); Ok(names) } fn check_name(&self, item: &GvdbHashItem, key: &str) -> bool { let this_key = match self.get_key(item) { Ok(this_key) => this_key, Err(_) => return false, }; if !key.ends_with(&this_key) { return false; } let parent = item.parent(); if key.len() == this_key.len() && parent == 0xffffffff { return true; } if parent < self.n_hash_items() as u32 && !key.is_empty() { let parent_item = match self.get_hash_item_for_index(parent as usize) { Ok(p) => p, Err(_) => return false, }; let parent_key_len = key.len() - this_key.len(); return self.check_name(&parent_item, &key[0..parent_key_len]); } false } /// Gets the item at key `key` pub fn get_hash_item(&self, key: &str) -> GvdbReaderResult { if self.header.n_buckets() == 0 || self.n_hash_items() == 0 { return Err(GvdbReaderError::KeyError(key.to_string())); } let hash_value = djb_hash(key); if !self.bloom_filter(hash_value) { return Err(GvdbReaderError::KeyError(key.to_string())); } let bucket = hash_value % self.header.n_buckets(); let mut itemno = self.get_hash(bucket as usize)? as usize; let lastno = if bucket == self.header.n_buckets() - 1 { self.n_hash_items() } else { min( self.get_hash(bucket as usize + 1)?, self.n_hash_items() as u32, ) as usize }; while itemno < lastno { let item = self.get_hash_item_for_index(itemno)?; if hash_value == item.hash_value() && self.check_name(&item, key) { return Ok(item); } itemno += 1; } Err(GvdbReaderError::KeyError(key.to_string())) } /// Get the item at key `key` and try to interpret it as a [`enum@zvariant::Value`] pub fn get_value(&self, key: &str) -> GvdbReaderResult { self.get_value_for_item(&self.get_hash_item(key)?) } /// Get the item at key `key` and try to convert it from [`enum@zvariant::Value`] to T pub fn get(&self, key: &str) -> GvdbReaderResult where T: TryFrom, { T::try_from(zvariant::OwnedValue::try_from( self.get_value_for_item(&self.get_hash_item(key)?)?, )?) .map_err(|_| { GvdbReaderError::DataError("Can't convert Value to specified type".to_string()) }) } #[cfg(feature = "glib")] /// Get the item at key `key` and try to interpret it as a [`struct@glib::Variant`] pub fn get_gvariant(&self, key: &str) -> GvdbReaderResult { self.get_gvariant_for_item(&self.get_hash_item(key)?) } /// Get the item at key `key` and try to interpret it as a [`GvdbHashTable`] pub fn get_hash_table(&self, key: &str) -> GvdbReaderResult { self.get_hash_table_for_item(&self.get_hash_item(key)?) } fn get_key(&self, item: &GvdbHashItem) -> GvdbReaderResult { self.root.get_key(item) } fn get_value_for_item(&self, item: &GvdbHashItem) -> GvdbReaderResult { self.root.get_value_for_item(item) } #[cfg(feature = "glib")] fn get_gvariant_for_item(&self, item: &GvdbHashItem) -> GvdbReaderResult { self.root.get_gvariant_for_item(item) } fn get_hash_table_for_item(&self, item: &GvdbHashItem) -> GvdbReaderResult { self.root.get_hash_table_for_item(item) } } impl std::fmt::Debug for GvdbHashTable<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("GvdbHashTable") .field("header", &self.header) .field( "map", &self.get_names().map(|res| { res.iter() .map(|name| { let item = self.get_hash_item(name); match item { Ok(item) => { let value = match item.typ() { Ok(super::GvdbHashItemType::Container) => { Ok(Box::new(item) as Box) } Ok(super::GvdbHashItemType::HashTable) => { self.get_hash_table_for_item(&item).map(|table| { Box::new(table) as Box }) } Ok(super::GvdbHashItemType::Value) => { self.get_value_for_item(&item).map(|value| { Box::new(value) as Box }) } Err(err) => Err(err), }; (name.to_string(), Ok((item, value))) } Err(err) => (name.to_string(), Err(err)), } }) .collect::>() }), ) .finish() } } #[cfg(test)] pub(crate) mod test { use crate::read::{GvdbFile, GvdbHashHeader, GvdbHashItem, GvdbPointer, GvdbReaderError}; use crate::test::*; use crate::test::{assert_eq, assert_matches, assert_ne}; #[test] fn debug() { let header = GvdbHashHeader::new(0, 0, 0); let header2 = header.clone(); println!("{:?}", header2); let file = new_empty_file(); let table = file.hash_table().unwrap(); let table2 = table.clone(); println!("{:?}", table2); } #[test] fn get_header() { let file = new_empty_file(); let table = file.hash_table().unwrap(); let header = table.get_header(); assert_eq!(header.n_buckets(), 0); let file = new_simple_file(false); let table = file.hash_table().unwrap(); let header = table.get_header(); assert_eq!(header.n_buckets(), 1); println!("{:?}", table); } #[test] fn bloom_words() { let file = new_empty_file(); let table = file.hash_table().unwrap(); let header = table.get_header(); assert_eq!(header.n_bloom_words(), 0); assert_eq!(header.bloom_words_len(), 0); assert_eq!(table.bloom_words(), None); } #[test] fn get_item() { let file = new_empty_file(); let table = file.hash_table().unwrap(); let res = table.get_hash_item("test"); assert_matches!(res, Err(GvdbReaderError::KeyError(_))); for endianess in [true, false] { let file = new_simple_file(endianess); let table = file.hash_table().unwrap(); let item = table.get_hash_item("test").unwrap(); assert_ne!(item.value_ptr(), &GvdbPointer::NULL); let value: String = table.get_value_for_item(&item).unwrap().try_into().unwrap(); assert_eq!(value, "test"); let item_fail = table.get_hash_item("fail").unwrap_err(); assert_matches!(item_fail, GvdbReaderError::KeyError(_)); let res_item = table.get_hash_item("test_fail"); assert_matches!(res_item, Err(GvdbReaderError::KeyError(_))); } } #[test] fn get() { for endianess in [true, false] { let file = new_simple_file(endianess); let table = file.hash_table().unwrap(); let res: String = table.get::("test").unwrap().into(); assert_eq!(&res, "test"); let res = table.get::("test"); assert_matches!(res, Err(GvdbReaderError::DataError(_))); } } #[test] fn get_bloom_word() { for endianess in [true, false] { let file = new_simple_file(endianess); let table = file.hash_table().unwrap(); let res = table.get_bloom_word(0); assert_matches!(res, Err(GvdbReaderError::DataOffset)); } } #[test] fn bloom_shift() { for endianess in [true, false] { let file = new_simple_file(endianess); let table = file.hash_table().unwrap(); let res = table.bloom_shift(); assert_eq!(res, 0); } } #[test] fn get_value() { for endianess in [true, false] { let file = new_simple_file(endianess); let table = file.hash_table().unwrap(); let res = table.get_value("test").unwrap(); assert_eq!(&res, &zvariant::Value::from("test")); let fail = table.get_value("fail").unwrap_err(); assert_matches!(fail, GvdbReaderError::KeyError(_)); } } #[test] fn get_hash_table() { let file = GvdbFile::from_file(&TEST_FILE_2).unwrap(); let table = file.hash_table().unwrap(); let table = table.get_hash_table("table").unwrap(); let fail = table.get_hash_table("fail").unwrap_err(); assert_matches!(fail, GvdbReaderError::KeyError(_)); } #[test] fn check_name_pass() { let file = GvdbFile::from_file(&TEST_FILE_2).unwrap(); let table = file.hash_table().unwrap(); let item = table.get_hash_item("string").unwrap(); assert_eq!(table.check_name(&item, "string"), true); } #[test] fn check_name_invalid_name() { let file = GvdbFile::from_file(&TEST_FILE_2).unwrap(); let table = file.hash_table().unwrap(); let item = table.get_hash_item("string").unwrap(); assert_eq!(table.check_name(&item, "fail"), false); } #[test] fn check_name_wrong_item() { let file = GvdbFile::from_file(&TEST_FILE_2).unwrap(); let table = file.hash_table().unwrap(); let table = table.get_hash_table("table").unwrap(); // Get an item from the sub-hash table and call check_names on the root let item = table.get_hash_item("int").unwrap(); assert_eq!(table.check_name(&item, "table"), false); } #[test] fn check_name_broken_key_pointer() { let file = GvdbFile::from_file(&TEST_FILE_2).unwrap(); let table = file.hash_table().unwrap(); let table = table.get_hash_table("table").unwrap(); // Break the key pointer let item = table.get_hash_item("int").unwrap(); let key_ptr = GvdbPointer::new(500, 500); let broken_item = GvdbHashItem::new( item.hash_value(), item.parent(), key_ptr, item.typ().unwrap(), item.value_ptr().clone(), ); assert_eq!(table.check_name(&broken_item, "table"), false); } #[test] fn check_name_invalid_parent() { let file = GvdbFile::from_file(&TEST_FILE_3).unwrap(); let table = file.hash_table().unwrap(); // Break the key pointer let item = table .get_hash_item("/gvdb/rs/test/online-symbolic.svg") .unwrap(); let broken_item = GvdbHashItem::new( item.hash_value(), 50, item.key_ptr(), item.typ().unwrap(), item.value_ptr().clone(), ); assert_eq!( table.check_name(&broken_item, "/gvdb/rs/test/online-symbolic.svg"), false ); } } #[cfg(all(feature = "glib", test))] mod test_glib { use crate::read::GvdbReaderError; use crate::test::new_simple_file; use glib::prelude::*; use matches::assert_matches; #[test] fn get_gvariant() { for endianess in [true, false] { let file = new_simple_file(endianess); let table = file.hash_table().unwrap(); let res: glib::Variant = table.get_gvariant("test").unwrap().get().unwrap(); assert_eq!(&res, &"test".to_variant()); let fail = table.get_gvariant("fail").unwrap_err(); assert_matches!(fail, GvdbReaderError::KeyError(_)); } } } gvdb-0.6.1/src/read/hash_item.rs000064400000000000000000000120171046102023000145640ustar 00000000000000use crate::read::error::{GvdbReaderError, GvdbReaderResult}; use crate::read::pointer::GvdbPointer; use safe_transmute::TriviallyTransmutable; use std::fmt::{Display, Formatter}; #[derive(PartialEq, Eq, Debug)] pub enum GvdbHashItemType { Value, HashTable, Container, } impl From for u8 { fn from(item: GvdbHashItemType) -> Self { match item { GvdbHashItemType::Value => b'v', GvdbHashItemType::HashTable => b'H', GvdbHashItemType::Container => b'L', } } } impl TryFrom for GvdbHashItemType { type Error = GvdbReaderError; fn try_from(value: u8) -> Result { let chr = value as char; if chr == 'v' { Ok(GvdbHashItemType::Value) } else if chr == 'H' { Ok(GvdbHashItemType::HashTable) } else if chr == 'L' { Ok(GvdbHashItemType::Container) } else { Err(GvdbReaderError::InvalidData) } } } impl Display for GvdbHashItemType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let text = match self { GvdbHashItemType::Value => "Value", GvdbHashItemType::HashTable => "HashTable", GvdbHashItemType::Container => "Child", }; write!(f, "{}", text) } } #[repr(C)] #[derive(Copy, Clone)] pub struct GvdbHashItem { hash_value: u32, parent: u32, key_start: u32, key_size: u16, typ: u8, unused: u8, value: GvdbPointer, } unsafe impl TriviallyTransmutable for GvdbHashItem {} impl GvdbHashItem { pub fn new( hash_value: u32, parent: u32, key_ptr: GvdbPointer, typ: GvdbHashItemType, value: GvdbPointer, ) -> Self { let key_start = key_ptr.start().to_le(); let key_size = (key_ptr.size() as u16).to_le(); Self { hash_value: hash_value.to_le(), parent: parent.to_le(), key_start, key_size, typ: typ.into(), unused: 0, value, } } pub fn hash_value(&self) -> u32 { u32::from_le(self.hash_value) } pub fn parent(&self) -> u32 { u32::from_le(self.parent) } pub fn key_start(&self) -> u32 { u32::from_le(self.key_start) } pub fn key_size(&self) -> u16 { u16::from_le(self.key_size) } pub fn key_ptr(&self) -> GvdbPointer { GvdbPointer::new( self.key_start() as usize, self.key_start() as usize + self.key_size() as usize, ) } pub fn typ(&self) -> GvdbReaderResult { self.typ.try_into() } pub fn value_ptr(&self) -> &GvdbPointer { &self.value } } impl std::fmt::Debug for GvdbHashItem { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("GvdbHashItem") .field("hash_value", &self.hash_value()) .field("parent", &self.parent()) .field("key_start", &self.key_start()) .field("key_size", &self.key_size()) .field("typ", &self.typ()) .field("unused", &self.unused) .field("value", &self.value_ptr()) .finish() } } #[cfg(test)] mod test { use crate::read::{GvdbHashItem, GvdbHashItemType, GvdbPointer, GvdbReaderError}; use matches::assert_matches; #[test] fn derives() { let typ = GvdbHashItemType::Value; println!("{}, {:?}", typ, typ); let typ = GvdbHashItemType::HashTable; println!("{}, {:?}", typ, typ); let typ = GvdbHashItemType::Container; println!("{}, {:?}", typ, typ); let item = GvdbHashItem::new( 0, 0, GvdbPointer::NULL, GvdbHashItemType::Value, GvdbPointer::NULL, ); let item2 = item.clone(); println!("{:?}", item2); } #[test] fn type_try_from() { assert_matches!( GvdbHashItemType::try_from(b'v'), Ok(GvdbHashItemType::Value) ); assert_matches!( GvdbHashItemType::try_from(b'H'), Ok(GvdbHashItemType::HashTable) ); assert_matches!( GvdbHashItemType::try_from(b'L'), Ok(GvdbHashItemType::Container) ); assert_matches!( GvdbHashItemType::try_from(b'x'), Err(GvdbReaderError::InvalidData) ); assert_matches!( GvdbHashItemType::try_from(b'?'), Err(GvdbReaderError::InvalidData) ); } #[test] fn item() { let item = GvdbHashItem::new( 0, 0, GvdbPointer::NULL, GvdbHashItemType::Value, GvdbPointer::NULL, ); assert_eq!(item.hash_value(), 0); assert_eq!(item.parent(), 0); assert_eq!(item.key_ptr(), GvdbPointer::NULL); assert_matches!(item.typ(), Ok(GvdbHashItemType::Value)); assert_eq!(item.value_ptr(), &GvdbPointer::NULL); } } gvdb-0.6.1/src/read/header.rs000064400000000000000000000062021046102023000140520ustar 00000000000000use crate::read::error::{GvdbReaderError, GvdbReaderResult}; use crate::read::pointer::GvdbPointer; use safe_transmute::TriviallyTransmutable; // This is just a string, but it is stored in the byteorder of the file // Default byteorder is little endian, but the format supports big endian as well // "GVar" const GVDB_SIGNATURE0: u32 = 1918981703; // "iant" const GVDB_SIGNATURE1: u32 = 1953390953; #[repr(C)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct GvdbHeader { signature: [u32; 2], version: u32, options: u32, root: GvdbPointer, } unsafe impl TriviallyTransmutable for GvdbHeader {} impl GvdbHeader { #[cfg(test)] pub fn new_le(version: u32, root: GvdbPointer) -> Self { #[cfg(target_endian = "little")] let byteswap = false; #[cfg(target_endian = "big")] let byteswap = true; Self::new(byteswap, version, root) } #[cfg(test)] pub fn new_be(version: u32, root: GvdbPointer) -> Self { #[cfg(target_endian = "little")] let byteswap = true; #[cfg(target_endian = "big")] let byteswap = false; Self::new(byteswap, version, root) } pub fn new(byteswap: bool, version: u32, root: GvdbPointer) -> Self { let signature = if !byteswap { [GVDB_SIGNATURE0, GVDB_SIGNATURE1] } else { [GVDB_SIGNATURE0.swap_bytes(), GVDB_SIGNATURE1.swap_bytes()] }; Self { signature, version: version.to_le(), options: 0, root, } } pub fn is_byteswap(&self) -> GvdbReaderResult { if self.signature[0] == GVDB_SIGNATURE0 && self.signature[1] == GVDB_SIGNATURE1 { Ok(false) } else if self.signature[0] == GVDB_SIGNATURE0.swap_bytes() && self.signature[1] == GVDB_SIGNATURE1.swap_bytes() { Ok(true) } else { Err(GvdbReaderError::InvalidData) } } pub fn header_valid(&self) -> bool { self.is_byteswap().is_ok() } pub fn version(&self) -> u32 { self.version } pub fn root(&self) -> &GvdbPointer { &self.root } } #[cfg(test)] mod test { use super::*; use safe_transmute::{transmute_one_pedantic, transmute_one_to_bytes}; #[test] fn derives() { let header = GvdbHeader::new(false, 0, GvdbPointer::NULL); let header2 = header.clone(); println!("{:?}", header2); } #[test] fn header_serialize() { let header = GvdbHeader::new(false, 123, GvdbPointer::NULL); assert_eq!(header.is_byteswap().unwrap(), false); let data = transmute_one_to_bytes(&header); let parsed_header: GvdbHeader = transmute_one_pedantic(data.as_ref()).unwrap(); assert_eq!(parsed_header.is_byteswap().unwrap(), false); let header = GvdbHeader::new(true, 0, GvdbPointer::NULL); assert_eq!(header.is_byteswap().unwrap(), true); let data = transmute_one_to_bytes(&header); let parsed_header: GvdbHeader = transmute_one_pedantic(data.as_ref()).unwrap(); assert_eq!(parsed_header.is_byteswap().unwrap(), true); } } gvdb-0.6.1/src/read/pointer.rs000064400000000000000000000023331046102023000143030ustar 00000000000000#[repr(C)] #[derive(Copy, Clone, PartialEq, Eq)] pub struct GvdbPointer { start: u32, end: u32, } impl GvdbPointer { pub const NULL: Self = Self { start: 0, end: 0 }; pub fn new(start: usize, end: usize) -> Self { Self { start: (start as u32).to_le(), end: (end as u32).to_le(), } } pub fn start(&self) -> u32 { u32::from_le(self.start) } pub fn end(&self) -> u32 { u32::from_le(self.end) } pub fn size(&self) -> usize { self.end().saturating_sub(self.start()) as usize } } impl std::fmt::Debug for GvdbPointer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GvdbPointer") .field("start", &self.start()) .field("end", &self.end()) .finish() } } #[cfg(test)] mod test { use crate::read::GvdbPointer; #[test] fn derives() { let pointer = GvdbPointer::new(0, 2); let pointer2 = pointer.clone(); println!("{:?}", pointer2); } #[test] fn no_panic_invalid_size() { let invalid_ptr = GvdbPointer::new(100, 0); let size = invalid_ptr.size(); assert_eq!(size, 0); } } gvdb-0.6.1/src/read.rs000064400000000000000000000005331046102023000126230ustar 00000000000000mod error; mod file; mod hash; mod hash_item; mod header; mod pointer; pub use error::{GvdbReaderError, GvdbReaderResult}; pub use file::GvdbFile; pub use hash::GvdbHashTable; pub(crate) use hash::GvdbHashHeader; pub(crate) use hash_item::{GvdbHashItem, GvdbHashItemType}; pub(crate) use header::GvdbHeader; pub(crate) use pointer::GvdbPointer; gvdb-0.6.1/src/test.rs000064400000000000000000000320051046102023000126660ustar 00000000000000#![allow(unused)] use crate::read::{GvdbFile, GvdbHashItemType, GvdbHashTable}; use crate::write::{GvdbFileWriter, GvdbHashTableBuilder}; use lazy_static::lazy_static; pub use matches::assert_matches; pub use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use std::borrow::Cow; use std::cmp::{max, min}; use std::io::{Cursor, Read}; use std::path::{Path, PathBuf}; lazy_static! { pub(crate) static ref TEST_FILE_DIR: PathBuf = PathBuf::from("test-data"); pub(crate) static ref TEST_FILE_1: PathBuf = TEST_FILE_DIR.join("test1.gvdb"); pub(crate) static ref TEST_FILE_2: PathBuf = TEST_FILE_DIR.join("test2.gvdb"); pub(crate) static ref TEST_FILE_3: PathBuf = TEST_FILE_DIR.join("test3.gresource"); pub(crate) static ref GRESOURCE_DIR: PathBuf = TEST_FILE_DIR.join("gresource"); pub(crate) static ref GRESOURCE_XML: PathBuf = GRESOURCE_DIR.join("test3.gresource.xml"); } fn write_byte_row( f: &mut dyn std::io::Write, offset: usize, bytes_per_row: usize, bytes: &[u8], ) -> std::io::Result<()> { write!(f, "{:08X}", offset)?; for (index, byte) in bytes.iter().enumerate() { if index % 4 == 0 { write!(f, " ")?; } write!(f, " {:02X}", byte)?; } let bytes_per_row = max(bytes_per_row, bytes.len()); for index in bytes.len()..bytes_per_row { if index % 4 == 0 { write!(f, " ")?; } write!(f, " ")?; } write!(f, " ")?; for byte in bytes { if byte.is_ascii_alphanumeric() || byte.is_ascii_whitespace() { write!(f, "{}", *byte as char)?; } else { write!(f, ".")?; } } writeln!(f) } fn write_byte_rows( f: &mut dyn std::io::Write, center_offset: usize, additional_rows_top: usize, additional_rows_bottom: usize, bytes_per_row: usize, bytes: &[u8], ) -> std::io::Result<()> { let center_row_num = center_offset / bytes_per_row; let start_row = center_row_num - min(center_row_num, additional_rows_top); // We add 1 because we can add partial rows at the end let last_row = min( additional_rows_bottom + center_row_num, bytes.len() / bytes_per_row + 1, ); let row_count = last_row - start_row; for row in 0..row_count { let offset_start = (start_row + row) * bytes_per_row; let offset_end = min(bytes.len(), offset_start + bytes_per_row); write_byte_row( f, offset_start, bytes_per_row, &bytes[offset_start..offset_end], )?; } Ok(()) } pub fn assert_bytes_eq(a: &[u8], b: &[u8], context: &str) { const WIDTH: usize = 16; const EXTRA_ROWS_TOP: usize = 8; const EXTRA_ROWS_BOTTOM: usize = 4; let max_len = max(a.len(), b.len()); for index in 0..max_len { let a_byte = a.get(index); let b_byte = b.get(index); if a_byte.is_none() || b_byte.is_none() || a_byte.unwrap() != b_byte.unwrap() { let mut a_bytes_buf = Vec::new(); write_byte_rows( &mut a_bytes_buf, index, EXTRA_ROWS_TOP, EXTRA_ROWS_BOTTOM, WIDTH, &a, ) .unwrap(); let str_a = String::from_utf8(a_bytes_buf).unwrap(); let mut b_bytes_buf = Vec::new(); write_byte_rows( &mut b_bytes_buf, index, EXTRA_ROWS_TOP, EXTRA_ROWS_BOTTOM, WIDTH, &b, ) .unwrap(); let str_b = String::from_utf8(b_bytes_buf).unwrap(); eprintln!("{}", context); assert_str_eq!(str_a, str_b); } } } pub fn byte_compare_gvdb_file(a: &GvdbFile, b: &GvdbFile) { assert_eq!(a.get_header().unwrap(), b.get_header().unwrap()); let a_hash = a.hash_table().unwrap(); let b_hash = b.hash_table().unwrap(); byte_compare_gvdb_hash_table(&a_hash, &b_hash); } fn byte_compare_file(file: &GvdbFile, reference_path: &Path) { let mut reference_file = std::fs::File::open(reference_path).unwrap(); let mut reference_data = Vec::new(); reference_file.read_to_end(&mut reference_data).unwrap(); assert_bytes_eq( &reference_data, file.data.as_ref(), &format!("Byte comparing with file '{}'", reference_path.display()), ); } pub fn byte_compare_file_1(file: &GvdbFile) { byte_compare_file(file, &TEST_FILE_1); } pub fn assert_is_file_1(file: &GvdbFile) { let table = file.hash_table().unwrap(); let names = table.get_names().unwrap(); assert_eq!(names.len(), 1); assert_eq!(names[0], "root_key"); let value = table.get_value("root_key").unwrap(); assert_matches!(value, zvariant::Value::Structure(_)); assert_eq!(value.value_signature(), "(uus)"); let tuple = zvariant::Structure::try_from(value).unwrap(); let fields = tuple.into_fields(); assert_eq!(u32::try_from(&fields[0]), Ok(1234)); assert_eq!(u32::try_from(&fields[1]), Ok(98765)); assert_eq!(<&str>::try_from(&fields[2]), Ok("TEST_STRING_VALUE")); } pub fn byte_compare_file_2(file: &GvdbFile) { byte_compare_file(file, &TEST_FILE_2); } pub fn assert_is_file_2(file: &GvdbFile) { let table = file.hash_table().unwrap(); let names = table.get_names().unwrap(); assert_eq!(names.len(), 2); assert_eq!(names[0], "string"); assert_eq!(names[1], "table"); let str_value = table.get_value("string").unwrap(); assert_matches!(str_value, zvariant::Value::Str(_)); assert_eq!(<&str>::try_from(&str_value), Ok("test string")); let sub_table = table.get_hash_table("table").unwrap(); let sub_table_names = sub_table.get_names().unwrap(); assert_eq!(sub_table_names.len(), 1); assert_eq!(sub_table_names[0], "int"); let int_value = sub_table.get_value("int").unwrap(); assert_eq!(u32::try_from(int_value), Ok(42)); } pub fn byte_compare_file_3(file: &GvdbFile) { let ref_root = GvdbFile::from_file(&TEST_FILE_3).unwrap(); byte_compare_gvdb_file(file, &ref_root); } pub fn assert_is_file_3(file: &GvdbFile) { let table = file.hash_table().unwrap(); let mut names = table.get_names().unwrap(); names.sort(); let reference_names = vec![ "/", "/gvdb/", "/gvdb/rs/", "/gvdb/rs/test/", "/gvdb/rs/test/icons/", "/gvdb/rs/test/icons/scalable/", "/gvdb/rs/test/icons/scalable/actions/", "/gvdb/rs/test/icons/scalable/actions/send-symbolic.svg", "/gvdb/rs/test/json/", "/gvdb/rs/test/json/test.json", "/gvdb/rs/test/online-symbolic.svg", "/gvdb/rs/test/test.css", ]; assert_eq!(names, reference_names); #[derive(zvariant::OwnedValue)] struct GResourceData { size: u32, flags: u32, content: Vec, } let svg1: GResourceData = table.get("/gvdb/rs/test/online-symbolic.svg").unwrap(); // Convert back and forth to prove that works let svg1_owned_value = zvariant::OwnedValue::try_from(svg1).unwrap(); let svg1 = GResourceData::try_from(svg1_owned_value).unwrap(); assert_eq!(svg1.size, 1390); assert_eq!(svg1.flags, 0); assert_eq!(svg1.size as usize, svg1.content.len() - 1); // Ensure the last byte is zero because of zero-padding defined in the format assert_eq!(svg1.content[svg1.content.len() - 1], 0); let svg1_str = std::str::from_utf8(&svg1.content[0..svg1.content.len() - 1]).unwrap(); assert!(svg1_str.starts_with( &(r#""#.to_string() + "\n\n" + r#" = >::try_from(svg2_fields[2].try_clone().unwrap()).unwrap(); assert_eq!(svg2_size, 345); assert_eq!(svg2_flags, 1); let mut decoder = flate2::read::ZlibDecoder::new(&*svg2_content); let mut svg2_data = Vec::new(); decoder.read_to_end(&mut svg2_data).unwrap(); // Ensure the last byte is *not* zero and len is not one bigger than specified because // compressed data is not zero-padded assert_ne!(svg2_data[svg2_data.len() - 1], 0); assert_eq!(svg2_size as usize, svg2_data.len()); let svg2_str = std::str::from_utf8(&svg2_data).unwrap(); let mut svg2_reference = String::new(); std::fs::File::open(GRESOURCE_DIR.join("icons/scalable/actions/send-symbolic.svg")) .unwrap() .read_to_string(&mut svg2_reference) .unwrap(); assert_str_eq!(svg2_str, svg2_reference); let json = zvariant::Structure::try_from(table.get_value("/gvdb/rs/test/json/test.json").unwrap()) .unwrap() .into_fields(); let json_size: u32 = (&json[0]).try_into().unwrap(); let json_flags: u32 = (&json[1]).try_into().unwrap(); let json_content: Vec = json[2].try_clone().unwrap().try_into().unwrap(); // Ensure the last byte is zero because of zero-padding defined in the format assert_eq!(json_content[json_content.len() - 1], 0); assert_eq!(json_size as usize, json_content.len() - 1); let json_str = std::str::from_utf8(&json_content[0..json_content.len() - 1]).unwrap(); assert_eq!(json_flags, 0); assert_str_eq!( json_str, r#"["test_string",42,{"bool":true}]"#.to_string() + "\n" ); } pub(crate) fn new_empty_file() -> GvdbFile { let writer = GvdbFileWriter::new(); let table_builder = GvdbHashTableBuilder::new(); let data = Vec::new(); let mut cursor = Cursor::new(data); writer.write_with_table(table_builder, &mut cursor).unwrap(); GvdbFile::from_bytes(Cow::Owned(cursor.into_inner())).unwrap() } pub(crate) fn new_simple_file(big_endian: bool) -> GvdbFile { let writer = if big_endian { GvdbFileWriter::for_big_endian() } else { GvdbFileWriter::new() }; let mut table_builder = GvdbHashTableBuilder::new(); table_builder.insert("test", "test").unwrap(); let data = Vec::new(); let mut cursor = Cursor::new(data); writer.write_with_table(table_builder, &mut cursor).unwrap(); GvdbFile::from_bytes(Cow::Owned(cursor.into_inner())).unwrap() } pub(crate) fn byte_compare_gvdb_hash_table(a: &GvdbHashTable, b: &GvdbHashTable) { assert_eq!(a.get_header(), b.get_header()); let mut keys_a = a.get_names().unwrap(); let mut keys_b = b.get_names().unwrap(); keys_a.sort(); keys_b.sort(); assert_eq!(keys_a, keys_b); for key in keys_a { let item_a = a.get_hash_item(&key).unwrap(); let item_b = b.get_hash_item(&key).unwrap(); assert_eq!(item_a.hash_value(), item_b.hash_value()); assert_eq!(item_a.key_size(), item_b.key_size()); assert_eq!(item_a.typ().unwrap(), item_b.typ().unwrap()); assert_eq!(item_a.value_ptr().size(), item_b.value_ptr().size()); let data_a = a.root.dereference(item_a.value_ptr(), 1).unwrap(); let data_b = b.root.dereference(item_b.value_ptr(), 1).unwrap(); // We don't compare containers, only their length if item_a.typ().unwrap() == GvdbHashItemType::Container { if data_a.len() != data_b.len() { // The lengths should not be different. For context we will compare the data assert_bytes_eq( data_a, data_b, &format!("Containers with key '{}' have different lengths", key), ); } } else { assert_bytes_eq( data_a, data_b, &format!("Comparing items with key '{}'", key), ); } } } #[cfg(test)] mod test { #[test] fn assert_bytes_eq1() { super::assert_bytes_eq(&[1, 2, 3], &[1, 2, 3], "test"); } #[test] fn assert_bytes_eq2() { // b is exactly 16 bytes long to test "b is too small" panic super::assert_bytes_eq( b"help i am stuck in a test case", b"help i am stuck in a test case", "test", ); } #[test] #[should_panic] fn assert_bytes_eq_fail1() { super::assert_bytes_eq(&[1, 2, 4], &[1, 2, 3], "test"); } #[test] #[should_panic] fn assert_bytes_eq_fail2() { super::assert_bytes_eq(&[1, 2, 3, 4], &[1, 2, 3], "test"); } #[test] #[should_panic] fn assert_bytes_eq_fail3() { super::assert_bytes_eq(&[1, 2, 3], &[1, 2, 3, 4], "test"); } #[test] #[should_panic] fn assert_bytes_eq_fail4() { // b is exactly 16 bytes long to test "b is too small" panic super::assert_bytes_eq( b"help i am stuck in a test case", b"help i am stuck in a test cas", "test", ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/src/util.rs������������������������������������������������������������������������������0000644�0000000�0000000�00000002720�10461020230�0012665�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/// Perform the djb2 hash function pub fn djb_hash(key: &str) -> u32 { let mut hash_value: u32 = 5381; for char in key.bytes() { hash_value = hash_value.wrapping_mul(33).wrapping_add(char as u32); } hash_value } /// Align an arbitrary offset to a multiple of 2 /// The result is undefined for alignments that are not a multiple of 2 pub fn align_offset(offset: usize, alignment: usize) -> usize { //(alignment - (offset % alignment)) % alignment (offset + alignment - 1) & !(alignment - 1) } #[cfg(test)] mod test { use super::align_offset; #[test] fn align() { assert_eq!(align_offset(17, 16), 32); assert_eq!(align_offset(13, 8), 16); assert_eq!(align_offset(1, 8), 8); assert_eq!(align_offset(2, 8), 8); assert_eq!(align_offset(3, 8), 8); assert_eq!(align_offset(4, 8), 8); assert_eq!(align_offset(5, 8), 8); assert_eq!(align_offset(6, 8), 8); assert_eq!(align_offset(7, 8), 8); assert_eq!(align_offset(8, 8), 8); assert_eq!(align_offset(1, 4), 4); assert_eq!(align_offset(2, 4), 4); assert_eq!(align_offset(3, 4), 4); assert_eq!(align_offset(4, 4), 4); assert_eq!(align_offset(0, 2), 0); assert_eq!(align_offset(1, 2), 2); assert_eq!(align_offset(2, 2), 2); assert_eq!(align_offset(3, 2), 4); assert_eq!(align_offset(0, 1), 0); assert_eq!(align_offset(1, 1), 1); } } ������������������������������������������������gvdb-0.6.1/src/write/error.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000004364�10461020230�0014201�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; /// Error type for GvdbFileWriter pub enum GvdbWriterError { /// Generic I/O error. Path contains an optional filename if applicable Io(std::io::Error, Option), /// An internal inconsistency was found Consistency(String), /// An error occured when serializing variant data with zvariant ZVariant(zvariant::Error), } impl Error for GvdbWriterError {} impl From for GvdbWriterError { fn from(err: std::io::Error) -> Self { Self::Io(err, None) } } impl From for GvdbWriterError { fn from(err: zvariant::Error) -> Self { Self::ZVariant(err) } } impl Display for GvdbWriterError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { GvdbWriterError::Io(err, path) => { if let Some(path) = path { write!(f, "I/O error for file '{}': {}", path.display(), err) } else { write!(f, "I/O error: {}", err) } } GvdbWriterError::Consistency(context) => { write!(f, "Internal inconsistency: {}", context) } GvdbWriterError::ZVariant(err) => { write!(f, "Error writing ZVariant data: {}", err) } } } } impl Debug for GvdbWriterError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } /// The Result type for [`GvdbWriterError`] pub type GvdbBuilderResult = Result; #[cfg(test)] mod test { use super::GvdbWriterError; use matches::assert_matches; use std::path::PathBuf; #[test] fn from() { let err = GvdbWriterError::from(zvariant::Error::Message("Test".to_string())); assert_matches!(err, GvdbWriterError::ZVariant(_)); assert!(format!("{}", err).contains("ZVariant")); let err = GvdbWriterError::Io( std::io::Error::from(std::io::ErrorKind::NotFound), Some(PathBuf::from("test_path")), ); assert_matches!(err, GvdbWriterError::Io(..)); assert!(format!("{}", err).contains("test_path")); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/src/write/file.rs������������������������������������������������������������������������0000644�0000000�0000000�00000073762�10461020230�0013777�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::read::GvdbHashHeader; use crate::read::GvdbHashItem; use crate::read::GvdbHeader; use crate::read::GvdbPointer; use crate::util::align_offset; use crate::write::error::{GvdbBuilderResult, GvdbWriterError}; use crate::write::hash::SimpleHashTable; use crate::write::item::GvdbBuilderItemValue; use safe_transmute::transmute_one_to_bytes; use std::collections::{HashMap, VecDeque}; use std::io::Write; use std::mem::size_of; /// Create hash tables for use in GVDB files /// /// # Example /// /// ``` /// use glib::prelude::*; /// use gvdb::write::{GvdbFileWriter, GvdbHashTableBuilder}; /// /// let file_writer = GvdbFileWriter::new(); /// let mut table_builder = GvdbHashTableBuilder::new(); /// table_builder /// .insert_string("string", "test string") /// .unwrap(); /// let gvdb_data = file_writer.write_to_vec_with_table(table_builder).unwrap(); /// ``` #[derive(Debug)] pub struct GvdbHashTableBuilder<'a> { items: HashMap>, path_separator: Option, } impl<'a> GvdbHashTableBuilder<'a> { /// Create a new empty GvdbHashTableBuilder with the default path separator `/` /// /// ``` /// # use gvdb::write::GvdbHashTableBuilder; /// let mut table_builder = GvdbHashTableBuilder::new(); /// ``` pub fn new() -> Self { Self::with_path_separator(Some("/")) } /// Create a new empty GvdbHashTableBuilder a different path separator than `/` or none at all /// /// ``` /// # use gvdb::write::GvdbHashTableBuilder; /// let mut table_builder = GvdbHashTableBuilder::with_path_separator(Some(":")); /// ``` pub fn with_path_separator(sep: Option<&str>) -> Self { Self { items: Default::default(), path_separator: sep.map(|s| s.to_string()), } } fn insert_item_value( &mut self, key: &(impl ToString + ?Sized), item: GvdbBuilderItemValue<'a>, ) -> GvdbBuilderResult<()> { let key = key.to_string(); if let Some(sep) = &self.path_separator { let mut this_key = "".to_string(); let mut last_key: Option = None; for segment in key.split(sep) { this_key += segment; if this_key != key { this_key += sep; } if let Some(last_key) = last_key { if let Some(last_item) = self.items.get_mut(&last_key) { if let GvdbBuilderItemValue::Container(ref mut container) = last_item { if !container.contains(&this_key) { container.push(this_key.clone()); } } else { return Err(GvdbWriterError::Consistency(format!( "Parent item with key '{}' is not of type container", this_key ))); } } else { let parent_item = GvdbBuilderItemValue::Container(vec![this_key.clone()]); self.items.insert(last_key.to_string(), parent_item); } } if key == this_key { // The item we actually want to insert self.items.insert(key.to_string(), item); break; } last_key = Some(this_key.clone()); } } else { self.items.insert(key, item); } Ok(()) } /// Insert Value `item` for `key` /// /// ``` /// use zvariant::Value; /// let mut table_builder = gvdb::write::GvdbHashTableBuilder::new(); /// let variant = Value::new(123u32); /// table_builder.insert_value("variant_123", variant); /// ``` pub fn insert_value( &mut self, key: &(impl ToString + ?Sized), value: zvariant::Value<'a>, ) -> GvdbBuilderResult<()> { let item = GvdbBuilderItemValue::Value(value); self.insert_item_value(key, item) } /// Insert `item` for `key` where item needs to be `Into` /// /// ``` /// use zvariant::Value; /// let mut table_builder = gvdb::write::GvdbHashTableBuilder::new(); /// let value = 123u32; /// table_builder.insert("variant_123", value); /// ``` pub fn insert( &mut self, key: &(impl ToString + ?Sized), value: T, ) -> GvdbBuilderResult<()> where T: Into>, { let item = GvdbBuilderItemValue::Value(value.into()); self.insert_item_value(key, item) } /// Insert GVariant `item` for `key` /// /// ``` /// # #[cfg(feature = "glib")] /// # use glib::prelude::*; /// # /// let mut table_builder = gvdb::write::GvdbHashTableBuilder::new(); /// let variant = 123u32.to_variant(); /// table_builder.insert_gvariant("variant_123", variant); /// ``` #[cfg(feature = "glib")] pub fn insert_gvariant( &mut self, key: &(impl ToString + ?Sized), variant: glib::Variant, ) -> GvdbBuilderResult<()> { let item = GvdbBuilderItemValue::GVariant(variant); self.insert_item_value(key, item) } /// Convenience method to create a string type GVariant for `value` and insert it at `key` /// /// ``` /// # let mut table_builder = gvdb::write::GvdbHashTableBuilder::new(); /// table_builder.insert_string("string_key", "string_data"); /// ``` pub fn insert_string( &mut self, key: &(impl ToString + ?Sized), string: &(impl ToString + ?Sized), ) -> GvdbBuilderResult<()> { let variant = zvariant::Value::new(string.to_string()); self.insert_value(key, variant) } /// Convenience method to create a byte type GVariant for `value` and insert it at `key` /// /// ``` /// # let mut table_builder = gvdb::write::GvdbHashTableBuilder::new(); /// table_builder.insert_bytes("bytes", &[1, 2, 3, 4, 5]); /// ``` pub fn insert_bytes( &mut self, key: &(impl ToString + ?Sized), bytes: &'a [u8], ) -> GvdbBuilderResult<()> { let value = zvariant::Value::new(bytes); self.insert_value(key, value) } /// Insert an entire hash table at `key`. /// /// ``` /// # use zvariant::Value; /// # use gvdb::write::GvdbHashTableBuilder; /// let mut table_builder = GvdbHashTableBuilder::new(); /// let mut table_builder_2 = GvdbHashTableBuilder::new(); /// table_builder_2 /// .insert_value("int", Value::new(42u32)) /// .unwrap(); /// /// table_builder /// .insert_table("table", table_builder_2) /// .unwrap(); /// ``` pub fn insert_table( &mut self, key: &(impl ToString + ?Sized), table_builder: GvdbHashTableBuilder<'a>, ) -> GvdbBuilderResult<()> { let item = GvdbBuilderItemValue::TableBuilder(table_builder); self.insert_item_value(key, item) } /// The number of items contained in the hash table builder pub fn len(&self) -> usize { self.items.len() } /// Whether the hash table builder contains no items pub fn is_empty(&self) -> bool { self.items.is_empty() } pub(crate) fn build(mut self) -> GvdbBuilderResult> { let mut hash_table = SimpleHashTable::with_n_buckets(self.items.len()); let mut keys: Vec = self.items.keys().cloned().collect(); keys.sort(); for key in keys { let value = self.items.remove(&key).unwrap(); hash_table.insert(&key, value); } for (key, item) in hash_table.iter() { if let GvdbBuilderItemValue::Container(container) = &*item.value_ref() { for child in container { let child_item = hash_table.get(child); if let Some(child_item) = child_item { child_item.parent().replace(Some(item.clone())); } else { return Err(GvdbWriterError::Consistency(format!("Tried to set parent for child '{}' to '{}' but the child was not found.", child, key))); } } } } Ok(hash_table) } } impl<'a> Default for GvdbHashTableBuilder<'a> { fn default() -> Self { Self::new() } } #[derive(Debug)] struct GvdbChunk { // The pointer that points to the data where the chunk will be in memory in the finished file pointer: GvdbPointer, // We use a boxed slice because this guarantees that the size is not changed afterwards data: Box<[u8]>, } impl GvdbChunk { pub fn new(data: Box<[u8]>, pointer: GvdbPointer) -> Self { Self { pointer, data } } pub fn data_mut(&mut self) -> &mut [u8] { &mut self.data } pub fn into_data(self) -> Box<[u8]> { self.data } pub fn pointer(&self) -> GvdbPointer { self.pointer } } /// Create GVDB files /// /// # Example /// ``` /// use glib::prelude::*; /// use gvdb::write::{GvdbFileWriter, GvdbHashTableBuilder}; /// /// fn create_gvdb_file() { /// let mut file_writer = GvdbFileWriter::new(); /// let mut table_builder = GvdbHashTableBuilder::new(); /// table_builder /// .insert_string("string", "test string") /// .unwrap(); /// let file_data = file_writer.write_to_vec_with_table(table_builder).unwrap(); /// } /// ``` pub struct GvdbFileWriter { offset: usize, chunks: VecDeque, byteswap: bool, } impl GvdbFileWriter { /// Create a new instance configured for writing little endian data (preferred endianness) /// ``` /// let file_writer = gvdb::write::GvdbFileWriter::new(); /// ``` pub fn new() -> Self { #[cfg(target_endian = "little")] let byteswap = false; #[cfg(target_endian = "big")] let byteswap = true; Self::with_byteswap(byteswap) } /// Create a new instance configured for writing big endian data /// (not recommended for most use cases) /// ``` /// let file_writer = gvdb::write::GvdbFileWriter::new(); /// ``` pub fn for_big_endian() -> Self { #[cfg(target_endian = "little")] let byteswap = true; #[cfg(target_endian = "big")] let byteswap = false; Self::with_byteswap(byteswap) } /// Specify manually whether you want to swap the endianness of the file. The default is to /// always create a little-endian file fn with_byteswap(byteswap: bool) -> Self { let mut this = Self { offset: 0, chunks: Default::default(), byteswap, }; this.allocate_empty_chunk(size_of::(), 1); this } /// Allocate a chunk fn allocate_chunk_with_data( &mut self, data: Box<[u8]>, alignment: usize, ) -> (usize, &mut GvdbChunk) { // Align the data self.offset = align_offset(self.offset, alignment); // Calculate the pointer let offset_start = self.offset; let offset_end = offset_start + data.len(); let pointer = GvdbPointer::new(offset_start, offset_end); // Update the offset to the end of the chunk self.offset = offset_end; let chunk = GvdbChunk::new(data, pointer); self.chunks.push_back(chunk); let index = self.chunks.len() - 1; (index, &mut self.chunks[index]) } fn allocate_empty_chunk(&mut self, size: usize, alignment: usize) -> (usize, &mut GvdbChunk) { let data = vec![0; size].into_boxed_slice(); self.allocate_chunk_with_data(data, alignment) } fn add_value(&mut self, value: &zvariant::Value) -> GvdbBuilderResult<(usize, &mut GvdbChunk)> { #[cfg(target_endian = "little")] let le = true; #[cfg(target_endian = "big")] let le = false; let data: Box<[u8]> = if le && !self.byteswap || !le && self.byteswap { let context = zvariant::serialized::Context::new_gvariant(zvariant::LE, 0); Box::from(&*zvariant::to_bytes(context, value)?) } else { let context = zvariant::serialized::Context::new_gvariant(zvariant::BE, 0); Box::from(&*zvariant::to_bytes(context, value)?) }; Ok(self.allocate_chunk_with_data(data, 8)) } #[cfg(feature = "glib")] fn add_gvariant(&mut self, variant: &glib::Variant) -> (usize, &mut GvdbChunk) { let value = if self.byteswap { glib::Variant::from_variant(&variant.byteswap()) } else { glib::Variant::from_variant(variant) }; let normal = value.normal_form(); let data = normal.data(); self.allocate_chunk_with_data(data.to_vec().into_boxed_slice(), 8) } fn add_string(&mut self, string: &str) -> (usize, &mut GvdbChunk) { let data = string.to_string().into_boxed_str().into_boxed_bytes(); self.allocate_chunk_with_data(data, 1) } fn add_simple_hash_table( &mut self, table: SimpleHashTable, ) -> GvdbBuilderResult<(usize, &mut GvdbChunk)> { for (index, (_bucket, item)) in table.iter().enumerate() { item.set_assigned_index(index as u32); } let header = GvdbHashHeader::new(5, 0, table.n_buckets() as u32); let items_len = table.n_items() * size_of::(); let size = size_of::() + header.bloom_words_len() + header.buckets_len() + items_len; let hash_buckets_offset = size_of::() + header.bloom_words_len(); let hash_items_offset = hash_buckets_offset + header.buckets_len(); let (hash_table_chunk_index, hash_table_chunk) = self.allocate_empty_chunk(size, 4); let header = transmute_one_to_bytes(&header); hash_table_chunk.data_mut()[0..header.len()].copy_from_slice(header); let mut n_item = 0; for bucket in 0..table.n_buckets() { let hash_bucket_start = hash_buckets_offset + bucket * size_of::(); let hash_bucket_end = hash_bucket_start + size_of::(); self.chunks[hash_table_chunk_index].data[hash_bucket_start..hash_bucket_end] .copy_from_slice(u32::to_le_bytes(n_item as u32).as_slice()); for current_item in table.iter_bucket(bucket) { let parent = if let Some(parent) = &*current_item.parent_ref() { parent.assigned_index() } else { u32::MAX }; let key = if let Some(parent) = &*current_item.parent_ref() { current_item.key().strip_prefix(parent.key()).unwrap_or("") } else { current_item.key() }; if key.is_empty() { return Err(GvdbWriterError::Consistency(format!( "Item '{}' already exists in hash map or key is empty", current_item.key() ))); } let key_ptr = self.add_string(key).1.pointer(); let typ = current_item.value_ref().typ(); let value_ptr = match current_item.value().take() { GvdbBuilderItemValue::Value(value) => self.add_value(&value)?.1.pointer(), #[cfg(feature = "glib")] GvdbBuilderItemValue::GVariant(variant) => { self.add_gvariant(&variant).1.pointer() } GvdbBuilderItemValue::TableBuilder(tb) => { self.add_table_builder(tb)?.1.pointer() } GvdbBuilderItemValue::Container(children) => { let size = children.len() * size_of::(); let chunk = self.allocate_empty_chunk(size, 4).1; let mut offset = 0; for child in children { let child_item = table.get(&child); if let Some(child_item) = child_item { child_item.parent().replace(Some(current_item.clone())); chunk.data_mut()[offset..offset + size_of::()] .copy_from_slice(&u32::to_le_bytes( child_item.assigned_index(), )); offset += size_of::(); } else { return Err(GvdbWriterError::Consistency(format!( "Child item '{}' not found for parent: '{}'", child, key ))); } } chunk.pointer() } }; let hash_item = GvdbHashItem::new(current_item.hash(), parent, key_ptr, typ, value_ptr); let hash_item_start = hash_items_offset + n_item * size_of::(); let hash_item_end = hash_item_start + size_of::(); self.chunks[hash_table_chunk_index].data[hash_item_start..hash_item_end] .copy_from_slice(transmute_one_to_bytes(&hash_item)); n_item += 1; } } Ok(( hash_table_chunk_index, &mut self.chunks[hash_table_chunk_index], )) } fn add_table_builder( &mut self, table_builder: GvdbHashTableBuilder, ) -> GvdbBuilderResult<(usize, &mut GvdbChunk)> { self.add_simple_hash_table(table_builder.build()?) } fn file_size(&self) -> usize { self.chunks[self.chunks.len() - 1].pointer().end() as usize } fn serialize( mut self, root_chunk_index: usize, writer: &mut dyn Write, ) -> GvdbBuilderResult { let root_ptr = self .chunks .get(root_chunk_index) .ok_or_else(|| { GvdbWriterError::Consistency(format!( "Root chunk with id {} not found", root_chunk_index )) })? .pointer(); let header = GvdbHeader::new(self.byteswap, 0, root_ptr); self.chunks[0].data_mut()[0..size_of::()] .copy_from_slice(transmute_one_to_bytes(&header)); let mut size = 0; for chunk in self.chunks.into_iter() { // Align if size < chunk.pointer().start() as usize { let padding = chunk.pointer().start() as usize - size; size += padding; writer.write_all(&vec![0; padding])?; } size += chunk.pointer().size(); writer.write_all(&chunk.into_data())?; } Ok(size) } fn serialize_to_vec(self, root_chunk_index: usize) -> GvdbBuilderResult> { let mut vec = Vec::with_capacity(self.file_size()); self.serialize(root_chunk_index, &mut vec)?; Ok(vec) } /// Write the GVDB file into the provided [`std::io::Write`] pub fn write_with_table( mut self, table_builder: GvdbHashTableBuilder, writer: &mut dyn Write, ) -> GvdbBuilderResult { let index = self.add_table_builder(table_builder)?.0; self.serialize(index, writer) } /// Create a [`Vec`] with the GVDB file data pub fn write_to_vec_with_table( mut self, table_builder: GvdbHashTableBuilder, ) -> GvdbBuilderResult> { let index = self.add_table_builder(table_builder)?.0; self.serialize_to_vec(index) } } impl Default for GvdbFileWriter { fn default() -> Self { Self::new() } } #[cfg(test)] mod test { use super::*; use crate::read::{GvdbFile, GvdbHashItemType}; use matches::assert_matches; use std::borrow::Cow; use std::io::Cursor; use crate::test::{ assert_bytes_eq, assert_is_file_1, assert_is_file_2, byte_compare_file_1, byte_compare_file_2, }; #[allow(unused_imports)] use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; #[test] fn derives() { let ht_builder = GvdbHashTableBuilder::default(); println!("{:?}", ht_builder); let chunk = GvdbChunk::new(Box::new([0; 0]), GvdbPointer::NULL); assert!(format!("{:?}", chunk).contains("GvdbChunk")); } #[test] fn hash_table_builder1() { let mut builder = GvdbHashTableBuilder::new(); assert!(builder.is_empty()); builder.insert_string("string", "Test").unwrap(); builder .insert_value("123", zvariant::Value::new(123u32)) .unwrap(); assert!(!builder.is_empty()); assert_eq!(builder.len(), 2); let mut builder2 = GvdbHashTableBuilder::new(); builder2.insert_bytes("bytes", &[1, 2, 3, 4]).unwrap(); builder.insert_table("table", builder2).unwrap(); let table = builder.build().unwrap(); assert_eq!( table.get("string").unwrap().value_ref().value().unwrap(), &zvariant::Value::new("Test") ); assert_eq!( table.get("123").unwrap().value_ref().value().unwrap(), &zvariant::Value::new(123u32) ); let item = table.get("table").unwrap(); assert_matches!(item.value_ref().table_builder(), Some(_)); let val = item.value().take(); assert_matches!(val, GvdbBuilderItemValue::TableBuilder(..)); let GvdbBuilderItemValue::TableBuilder(tb) = val else { panic!("Invalid value"); }; let table2 = tb.build().unwrap(); let data: &[u8] = &[1, 2, 3, 4]; assert_eq!( table2.get("bytes").unwrap().value_ref().value().unwrap(), &zvariant::Value::new(data) ); } #[test] fn hash_table_builder2() { let mut builder = GvdbHashTableBuilder::new(); // invalid path builder.insert_string("string/", "collision").unwrap(); let err = builder.insert_string("string/test", "test").unwrap_err(); assert_matches!(err, GvdbWriterError::Consistency(_)); let mut builder = GvdbHashTableBuilder::with_path_separator(None); // invalid path but this isn't important as path handling is turned off builder.insert_string("string/", "collision").unwrap(); builder.insert_string("string/test", "test").unwrap(); } #[test] fn file_builder_file_1() { let mut file_builder = GvdbFileWriter::new(); let mut table_builder = GvdbHashTableBuilder::new(); let value1 = 1234u32; let value2 = 98765u32; let value3 = "TEST_STRING_VALUE"; let tuple_data = (value1, value2, value3); let variant = zvariant::Value::new(tuple_data); table_builder.insert_value("root_key", variant).unwrap(); let root_index = file_builder.add_table_builder(table_builder).unwrap().0; let bytes = file_builder.serialize_to_vec(root_index).unwrap(); let root = GvdbFile::from_bytes(Cow::Owned(bytes)).unwrap(); println!("{:?}", root); assert_is_file_1(&root); byte_compare_file_1(&root); } #[test] fn file_builder_file_2() { let mut file_builder = GvdbFileWriter::for_big_endian(); let mut table_builder = GvdbHashTableBuilder::new(); table_builder .insert_string("string", "test string") .unwrap(); let mut table_builder_2 = GvdbHashTableBuilder::new(); table_builder_2.insert("int", 42u32).unwrap(); table_builder .insert_table("table", table_builder_2) .unwrap(); let root_index = file_builder.add_table_builder(table_builder).unwrap().0; let bytes = file_builder.serialize_to_vec(root_index).unwrap(); let root = GvdbFile::from_bytes(Cow::Owned(bytes)).unwrap(); println!("{:?}", root); assert_is_file_2(&root); byte_compare_file_2(&root); } #[test] fn reproducible_build() { let mut last_data: Option> = None; for _ in 0..100 { let file_builder = GvdbFileWriter::new(); let mut table_builder = GvdbHashTableBuilder::new(); for num in 0..200 { let str = format!("{}", num); table_builder.insert_string(&str, &str).unwrap(); } let data = file_builder.write_to_vec_with_table(table_builder).unwrap(); if last_data.is_some() { assert_bytes_eq(&last_data.unwrap(), &data, "Reproducible builds"); } last_data = Some(data); } } #[test] fn big_endian() { let mut file_builder = GvdbFileWriter::for_big_endian(); let mut table_builder = GvdbHashTableBuilder::new(); let value1 = 1234u32; let value2 = 98765u32; let value3 = "TEST_STRING_VALUE"; let tuple_data = (value1, value2, value3); let variant = zvariant::Value::new(tuple_data); table_builder.insert_value("root_key", variant).unwrap(); let root_index = file_builder.add_table_builder(table_builder).unwrap().0; let bytes = file_builder.serialize_to_vec(root_index).unwrap(); // "GVariant" byteswapped at 32 bit boundaries is the header for big-endian GVariant files assert_eq!("raVGtnai", std::str::from_utf8(&bytes[0..8]).unwrap()); let root = GvdbFile::from_bytes(Cow::Owned(bytes)).unwrap(); println!("{:?}", root); assert_is_file_1(&root); } #[test] fn container() { let mut file_builder = GvdbFileWriter::new(); let mut table_builder = GvdbHashTableBuilder::new(); table_builder .insert_string("contained/string", "str") .unwrap(); let root_index = file_builder.add_table_builder(table_builder).unwrap().0; let bytes = file_builder.serialize_to_vec(root_index).unwrap(); let root = GvdbFile::from_bytes(Cow::Owned(bytes)).unwrap(); let container_item = root .hash_table() .unwrap() .get_hash_item("contained/") .unwrap(); assert_eq!(container_item.typ().unwrap(), GvdbHashItemType::Container); println!("{:?}", root); } #[test] fn missing_root() { let file = GvdbFileWriter::new(); assert_matches!( file.serialize_to_vec(1), Err(GvdbWriterError::Consistency(_)) ); } #[test] fn missing_child() { let mut table = GvdbHashTableBuilder::new(); let item = GvdbBuilderItemValue::Container(vec!["missing".to_string()]); table.insert_item_value("test", item).unwrap(); assert_matches!(table.build(), Err(GvdbWriterError::Consistency(_))); } #[test] fn empty_key() { let mut table = GvdbHashTableBuilder::new(); table.insert_string("", "test").unwrap(); let file = GvdbFileWriter::new(); let err = file.write_to_vec_with_table(table).unwrap_err(); assert_matches!(err, GvdbWriterError::Consistency(_)) } #[test] fn remove_child() { let mut table_builder = GvdbHashTableBuilder::new(); table_builder.insert_string("test/test", "test").unwrap(); table_builder.items.remove("test/test"); let file = GvdbFileWriter::new(); let err = file.write_to_vec_with_table(table_builder).unwrap_err(); assert_matches!(err, GvdbWriterError::Consistency(_)) } #[test] fn remove_child2() { let mut table_builder = GvdbHashTableBuilder::new(); table_builder.insert_string("test/test", "test").unwrap(); let mut table = table_builder.build().unwrap(); table.remove("test/test"); let mut file = GvdbFileWriter::new(); let err = file.add_simple_hash_table(table).unwrap_err(); assert_matches!(err, GvdbWriterError::Consistency(_)) } #[test] fn io_error() { let file = GvdbFileWriter::default(); // This buffer is intentionally too small to result in I/O error let buffer = [0u8; 10]; let mut cursor = Cursor::new(buffer); let mut table = GvdbHashTableBuilder::new(); table.insert("test", "test").unwrap(); let err = file.write_with_table(table, &mut cursor).unwrap_err(); assert_matches!(err, GvdbWriterError::Io(_, _)); assert!(format!("{}", err).contains("I/O error")); assert!(format!("{:?}", err).contains("I/O error")); } } #[cfg(all(feature = "glib", test))] mod test_glib { use crate::write::hash::SimpleHashTable; use crate::write::item::GvdbBuilderItemValue; use crate::write::{GvdbFileWriter, GvdbHashTableBuilder}; use glib::prelude::*; #[test] fn simple_hash_table() { let mut table: SimpleHashTable = SimpleHashTable::with_n_buckets(10); let item = GvdbBuilderItemValue::GVariant("test".to_variant()); table.insert("test", item); assert_eq!(table.n_items(), 1); assert_eq!( table.get("test").unwrap().value_ref().gvariant().unwrap(), &"test".to_variant() ); } #[test] fn hash_table_builder() { let mut table = GvdbHashTableBuilder::new(); table.insert_gvariant("test", "test".to_variant()).unwrap(); let simple_ht = table.build().unwrap(); assert_eq!( simple_ht .get("test") .unwrap() .value_ref() .gvariant() .unwrap(), &"test".to_variant() ); } #[test] fn file_writer() { for byteswap in [true, false] { let mut table = GvdbHashTableBuilder::default(); table.insert_gvariant("test", "test".to_variant()).unwrap(); let writer = GvdbFileWriter::with_byteswap(byteswap); let _ = writer.write_to_vec_with_table(table).unwrap(); } } } ��������������gvdb-0.6.1/src/write/hash.rs������������������������������������������������������������������������0000644�0000000�0000000�00000021757�10461020230�0014000�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::util::djb_hash; use crate::write::item::{GvdbBuilderItem, GvdbBuilderItemValue}; use std::rc::Rc; #[derive(Debug)] pub struct SimpleHashTable<'a> { buckets: Vec>>>, n_items: usize, } impl<'a> SimpleHashTable<'a> { pub fn with_n_buckets(n_buckets: usize) -> Self { let mut buckets = Vec::with_capacity(n_buckets); buckets.resize_with(n_buckets, || None); Self { buckets, n_items: 0, } } pub fn n_buckets(&self) -> usize { self.buckets.len() } pub fn n_items(&self) -> usize { self.n_items } fn hash_bucket(&self, hash_value: u32) -> usize { (hash_value % self.buckets.len() as u32) as usize } pub fn insert(&mut self, key: &str, item: GvdbBuilderItemValue<'a>) -> Rc> { let hash_value = djb_hash(key); let bucket = self.hash_bucket(hash_value); let item = Rc::new(GvdbBuilderItem::new(key, hash_value, item)); let replaced_item = std::mem::replace(&mut self.buckets[bucket], Some(item.clone())); if let Some(replaced_item) = replaced_item { if replaced_item.key() == key { // Replace self.buckets[bucket] .as_ref() .unwrap() .next() .replace(replaced_item.next().take()); } else { // Insert self.buckets[bucket] .as_ref() .unwrap() .next() .replace(Some(replaced_item)); self.n_items += 1; } } else { // Insert to empty bucket self.n_items += 1; } item } #[allow(dead_code)] /// Remove the item with the specified key pub fn remove(&mut self, key: &str) -> bool { let hash_value = djb_hash(key); let bucket = self.hash_bucket(hash_value); // Remove the item if it already exists if let Some((previous, item)) = self.get_from_bucket(key, bucket) { if let Some(previous) = previous { previous.next().replace(item.next().take()); } else { self.buckets[bucket] = item.next().take(); } self.n_items -= 1; true } else { false } } fn get_from_bucket( &self, key: &str, bucket: usize, ) -> Option<(Option>>, Rc>)> { let mut item = self.buckets.get(bucket)?.clone(); let mut previous = None; while let Some(current_item) = item { if current_item.key() == key { return Some((previous, current_item)); } else { previous = Some(current_item.clone()); item = current_item.next().borrow().clone(); } } None } pub fn get(&self, key: &str) -> Option>> { let hash_value = djb_hash(key); let bucket = self.hash_bucket(hash_value); self.get_from_bucket(key, bucket).map(|r| r.1) } pub fn iter(&self) -> SimpleHashTableIter<'_, 'a> { SimpleHashTableIter { hash_table: self, bucket: 0, last_item: None, } } pub fn iter_bucket(&self, bucket: usize) -> SimpleHashTableBucketIter<'_, 'a> { SimpleHashTableBucketIter { hash_table: self, bucket, last_item: None, } } } pub struct SimpleHashTableBucketIter<'it, 'h> { hash_table: &'it SimpleHashTable<'h>, bucket: usize, last_item: Option>>, } impl<'it, 'h> Iterator for SimpleHashTableBucketIter<'it, 'h> { type Item = Rc>; fn next(&mut self) -> Option { if let Some(last_item) = self.last_item.clone() { // First check if there are more items in this bucket if let Some(next_item) = &*last_item.next().borrow() { // Next item in the same bucket self.last_item = Some(next_item.clone()); Some(next_item.clone()) } else { // Last item in the bucket, return None } } else if let Some(Some(item)) = self.hash_table.buckets.get(self.bucket).cloned() { // We found something: Bucket exists and is not empty self.last_item = Some(item.clone()); Some(item.clone()) } else { None } } } pub struct SimpleHashTableIter<'it, 'h> { hash_table: &'it SimpleHashTable<'h>, bucket: usize, last_item: Option>>, } impl<'it, 'h> Iterator for SimpleHashTableIter<'it, 'h> { type Item = (usize, Rc>); fn next(&mut self) -> Option { if let Some(last_item) = self.last_item.clone() { // First check if there are more items in this bucket if let Some(next_item) = &*last_item.next().borrow() { // Next item in the same bucket self.last_item = Some(next_item.clone()); return Some((self.bucket, next_item.clone())); } else { // Last item in the bucket, check the next bucket self.bucket += 1; } } while let Some(bucket_item) = self.hash_table.buckets.get(self.bucket) { self.last_item = None; // This bucket might be empty if let Some(item) = bucket_item { // We found something self.last_item = Some(item.clone()); return Some((self.bucket, item.clone())); } else { // Empty bucket, continue with next bucket self.bucket += 1; } } // Nothing left None } } #[cfg(test)] mod test { use std::collections::HashSet; use matches::assert_matches; use crate::write::hash::SimpleHashTable; use crate::write::item::GvdbBuilderItemValue; #[test] fn derives() { let table = SimpleHashTable::with_n_buckets(1); assert!(format!("{:?}", table).contains("SimpleHashTable")); } #[test] fn simple_hash_table() { let mut table: SimpleHashTable = SimpleHashTable::with_n_buckets(10); let item = GvdbBuilderItemValue::Value(zvariant::Value::new("test_overwrite")); table.insert("test", item); assert_eq!(table.n_items(), 1); let item2 = GvdbBuilderItemValue::Value(zvariant::Value::new("test")); table.insert("test", item2); assert_eq!(table.n_items(), 1); assert_eq!( table.get("test").unwrap().value_ref().value().unwrap(), &"test".into() ); } #[test] fn simple_hash_table_2() { let mut table: SimpleHashTable = SimpleHashTable::with_n_buckets(10); for index in 0..20 { table.insert(&format!("{}", index), zvariant::Value::new(index).into()); } assert_eq!(table.n_items(), 20); for index in 0..20 { assert_eq!( zvariant::Value::new(index), *table .get(&format!("{}", index)) .unwrap() .value_ref() .value() .unwrap() ); } for index in 0..10 { let index = index * 2; assert!(table.remove(&format!("{}", index))); } for index in 0..20 { let item = table.get(&format!("{}", index)); assert_eq!(index % 2 == 1, item.is_some()); } assert!(!table.remove("50")); } #[test] fn simple_hash_table_iter() { let mut table: SimpleHashTable = SimpleHashTable::with_n_buckets(10); for index in 0..20 { table.insert(&format!("{}", index), zvariant::Value::new(index).into()); } let mut iter = table.iter(); for _ in 0..20 { let value: i32 = iter .next() .unwrap() .1 .value() .borrow() .value() .unwrap() .try_into() .unwrap(); assert_matches!(value, 0..=19); } } #[test] fn simple_hash_table_bucket_iter() { let mut table: SimpleHashTable = SimpleHashTable::with_n_buckets(10); for index in 0..20 { table.insert(&format!("{}", index), zvariant::Value::new(index).into()); } let mut values: HashSet = (0..20).collect(); for bucket in 0..table.n_buckets() { let mut iter = table.iter_bucket(bucket); while let Some(next) = iter.next() { let num: i32 = next.value().borrow().value().unwrap().try_into().unwrap(); assert_eq!(values.remove(&num), true); } } } } �����������������gvdb-0.6.1/src/write/item.rs������������������������������������������������������������������������0000644�0000000�0000000�00000014316�10461020230�0014004�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::read::GvdbHashItemType; use crate::write::file::GvdbHashTableBuilder; use std::cell::{Cell, Ref, RefCell}; use std::rc::Rc; #[derive(Debug)] pub enum GvdbBuilderItemValue<'a> { // A zvariant::Value Value(zvariant::Value<'a>), // A glib::Variant #[cfg(feature = "glib")] GVariant(glib::Variant), TableBuilder(GvdbHashTableBuilder<'a>), // A child container with no additional value Container(Vec), } impl<'a> Default for GvdbBuilderItemValue<'a> { fn default() -> Self { Self::Container(Vec::new()) } } #[allow(dead_code)] impl<'a> GvdbBuilderItemValue<'a> { pub fn typ(&self) -> GvdbHashItemType { match self { GvdbBuilderItemValue::Value(_) => GvdbHashItemType::Value, #[cfg(feature = "glib")] GvdbBuilderItemValue::GVariant(_) => GvdbHashItemType::Value, GvdbBuilderItemValue::TableBuilder(_) => GvdbHashItemType::HashTable, GvdbBuilderItemValue::Container(_) => GvdbHashItemType::Container, } } pub fn value(&self) -> Option<&zvariant::Value> { match self { GvdbBuilderItemValue::Value(value) => Some(value), _ => None, } } #[cfg(feature = "glib")] pub fn gvariant(&self) -> Option<&glib::Variant> { match self { GvdbBuilderItemValue::GVariant(variant) => Some(variant), _ => None, } } #[allow(dead_code)] pub fn table_builder(&self) -> Option<&GvdbHashTableBuilder> { match self { GvdbBuilderItemValue::TableBuilder(tb) => Some(tb), _ => None, } } pub fn container(&self) -> Option<&Vec> { match self { GvdbBuilderItemValue::Container(children) => Some(children), _ => None, } } } impl<'a> From> for GvdbBuilderItemValue<'a> { fn from(var: zvariant::Value<'a>) -> Self { GvdbBuilderItemValue::Value(var) } } #[cfg(feature = "glib")] impl<'a> From for GvdbBuilderItemValue<'a> { fn from(var: glib::Variant) -> Self { GvdbBuilderItemValue::GVariant(var) } } impl<'a> From> for GvdbBuilderItemValue<'a> { fn from(tb: GvdbHashTableBuilder<'a>) -> Self { GvdbBuilderItemValue::TableBuilder(tb) } } #[derive(Debug)] pub struct GvdbBuilderItem<'a> { // The key string of the item key: String, // The djb hash hash: u32, // An arbitrary data container value: RefCell>, // The assigned index for the gvdb file assigned_index: Cell, // The parent item of this builder item parent: RefCell>>>, // The next item in the hash bucket next: RefCell>>>, } impl<'a> GvdbBuilderItem<'a> { pub fn new(key: &str, hash: u32, value: GvdbBuilderItemValue<'a>) -> Self { let key = key.to_string(); Self { key, hash, value: RefCell::new(value), assigned_index: Cell::new(u32::MAX), parent: Default::default(), next: Default::default(), } } pub fn key(&self) -> &str { &self.key } pub fn hash(&self) -> u32 { self.hash } pub fn next(&self) -> &RefCell>>> { &self.next } pub fn value(&self) -> &RefCell> { &self.value } pub fn value_ref(&self) -> Ref> { self.value.borrow() } pub fn parent(&self) -> &RefCell>>> { &self.parent } pub fn parent_ref(&self) -> Ref>>> { self.parent.borrow() } pub fn assigned_index(&self) -> u32 { self.assigned_index.get() } pub fn set_assigned_index(&self, index: u32) { self.assigned_index.set(index); } } #[cfg(test)] mod test { use crate::read::GvdbHashItemType; use crate::write::item::{GvdbBuilderItem, GvdbBuilderItemValue}; use crate::write::GvdbHashTableBuilder; use matches::assert_matches; #[test] fn derives() { let value1: zvariant::Value = "test".into(); let item1 = GvdbBuilderItemValue::Value(value1); println!("{:?}", item1); } #[test] fn item_value() { let value1: zvariant::Value = "test".into(); let item1 = GvdbBuilderItemValue::Value( value1 .try_clone() .expect("Value to not contain a file descriptor"), ); assert_eq!(item1.typ(), GvdbHashItemType::Value); assert_eq!(item1.value().unwrap(), &value1); #[cfg(feature = "glib")] assert_matches!(item1.gvariant(), None); let value2 = GvdbHashTableBuilder::new(); let item2 = GvdbBuilderItemValue::from(value2); assert_eq!(item2.typ(), GvdbHashItemType::HashTable); assert!(item2.table_builder().is_some()); assert_matches!(item2.container(), None); let value3 = vec!["test".to_string(), "test2".to_string()]; let item3 = GvdbBuilderItemValue::Container(value3.clone()); assert_eq!(item3.typ(), GvdbHashItemType::Container); assert_eq!(item3.container().unwrap(), &value3); assert_matches!(item3.table_builder(), None); } #[test] fn builder_item() { let value1: zvariant::Value = "test".into(); let item1 = GvdbBuilderItemValue::Value(value1); let item = GvdbBuilderItem::new("test", 0, item1); println!("{:?}", item); assert_eq!(item.key(), "test"); assert_matches!(&*item.value().borrow(), GvdbBuilderItemValue::Value(_)); } } #[cfg(all(feature = "glib", test))] mod test_glib { use crate::read::GvdbHashItemType; use crate::write::item::GvdbBuilderItemValue; use glib::prelude::*; use matches::assert_matches; #[test] fn item_value() { let value1 = "test".to_variant(); let item1 = GvdbBuilderItemValue::from(value1.clone()); assert_eq!(item1.typ(), GvdbHashItemType::Value); assert_eq!(item1.gvariant().unwrap(), &value1); assert_matches!(item1.value(), None); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/src/write.rs�����������������������������������������������������������������������������0000644�0000000�0000000�00000000225�10461020230�0013040�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod error; mod file; mod hash; mod item; pub use error::{GvdbBuilderResult, GvdbWriterError}; pub use file::{GvdbFileWriter, GvdbHashTableBuilder}; ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/gresource/icons/scalable/actions/online-symbolic.svg���������������������������0000644�0000000�0000000�00000002556�10461020230�0024613�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ ��������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/gresource/icons/scalable/actions/send-symbolic.svg�����������������������������0000644�0000000�0000000�00000000531�10461020230�0024247�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ �����������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/gresource/json/test.json�������������������������������������������������������0000644�0000000�0000000�00000000063�10461020230�0017260�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ "test_string", 42, { "bool": true } ]�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/gresource/test.css�������������������������������������������������������������0000644�0000000�0000000�00000000047�10461020230�0016130�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.test { background-color: black; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/gresource/test3.gresource.xml��������������������������������������������������0000644�0000000�0000000�00000001002�10461020230�0020210�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ icons/scalable/actions/online-symbolic.svg icons/scalable/actions/send-symbolic.svg json/test.json test.css ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/test1.gvdb���������������������������������������������������������������������0000644�0000000�0000000�00000000150�10461020230�0014340�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������GVariant�����������<������(�������ÑÚÿÿÿÿ<����v�H���h���root_key����Ò��Í�TEST_STRING_VALUE��(uus)������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/test2.gvdb���������������������������������������������������������������������0000644�0000000�0000000�00000000246�10461020230�0014347�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������raVGtnai�����������X������(����������ü¯“ÿÿÿÿX����v�`���n���úhÿÿÿÿn����H�t���˜���string��test string��stable����(�������0€ˆ ÿÿÿÿ˜����v� ���¦���int��������*�u����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������gvdb-0.6.1/test-data/test3.gresource����������������������������������������������������������������0000644�0000000�0000000�00000004416�10461020230�0015427�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������GVariant�����������p�����( ������������������������������� ��� ��� ���`ØÊÔ���p���v�x��³��‘)ó7���³���v�È��Õ��©ˆ~³���Õ���L�Ü��ì��ÒuÊa���ì���L�ô��ø��:†áÎ���ø�� �v���9��ZëÏ ���9���L�<��@��ºý"í���@���L�H��L��úD  ���L�� �L�X��\��Ôµ�ÿÿÿÿ\���L�`��d��Tˆ¢—���d���L�l��p��&Á }���p���L�x��|��ŠUè���|���v��� ��test.css'������xÚÓ+I-.Q¨æR�‚¤Ääìô¢üÒ¼Ýäüœü"+…¤ 5W-�û½ £�(uuay)send-symbolic.svg����Y�����xÚ-Ûj…0EßýŠ!}vÌýr0èC¿ ý€Òc5à£é×w´…@BØk {êû1°·Ï5ÍSd9ƒvúšiê"ûx+=»7EQ¯{ÖÈúm[nU•sƬp~v•äœW”`з©ë7Ùå`°§6¿ÎGd8K‡AN­ÿ4@½|n=<"AôA%@Â�%†n¥sJkØÁ ’Z Oÿ½ôB(%jg½ ŽÖraìùræÒ °.Ø�TN„Ó¾C©«À/Dp$–4•·N„ ¨NµCë/‹Æ¿Y Åþ‘*Ѳ¾Ó0Dö"[¥µ«š¢>7ÑüÊT_�(uuay)test/�� ������ �������json/������test.json�������!�������["test_string",42,{"bool":true}] ��(uuay)rs/���actions/���scalable/������/��� ���icons/�����gvdb/������online-symbolic.svg�n������ ��(uuay)������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������