use std::{
env,
path::{Path, PathBuf},
};
use anyhow::Result;
use convert_case::{Case, Casing};
use crate::{dfx_json::DfxCanister, ic_test_json::IcpTestSetup};
pub const FOUNDRY_TOML: &str = "foundry.toml";
pub const HOME_VAR: &str = "$HOME";
pub fn get_home_dir() -> PathBuf {
dirs::home_dir().expect("Home directory not found!")
}
pub fn expand_path(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
let ret = if let Some(suffix) = path_str.strip_prefix("$HOME/") {
let home = get_home_dir();
home.join(suffix)
} else {
path.to_path_buf()
};
ret
}
pub fn get_main_project_dir() -> Result<PathBuf> {
let cur_dir = env::current_dir()?;
let toml = cur_dir.join("Cargo.toml");
if !toml.is_file() {
return Err(anyhow::anyhow!(
"The main project directory containing 'Cargo.toml' was not found. Please start the tool from the root folder of your project.",
));
}
Ok(cur_dir)
}
pub fn get_test_project_dir(setup: &IcpTestSetup) -> Result<PathBuf> {
let mut cur_dir = env::current_dir()?;
cur_dir.push(setup.test_folder.clone());
Ok(cur_dir)
}
pub fn get_relative_path(target_path: &Path) -> Result<PathBuf> {
let relative_path = if target_path.is_absolute() {
let project_dir = get_main_project_dir()?;
if let Ok(path) = target_path.strip_prefix(&project_dir) {
PathBuf::from(path)
} else {
let home: PathBuf = get_home_dir();
let stripped = target_path.strip_prefix(home.as_path())?;
let home = PathBuf::from(HOME_VAR);
home.join(stripped)
}
} else {
let mut rel_path = PathBuf::new();
rel_path.push(target_path);
rel_path
};
Ok(relative_path)
}
pub fn get_path_relative_to_test_dir(target_path: &Path, test_folder: &str) -> Result<PathBuf> {
if target_path.starts_with(HOME_VAR) || target_path.starts_with("/") {
return Ok(PathBuf::new().join(target_path));
}
let mut ret = PathBuf::new();
for _ in test_folder.trim().split("/") {
ret.push("..");
}
let relative_path = get_relative_path(target_path)?;
ret.push(relative_path);
Ok(ret)
}
pub fn get_pull_folder(canister: &DfxCanister) -> Option<PathBuf> {
if let Some(canister_type) = &canister.canister_type {
if canister_type == "pull" {
if let Some(id) = &canister.id {
let cache_canister_dir =
PathBuf::new().join(format!("{HOME_VAR}/.cache/dfinity/pulled/{id}"));
return Some(cache_canister_dir);
}
}
}
None
}
pub fn find_candid(canister_name: &str, canister: &DfxCanister) -> Option<PathBuf> {
let mut files = Vec::new();
if let Some(candid) = &canister.candid {
files.push(PathBuf::from(candid.clone()));
}
let pull_dir = get_pull_folder(canister);
if let Some(pull) = pull_dir {
files.push(pull.join("service.did"));
}
files.push(PathBuf::from(format!(
".dfx/local/canisters/{canister_name}/constructor.did"
)));
for file in files {
if file.exists() && file.is_file() {
let candid = get_relative_path(file.as_path()).expect("Error finding relative path!");
return Some(candid);
}
}
None
}
pub fn find_wasm(
canister_name: &str,
canister: &DfxCanister,
_setup: &IcpTestSetup,
) -> Result<Option<String>> {
let mut files = Vec::new();
if let Some(wasm) = &canister.wasm {
files.push(PathBuf::from(wasm.clone()));
}
if let Some(wasm) = &canister.wasm {
if wasm.ends_with(".wasm") {
files.push(PathBuf::from(wasm.to_owned() + ".gz"));
}
}
let main_project_dir = get_main_project_dir()?;
let snakecase_canister_name = canister_name.to_case(Case::Snake);
let wasm_candidates = [
format!("target/wasm32-unknown-unknown/release/{snakecase_canister_name}.wasm"),
format!("target/wasm32-unknown-unknown/release/{snakecase_canister_name}.wasm.gz"),
format!("target/wasm32-unknown-unknown/debug/{snakecase_canister_name}.wasm"),
format!("target/wasm32-unknown-unknown/debug/{snakecase_canister_name}.wasm.gz"),
format!("target/wasm32-unknown-unknown/release/{canister_name}.wasm"),
format!("target/wasm32-unknown-unknown/release/{canister_name}.wasm.gz"),
format!("target/wasm32-unknown-unknown/debug/{canister_name}.wasm"),
format!("target/wasm32-unknown-unknown/debug/{canister_name}.wasm.gz"),
format!(".dfx/local/canisters/{canister_name}/{canister_name}.wasm"),
format!(".dfx/local/canisters/{canister_name}/{canister_name}.wasm.gz"),
];
for candidate in wasm_candidates {
files.push(main_project_dir.join(&candidate));
}
let pull_dir = get_pull_folder(canister);
if let Some(dir) = pull_dir {
files.push(dir.join("canister.wasm"));
files.push(dir.join("canister.wasm.gz"));
}
for wasm_file in &files {
let wasm_file = expand_path(wasm_file.as_path());
if wasm_file.exists() && wasm_file.is_file() {
let relative_wasm =
get_relative_path(wasm_file.as_path()).expect("Failed to get relative path: {}");
return Ok(Some(relative_wasm.to_string_lossy().to_string()));
}
}
Ok(None)
}
pub fn search_file_recursively(path: &Path, depth: u32, search_name: &str) -> Option<PathBuf> {
let path = Path::new(path);
let file = path.join(search_name);
if file.is_file() {
return Some(file);
}
if depth == 0 {
return None;
}
let entries =
std::fs::read_dir(path).expect("Failed to read the directory during search: {path:?}");
for entry in entries.filter_map(|entry| entry.ok()) {
let file_name = entry.file_name().to_string_lossy().to_string();
let cur_path = path.join(&file_name);
if file_name.starts_with(".") || file_name == "target" {
continue;
}
if !cur_path.is_dir() {
continue;
}
let found = search_file_recursively(&cur_path, depth - 1, search_name);
if found.is_some() {
return found;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn relative_path_inside_project_dir() {
let project_dir = get_main_project_dir().unwrap();
let target = project_dir.join("src/main.rs");
let result = get_relative_path(&target).unwrap();
assert_eq!(result, PathBuf::from("src/main.rs"));
}
#[test]
fn relative_path_for_relative_input() {
let target = Path::new("data/config.json");
let result = get_relative_path(target).unwrap();
assert_eq!(result, PathBuf::from("data/config.json"));
}
#[test]
fn error_if_not_under_project_or_home() {
let unrelated_path = Path::new("/tmp/file.txt");
let result = get_relative_path(unrelated_path);
assert!(
result.is_err(),
"Expected error when path is outside home/project"
);
}
#[test]
fn search_rec() {
let r = search_file_recursively(Path::new("."), 2, "main.rs");
assert!(r.is_some());
let r = search_file_recursively(Path::new("."), 1, "main.rs");
assert!(r.is_none());
}
#[test]
fn expand() {
let expanded = expand_path(Path::new("$HOME/test"));
println!("{expanded:?}");
}
}