ctoolbox/
utilities.rs

1pub use anyhow::{Context, Result};
2use clap::{crate_name, crate_version};
3use core::panic;
4use fork::{Fork, daemon};
5use passwords::PasswordGenerator;
6use serde::Serialize;
7use serde_json::Value;
8use std::backtrace::Backtrace;
9use std::collections::HashMap;
10use std::error::Error;
11use std::path::PathBuf;
12use std::process::Command;
13use std::{env, fmt, fs};
14use sysinfo::{Pid, Process, System};
15use unicode_segmentation::UnicodeSegmentation;
16
17pub mod debug_tools;
18pub mod ipc;
19pub mod json;
20pub mod logging;
21pub mod panic_hooks;
22pub mod password;
23pub mod process;
24pub mod reader;
25pub mod resource_lock;
26pub mod serde_value;
27use crate::cli::Invocation;
28use crate::formats::unicode::scalars_to_string_lossy;
29use crate::utilities::ipc::Channel;
30use crate::utilities::process::ProcessManager;
31pub use crate::utilities_json_json as json;
32pub use ctb_test_macro::ctb_test;
33
34pub const COLUMN_UUID_DELIM: &str = "d13420ff-b2d7-4e52-a390-7a0d6159e8d6";
35
36pub fn strtovec(s: &str) -> Vec<u8> {
37    s.as_bytes().to_owned()
38}
39
40pub fn vectostr(v: &[u8]) -> String {
41    String::from_utf8_lossy(v).to_string()
42}
43
44pub fn strtohex<T>(s: T) -> String
45where
46    T: AsRef<[u8]>,
47{
48    hex::encode(s)
49}
50
51pub fn vectohex<T>(s: T) -> String
52where
53    T: AsRef<[u8]>,
54{
55    hex::encode(s)
56}
57
58pub fn substr_mb(s: &str, start: i128, end: i128) -> String {
59    let chars: Vec<&str> = s.graphemes(true).collect();
60    let len = i128::try_from(chars.len()).expect("usize did not fit in i128");
61
62    // Convert negative indices to positive ones
63    let start = if start < 0 { len + start } else { start };
64    let end = if end < 0 { len + end } else { end };
65
66    // Clamp indices within valid character bounds
67    let start = start.max(0).min(len); // looks backwards but yes
68    let end = end.max(0).min(len);
69
70    // Extract substring
71    chars[usize::try_from(start).expect("Did not fit")
72        ..usize::try_from(end).expect("Did not fit")]
73        .join("")
74}
75
76// edited from https://stackoverflow.com/a/59401721
77pub fn find_first_matching_key_for_value(
78    map: HashMap<Vec<u8>, Vec<u8>>,
79    needle: Vec<u8>,
80) -> Option<Vec<u8>> {
81    map.iter().find_map(|(key, val)| {
82        if *val == needle {
83            Some(key.clone())
84        } else {
85            None
86        }
87    })
88}
89
90pub use log::{
91    debug as hc_internal_log_debug, error as hc_internal_log_error,
92    info as hc_internal_log_info, warn as hc_internal_log_warn,
93};
94
95// Random UUID to allow tracing to pull the column out after
96#[macro_export]
97macro_rules! log {
98    ($($arg:expr),*) => {
99        $crate::hc_internal_log_debug!("{:?}{}{}", ($($arg),*), $crate::COLUMN_UUID_DELIM, column!());
100    }
101}
102pub use crate::log;
103
104#[macro_export]
105macro_rules! debug {
106    ($($arg:expr),*) => {
107        $crate::hc_internal_log_debug!("{:?}{}{}", ($($arg),*), $crate::COLUMN_UUID_DELIM, column!());
108    }
109}
110pub use crate::debug;
111
112#[macro_export]
113macro_rules! info {
114    ($($arg:expr),*) => {
115        $crate::hc_internal_log_info!("{:?}{}{}", ($($arg),*), $crate::COLUMN_UUID_DELIM, column!());
116    }
117}
118pub use crate::info;
119
120#[macro_export]
121macro_rules! warn {
122    ($($arg:expr),*) => {
123        $crate::hc_internal_log_warn!("{:?}{}{}", ($($arg),*), $crate::COLUMN_UUID_DELIM, column!());
124    }
125}
126pub use crate::warn;
127
128#[macro_export]
129macro_rules! error {
130    ($($arg:expr),*) => {
131        $crate::hc_internal_log_error!("{:?}{}{}", ($($arg),*), $crate::COLUMN_UUID_DELIM, column!());
132    }
133}
134pub use crate::error;
135
136#[macro_export]
137macro_rules! log_fmt {
138    ($fmt:expr, $($arg:tt)*) => {
139        $crate::hc_internal_log_debug!("{}{}{}", format!($fmt, $($arg)*), $crate::COLUMN_UUID_DELIM, column!());
140    };
141    ($msg:expr) => {
142        $crate::hc_internal_log_debug!("{}{}{}", format!($msg), $crate::COLUMN_UUID_DELIM, column!());
143    };
144}
145pub use crate::log_fmt;
146
147#[macro_export]
148macro_rules! debug_fmt {
149    ($fmt:expr, $($arg:tt)*) => {
150        $crate::hc_internal_log_debug!("{}{}{}", format!($fmt, $($arg)*), $crate::COLUMN_UUID_DELIM, column!());
151    };
152    ($msg:expr) => {
153        $crate::hc_internal_log_debug!("{}{}{}", format!($msg), $crate::COLUMN_UUID_DELIM, column!());
154    };
155}
156pub use crate::debug_fmt;
157
158#[macro_export]
159macro_rules! info_fmt {
160    ($fmt:expr, $($arg:tt)*) => {
161        $crate::hc_internal_log_info!("{}{}{}", format!($fmt, $($arg)*), $crate::COLUMN_UUID_DELIM, column!());
162    };
163    ($msg:expr) => {
164        $crate::hc_internal_log_info!("{}{}{}", format!($msg), $crate::COLUMN_UUID_DELIM, column!());
165    };
166}
167pub use crate::info_fmt;
168
169#[macro_export]
170macro_rules! warn_fmt {
171    ($fmt:expr, $($arg:tt)*) => {
172        $crate::hc_internal_log_warn!("{}{}{}", format!($fmt, $($arg)*), $crate::COLUMN_UUID_DELIM, column!());
173    };
174    ($msg:expr) => {
175        $crate::hc_internal_log_warn!("{}{}{}", format!($msg), $crate::COLUMN_UUID_DELIM, column!());
176    };
177}
178pub use crate::warn_fmt;
179
180#[macro_export]
181macro_rules! log_string {
182    ($document:expr) => {
183        log!($document);
184    };
185}
186
187#[macro_export]
188macro_rules! log_type {
189    ($t:ty) => {
190        log!(std::any::type_name::<$t>());
191    };
192}
193
194#[macro_export]
195macro_rules! json_value {
196    // Match key-value pairs: json_value!({ "foo" => bar, ... })
197    ({ $($key:expr => $value:expr),* $(,)? }) => {
198        {
199            #[allow(unused_mut)]
200            let mut map = serde_json::Map::new();
201            $(
202                map.insert($key.into(), serde_json::to_value($value).expect("Failed to convert to JSON value"));
203            )*
204            serde_json::Value::Object(map)
205        }
206    };
207    // Match a single value: json_value!("foo")
208    ($key:expr) => {
209        {
210            serde_json::to_value($key).expect("Failed to convert to JSON value")
211        }
212    };
213}
214
215// Use this by defining an error_abort macro in other code for it to expand to, e.g. in lib.rs
216#[macro_export]
217macro_rules! unwrap_or_custom_error {
218    ($result:expr, $error:expr) => {
219        match $result {
220            Ok(value) => value,
221            Err(_) => {
222                // log!($error);
223                error_abort!($result.context($error))
224            }
225        }
226    };
227}
228
229#[macro_export]
230macro_rules! unwrap_or_result_error {
231    ($result:expr, $error:expr) => {
232        unwrap_or_custom_error!(
233            $result.map_err(|e| anyhow::anyhow!("{}", e)),
234            $error
235        )
236    };
237}
238
239#[macro_export]
240macro_rules! bail_if_none {
241    // case: no message — produce a default anyhow::Error
242    ($opt:expr) => {
243        $opt.ok_or_else(|| anyhow::anyhow!(format!("unexpected None, at {} line: {}, column: {}", file!(), line!(), column!())))?
244    };
245
246    // case: single literal/message without formatting
247    ($opt:expr, $msg:expr) => {
248        $opt.ok_or_else(|| anyhow::anyhow!(format!("{:?}, at {} line: {}, column: {}", $msg, file!(), line!(), column!())))?
249    };
250
251    // case: format-like message with args
252    ($opt:expr, $fmt:expr, $($args:tt)+) => {
253        $opt.ok_or_else(|| anyhow::anyhow!($fmt, $($args)+))?
254    };
255}
256
257pub fn backtrace_string() -> String {
258    format!("{}", Backtrace::capture())
259}
260
261pub fn backtrace_print() {
262    println!("Backtrace: {}", backtrace_string());
263}
264
265pub fn this_pid() -> Vec<u8> {
266    std::process::id().to_string().into_bytes()
267}
268
269pub fn in_array(needle: Vec<u8>, map: HashMap<u32, Vec<u8>>) -> bool {
270    map.iter().any(|(_, val)| *val == needle)
271}
272
273pub fn is_subprocess(invocation: &Invocation) -> bool {
274    invocation.is_subprocess()
275}
276
277pub fn get_service_name(invocation: &Invocation) -> String {
278    if is_subprocess(invocation) {
279        invocation
280            .subprocess()
281            .map(|s| s.service_name.clone())
282            .unwrap_or_default()
283    } else {
284        String::new()
285    }
286}
287
288pub fn remove_suffix<'a>(string: &'a str, suffix: &str) -> &'a str {
289    if string.to_string().ends_with(suffix) {
290        &string[..string.len() - suffix.len()]
291    } else {
292        string
293    }
294}
295
296pub fn sleep(seconds: u64) {
297    std::thread::sleep(std::time::Duration::from_secs(seconds));
298}
299
300pub fn usleep(microseconds: u64) {
301    std::thread::sleep(std::time::Duration::from_micros(microseconds));
302}
303
304pub fn setup_panic_hooks(manager: &ProcessManager) {
305    panic_hooks::setup_panic_hooks(manager)
306}
307
308pub fn send_message(channel: &Channel, message: &str) -> String {
309    crate::workspace::ipc_old::send_message(channel, message)
310}
311
312pub fn maybe_send_message(
313    channel: &Channel,
314    message: &str,
315) -> Result<String, Box<dyn Error>> {
316    crate::workspace::ipc_old::maybe_send_message(channel, message)
317}
318
319pub async fn wait_for_message(channel: &Channel, msgid: u64) -> String {
320    crate::workspace::ipc_old::wait_for_message(channel, msgid).await
321}
322
323pub fn listen_for_message(channel: &Channel, msgid: u64) -> String {
324    crate::workspace::ipc_old::listen_for_message(channel, msgid)
325}
326
327pub fn call_ipc_sync(
328    channel: &Channel,
329    method: &str,
330    args: Value,
331) -> Result<String> {
332    crate::workspace::ipc_old::call_ipc_sync(channel, method, args)
333}
334
335pub async fn call_ipc(
336    channel: &Channel,
337    method: &str,
338    args: Value,
339) -> Result<String> {
340    crate::workspace::ipc_old::call_ipc(channel, method, args).await
341}
342
343pub fn shutdown(process_manager: &ProcessManager) {
344    println!("Shutting down");
345    cleanup_processes(process_manager);
346    std::process::exit(1);
347}
348
349pub fn upgrade_in_place(
350    temp_path: &PathBuf,
351    target_path: &PathBuf,
352) -> Result<()> {
353    fs::copy(temp_path, target_path)
354        .with_context(|| "Failed to copy new executable over old one")?;
355    Ok(())
356}
357
358pub fn fork(path: &PathBuf, args: Vec<&str>) {
359    if let Ok(Fork::Child) = daemon(false, false) {
360        Command::new(path)
361            .args(args)
362            .output()
363            .expect("failed to execute process");
364    }
365}
366
367// This API is annoying, I can't figure out how to get it to take just the PID
368fn get_ctoolbox_process(s: &mut System, pid: u32) -> Option<&Process> {
369    s.refresh_all();
370    let process = s.process(Pid::from_u32(pid))?;
371    let subprocess_exe = process.exe()?;
372    let this_exe = env::current_exe().unwrap();
373    if this_exe != subprocess_exe {
374        return None;
375    }
376    Some(process)
377}
378
379pub fn get_this_executable() -> PathBuf {
380    let mut s = System::new_all();
381    s.refresh_all();
382    env::current_exe().unwrap()
383}
384
385pub fn wait_for_ctoolbox_process_exit<'a>(pid: u32) {
386    let mut s = System::new_all();
387    let mut process = get_ctoolbox_process(&mut s, pid);
388    while process.is_some() {
389        process = get_ctoolbox_process(&mut s, pid);
390        std::thread::sleep(std::time::Duration::from_millis(100));
391    }
392}
393
394pub fn wait_for_ctoolbox_exit_and_clean_up(pid: u32) {
395    wait_for_ctoolbox_process_exit(pid);
396}
397
398pub fn cleanup_processes(process_manager: &ProcessManager) {
399    // let json = json!(process_manager);
400    // println!("Cleaning up processes: {}", json);
401    process_manager
402        .processes
403        .iter()
404        .for_each(|(_id, process_record)| {
405            let mut s = System::new_all();
406            if let Some(process) = get_ctoolbox_process(
407                &mut s,
408                process_record
409                    .system_pid
410                    .try_into()
411                    .expect("Invalid PID for this platform"),
412            ) {
413                // println!("Killing process {}", process.pid());
414                process.kill();
415            }
416        });
417}
418
419pub fn generate_authentication_key() -> String {
420    let pg = PasswordGenerator {
421        length: 64,
422        numbers: true,
423        lowercase_letters: true,
424        uppercase_letters: true,
425        symbols: true,
426        spaces: true,
427        exclude_similar_characters: false,
428        strict: true,
429    };
430
431    pg.generate_one().unwrap()
432}
433
434pub fn u8_vec_to_formatted_hex(values: &[u8]) -> String {
435    let mut out = String::new();
436    for &v in values {
437        out.push_str(&format!("{v:02x} "));
438    }
439    out.to_uppercase().trim().to_string()
440}
441
442pub fn u32_vec_to_formatted_hex(values: &[u32]) -> String {
443    let mut out = String::new();
444    for &v in values {
445        out.push_str(&format!("{v:02x} "));
446    }
447    out.to_uppercase().trim().to_string()
448}
449
450// Test helpers to compare slices with clearer diff on failure.
451pub fn assert_vec_u32_eq(expected: &[u32], actual: &[u32]) -> Vec<u32> {
452    assert!(
453        expected == actual,
454        "Vectors (u32) differ.\n{}",
455        fmt_mismatch_vec_u32(expected, actual)
456    );
457    actual.to_vec()
458}
459
460pub fn assert_vec_u8_eq(expected: &[u8], actual: &[u8]) -> Vec<u8> {
461    assert!(
462        expected == actual,
463        "Vectors (u8) differ.\n{}",
464        fmt_mismatch_vec_u8(expected, actual)
465    );
466    actual.to_vec()
467}
468
469pub fn fmt_mismatch_vec_u8(expected: &[u8], actual: &[u8]) -> String {
470    format!(
471        "Expected: {:?}\nActual:   {:?}\nExpected (hex): {:?}\nActual   (hex): {:?}\nExpected (lossy): {:?}\nActual   (lossy): {:?}",
472        expected,
473        actual,
474        u8_vec_to_formatted_hex(expected),
475        u8_vec_to_formatted_hex(actual),
476        String::from_utf8_lossy(expected),
477        String::from_utf8_lossy(actual)
478    )
479}
480
481pub fn fmt_mismatch_vec_u32(expected: &[u32], actual: &[u32]) -> String {
482    format!(
483        "Expected: {:?}\nActual:   {:?}\nExpected (hex): {:?}\nActual   (hex): {:?}\nExpected (lossy): {:?}\nActual   (lossy): {:?}",
484        expected,
485        actual,
486        u32_vec_to_formatted_hex(expected),
487        u32_vec_to_formatted_hex(actual),
488        scalars_to_string_lossy(expected),
489        scalars_to_string_lossy(actual)
490    )
491}
492
493pub fn fmt_mismatch_string(expected: &str, actual: &str) -> String {
494    format!(
495        "Expected: {:?}\nActual:   {:?}\nExpected (hex): {:?}\nActual   (hex): {:?}",
496        expected,
497        actual,
498        u8_vec_to_formatted_hex(expected.as_bytes()),
499        u8_vec_to_formatted_hex(actual.as_bytes()),
500    )
501}
502
503pub fn feature(feature_name: &str) -> bool {
504    let current_settings =
505        crate::storage::pc_settings::PcSettings::load().unwrap_or_default();
506    match feature_name {
507        "login" => current_settings.feature_login,
508        "registration" => current_settings.feature_registration,
509        _ => false,
510    }
511}
512
513thread_local! {
514    pub static CURRENT_TEST_NAME: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
515}
516
517#[cfg(test)]
518pub fn set_current_test_name(p: String) {
519    CURRENT_TEST_NAME.with(|c| *c.borrow_mut() = Some(p));
520}
521
522pub fn get_current_test_name() -> String {
523    assert!(cfg!(test), "get_current_test_name called outside of test");
524
525    if let Some(p) = CURRENT_TEST_NAME.with(|c| c.borrow().clone()) {
526        p
527    } else {
528        panic!("Test name not set");
529    }
530}
531
532#[derive(Serialize)]
533pub struct BuildInfo {
534    pub(crate) name: String,
535    pub(crate) version: String,
536    pub(crate) build_date: String,
537    pub(crate) commit: String,
538}
539
540pub fn build_info() -> BuildInfo {
541    BuildInfo {
542        name: crate_name!().to_string(),
543        version: crate_version!().to_string(),
544        build_date: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
545        commit: env!("VERGEN_GIT_SHA").to_string(),
546    }
547}
548
549pub fn assert_vec_u32_ok_eq(
550    expected: &[u32],
551    actual: anyhow::Result<Vec<u32>>,
552) -> Vec<u32> {
553    match actual {
554        Ok(v) => assert_vec_u32_eq(expected, &v),
555        Err(e) => panic!("Expected Ok(Vec<u32>), got Err: {e:?}"),
556    }
557}
558
559pub fn assert_vec_u8_ok_eq(
560    expected: &[u8],
561    actual: anyhow::Result<Vec<u8>>,
562) -> Vec<u8> {
563    match actual {
564        Ok(v) => assert_vec_u8_eq(expected, &v),
565        Err(e) => panic!("Expected Ok(Vec<u8>), got Err: {e:?}"),
566    }
567}
568
569pub fn assert_string_contains(expected: &str, actual: &str) {
570    assert!(
571        actual.contains(expected),
572        "String '{actual}' does not contain expected substring '{expected}'."
573    );
574}
575
576pub fn assert_string_not_contains(expected: &str, actual: &str) {
577    assert!(
578        !actual.contains(expected),
579        "String '{actual}' unexpectedly contains forbidden substring '{expected}'."
580    );
581}