initial commit

This commit is contained in:
Joseph Montanaro 2024-02-20 07:37:35 -08:00
commit f8ba5c1f0b
18 changed files with 935 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/tests/output

477
Cargo.lock generated Normal file
View File

@ -0,0 +1,477 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anstream"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "ark"
version = "0.1.0"
dependencies = [
"bzip2",
"clap",
"flate2",
"tar",
"tempdir",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "crc32fast"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
]
[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler",
]
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "rustix"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tar"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand",
"remove_dir_all",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "xattr"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
dependencies = [
"libc",
"linux-raw-sys",
"rustix",
]

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "ark"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bzip2 = "0.4.4"
clap = { version = "4.5.0", features = ["derive"] }
flate2 = "1.0.28"
tar = "0.4.40"
[dev-dependencies]
tempdir = "0.3.7"

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# `ark` - Archiving and compression toolkit with a simple, obvious interface
`ark` is a tool for creating and extracting archives, and (eventually) for compressing and decompressing files. It aims to support the most common operations on a broad variety of formats: `tar`, `zip`, `7z`, `ar`, etc.
## Features
* Intuitive, subcommand-based interface
* Automatically infers archive format and compression algorithm from filenames
* Cross-platform, supporting Windows/Mac/Linux (only tested on Linux so far)
* Optionally respects `.gitignore` files (not yet implemented)
* Optionally deduplicates top-level directory when extracting, in the event that the archive is being extracted into a directory with the same name as the top-level directory _inside_ the archive. (not yet implemented)
* Tree view when listing contents of an archive (not yet implemented)
## Examples
```sh
# Create an archive
ark pack somedir archive.tar.gz
# Like mv or cp, the last argument is interpreted as the destination
ark pack file1 file2 anotherdir archive.zip
# You can explicitly specify the format/compression, in case you need to do something weird
ark pack --format zip --compression deflate files/* archive.vl2
# Unpack an archive into the current directory
ark unpack archive.tar.gz
# Automatically creates destination directory if not extant
ark unpack archive.tar.gz somedir
# Deduplicate top-level directory (i.e. if "targetdir" is the root of all paths in the archive,
# this will prevent the output tree from starting with targetdir/targetdir)
ark unpack archive.tar.xz --deduplicate targetdir
```

81
src/compression.rs Normal file
View File

@ -0,0 +1,81 @@
use std::io::{self, Read, Write};
use clap::ValueEnum;
use flate2::Compression as GzCompression;
use flate2::write::GzEncoder;
use flate2::read::GzDecoder;
use bzip2::Compression as BzCompression;
use bzip2::write::BzEncoder;
use bzip2::read::BzDecoder;
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum CompressionType {
Gzip,
Bzip2,
}
pub enum Encoder<W: Write> {
Gzip(GzEncoder<W>),
Bzip2(BzEncoder<W>),
}
impl<W: Write> Encoder<W> {
pub fn new(sink: W, alg: CompressionType) -> Self {
match alg {
CompressionType::Gzip => {
let inner = GzEncoder::new(sink, GzCompression::default());
Self::Gzip(inner)
},
CompressionType::Bzip2 => {
let inner = BzEncoder::new(sink, BzCompression::new(5));
Self::Bzip2(inner)
},
}
}
}
impl<W: Write> Write for Encoder<W> {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
match self {
Self::Gzip(inner) => inner.write(data),
Self::Bzip2(inner) => inner.write(data),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Gzip(inner) => inner.flush(),
Self::Bzip2(inner) => inner.flush(),
}
}
}
pub enum Decoder<R: Read> {
Gzip(GzDecoder<R>),
Bzip2(BzDecoder<R>),
}
impl<R: Read> Decoder<R> {
pub fn new(src: R, alg: CompressionType) -> Self {
match alg {
CompressionType::Gzip => {
let inner = GzDecoder::new(src);
Self::Gzip(inner)
},
CompressionType::Bzip2 => {
let inner = BzDecoder::new(src);
Self::Bzip2(inner)
},
}
}
}
impl<R: Read> Read for Decoder<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Gzip(inner) => inner.read(buf),
Self::Bzip2(inner) => inner.read(buf),
}
}
}

123
src/lib.rs Normal file
View File

