wot-network 0.0.6

Data structures for OpenPGP Web of Trust calculations
Documentation
//! Import/export functionality for a simplistic but human-friendly textual representation
//! of [Network].
//!
//! This format is not generally parseable when identifiers use special characters that collide
//! with the format's separators. There's no suitable escaping.
//!
//! For more robust representation of [Network]s, a different format must be used.

use std::collections::BTreeSet;

use crate::{Binding, Network, Regex};

/// Import [Network] from WoT text format.
///
/// NOTE: the format and this parser are currently very optimistic, and not robust at all.
/// The primary goal of this format is currently human-readability, at the cost of robustness.
///
/// The format will probably be (incompatibly) changed towards a more robust one, later.
///
///
/// The "simple text" wot network format consists of a text file that is split into two blocks.
/// Each block is terminated with a trailing empty line.
///
/// The first block specifies certifications, one per line. E.g.:
///
/// `C -> (T, <target@foo.example>) 120`
///
/// In this line, the certificate `C` certifies a binding between certificate `T` and the identity
/// `<target@foo.example>`, and assigns an amount of `120` to that certification.
///
/// - The name of the issuing certificate is followed by ` -> `
/// - The binding is delimited with `(` and `)` characters, its two components are separated by `, `
/// - The binding is followed by a space, and the numerical value of the amount.
///
/// The second block specifies delegations, one per line. E.g.:
///
/// `B -> C 60/2`
///
/// In this line, the certificate `B` delegates trust decisions to certificate `C`
/// It assigns and amount of `60` and a depth of `2` to the delegation.
///
/// - The name of the issuing certificate is followed by ` -> `
/// - Followed by the target certificate name, followed by a space
/// - The delegation amount and depth are separated by `/`
///
/// Also see <https://codeberg.org/heiko/wot-search-tests> for some test data in this format.
pub fn import(data: &str) -> Network {
    let mut net = Network::new();

    enum State {
        Certifications,
        Delegations,
    }

    let mut state = State::Certifications;

    for line in data.lines() {
        if line.is_empty() {
            if matches!(state, State::Certifications) {
                state = State::Delegations;
            }

            continue;
        }

        match state {
            State::Certifications => {
                let (issuer, rest) = line.split_once(" -> ").expect("FIXME");

                assert!(rest.starts_with('('));
                assert!(rest.ends_with(')'));

                let rest = &rest[1..rest.len() - 1];
                let (target_id, target_user) = rest.split_once(", ").expect("FIXME");

                net.add_binding(
                    issuer.into(),
                    Binding {
                        cert: target_id.into(),
                        identity: target_user.into(),
                    },
                );
            }
            State::Delegations => {
                // FIXME: this is all rather optimistic
                let (rest, regexes) = if line.ends_with(']') {
                    let (rest, regex) = line.split_once(" [\"").unwrap();
                    assert!(regex.ends_with("\"]"));
                    let regex = &regex[0..regex.len() - 2];

                    let regexes: Vec<_> = regex
                        .split("\", \"")
                        .map(|r| Regex::new(r.to_string()))
                        .collect();

                    (rest, regexes)
                } else {
                    (line, vec![])
                };

                let (issuer, rest) = rest.split_once(" -> ").expect("FIXME");

                let pos = rest.rfind(' ').expect("FIXME");
                let (target, trust) = rest.split_at(pos);

                let trust = &trust[1..]; // drop space
                let (amount, depth) = trust.split_once("/").expect("FIXME");

                let amount: u8 = amount.parse().expect("FIXME");
                let depth: u8 = depth.parse().expect("FIXME");

                net.add_delegation(issuer.into(), target.into(), amount, depth.into(), regexes);
            }
        }
    }
    net
}

/// Export as WoT text format.
///
/// NOTE: the format and this parser are currently very optimistic, and not robust at all.
/// The primary goal of this format is currently human-readability, at the cost of robustness.
///
/// The format will probably be (incompatibly) changed towards a more robust one, later.
pub fn export(network: &Network) -> String {
    let mut bindings = BTreeSet::new();

    for certification in network.certifications.values().flatten() {
        bindings.insert(format!(
            "{} -> ({}, {})\n",
            certification.issuer.0, certification.target.cert.0, certification.target.identity.0,
        ));
    }

    let mut delegations = BTreeSet::new();

    for delegation in network.delegations.values().flatten() {
        let regex = if delegation.regexes.is_empty() {
            "".to_string()
        } else {
            format!(
                " [{}]",
                delegation
                    .regexes
                    .iter()
                    .map(|r| format!("\"{}\"", &r.0))
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        };

        delegations.insert(format!(
            "{} -> {} {}/{}{}\n",
            delegation.issuer.0,
            delegation.target.0,
            delegation.trust_amount,
            u8::from(delegation.trust_depth),
            regex
        ));
    }

    let mut out = String::new();

    for binding in bindings {
        out.push_str(&binding);
    }

    out.push('\n');

    for delegation in delegations {
        out.push_str(&delegation);
    }

    out
}