diff --git a/.gitignore b/.gitignore index ad67955..0728338 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e3e1433 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,328 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "commands" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688226b9769bbf11a9d82a94fb4adda15793a6023d33a8ca7695dbafec6f4123" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "huffman-compression-rs" +version = "0.1.0" +dependencies = [ + "bit-vec", + "clap", + "commands", + "lock", + "rayon", + "rmp-serde", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058cf9c2b0daa80dafc6d65b594ea04db8050522cfbf39f5d5aef2184f2a5a55" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..67ae517 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "huffman-compression-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +bit-vec = { version = "0.9.1", features = ["serde"] } +clap = { version = "4.6.1", features = ["derive"] } +commands = "0.0.5" +lock = "0.1.0" +rayon = "1.12.0" +rmp-serde = "1.3.1" +serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/compression.rs b/src/compression.rs new file mode 100644 index 0000000..93787e2 --- /dev/null +++ b/src/compression.rs @@ -0,0 +1,166 @@ +use bit_vec::BitVec; +use rayon::prelude::*; +use rmp_serde; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, hash::Hash}; + +use crate::huffman::{self, Tree}; +use Tree::*; + +#[derive(serde::Serialize, serde::Deserialize)] +struct CompressedData { + encoder: HashMap, + data: Vec, +} + +pub fn compress<'a, T, FreqsF, TokenExtractor, TokensIter>( + lines: &'a [String], + get_freqs: FreqsF, + line_to_tokens: TokenExtractor, +) -> Result, Box> +where + T: Clone + Eq + Hash + Send + Sync + Serialize, + FreqsF: Fn(&'a [String]) -> HashMap, + TokenExtractor: Fn(&'a str) -> TokensIter + Send + Sync, + TokensIter: Iterator, +{ + let freqs = get_freqs(lines); + let tree = huffman::huffman_tree(&freqs); + let encoder = tree.to_encoder(); + + let data = lines + .par_iter() + .map(|line| { + let mut bits = BitVec::new(); + for token in line_to_tokens(line) { + bits.extend(encoder.get(&token).unwrap().iter()); + } + bits + }) + .collect(); + + let compressed_data = CompressedData { encoder, data }; + rmp_serde::encode::to_vec(&compressed_data).map_err(|err| err.into()) +} + +pub fn extract<'a, T, F>( + data: &'a [u8], + tokens_to_line: F, +) -> Result, Box> +where + T: Clone + Eq + Hash + Send + Sync + Deserialize<'a>, + F: Fn(Vec) -> String + Send + Sync, +{ + let CompressedData { encoder, data }: CompressedData = rmp_serde::decode::from_slice(data)?; + + let decoder = build_decoder(&encoder); + let lines = data + .par_iter() + .map(|line| { + let mut tokens = Vec::new(); + let mut node = &decoder; + + for bit in line.iter() { + node = if bit { + node.right.as_ref().expect("invalid Huffman bit sequence") + } else { + node.left.as_ref().expect("invalid Huffman bit sequence") + }; + + if let Some(token) = &node.token { + tokens.push(token.clone()); + node = &decoder; + } + } + tokens_to_line(tokens) + }) + .collect(); + + Ok(lines) +} + +struct DecoderNode { + token: Option, + left: Option>>, + right: Option>>, +} + +impl Default for DecoderNode { + fn default() -> Self { + DecoderNode { + token: None, + left: None, + right: None, + } + } +} + +fn build_decoder(encoder: &HashMap) -> DecoderNode { + let mut root = DecoderNode::default(); + + for (token, code) in encoder { + let mut node = &mut root; + for bit in code.iter() { + node = if bit { + node.right.get_or_insert_with(|| Box::new(DecoderNode::default())) + } else { + node.left.get_or_insert_with(|| Box::new(DecoderNode::default())) + }; + } + node.token = Some(token.clone()); + } + + root +} + +impl Tree { + pub fn to_encoder(&self) -> HashMap { + let mut encoder = HashMap::new(); + + let mut stack = vec![(self, BitVec::new())]; + while let Some((node, path)) = stack.pop() { + match node { + Leaf { token, .. } => { + encoder.insert(token.clone(), path.clone()); + } + Node { left, right, .. } => { + let mut left_path = path.clone(); + left_path.push(false); + stack.push((left, left_path)); + + let mut right_path = path.clone(); + right_path.push(true); + stack.push((right, right_path)); + } + } + } + + encoder + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::freq::{char_frequencies, word_frequencies}; + + #[test] + fn compress_decompress_test() { + let lines = vec![ + "hey there! nice to meet you.".to_string(), + "Serde is a framework for serializing and deserializing Rust data structures" + .to_string(), + ]; + + let data = compress(&lines, char_frequencies, |line| line.chars()).unwrap(); + let res_lines = extract(&data, |x: Vec| x.into_iter().collect()).unwrap(); + assert_eq!(&lines, &res_lines); + + let data = compress(&lines, word_frequencies, |line| { + line.split_ascii_whitespace().map(|token| token.to_string()) + }) + .unwrap(); + let res_lines = extract(&data, |x: Vec| x.join(" ")).unwrap(); + assert_eq!(&lines, &res_lines); + } +} \ No newline at end of file diff --git a/src/freq.rs b/src/freq.rs new file mode 100644 index 0000000..47d23bf --- /dev/null +++ b/src/freq.rs @@ -0,0 +1,48 @@ +use rayon::prelude::*; +use std::collections::HashMap; + +pub fn char_frequencies(lines: &[String]) -> HashMap { + lines + .par_iter() + .fold( + HashMap::new, + |mut freqs: HashMap<_, _>, line: &String| { + for ch in line.chars() { + *freqs.entry(ch).or_insert(0) += 1; + } + freqs + }, + ) + .reduce( + HashMap::new, + |mut freqs1, freqs2| { + freqs2 + .into_iter() + .for_each(|(ch, n)| *freqs1.entry(ch).or_insert(0) += n); + freqs1 + }, + ) +} + +pub fn word_frequencies(lines: &[String]) -> HashMap { + lines + .par_iter() + .fold( + HashMap::new, + |mut freqs: HashMap<_, _>, line: &String| { + for word in line.split_ascii_whitespace() { + *freqs.entry(word.to_string()).or_insert(0) += 1; + } + freqs + }, + ) + .reduce( + HashMap::new, + |mut freqs1, freqs2| { + freqs2 + .into_iter() + .for_each(|(word, n)| *freqs1.entry(word).or_insert(0) += n); + freqs1 + }, + ) +} \ No newline at end of file diff --git a/src/huffman.rs b/src/huffman.rs new file mode 100644 index 0000000..fafe9f0 --- /dev/null +++ b/src/huffman.rs @@ -0,0 +1,158 @@ +use std::{ + cmp::Reverse, + collections::{BinaryHeap, HashMap}, +}; +use Tree::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Tree { + Leaf { + freq: u64, + token: T, + }, + Node { + freq: u64, + left: Box>, + right: Box>, + }, +} + +#[allow(dead_code)] +impl Tree { + pub fn freq(&self) -> u64 { + match self { + Leaf { freq, .. } => *freq, + Node { freq, .. } => *freq, + } + } + + pub fn token(&self) -> Option { + match self { + Leaf { token, .. } => Some(token.clone()), + Node { .. } => None, + } + } + + pub fn left(&self) -> Option<&Tree> { + match self { + Node { left, .. } => Some(left), + Leaf { .. } => None, + } + } + + pub fn right(&self) -> Option<&Tree> { + match self { + Node { right, .. } => Some(right), + Leaf { .. } => None, + } + } +} + +impl Ord for Tree { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.freq().cmp(&other.freq()) + } +} + +impl PartialOrd for Tree { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +pub fn huffman_tree(freqs: &HashMap) -> Tree { + let mut heap = BinaryHeap::new(); + for (token, freq) in freqs { + let (freq, token) = (*freq, token.clone()); + heap.push(Reverse(Leaf { freq, token })) + } + + while heap.len() > 1 { + let node1 = heap.pop().unwrap().0; + let node2 = heap.pop().unwrap().0; + + let merged_node = Node { + freq: node1.freq() + node2.freq(), + left: Box::new(node1), + right: Box::new(node2), + }; + heap.push(Reverse(merged_node)); + } + + heap.pop().unwrap().0 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::freq::char_frequencies; + + #[test] + fn learn_frequencies_test() { + let input = vec!["this is an epic sentence".to_string(), "xyz ".to_string()]; + let freqs = char_frequencies(&input); + assert_eq!(freqs[&' '], 5); + assert_eq!(freqs[&'t'], 2); + assert_eq!(freqs[&'i'], 3); + assert_eq!(freqs[&'p'], 1); + assert_eq!(freqs[&'z'], 1); + assert_eq!(freqs.keys().len(), 13); + } + + #[test] + fn huffman_tree_test() { + let mut freqs = HashMap::new(); + freqs.insert('a', 40); + freqs.insert('b', 35); + freqs.insert('c', 20); + freqs.insert('d', 5); + + let tree = huffman_tree(&freqs); + assert_eq!(tree.freq(), 100); + + // the most frequent character only requires 1 bit + assert_eq!(tree.left().and_then(|n| n.token()), Some('a')); + assert_eq!(tree.left().map(|n| n.freq()), Some(40)); + + // the second most frequent character requires 2 bits + assert_eq!( + tree.right().and_then(|t| t.right()).and_then(|n| n.token()), + Some('b') + ); + assert_eq!( + tree.right().and_then(|t| t.right()).map(|n| n.freq()), + Some(35) + ); + + // the least frequent characters require 3 bits + assert_eq!( + tree.right() + .and_then(|t| t.left()) + .and_then(|t| t.left()) + .and_then(|n| n.token()), + Some('d') + ); + assert_eq!( + tree.right() + .and_then(|t| t.left()) + .and_then(|t| t.left()) + .map(|n| n.freq()), + Some(5) + ); + + assert_eq!( + tree.right() + .and_then(|t| t.left()) + .and_then(|t| t.right()) + .and_then(|n| n.token()), + Some('c') + ); + assert_eq!( + tree.right() + .and_then(|t| t.left()) + .and_then(|t| t.right()) + .map(|n| n.freq()), + Some(20) + ); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d77c53d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,106 @@ +mod compression; +mod freq; +mod huffman; + +use clap::Parser; +use clap::ValueEnum; +use std::fs::{self, File}; +use std::io::Write; +use std::path::PathBuf; +use std::time; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(value_enum)] + action: Action, + #[arg(value_enum)] + mode: Mode, + input: PathBuf, + output: PathBuf, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum Action { + Compress, + Extract, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum Mode { + Words, + Chars, +} + +// cargo run --release -- compress words data/wikisent2.txt data/words.huffman +// cargo run --release -- extract words data/words.huffman data/extracted.txt +// (to check if the extraction was correct: `diff data/wikisent2.txt data/extracted.txt`) +// +// cargo run --release -- compress chars data/wikisent2.txt data/chars.huffman +// cargo run --release -- extract chars data/chars.huffman data/extracted.txt +// +// to compare with zip: +// time zip data/test.zip data/wikisent2.txt +// time unzip data/test.zip -d data/test_zip +fn main() -> Result<(), Box> { + let args = Args::parse(); + + match args.action { + Action::Compress => { + let timer = time::Instant::now(); + let text = fs::read_to_string(args.input)?; + let lines: Vec<_> = text.split('\n').map(|x| x.to_string()).collect(); + let time = timer.elapsed(); + let lines_count = lines.len(); + println!("Read the source file with {lines_count} lines in {time:?}"); + + let timer = time::Instant::now(); + let compressed = match args.mode { + Mode::Words => compression::compress(&lines, freq::word_frequencies, |line| { + line.split_ascii_whitespace().map(|token| token.to_string()) + }), + Mode::Chars => { + compression::compress(&lines, freq::char_frequencies, |line| line.chars()) + } + }?; + let time = timer.elapsed(); + println!("Compressed as {mode:?} in {time:?}.", mode = args.mode); + + let timer = time::Instant::now(); + let mut out_f = File::create(&args.output)?; + out_f.write_all(&compressed)?; + let time = timer.elapsed(); + println!( + "Wrote to {output_path:?} in {time:?}", + output_path = args.output + ); + } + Action::Extract => { + let timer = time::Instant::now(); + let data = fs::read(&args.input)?; + let time = timer.elapsed(); + println!("Read the compressed file in {time:?}"); + + let timer = time::Instant::now(); + let content = match args.mode { + Mode::Words => compression::extract(&data, |tokens: Vec| tokens.join(" "))?, + Mode::Chars => { + compression::extract(&data, |tokens: Vec| tokens.into_iter().collect())? + } + }; + let time = timer.elapsed(); + let lines_count = content.len(); + println!("Extracted file with {lines_count} lines in {time:?}."); + + let timer = time::Instant::now(); + fs::write(&args.output, content.join("\n"))?; + let time = timer.elapsed(); + println!( + "Wrote to {output_path:?} in {time:?}", + output_path = args.output + ); + } + } + + Ok(()) +}