@ -0,0 +1,123 @@
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::Path;
use tar::{Archive, Builder, EntryType, Header};
mod compression;
pub use crate::compression::CompressionType;
use crate::compression::{Encoder, Decoder};
pub fn infer_formats<P: AsRef<Path>>(archive_path: P) -> Option<CompressionType> {
match archive_path.as_ref().to_string_lossy() {
s if s.ends_with(".tar.gz") => Some(CompressionType::Gzip),
s if s.ends_with(".tar.bz2") => Some(CompressionType::Bzip2),
_ => None,
}
}
pub fn pack<I, P, Q>(targets: I, archive_path: Q) -> io::Result<()>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
Q: AsRef<Path>,
{
let alg = infer_formats(&archive_path).unwrap();
pack_with(targets, archive_path, alg)
}
pub fn pack_with<I, P, Q>(targets: I, archive_path: Q, alg: CompressionType) -> io::Result<()>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
Q: AsRef<Path>,
{
let dst = File::create(&archive_path)?;
let encoder = Encoder::new(dst, alg);
let mut archive = Builder::new(encoder);
for target in targets {
add_from_path(&mut archive, &target, &archive_path)?;
}
archive.finish()
}
fn add_from_path<W, P, Q>(archive: &mut Builder<W>, path: &P, archive_path: &Q) -> io::Result<()>
where
W: Write,
P: AsRef<Path>,
Q: AsRef<Path>,
{
let meta = path.as_ref().symlink_metadata()?;
if meta.is_file() {
archive.append_path(path)?;
}
else if meta.is_dir() {
for entry in fs::read_dir(path)? {
let child = entry?.path();
if child == archive_path.as_ref() {
continue;
}
add_from_path(archive, &child.as_path(), archive_path)?;
}
}
else if meta.is_symlink() {
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::Symlink);
header.set_metadata(&meta);
let target = fs::read_link(path)?;
archive.append_link(&mut header, path, &target)?;
}
Ok(())
}
pub fn unpack<P, Q>(archive_path: P, output_path: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let alg = infer_formats(&archive_path).unwrap();
unpack_with(archive_path, output_path, alg)
}
pub fn unpack_with<P, Q>(archive_path: P, output_path: Q, alg: CompressionType) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let archive_file = File::open(&archive_path)?;
let decoder = Decoder::new(archive_file, alg);
let mut archive = Archive::new(decoder);
if !output_path.as_ref().exists() {
fs::create_dir(&output_path)?;
}
archive.unpack(&output_path)
// check for duplicated top-level directory and fix
// let mut iter = fs::read_dir(&output_path).into_iter().next();
// if let (Some(entry), None) = (iter.next(), iter.next()) {
// let child = entry?.path();
// if child.file_name() == output_path.file_name() {
// let mut name = output_path.file_name().to_os_string();
// name.push("_tmp".into())
// }
// }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_formats() {
assert_eq!(infer_formats("test.tar.gz"), Some(CompressionType::Gzip));
assert_eq!(infer_formats("test.tar.bz2"), Some(CompressionType::Bzip2));
assert_eq!(infer_formats("test.nothing"), None);
assert_eq!(infer_formats("test"), None);
}
}

69
src/main.rs Normal file
View File

@ -0,0 +1,69 @@
use std::io;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use ark::CompressionType;
/// Intuitive CLI archiving/compression toolkit
#[derive(Debug, Parser)]
#[command(version, about)]
struct Cli {
#[command(subcommand)]
op: Command
}
#[derive(Debug, Subcommand)]
enum Command {
Pack(PackCmd),
Unpack(UnpackCmd),
}
/// Create an archive
#[derive(Debug, Args)]
struct PackCmd {
/// Target files/directories to include in archive
#[arg(required = true)]
targets: Vec<PathBuf>,
/// Where to create archive file
archive_path: PathBuf,
/// Compression type (inferred from path if not specified)
#[arg(short, long, value_enum)]
compression: Option<CompressionType>,
}
/// Extract an archive
#[derive(Debug, Args)]
struct UnpackCmd {
/// Archive to unpack
archive_path: PathBuf,
/// Destionation to unpack into (will be created if necessary)
#[arg(default_value = ".")]
output_path: PathBuf,
/// Compression type (inferred from path if not specified)
#[arg(short, long, value_enum)]
compression: Option<CompressionType>,
}
fn main() -> io::Result<()> {
let cli = Cli::parse();
match cli.op {
Command::Pack(pack) => {
let alg = pack.compression
.or_else(|| ark::infer_formats(&pack.archive_path))
.unwrap(); // replace with proper error handling later
ark::pack_with(pack.targets, pack.archive_path, alg)
},
Command::Unpack(unpack) => {
let alg = unpack.compression
.or_else(|| ark::infer_formats(&unpack.archive_path))
.unwrap();
ark::unpack_with(unpack.archive_path, unpack.output_path, alg)
},
}
}

BIN
tests/expected/simple.tar Normal file

Binary file not shown.

78
tests/pack.rs Normal file
View File

