Skip to content

Commit eff39ae

Browse files
authored
Improve Zstd binary provided (#93)
* sync * feat(cli): clap compress progress * fmt(clippy): apply is_multiple_of changes * cli: split bin/zstd.rs into multiple files * docs: update changelog for clap cli revision * feat(cli): change compression ratio display and improve time display * refactor: migrate to cargo workspace --------- Co-authored-by: arc <zleyyij@users.noreply.github.com>
1 parent 2cebcfc commit eff39ae

File tree

734 files changed

+420
-348
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

734 files changed

+420
-348
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ jobs:
1212

1313
- name: Install stable toolchain
1414
uses: dtolnay/rust-toolchain@stable
15+
with:
16+
components: clippy
1517
- name: Install cargo-hack
1618
uses: taiki-e/install-action@v2
1719
with:
1820
tool: cargo-hack
19-
- run: cargo hack check --feature-powerset --exclude-features rustc-dep-of-std
20-
- run: cargo hack clippy --feature-powerset --exclude-features rustc-dep-of-std
21-
- run: cargo hack test --feature-powerset --exclude-features rustc-dep-of-std
21+
- run: cargo hack check --workspace --feature-powerset --exclude-features rustc-dep-of-std
22+
- run: cargo hack clippy --workspace --feature-powerset --exclude-features rustc-dep-of-std
23+
- run: cargo hack test --workspace --feature-powerset --exclude-features rustc-dep-of-std
2224

2325
nightly-stuff:
2426
name: nightly clippy, format and miri tests
@@ -33,8 +35,8 @@ jobs:
3335
components: rustfmt, clippy, miri
3436

3537
- run: cargo +nightly fmt --all -- --check
36-
- run: cargo +nightly clippy --no-default-features -- -D warnings
37-
- run: cargo +nightly clippy -- -D warnings
38+
- run: cargo +nightly clippy --workspace --no-default-features -- -D warnings
39+
- run: cargo +nightly clippy --workspace -- -D warnings
3840
- run: cargo +nightly miri test ringbuffer
3941
- run: cargo +nightly miri test short_Writer
4042

Cargo.toml

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,3 @@
1-
[package]
2-
name = "ruzstd"
3-
version = "0.8.1"
4-
authors = ["Moritz Borcherding <moritz.borcherding@web.de>"]
5-
edition = "2018"
6-
license = "MIT"
7-
homepage = "https://github.com/KillingSpark/zstd-rs"
8-
repository = "https://github.com/KillingSpark/zstd-rs"
9-
description = "A decoder for the zstd compression format"
10-
exclude = ["decodecorpus_files/*", "dict_tests/*", "fuzz_decodecorpus/*"]
11-
readme = "Readme.md"
12-
keywords = ["zstd", "zstandard", "decompression"]
13-
categories = ["compression"]
14-
15-
[package.metadata.docs.rs]
16-
all-features = true
17-
rustdoc-args = ["--cfg", "docsrs"]
18-
19-
[dependencies]
20-
twox-hash = { version = "2.0", default-features = false, features = ["xxhash64"], optional = true }
21-
22-
# Internal feature, only used when building as part of libstd, not part of the
23-
# stable interface of this crate.
24-
compiler_builtins = { version = "0.1.2", optional = true }
25-
core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" }
26-
alloc = { version = "1.0.0", optional = true, package = "rustc-std-workspace-alloc" }
27-
fastrand = "2.3.0"
28-
29-
30-
[dev-dependencies]
31-
criterion = "0.5"
32-
rand = { version = "0.8.5", features = ["small_rng"] }
33-
zstd = { version = "0.13.2", features = ["zstdmt"]}
34-
35-
[features]
36-
default = ["hash", "std"]
37-
hash = ["dep:twox-hash"]
38-
fuzz_exports = []
39-
std = []
40-
dict_builder = ["std"]
41-
42-
# Internal feature, only used when building as part of libstd, not part of the
43-
# stable interface of this crate.
44-
rustc-dep-of-std = ["dep:compiler_builtins", "dep:core", "dep:alloc"]
45-
46-
[[bench]]
47-
name = "decode_all"
48-
harness = false
49-
50-
[[bin]]
51-
name = "zstd"
52-
required-features = ["std"]
53-
54-
[[bin]]
55-
name = "zstd_stream"
56-
required-features = ["std"]
57-
58-
[[bin]]
59-
name = "zstd_dict"
60-
required-features = ["std", "dict_builder"]
1+
[workspace]
2+
resolver = "3"
3+
members = ["ruzstd", "cli"]

Changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This document records the changes made between versions, starting with version 0
44

55
# After 0.8.0 (Current)
66
* The compressor now includes a `content_checksum` when the `hash` feature is enabled
7+
* Dictionary generation has been added
8+
* The CLI has been refactored to use `clap`
79

810
# After 0.7.3
911
* Add initial compression support

cli/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
[package]
3+
name = "ruzstd-cli"
4+
version = "0.8.1"
5+
authors = ["Moritz Borcherding <moritz.borcherding@web.de>"]
6+
edition = "2018"
7+
license = "MIT"
8+
homepage = "https://github.com/KillingSpark/zstd-rs"
9+
repository = "https://github.com/KillingSpark/zstd-rs"
10+
description = "A command line interface for the `ruzstd` zstd implementation"
11+
readme = "../Readme.md"
12+
keywords = ["zstd", "zstandard", "decompression"]
13+
categories = ["compression"]
14+
15+
[dependencies]
16+
# Used for the command line binary only
17+
clap = { version = "4.5.46", features = ["derive"]}
18+
color-eyre = { version = "0.6.5" }
19+
tracing = { version = "0.1.41" }
20+
indicatif = { version = "0.18.0" }
21+
tracing-indicatif = { version = "0.3.13" }
22+
console = { version = "0.16.1" }
23+
tracing-subscriber = { version = "0.3.20" }
24+
ruzstd = { path = "../ruzstd", features = ["std"] }
25+
26+
[dev-dependencies]
27+
criterion = "0.5"
28+
rand = { version = "0.8.5", features = ["small_rng"] }
29+
zstd = "0.13.2"

cli/src/main.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
extern crate ruzstd;
2+
mod progress;
3+
use progress::ProgressMonitor;
4+
5+
use std::fs::File;
6+
use std::io::BufReader;
7+
use std::os::unix::fs::MetadataExt;
8+
use std::path::Path;
9+
use std::path::PathBuf;
10+
11+
use progress::fmt_size;
12+
13+
use clap::{Parser, Subcommand};
14+
use color_eyre::eyre::{ContextCompat, WrapErr};
15+
use ruzstd::encoding::CompressionLevel;
16+
use tracing::info;
17+
use tracing_indicatif::IndicatifLayer;
18+
use tracing_subscriber::layer::SubscriberExt;
19+
use tracing_subscriber::util::SubscriberInitExt;
20+
21+
#[derive(Parser)]
22+
#[command(version, about)]
23+
struct Cli {
24+
#[command(subcommand)]
25+
command: Option<Commands>,
26+
}
27+
28+
// TODO: implement a dictionary creation command, and a command for benchmarking
29+
#[derive(Subcommand)]
30+
enum Commands {
31+
/// Compress a single file. If no output file is specified,
32+
/// output will be written to <INPUT_FILE>.zst
33+
Compress {
34+
/// File to compress
35+
input_file: PathBuf,
36+
/// Where the compressed file is written
37+
/// [default: <INPUT_FILE>.zst]
38+
output_file: Option<PathBuf>,
39+
/// How thoroughly the file should be compressed. A higher level will take
40+
/// more time to compress but result in a smaller file, and vice versa.
41+
///
42+
/// - 0: Uncompressed
43+
/// - 1: Fastest
44+
/// - 2: Default
45+
/// - 3: Better
46+
/// - 4: Best
47+
#[arg(
48+
short,
49+
long,
50+
value_name = "COMPRESSION_LEVEL",
51+
default_value_t = 2,
52+
verbatim_doc_comment
53+
)]
54+
level: u8,
55+
},
56+
Decompress {
57+
/// .zst archive to decompress
58+
input_file: PathBuf,
59+
/// Where the compressed file is written
60+
/// [default: <ARCHIVE_NAME>]
61+
output_file: Option<PathBuf>,
62+
},
63+
}
64+
65+
fn main() -> color_eyre::Result<()> {
66+
// Process CLI arguments
67+
let cli = Cli::parse();
68+
// Initialize logging (with indicatif integration)
69+
let indicatif_layer = IndicatifLayer::new();
70+
tracing_subscriber::registry()
71+
.with(
72+
tracing_subscriber::fmt::layer()
73+
.with_writer(indicatif_layer.get_stderr_writer())
74+
.without_time(),
75+
)
76+
.with(indicatif_layer)
77+
.init();
78+
79+
let command: Commands = cli.command.wrap_err("no subcommand provided").unwrap();
80+
match command {
81+
Commands::Compress {
82+
input_file,
83+
output_file,
84+
level,
85+
} => {
86+
let output_file = output_file.unwrap_or_else(|| add_extension(&input_file, ".zst"));
87+
compress(input_file, output_file, level)?;
88+
}
89+
Commands::Decompress {
90+
input_file,
91+
output_file,
92+
} => {
93+
let output_file = output_file.unwrap_or(
94+
input_file
95+
.file_stem()
96+
.expect("input has a file name")
97+
.into(),
98+
);
99+
decompress(input_file, output_file)?;
100+
}
101+
}
102+
Ok(())
103+
}
104+
105+
fn compress(input: PathBuf, output: PathBuf, level: u8) -> color_eyre::Result<()> {
106+
info!("compressing {input:?} to {output:?}");
107+
let compression_level: ruzstd::encoding::CompressionLevel = match level {
108+
0 => CompressionLevel::Uncompressed,
109+
1 => CompressionLevel::Fastest,
110+
2 => CompressionLevel::Default,
111+
3 => CompressionLevel::Better,
112+
4 => CompressionLevel::Best,
113+
_ => {
114+
unimplemented!("unsupported compression level: {}", level);
115+
}
116+
};
117+
let source_file = File::open(input).wrap_err("failed to open input file")?;
118+
let source_size = source_file.metadata()?.len() as usize;
119+
let buffered_source = BufReader::new(source_file);
120+
let encoder_input = ProgressMonitor::new(buffered_source, source_size);
121+
let output: File = File::create(output).wrap_err("failed to open output file for writing")?;
122+
123+
ruzstd::encoding::compress(encoder_input, &output, compression_level);
124+
let compressed_size = output.metadata()?.len();
125+
let compression_ratio = compressed_size as f64 / source_size as f64 * 100.0;
126+
info!(
127+
"{} ——> {} ({compression_ratio:.2}%)",
128+
fmt_size(source_size as f64),
129+
fmt_size(compressed_size as f64)
130+
);
131+
Ok(())
132+
}
133+
134+
fn decompress(input: PathBuf, output: PathBuf) -> color_eyre::Result<()> {
135+
info!("extracting {input:?} to {output:?}");
136+
let source_file = File::open(input).wrap_err("failed to open input file")?;
137+
let source_size = source_file.metadata()?.size() as usize;
138+
let buffered_source = BufReader::new(source_file);
139+
let decoder_input = ProgressMonitor::new(buffered_source, source_size);
140+
let mut output: File =
141+
File::create(output).wrap_err("failed to open output file for writing")?;
142+
143+
let mut decoder = ruzstd::decoding::StreamingDecoder::new(decoder_input)?;
144+
145+
std::io::copy(&mut decoder, &mut output)?;
146+
147+
info!(
148+
"inflated {} ——> {}",
149+
fmt_size(source_size as f64),
150+
fmt_size(output.metadata()?.len() as f64),
151+
);
152+
Ok(())
153+
}
154+
155+
/// A temporary utility function that appends a file extension
156+
/// to the provided path buf.
157+
///
158+
/// Pending removal when our MSRV reaches 1.91 so we can use
159+
///
160+
/// <https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.add_extension>
161+
fn add_extension<P: AsRef<Path>>(path: &Path, extension: P) -> PathBuf {
162+
let mut output = path.to_path_buf().into_os_string();
163+
output.push(extension.as_ref().as_os_str());
164+
165+
output.into()
166+
}
167+
168+
#[cfg(test)]
169+
mod tests {
170+
use std::path::PathBuf;
171+
172+
use crate::add_extension;
173+
174+
#[test]
175+
fn extension_added() {
176+
let filename = PathBuf::from("README.md");
177+
assert_eq!(
178+
add_extension(&filename, ".zst"),
179+
PathBuf::from("README.md.zst")
180+
);
181+
}
182+
}

0 commit comments

Comments
 (0)