sfv/
lib.rs

1/*
2Copyright (c) 2020 aspen <luxx4x@protonmail.com>
3This software is provided 'as-is', without any express or implied warranty. In no event will
4the authors be held liable for any damages arising from the use of this software.
5Permission is granted to anyone to use this software for any purpose,
6including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
7	1. The origin of this software must not be misrepresented; you must not claim that you wrote
8		the original software. If you use this software in a product, an acknowledgment in the
9		product documentation would be appreciated but is not required.
10	2. Altered source versions must be plainly marked as such,
11		and must not be misrepresented as being the original software.
12	3. This notice may not be removed or altered from any source distribution.
13*/
14
15#![forbid(unsafe_code)]
16#![deny(
17	clippy::complexity,
18	clippy::correctness,
19	clippy::perf,
20	clippy::style,
21	broken_intra_doc_links
22)]
23
24use std::{
25	collections::BTreeMap,
26	convert::AsRef,
27	fmt::{self, Display},
28	path::{Path, PathBuf},
29};
30
31#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Default)]
32pub struct Sfv {
33	/// The file paths and their checksums/hashes
34	pub files: BTreeMap<PathBuf, Vec<u8>>,
35}
36
37impl Sfv {
38	/// Create a new, empty [Sfv] object
39	pub fn new() -> Self {
40		Self::default()
41	}
42
43	/// Convert a SFV file in string format to a [Sfv] object.
44	///
45	/// ```rust
46	/// use sfv::Sfv;
47	/// use std::collections::BTreeMap;
48	/// use std::path::PathBuf;
49	///
50	/// let sfv_file = r#"
51	///  ; This is a comment
52	///   file_one.zip   c45ad668
53	///   file_two.zip   7903b8e6
54	///   file_three.zip e99a65fb
55	/// "#;
56	///
57	/// let sfv = Sfv::from_sfv("/test", sfv_file).unwrap();
58	/// assert_eq!(
59	///     *sfv.files.get(&PathBuf::from("/test/file_one.zip")).unwrap(),
60	///     vec![0xc4, 0x5a, 0xd6, 0x68]
61	/// )
62	/// ```
63	pub fn from_sfv<P: AsRef<Path>, S: AsRef<str>>(base_directory: P, sfv: S) -> Result<Self, ()> {
64		Self::from_sfv_impl(base_directory.as_ref().to_path_buf(), sfv.as_ref())
65	}
66
67	fn from_sfv_impl(base_directory: PathBuf, sfv: &str) -> Result<Self, ()> {
68		let mut files: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
69		sfv
70			// Trim the file's training/leading whitespace
71			.trim()
72			// Split the file into each line
73			.lines()
74			// Trim all the leading/trailing whitespace
75			.map(str::trim)
76			// Filter out all comments
77			.filter(|line| !line.starts_with(';'))
78			// Now we do the actual parsing
79			.try_for_each(|line| -> Result<(), ()> {
80				// Split the line by it's whitespace (spaces/tabs/etc)
81				let split = line.split_ascii_whitespace().collect::<Vec<&str>>();
82				// If there's not at least two entries, skip.
83				if split.len() < 2 {
84					return Ok(());
85				}
86				let (file, checksum) = (
87					base_directory.join(split[0]),
88					hex::decode(split[1]).map_err(|_| ())?,
89				);
90				files.insert(file, checksum);
91				Ok(())
92			})?;
93		Ok(Self { files })
94	}
95
96	/// Adds a file and checksum entry to the [Sfv].
97	///
98	/// ```rust
99	/// use sfv::Sfv;
100	/// use std::path::PathBuf;
101	///
102	/// let mut sfv = Sfv::new();
103	/// let path = PathBuf::from("/test.txt");
104	/// let checksum: Vec<u8> = vec![0x42, 0x42, 0x42, 0x42];
105	/// sfv.add_file(&path, &checksum);
106	///
107	/// assert_eq!(
108	///     *sfv.files.get(&PathBuf::from("/test.txt")).unwrap(),
109	///     vec![0x42, 0x42, 0x42, 0x42]
110	/// )
111	/// ```
112	pub fn add_file<P: AsRef<Path>, D: AsRef<[u8]>>(&mut self, path: P, data: D) {
113		self.add_file_impl(path.as_ref().to_path_buf(), data.as_ref())
114	}
115
116	fn add_file_impl(&mut self, path: PathBuf, data: &[u8]) {
117		self.files.insert(path, data.to_vec());
118	}
119}
120
121impl Display for Sfv {
122	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123		writeln!(f, "; Generated by sfv-rs {}", env!("CARGO_PKG_VERSION"))?;
124		self.files
125			.iter()
126			.try_for_each(|(path, checksum)| -> fmt::Result {
127				let path = path.to_string_lossy();
128				let checksum = hex::encode(checksum);
129				writeln!(f, "{}    {}", path, checksum)
130			})
131	}
132}