@ -0,0 +1,78 @@
use std::fs;
use std::process::Command;
use std::path::PathBuf;
use tempdir::TempDir;
fn verify(targets: &[&str], archive_path: &str, compress_flag: &str)
{
let unpack_dir = TempDir::new("ark_pack").unwrap();
dbg!(Command::new("tar")
.arg(compress_flag)
.args(["-xf", archive_path, "-C"])
.arg(unpack_dir.path())
.output()
.unwrap());
for target in targets {
let unpack_path = unpack_dir.path().join(&target);
compare_trees(target.into(), unpack_path);
}
}
fn compare_trees(src: PathBuf, dst: PathBuf) {
// we don't want to follow symlinks, because if the symlink points
// within the archive then we'll be looking at it anyway, and if
// points outside then it's out of scope for us
let src_meta = src.symlink_metadata().unwrap();
let dst_meta = dst.symlink_metadata().unwrap();
assert_eq!(src_meta.file_type(), dst_meta.file_type());
assert_eq!(src_meta.permissions(), dst_meta.permissions());
if src_meta.is_symlink() {
let src_target = fs::read_link(&src).unwrap();
let dst_target = fs::read_link(&dst).unwrap();
assert_eq!(src_target, dst_target);
}
// eventually compare content of files here
else if src_meta.is_dir() {
for entry in fs::read_dir(&src).unwrap() {
let src_child = entry.unwrap().path();
let dst_child = dst.join(src_child.file_name().unwrap());
compare_trees(src_child, dst_child);
}
}
}
#[test]
fn test_pack_simple_files() {
let targets = [
"tests/samples/simple/a.txt",
"tests/samples/simple/b.txt",
];
let archive_path = "tests/output/pack_simple_files.tar.gz";
ark::pack(&targets, &archive_path).unwrap();
verify(&targets, &archive_path, "-z");
}
#[test]
fn test_pack_simple_dir() {
let targets = ["tests/samples/simple"];
let archive_path = "tests/output/pack_simple_dir.tar.gz";
ark::pack(&targets, &archive_path).unwrap();
verify(&targets, &archive_path, "-z");
}
#[test]
fn test_pack_symlinks() {
let targets = ["tests/samples/symlinks"];
let archive_path = "tests/output/pack_symlinks.tar.gz";
ark::pack(&targets, &archive_path).unwrap();
verify(&targets, &archive_path, "-z");
}
#[test]
fn test_bz2() {
let targets = ["tests/samples/simple"];
let archive_path = "tests/output/pack_simple_dir.tar.bz2";
ark::pack(&targets, &archive_path).unwrap();
verify(&targets, &archive_path, "-j");
}

Binary file not shown.

BIN
tests/samples/simple.tar.gz Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
this is file A

View File

@ -0,0 +1 @@
this is file B

Binary file not shown.

View File

@ -0,0 +1 @@
this is file one

View File

@ -0,0 +1 @@
this is file two

View File

@ -0,0 +1 @@
somedir/two.txt

50
tests/unpack.rs Normal file
View File

@ -0,0 +1,50 @@
use std::fs;
use std::path::PathBuf;
use tempdir::TempDir;
#[test]
fn test_unpack_simple() {
let unpack_dir = TempDir::new("ark_unpack").unwrap();
ark::unpack("tests/samples/simple.tar.gz", unpack_dir.path()).unwrap();
let unpack_path = unpack_dir.path().join("tests/samples/simple");
compare_trees("tests/samples/simple".into(), unpack_path);
}
#[test]
fn test_unpack_symlinks() {
let unpack_dir = TempDir::new("ark_unpack").unwrap();
ark::unpack("tests/samples/symlinks.tar.gz", unpack_dir.path()).unwrap();
let unpack_path = unpack_dir.path().join("tests/samples/symlinks");
compare_trees("tests/samples/symlinks".into(), unpack_path);
}
#[test]
fn test_unpack_bz2() {
let unpack_dir = TempDir::new("ark_unpack").unwrap();
ark::unpack("tests/samples/simple.tar.bz2", unpack_dir.path()).unwrap();
let unpack_path = unpack_dir.path().join("tests/samples/simple");
compare_trees("tests/samples/simple".into(), unpack_path);
}
fn compare_trees(src: PathBuf, dst: PathBuf) {
// we don't want to follow symlinks, because if the symlink points
// within the archive then we'll be looking at it anyway, and if
// points outside then it's out of scope for us
let src_meta = src.symlink_metadata().unwrap();
let dst_meta = dst.symlink_metadata().unwrap();
assert_eq!(src_meta.file_type(), dst_meta.file_type());
assert_eq!(src_meta.permissions(), dst_meta.permissions());
if src_meta.is_symlink() {
let src_target = fs::read_link(&src).unwrap();
let dst_target = fs::read_link(&dst).unwrap();
assert_eq!(src_target, dst_target);
}
// eventually compare content of files here
else if src_meta.is_dir() {
for entry in fs::read_dir(&src).unwrap() {
let src_child = entry.unwrap().path();
let dst_child = dst.join(src_child.file_name().unwrap());
compare_trees(src_child, dst_child);
}
}
}