use std::io::Write as _;
use std::path::PathBuf;
use std::process::ExitCode;
use haematite::db::ReadOnlyDatabase;
pub const USAGE: &str =
"usage: haematite observe-get <data_dir> (--key <text> | --key-hex <hex>) [--hex]";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Output {
Raw,
Hex,
}
#[derive(Debug, PartialEq, Eq)]
struct Request {
data_dir: PathBuf,
key: Vec<u8>,
output: Output,
}
#[must_use]
pub fn run(args: impl Iterator<Item = String>) -> ExitCode {
let request = match parse(args) {
Ok(request) => request,
Err(message) => {
eprintln!("{message}\n{USAGE}");
return ExitCode::from(2);
}
};
let observer = match ReadOnlyDatabase::open(&request.data_dir) {
Ok(observer) => observer,
Err(error) => {
eprintln!("refused: {}: {error}", request.data_dir.display());
return ExitCode::from(2);
}
};
match observer.get(&request.key) {
Ok(Some(value)) => match write(&value, request.output) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("refused: standard output: {error}");
ExitCode::from(2)
}
},
Ok(None) => {
eprintln!("absent: no committed value at that key");
ExitCode::from(1)
}
Err(error) => {
eprintln!("refused: {error}");
ExitCode::from(2)
}
}
}
fn write(value: &[u8], output: Output) -> std::io::Result<()> {
let mut stdout = std::io::stdout().lock();
match output {
Output::Raw => stdout.write_all(value)?,
Output::Hex => {
for byte in value {
write!(stdout, "{byte:02x}")?;
}
writeln!(stdout)?;
}
}
stdout.flush()
}
fn parse(mut args: impl Iterator<Item = String>) -> Result<Request, String> {
let data_dir = args.next().ok_or("observe-get needs a data directory")?;
let mut key = None;
let mut output = Output::Raw;
while let Some(flag) = args.next() {
match flag.as_str() {
"--key" | "--key-hex" if key.is_some() => {
return Err("observe-get takes one key".to_owned());
}
"--key" => {
key = Some(args.next().ok_or("--key needs a value")?.into_bytes());
}
"--key-hex" => {
key = Some(from_hex(&args.next().ok_or("--key-hex needs a value")?)?);
}
"--hex" if output == Output::Hex => {
return Err("--hex is repeated".to_owned());
}
"--hex" => output = Output::Hex,
other => return Err(format!("unknown argument {other:?}")),
}
}
Ok(Request {
data_dir: PathBuf::from(data_dir),
key: key.ok_or("observe-get needs --key or --key-hex")?,
output,
})
}
fn from_hex(text: &str) -> Result<Vec<u8>, String> {
if text.is_empty() || !text.len().is_multiple_of(2) {
return Err(format!(
"--key-hex {text:?} is not an even number of hex digits"
));
}
text.as_bytes()
.chunks(2)
.map(|pair| {
std::str::from_utf8(pair)
.ok()
.and_then(|digits| u8::from_str_radix(digits, 16).ok())
.ok_or_else(|| format!("--key-hex {text:?} holds a non-hex digit"))
})
.collect()
}
#[cfg(test)]
#[path = "observe_tests.rs"]
mod tests;