use super::places::Places;
use super::{Laid, roster};
use crate::wire::material::{ANCHORS, Role};
use crate::wire::provision::LOOPBACK;
use crate::xdg::Env;
use std::path::PathBuf;
pub const SUBCMD: &str = "fixture";
pub const READS: [&str; 3] = ["FIXTURE_ROOT", "WIRE_HOST", "WIRE_PORT"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Plan {
List,
Lay {
state: String,
root: PathBuf,
address: String,
},
Refuse(String),
}
pub fn plan(
ambient: &Env,
state: Option<String>,
root: Option<String>,
host: Option<String>,
port: Option<String>,
) -> Plan {
let Some(state) = stated(state) else {
return Plan::List;
};
if roster::resolve(&state).is_none() {
return Plan::Refuse(format!(
"yog {SUBCMD}: {state:?} is not a state — try one of: {}",
roster::names().join(", ")
));
}
let root = stated(root).map_or_else(|| default_root(ambient, &state), PathBuf::from);
if let Some(refusal) = overlaps(&root, &ambient.yog_data_root()) {
return Plan::Refuse(refusal);
}
Plan::Lay {
address: format!(
"{}:{}",
stated(host).unwrap_or_else(|| LOOPBACK.to_owned()),
stated(port).unwrap_or_else(free_port)
),
state,
root,
}
}
fn default_root(ambient: &Env, state: &str) -> PathBuf {
ambient.yog_cache_root().join("fixture").join(state)
}
fn overlaps(root: &std::path::Path, live: &std::path::Path) -> Option<String> {
let (a, b) = (with_slash(root), with_slash(live));
if !a.starts_with(&b) && !b.starts_with(&a) {
return None;
}
Some(format!(
"yog {SUBCMD}: refusing to lay a fixture over the LIVE world.\n \
fixture: {}\n live: {}\n \
A lay wipes its root before it writes, so the two may never overlap. \
Point {} somewhere else; the live world is the engine's.",
root.display(),
live.display(),
READS[0]
))
}
fn with_slash(path: &std::path::Path) -> String {
format!("{}/", path.display().to_string().trim_end_matches('/'))
}
fn free_port() -> String {
port_of(std::net::TcpListener::bind((LOOPBACK, 0)).and_then(|l| l.local_addr()))
}
fn port_of(bound: std::io::Result<std::net::SocketAddr>) -> String {
bound.map_or_else(
|_| crate::wire::provision::PORT.to_owned(),
|a| a.port().to_string(),
)
}
fn stated(value: Option<String>) -> Option<String> {
value.filter(|v| !v.is_empty())
}
pub fn perform(plan: &Plan) -> i32 {
match plan {
Plan::List => {
for (name, recipe) in roster::ROSTER {
println!("{name:<12} {}", recipe.summary);
}
0
}
Plan::Refuse(sentence) => {
eprintln!("{sentence}");
1
}
Plan::Lay {
state,
root,
address,
} => match perform_lay(state, root, address) {
Ok(laid) => {
println!("{}", laid.json());
0
}
Err(e) => {
eprintln!("yog {SUBCMD}: {e}");
1
}
},
}
}
pub fn perform_lay(state: &str, root: &PathBuf, address: &str) -> Result<Laid, String> {
let recipe = roster::resolve(state).ok_or_else(|| format!("{state:?} is not a state"))?;
if let Err(e) = std::fs::remove_dir_all(root)
&& e.kind() != std::io::ErrorKind::NotFound
{
return Err(format!("clear {}: {e}", root.display()));
}
let origin = now_unix();
let hold = super::lay::lay(root, recipe, origin)?;
let places = Places::under(root);
crate::wire::provision::mint(&places.wire, address, &[], false)?;
let leaf = Role::Client.leaf();
Ok(Laid {
state: state.to_owned(),
root: root.clone(),
address: address.to_owned(),
anchors: places.wire.join(ANCHORS),
chain: places.wire.join(format!("{leaf}.pem")),
key: places.wire.join(format!("{leaf}.key")),
origin,
hold,
})
}
fn now_unix() -> i64 {
unix_of(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH))
}
fn unix_of(since: Result<std::time::Duration, std::time::SystemTimeError>) -> i64 {
since.map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
}
pub fn stray(tail: &[String]) -> Option<String> {
let word = tail.get(1)?;
Some(format!(
"yog {SUBCMD}: {word:?} is a second word — one state per lay, and {} are \
environment readings, so they go BEFORE the verb: \
`{}=<dir> yog {SUBCMD} <state>`",
READS.join(", "),
READS[0]
))
}
#[cfg(test)]
mod tests;