use crate::batbelt::command_line::{execute_command, CodeEditor};
use crate::config::{BatAuditorConfig, BatConfig};
use crate::batbelt::BatEnumerator;
use error_stack::{FutureExt, IntoReport, Report, Result, ResultExt};
use crate::batbelt::git::git_commit::GitCommit;
use serde::{Deserialize, Serialize};
use std::{error::Error, fmt, fs, path::Path};
use walkdir::{DirEntry, WalkDir};
#[derive(Debug)]
pub struct BatPathError;
impl fmt::Display for BatPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BatPath error")
}
}
impl Error for BatPathError {}
type BatPathResult<T> = Result<T, BatPathError>;
#[derive(
Debug, PartialEq, Clone, strum_macros::Display, strum_macros::EnumIter, Serialize, Deserialize,
)]
pub enum BatFile {
BatToml,
BatAuditorToml,
Batlog,
BatMetadataFile,
BatAnalyticsFile,
ThreatModeling,
FindingCandidates,
OpenQuestions,
ProgramLib,
Readme,
GitIgnore,
PackageJson,
RobotFile,
ProgramAccountsMetadataFile,
CodeOverhaulSummaryFile,
CodeOverhaulToReview { file_name: String },
CodeOverhaulStarted { file_name: String },
CodeOverhaulFinished { file_name: String },
CodeOverhaulDeprecated { file_name: String },
FindingToReview { file_name: String },
FindingAccepted { file_name: String },
FindingRejected { file_name: String },
Generic { file_path: String },
}
impl BatEnumerator for BatFile {}
impl BatFile {
pub fn get_path(&self, canonicalize: bool) -> BatPathResult<String> {
let path = match self {
BatFile::BatToml => "Bat.toml".to_string(),
BatFile::BatAuditorToml => "BatAuditor.toml".to_string(),
BatFile::Batlog => "Batlog.log".to_string(),
BatFile::PackageJson => "./package.json".to_string(),
BatFile::GitIgnore => "./.gitignore".to_string(),
BatFile::ProgramLib => {
BatConfig::get_config()
.change_context(BatPathError)?
.program_lib_path
}
BatFile::Readme => "./README.md".to_string(),
BatFile::RobotFile => format!(
"{}/robot.md",
BatFolder::AuditorNotes.get_path(canonicalize)?
),
BatFile::FindingCandidates => {
format!(
"{}/finding_candidates.md",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFile::OpenQuestions => {
format!(
"{}/open_questions.md",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFile::ThreatModeling => {
format!(
"{}/threat_modeling.md",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFile::ProgramAccountsMetadataFile => {
format!(
"{}/program_accounts_metadata.json",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFile::CodeOverhaulSummaryFile => {
format!(
"{}/code_overhaul_summary.md",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFile::CodeOverhaulToReview { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::CodeOverhaulToReview.get_path(canonicalize)?
)
}
BatFile::CodeOverhaulStarted { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::CodeOverhaulStarted.get_path(canonicalize)?
)
}
BatFile::CodeOverhaulFinished { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::CodeOverhaulFinished.get_path(canonicalize)?
)
}
BatFile::CodeOverhaulDeprecated { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::CodeOverhaulDeprecated.get_path(canonicalize)?
)
}
BatFile::FindingToReview { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::FindingsToReview.get_path(canonicalize)?
)
}
BatFile::FindingAccepted { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::FindingsAccepted.get_path(canonicalize)?
)
}
BatFile::FindingRejected { file_name } => {
let entrypoint_name = file_name.trim_end_matches(".md");
format!(
"{}/{entrypoint_name}.md",
BatFolder::FindingsRejected.get_path(canonicalize)?
)
}
BatFile::BatMetadataFile => "./BatMetadata.json".to_string(),
BatFile::BatAnalyticsFile => {
format!(
"{}/BatAnalytics.json",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFile::Generic { file_path } => file_path.clone(),
};
if canonicalize {
return canonicalize_path(path);
}
Ok(path)
}
pub fn read_content(&self, canonicalize: bool) -> BatPathResult<String> {
fs::read_to_string(self.get_path(canonicalize)?)
.into_report()
.change_context(BatPathError)
.attach_printable(format!(
"Error reading content for file in path:\n {}",
self.get_path(canonicalize)?
))
}
pub fn write_content(&self, canonicalize: bool, content: &str) -> BatPathResult<()> {
log::debug!("{}.write_content:\n{} ", self, content);
fs::write(self.get_path(canonicalize)?, content)
.into_report()
.change_context(BatPathError)
.attach_printable(format!(
"Error writing content for file in path:\n {}",
self.get_path(canonicalize)?
))
}
pub fn remove_file(&self) -> BatPathResult<()> {
if self.file_exists()? {
fs::remove_file(self.get_path(false)?)
.into_report()
.change_context(BatPathError)
.attach_printable(format!(
"Error removing file in path:\n {}",
self.get_path(false)?
))?;
}
Ok(())
}
pub fn create_empty(&self, canonicalize: bool) -> BatPathResult<()> {
execute_command("touch", &[&self.get_path(canonicalize)?], false)
.change_context(BatPathError)
.attach_printable(format!(
"Error creating empty file in path:\n {}",
self.get_path(canonicalize)?
))?;
Ok(())
}
pub fn move_file(&self, destination_path: &str) -> BatPathResult<()> {
execute_command("mv", &[&self.get_path(true)?, destination_path], false)
.change_context(BatPathError)
.attach_printable(format!(
"Error moving file :\n{} \nto path:\n {}",
self.get_path(true)?,
destination_path
))?;
Ok(())
}
pub fn open_in_editor(
&self,
canonicalize: bool,
line_index: Option<usize>,
) -> BatPathResult<()> {
CodeEditor::open_file_in_editor(&self.get_path(canonicalize)?, line_index)
.change_context(BatPathError)
}
pub fn file_exists(&self) -> BatPathResult<bool> {
Ok(Path::new(&self.get_path(false)?).is_file())
}
pub fn get_file_name(&self) -> BatPathResult<String> {
Ok(Path::new(&self.get_path(false)?)
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string())
}
pub fn commit_file(&self, commit_message: Option<String>) -> BatPathResult<()> {
let commit_message = commit_message.unwrap_or(self.default_commit_message()?);
GitCommit::BatFileCommit {
bat_file: self.clone(),
commit_message,
}
.create_commit(true)
.change_context(BatPathError)?;
Ok(())
}
pub fn default_commit_message(&self) -> BatPathResult<String> {
let message = match self {
BatFile::BatAnalyticsFile => "cache: BatAnalytics.json updated".to_string(),
_ => {
return Err(Report::new(BatPathError).attach_printable(format!(
"{} does not implement default commit message",
self.to_string()
)));
}
};
Ok(message)
}
}
#[derive(
Debug, PartialEq, Clone, strum_macros::Display, strum_macros::EnumIter, Serialize, Deserialize,
)]
pub enum BatFolder {
ProgramPath,
ProjectFolderPath,
FindingsFolderPath,
FindingsToReview,
FindingsAccepted,
FindingsRejected,
CodeOverhaulFolderPath,
CodeOverhaulToReview,
CodeOverhaulStarted,
CodeOverhaulFinished,
CodeOverhaulDeprecated,
AuditorNotes,
AuditorFigures,
Notes,
}
impl BatEnumerator for BatFolder {}
impl BatFolder {
pub fn get_path(&self, canonicalize: bool) -> Result<String, BatPathError> {
let bat_config = BatConfig::get_config().change_context(BatPathError)?;
let path = match self {
BatFolder::Notes => "notes".to_string(),
BatFolder::ProjectFolderPath => format!("{}", bat_config.project_name),
BatFolder::AuditorNotes => {
let bat_auditor_config =
BatAuditorConfig::get_config().change_context(BatPathError)?;
let auditor_notes_folder_path = format!(
"{}/{}-notes",
Self::Notes.get_path(canonicalize)?,
bat_auditor_config.auditor_name
);
auditor_notes_folder_path
}
BatFolder::AuditorFigures => {
format!(
"{}/figures",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFolder::ProgramPath => bat_config
.program_lib_path
.trim_end_matches("/lib.rs")
.to_string(),
BatFolder::FindingsFolderPath => {
format!("{}/findings", BatFolder::AuditorNotes.get_path(true)?)
}
BatFolder::FindingsToReview => {
format!(
"{}/to-review",
BatFolder::FindingsFolderPath.get_path(canonicalize)?
)
}
BatFolder::FindingsAccepted => {
format!(
"{}/accepted",
BatFolder::FindingsFolderPath.get_path(canonicalize)?
)
}
BatFolder::FindingsRejected => {
format!(
"{}/rejected",
BatFolder::FindingsFolderPath.get_path(canonicalize)?
)
}
BatFolder::CodeOverhaulFolderPath => {
format!(
"{}/code-overhaul",
BatFolder::AuditorNotes.get_path(canonicalize)?
)
}
BatFolder::CodeOverhaulToReview => {
format!(
"{}/to-review",
BatFolder::CodeOverhaulFolderPath.get_path(canonicalize)?
)
}
BatFolder::CodeOverhaulStarted => {
format!(
"{}/started",
BatFolder::CodeOverhaulFolderPath.get_path(canonicalize)?
)
}
BatFolder::CodeOverhaulDeprecated => {
format!(
"{}/deprecated",
BatFolder::CodeOverhaulFolderPath.get_path(canonicalize)?
)
}
BatFolder::CodeOverhaulFinished => {
format!(
"{}/finished",
BatFolder::CodeOverhaulFolderPath.get_path(canonicalize)?
)
}
};
if canonicalize {
return canonicalize_path(path);
}
Ok(path)
}
pub fn get_all_files_dir_entries(
&self,
sorted: bool,
file_name_to_exclude_filters: Option<Vec<String>>,
file_extension_to_include_filters: Option<Vec<String>>,
) -> Result<Vec<DirEntry>, BatPathError> {
let folder_path = self.get_path(false)?;
let mut dir_entries = WalkDir::new(folder_path)
.into_iter()
.filter_map(|f| {
let dir_entry = f.unwrap();
if !dir_entry.metadata().unwrap().is_file() || dir_entry.file_name() == ".gitkeep" {
return None;
}
if file_name_to_exclude_filters.is_some()
&& file_name_to_exclude_filters
.clone()
.unwrap()
.into_iter()
.any(|filter| dir_entry.file_name().to_str().unwrap() == filter)
{
return None;
}
if file_extension_to_include_filters.is_some()
&& !file_extension_to_include_filters
.clone()
.unwrap()
.into_iter()
.any(|filter| dir_entry.file_name().to_str().unwrap().ends_with(&filter))
{
return None;
}
Some(dir_entry)
})
.collect::<Vec<_>>();
if sorted {
dir_entries.sort_by(|dir_entry_a, dir_entry_b| {
dir_entry_a.file_name().cmp(dir_entry_b.file_name())
});
}
Ok(dir_entries)
}
pub fn get_all_files_names(
&self,
sorted: bool,
file_name_to_exclude_filters: Option<Vec<String>>,
file_extension_to_include_filters: Option<Vec<String>>,
) -> Result<Vec<String>, BatPathError> {
let dir_entries = self.get_all_files_dir_entries(
sorted,
file_name_to_exclude_filters,
file_extension_to_include_filters,
)?;
Ok(dir_entries
.into_iter()
.map(|dir_entry| dir_entry.file_name().to_str().unwrap().to_string())
.collect::<Vec<_>>())
}
pub fn get_all_bat_files(
&self,
sorted: bool,
file_name_to_exclude_filters: Option<Vec<String>>,
file_extension_to_include_filters: Option<Vec<String>>,
) -> BatPathResult<Vec<BatFile>> {
let generic_vec = self
.get_all_files_dir_entries(
sorted,
file_name_to_exclude_filters,
file_extension_to_include_filters,
)?
.into_iter()
.map(|entry| BatFile::Generic {
file_path: entry.path().to_str().unwrap().to_string(),
})
.collect::<Vec<_>>();
Ok(generic_vec)
}
pub fn folder_exists(&self) -> BatPathResult<bool> {
Ok(Path::new(&self.get_path(false)?).is_dir())
}
pub fn create_folder(&self) -> BatPathResult<()> {
fs::create_dir_all(&self.get_path(false)?)
.into_report()
.change_context(BatPathError)
}
}
pub fn prettify_source_code_path(path: &str) -> BatPathResult<String> {
let mut path_split = path.split("/src/");
let prefix_with_program = path_split.next().unwrap();
let program_name = prefix_with_program.split("/").last().unwrap();
let prefix = prefix_with_program.trim_end_matches(program_name);
let pretty_path = path.trim_start_matches(prefix);
Ok(pretty_path.to_string())
}
pub fn canonicalize_path(path_to_canonicalize: String) -> Result<String, BatPathError> {
let error_message = format!("Error canonicalization path: {}", path_to_canonicalize);
let canonicalized_path = Path::new(&(path_to_canonicalize))
.canonicalize()
.into_report()
.change_context(BatPathError)
.attach_printable(error_message)?
.into_os_string()
.into_string()
.unwrap();
Ok(canonicalized_path)
}