mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
Added foundational modules for core functionalities:
- Introduced `walk.rs` as a parallel directory walker for search operations. - Implemented basic index handling in `commands/index.rs`. - Created `utils/config.rs` for configuration management with placeholders for future enhancements.
This commit is contained in:
commit
ab5558f537
16 changed files with 2187 additions and 0 deletions
88
src/cli.rs
Normal file
88
src/cli.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "nano")]
|
||||
#[command(about = "A fast vulnerability scanner with project indexing")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Scan project for vulnerabilities
|
||||
Scan {
|
||||
/// Path to scan (defaults to current directory)
|
||||
#[arg(default_value = ".")]
|
||||
path: String,
|
||||
|
||||
/// Skip using/building index, scan directly
|
||||
#[arg(long)]
|
||||
no_index: bool,
|
||||
|
||||
/// Force rebuild index before scanning
|
||||
#[arg(long)]
|
||||
rebuild_index: bool,
|
||||
|
||||
/// Output format
|
||||
#[arg(short, long, value_enum, default_value = "table")]
|
||||
format: OutputFormat,
|
||||
|
||||
/// Show only high severity issues
|
||||
#[arg(long)]
|
||||
high_only: bool,
|
||||
},
|
||||
|
||||
/// Manage project indexes
|
||||
Index {
|
||||
#[command(subcommand)]
|
||||
action: IndexAction,
|
||||
},
|
||||
|
||||
/// List all indexed projects
|
||||
List {
|
||||
/// Show detailed information
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
|
||||
/// Remove project from index
|
||||
Clean {
|
||||
/// Project name or path to clean
|
||||
project: Option<String>,
|
||||
|
||||
/// Clean all projects
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum IndexAction {
|
||||
/// Build or update index for current project
|
||||
Build {
|
||||
/// Path to index (defaults to current directory)
|
||||
#[arg(default_value = ".")]
|
||||
path: String,
|
||||
|
||||
/// Force full rebuild
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Show index status and statistics
|
||||
Status {
|
||||
/// Project path to check
|
||||
#[arg(default_value = ".")]
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||
pub enum OutputFormat {
|
||||
Table,
|
||||
Json,
|
||||
Csv,
|
||||
Sarif,
|
||||
}
|
||||
37
src/commands/clean.rs
Normal file
37
src/commands/clean.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::{env, fs};
|
||||
use crate::utils::get_project_info;
|
||||
|
||||
pub fn handle(
|
||||
project: Option<String>,
|
||||
all: bool,
|
||||
config_dir: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if all {
|
||||
println!("Cleaning all indexes...");
|
||||
if config_dir.exists() {
|
||||
fs::remove_dir_all(config_dir)?;
|
||||
fs::create_dir_all(config_dir)?;
|
||||
}
|
||||
println!("All indexes cleaned.");
|
||||
} else if let Some(proj_name) = project {
|
||||
let db_path = config_dir.join(format!("{}.sqlite", proj_name));
|
||||
if db_path.exists() {
|
||||
fs::remove_file(&db_path)?;
|
||||
println!("Cleaned index for: {}", proj_name);
|
||||
} else {
|
||||
println!("No index found for: {}", proj_name);
|
||||
}
|
||||
} else {
|
||||
let current_dir = env::current_dir()?;
|
||||
let (project_name, db_path) = get_project_info(¤t_dir, config_dir)?;
|
||||
|
||||
if db_path.exists() {
|
||||
fs::remove_file(&db_path)?;
|
||||
println!("Cleaned index for: {}", project_name);
|
||||
} else {
|
||||
println!("No index found for current project: {}", project_name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
48
src/commands/index.rs
Normal file
48
src/commands/index.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use std::fs;
|
||||
use crate::cli::IndexAction;
|
||||
use crate::utils::project::get_project_info;
|
||||
|
||||
pub fn handle(
|
||||
action: IndexAction,
|
||||
database_dir: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match action {
|
||||
IndexAction::Build { path, force } => {
|
||||
let build_path = std::path::Path::new(&path).canonicalize()?;
|
||||
let (project_name, db_path) = get_project_info(&build_path, database_dir)?;
|
||||
|
||||
if force || !db_path.exists() {
|
||||
println!("Building index for: {}", project_name);
|
||||
build_index(&build_path, &db_path)?;
|
||||
println!("Index built: {}", db_path.display());
|
||||
} else {
|
||||
println!("Index already exists. Use --force to rebuild.");
|
||||
}
|
||||
}
|
||||
IndexAction::Status { path } => {
|
||||
let status_path = std::path::Path::new(&path).canonicalize()?;
|
||||
let (project_name, db_path) = get_project_info(&status_path, database_dir)?;
|
||||
|
||||
println!("Project: {}", project_name);
|
||||
println!("Index path: {}", db_path.display());
|
||||
println!("Index exists: {}", db_path.exists());
|
||||
|
||||
if db_path.exists() {
|
||||
let metadata = fs::metadata(&db_path)?;
|
||||
println!("Index size: {} bytes", metadata.len());
|
||||
println!("Last modified: {:?}", metadata.modified()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_index(
|
||||
_project_path: &std::path::Path,
|
||||
db_path: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// TODO: Implement actual index building
|
||||
fs::File::create(db_path)?;
|
||||
println!("Index building logic goes here...");
|
||||
Ok(())
|
||||
}
|
||||
35
src/commands/list.rs
Normal file
35
src/commands/list.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use std::fs;
|
||||
|
||||
pub fn handle(
|
||||
verbose: bool,
|
||||
database_dir: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Indexed projects:");
|
||||
|
||||
if database_dir.exists() {
|
||||
for entry in fs::read_dir(database_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("sqlite") {
|
||||
let project_name = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
println!(" {}", project_name);
|
||||
|
||||
if verbose {
|
||||
let metadata = fs::metadata(&path)?;
|
||||
println!(" Path: {}", path.display());
|
||||
println!(" Size: {} bytes", metadata.len());
|
||||
println!(" Modified: {:?}", metadata.modified()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" No indexed projects found.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
29
src/commands/mod.rs
Normal file
29
src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
pub mod scan;
|
||||
pub mod index;
|
||||
pub mod list;
|
||||
pub mod clean;
|
||||
|
||||
use crate::cli::Commands;
|
||||
use std::path::Path;
|
||||
use crate::utils::config::Config;
|
||||
|
||||
pub fn handle_command(
|
||||
command: Commands,
|
||||
database_dir: &Path,
|
||||
config: &Config
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match command {
|
||||
Commands::Scan { path, no_index, rebuild_index, format, high_only } => {
|
||||
scan::handle(&path, no_index, rebuild_index, format, high_only, database_dir, config)
|
||||
}
|
||||
Commands::Index { action } => {
|
||||
index::handle(action, database_dir)
|
||||
}
|
||||
Commands::List { verbose } => {
|
||||
list::handle(verbose, database_dir)
|
||||
}
|
||||
Commands::Clean { project, all } => {
|
||||
clean::handle(project, all, database_dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/commands/scan.rs
Normal file
53
src/commands/scan.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use crate::cli::OutputFormat;
|
||||
use crate::utils::project::get_project_info;
|
||||
use std::path::Path;
|
||||
use crate::utils::config::Config;
|
||||
|
||||
pub fn handle(
|
||||
path: &str,
|
||||
no_index: bool,
|
||||
rebuild_index: bool,
|
||||
format: OutputFormat,
|
||||
high_only: bool,
|
||||
database_dir: &Path,
|
||||
config: &Config,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let scan_path = Path::new(path).canonicalize()?;
|
||||
let (project_name, db_path) = get_project_info(&scan_path, database_dir)?;
|
||||
|
||||
tracing::info!("Config: {:?}", config);
|
||||
tracing::info!("Scanning project: {}", project_name);
|
||||
tracing::info!("Scan path: {}", scan_path.display());
|
||||
|
||||
if no_index {
|
||||
tracing::info!("Scanning without index...");
|
||||
scan_filesystem(&scan_path)?;
|
||||
} else {
|
||||
if rebuild_index || !db_path.exists() {
|
||||
tracing::info!("Building/updating index...");
|
||||
crate::commands::index::build_index(&scan_path, &db_path)?;
|
||||
}
|
||||
|
||||
tracing::info!("Using index: {}", db_path.display());
|
||||
scan_with_index(&db_path)?;
|
||||
}
|
||||
|
||||
tracing::info!("Output format: {:?}", format);
|
||||
if high_only {
|
||||
tracing::info!("Filtering: High severity only");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_filesystem(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// TODO: Implement direct filesystem scanning
|
||||
tracing::info!("Direct filesystem scan of: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_with_index(db_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// TODO: Implement index-based scanning
|
||||
tracing::info!("Index-based scan using: {}", db_path.display());
|
||||
Ok(())
|
||||
}
|
||||
0
src/exit_codes.rs
Normal file
0
src/exit_codes.rs
Normal file
43
src/filetypes.rs
Normal file
43
src/filetypes.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// use crate::dir_entry;
|
||||
// use crate::filesystem;
|
||||
//
|
||||
// use faccess::PathExt;
|
||||
//
|
||||
// /// Whether or not to show
|
||||
// #[derive(Default)]
|
||||
// pub struct FileTypes {
|
||||
// pub files: bool,
|
||||
// pub directories: bool,
|
||||
// pub symlinks: bool,
|
||||
// pub block_devices: bool,
|
||||
// pub char_devices: bool,
|
||||
// pub sockets: bool,
|
||||
// pub pipes: bool,
|
||||
// pub executables_only: bool,
|
||||
// pub empty_only: bool,
|
||||
// }
|
||||
//
|
||||
// impl FileTypes {
|
||||
// pub fn should_ignore(&self, entry: &dir_entry::DirEntry) -> bool {
|
||||
// if let Some(ref entry_type) = entry.file_type() {
|
||||
// (!self.files && entry_type.is_file())
|
||||
// || (!self.directories && entry_type.is_dir())
|
||||
// || (!self.symlinks && entry_type.is_symlink())
|
||||
// || (!self.block_devices && filesystem::is_block_device(*entry_type))
|
||||
// || (!self.char_devices && filesystem::is_char_device(*entry_type))
|
||||
// || (!self.sockets && filesystem::is_socket(*entry_type))
|
||||
// || (!self.pipes && filesystem::is_pipe(*entry_type))
|
||||
// || (self.executables_only && !entry.path().executable())
|
||||
// || (self.empty_only && !filesystem::is_empty(entry))
|
||||
// || !(entry_type.is_file()
|
||||
// || entry_type.is_dir()
|
||||
// || entry_type.is_symlink()
|
||||
// || filesystem::is_block_device(*entry_type)
|
||||
// || filesystem::is_char_device(*entry_type)
|
||||
// || filesystem::is_socket(*entry_type)
|
||||
// || filesystem::is_pipe(*entry_type))
|
||||
// } else {
|
||||
// true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
59
src/main.rs
Normal file
59
src/main.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
mod cli;
|
||||
mod commands;
|
||||
mod utils;
|
||||
|
||||
use crate::utils::Config;
|
||||
use cli::Cli;
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
use std::fs;
|
||||
|
||||
use tracing_subscriber::{fmt, EnvFilter, Registry};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::fmt::time;
|
||||
|
||||
// use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
// use tracing_appender::non_blocking;
|
||||
|
||||
fn init_tracing() {
|
||||
// let file_appender = RollingFileAppender::new(Rotation::HOURLY, "logs", "nano-scanner.log");
|
||||
// let (file_writer, guard) = non_blocking(file_appender);
|
||||
|
||||
let fmt_layer = fmt::layer()
|
||||
.pretty()
|
||||
.with_thread_ids(true)
|
||||
.with_timer(time::UtcTime::rfc_3339());
|
||||
|
||||
// let file_layer = fmt::layer()
|
||||
// .with_writer(file_writer)
|
||||
// .without_time()
|
||||
// .json();
|
||||
|
||||
Registry::default()
|
||||
.with(EnvFilter::from_default_env()) // obey RUST_LOG
|
||||
.with(fmt_layer)
|
||||
.init(); // install as the global subscriber
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
init_tracing();
|
||||
|
||||
tracing::debug!("CLI starting up");
|
||||
let cli = Cli::parse();
|
||||
|
||||
let proj_dirs = ProjectDirs::from("dev", "ecpeter23", "nano")
|
||||
.ok_or("Unable to determine project directories")?;
|
||||
|
||||
let config_dir = proj_dirs.config_dir();
|
||||
fs::create_dir_all(config_dir)?;
|
||||
|
||||
let database_dir = proj_dirs.data_local_dir();
|
||||
fs::create_dir_all(database_dir)?;
|
||||
|
||||
let config = Config::load(config_dir)?;
|
||||
|
||||
commands::handle_command(cli.command, database_dir, &config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
260
src/utils/config.rs
Normal file
260
src/utils/config.rs
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path};
|
||||
use std::fs;
|
||||
use toml;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct ScannerConfig {
|
||||
/// The maximum file size to scan, in megabytes. TODO: IMPLEMENT
|
||||
pub max_file_size_mb: u64,
|
||||
|
||||
/// File extensions to exclude from scanning. TODO: IMPLEMENT
|
||||
pub excluded_extensions: Vec<String>,
|
||||
|
||||
/// Directories to exclude from scanning. TODO: IMPLEMENT
|
||||
pub excluded_directories: Vec<String>,
|
||||
|
||||
/// Whether to respect the global ignore file or not. TODO: IMPLEMENT
|
||||
pub read_global_ignore: bool,
|
||||
|
||||
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not. TODO: IMPLEMENT
|
||||
pub read_vcsignore: bool,
|
||||
|
||||
/// Whether to require a `.git` directory to respect gitignore files. TODO: IMPLEMENT
|
||||
pub require_git_to_read_vcsignore: bool,
|
||||
|
||||
/// Whether to limit the search to starting file system or not. TODO: IMPLEMENT
|
||||
pub one_file_system: bool,
|
||||
|
||||
/// Whether to follow symlinks or not. TODO: IMPLEMENT
|
||||
pub follow_symlinks: bool,
|
||||
|
||||
/// Whether to scan hidden files or not. TODO: IMPLEMENT
|
||||
pub scan_hidden_files: bool,
|
||||
}
|
||||
impl Default for ScannerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_file_size_mb: 100,
|
||||
excluded_extensions: vec![
|
||||
"jpg", "png", "gif", "mp4", "avi", "mkv",
|
||||
"zip", "tar", "gz", "exe", "dll", "so",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect(),
|
||||
excluded_directories: vec![
|
||||
"node_modules", ".git", "target", ".vscode", ".idea", "build", "dist",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect(),
|
||||
read_global_ignore: false,
|
||||
read_vcsignore: true,
|
||||
require_git_to_read_vcsignore: true,
|
||||
one_file_system: false,
|
||||
follow_symlinks: false,
|
||||
scan_hidden_files: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct DatabaseConfig {
|
||||
/// The number of days to keep database files for. TODO: IMPLEMENT
|
||||
pub auto_cleanup_days: u32,
|
||||
|
||||
/// The maximum size of the database, in megabytes. TODO: IMPLEMENT
|
||||
pub max_db_size_mb: u64,
|
||||
|
||||
/// Whether to run a VACUUM on startup or not. TODO: IMPLEMENT
|
||||
pub vacuum_on_startup: bool,
|
||||
}
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_cleanup_days: 30,
|
||||
max_db_size_mb: 1024,
|
||||
vacuum_on_startup: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct OutputConfig {
|
||||
/// The default output format. TODO: IMPLEMENT
|
||||
pub default_format: String,
|
||||
|
||||
/// Whether to show progress or not. TODO: IMPLEMENT
|
||||
pub show_progress: bool,
|
||||
|
||||
/// Whether to colorize output or not. TODO: IMPLEMENT
|
||||
pub color_output: bool,
|
||||
|
||||
/// The maximum number of results to show. TODO: IMPLEMENT
|
||||
pub max_results: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for OutputConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_format: "table".into(),
|
||||
show_progress: true,
|
||||
color_output: true,
|
||||
max_results: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct PerformanceConfig {
|
||||
/// The maximum search depth, or `None` if no maximum search depth should be set.
|
||||
///
|
||||
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
|
||||
/// all files under subdirectories of the current directory, etc.
|
||||
pub max_depth: Option<usize>, // TODO: IMPLEMENT
|
||||
|
||||
/// The minimum depth for reported entries, or `None`.
|
||||
pub min_depth: Option<usize>, // TODO: IMPLEMENT
|
||||
|
||||
/// Whether to stop traversing into matching directories.
|
||||
pub prune: bool, // TODO: IMPLEMENT
|
||||
|
||||
/// The maximum number of worker threads to use., or `None` to auto-detect.
|
||||
pub worker_threads: Option<u32>, // TODO: IMPLEMENT
|
||||
|
||||
/// The maximum number of entries to index in a single chunk.
|
||||
pub index_chunk_size: u32, // TODO: IMPLEMENT
|
||||
|
||||
/// The maximum amount of memory to use, in megabytes.
|
||||
pub memory_limit_mb: u64, // TODO: IMPLEMENT
|
||||
}
|
||||
|
||||
impl Default for PerformanceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_depth: None,
|
||||
min_depth: None,
|
||||
prune: false,
|
||||
worker_threads: None,
|
||||
index_chunk_size: 1_000,
|
||||
memory_limit_mb: 512,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub scanner: ScannerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub output: OutputConfig,
|
||||
pub performance: PerformanceConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scanner: ScannerConfig::default(),
|
||||
database: DatabaseConfig::default(),
|
||||
output: OutputConfig::default(),
|
||||
performance: PerformanceConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(
|
||||
config_dir: &Path,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut config = Config::default();
|
||||
|
||||
let default_config_path = config_dir.join("nano.conf");
|
||||
if !default_config_path.exists() {
|
||||
create_example_config(config_dir)?;
|
||||
}
|
||||
|
||||
let user_config_path = config_dir.join("nano.local");
|
||||
if user_config_path.exists() {
|
||||
let user_config_content = fs::read_to_string(&user_config_path)?;
|
||||
let user_config: Config = toml::from_str(&user_config_content)?;
|
||||
|
||||
config = merge_configs(config, user_config);
|
||||
|
||||
println!("Loaded user config from: {}", user_config_path.display());
|
||||
} else {
|
||||
println!("Using default configuration. Create {} to customize.", user_config_path.display());
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_example_config(
|
||||
config_dir: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let example_path = config_dir.join("nano.conf");
|
||||
|
||||
let default_config = Config::default();
|
||||
let toml_content = toml::to_string_pretty(&default_config)?;
|
||||
|
||||
// Add comments to make it user-friendly
|
||||
let commented_content = format!(
|
||||
"# Nano Vulnerability Scanner Configuration\n\
|
||||
# YOU SHOULD NOT MODIFY THIS FILE.\n\
|
||||
# Create/modify 'nano.local' to set configs\n\
|
||||
# Only include the sections you want to override\n\n{}",
|
||||
toml_content
|
||||
);
|
||||
|
||||
fs::write(&example_path, commented_content)?;
|
||||
println!("Example config created at: {}", example_path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge user config into default config, preserving defaults where the user didn't
|
||||
/// supply new exclusions and overriding everything else.
|
||||
fn merge_configs(mut default: Config, user: Config) -> Config {
|
||||
// --- ScannerConfig ---
|
||||
default.scanner.max_file_size_mb = user.scanner.max_file_size_mb;
|
||||
default.scanner.read_global_ignore = user.scanner.read_global_ignore;
|
||||
default.scanner.read_vcsignore = user.scanner.read_vcsignore;
|
||||
default.scanner.require_git_to_read_vcsignore = user.scanner.require_git_to_read_vcsignore;
|
||||
default.scanner.one_file_system = user.scanner.one_file_system;
|
||||
default.scanner.follow_symlinks = user.scanner.follow_symlinks;
|
||||
default.scanner.scan_hidden_files = user.scanner.scan_hidden_files;
|
||||
|
||||
// Merge exclusion lists (default ⊔ user), then sort & dedupe
|
||||
default.scanner.excluded_extensions.extend(user.scanner.excluded_extensions);
|
||||
default.scanner.excluded_directories.extend(user.scanner.excluded_directories);
|
||||
default.scanner.excluded_extensions.sort_unstable();
|
||||
default.scanner.excluded_extensions.dedup();
|
||||
default.scanner.excluded_directories.sort_unstable();
|
||||
default.scanner.excluded_directories.dedup();
|
||||
|
||||
// --- DatabaseConfig ---
|
||||
default.database.auto_cleanup_days = user.database.auto_cleanup_days;
|
||||
default.database.max_db_size_mb = user.database.max_db_size_mb;
|
||||
default.database.vacuum_on_startup = user.database.vacuum_on_startup;
|
||||
|
||||
// --- OutputConfig ---
|
||||
default.output.default_format = user.output.default_format;
|
||||
default.output.show_progress = user.output.show_progress;
|
||||
default.output.color_output = user.output.color_output;
|
||||
default.output.max_results = user.output.max_results;
|
||||
|
||||
// --- PerformanceConfig ---
|
||||
default.performance.max_depth = user.performance.max_depth;
|
||||
default.performance.min_depth = user.performance.min_depth;
|
||||
default.performance.prune = user.performance.prune;
|
||||
default.performance.worker_threads = user.performance.worker_threads;
|
||||
default.performance.index_chunk_size = user.performance.index_chunk_size;
|
||||
default.performance.memory_limit_mb = user.performance.memory_limit_mb;
|
||||
|
||||
default
|
||||
}
|
||||
6
src/utils/mod.rs
Normal file
6
src/utils/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod project;
|
||||
pub mod config;
|
||||
|
||||
// Re-export commonly used functions for convenience
|
||||
pub use project::{get_project_info, sanitize_project_name};
|
||||
pub use config::Config;
|
||||
31
src/utils/project.rs
Normal file
31
src/utils/project.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn get_project_info(
|
||||
project_path: &Path,
|
||||
config_dir: &Path,
|
||||
) -> Result<(String, PathBuf), Box<dyn std::error::Error>> {
|
||||
let project_name = project_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or("Unable to determine project name")?;
|
||||
|
||||
let db_name = sanitize_project_name(project_name);
|
||||
let db_path = config_dir.join(format!("{}.sqlite", db_name));
|
||||
|
||||
Ok((project_name.to_string(), db_path))
|
||||
}
|
||||
|
||||
pub fn sanitize_project_name(name: &str) -> String {
|
||||
name.to_lowercase()
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
' ' | '\t' | '\n' | '\r' => '_',
|
||||
c if c.is_alphanumeric() || c == '_' || c == '-' => c,
|
||||
_ => '_'
|
||||
})
|
||||
.collect::<String>()
|
||||
.split('_')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
}
|
||||
670
src/walk.rs
Normal file
670
src/walk.rs
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
// use std::borrow::Cow;
|
||||
// use std::ffi::OsStr;
|
||||
// use std::io::{self, Write};
|
||||
// use std::mem;
|
||||
// use std::path::PathBuf;
|
||||
// use std::sync::atomic::{AtomicBool, Ordering};
|
||||
// use std::sync::{Arc, Mutex, MutexGuard};
|
||||
// use std::thread;
|
||||
// use std::time::{Duration, Instant};
|
||||
//
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, SendError, Sender};
|
||||
// use etcetera::BaseStrategy;
|
||||
// use ignore::overrides::{Override, OverrideBuilder};
|
||||
// use ignore::{WalkBuilder, WalkParallel, WalkState};
|
||||
// use regex::bytes::Regex;
|
||||
//
|
||||
// use crate::config::Config;
|
||||
// use crate::dir_entry::DirEntry;
|
||||
// use crate::error::print_error;
|
||||
// use crate::exec;
|
||||
// use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||
// use crate::filesystem;
|
||||
// use crate::output;
|
||||
//
|
||||
// /// The receiver thread can either be buffering results or directly streaming to the console.
|
||||
// #[derive(PartialEq)]
|
||||
// enum ReceiverMode {
|
||||
// /// Receiver is still buffering in order to sort the results, if the search finishes fast
|
||||
// /// enough.
|
||||
// Buffering,
|
||||
//
|
||||
// /// Receiver is directly printing results to the output.
|
||||
// Streaming,
|
||||
// }
|
||||
//
|
||||
// /// The Worker threads can result in a valid entry having PathBuf or an error.
|
||||
// #[allow(clippy::large_enum_variant)]
|
||||
// #[derive(Debug)]
|
||||
// pub enum WorkerResult {
|
||||
// // Errors should be rare, so it's probably better to allow large_enum_variant than
|
||||
// // to box the Entry variant
|
||||
// Entry(ignore::DirEntry), // TODO: CHECK IF ERRORS
|
||||
// Error(ignore::Error),
|
||||
// }
|
||||
//
|
||||
// /// A batch of WorkerResults to send over a channel.
|
||||
// #[derive(Clone)]
|
||||
// struct Batch {
|
||||
// items: Arc<Mutex<Option<Vec<WorkerResult>>>>,
|
||||
// }
|
||||
//
|
||||
// impl Batch {
|
||||
// fn new() -> Self {
|
||||
// Self {
|
||||
// items: Arc::new(Mutex::new(Some(vec![]))),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn lock(&self) -> MutexGuard<'_, Option<Vec<WorkerResult>>> {
|
||||
// self.items.lock().unwrap()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl IntoIterator for Batch {
|
||||
// type Item = WorkerResult;
|
||||
// type IntoIter = std::vec::IntoIter<WorkerResult>;
|
||||
//
|
||||
// fn into_iter(self) -> Self::IntoIter {
|
||||
// self.lock().take().unwrap().into_iter()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Wrapper that sends batches of items at once over a channel.
|
||||
// struct BatchSender {
|
||||
// batch: Batch,
|
||||
// tx: Sender<Batch>,
|
||||
// limit: usize,
|
||||
// }
|
||||
//
|
||||
// impl BatchSender {
|
||||
// fn new(tx: Sender<Batch>, limit: usize) -> Self {
|
||||
// Self {
|
||||
// batch: Batch::new(),
|
||||
// tx,
|
||||
// limit,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Check if we need to flush a batch.
|
||||
// fn needs_flush(&self, batch: Option<&Vec<WorkerResult>>) -> bool {
|
||||
// match batch {
|
||||
// // Limit the batch size to provide some backpressure
|
||||
// Some(vec) => vec.len() >= self.limit,
|
||||
// // Batch was already taken by the receiver, so make a new one
|
||||
// None => true,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Add an item to a batch.
|
||||
// fn send(&mut self, item: WorkerResult) -> Result<(), SendError<()>> {
|
||||
// let mut batch = self.batch.lock();
|
||||
//
|
||||
// if self.needs_flush(batch.as_ref()) {
|
||||
// drop(batch);
|
||||
// self.batch = Batch::new();
|
||||
// batch = self.batch.lock();
|
||||
// }
|
||||
//
|
||||
// let items = batch.as_mut().unwrap();
|
||||
// items.push(item);
|
||||
//
|
||||
// if items.len() == 1 {
|
||||
// // New batch, send it over the channel
|
||||
// self.tx
|
||||
// .send(self.batch.clone())
|
||||
// .map_err(|_| SendError(()))?;
|
||||
// }
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Maximum size of the output buffer before flushing results to the console
|
||||
// const MAX_BUFFER_LENGTH: usize = 1000;
|
||||
// /// Default duration until output buffering switches to streaming.
|
||||
// const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100);
|
||||
//
|
||||
// /// Wrapper for the receiver thread's buffering behavior.
|
||||
// struct ReceiverBuffer<'a, W> {
|
||||
// /// The configuration.
|
||||
// config: &'a Config,
|
||||
// /// For shutting down the senders.
|
||||
// quit_flag: &'a AtomicBool,
|
||||
// /// The ^C notifier.
|
||||
// interrupt_flag: &'a AtomicBool,
|
||||
// /// Receiver for worker results.
|
||||
// rx: Receiver<Batch>,
|
||||
// /// Standard output.
|
||||
// stdout: W,
|
||||
// /// The current buffer mode.
|
||||
// mode: ReceiverMode,
|
||||
// /// The deadline to switch to streaming mode.
|
||||
// deadline: Instant,
|
||||
// /// The buffer of quickly received paths.
|
||||
// buffer: Vec<ignore::DirEntry>,
|
||||
// /// Result count.
|
||||
// num_results: usize,
|
||||
// }
|
||||
//
|
||||
// impl<'a, W: Write> ReceiverBuffer<'a, W> {
|
||||
// /// Create a new receiver buffer.
|
||||
// fn new(state: &'a WorkerState, rx: Receiver<Batch>, stdout: W) -> Self {
|
||||
// let config = &state.config;
|
||||
// let quit_flag = state.quit_flag.as_ref();
|
||||
// let interrupt_flag = state.interrupt_flag.as_ref();
|
||||
// let max_buffer_time = config.max_buffer_time.unwrap_or(DEFAULT_MAX_BUFFER_TIME);
|
||||
// let deadline = Instant::now() + max_buffer_time;
|
||||
//
|
||||
// Self {
|
||||
// config,
|
||||
// quit_flag,
|
||||
// interrupt_flag,
|
||||
// rx,
|
||||
// stdout,
|
||||
// mode: ReceiverMode::Buffering,
|
||||
// deadline,
|
||||
// buffer: Vec::with_capacity(MAX_BUFFER_LENGTH),
|
||||
// num_results: 0,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Process results until finished.
|
||||
// fn process(&mut self) -> ExitCode {
|
||||
// loop {
|
||||
// if let Err(ec) = self.poll() {
|
||||
// self.quit_flag.store(true, Ordering::Relaxed);
|
||||
// return ec;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Receive the next worker result.
|
||||
// fn recv(&self) -> Result<Batch, RecvTimeoutError> {
|
||||
// match self.mode {
|
||||
// ReceiverMode::Buffering => {
|
||||
// // Wait at most until we should switch to streaming
|
||||
// self.rx.recv_deadline(self.deadline)
|
||||
// }
|
||||
// ReceiverMode::Streaming => {
|
||||
// // Wait however long it takes for a result
|
||||
// Ok(self.rx.recv()?)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Wait for a result or state change.
|
||||
// fn poll(&mut self) -> Result<(), ExitCode> {
|
||||
// match self.recv() {
|
||||
// Ok(batch) => {
|
||||
// for result in batch {
|
||||
// match result {
|
||||
// WorkerResult::Entry(dir_entry) => {
|
||||
// if self.config.quiet {
|
||||
// return Err(ExitCode::HasResults(true));
|
||||
// }
|
||||
//
|
||||
// match self.mode {
|
||||
// ReceiverMode::Buffering => {
|
||||
// self.buffer.push(dir_entry);
|
||||
// if self.buffer.len() > MAX_BUFFER_LENGTH {
|
||||
// self.stream()?;
|
||||
// }
|
||||
// }
|
||||
// ReceiverMode::Streaming => {
|
||||
// self.print(&dir_entry)?;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// self.num_results += 1;
|
||||
// if let Some(max_results) = self.config.max_results {
|
||||
// if self.num_results >= max_results {
|
||||
// return self.stop();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// WorkerResult::Error(err) => {
|
||||
// if self.config.show_filesystem_errors {
|
||||
// print_error(err.to_string());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // If we don't have another batch ready, flush before waiting
|
||||
// if self.mode == ReceiverMode::Streaming && self.rx.is_empty() {
|
||||
// self.flush()?;
|
||||
// }
|
||||
// }
|
||||
// Err(RecvTimeoutError::Timeout) => {
|
||||
// self.stream()?;
|
||||
// }
|
||||
// Err(RecvTimeoutError::Disconnected) => {
|
||||
// return self.stop();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// /// Output a path.
|
||||
// fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
|
||||
// if let Err(e) = output::print_entry(&mut self.stdout, entry, self.config) {
|
||||
// if e.kind() != ::std::io::ErrorKind::BrokenPipe {
|
||||
// print_error(format!("Could not write to output: {e}"));
|
||||
// return Err(ExitCode::GeneralError);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if self.interrupt_flag.load(Ordering::Relaxed) {
|
||||
// // Ignore any errors on flush, because we're about to exit anyway
|
||||
// let _ = self.flush();
|
||||
// return Err(ExitCode::KilledBySigint);
|
||||
// }
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// /// Switch ourselves into streaming mode.
|
||||
// fn stream(&mut self) -> Result<(), ExitCode> {
|
||||
// self.mode = ReceiverMode::Streaming;
|
||||
//
|
||||
// let buffer = mem::take(&mut self.buffer);
|
||||
// for path in buffer {
|
||||
// self.print(&path)?;
|
||||
// }
|
||||
//
|
||||
// self.flush()
|
||||
// }
|
||||
//
|
||||
// /// Stop looping.
|
||||
// fn stop(&mut self) -> Result<(), ExitCode> {
|
||||
// if self.mode == ReceiverMode::Buffering {
|
||||
// self.buffer.sort();
|
||||
// self.stream()?;
|
||||
// }
|
||||
//
|
||||
// if self.config.quiet {
|
||||
// Err(ExitCode::HasResults(self.num_results > 0))
|
||||
// } else {
|
||||
// Err(ExitCode::Success)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Flush stdout if necessary.
|
||||
// fn flush(&mut self) -> Result<(), ExitCode> {
|
||||
// if self.stdout.flush().is_err() {
|
||||
// // Probably a broken pipe. Exit gracefully.
|
||||
// return Err(ExitCode::GeneralError);
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// State shared by the sender and receiver threads.
|
||||
// struct WorkerState {
|
||||
// /// The search patterns.
|
||||
// patterns: Vec<Regex>,
|
||||
// /// The command line configuration.
|
||||
// config: Config,
|
||||
// /// Flag for cleanly shutting down the parallel walk
|
||||
// quit_flag: Arc<AtomicBool>,
|
||||
// /// Flag specifically for quitting due to ^C
|
||||
// interrupt_flag: Arc<AtomicBool>,
|
||||
// }
|
||||
//
|
||||
// impl WorkerState {
|
||||
// fn new(patterns: Vec<Regex>, config: Config) -> Self {
|
||||
// let quit_flag = Arc::new(AtomicBool::new(false));
|
||||
// let interrupt_flag = Arc::new(AtomicBool::new(false));
|
||||
//
|
||||
// Self {
|
||||
// patterns,
|
||||
// config,
|
||||
// quit_flag,
|
||||
// interrupt_flag,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn build_overrides(&self, paths: &[PathBuf]) -> Result<Override> {
|
||||
// let first_path = &paths[0];
|
||||
// let config = &self.config;
|
||||
//
|
||||
// let mut builder = OverrideBuilder::new(first_path);
|
||||
//
|
||||
// for pattern in &config.exclude_patterns {
|
||||
// builder
|
||||
// .add(pattern)
|
||||
// .map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
|
||||
// }
|
||||
//
|
||||
// builder
|
||||
// .build()
|
||||
// .map_err(|_| anyhow!("Mismatch in exclude patterns"))
|
||||
// }
|
||||
//
|
||||
// fn build_walker(&self, paths: &[PathBuf]) -> Result<WalkParallel> {
|
||||
// let first_path = &paths[0];
|
||||
// let config = &self.config;
|
||||
// let overrides = self.build_overrides(paths)?;
|
||||
//
|
||||
// let mut builder = WalkBuilder::new(first_path);
|
||||
// builder
|
||||
// .hidden(config.ignore_hidden)
|
||||
// .ignore(config.read_fdignore)
|
||||
// .parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
|
||||
// .git_ignore(config.read_vcsignore)
|
||||
// .git_global(config.read_vcsignore)
|
||||
// .git_exclude(config.read_vcsignore)
|
||||
// .require_git(config.require_git_to_read_vcsignore)
|
||||
// .overrides(overrides)
|
||||
// .follow_links(config.follow_links)
|
||||
// // No need to check for supported platforms, option is unavailable on unsupported ones
|
||||
// .same_file_system(config.one_file_system)
|
||||
// .max_depth(config.max_depth);
|
||||
//
|
||||
// if config.read_fdignore {
|
||||
// builder.add_custom_ignore_filename(".fdignore");
|
||||
// }
|
||||
//
|
||||
// if config.read_global_ignore {
|
||||
// if let Ok(basedirs) = etcetera::choose_base_strategy() {
|
||||
// let global_ignore_file = basedirs.config_dir().join("fd").join("ignore");
|
||||
// if global_ignore_file.is_file() {
|
||||
// let result = builder.add_ignore(global_ignore_file);
|
||||
// match result {
|
||||
// Some(ignore::Error::Partial(_)) => (),
|
||||
// Some(err) => {
|
||||
// print_error(format!("Malformed pattern in global ignore file. {err}."));
|
||||
// }
|
||||
// None => (),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for ignore_file in &config.ignore_files {
|
||||
// let result = builder.add_ignore(ignore_file);
|
||||
// match result {
|
||||
// Some(ignore::Error::Partial(_)) => (),
|
||||
// Some(err) => {
|
||||
// print_error(format!("Malformed pattern in custom ignore file. {err}."));
|
||||
// }
|
||||
// None => (),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for path in &paths[1..] {
|
||||
// builder.add(path);
|
||||
// }
|
||||
//
|
||||
// let walker = builder.threads(config.threads).build_parallel();
|
||||
// Ok(walker)
|
||||
// }
|
||||
//
|
||||
// /// Run the receiver work, either on this thread or a pool of background
|
||||
// /// threads (for --exec).
|
||||
// fn receive(&self, rx: Receiver<Batch>) -> ExitCode {
|
||||
// let config = &self.config;
|
||||
//
|
||||
// // This will be set to `Some` if the `--exec` argument was supplied.
|
||||
// if let Some(ref cmd) = config.command {
|
||||
// if cmd.in_batch_mode() {
|
||||
// exec::batch(rx.into_iter().flatten(), cmd, config)
|
||||
// } else {
|
||||
// let out_perm = Mutex::new(());
|
||||
//
|
||||
// thread::scope(|scope| {
|
||||
// // Each spawned job will store its thread handle in here.
|
||||
// let threads = config.threads;
|
||||
// let mut handles = Vec::with_capacity(threads);
|
||||
// for _ in 0..threads {
|
||||
// let rx = rx.clone();
|
||||
//
|
||||
// // Spawn a job thread that will listen for and execute inputs.
|
||||
// let handle = scope
|
||||
// .spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
|
||||
//
|
||||
// // Push the handle of the spawned thread into the vector for later joining.
|
||||
// handles.push(handle);
|
||||
// }
|
||||
// let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
|
||||
// merge_exitcodes(exit_codes)
|
||||
// })
|
||||
// }
|
||||
// } else {
|
||||
// let stdout = io::stdout().lock();
|
||||
// let stdout = io::BufWriter::new(stdout);
|
||||
//
|
||||
// ReceiverBuffer::new(self, rx, stdout).process()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Spawn the sender threads.
|
||||
// fn spawn_senders(&self, walker: WalkParallel, tx: Sender<Batch>) {
|
||||
// walker.run(|| {
|
||||
// let patterns = &self.patterns;
|
||||
// let config = &self.config;
|
||||
// let quit_flag = self.quit_flag.as_ref();
|
||||
//
|
||||
// let mut limit = 0x100;
|
||||
// if let Some(cmd) = &config.command {
|
||||
// if !cmd.in_batch_mode() && config.threads > 1 {
|
||||
// // Evenly distribute work between multiple receivers
|
||||
// limit = 1;
|
||||
// }
|
||||
// }
|
||||
// let mut tx = BatchSender::new(tx.clone(), limit);
|
||||
//
|
||||
// Box::new(move |entry| {
|
||||
// if quit_flag.load(Ordering::Relaxed) {
|
||||
// return WalkState::Quit;
|
||||
// }
|
||||
//
|
||||
// let entry = match entry {
|
||||
// Ok(ref e) if e.depth() == 0 => {
|
||||
// // Skip the root directory entry.
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// Ok(e) => DirEntry::normal(e),
|
||||
// Err(ignore::Error::WithPath {
|
||||
// path,
|
||||
// err: inner_err,
|
||||
// }) => match inner_err.as_ref() {
|
||||
// ignore::Error::Io(io_error)
|
||||
// if io_error.kind() == io::ErrorKind::NotFound
|
||||
// && path
|
||||
// .symlink_metadata()
|
||||
// .ok()
|
||||
// .is_some_and(|m| m.file_type().is_symlink()) =>
|
||||
// {
|
||||
// DirEntry::broken_symlink(path)
|
||||
// }
|
||||
// _ => {
|
||||
// return match tx.send(WorkerResult::Error(ignore::Error::WithPath {
|
||||
// path,
|
||||
// err: inner_err,
|
||||
// })) {
|
||||
// Ok(_) => WalkState::Continue,
|
||||
// Err(_) => WalkState::Quit,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// Err(err) => {
|
||||
// return match tx.send(WorkerResult::Error(err)) {
|
||||
// Ok(_) => WalkState::Continue,
|
||||
// Err(_) => WalkState::Quit,
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// if let Some(min_depth) = config.min_depth {
|
||||
// if entry.depth().map_or(true, |d| d < min_depth) {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Check the name first, since it doesn't require metadata
|
||||
// let entry_path = entry.path();
|
||||
//
|
||||
// let search_str: Cow<OsStr> = if config.search_full_path {
|
||||
// let path_abs_buf = filesystem::path_absolute_form(entry_path)
|
||||
// .expect("Retrieving absolute path succeeds");
|
||||
// Cow::Owned(path_abs_buf.as_os_str().to_os_string())
|
||||
// } else {
|
||||
// match entry_path.file_name() {
|
||||
// Some(filename) => Cow::Borrowed(filename),
|
||||
// None => unreachable!(
|
||||
// "Encountered file system entry without a file name. This should only \
|
||||
// happen for paths like 'foo/bar/..' or '/' which are not supposed to \
|
||||
// appear in a file system traversal."
|
||||
// ),
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// if !patterns
|
||||
// .iter()
|
||||
// .all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())))
|
||||
// {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
//
|
||||
// // Filter out unwanted extensions.
|
||||
// if let Some(ref exts_regex) = config.extensions {
|
||||
// if let Some(path_str) = entry_path.file_name() {
|
||||
// if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// } else {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Filter out unwanted file types.
|
||||
// if let Some(ref file_types) = config.file_types {
|
||||
// if file_types.should_ignore(&entry) {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[cfg(unix)]
|
||||
// {
|
||||
// if let Some(ref owner_constraint) = config.owner_constraint {
|
||||
// if let Some(metadata) = entry.metadata() {
|
||||
// if !owner_constraint.matches(metadata) {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// } else {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Filter out unwanted sizes if it is a file and we have been given size constraints.
|
||||
// if !config.size_constraints.is_empty() {
|
||||
// if entry_path.is_file() {
|
||||
// if let Some(metadata) = entry.metadata() {
|
||||
// let file_size = metadata.len();
|
||||
// if config
|
||||
// .size_constraints
|
||||
// .iter()
|
||||
// .any(|sc| !sc.is_within(file_size))
|
||||
// {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// } else {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// } else {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Filter out unwanted modification times
|
||||
// if !config.time_constraints.is_empty() {
|
||||
// let mut matched = false;
|
||||
// if let Some(metadata) = entry.metadata() {
|
||||
// if let Ok(modified) = metadata.modified() {
|
||||
// matched = config
|
||||
// .time_constraints
|
||||
// .iter()
|
||||
// .all(|tf| tf.applies_to(&modified));
|
||||
// }
|
||||
// }
|
||||
// if !matched {
|
||||
// return WalkState::Continue;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if config.is_printing() {
|
||||
// if let Some(ls_colors) = &config.ls_colors {
|
||||
// // Compute colors in parallel
|
||||
// entry.style(ls_colors);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let send_result = tx.send(WorkerResult::Entry(entry));
|
||||
//
|
||||
// if send_result.is_err() {
|
||||
// return WalkState::Quit;
|
||||
// }
|
||||
//
|
||||
// // Apply pruning.
|
||||
// if config.prune {
|
||||
// return WalkState::Skip;
|
||||
// }
|
||||
//
|
||||
// WalkState::Continue
|
||||
// })
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// /// Perform the recursive scan.
|
||||
// fn scan(&self, paths: &[PathBuf]) -> Result<ExitCode> {
|
||||
// let config = &self.config;
|
||||
// let walker = self.build_walker(paths)?;
|
||||
//
|
||||
// if config.ls_colors.is_some() && config.is_printing() {
|
||||
// let quit_flag = Arc::clone(&self.quit_flag);
|
||||
// let interrupt_flag = Arc::clone(&self.interrupt_flag);
|
||||
//
|
||||
// ctrlc::set_handler(move || {
|
||||
// quit_flag.store(true, Ordering::Relaxed);
|
||||
//
|
||||
// if interrupt_flag.fetch_or(true, Ordering::Relaxed) {
|
||||
// // Ctrl-C has been pressed twice, exit NOW
|
||||
// ExitCode::KilledBySigint.exit();
|
||||
// }
|
||||
// })
|
||||
// .unwrap();
|
||||
// }
|
||||
//
|
||||
// let (tx, rx) = bounded(2 * config.threads);
|
||||
//
|
||||
// let exit_code = thread::scope(|scope| {
|
||||
// // Spawn the receiver thread(s)
|
||||
// let receiver = scope.spawn(|| self.receive(rx));
|
||||
//
|
||||
// // Spawn the sender threads.
|
||||
// self.spawn_senders(walker, tx);
|
||||
//
|
||||
// receiver.join().unwrap()
|
||||
// });
|
||||
//
|
||||
// if self.interrupt_flag.load(Ordering::Relaxed) {
|
||||
// Ok(ExitCode::KilledBySigint)
|
||||
// } else {
|
||||
// Ok(exit_code)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Recursively scan the given search path for files / pathnames matching the patterns.
|
||||
// ///
|
||||
// /// If the `--exec` argument was supplied, this will create a thread pool for executing
|
||||
// /// jobs in parallel from a given command line and the discovered paths. Otherwise, each
|
||||
// /// path will simply be written to standard output.
|
||||
// pub fn scan(paths: &[PathBuf], patterns: Vec<Regex>, config: Config) -> Result<ExitCode> {
|
||||
// WorkerState::new(patterns, config).scan(paths)
|
||||
// }
|
||||
Loading…
Add table
Add a link
Reference in a new issue