Add README, LICENSE, handling of tars and stuff
Signed-off-by: trivernis <trivernis@protonmail.com>main
parent
ad83f38056
commit
c0c532f0c0
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Trivernis
|
||||||
|
|
||||||
|
Permission 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.
|
@ -0,0 +1,29 @@
|
|||||||
|
# Universal Archiver
|
||||||
|
|
||||||
|
Universal Archiver is a tool to easily extract well known archive files
|
||||||
|
based on their signature. The type of the file doesn't need to be specified.
|
||||||
|
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Because it's annoying to learn all the tar and zip commands.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
USAGE:
|
||||||
|
universal-archiver <SUBCOMMAND>
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-h, --help Print help information
|
||||||
|
-V, --version Print version information
|
||||||
|
|
||||||
|
SUBCOMMANDS:
|
||||||
|
extract Extracts a given file
|
||||||
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
@ -0,0 +1,35 @@
|
|||||||
|
use crate::format::{FileFormat, FileObject};
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
use libflate::gzip::Decoder;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub const GZIP_HEADER: &[u8] = &[0x1f, 0x8b];
|
||||||
|
|
||||||
|
pub struct GZipFormat;
|
||||||
|
|
||||||
|
impl FileFormat for GZipFormat {
|
||||||
|
fn parse(file: &FileObject) -> anyhow::Result<Self> {
|
||||||
|
if file.header.starts_with(GZIP_HEADER) {
|
||||||
|
if !file.ext.ends_with(".gz") && !file.ext.ends_with(".gzip") {
|
||||||
|
tracing::error!("The file has a valid gzip signature but not a gzip extension");
|
||||||
|
}
|
||||||
|
Ok(Self)
|
||||||
|
} else {
|
||||||
|
bail!("Not a gzip file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract(&self, file: &Path, output: &Path) -> anyhow::Result<()> {
|
||||||
|
let mut reader = BufReader::new(File::open(file).context("Opening input")?);
|
||||||
|
let mut decoder = Decoder::new(&mut reader).context("Creating decoder")?;
|
||||||
|
let mut output_file =
|
||||||
|
File::create(output).with_context(|| format!("Creating output file {output:?}"))?;
|
||||||
|
tracing::debug!("Extracting to {output:?}");
|
||||||
|
io::copy(&mut decoder, &mut output_file).context("Deompressing file to output")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,131 @@
|
|||||||
|
use crate::format::gzip::GZipFormat;
|
||||||
|
use crate::format::xz::XZFormat;
|
||||||
|
use crate::format::{get_file_header, FileFormat, FileObject};
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use std::{fs, io};
|
||||||
|
use tar::{Archive, EntryType};
|
||||||
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
|
const TAR_HEADER: &[u8] = &[0x75, 0x73, 0x74, 0x61, 0x72];
|
||||||
|
|
||||||
|
pub enum TarFormat {
|
||||||
|
Xz(XZFormat),
|
||||||
|
Gz(GZipFormat),
|
||||||
|
Uncompressed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileFormat for TarFormat {
|
||||||
|
fn parse(file: &FileObject) -> anyhow::Result<Self> {
|
||||||
|
if file.header.starts_with(TAR_HEADER) {
|
||||||
|
tracing::info!("Detected uncompressed tar file");
|
||||||
|
|
||||||
|
Ok(Self::Uncompressed)
|
||||||
|
} else if file.ext.contains(".tar.") {
|
||||||
|
if let Ok(xz) = XZFormat::parse(file) {
|
||||||
|
tracing::info!("Detected tar file compressed with xz");
|
||||||
|
|
||||||
|
Ok(Self::Xz(xz))
|
||||||
|
} else if let Ok(gz) = GZipFormat::parse(file) {
|
||||||
|
tracing::info!("Detected tarfile compressed with gz");
|
||||||
|
|
||||||
|
Ok(Self::Gz(gz))
|
||||||
|
} else {
|
||||||
|
bail!("Not a tar file or a tar with unknown compression");
|
||||||
|
}
|
||||||
|
} else if file.ext.ends_with(".tar") {
|
||||||
|
tracing::info!("Assuming tar based on the file extension");
|
||||||
|
Ok(Self::Uncompressed)
|
||||||
|
} else {
|
||||||
|
bail!("Not a tar file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract(&self, file: &Path, output: &Path) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
TarFormat::Xz(xz) => {
|
||||||
|
let (tmp, _h) = create_tempfile()?;
|
||||||
|
xz.extract(file, &tmp).context("Decompress with xz")?;
|
||||||
|
check_extract_tar(&tmp, output)
|
||||||
|
}
|
||||||
|
TarFormat::Gz(gz) => {
|
||||||
|
let (tmp, _h) = create_tempfile()?;
|
||||||
|
gz.extract(file, &tmp).context("Decompress with gz")?;
|
||||||
|
check_extract_tar(&tmp, output)
|
||||||
|
}
|
||||||
|
TarFormat::Uncompressed => extract_tar(file, output).context("Extract tar"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the given tar has a valid tar signature and extracts it if that's the case
|
||||||
|
fn check_extract_tar(file: &Path, output: &Path) -> anyhow::Result<()> {
|
||||||
|
if !has_tar_header(file)? {
|
||||||
|
tracing::debug!("The extracted tar doesn't have a valid tar signature. This is normal for non POSIX compliant tars.");
|
||||||
|
}
|
||||||
|
extract_tar(file, output).context("Extract tar")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a tar file to the given output directory
|
||||||
|
fn extract_tar(file: &Path, output: &Path) -> anyhow::Result<()> {
|
||||||
|
if output.is_file() {
|
||||||
|
bail!("The output must be a directory.");
|
||||||
|
}
|
||||||
|
let reader = BufReader::new(File::open(file).context("Opening input file")?);
|
||||||
|
let mut archive = Archive::new(reader);
|
||||||
|
|
||||||
|
for file in archive.entries().context("Reading tar entries")? {
|
||||||
|
let mut file = file.context("Retrieving tar file entry")?;
|
||||||
|
let header = file.header();
|
||||||
|
let file_path = header.path().context("Retrieving path of file")?;
|
||||||
|
let output_path = output.join(file_path);
|
||||||
|
|
||||||
|
match header.entry_type() {
|
||||||
|
EntryType::Regular => {
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
if !parent.exists() {
|
||||||
|
tracing::debug!("Creating parent path {parent:?}");
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::debug!("Decompressing entry to {output_path:?}");
|
||||||
|
let mut output_file = File::create(&output_path)
|
||||||
|
.with_context(|| format!("Create output file {output_path:?}"))?;
|
||||||
|
io::copy(&mut file, &mut output_file).context("writing tar entry to output")?;
|
||||||
|
}
|
||||||
|
EntryType::Directory => {
|
||||||
|
tracing::debug!("Creating output directory {output_path:?}");
|
||||||
|
fs::create_dir_all(&output_path)
|
||||||
|
.with_context(|| format!("Failed to create output directory {output_path:?}"))?
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
tracing::debug!("Ignoring entry of type {other:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tempfile() -> anyhow::Result<(PathBuf, TempDir)> {
|
||||||
|
let tmp_dir = tempdir().context("Create tempdir")?;
|
||||||
|
let tmp_file = tmp_dir.path().join(format!(
|
||||||
|
".extract-file-{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok((tmp_file, tmp_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the header of the given file to check if it's a tar file
|
||||||
|
fn has_tar_header(file: &Path) -> anyhow::Result<bool> {
|
||||||
|
let header = get_file_header(file).context("Get file header")?;
|
||||||
|
|
||||||
|
Ok(header.starts_with(TAR_HEADER))
|
||||||
|
}
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
/// Container Formats
|
|
||||||
|
|
||||||
/// Compression Methods
|
|
||||||
pub const GZIP_HEADER: &[u8] = &[0x1f, 0x8b];
|
|
Loading…
Reference in New Issue