Files
keep/src/modes/common.rs
Andrew Phillips b166477202 fix: harden security, eliminate panics, remove dead code, add Dockerfile
Security:
- Use constant-time password comparison (subtle crate) to prevent timing attacks
- Replace permissive CORS with configurable origin-restricted CORS
- Add TLS warning when password auth is used without HTTPS

Bug fixes:
- Convert MetaPlugin panics to anyhow::Result (get_meta_plugin, outputs_mut, options_mut)
- Replace item.id.unwrap() with proper error handling across 15 call sites
- Fix panic on unknown column type in list mode
- Fix conflicting PIPESIZE constant (was 8192 vs 65536, now unified to 8192)
- Add 256MB filter chain buffer limit to prevent OOM
- Gracefully skip unregistered plugins instead of panicking

Dead code removal:
- Delete unused filter parser files (filter_parser.rs, filter.pest, parser/ module)
- ~260 lines of dead PEG parser code removed

Code consolidation:
- Add is_content_binary_from_metadata() helper (was duplicated in 4 places)
- Simplify save_item_raw() to delegate to save_item_raw_streaming() (~90 lines removed)

Incomplete features:
- Populate filter_plugins in status output from global registry
- Add FallbackMagicFileMetaPlugin (was referenced but never implemented)
- Document init_plugins() as intentional no-op

Infrastructure:
- Add Dockerfile (static musl binary on scratch, 4.8MB)
- Add .dockerignore
- Add cors_origin to ServerConfig and config.rs
2026-03-13 07:57:36 -03:00

451 lines
13 KiB
Rust

