refactor: split the library into fumi-core and make it embeddable

This commit is contained in:
randogoth 2026-09-28 23:21:08 +03:00
parent d3cd0afb4f
commit 8fb5661b53
28 changed files with 975 additions and 724 deletions

22
cli/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "fumi"
version = "0.1.0"
edition = "2021"
description = "Smol Mail client"
license = "Apache-2.0"
[[bin]]
name = "fumi"
path = "src/main.rs"
[features]
# Passes the core's carrier feature through; addresses select the carrier by
# scheme, so enabling it only affects smol+rns:// dials.
rns = ["fumi-core/rns"]
[dependencies]
fumi-core = { path = "../core" }
clap = { version = "4", features = ["derive"] }
anyhow = "1"
rand_core = { version = "0.6", features = ["getrandom"] }
data-encoding = "2"

863
cli/src/main.rs Normal file
View file

@ -0,0 +1,863 @@
//! The command line: one master per store, selected by `--key` and `--db`.
//! A second identity is a second pair of files.
use std::io::{IsTerminal, Read, Write};
use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use fumi_core as fumi;
use fumi::account::{identity_seed, Account};
use fumi::address::Address;
use fumi::client::{self, Pushed, SendDraft, TrustChange};
use fumi::crypto::{b32, fingerprint, unb32, KEY_LEN};
use fumi::store::Store;
use fumi::transport::ID_LEN;
#[derive(Parser)]
#[command(about = "Smol Mail client")]
struct Cli {
/// Master secret file
#[arg(long, global = true, default_value = "identity.key")]
key: PathBuf,
/// Local state and mail
#[arg(long, global = true, default_value = "fumi.db")]
db: PathBuf,
/// Network timeout in seconds
#[arg(long, global = true, default_value_t = 30)]
timeout: u64,
#[cfg(feature = "rns")]
#[command(flatten)]
rns: RnsArgs,
#[command(subcommand)]
command: Command,
}
/// RNS carrier flags; addresses select the carrier by scheme, so these apply
/// only to `smol+rns://` dials.
#[cfg(feature = "rns")]
#[derive(clap::Args)]
struct RnsArgs {
/// UDP interface to listen on, host[:port]
#[arg(long = "rns-udp", global = true, default_value = "127.0.0.1:0")]
udp: String,
/// UDP forward target, host[:port]; a client speaks first, so this should
/// point at an rnsd or at the server's UDP interface
#[arg(long = "rns-udp-forward", global = true, value_name = "HOST:PORT")]
udp_forward: Option<String>,
/// Reticulum storage directory
#[arg(long = "rns-storage", global = true, default_value = "fumi.rns")]
storage: String,
}
#[derive(Subcommand)]
enum Command {
/// Create a master secret
Keygen {
#[arg(long)]
force: bool,
},
/// Show this identity
Whoami,
/// Recover local state from the master alone
Restore { address: String },
/// Advance to the next identity, signing the change
Rotate,
/// Pin a server's Noise static key (TCP only)
Trust {
host: String,
key_b32: String,
#[arg(long)]
force: bool,
},
/// Bind this identity to a username
Register {
address: String,
/// Invite token, if the server requires one
#[arg(long)]
invite: Option<String>,
},
/// Look up a contact's key, walk the chain, pin it
Resolve { address: String },
/// Add a contact from a self-certifying address
Import { uri: String },
/// List known keys and how each was learned
Contacts,
/// Issue an accept token, admit to the main tier, sync the set
Accept { address: String },
/// Withdraw a contact's accept token, sync the set
Block { address: String },
/// Seal and deliver a message
Send {
address: String,
#[arg(long)]
subject: Option<String>,
/// Message text; stdin is read when neither --body nor --file is given
#[arg(long)]
body: Option<String>,
/// Read the message text from this file ("-" for stdin)
#[arg(long)]
file: Option<String>,
/// Extra `Key: value` frontmatter field; repeatable
#[arg(long = "header", value_name = "KEY:VALUE")]
headers: Vec<String>,
/// The id this message replies to, 64 hex characters
#[arg(long = "reply-to", value_name = "ID")]
reply_to: Option<String>,
/// Omit the Reply-To field carrying this address
#[arg(long)]
anonymous: bool,
/// Do not pad the payload to 1 KiB
#[arg(long = "no-pad")]
no_pad: bool,
},
/// Retrieve, verify, store and acknowledge mail
Fetch {
/// Do not delete from the server; remember the cursor instead
#[arg(long)]
keep: bool,
/// Forget the cursor and page again
#[arg(long)]
reset: bool,
},
/// Remove messages from the server explicitly
Delete {
/// Message ids, or unambiguous prefixes
ids: Vec<String>,
},
/// List stored mail
List {
#[arg(long)]
sent: bool,
/// Mail that arrived without an accept token
#[arg(long)]
requests: bool,
},
/// Show one message
Read {
id: String,
#[arg(long)]
sent: bool,
},
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("error: {e}");
hint(e.downcast_ref::<fumi::error::Error>());
std::process::exit(1);
}
}
/// The remedy for the errors whose remedy is a command to run. The library
/// reports the distinction; only the CLI knows the command.
fn hint(error: Option<&fumi::error::Error>) {
match error {
Some(fumi::error::Error::NotPinned { host }) => eprintln!(
"obtain the key from the operator through a trusted channel, then:\n fumi trust {host} <key>"
),
Some(fumi::error::Error::KeyChanged { address, offered, .. }) => {
if let Ok(addr) = Address::parse(address) {
eprintln!("verify out of band, then:\n fumi import {}", addr.uri(offered));
}
}
Some(fumi::error::Error::NotRegistered) => {
eprintln!("run: fumi register <user@host>")
}
_ => {}
}
}
fn run(cli: Cli) -> Result<()> {
let store = Store::open(&cli.db)?;
#[cfg(feature = "rns")]
fumi::rns::configure(rns_config(&cli));
match &cli.command {
Command::Keygen { force } => keygen(&cli.key, *force),
Command::Whoami => whoami(&cli.key, &store),
Command::Restore { address } => restore(&cli.key, &store, address, cli.timeout),
Command::Rotate => rotate(&cli.key, &store, cli.timeout),
Command::Trust {
host,
key_b32,
force,
} => trust(&store, host, key_b32, *force),
Command::Register { address, invite } => {
register(&cli.key, &store, address, invite.as_deref(), cli.timeout)
}
Command::Resolve { address } => resolve(&store, address, cli.timeout),
Command::Import { uri } => import(&store, uri),
Command::Contacts => contacts(&store),
Command::Accept { address } => accept(&cli.key, &store, address, cli.timeout),
Command::Block { address } => block(&cli.key, &store, address, cli.timeout),
Command::Send { .. } => send(&cli, &store),
Command::Fetch { keep, reset } => fetch(&cli.key, &store, *keep, *reset, cli.timeout),
Command::Delete { ids } => delete(&cli.key, &store, ids, cli.timeout),
Command::List { sent, requests } => list(&cli.key, &store, *sent, *requests),
Command::Read { id, sent } => read(&cli.key, &store, id, *sent),
}
}
fn warn(message: &str) {
eprintln!("warning: {message}");
}
fn load_master(path: &PathBuf) -> Result<[u8; KEY_LEN]> {
let bytes = std::fs::read(path).map_err(|_| {
anyhow!(format!(
"no identity at {}; run: fumi keygen",
path.display()
))
})?;
bytes.try_into().map_err(|v: Vec<u8>| {
anyhow!(format!(
"master secret at {} is {} bytes, expected {KEY_LEN}",
path.display(),
v.len()
))
})
}
/// The master from disk plus the rotation index from local state.
fn load_account(path: &PathBuf, store: &Store) -> Result<Account> {
Ok(Account::new(load_master(path)?, store.rotations()?)?)
}
/// Writes a secret with mode 0600 before any bytes land, so it is never
/// briefly world-readable.
fn write_secret(path: &PathBuf, secret: &[u8]) -> Result<()> {
#[cfg(unix)]
let file = {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
};
#[cfg(not(unix))]
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path);
let mut file = file.with_context(|| format!("cannot write {}", path.display()))?;
file.write_all(secret)?;
Ok(())
}
fn keygen(path: &PathBuf, force: bool) -> Result<()> {
if path.exists() && !force {
return Err(anyhow!(format!(
"{} exists; refusing to overwrite (use --force)",
path.display()
))
.into());
}
let mut master = [0u8; KEY_LEN];
use rand_core::RngCore;
rand_core::OsRng.fill_bytes(&mut master);
write_secret(path, &master)?;
let account = Account::new(master, 0)?;
println!(
"master: {} (back this up; it is the only secret)",
path.display()
);
println!("public key: {}", b32(&account.me().pk()));
println!("fingerprint: {}", fingerprint(&account.me().pk()));
Ok(())
}
fn whoami(key: &PathBuf, store: &Store) -> Result<()> {
let account = load_account(key, store)?;
println!("public key: {}", b32(&account.me().pk()));
println!("fingerprint: {}", fingerprint(&account.me().pk()));
match store.account()? {
Some(addr) => {
println!("address: {}", addr.short());
println!("uri: {}", addr.uri(&account.me().pk()));
}
None => println!("address: (not registered)"),
}
if account.index() > 0 {
println!(
"rotations: {} (earlier keys derived on demand)",
account.index()
);
}
let live = store
.contact_rows()?
.iter()
.filter(|(_, _, _, active)| *active == Some(1))
.count();
let sync = store.sync_ok()?;
println!(
"accepted: {live} correspondent(s){}",
if sync {
String::new()
} else {
", not pushed to the server until rebuilt".to_string()
}
);
Ok(())
}
fn restore(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<()> {
let master = load_master(key)?;
let addr = Address::parse(address)?;
let index = client::restore(store, &master, &addr, timeout)?;
let identity = fumi::account::Identity::from_seed(identity_seed(&master, index));
println!("restored {} at rotation {index}", addr.short());
println!("public key: {}", b32(&identity.pk()));
println!(
"Your accepted correspondents are still live on the server; `accept` them again only \
when you are ready to replace that set."
);
Ok(())
}
fn rotate(key: &PathBuf, store: &Store, timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let rotated = client::rotate(store, &account, timeout)?;
let addr = store.account()?.expect("rotate checked for an account");
println!("rotated {}", addr.short());
println!("new key: {}", b32(&rotated.new_pk));
println!("fingerprint: {}", fingerprint(&rotated.new_pk));
println!("share: {}", addr.uri(&rotated.new_pk));
println!();
println!("Tell your contacts; they will accept the change from the signed chain.");
Ok(())
}
fn trust(store: &Store, host: &str, key_b32: &str, force: bool) -> Result<()> {
let key: [u8; KEY_LEN] = unb32(key_b32)?.try_into().map_err(|v: Vec<u8>| {
anyhow!(format!(
"server key is {} bytes, expected {KEY_LEN}",
v.len()
))
})?;
// A pin is per server, and the reference client strips any :port the
// same way its address grammar does: at the first colon.
let host = host.split(':').next().unwrap_or(host);
match store.server_pin(host)? {
Some(old) if old != key && !force => {
return Err(anyhow!(format!(
"{host} is already pinned to {}; use --force to replace",
b32(&old)
))
.into());
}
_ => {}
}
store.pin_server(host, &key)?;
println!("pinned {host} {}", b32(&key));
Ok(())
}
fn register(
key: &PathBuf,
store: &Store,
address: &str,
invite: Option<&str>,
timeout: u64,
) -> Result<()> {
let account = load_account(key, store)?;
let addr = Address::parse(address)?;
client::register(store, &addr, &account, invite, timeout)?;
println!("registered {}", addr.short());
println!("share: {}", addr.uri(&account.me().pk()));
Ok(())
}
fn resolve(store: &Store, address: &str, timeout: u64) -> Result<()> {
let addr = Address::parse(address)?;
if addr.identity.is_some() {
return Err(
anyhow!("that address already carries a key; use `import` instead").into(),
);
}
let mut session = client::connect(store, &addr, false, timeout)?;
if let Some(key) = session.unpinned_static {
warn(&format!(
"{} is not pinned; its key is {}",
addr.host,
b32(&key)
));
}
let transport = session.transport();
let (identity, change) = client::trust_key(store, transport, &addr)?;
session.close();
match change {
TrustChange::New { unverified } => println!(
"{} {}\n pinned (trust on first use{})",
addr.short(),
b32(&identity),
if unverified {
", UNVERIFIED server"
} else {
""
}
),
TrustChange::Rotated => {
warn(&format!(
"{} rotated its key; a signed chain confirms it",
addr.short()
));
println!(" now {}", b32(&identity));
}
TrustChange::None => println!("{} {}", addr.short(), b32(&identity)),
}
Ok(())
}
fn import(store: &Store, uri: &str) -> Result<()> {
let addr = Address::parse(uri)?;
let key = addr
.identity
.ok_or_else(|| anyhow!("import needs a self-certifying address carrying a key"))?;
store.save_contact(&addr.short(), &key, true)?;
println!("imported {} {} (verified)", addr.short(), b32(&key));
println!("fingerprint: {}", fingerprint(&key));
Ok(())
}
fn contacts(store: &Store) -> Result<()> {
let rows = store.contact_rows()?;
if rows.is_empty() {
println!("no contacts");
return Ok(());
}
let width = rows.iter().map(|(a, _, _, _)| a.len()).max().unwrap_or(0);
for (address, identity, verified, active) in rows {
let state = match active {
Some(1) => "accepted",
Some(_) => "blocked",
None => "",
};
println!(
"{address:<width$} {} {:<8} {state}",
b32(&identity),
if verified { "verified" } else { "tofu" }
);
}
Ok(())
}
/// An address plus the key we hold for it, for commands naming a contact.
fn target_contact(store: &Store, text: &str) -> Result<(String, [u8; KEY_LEN])> {
let addr = Address::parse(text)?;
if let Some(key) = addr.identity {
return Ok((addr.short(), key));
}
match store.contact(&addr.short())? {
Some((key, _)) => Ok((addr.short(), key)),
None => Err(anyhow!(format!(
"no key for {}; run `resolve` or `import` first",
addr.short()
))
.into()),
}
}
fn accept(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let (address, identity) = target_contact(store, address)?;
if !store.sync_ok()? {
warn(
"this client's accepted set was not restored; from now on it replaces the server's, \
so re-accept everyone you still correspond with",
);
}
store.accept(&address, &identity)?;
println!("accepted {address}; its token travels in your next message to them");
match client::push_tokens(store, &account, timeout)? {
Pushed::NotRegistered => {
warn("not registered; the set will be pushed with your first fetch")
}
Pushed::Held(held) => println!("server now holds {held} accept token(s)"),
}
Ok(())
}
fn block(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let (address, _) = target_contact(store, address)?;
if !store.block(&address)? {
return Err(anyhow!(format!("{address} was never accepted")));
}
println!("blocked {address}; their mail lands in requests from their next message on");
match client::push_tokens(store, &account, timeout)? {
Pushed::NotRegistered => {
warn("not registered; the set will be pushed with your first fetch")
}
Pushed::Held(held) => println!("server now holds {held} accept token(s)"),
}
Ok(())
}
fn send(cli: &Cli, store: &Store) -> Result<()> {
let Command::Send {
address,
subject,
body,
file,
headers,
reply_to,
anonymous,
no_pad,
} = &cli.command
else {
unreachable!("send dispatches only from the Send command")
};
let account = load_account(&cli.key, store)?;
let addr = Address::parse(address)?;
let text = match (body, file) {
(Some(text), _) => text.clone(),
(None, Some(path)) if path == "-" => read_stdin()?,
(None, Some(path)) => {
std::fs::read_to_string(path).with_context(|| format!("cannot read {path}"))?
}
(None, None) if !std::io::stdin().is_terminal() => read_stdin()?,
(None, None) => {
return Err(anyhow!("no message body; pass --body or pipe it on stdin").into())
}
};
if let Some(reply) = reply_to {
if reply.len() != 64 || !reply.bytes().all(|c| c.is_ascii_hexdigit()) {
return Err(
anyhow!("--reply-to must be a message id: 64 hex characters"),
);
}
}
// Extra headers must be valid `Key: value` fields (sec 5.5).
let mut extra: Vec<(String, String)> = Vec::new();
for raw in headers {
let Some((k, v)) = raw.split_once(':') else {
return Err(
anyhow!(format!("{raw:?} is not a valid `Key: value` header")),
);
};
if !valid_frontmatter_key(k.trim()) {
return Err(
anyhow!(format!("{raw:?} is not a valid `Key: value` header")),
);
}
extra.push((k.trim().to_string(), v.trim().to_string()));
}
let borrowed: Vec<(&str, &str)> = extra
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let draft = SendDraft {
address: &addr,
text,
subject: subject.as_deref(),
reply_to: reply_to.as_deref(),
headers: &borrowed,
anonymous: *anonymous,
no_pad: *no_pad,
};
let sent = client::send(store, &account, &draft, cli.timeout)?;
if let Some(warning) = &sent.warning {
warn(warning);
}
match sent.change {
TrustChange::New { unverified } => warn(&format!(
"{} is new; its key was learned by trust on first use{}",
addr.short(),
if unverified {
" over an UNVERIFIED server"
} else {
""
}
)),
TrustChange::Rotated => warn(&format!(
"{} rotated its key; a signed chain confirms it",
addr.short()
)),
TrustChange::None => {}
}
println!(
"sent {} to {} ({} bytes{})",
&hex(&sent.id)[..16],
addr.short(),
sent.bytes,
if sent.token_used { ", accepted" } else { "" }
);
Ok(())
}
fn read_stdin() -> Result<String> {
let mut text = String::new();
std::io::stdin()
.read_to_string(&mut text)
.context("cannot read stdin")?;
Ok(text)
}
fn valid_frontmatter_key(key: &str) -> bool {
!key.is_empty()
&& key.len() <= 64
&& key.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-')
}
fn fetch(key: &PathBuf, store: &Store, keep: bool, reset: bool, timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let summary = client::fetch(store, &account, keep, reset, timeout)?;
for rejected in &summary.rejected {
warn(&format!(
"{}: {}; left on server",
&hex(&rejected.id)[..16],
rejected.reason
));
}
let mut line = format!(
"{} message(s): {} new, {} rejected",
summary.total,
summary.stored,
summary.rejected.len()
);
if summary.cancelled {
line.push_str("; cancelled, fetching again continues");
} else if keep && summary.total > 0 {
line.push_str(" (left on the server)");
}
println!("{line}");
Ok(())
}
/// Resolves an unambiguous id prefix against stored mail and returns the full
/// ids, ready for the wire.
fn resolve_id_prefixes(store: &Store, ids: &[String]) -> Result<Vec<[u8; ID_LEN]>> {
if ids.is_empty() {
return Err(anyhow!("no message ids given"));
}
let stored = store
.mail("all")?
.into_iter()
.chain(store.mail("sent")?)
.map(|m| m.id)
.collect::<Vec<_>>();
let mut full = Vec::with_capacity(ids.len());
for id in ids {
let prefix = id.to_ascii_lowercase();
let matches: Vec<[u8; ID_LEN]> = stored
.iter()
.filter(|m| hex(m).starts_with(&prefix))
.copied()
.collect();
match matches.len() {
0 => return Err(anyhow!(format!("no message matching {id:?}"))),
1 => full.push(matches[0]),
_ => {
return Err(anyhow!(format!(
"{id:?} matches {} messages; be more specific",
matches.len()
))
.into())
}
}
}
Ok(full)
}
fn delete(key: &PathBuf, store: &Store, ids: &[String], timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let full = resolve_id_prefixes(store, ids)?;
let removed = client::delete(store, &account, &full, timeout)?;
println!("removed {removed} message(s) from the server");
Ok(())
}
fn list(key: &PathBuf, store: &Store, sent: bool, requests: bool) -> Result<()> {
let account = load_account(key, store)?;
let folder = if sent {
"sent"
} else if requests {
"requests"
} else {
"inbox"
};
let rows = store.mail(folder)?;
if rows.is_empty() {
println!("{folder} is empty");
return Ok(());
}
for row in rows {
let who = row.recipient.clone();
match client::describe(store, &account, &row) {
Ok(described) => {
let who = who.unwrap_or(described.from);
let subject = if described.subject.is_empty() {
"(no subject)".to_string()
} else {
described.subject
};
println!(
"{} {} {:<28.28} {subject}",
&hex(&row.id)[..8],
format_utc(row.at),
who
);
}
Err(e) => println!(
"{} {} {:<28.28} <unreadable: {e}>",
&hex(&row.id)[..8],
format_utc(row.at),
"?"
),
}
}
Ok(())
}
fn read(key: &PathBuf, store: &Store, id: &str, sent: bool) -> Result<()> {
let account = load_account(key, store)?;
let folder = if sent { "sent" } else { "all" };
let prefix = id.to_ascii_lowercase();
let matches: Vec<_> = store
.mail(folder)?
.into_iter()
.filter(|m| hex(&m.id).starts_with(&prefix))
.collect();
match matches.len() {
0 => Err(anyhow!(format!(
"no {}message matching {id:?}",
if sent { "sent " } else { "" }
))),
1 => {
let row = &matches[0];
let described = client::describe(store, &account, row)?;
fn field(key: &str, value: &str) {
println!("{:<12} {value}", format!("{key}:"));
}
field("id", &hex(&row.id));
if let Some(recipient) = &row.recipient {
field("to", recipient);
}
field("from", &described.from);
field("key", &b32(&described.sender));
field("date", &format_utc(described.time));
for (k, v) in &described.fields {
if k != "accept" {
field(k, v);
}
}
println!("signature verified");
println!();
print!("{}", described.text);
if !described.text.ends_with('\n') {
println!();
}
Ok(())
}
_ => Err(anyhow!(format!(
"{id:?} matches {} messages; be more specific",
matches.len()
))
.into()),
}
}
/// Parses host[:port]; `default_port` applies when the port is omitted.
#[cfg(feature = "rns")]
fn split_host_port(text: &str, default_port: u16, what: &str) -> Result<(String, u16)> {
match text.rsplit_once(':') {
Some((host, port)) => {
let port: u16 = port
.parse()
.with_context(|| format!("{what} port {port:?} is not a number"))?;
Ok((host.to_string(), port))
}
None => Ok((text.to_string(), default_port)),
}
}
/// Carrier configuration from the global flags.
#[cfg(feature = "rns")]
fn rns_config(cli: &Cli) -> fumi::rns::RnsConfig {
let listen =
split_host_port(&cli.rns.udp, 0, "--rns-udp").expect("--rns-udp defaults are valid");
let forward = cli
.rns
.udp_forward
.as_deref()
.map(|target| split_host_port(target, 4242, "--rns-udp-forward"))
.transpose()
.expect("clap-validated forward target");
let (forward_host, forward_port) = match forward {
Some((host, port)) => (Some(host), port),
None => (None, 4242),
};
fumi::rns::RnsConfig {
storage_dir: cli.rns.storage.clone(),
udp_listen_host: listen.0,
udp_listen_port: listen.1,
udp_forward_host: forward_host,
udp_forward_port: forward_port,
}
}
fn hex(id: &[u8; ID_LEN]) -> String {
data_encoding::HEXLOWER.encode(id)
}
/// UTC date and time, so no time-zone dependency is needed for a mail listing.
fn format_utc(secs: i64) -> String {
let days = secs.div_euclid(86_400);
let time_of_day = secs.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}-{month:02}-{day:02} {:02}:{:02}",
time_of_day / 3600,
time_of_day % 3600 / 60
)
}
/// Days since the Unix epoch to a civil date (Howard Hinnant's algorithm).
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
(if m <= 2 { y + 1 } else { y }, m as u32, d as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "rns")]
#[test]
fn rns_flag_parsing() {
assert_eq!(
split_host_port("127.0.0.1:4242", 0, "--rns-udp").unwrap(),
("127.0.0.1".to_string(), 4242)
);
assert_eq!(
split_host_port("127.0.0.1", 4242, "--rns-udp-forward").unwrap(),
("127.0.0.1".to_string(), 4242)
);
assert!(split_host_port("host:notaport", 0, "--rns-udp").is_err());
}
#[test]
fn utc_formatting_matches_known_dates() {
assert_eq!(format_utc(0), "1970-01-01 00:00");
assert_eq!(format_utc(1_700_000_000), "2023-11-14 22:13");
assert_eq!(format_utc(951_782_400), "2000-02-29 00:00"); // leap day
assert_eq!(format_utc(-1), "1969-12-31 23:59");
}
}