use clap::{Parser, Subcommand};
use std::ffi::OsString;
use std::io::{self, Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use syntheca::{
Cella, DepositError, Digest256, GetError, GetPinaxError, Hash, Name, SetPinaxError,
SetPinaxOutcome, StatError,
};
#[derive(Parser)]
#[command(name = "syn", version, about = "syntheca content-addressable store")]
struct Cli {
#[arg(long, global = true)]
cella: Option<PathBuf>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Deposit {
path: OsString,
},
Get { hash: String },
Stat { hash: String },
Pinax {
#[command(subcommand)]
cmd: PinaxCmd,
},
}
#[derive(Subcommand)]
enum PinaxCmd {
Get { name: OsString },
Set {
#[arg(long)]
name: OsString,
#[arg(long, conflicts_with = "expect")]
expect_absent: bool,
#[arg(long, conflicts_with = "expect_absent")]
expect: Option<String>,
path: OsString,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
let cella_root = cli.cella.unwrap_or_else(default_cella_root);
let cella = match Cella::open(&cella_root) {
Ok(c) => c,
Err(e) => {
let _ = writeln!(
io::stderr(),
"syn: open cella {}: {e}",
cella_root.display()
);
return ExitCode::from(1);
}
};
match cli.cmd {
Cmd::Deposit { path } => cmd_deposit(&cella, path),
Cmd::Get { hash } => cmd_get(&cella, hash),
Cmd::Stat { hash } => cmd_stat(&cella, hash),
Cmd::Pinax { cmd } => match cmd {
PinaxCmd::Get { name } => cmd_pinax_get(&cella, name),
PinaxCmd::Set {
name,
expect_absent,
expect,
path,
} => cmd_pinax_set(&cella, name, expect_absent, expect, path),
},
}
}
fn cmd_deposit(cella: &Cella, path: OsString) -> ExitCode {
let from_stdin = path.as_bytes() == b"-";
let bytes = match read_input(from_stdin, &path) {
Ok(v) => v,
Err(code) => return code,
};
match cella.deposit(&bytes) {
Ok(hash) => {
let mut out = io::stdout().lock();
if writeln!(out, "{hash}").is_err() {
return ExitCode::from(1);
}
ExitCode::from(0)
}
Err(DepositError::HashCollision) => {
let _ = writeln!(
io::stderr(),
"syn: hash collision: distinct bytes hashed to an existing depositum's name"
);
ExitCode::from(1)
}
Err(e) => {
let _ = writeln!(io::stderr(), "syn: deposit: {e}");
ExitCode::from(1)
}
}
}
fn cmd_get(cella: &Cella, hash_str: String) -> ExitCode {
let hash = match Hash::from_hex(&hash_str) {
Ok(h) => h,
Err(e) => {
let _ = writeln!(io::stderr(), "syn: invalid hash: {e}");
return ExitCode::from(1);
}
};
match cella.get(&hash) {
Ok(bytes) => {
let mut out = io::stdout().lock();
if let Err(e) = out.write_all(&bytes) {
let _ = writeln!(io::stderr(), "syn: write stdout: {e}");
return ExitCode::from(1);
}
ExitCode::from(0)
}
Err(GetError::NotFound) => {
let _ = writeln!(io::stderr(), "syn: not found");
ExitCode::from(1)
}
Err(GetError::IntegrityError) => {
let _ = writeln!(
io::stderr(),
"syn: integrity error: stored bytes do not match expected hash"
);
ExitCode::from(1)
}
Err(e) => {
let _ = writeln!(io::stderr(), "syn: get: {e}");
ExitCode::from(1)
}
}
}
fn cmd_stat(cella: &Cella, hash_str: String) -> ExitCode {
let hash = match Hash::from_hex(&hash_str) {
Ok(h) => h,
Err(e) => {
let _ = writeln!(io::stderr(), "syn: invalid hash: {e}");
return ExitCode::from(1);
}
};
match cella.stat(&hash) {
Ok(stat) => {
let mut out = io::stdout().lock();
if writeln!(out, "size {}", stat.size).is_err()
|| writeln!(out, "sha256 {}", hex::encode(stat.sha256)).is_err()
{
return ExitCode::from(1);
}
ExitCode::from(0)
}
Err(StatError::NotFound) => {
let _ = writeln!(io::stderr(), "syn: not found");
ExitCode::from(1)
}
Err(e) => {
let _ = writeln!(io::stderr(), "syn: stat: {e}");
ExitCode::from(1)
}
}
}
fn cmd_pinax_get(cella: &Cella, name_os: OsString) -> ExitCode {
let name = match Name::new(name_os.as_bytes()) {
Ok(n) => n,
Err(e) => {
let _ = writeln!(io::stderr(), "syn: invalid name: {e}");
return ExitCode::from(1);
}
};
match cella.get_pinax(&name) {
Ok(bytes) => {
let mut out = io::stdout().lock();
if let Err(e) = out.write_all(&bytes) {
let _ = writeln!(io::stderr(), "syn: write stdout: {e}");
return ExitCode::from(1);
}
ExitCode::from(0)
}
Err(GetPinaxError::NotFound) => {
let _ = writeln!(io::stderr(), "syn: not found");
ExitCode::from(1)
}
Err(GetPinaxError::IntegrityError) => {
let _ = writeln!(
io::stderr(),
"syn: integrity error: stored bytes do not match digest"
);
ExitCode::from(1)
}
Err(e) => {
let _ = writeln!(io::stderr(), "syn: pinax get: {e}");
ExitCode::from(1)
}
}
}
fn cmd_pinax_set(
cella: &Cella,
name_os: OsString,
expect_absent: bool,
expect: Option<String>,
path: OsString,
) -> ExitCode {
if !expect_absent && expect.is_none() {
let _ = writeln!(
io::stderr(),
"syn: pinax set requires --expect-absent or --expect <hex>"
);
return ExitCode::from(1);
}
let name = match Name::new(name_os.as_bytes()) {
Ok(n) => n,
Err(e) => {
let _ = writeln!(io::stderr(), "syn: invalid name: {e}");
return ExitCode::from(1);
}
};
let expected: Option<Digest256> = if expect_absent {
None
} else {
let hex = expect.as_deref().unwrap();
match parse_hex_digest(hex) {
Some(d) => Some(d),
None => {
let _ = writeln!(
io::stderr(),
"syn: --expect must be 64 lowercase hex digits"
);
return ExitCode::from(1);
}
}
};
let from_stdin = path.as_bytes() == b"-";
let bytes = match read_input(from_stdin, &path) {
Ok(v) => v,
Err(code) => return code,
};
match cella.set_pinax(&name, &bytes, expected) {
Ok(SetPinaxOutcome::Ok) => ExitCode::from(0),
Ok(SetPinaxOutcome::Conflict { actual }) => {
let msg = match actual {
None => "conflict: actual=absent".to_string(),
Some(d) => format!("conflict: actual={}", hex::encode(d)),
};
let _ = writeln!(io::stderr(), "syn: {msg}");
ExitCode::from(1)
}
Err(SetPinaxError::InvalidName(e)) => {
let _ = writeln!(io::stderr(), "syn: invalid name: {e}");
ExitCode::from(1)
}
Err(SetPinaxError::Io(e)) => {
let _ = writeln!(io::stderr(), "syn: pinax set: {e}");
ExitCode::from(1)
}
}
}
fn read_input(from_stdin: bool, path: &OsString) -> Result<Vec<u8>, ExitCode> {
if from_stdin {
let mut v = Vec::new();
if let Err(e) = io::stdin().lock().read_to_end(&mut v) {
let _ = writeln!(io::stderr(), "syn: read stdin: {e}");
return Err(ExitCode::from(1));
}
Ok(v)
} else {
match std::fs::read(Path::new(path)) {
Ok(v) => Ok(v),
Err(e) => {
let _ = writeln!(io::stderr(), "syn: read {:?}: {e}", path);
Err(ExitCode::from(1))
}
}
}
}
fn parse_hex_digest(s: &str) -> Option<Digest256> {
if s.len() != 64 || !s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
return None;
}
let mut buf = [0u8; 32];
hex::decode_to_slice(s, &mut buf).ok()?;
Some(buf)
}
fn default_cella_root() -> PathBuf {
let home = std::env::var_os("HOME").unwrap_or_else(|| OsString::from("."));
let mut p = PathBuf::from(home);
p.push(".syntheca");
p
}