i2pd-launch 0.5.0-beta.2

Launches i2pd with clean state
/*
Delete state and launch i2pd installed on Windows using scoop.
Authored by Radim Kolar 2024

This is free and unencumbered software released into the public domain.
SPDX-License-Identifier: Unlicense OR CC0-1.0
*/

#![forbid(unsafe_code)]
#![allow(non_snake_case)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::bool_comparison)]

use std::path::{Path, PathBuf};
use std::process::ExitCode;
use windows_sys::core::GUID;
use windows_sys::Win32::UI::Shell::FOLDERID_Profile;
use windows_sys::Win32::UI::Shell::FOLDERID_ProgramData;

/** program version pulled from cargo environment */
const PROGRAM_VERSION: &str = env!("CARGO_PKG_VERSION");

/** command line getopt flags supported */
const PROGRAM_FLAGS: &str = "h?vzw";

#[cfg(not(target_os = "windows"))]
fn main() -> ExitCode {
   eprintln!("This program is supported only on Windows.");
   ExitCode::From(3)
}

fn do_help() {
   println!("i2pd-launch [ -z ] | [ -w ] | [ -h | -v | -? ]");
   println!("Clear i2pd local router state and launch it");
   println!();
   println!("  -h | -? display this help message");
   println!("  -v  print program version and exit");
   println!("  -z  do not start i2pd only clear sensitive information");
   println!("  -w  wait for i2pd to exit and then clear sensitive information again");
   println!();
   println!("This is free and unencumbered software released into the public domain.");
}

/**
  Clean i2p router state and start i2pd

  ## Return codes
  - 0 Success
  - 1 No scoop i2pd installation detected
  - 2 i2pd Executable not found
  - 3 This program supported only on Windows
  - 4 Not all state files were succesfully deleted
  - 5 i2pd launch failed
*/
fn main() -> ExitCode {
   // parse command line
   let g = getopt3::new(getopt3::hideBin(std::env::args()), PROGRAM_FLAGS).unwrap();
   // handle simple commands
   if g.has('v') {
      println!("{}", PROGRAM_VERSION);
      ExitCode::SUCCESS
   } else if g.has('h') || g.has('?') {
      do_help();
      ExitCode::SUCCESS
   } else {
      //
      //   m a i n   p r o g r a m
      //

      // detect i2pd scoop install
      let (global_i2pd, user_i2pd) = detect_scoop_i2pd_installations();
      if !(global_i2pd || user_i2pd) {
         eprintln!("Error: no scoop i2pd installation detected.");
         ExitCode::from(1)
      } else {
         println!(
            "scoop i2pd installation detected: global {}, user {}.",
            global_i2pd, user_i2pd
         );
         if global_i2pd && user_i2pd {
            println!(concat!(
               "both global and user i2pd installations detected,",
               " using user i2pd."
            ));
         };
         // get workdir based on our installation
         let workdir = {
            if user_i2pd {
               get_scoop_i2pd_dir(FOLDERID_Profile)
            } else {
               get_scoop_i2pd_dir(FOLDERID_ProgramData)
            }
         };
         // wipe sensitive files
         let mut wipeOP = wipe_workdir!(&workdir);

         // if no -z is given, we launch i2pd
         if !g.has('z') {
            // Check if I2PD_EXE exists becaue it might be deleted by antivirus
            let exefile = {
               let mut p = PathBuf::from(&workdir);
               p.push(I2PD_EXE);
               p
            };
            if !exefile.exists() {
               // Handle the case when the main executable is missing
               eprintln!(
                  concat!("Error: {} not exists /", " might have been deleted by antivirus."),
                  I2PD_EXE
               );
               return ExitCode::from(2);
            }

            // Change to the I2PD installation directory
            if let Err(e) = std::env::set_current_dir(&workdir) {
               eprintln!(
                  concat!(
                     "Error: Failed to change working directory",
                     " to scoop i2pd installation: {}"
                  ),
                  e
               );
               return ExitCode::from(5);
            }

            // Launch I2PD_EXE without arguments
            println!("starting {}", I2PD_EXE);
            match std::process::Command::new(I2PD_EXE).spawn() {
               Err(e) => {
                  // Selhani spusteni: Vypiseme chybovou zpravu a vratime kod 5
                  eprintln!("Error: Failed to launch i2pd: {}", e);
                  return ExitCode::from(5);
               }
               Ok(mut pid) if g.has('w') => {
                  println!("Waiting for {} to exit.", I2PD_EXE);
                  // wait for pid
                  let exitcode = pid.wait();
                  if let Err(e) = exitcode {
                     // Selhani spusteni: Vypiseme chybovou zpravu a vratime kod 5
                     eprintln!("Error: Failed to wait for i2pd: {}", e);
                     return ExitCode::from(5);
                  } else if let Ok(code) = exitcode {
                     println!("{} exited with code: {}.", I2PD_EXE, code.code().unwrap_or(137));
                  }
                  // Wipe workdir after i2pd exit
                  wipeOP = wipe_workdir!(&workdir);
               }
               Ok(_) => (),
            };
         }
         if wipeOP == true {
            ExitCode::SUCCESS
         } else {
            ExitCode::from(4)
         }
      }
   }
}