use crate::compression_engine::CompressionType;
/// Common utilities shared across different modes in the Keep application.
///
/// This module provides helper functions for formatting, configuration parsing,
/// table creation, and environment variable handling used by various CLI modes.
///
/// # Usage
///
/// These utilities are typically used internally by mode implementations:
///
/// ```
/// # use keep::modes::common::{format_size, OutputFormat};
/// let formatted = format_size(1024, true); // "1.0K"
/// // let format = OutputFormat::from_str("json")?;
/// ```
use crate::config;
use crate::meta_plugin::MetaPluginType;
use clap::Command;
use clap::error::ErrorKind;
use comfy_table::{ContentArrangement, Table};
use log::debug;
use regex::Regex;
use std::collections::HashMap;
use std::env;
use std::io::IsTerminal;
use std::str::FromStr;
use strum::IntoEnumIterator;
#[derive(Debug, Clone, strum::EnumString, strum::Display, PartialEq)]
#[strum(ascii_case_insensitive)]
/// Enum representing supported output formats for structured data.
///
/// Used to determine how to display lists, info, and status information in CLI modes.
/// Defaults to Table for human-readable output; JSON/YAML for machine parsing.
///
/// # Variants
///
/// * `Table` - Formatted table output (default).
/// * `Json` - JSON structured output.
/// * `Yaml` - YAML structured output.
///
/// # Examples
///
/// ```
/// # use keep::modes::common::OutputFormat;
/// # use std::str::FromStr;
/// assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json);
/// ```
pub enum OutputFormat {
Table,
Json,
Yaml,
}
/// Extracts metadata from KEEP_META_* environment variables.
///
/// Scans environment for variables prefixed with KEEP_META_ and extracts
/// key-value pairs for initial item metadata. Ignores KEEP_META_PLUGINS.
///
/// # Returns
///
/// `HashMap<String, String>` - Metadata from environment variables, with keys in uppercase without prefix.
///
/// # Errors
///
/// None; silently ignores non-matching vars and PLUGINS.
///
/// # Examples
///
/// ```ignore
/// use std::env;
/// env::set_var("KEEP_META_COMMAND", "ls -la");
/// let meta = keep::modes::common::get_meta_from_env();
/// assert_eq!(meta.get("COMMAND"), Some(&"ls -la".to_string()));
/// ```
pub fn get_meta_from_env() -> HashMap<String, String> {
debug!("COMMON: Getting meta from KEEP_META_*");
let re = Regex::new(r"^KEEP_META_(.+)$").unwrap();
let mut meta_env: HashMap<String, String> = HashMap::new();
for (key, value) in env::vars() {
if let Some(meta_name_caps) = re.captures(key.as_str()) {
let name = String::from(meta_name_caps.get(1).unwrap().as_str());
// Ignore KEEP_META_PLUGINS
if name != "PLUGINS" {
debug!("COMMON: Found meta: {}={}", name.clone(), value.clone());
meta_env.insert(name, value.clone());
}
}
}
meta_env
}
/// Formats a file size in bytes to human-readable or raw format.
///
/// Uses the humansize crate for human-readable output with decimal units (KB, MB, etc.).
///
/// # Arguments
///
/// * `size` - Size in bytes as u64.
/// * `human_readable` - If true, use units like KB, MB; otherwise, raw bytes as string.
///
/// # Returns
///
/// `String` - Formatted size string, e.g., "1.0K" or "1024".
///
/// # Examples
///
/// ```
/// # use keep::modes::common::format_size;
/// let raw = format_size(1024, false); // "1024"
/// let human = format_size(1024, true); // "1.0K"
/// ```
pub fn format_size(size: u64, human_readable: bool) -> String {
match human_readable {
true => humansize::format_size(size, humansize::DECIMAL),
false => size.to_string(),
}
}
#[derive(Debug, Eq, PartialEq, Clone, strum::EnumIter, strum::Display)]
#[strum(ascii_case_insensitive)]
/// Enum representing column types for table display.
///
/// Defines standard and meta columns for list/info modes. Supports "meta:<name>" for specific metadata columns.
///
/// # Variants
///
/// * `Id` - Item ID column.
/// * `Time` - Timestamp column.
/// * `Size` - Content size column.
/// * `Compression` - Compression type column.
/// * `FileSize` - On-disk file size column.
/// * `FilePath` - File path column.
/// * `Tags` - Tags column.
/// * `Meta` - Metadata column (with sub-type via string parsing).
///
/// # Examples
///
/// ```
/// # use keep::modes::common::ColumnType;
/// # use std::str::FromStr;
/// assert_eq!(ColumnType::from_str("id").unwrap(), ColumnType::Id);
/// assert_eq!(ColumnType::from_str("meta:hostname").unwrap(), ColumnType::Meta);
/// ```
pub enum ColumnType {
Id,
Time,
Size,
Compression,
FileSize,
FilePath,
Tags,
Meta,
}
impl std::str::FromStr for ColumnType {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let lower_s = s.to_lowercase();
if s.starts_with("meta:") {
Ok(ColumnType::Meta)
} else {
for variant in ColumnType::iter() {
if variant.to_string().to_lowercase() == lower_s {
return Ok(variant);
}
}
Err(anyhow::anyhow!("Invalid column type: {}", s))
}
}
}
/// Extracts configured meta plugin types from settings and command.
///
/// Handles comma-separated plugin names and validates against registered types.
///
/// # Arguments
///
/// * `cmd` - Mutable Clap command for error reporting.
/// * `settings` - Application settings with plugin config.
///
/// # Returns
///
/// `Vec<MetaPluginType>` - List of enabled plugin types.
///
/// # Panics
///
/// Exits via Clap error if unknown plugin type specified.
pub fn settings_meta_plugin_types(
cmd: &mut Command,
settings: &config::Settings,
) -> Vec<MetaPluginType> {
let mut meta_plugin_types = Vec::new();
// Handle comma-separated values in each meta_plugins argument
for meta_plugin_names_str in &settings.meta_plugins_names() {
let meta_plugin_names: Vec<&str> = meta_plugin_names_str.split(',').collect();
for name in meta_plugin_names {
let trimmed_name = name.trim();
if trimmed_name.is_empty() {
continue;
}
// Try to find the MetaPluginType by meta name
let mut found = false;
for meta_plugin_type in MetaPluginType::iter() {
if let Ok(meta_plugin) =
crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone(), None, None)
{
if meta_plugin.meta_type().to_string() == trimmed_name {
meta_plugin_types.push(meta_plugin_type);
found = true;
break;
}
}
}
if !found {
cmd.error(
ErrorKind::InvalidValue,
format!("Unknown meta plugin type: {trimmed_name}"),
)
.exit();
}
}
}
meta_plugin_types
}
/// Determines compression type from settings and command arguments.
///
/// Validates the compression name and returns the corresponding enum variant.
///
/// # Arguments
///
/// * `cmd` - Mutable Clap command for error reporting.
/// * `settings` - Application settings.
///
/// # Returns
///
/// `CompressionType` - The resolved compression type.
///
/// # Panics
///
/// Exits via Clap error if invalid compression specified.
pub fn settings_compression_type(
cmd: &mut Command,
settings: &config::Settings,
) -> CompressionType {
let compression_name = settings
.compression()
.unwrap_or(CompressionType::LZ4.to_string());
let compression_type_opt = CompressionType::from_str(&compression_name);
if compression_type_opt.is_err() {
cmd.error(
ErrorKind::InvalidValue,
format!("Invalid compression algorithm '{compression_name}'. Supported algorithms: lz4, gzip, xz, zstd"),
)
.exit();
}
compression_type_opt.unwrap()
}
/// Parses output format from settings.
///
/// Defaults to `Table` if not specified or invalid. Uses case-insensitive string parsing.
///
/// # Arguments
///
/// * `settings` - Application settings with optional output_format field.
///
/// # Returns
///
/// `OutputFormat` - Parsed enum variant or Table as default.
///
/// # Examples
///
/// ```
/// # use keep::modes::common::{settings_output_format, OutputFormat};
/// // Example usage requires a Settings instance
/// // let format = settings_output_format(&settings);
/// ```
pub fn settings_output_format(settings: &config::Settings) -> OutputFormat {
settings
.output_format
.as_ref()
.and_then(|s| OutputFormat::from_str(s).ok())
.unwrap_or(OutputFormat::Table)
}
/// Trims trailing whitespace from each line in a multi-line string.
///
/// Useful for cleaning up table output before printing. Preserves newlines but removes spaces/tabs at line ends.
///
/// # Arguments
///
/// * `s` - Input string with potential trailing whitespace, e.g., "line1 \nline2 ".
///
/// # Returns
///
/// `String` - Cleaned string with trimmed lines, e.g., "line1\nline2".
///
/// # Examples
///
/// ```
/// # use keep::modes::common::trim_lines_end;
/// let cleaned = trim_lines_end("line1 \nline2 ");
/// assert_eq!(cleaned, "line1\nline2");
/// ```
pub fn trim_lines_end(s: &str) -> String {
s.lines()
.map(|line| line.trim_end())
.collect::<Vec<&str>>()
.join("\n")
}
/// Creates a new table with styling based on terminal detection.
///
/// Loads appropriate preset (UTF8 or ASCII) if styling is enabled.
///
/// # Arguments
///
/// * `use_styling` - If true, apply visual styling.
///
/// # Returns
///
/// `Table` - Configured table instance.
///
/// # Examples
///
/// ```
/// # use keep::modes::common::create_table;
/// let mut table = create_table(true);
/// table.add_row(vec!["Header1", "Header2"]);
/// ```
pub fn create_table(use_styling: bool) -> Table {
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
if use_styling {
if std::io::stdout().is_terminal() {
table
.load_preset(comfy_table::presets::UTF8_FULL)
.apply_modifier(comfy_table::modifiers::UTF8_SOLID_INNER_BORDERS);
} else {
table.load_preset(comfy_table::presets::ASCII_FULL);
}
} else {
table.load_preset(comfy_table::presets::NOTHING);
}
if !std::io::stdout().is_terminal() {
table.force_no_tty();
}
table
}
/// Creates a table configured from application table settings.
///
/// Applies style presets, modifiers, content arrangement, and truncation indicators.
///
/// # Arguments
///
/// * `table_config` - Table configuration from settings.
///
/// # Returns
///
/// `Table` - Fully configured table.
///
/// # Examples
///
/// ```
/// # use keep::modes::common::create_table_with_config;
/// # use keep::config::TableConfig;
/// let config = TableConfig::default();
/// let table = create_table_with_config(&config);
/// ```
pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Table {
let mut table = Table::new();
// Set content arrangement
match table_config.content_arrangement {
crate::config::ContentArrangement::Dynamic => {
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
}
crate::config::ContentArrangement::DynamicFullWidth => {
table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
}
crate::config::ContentArrangement::Disabled => {
table.set_content_arrangement(comfy_table::ContentArrangement::Disabled)
}
};
// Set style preset
match &table_config.style {
crate::config::TableStyle::Ascii => {
table.load_preset(comfy_table::presets::ASCII_FULL);
}
crate::config::TableStyle::Utf8 => {
table.load_preset(comfy_table::presets::UTF8_FULL);
}
crate::config::TableStyle::Utf8Full => {
table.load_preset(comfy_table::presets::UTF8_FULL);
}
crate::config::TableStyle::Nothing => {
table.load_preset(comfy_table::presets::NOTHING);
}
crate::config::TableStyle::Custom(preset) => {
// For custom presets, we'd need to parse the string
// This is a placeholder for custom preset handling
if preset == "ASCII_FULL" {
table.load_preset(comfy_table::presets::ASCII_FULL);
} else if preset == "UTF8_FULL" {
table.load_preset(comfy_table::presets::UTF8_FULL);
} else if preset == "NOTHING" {
table.load_preset(comfy_table::presets::NOTHING);
}
// Add more presets as needed
}
};
// Apply modifiers
for modifier in &table_config.modifiers {
match modifier.as_str() {
"UTF8_SOLID_INNER_BORDERS" => {
table.apply_modifier(comfy_table::modifiers::UTF8_SOLID_INNER_BORDERS);
}
"UTF8_ROUND_CORNERS" => {
table.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS);
}
_ => {} // Ignore unknown modifiers
}
}
// Set truncation indicator if specified
if !table_config.truncation_indicator.is_empty() {
table.set_truncation_indicator(&table_config.truncation_indicator);
}
if !std::io::stdout().is_terminal() {
table.force_no_tty();
}
table
}