// Copyright (C) 2026 Doan Luu Duc Tai // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::net::SocketAddr; use std::path::PathBuf; #[derive(Debug)] pub(super) struct Arguments { pub(super) root: PathBuf, pub(super) listener: Listener, pub(super) healthcheck: bool, pub(super) init: bool, pub(super) reset_owner: Option, pub(super) owner_fingerprint: bool, pub(super) snapshot: Option, } #[derive(Debug)] pub(super) struct Listener { pub(super) bind: Option, pub(super) proxy: bool, } fn flag(name: &'static str) -> Arg { Arg::new(name).long(name).action(ArgAction::SetTrue) } fn path(name: &'static str) -> Arg { Arg::new(name) .long(name) .value_parser(clap::builder::PathBufValueParser::new()) } fn command() -> Command { Command::new("stack-chat-server").about("Run private chat directly or behind the stack gateway") .args([ path("root").default_value("."), Arg::new("bind").long("bind").value_parser(str::parse::), flag("proxy").help("Run behind the stack gateway using its explicit container environment"), flag("healthcheck").help("Check the local HTTP health endpoint without opening the database"), flag("init").conflicts_with_all(["reset-owner", "owner-fingerprint", "snapshot"]), Arg::new("reset-owner").long("reset-owner").value_name("FINGERPRINT").conflicts_with_all(["owner-fingerprint", "snapshot"]) .help("Permanently clear the matching owner registration and all chat history after key loss"), flag("owner-fingerprint").conflicts_with("snapshot").help("Print the registered owner's public fingerprint"), path("snapshot"), ]) } impl Arguments { pub(super) fn try_parse() -> Result { Self::try_parse_from(std::env::args_os()) } fn try_parse_from( arguments: impl IntoIterator + Clone>, ) -> Result { let mut values = command().try_get_matches_from(arguments)?; let root = values.remove_one("root").ok_or_else(|| { clap::Error::raw( clap::error::ErrorKind::MissingRequiredArgument, "Missing application root", ) })?; Ok(Self { root, listener: Listener { bind: values.remove_one("bind"), proxy: values.get_flag("proxy"), }, healthcheck: values.get_flag("healthcheck"), init: values.get_flag("init"), reset_owner: values.remove_one("reset-owner"), owner_fingerprint: values.get_flag("owner-fingerprint"), snapshot: values.remove_one("snapshot"), }) } } #[cfg(test)] mod tests { use super::{Arguments, command}; use anyhow::{Result, ensure}; use std::path::Path; #[test] fn checked_command_schema() { command().debug_assert(); } #[test] fn listener_defaults_and_maintenance_conflicts() -> Result<()> { let defaults = Arguments::try_parse_from(["stack-chat-server"])?; anyhow::ensure!((defaults.root) == (Path::new(".")), "Test invariant failed"); anyhow::ensure!( !defaults.listener.proxy && defaults.listener.bind.is_none(), "Test invariant failed" ); let parsed = Arguments::try_parse_from([ "stack-chat-server", "--root", "/tmp/stack", "--bind", "127.0.0.1:9180", "--proxy", "--healthcheck", ])?; anyhow::ensure!( (parsed.root) == (Path::new("/tmp/stack")), "Test invariant failed" ); ensure!( parsed .listener .bind .is_some_and(|address| address.port() == 9180), "Listener port differs" ); anyhow::ensure!( parsed.listener.proxy && parsed.healthcheck, "Test invariant failed" ); for args in [ vec!["stack-chat-server", "--bind", "garbage"], vec!["stack-chat-server", "--init", "--snapshot", "snapshot"], vec![ "stack-chat-server", "--reset-owner", "abc", "--owner-fingerprint", ], vec![ "stack-chat-server", "--owner-fingerprint", "--snapshot", "snapshot", ], ] { anyhow::ensure!( Arguments::try_parse_from(args).is_err(), "Test invariant failed" ); } Ok(()) } }