#![warn(missing_docs)]
#[macro_use]
#[allow(unused_macros)]
mod __;
mod cmd_option;
mod version_info;
use {
core::borrow::Borrow,
std::{
borrow::Cow,
io::{Error, ErrorKind},
os::{
linux::net::SocketAddrExt,
unix::net::{SocketAddr, UnixStream},
},
process::{self, Command, Stdio},
},
self::cmd_option::CmdOption,
dia_args::{
Args,
docs::Project,
},
};
macro_rules! code_name { () => { "touch-als" }}
macro_rules! version { () => { "0.4.0" }}
pub const NAME: &str = "touch-als";
pub const CODE_NAME: &str = code_name!();
pub const ID: &str = concat!(
"c9e74de3-b0b0d8c4-9b84ee53-f1aae85f-26a06f64-37924a21-baee6dfd-e6bbcfbc-",
"868ccd21-a21be567-d078cf0b-5df23b4a-e32e6e2e-78c5dd39-eb564cce-aa606fe7",
);
pub const VERSION: &str = version!();
pub const RELEASE_DATE: (u16, u8, u8) = (2024, 6, 18);
pub const TAG: &str = concat!(code_name!(), "::c9e74de3::", version!());
pub (crate) type Result<T> = core::result::Result<T, std::io::Error>;
#[test]
fn test_crate_version() {
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
}
const CMD_HELP: &str = "help";
const CMD_HELP_DOCS: Cow<str> = Cow::Borrowed("Prints help and exits.");
const CMD_VERSION: &str = "version";
const CMD_VERSION_DOCS: Cow<str> = Cow::Borrowed("Prints version and exits.");
const CMD_LICENSES: &str = "licenses";
const CMD_LICENSES_DOCS: Cow<str> = Cow::Borrowed("Prints licenses and exits.");
const OPTION_CMD: &[&str] = &["--cmd"];
const OPTION_CMD_DOCS: Cow<str> = Cow::Borrowed(concat!(
"Command option.\n\n",
"Notes:\n\n",
"- This option is supported for only one single address.\n",
"- The command will be spawned and ignored.",
));
const OPTION_CMD_VALUES: &[CmdOption] = &[CmdOption::OnSuccess, CmdOption::OnFailure];
fn main() {
if let Err(err) = run() {
eprintln!("{}", __w!("{}", err));
process::exit(1);
}
}
fn run() -> Result<()> {
let args = dia_args::parse()?;
match args.cmd() {
Some(CMD_HELP) => {
ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
print_help()
},
Some(CMD_VERSION) => {
ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
print_version()
},
Some(CMD_LICENSES) => print_licenses(args.try_into_sub_cmd()?.1),
Some(_) => run_main_job(args),
None => Err(Error::new(ErrorKind::Other, "Missing address(es)")),
}
}
fn ensure_args_are_empty<A>(args: A) -> Result<()> where A: Borrow<Args> {
let args = args.borrow();
if args.is_empty() {
Ok(())
} else {
Err(Error::new(ErrorKind::InvalidInput, format!("Unknown arguments: {:?}", args)))
}
}
fn make_version_string<'a>() -> Cow<'a, str> {
Cow::Owned(format!(
"{name} {version} {release_date:?}",
name=NAME, version=VERSION, release_date=RELEASE_DATE,
))
}
fn print_version() -> Result<()> {
println!("{}", make_version_string());
Ok(())
}
fn parse_cmd_docs_from_readme<'a>() -> Cow<'a, str> {
const START: &str = "<!-- PROGRAM-DOCS:START -->";
const END: &str = "<!-- PROGRAM-DOCS:END -->";
let readme = include_str!("../README.md");
Cow::Borrowed(readme[readme.find(START).unwrap() + START.len() .. readme.find(END).unwrap()].trim())
}
#[test]
fn test_parse_cmd_docs_from_readme() {
parse_cmd_docs_from_readme();
}
fn make_project_info<'a>() -> Option<Project<'a>> {
Some(Project::new(
None,
"GNU Lesser General Public License, either version 3, or (at your option) any later version",
None,
))
}
#[test]
fn test_make_project_info() {
make_project_info().unwrap();
}
fn print_help() -> Result<()> {
use dia_args::docs::{Cmd, Docs, Option};
let options = Some(dia_args::make_options![
Option::new(OPTION_CMD, false, OPTION_CMD_VALUES, None, OPTION_CMD_DOCS),
]);
let commands = Some(dia_args::make_cmds![
Cmd::new(CMD_HELP, CMD_HELP_DOCS, None),
Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None),
Cmd::new(CMD_LICENSES, CMD_LICENSES_DOCS, None),
]);
let mut docs = Docs::new(make_version_string(), parse_cmd_docs_from_readme());
docs.options = options;
docs.commands = commands;
docs.project = make_project_info();
docs.print()?;
println!(
concat!(
"EXAMPLES\n\n",
"# Touch an address and echo some message on success\n",
"$ {code_name} 24047f96d377a477098106f6a474bd89 {arg_cmd}={on_success} -- echo ok",
),
code_name=CODE_NAME,
arg_cmd=OPTION_CMD[0],
on_success=CmdOption::OnSuccess,
);
Ok(())
}
fn print_licenses(args: Args) -> Result<()> {
ensure_args_are_empty(args)?;
dia_args::lock_write_out(format!(
"{copying}\n\n================================================================\n\n{copying_lesser}\n",
copying=include_str!("../COPYING"),
copying_lesser=include_str!("../COPYING.LESSER"),
));
Ok(())
}
fn run_main_job(mut args: Args) -> Result<()> {
let addresses = args.take_args()?;
if addresses.is_empty() {
return Err(Error::new(ErrorKind::InvalidInput, "Missing address(es)"));
}
let cmd_option = args.take::<CmdOption>(OPTION_CMD)?;
let mut cmd = match cmd_option {
Some(_) => {
if addresses.len() > 1 {
return Err(Error::new(ErrorKind::InvalidInput, format!("{OPTION_CMD:?} only supports one single address.")));
}
let mut sub_args = args.take_sub_args();
if sub_args.is_empty() {
return Err(Error::new(ErrorKind::InvalidInput, format!("{OPTION_CMD:?} requires a command.")));
}
let mut cmd = Command::new(sub_args.remove(0));
cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
for s in sub_args {
cmd.arg(s);
}
Some(cmd)
},
None => None,
};
ensure_args_are_empty(args)?;
for addr in addresses {
match UnixStream::connect_addr(&SocketAddr::from_abstract_name(&hex_to_address(&addr)?)?) {
Ok(_) => {
println!("Ok: {}", addr);
if let (Some(CmdOption::OnSuccess), Some(mut cmd)) = (cmd_option.as_ref(), cmd.take()) {
cmd.spawn()?;
}
},
Err(err) => {
eprintln!("{}", __w!("Failed: {addr} -> {err}", addr=addr, err=err));
if let (Some(CmdOption::OnFailure), Some(mut cmd)) = (cmd_option.as_ref(), cmd.take()) {
cmd.spawn()?;
}
},
};
}
Ok(())
}
fn hex_to_address<S>(hex: S) -> Result<Vec<u8>> where S: AsRef<str> {
const RAW: &str = "raw:";
let hex = hex.as_ref();
if hex.starts_with(RAW) {
return Ok(hex[RAW.len()..].as_bytes().to_vec());
}
let len = hex.len();
let result_len = len / 2;
if len % 2 != 0 || result_len >= 512 {
return Err(Error::new(ErrorKind::InvalidInput, format!("Invalid address: {:?}", hex)));
}
let mut result = Vec::with_capacity(result_len);
result.push(0);
for i in (0..len).step_by(2) {
match hex.get(i .. i + 2).map(|s| u8::from_str_radix(s, 16)) {
Some(Ok(b)) => result.push(b),
_ => return Err(Error::new(ErrorKind::InvalidInput, format!("Invalid address: {:?}", hex))),
};
}
Ok(result)
}