zachery.lol/crates/cli/src/main.rs

347 lines
10 KiB
Rust
Raw Normal View History

2025-11-17 22:21:49 +00:00
use clap::{Parser, Subcommand};
2025-11-18 16:41:58 +00:00
use message_tools::BoxedMessage;
use std::{
fs::OpenOptions,
io::{BufRead, Write},
os::unix::process::ExitStatusExt,
process::{Command, ExitStatus},
};
2025-11-18 02:22:05 +00:00
const WEB_BUILD_DIR: &str = "web-build";
2025-11-17 22:21:49 +00:00
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Builds the website and server, including wasm files
Build,
2025-11-18 02:22:05 +00:00
/// Runs the server locally
Start {
/// Whether to build again or not
#[arg(long)]
rebuild: bool,
},
2025-11-17 22:21:49 +00:00
/// Generates new encryption keys
GenKeys {
/// How many keypairs to generate
#[arg(short, long, default_value = "100")]
2025-11-18 02:22:05 +00:00
number: usize,
/// Directory to put the generated secret key files
#[arg(short, long, default_value = ".local/secrets.json")]
sk_out_dir: String,
/// Directory to put the generated pubklic key pool
#[arg(long, default_value = "target/public_keys.json")]
pk_out_dir: String,
2025-11-17 22:21:49 +00:00
},
/// Reads fetched messages
ReadMessages {
2025-11-18 02:22:05 +00:00
/// Source file path
#[arg(short, long, default_value = ".local/messages")]
source: String,
2025-11-17 22:21:49 +00:00
2025-11-18 16:41:58 +00:00
/// Where to find the secret keys for decrypting boxed messages
#[arg(long, default_value = ".local/secrets.json")]
sk_dir: String,
2025-11-18 02:22:05 +00:00
/// Optional output file path for decoded messages
/// Default: stdout
2025-11-17 22:21:49 +00:00
#[arg(short, long)]
out_dir: Option<String>,
},
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Commands::Build => {
println!("Executing build command...");
build();
}
2025-11-18 02:22:05 +00:00
Commands::Start { rebuild } => {
println!("Starting server...");
if *rebuild {
println!("Rebuilding web pages...");
build();
} // Change the current working directory of the running Rust program
println!("Changing directory to {WEB_BUILD_DIR}...");
let cd_result = std::env::set_current_dir(WEB_BUILD_DIR);
// Use the existing helper to check the result
handle_command_status(
cd_result.map(|_| std::process::ExitStatus::from_raw(0)),
"cd build dir",
);
println!("Running server executable...");
// Now run the server executable from within the new current directory
handle_command_status(Command::new("./server").status(), "run server");
2025-11-17 22:21:49 +00:00
}
2025-11-18 02:22:05 +00:00
Commands::GenKeys {
number,
sk_out_dir,
pk_out_dir,
} => {
2025-11-17 22:21:49 +00:00
println!("Executing gen-keys command...");
2025-11-18 02:22:05 +00:00
message_tools::gen_keys::generate_keys_to_file(pk_out_dir, sk_out_dir, *number)
.expect("write key files");
2025-11-17 22:21:49 +00:00
}
2025-11-18 16:41:58 +00:00
Commands::ReadMessages {
source,
sk_dir,
out_dir,
} => {
2025-11-17 22:21:49 +00:00
println!("Executing read-messages command...");
2025-11-18 16:41:58 +00:00
let kps = load_kps(sk_dir);
let msgs = {
let ms = fetch_all_messages_from_dir(source).expect("failed to read messages");
let mut unboxed: Vec<UnboxedMessage> = vec![];
// for loop go brrr, its less than 1000 keys
for m in ms {
if let Some(sk) =
fetch_sk(&kps, &m.remote_pk().expect("failed to parse pk bytes"))
{
let who = m.tag();
let message =
message_tools::unbox_message(m, sk).expect("failed to unbox message");
unboxed.push(UnboxedMessage { who, message });
} else {
eprintln!("No secret key found for message {m:?}");
continue;
}
}
unboxed
};
if let Some(dir) = out_dir {
let mut f = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(dir)
.expect("failed to create message output file");
for msg in msgs {
writeln!(f, "{:?}", msg).expect("failed to write message");
}
} else {
println!("{msgs:?}");
}
}
}
}
fn fetch_sk(
kps: &Vec<KeyPair>,
pk: &message_tools::PublicKey,
) -> Option<message_tools::StaticSecret> {
for kp in kps {
if &kp.pk == pk {
return Some(kp.sk.clone());
}
}
return None;
}
#[allow(dead_code)] // struct for easy display using debug
#[derive(Debug)]
struct UnboxedMessage {
who: String,
message: String,
}
struct KeyPair {
pub pk: message_tools::PublicKey,
pub sk: message_tools::StaticSecret,
}
fn load_kps(path: impl AsRef<std::path::Path>) -> Vec<KeyPair> {
let sks_bytes: Vec<Vec<u8>> = process_json_file(path).expect("failed to parse sk file");
sks_bytes
.into_iter()
.map(|sk| {
let arr: [u8; 32] = sk.try_into().expect("invalid secret");
message_tools::StaticSecret::from(arr)
})
.map(|sk| KeyPair {
pk: message_tools::PublicKey::from(&sk),
sk,
})
.collect()
}
fn fetch_all_messages_from_dir(
dir_path: impl AsRef<std::path::Path>,
) -> std::io::Result<Vec<BoxedMessage>> {
let mut all_messages: Vec<BoxedMessage> = Vec::new();
for entry in std::fs::read_dir(dir_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().unwrap_or_default() == "json" {
let file_messages = process_json_file(&path)?;
all_messages.extend(file_messages);
}
}
Ok(all_messages)
}
fn process_json_file<D: serde::de::DeserializeOwned>(
file_path: impl AsRef<std::path::Path>,
) -> std::io::Result<Vec<D>> {
let f = std::fs::OpenOptions::new()
.read(true)
.write(false)
.open(file_path.as_ref())
.expect(&format!("failed to open file at {:?}", file_path.as_ref()));
let reader = std::io::BufReader::new(f);
let mut ds: Vec<D> = vec![];
for line in reader.lines() {
let line = line.expect("failed to read line");
let line = line.trim();
if line.is_empty() {
continue;
2025-11-17 22:21:49 +00:00
}
2025-11-18 16:41:58 +00:00
let d: D = serde_json::from_str::<D>(line).expect(&format!(
"failed to parse message from file {:?}",
file_path.as_ref()
));
ds.push(d);
2025-11-17 22:21:49 +00:00
}
2025-11-18 16:41:58 +00:00
Ok(ds)
2025-11-17 22:21:49 +00:00
}
2025-11-18 02:22:05 +00:00
// A helper function to process the Result<ExitStatus, io::Error>
fn handle_command_status(status_result: std::io::Result<ExitStatus>, command_name: &str) {
match status_result {
Ok(exit_status) => {
if !exit_status.success() {
eprintln!(
"\nCommand '{}' failed with exit code: {:?}",
command_name,
exit_status.code()
);
std::process::exit(exit_status.code().unwrap_or(1));
}
}
Err(e) => {
eprintln!("\nFailed to execute command '{}': {}", command_name, e);
std::process::exit(1); // Exit with an error code
}
}
}
2025-11-17 22:21:49 +00:00
fn build() {
println!("Building wasm package (message-tools)...");
2025-11-18 02:22:05 +00:00
// build the wasm package
2025-11-17 22:21:49 +00:00
let status = Command::new("wasm-pack")
.arg("build")
.arg("--out-dir")
.arg("../../routes/contact/pkg")
.arg("crates/message-tools")
.arg("--target")
.arg("no-modules")
.status();
handle_command_status(status, "wasm-pack build message-tools");
println!("Building wasm package (fractal-engine)...");
let status = Command::new("wasm-pack")
.arg("build")
.arg("--out-dir")
.arg("../../routes/root/pkg")
.arg("crates/fractal-engine")
.arg("--target")
.arg("no-modules")
.status();
handle_command_status(status, "wasm-pack build fractal-engine");
2025-11-17 22:21:49 +00:00
2025-11-18 02:22:05 +00:00
println!("Building server executable...");
// build the server executable
let status = Command::new("cargo")
.arg("build")
.arg("--release")
.args(["-p", "server"])
.args(["--target", "x86_64-unknown-linux-musl"])
.status();
handle_command_status(status, "cargo build server");
let server_bin = "target/x86_64-unknown-linux-musl/release/server";
println!("Creating build directory...");
// make webpage build directory if it doesn't already exist
let status = Command::new("mkdir").args(["-p", WEB_BUILD_DIR]).status();
handle_command_status(status, "mkdir web-build");
println!("Copying files...");
// copy relevant files (use trailing slash to merge into existing directory)
2025-11-18 02:22:05 +00:00
let status = Command::new("cp")
2025-11-18 16:41:58 +00:00
.args(["-R", "-f", "routes"])
.arg(WEB_BUILD_DIR)
2025-11-18 02:22:05 +00:00
.status();
handle_command_status(status, "cp routes");
let status = Command::new("cp")
2025-11-18 16:41:58 +00:00
.arg("-f")
2025-11-18 02:22:05 +00:00
.arg(server_bin)
.arg(format!("{WEB_BUILD_DIR}"))
.status();
handle_command_status(status, "cp server bin");
let pubkey_file = "target/public_keys.json";
if !std::path::Path::new(pubkey_file).exists() {
println!("No public keys found at {pubkey_file}. Generating keys automatically...");
let sk_out = ".local/secrets.json";
let num_keys = 100;
println!("Creating .local directory...");
std::fs::create_dir_all(".local").expect("failed to create .local directory");
println!("Generating {num_keys} keypairs...");
println!(" Public keys -> {pubkey_file}");
println!(" Secret keys -> {sk_out}");
message_tools::gen_keys::generate_keys_to_file(pubkey_file, sk_out, num_keys)
.expect("failed to generate keys");
println!("Key generation complete.");
2025-11-17 22:21:49 +00:00
}
2025-11-18 02:22:05 +00:00
let status = Command::new("mkdir")
.args(["-p", &format!("{WEB_BUILD_DIR}/pubkeys")])
.status();
handle_command_status(status, "mkdir web-build");
let status = Command::new("cp")
2025-11-18 16:41:58 +00:00
.arg("-f")
2025-11-18 02:22:05 +00:00
.arg("target/public_keys.json")
.arg(format!("{WEB_BUILD_DIR}/pubkeys/"))
.status();
handle_command_status(status, "cp pubkeys");
println!("\nBuild successful!");
2025-11-17 22:21:49 +00:00
}