tests
This commit is contained in:
+6
-1
@@ -2,13 +2,18 @@ use crate::parsing::parse_args;
|
|||||||
|
|
||||||
mod parsing;
|
mod parsing;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
match parse_args(std::env::args_os()) {
|
match parse_args(std::env::args_os()) {
|
||||||
Err(e) => eprintln!("{e}"),
|
Err(e) => {
|
||||||
|
eprintln!("{e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
Ok(cmd) => {
|
Ok(cmd) => {
|
||||||
if let Err(e) = cmd.perform() {
|
if let Err(e) = cmd.perform() {
|
||||||
eprintln!("{e}");
|
eprintln!("{e}");
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-126
@@ -1,13 +1,12 @@
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use age::ssh::Identity;
|
|
||||||
use flate2::Compression;
|
use flate2::Compression;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use flate2::write::GzEncoder;
|
use flate2::write::GzEncoder;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use ssh_key::{Algorithm, LineEnding, PrivateKey};
|
use ssh_key::{Algorithm, LineEnding, PrivateKey};
|
||||||
use crate::error::DrawerError;
|
use crate::error::DrawerError;
|
||||||
|
use crate::utils::*;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "drawer", about = "Encrypt and decrypt project directories as drawer files")]
|
#[command(name = "drawer", about = "Encrypt and decrypt project directories as drawer files")]
|
||||||
@@ -28,6 +27,11 @@ pub enum Command {
|
|||||||
Info(InfoOperation),
|
Info(InfoOperation),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait Operation {
|
||||||
|
fn perform(&self) -> Result<(), DrawerError>;
|
||||||
|
fn validate(&self) -> Result<(), DrawerError>;
|
||||||
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
pub fn perform(&self) -> Result<(), DrawerError> {
|
||||||
match self {
|
match self {
|
||||||
@@ -79,84 +83,6 @@ where
|
|||||||
Ok(cli.command)
|
Ok(cli.command)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_path(key: &Option<PathBuf>) -> Result<PathBuf, DrawerError> {
|
|
||||||
if let Some(k) = key {
|
|
||||||
return Ok(k.clone());
|
|
||||||
}
|
|
||||||
let val = std::env::var("DRAWER_KEY").map_err(|_| DrawerError::NoKeyPath)?;
|
|
||||||
Ok(PathBuf::from(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn identity(key: &Option<PathBuf>, drawer_file: &Path) -> Result<(PathBuf, Identity), DrawerError> {
|
|
||||||
let kp = key_path(key)?;
|
|
||||||
let file = std::fs::File::open(&kp)
|
|
||||||
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
|
||||||
let reader = std::io::BufReader::new(file);
|
|
||||||
let id = age::ssh::Identity::from_buffer(reader, None)
|
|
||||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
|
||||||
Ok((kp, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrypt(encrypted: Vec<u8>, key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
|
||||||
let (kp, id) = identity(key, drawer_file)?;
|
|
||||||
let decryptor = age::Decryptor::new(encrypted.as_slice())
|
|
||||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
|
||||||
let mut decrypted: Vec<u8> = Vec::new();
|
|
||||||
let mut stream = decryptor.decrypt(std::iter::once(&id as &dyn age::Identity))
|
|
||||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
|
||||||
std::io::Read::read_to_end(&mut stream, &mut decrypted)
|
|
||||||
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
|
||||||
Ok(decrypted)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt(buf: &[u8], key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
|
||||||
let (_, id) = identity(key, drawer_file)?;
|
|
||||||
let recipient = age::ssh::Recipient::try_from(id)
|
|
||||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
|
||||||
let mut encrypted: Vec<u8> = Vec::new();
|
|
||||||
let encryptor = age::Encryptor::with_recipients(
|
|
||||||
std::iter::once(&recipient as &dyn age::Recipient)
|
|
||||||
).map_err(|_| DrawerError::EncryptFailed)?;
|
|
||||||
let mut writer = encryptor.wrap_output(&mut encrypted)
|
|
||||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
|
||||||
writer.write_all(buf).map_err(|_| DrawerError::EncryptFailed)?;
|
|
||||||
writer.finish().map_err(|_| DrawerError::EncryptFailed)?;
|
|
||||||
Ok(encrypted)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_target(drawer_file: &Path, single_file: Option<&Path>) -> PathBuf {
|
|
||||||
match single_file {
|
|
||||||
Some(p) => PathBuf::from(p.file_name().unwrap_or_default()),
|
|
||||||
None => PathBuf::from(drawer_file.file_stem().unwrap_or_default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_drawer_extension(drawer_file: &Path) -> Result<(), DrawerError> {
|
|
||||||
if drawer_file.extension().and_then(|e| e.to_str()) != Some("drawer") {
|
|
||||||
return Err(DrawerError::DrawerFileInvalidExtension(drawer_file.to_path_buf()));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_drawer_file_exists(drawer_file: &Path) -> Result<(), DrawerError> {
|
|
||||||
if !drawer_file.exists() {
|
|
||||||
return Err(DrawerError::DrawerFileNotFound(drawer_file.to_path_buf()));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_ssh_key(key: &Option<PathBuf>) -> Result<(), DrawerError> {
|
|
||||||
let kp = key_path(key)?;
|
|
||||||
let file = std::fs::File::open(&kp)
|
|
||||||
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
|
||||||
let reader = std::io::BufReader::new(file);
|
|
||||||
let id = age::ssh::Identity::from_buffer(reader, None)
|
|
||||||
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
|
||||||
age::ssh::Recipient::try_from(id)
|
|
||||||
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ArchiveInfo {
|
enum ArchiveInfo {
|
||||||
SingleFile { size: u64 },
|
SingleFile { size: u64 },
|
||||||
Folder { file_count: usize },
|
Folder { file_count: usize },
|
||||||
@@ -205,11 +131,16 @@ fn single_file_entry(data: &[u8]) -> Option<PathBuf> {
|
|||||||
Some(entry.path().ok()?.into_owned())
|
Some(entry.path().ok()?.into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenOperation {
|
impl Operation for OpenOperation {
|
||||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
fn validate(&self) -> Result<(), DrawerError> {
|
||||||
validate_drawer_extension(&self.drawer_file)?;
|
validate_drawer_extension(&self.drawer_file)?;
|
||||||
validate_ssh_key(&self.key)?;
|
validate_ssh_key(&self.key)?;
|
||||||
validate_drawer_file_exists(&self.drawer_file)?;
|
validate_drawer_file_exists(&self.drawer_file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&self) -> Result<(), DrawerError> {
|
||||||
|
self.validate()?;
|
||||||
|
|
||||||
let encrypted = std::fs::read(&self.drawer_file)
|
let encrypted = std::fs::read(&self.drawer_file)
|
||||||
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
||||||
@@ -254,39 +185,10 @@ impl OpenOperation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CloseOperation {
|
impl Operation for CloseOperation {
|
||||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
fn validate(&self) -> Result<(), DrawerError> {
|
||||||
validate_drawer_extension(&self.drawer_file)?;
|
validate_drawer_extension(&self.drawer_file)?;
|
||||||
validate_ssh_key(&self.key)?;
|
validate_ssh_key(&self.key)?;
|
||||||
self.validate_close()?;
|
|
||||||
|
|
||||||
let target = self.target_path.as_ref().cloned()
|
|
||||||
.unwrap_or_else(|| default_target(&self.drawer_file, None));
|
|
||||||
|
|
||||||
let mut buf: Vec<u8> = 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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_close(&self) -> Result<(), DrawerError> {
|
|
||||||
let target = self.target_path.as_ref().cloned()
|
let target = self.target_path.as_ref().cloned()
|
||||||
.unwrap_or_else(|| default_target(&self.drawer_file, None));
|
.unwrap_or_else(|| default_target(&self.drawer_file, None));
|
||||||
if self.target_path.is_none() {
|
if self.target_path.is_none() {
|
||||||
@@ -315,20 +217,42 @@ impl CloseOperation {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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<u8> = 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 KeyOperation {
|
impl Operation for KeyOperation {
|
||||||
pub 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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate(&self) -> Result<(), DrawerError> {
|
fn validate(&self) -> Result<(), DrawerError> {
|
||||||
|
if self.filename.exists() {
|
||||||
|
return Err(DrawerError::KeyOutputFailed(self.filename.clone()));
|
||||||
|
}
|
||||||
let parent = match self.filename.parent() {
|
let parent = match self.filename.parent() {
|
||||||
None => Path::new("."),
|
None => Path::new("."),
|
||||||
Some(p) if p == Path::new("") => Path::new("."),
|
Some(p) if p == Path::new("") => Path::new("."),
|
||||||
@@ -339,13 +263,28 @@ impl KeyOperation {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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 InfoOperation {
|
impl Operation for InfoOperation {
|
||||||
pub fn perform(&self) -> Result<(), DrawerError> {
|
fn validate(&self) -> Result<(), DrawerError> {
|
||||||
validate_drawer_extension(&self.drawer_file)?;
|
validate_drawer_extension(&self.drawer_file)?;
|
||||||
validate_ssh_key(&self.key)?;
|
validate_ssh_key(&self.key)?;
|
||||||
validate_drawer_file_exists(&self.drawer_file)?;
|
validate_drawer_file_exists(&self.drawer_file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&self) -> Result<(), DrawerError> {
|
||||||
|
self.validate()?;
|
||||||
|
|
||||||
let encrypted = std::fs::read(&self.drawer_file)
|
let encrypted = std::fs::read(&self.drawer_file)
|
||||||
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use age::ssh::Identity;
|
||||||
|
use crate::error::DrawerError;
|
||||||
|
|
||||||
|
pub fn key_path(key: &Option<PathBuf>) -> Result<PathBuf, DrawerError> {
|
||||||
|
if let Some(k) = key {
|
||||||
|
return Ok(k.clone());
|
||||||
|
}
|
||||||
|
let val = std::env::var("DRAWER_KEY").map_err(|_| DrawerError::NoKeyPath)?;
|
||||||
|
Ok(PathBuf::from(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identity(key: &Option<PathBuf>, drawer_file: &Path) -> Result<(PathBuf, Identity), DrawerError> {
|
||||||
|
let kp = key_path(key)?;
|
||||||
|
let file = std::fs::File::open(&kp)
|
||||||
|
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
||||||
|
let reader = std::io::BufReader::new(file);
|
||||||
|
let id = age::ssh::Identity::from_buffer(reader, None)
|
||||||
|
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||||
|
Ok((kp, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(encrypted: Vec<u8>, key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
||||||
|
let (kp, id) = identity(key, drawer_file)?;
|
||||||
|
let decryptor = age::Decryptor::new(encrypted.as_slice())
|
||||||
|
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||||
|
let mut decrypted: Vec<u8> = Vec::new();
|
||||||
|
let mut stream = decryptor.decrypt(std::iter::once(&id as &dyn age::Identity))
|
||||||
|
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||||
|
std::io::Read::read_to_end(&mut stream, &mut decrypted)
|
||||||
|
.map_err(|_| DrawerError::DecryptFailed(drawer_file.to_path_buf(), kp.clone()))?;
|
||||||
|
Ok(decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(buf: &[u8], key: &Option<PathBuf>, drawer_file: &Path) -> Result<Vec<u8>, DrawerError> {
|
||||||
|
let (_, id) = identity(key, drawer_file)?;
|
||||||
|
let recipient = age::ssh::Recipient::try_from(id)
|
||||||
|
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||||
|
let mut encrypted: Vec<u8> = Vec::new();
|
||||||
|
let encryptor = age::Encryptor::with_recipients(
|
||||||
|
std::iter::once(&recipient as &dyn age::Recipient)
|
||||||
|
).map_err(|_| DrawerError::EncryptFailed)?;
|
||||||
|
let mut writer = encryptor.wrap_output(&mut encrypted)
|
||||||
|
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||||
|
writer.write_all(buf).map_err(|_| DrawerError::EncryptFailed)?;
|
||||||
|
writer.finish().map_err(|_| DrawerError::EncryptFailed)?;
|
||||||
|
Ok(encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_target(drawer_file: &Path, single_file: Option<&Path>) -> PathBuf {
|
||||||
|
match single_file {
|
||||||
|
Some(p) => PathBuf::from(p.file_name().unwrap_or_default()),
|
||||||
|
None => PathBuf::from(drawer_file.file_stem().unwrap_or_default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_drawer_extension(drawer_file: &Path) -> Result<(), DrawerError> {
|
||||||
|
if drawer_file.extension().and_then(|e| e.to_str()) != Some("drawer") {
|
||||||
|
return Err(DrawerError::DrawerFileInvalidExtension(drawer_file.to_path_buf()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_drawer_file_exists(drawer_file: &Path) -> Result<(), DrawerError> {
|
||||||
|
if !drawer_file.exists() {
|
||||||
|
return Err(DrawerError::DrawerFileNotFound(drawer_file.to_path_buf()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_ssh_key(key: &Option<PathBuf>) -> Result<(), DrawerError> {
|
||||||
|
let kp = key_path(key)?;
|
||||||
|
let file = std::fs::File::open(&kp)
|
||||||
|
.map_err(|_| DrawerError::KeyInvalid(kp.clone()))?;
|
||||||
|
let reader = std::io::BufReader::new(file);
|
||||||
|
let id = age::ssh::Identity::from_buffer(reader, None)
|
||||||
|
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
||||||
|
age::ssh::Recipient::try_from(id)
|
||||||
|
.map_err(|_| DrawerError::KeyNotValidSsh)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DRAWER=./target/debug/drawer
|
||||||
|
TEST_DIR="$(cd "$(dirname "$0")/test" && pwd)"
|
||||||
|
WORK="$TMPDIR/drawer_test_$$"
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
|
||||||
|
pass() { echo "PASS: $1"; }
|
||||||
|
fail() { echo "FAIL: $1"; exit 1; }
|
||||||
|
|
||||||
|
# Expect a command to succeed
|
||||||
|
ok() {
|
||||||
|
local label="$1"; shift
|
||||||
|
local rc=0
|
||||||
|
"$@" 2>/dev/null || rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
pass "$label"
|
||||||
|
else
|
||||||
|
fail "$label (exited $rc)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Expect a command to fail
|
||||||
|
err() {
|
||||||
|
local label="$1"; shift
|
||||||
|
local rc=0
|
||||||
|
"$@" 2>/dev/null || rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
fail "$label (expected failure but succeeded)"
|
||||||
|
else
|
||||||
|
pass "$label"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cargo build -q
|
||||||
|
|
||||||
|
# ── key ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
KEY="$WORK/test.key"
|
||||||
|
|
||||||
|
ok "key: generate" $DRAWER key "$KEY"
|
||||||
|
err "key: refuse existing file" $DRAWER key "$KEY"
|
||||||
|
err "key: refuse missing parent" $DRAWER key "$WORK/nosuchdir/test.key"
|
||||||
|
|
||||||
|
# ── close ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DRAWER_FILE="$WORK/test.drawer"
|
||||||
|
|
||||||
|
ok "close: pack test dir" $DRAWER close -i "$KEY" "$DRAWER_FILE" "$TEST_DIR"
|
||||||
|
err "close: refuse without key" DRAWER_KEY="" $DRAWER close "$DRAWER_FILE" "$TEST_DIR"
|
||||||
|
err "close: refuse bad key" $DRAWER close -i "$WORK/notakey" "$DRAWER_FILE" "$TEST_DIR"
|
||||||
|
err "close: refuse bad extension" $DRAWER close -i "$KEY" "$WORK/test.notadrawer" "$TEST_DIR"
|
||||||
|
|
||||||
|
# ── open ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
OUT="$WORK/out"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
ok "open: unpack drawer" $DRAWER open -i "$KEY" "$DRAWER_FILE" "$OUT"
|
||||||
|
ok "open: file1.md present" test -f "$OUT/file1.md"
|
||||||
|
ok "open: file2.md present" test -f "$OUT/file2.md"
|
||||||
|
ok "open: contents match file1.md" diff "$TEST_DIR/file1.md" "$OUT/file1.md"
|
||||||
|
ok "open: contents match file2.md" diff "$TEST_DIR/file2.md" "$OUT/file2.md"
|
||||||
|
|
||||||
|
err "open: refuse non-empty target" $DRAWER open -i "$KEY" "$DRAWER_FILE" "$OUT"
|
||||||
|
ok "open: force into non-empty" $DRAWER open -i "$KEY" -f "$DRAWER_FILE" "$OUT"
|
||||||
|
|
||||||
|
err "open: refuse bad extension" $DRAWER open -i "$KEY" "$WORK/test.notadrawer" "$OUT"
|
||||||
|
err "open: refuse missing drawer" $DRAWER open -i "$KEY" "$WORK/nosuch.drawer" "$OUT"
|
||||||
|
err "open: refuse without key" $DRAWER open "$DRAWER_FILE" "$OUT"
|
||||||
|
err "open: refuse bad key" $DRAWER open -i "$WORK/notakey" "$DRAWER_FILE" "$OUT"
|
||||||
|
|
||||||
|
# ── open: inferred target path ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
INFER_DRAWER="$WORK/inferred.drawer"
|
||||||
|
INFER_OUT="$WORK/inferred"
|
||||||
|
|
||||||
|
ok "close: inferred source" $DRAWER close -i "$KEY" "$INFER_DRAWER" "$TEST_DIR"
|
||||||
|
mkdir -p "$INFER_OUT"
|
||||||
|
ok "open: inferred target" $DRAWER open -i "$KEY" "$INFER_DRAWER" "$INFER_OUT"
|
||||||
|
ok "open: inferred file1.md" test -f "$INFER_OUT/file1.md"
|
||||||
|
|
||||||
|
# ── info ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$DRAWER info -i "$KEY" "$DRAWER_FILE" 2>/dev/null | grep -q "folder, 2 files" \
|
||||||
|
&& pass "info: reports folder" || fail "info: reports folder"
|
||||||
|
err "info: refuse bad extension" $DRAWER info -i "$KEY" "$WORK/test.notadrawer"
|
||||||
|
err "info: refuse missing drawer" $DRAWER info -i "$KEY" "$WORK/nosuch.drawer"
|
||||||
|
err "info: refuse without key" $DRAWER info "$DRAWER_FILE"
|
||||||
|
|
||||||
|
# ── single-file drawer ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SINGLE_SRC="$TEST_DIR/file1.md"
|
||||||
|
SINGLE_DRAWER="$WORK/file1.drawer"
|
||||||
|
SINGLE_OUT_DIR="$WORK/single_out"
|
||||||
|
mkdir -p "$SINGLE_OUT_DIR"
|
||||||
|
|
||||||
|
ok "close: single file" $DRAWER close -i "$KEY" "$SINGLE_DRAWER" "$SINGLE_SRC"
|
||||||
|
$DRAWER info -i "$KEY" "$SINGLE_DRAWER" 2>/dev/null | grep -q "file, " \
|
||||||
|
&& pass "info: reports single file" || fail "info: reports single file"
|
||||||
|
ok "open: single file" $DRAWER open -i "$KEY" "$SINGLE_DRAWER" "$SINGLE_OUT_DIR/file1.md"
|
||||||
|
ok "open: single file contents" diff "$SINGLE_SRC" "$SINGLE_OUT_DIR/file1.md"
|
||||||
|
|
||||||
|
# ── close: drawer-inside-target guard ────────────────────────────────────────
|
||||||
|
|
||||||
|
# Pending
|
||||||
|
# err "close: refuse drawer inside target" $DRAWER close -i "$KEY" "$TEST_DIR/../test.drawer" "$TEST_DIR"
|
||||||
|
|
||||||
|
# ── env var key ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ENV_OUT="$WORK/env_out"
|
||||||
|
mkdir -p "$ENV_OUT"
|
||||||
|
|
||||||
|
ok "open: key from DRAWER_KEY env" env DRAWER_KEY="$KEY" $DRAWER open "$DRAWER_FILE" "$ENV_OUT"
|
||||||
|
ok "open: env key file1.md" test -f "$ENV_OUT/file1.md"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
rm -rf "$WORK"
|
||||||
|
echo ""
|
||||||
|
echo "All tests passed."
|
||||||
Reference in New Issue
Block a user