fn get_scoop_i2pd_dir(folder: GUID) -> PathBuf {
   let mut f = dirs_sys::known_folder(folder).unwrap();
   f.push(SCOOP_I2PD_INSTALL);
   f
}

/**
  Detect if scoop i2pd is installed in a directory.
*/
fn detect_scoop_i2pd(search: impl AsRef<Path>) -> bool {
   #[cfg(debug_assertions)]
   println!("Testing {}", search.as_ref().display());
   let mut p = PathBuf::from(search.as_ref());
   if p.is_dir() {
      p.push(SCOOP_I2PD_INSTALL);
      #[cfg(debug_assertions)]
      println!("Testing2 {}", p.display());
      p.is_dir()
   } else {
      false
   }
}

/**
 Wipe and delete set of predefined files in a directory

 If file can't be wiped and deleted operation is retried after some time.

 If file doesn't exists its skipped and this state is considered success.

 ### Returns
     true - if all requested files were wiped
     false - one or more files wipe failed
*/
fn wipe_files(workdir: &Path) -> bool {
   /* retry queue */
   let mut TO_RETRY = Vec::new();

   // Delete files listed in TO_DELETE, queue retries on errors
   for file in TO_DELETE.iter() {
      let file_path = workdir.join(file);
      if file_path.exists() {
         let rc = wipe_and_delete!(&file_path);
         if rc.is_err() {
            eprintln!("Can not delete {}, will retry.", file_path.display());
            TO_RETRY.push(file_path);
         }
      }
   }

   // Retry to delete files
   let mut RETRY_COUNTER = RETRY_MAX;
   while !TO_RETRY.is_empty() && RETRY_COUNTER > 0 {
      // Pause execution for 5 seconds
      std::thread::sleep(std::time::Duration::from_secs(RETRY_WAIT));
      RETRY_COUNTER -= 1;
      // Retain only those file paths where deletion failed
      TO_RETRY.retain(|file_path| {
         if file_path.exists() {
            match wipe_and_delete!(file_path) {
               Ok(()) => {
                  eprintln!("Successfully deleted {}", file_path.display());
                  false // Successfully deleted, do not retain
               }
               Err(_) => {
                  eprintln!("Still cannot delete {}", file_path.display());
                  true // Failed to delete, retain this path
               }
            }
         } else {
            false // File does not exist, do not retain
         }
      });
   }

   // If retry queue is empty delete + wipe was succesfull
   TO_RETRY.is_empty()
}

/**
   Detects if global and local scoop i2pd installations exists

   ### Return value
       Tupple of two booleans - ( global, local ) i2pd detected
*/
fn detect_scoop_i2pd_installations() -> (bool, bool) {
   let global_i2pd = dirs_sys::known_folder(FOLDERID_ProgramData).map_or(false, |f| detect_scoop_i2pd(&f));
   let user_i2pd = dirs_sys::known_folder(FOLDERID_Profile).map_or(false, |f| detect_scoop_i2pd(&f));
   (global_i2pd, user_i2pd)
}

/** list of sensitive files to delete */
const TO_DELETE: [&str; 10] = [
   "i2pd.log",
   "ntcp2.keys",
   "ssu2.keys",
   "router.info",
   "router.keys",
   "http-proxy-keys.dat",
   "socks-proxy-keys.dat",
   "irc-keys.dat",
   "smtp-keys.dat",
   "pop3-keys.dat",
];

/** How many times we retry wipe/delete */
const RETRY_MAX: usize = 17;
/** Wait time in sec before next try */
const RETRY_WAIT: u64 = 3;
/** Where in profile folder should be i2pd */
const SCOOP_I2PD_INSTALL: &str = "scoop\\apps\\i2pd\\current";
/** i2pd executable name */
const I2PD_EXE: &str = "i2pd.exe";

/** Functions for wiping files */
mod wipe;