keyfork/docs/src/dev-guide/index.md

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.