diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index c160216..b6a7341 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -2,8 +2,11 @@ use std::path::PathBuf; use crate::{error::OuchResult, file::File}; -/// This file should/could store a Decompressor trait +pub enum DecompressionResult { + FilesUnpacked(Vec), + FileInMemory(Vec) +} pub trait Decompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult>; + fn decompress(&self, from: &File, into: &Option) -> OuchResult; } \ No newline at end of file diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 4edba7c..8c3880a 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,7 +1,10 @@ mod decompressor; mod tar; mod zip; +mod niffler; pub use decompressor::Decompressor; +pub use decompressor::DecompressionResult; pub use self::tar::TarDecompressor; -pub use self::zip::ZipDecompressor; \ No newline at end of file +pub use self::zip::ZipDecompressor; +pub use self::niffler::NifflerDecompressor; \ No newline at end of file diff --git a/src/decompressors/niffler.rs b/src/decompressors/niffler.rs new file mode 100644 index 0000000..d51be06 --- /dev/null +++ b/src/decompressors/niffler.rs @@ -0,0 +1,54 @@ +use std::{io::Read, path::Path}; + +use colored::Colorize; +use niffler; + +use crate::file::File; +use crate::{ + error::{self, OuchResult}, + utils, +}; + +use super::decompressor::Decompressor; +use super::decompressor::DecompressionResult; + +pub struct NifflerDecompressor {} + +impl NifflerDecompressor { + fn unpack_file(from: &Path) -> OuchResult> { + + println!("{}: trying to decompress {:?}", "info".yellow(), from); + + let file = std::fs::read(from)?; + + let (mut reader, compression) = niffler::get_reader(Box::new(&file[..]))?; + + match compression { + niffler::Format::No => { + return Err(error::Error::InvalidInput); + }, + other => { + println!("{}: {:?} detected.", "info".yellow(), other); + } + } + + let mut buffer = Vec::new(); + let bytes_read = reader.read_to_end(&mut buffer)?; + + println!("{}: {:?} extracted into memory ({} bytes).", "info".yellow(), from, bytes_read); + + Ok(buffer) + } +} + +impl Decompressor for NifflerDecompressor { + fn decompress(&self, from: &File, into: &Option) -> OuchResult { + let destination_path = utils::get_destination_path(into); + + utils::create_path_if_non_existent(destination_path)?; + + let bytes = Self::unpack_file(&from.path)?; + + Ok(DecompressionResult::FileInMemory(bytes)) + } +} diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index c7a39cf..913bdeb 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -9,7 +9,7 @@ use tar; use crate::{error::OuchResult, utils}; use crate::file::File; -use super::decompressor::Decompressor; +use super::decompressor::{DecompressionResult, Decompressor}; pub struct TarDecompressor {} @@ -45,13 +45,13 @@ impl TarDecompressor { } impl Decompressor for TarDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult> { + fn decompress(&self, from: &File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; let files_unpacked = Self::unpack_files(&from.path, destination_path)?; - Ok(files_unpacked) + Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } } \ No newline at end of file diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index cfdd4c9..c3ce796 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -6,7 +6,7 @@ use zip::{self, read::ZipFile}; use crate::{error::{self, OuchResult}, utils}; use crate::file::File; -use super::decompressor::Decompressor; +use super::decompressor::{DecompressionResult, Decompressor}; pub struct ZipDecompressor {} @@ -61,6 +61,8 @@ impl ZipDecompressor { io::copy(&mut file, &mut outfile)?; } + // TODO: check if permissions are correct when on Unix + let file_path = fs::canonicalize(file_path.clone())?; unpacked_files.push(file_path); } @@ -71,13 +73,13 @@ impl ZipDecompressor { impl Decompressor for ZipDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult> { + fn decompress(&self, from: &File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; let files_unpacked = Self::unpack_files(&from.path, destination_path)?; - Ok(files_unpacked) + Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 16bac57..63c6ba4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,7 @@ pub enum Error { InvalidZipArchive(&'static str), PermissionDenied, UnsupportedZipArchive(&'static str), + FileTooShort, InputsMustHaveBeenDecompressible(String), } @@ -70,4 +71,19 @@ impl From for Error { UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename) } } +} + +impl From for Error { + fn from(err: niffler::error::Error) -> Self { + use niffler::error::Error as NifErr; + match err { + NifErr::FeatureDisabled => { + // Ouch is using Niffler with all its features so + // this should be unreachable. + unreachable!(); + }, + NifErr::FileTooShort => Self::FileTooShort, + NifErr::IOError(io_err) => Self::from(io_err) + } + } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index bdb6d44..c1a7fbe 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,12 +1,20 @@ +use std::{ffi::OsStr, fs, io::Write}; use colored::Colorize; -use crate::{cli::{Command, CommandKind}, error, extension::CompressionFormat, file::File}; use crate::decompressors::Decompressor; use crate::decompressors::TarDecompressor; use crate::decompressors::ZipDecompressor; +use crate::{ + cli::{Command, CommandKind}, + decompressors::{DecompressionResult, NifflerDecompressor}, + error::{self, OuchResult}, + extension::CompressionFormat, + file::File, + utils, +}; -pub struct Evaluator { +pub struct Evaluator { command: Command, // verbosity: Verbosity } @@ -14,40 +22,101 @@ pub struct Evaluator { impl Evaluator { // todo: remove this? pub fn new(command: Command) -> Self { - Self { - command - } + Self { command } } - fn get_decompressor(&self, file: &File) -> error::OuchResult> { + fn get_decompressor( + &self, + file: &File, + ) -> error::OuchResult<(Option>, Box)> { if file.extension.is_none() { // This block *should* be unreachable - eprintln!("{}: reached Evaluator::get_decompressor without known extension.", "internal error".red()); + eprintln!( + "{}: reached Evaluator::get_decompressor without known extension.", + "internal error".red() + ); return Err(error::Error::InvalidInput); } let extension = file.extension.clone().unwrap(); - let decompressor: Box = match extension.second_ext { - CompressionFormat::Tar => { - Box::new(TarDecompressor{}) - }, - CompressionFormat::Zip => { - Box::new(ZipDecompressor{}) - } - _ => { - todo!() + + let decompressor_from_format = |ext| -> Box { + match ext { + CompressionFormat::Tar => Box::new(TarDecompressor {}), + + CompressionFormat::Zip => Box::new(ZipDecompressor {}), + + CompressionFormat::Gzip | CompressionFormat::Lzma | CompressionFormat::Bzip => { + Box::new(NifflerDecompressor {}) + } } }; + let second_decompressor = decompressor_from_format(extension.second_ext); - Ok(decompressor) + let first_decompressor = match extension.first_ext { + Some(ext) => Some(decompressor_from_format(ext)), + None => None, + }; + + Ok((first_decompressor, second_decompressor)) + } + + // todo: move this folder into decompressors/ later on + fn decompress_file_in_memory( + bytes: Vec, + file: &File, + decompressor: Option>, + output_file: &Option, + ) -> OuchResult<()> { + + let output_file = utils::get_destination_path(output_file); + + let mut filename = file.path.file_stem().unwrap_or(output_file.as_os_str()); + if filename == "." { + // I believe this is only possible when the supplied inout has a name + // of the sort `.tar` or `.zip' and no output has been supplied. + filename = OsStr::new("ouch-output"); + } + + if decompressor.is_none() { + // There is no more processing to be done on the input file (or there is but currently unsupported) + // Therefore, we'll save what we have in memory into a file. + + println!("{}: saving to {:?}.", "info".yellow(), filename); + + let mut f = fs::File::create(output_file.join(filename))?; + f.write_all(&bytes)?; + return Ok(()); + } + + // If there is a decompressor to use, we'll create a file in-memory (to-do) and decompress it + // TODO: change decompressor logic to use BufReader or something like that + + Ok(()) } fn decompress_file(&self, file: &File) -> error::OuchResult<()> { let output_file = &self.command.output; - let decompressor = self.get_decompressor(file)?; - let files_unpacked = decompressor.decompress(file, output_file)?; + let (first_decompressor, second_decompressor) = self.get_decompressor(file)?; - // TODO: decompress the first extension if it exists + let decompression_result = second_decompressor.decompress(file, output_file)?; + + match decompression_result { + DecompressionResult::FileInMemory(bytes) => { + // We'll now decompress a file currently in memory. + // This will currently happen in the case of .bz, .xz and .lzma + Self::decompress_file_in_memory(bytes, file, first_decompressor, output_file)?; + } + DecompressionResult::FilesUnpacked(files) => { + // If the file's last extension was an archival method, + // such as .tar, .zip or (to-do) .rar, then we won't look for + // further processing. + // The reason for this is that cases such as "file.xz.tar" are too rare + // to worry about, at least at the moment. + + // TODO: use the `files` variable for something + } + } Ok(()) } @@ -67,4 +136,4 @@ impl Evaluator { } Ok(()) } -} \ No newline at end of file +} diff --git a/src/extension.rs b/src/extension.rs index d4880a9..30fe428 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -49,7 +49,7 @@ impl Extension { "tar" => Ok(Tar), "gz" => Ok(Gzip), "bz" => Ok(Bzip), - "lzma" => Ok(Lzma), + "lz" | "lzma" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), } };