i2pd-launch 0.4.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)]

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;

const PROGRAM_VERSION: &str = env!("CARGO_PKG_VERSION");

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

fn do_help() {
   println!("i2pd-launch [ -z ] | [ -h | -v | -? ]");
   println!("Clears i2pd local state and launches it");
   println!();
   println!("  -h | -? display this help message");
   println!("  -v  print program version and exit");
   println!("  -z  do not start i2pd only wipe sensitive information");
   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()), "h?vz").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 {
      // main program
      //
      // detect i2pd scoop install
      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));
      if !(global_i2pd || user_i2pd) {
         println!("no scoop i2pd install detected.");
         ExitCode::from(1)
      } else {
         println!(
            "scoop i2pd installation detected: global {}, user {}.",
            global_i2pd, user_i2pd
         );
         let workdir = {
            if user_i2pd {
               get_scoop_i2pd_dir(FOLDERID_Profile)
            } else {
               get_scoop_i2pd_dir(FOLDERID_ProgramData)
            }
         };

         let wipeOP = wipe_files(&workdir);
         if !wipeOP {
            eprintln!("Unable to delete all i2pd state files, no more retries left.");
         } else {
            println!("Local state cleared.");
         }

         // if no -z is given, we start 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
               println!("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!("Failed to change working directory to scoop i2pd installation: {}", e);
               return ExitCode::from(5);
            }

            // Launch I2PD_EXE without arguments
            println!("starting {}", I2PD_EXE);
            if let Err(e) = std::process::Command::new(I2PD_EXE).spawn() {
               // Selhani spusteni: Vypiseme chybovou zpravu a vratime kod 5
               eprintln!("Failed to launch i2pd: {}", e);
               return ExitCode::from(5);
            };
         }
         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
}

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
   }
}

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()
}

/** 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 = 5;
/** Wait time in sec before next try */
const RETRY_WAIT: u64 = 5;

const SCOOP_I2PD_INSTALL: &str = "scoop\\apps\\i2pd\\current";
const I2PD_EXE: &str = "i2pd.exe";

mod wipe;