diff --git a/src/close_operation.rs b/src/close_operation.rs new file mode 100644 index 0000000..3c02697 --- /dev/null +++ b/src/close_operation.rs @@ -0,0 +1,78 @@ +use std::path::{Path, PathBuf}; +use flate2::Compression; +use flate2::write::GzEncoder; +use clap::Parser; +use crate::error::DrawerError; +use crate::operation::Operation; +use crate::utils::*; + +#[derive(Parser, Debug, PartialEq)] +pub struct CloseOperation { + pub drawer_file: PathBuf, + pub target_path: Option, + #[arg(short = 'i')] + pub key: Option, +} + +impl Operation for CloseOperation { + fn validate(&self) -> Result<(), DrawerError> { + validate_drawer_extension(&self.drawer_file)?; + validate_ssh_key(&self.key)?; + let target = self.target_path.as_ref().cloned() + .unwrap_or_else(|| default_target(&self.drawer_file, None)); + if self.target_path.is_none() { + println!("(assuming from {})", target.display()); + } + let meta = std::fs::metadata(&target) + .map_err(|_| DrawerError::TargetInvalid(target.clone()))?; + if !meta.is_dir() && !meta.is_file() { + return Err(DrawerError::TargetInvalid(target.clone())); + } + if meta.is_dir() && std::fs::read_dir(&target).is_err() { + return Err(DrawerError::TargetInvalid(target.clone())); + } + let canon_target = target.canonicalize() + .unwrap_or_else(|_| target.clone()); + let drawer_parent = match self.drawer_file.parent() { + None => Path::new("."), + Some(p) if p == Path::new("") => Path::new("."), + Some(p) => p, + }; + let canon_drawer = drawer_parent.canonicalize() + .unwrap_or_else(|_| drawer_parent.to_path_buf()) + .join(self.drawer_file.file_name().unwrap_or_default()); + if canon_drawer.starts_with(&canon_target) { + return Err(DrawerError::DrawerInsideTarget); + } + Ok(()) + } + + fn perform(&self) -> Result<(), DrawerError> { + self.validate()?; + + let target = self.target_path.as_ref().cloned() + .unwrap_or_else(|| default_target(&self.drawer_file, None)); + + let mut buf: Vec = Vec::new(); + { + let gz = GzEncoder::new(&mut buf, Compression::default()); + let mut archive = tar::Builder::new(gz); + if target.is_dir() { + archive.append_dir_all(".", &target) + .map_err(|_| DrawerError::TarFailed)?; + } else { + let name = target.file_name().unwrap_or_default(); + archive.append_path_with_name(&target, name) + .map_err(|_| DrawerError::TarFailed)?; + } + let gz = archive.into_inner() + .map_err(|_| DrawerError::TarFailed)?; + gz.finish() + .map_err(|_| DrawerError::TarFailed)?; + } + + let encrypted = encrypt(&buf, &self.key, &self.drawer_file)?; + std::fs::write(&self.drawer_file, &encrypted) + .map_err(|_| DrawerError::WriteFailed(self.drawer_file.clone())) + } +} diff --git a/src/info_operation.rs b/src/info_operation.rs new file mode 100644 index 0000000..98ac012 --- /dev/null +++ b/src/info_operation.rs @@ -0,0 +1,71 @@ +use std::path::PathBuf; +use flate2::read::GzDecoder; +use clap::Parser; +use crate::error::DrawerError; +use crate::operation::Operation; +use crate::utils::*; + +#[derive(Parser, Debug, PartialEq)] +pub struct InfoOperation { + pub drawer_file: PathBuf, + #[arg(short = 'i')] + pub key: Option, +} + +enum ArchiveInfo { + SingleFile { size: u64 }, + Folder { file_count: usize }, +} + +fn archive_info(data: &[u8]) -> Result { + let cursor = std::io::Cursor::new(data); + let gz = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(gz); + let mut entries = archive.entries().map_err(|_| DrawerError::UntarFailed)?; + + let first = entries.next() + .ok_or(DrawerError::UntarFailed)? + .map_err(|_| DrawerError::UntarFailed)?; + + if first.header().entry_type().is_file() { + let size = first.header().size().map_err(|_| DrawerError::UntarFailed)?; + if entries.next().is_none() { + return Ok(ArchiveInfo::SingleFile { size }); + } + } + + let cursor = std::io::Cursor::new(data); + let gz = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(gz); + let file_count = archive.entries() + .map_err(|_| DrawerError::UntarFailed)? + .filter_map(|e| e.ok()) + .filter(|e| e.header().entry_type().is_file()) + .count(); + Ok(ArchiveInfo::Folder { file_count }) +} + +impl Operation for InfoOperation { + fn validate(&self) -> Result<(), DrawerError> { + validate_drawer_extension(&self.drawer_file)?; + validate_ssh_key(&self.key)?; + validate_drawer_file_exists(&self.drawer_file)?; + Ok(()) + } + + fn perform(&self) -> Result<(), DrawerError> { + self.validate()?; + + let encrypted = std::fs::read(&self.drawer_file) + .map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?; + let decrypted = decrypt(encrypted, &self.key, &self.drawer_file)?; + let info = archive_info(&decrypted)?; + match info { + ArchiveInfo::SingleFile { size } => + println!("{} (file, {} bytes)", self.drawer_file.display(), size), + ArchiveInfo::Folder { file_count } => + println!("{} (folder, {} files)", self.drawer_file.display(), file_count), + } + Ok(()) + } +} diff --git a/src/key_operation.rs b/src/key_operation.rs new file mode 100644 index 0000000..29559cb --- /dev/null +++ b/src/key_operation.rs @@ -0,0 +1,37 @@ +use std::path::{Path, PathBuf}; +use clap::Parser; +use ssh_key::{Algorithm, LineEnding, PrivateKey}; +use crate::error::DrawerError; +use crate::operation::Operation; + +#[derive(Parser, Debug, PartialEq)] +pub struct KeyOperation { + pub filename: PathBuf, +} + +impl Operation for KeyOperation { + fn validate(&self) -> Result<(), DrawerError> { + if self.filename.exists() { + return Err(DrawerError::KeyOutputFailed(self.filename.clone())); + } + let parent = match self.filename.parent() { + None => Path::new("."), + Some(p) if p == Path::new("") => Path::new("."), + Some(p) => p, + }; + if !parent.exists() { + return Err(DrawerError::KeyOutputFailed(self.filename.clone())); + } + Ok(()) + } + + fn perform(&self) -> Result<(), DrawerError> { + self.validate()?; + let private_key = PrivateKey::random(&mut rand::rngs::OsRng, Algorithm::Ed25519) + .map_err(|_| DrawerError::KeyGenerateFailed)?; + let pem = private_key.to_openssh(LineEnding::LF) + .map_err(|_| DrawerError::KeyGenerateFailed)?; + std::fs::write(&self.filename, pem.as_bytes()) + .map_err(|_| DrawerError::KeyOutputFailed(self.filename.clone())) + } +} diff --git a/src/main.rs b/src/main.rs index af5ec98..0f6e8c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,11 @@ use crate::parsing::parse_args; mod parsing; mod error; mod utils; +mod operation; +mod open_operation; +mod close_operation; +mod key_operation; +mod info_operation; fn main() { match parse_args(std::env::args_os()) { diff --git a/src/open_operation.rs b/src/open_operation.rs new file mode 100644 index 0000000..6bd1aa7 --- /dev/null +++ b/src/open_operation.rs @@ -0,0 +1,85 @@ +use std::path::PathBuf; +use flate2::read::GzDecoder; +use clap::Parser; +use crate::error::DrawerError; +use crate::operation::Operation; +use crate::utils::*; + +#[derive(Parser, Debug, PartialEq)] +pub struct OpenOperation { + pub drawer_file: PathBuf, + pub target_path: Option, + #[arg(short = 'i')] + pub key: Option, + #[arg(short = 'f')] + pub force: bool, +} + +fn single_file_entry(data: &[u8]) -> Option { + let cursor = std::io::Cursor::new(data); + let gz = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(gz); + let mut entries = archive.entries().ok()?; + let entry = entries.next()?.ok()?; + if !entry.header().entry_type().is_file() { + return None; + } + if entries.next().is_some() { + return None; + } + Some(entry.path().ok()?.into_owned()) +} + +impl Operation for OpenOperation { + fn validate(&self) -> Result<(), DrawerError> { + validate_drawer_extension(&self.drawer_file)?; + validate_ssh_key(&self.key)?; + validate_drawer_file_exists(&self.drawer_file)?; + Ok(()) + } + + fn perform(&self) -> Result<(), DrawerError> { + self.validate()?; + + let encrypted = std::fs::read(&self.drawer_file) + .map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?; + let decrypted = decrypt(encrypted, &self.key, &self.drawer_file)?; + + let sf = single_file_entry(&decrypted); + let target = self.target_path.as_ref().cloned() + .unwrap_or_else(|| default_target(&self.drawer_file, sf.as_deref())); + + if self.target_path.is_none() { + println!("(assuming into {})", target.display()); + } + + if !self.force && target.exists() { + if target.is_file() { + return Err(DrawerError::TargetNotEmpty(target.clone())); + } + let mut entries = std::fs::read_dir(&target) + .map_err(|_| DrawerError::TargetInvalid(target.clone()))?; + if entries.next().is_some() { + return Err(DrawerError::TargetNotEmpty(target.clone())); + } + } + + if sf.is_some() { + let cursor = std::io::Cursor::new(&decrypted); + let gz = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(gz); + let mut entry = archive.entries() + .map_err(|_| DrawerError::UntarFailed)? + .next() + .ok_or(DrawerError::UntarFailed)? + .map_err(|_| DrawerError::UntarFailed)?; + entry.unpack(&target).map_err(|_| DrawerError::UntarFailed)?; + } else { + let cursor = std::io::Cursor::new(decrypted); + let gz = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(gz); + archive.unpack(&target).map_err(|_| DrawerError::UntarFailed)?; + } + Ok(()) + } +} diff --git a/src/operation.rs b/src/operation.rs new file mode 100644 index 0000000..4865230 --- /dev/null +++ b/src/operation.rs @@ -0,0 +1,6 @@ +use crate::error::DrawerError; + +pub trait Operation { + fn perform(&self) -> Result<(), DrawerError>; + fn validate(&self) -> Result<(), DrawerError>; +} diff --git a/src/parsing.rs b/src/parsing.rs index 6237bc9..ac548e5 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -1,12 +1,11 @@ use std::ffi::OsString; -use std::path::{Path, PathBuf}; -use flate2::Compression; -use flate2::read::GzDecoder; -use flate2::write::GzEncoder; use clap::{Parser, Subcommand}; -use ssh_key::{Algorithm, LineEnding, PrivateKey}; use crate::error::DrawerError; -use crate::utils::*; +use crate::operation::Operation; +pub use crate::open_operation::OpenOperation; +pub use crate::close_operation::CloseOperation; +pub use crate::key_operation::KeyOperation; +pub use crate::info_operation::InfoOperation; #[derive(Parser)] #[command(name = "drawer", about = "Encrypt and decrypt project directories as drawer files")] @@ -27,11 +26,6 @@ pub enum Command { Info(InfoOperation), } -pub trait Operation { - fn perform(&self) -> Result<(), DrawerError>; - fn validate(&self) -> Result<(), DrawerError>; -} - impl Command { pub fn perform(&self) -> Result<(), DrawerError> { match self { @@ -43,36 +37,6 @@ impl Command { } } -#[derive(Parser, Debug, PartialEq)] -pub struct OpenOperation { - pub drawer_file: PathBuf, - pub target_path: Option, - #[arg(short = 'i')] - pub key: Option, - #[arg(short = 'f')] - pub force: bool, -} - -#[derive(Parser, Debug, PartialEq)] -pub struct CloseOperation { - pub drawer_file: PathBuf, - pub target_path: Option, - #[arg(short = 'i')] - pub key: Option, -} - -#[derive(Parser, Debug, PartialEq)] -pub struct KeyOperation { - pub filename: PathBuf, -} - -#[derive(Parser, Debug, PartialEq)] -pub struct InfoOperation { - pub drawer_file: PathBuf, - #[arg(short = 'i')] - pub key: Option, -} - pub fn parse_args(args: I) -> Result where I: IntoIterator, @@ -82,220 +46,3 @@ where .map_err(|e| DrawerError::ParseError(e.to_string()))?; Ok(cli.command) } - -enum ArchiveInfo { - SingleFile { size: u64 }, - Folder { file_count: usize }, -} - -fn archive_info(data: &[u8]) -> Result { - let cursor = std::io::Cursor::new(data); - let gz = GzDecoder::new(cursor); - let mut archive = tar::Archive::new(gz); - let mut entries = archive.entries().map_err(|_| DrawerError::UntarFailed)?; - - let first = entries.next() - .ok_or(DrawerError::UntarFailed)? - .map_err(|_| DrawerError::UntarFailed)?; - - if first.header().entry_type().is_file() { - let size = first.header().size().map_err(|_| DrawerError::UntarFailed)?; - if entries.next().is_none() { - return Ok(ArchiveInfo::SingleFile { size }); - } - } - - let cursor = std::io::Cursor::new(data); - let gz = GzDecoder::new(cursor); - let mut archive = tar::Archive::new(gz); - let file_count = archive.entries() - .map_err(|_| DrawerError::UntarFailed)? - .filter_map(|e| e.ok()) - .filter(|e| e.header().entry_type().is_file()) - .count(); - Ok(ArchiveInfo::Folder { file_count }) -} - -fn single_file_entry(data: &[u8]) -> Option { - let cursor = std::io::Cursor::new(data); - let gz = GzDecoder::new(cursor); - let mut archive = tar::Archive::new(gz); - let mut entries = archive.entries().ok()?; - let entry = entries.next()?.ok()?; - if !entry.header().entry_type().is_file() { - return None; - } - if entries.next().is_some() { - return None; - } - Some(entry.path().ok()?.into_owned()) -} - -impl Operation for OpenOperation { - fn validate(&self) -> Result<(), DrawerError> { - validate_drawer_extension(&self.drawer_file)?; - validate_ssh_key(&self.key)?; - validate_drawer_file_exists(&self.drawer_file)?; - Ok(()) - } - - fn perform(&self) -> Result<(), DrawerError> { - self.validate()?; - - let encrypted = std::fs::read(&self.drawer_file) - .map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?; - let decrypted = decrypt(encrypted, &self.key, &self.drawer_file)?; - - let sf = single_file_entry(&decrypted); - let target = self.target_path.as_ref().cloned() - .unwrap_or_else(|| default_target(&self.drawer_file, sf.as_deref())); - - if self.target_path.is_none() { - println!("(assuming into {})", target.display()); - } - - if !self.force && target.exists() { - if target.is_file() { - return Err(DrawerError::TargetNotEmpty(target.clone())); - } - let mut entries = std::fs::read_dir(&target) - .map_err(|_| DrawerError::TargetInvalid(target.clone()))?; - if entries.next().is_some() { - return Err(DrawerError::TargetNotEmpty(target.clone())); - } - } - - if sf.is_some() { - let cursor = std::io::Cursor::new(&decrypted); - let gz = GzDecoder::new(cursor); - let mut archive = tar::Archive::new(gz); - let mut entry = archive.entries() - .map_err(|_| DrawerError::UntarFailed)? - .next() - .ok_or(DrawerError::UntarFailed)? - .map_err(|_| DrawerError::UntarFailed)?; - entry.unpack(&target).map_err(|_| DrawerError::UntarFailed)?; - } else { - let cursor = std::io::Cursor::new(decrypted); - let gz = GzDecoder::new(cursor); - let mut archive = tar::Archive::new(gz); - archive.unpack(&target).map_err(|_| DrawerError::UntarFailed)?; - } - Ok(()) - } -} - -impl Operation for CloseOperation { - fn validate(&self) -> Result<(), DrawerError> { - validate_drawer_extension(&self.drawer_file)?; - validate_ssh_key(&self.key)?; - let target = self.target_path.as_ref().cloned() - .unwrap_or_else(|| default_target(&self.drawer_file, None)); - if self.target_path.is_none() { - println!("(assuming from {})", target.display()); - } - let meta = std::fs::metadata(&target) - .map_err(|_| DrawerError::TargetInvalid(target.clone()))?; - if !meta.is_dir() && !meta.is_file() { - return Err(DrawerError::TargetInvalid(target.clone())); - } - if meta.is_dir() && std::fs::read_dir(&target).is_err() { - return Err(DrawerError::TargetInvalid(target.clone())); - } - let canon_target = target.canonicalize() - .unwrap_or_else(|_| target.clone()); - let drawer_parent = match self.drawer_file.parent() { - None => Path::new("."), - Some(p) if p == Path::new("") => Path::new("."), - Some(p) => p, - }; - let canon_drawer = drawer_parent.canonicalize() - .unwrap_or_else(|_| drawer_parent.to_path_buf()) - .join(self.drawer_file.file_name().unwrap_or_default()); - if canon_drawer.starts_with(&canon_target) { - return Err(DrawerError::DrawerInsideTarget); - } - Ok(()) - } - - fn perform(&self) -> Result<(), DrawerError> { - self.validate()?; - - let target = self.target_path.as_ref().cloned() - .unwrap_or_else(|| default_target(&self.drawer_file, None)); - - let mut buf: Vec = Vec::new(); - { - let gz = GzEncoder::new(&mut buf, Compression::default()); - let mut archive = tar::Builder::new(gz); - if target.is_dir() { - archive.append_dir_all(".", &target) - .map_err(|_| DrawerError::TarFailed)?; - } else { - let name = target.file_name().unwrap_or_default(); - archive.append_path_with_name(&target, name) - .map_err(|_| DrawerError::TarFailed)?; - } - let gz = archive.into_inner() - .map_err(|_| DrawerError::TarFailed)?; - gz.finish() - .map_err(|_| DrawerError::TarFailed)?; - } - - let encrypted = encrypt(&buf, &self.key, &self.drawer_file)?; - std::fs::write(&self.drawer_file, &encrypted) - .map_err(|_| DrawerError::WriteFailed(self.drawer_file.clone())) - } -} - -impl Operation for KeyOperation { - fn validate(&self) -> Result<(), DrawerError> { - if self.filename.exists() { - return Err(DrawerError::KeyOutputFailed(self.filename.clone())); - } - let parent = match self.filename.parent() { - None => Path::new("."), - Some(p) if p == Path::new("") => Path::new("."), - Some(p) => p, - }; - if !parent.exists() { - return Err(DrawerError::KeyOutputFailed(self.filename.clone())); - } - Ok(()) - } - - fn perform(&self) -> Result<(), DrawerError> { - self.validate()?; - let private_key = PrivateKey::random(&mut rand::rngs::OsRng, Algorithm::Ed25519) - .map_err(|_| DrawerError::KeyGenerateFailed)?; - let pem = private_key.to_openssh(LineEnding::LF) - .map_err(|_| DrawerError::KeyGenerateFailed)?; - std::fs::write(&self.filename, pem.as_bytes()) - .map_err(|_| DrawerError::KeyOutputFailed(self.filename.clone())) - } -} - -impl Operation for InfoOperation { - fn validate(&self) -> Result<(), DrawerError> { - validate_drawer_extension(&self.drawer_file)?; - validate_ssh_key(&self.key)?; - validate_drawer_file_exists(&self.drawer_file)?; - Ok(()) - } - - fn perform(&self) -> Result<(), DrawerError> { - self.validate()?; - - let encrypted = std::fs::read(&self.drawer_file) - .map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?; - let decrypted = decrypt(encrypted, &self.key, &self.drawer_file)?; - let info = archive_info(&decrypted)?; - match info { - ArchiveInfo::SingleFile { size } => - println!("{} (file, {} bytes)", self.drawer_file.display(), size), - ArchiveInfo::Folder { file_count } => - println!("{} (folder, {} files)", self.drawer_file.display(), file_count), - } - Ok(()) - } -}