3.0 KiB
{{#include ../links.md}}
Writing Binaries
Binaries - Porcelain and Plumbing
Binaries are split into two categories, porcelain (such as keyfork
) and
plumbing (just about everything else). Porcelain binaries include what can be
called "the kitchen sink". They offer support for everything - an intuitive
interface, automatic keyforkd
management, interconnectivity between
derivation utilities and provisioning utilities, and the ability to read from
and write to a configuration file. Plumbing binaries, on the other hand, are
often very rough around the edges and pull in as few dependencies as possible.
Usually, only cryptographic functionality (such as sequoia-openpgp
or
dalek-ed25519
) or hardware integration libraries (such as openpgp-card
) are
included.
Writing Binaries
Crates can be either a library, or a library and binary, but should never be
just a binary. When creating a crate with a binary, the main.rs
file should
be designed to validate arguments, load any necessary system resources, and
call a separate exposed function to do the heavy lifting. The following example
was taken from keyfork-shard
to demonstrate how a program can validate
arguments, parse input, and stream an output.
use std::{
collections::VecDeque,
env,
io::{stdin, stdout},
path::PathBuf,
process::ExitCode,
str::FromStr,
};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert};
use sequoia_openpgp as openpgp;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(threshold: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
let threshold = u8::from_str(threshold)?;
let key_discovery = PathBuf::from(key_discovery);
// Verify path exists
std::fs::metadata(&key_discovery)?;
// Load certs from path
let certs = discover_certs(key_discovery)?;
Ok((threshold, certs))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (threshold, cert_list) = match args.as_slice() {
[threshold, key_discovery] => validate(threshold, key_discovery)?,
_ => panic!("Usage: {program_name} threshold key_discovery"),
};
let encrypted_messages = parse_messages(stdin())?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
combine(
threshold,
cert_list,
encrypted_metadata,
encrypted_messages.into(),
stdout(),
)?;
Ok(())
}
fn main() -> ExitCode {
let result = run();
if let Err(e) = result {
eprintln!("Error: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
Designing binaries with this format makes it easier to load them to the Keyfork
porcelain binary, since the porcelain can call combine()
with arguments that
it has parsed using its own configuration systems, using a String
as a mut Write
as necessary.