ksud: config set support read from stdin, and less restriction

This commit is contained in:
Ylarod
2025-11-22 17:12:40 +08:00
committed by ShirkNeko
parent 823a3f9767
commit 48016fac7b
2 changed files with 39 additions and 34 deletions

View File

@@ -1,4 +1,4 @@
use anyhow::{Ok, Result}; use anyhow::{Context, Ok, Result};
use clap::Parser; use clap::Parser;
use std::path::PathBuf; use std::path::PathBuf;
@@ -322,8 +322,11 @@ enum ModuleConfigCmd {
Set { Set {
/// config key /// config key
key: String, key: String,
/// config value /// config value (omit to read from stdin)
value: String, value: Option<String>,
/// read value from stdin (default if value not provided)
#[arg(long)]
stdin: bool,
/// use temporary config (cleared on reboot) /// use temporary config (cleared on reboot)
#[arg(short, long)] #[arg(short, long)]
temp: bool, temp: bool,
@@ -576,17 +579,32 @@ pub fn run() -> Result<()> {
None => anyhow::bail!("Key '{key}' not found"), None => anyhow::bail!("Key '{key}' not found"),
} }
} }
ModuleConfigCmd::Set { key, value, temp } => { ModuleConfigCmd::Set { key, value, stdin, temp } => {
// Validate input at CLI layer for better user experience // Validate key at CLI layer for better user experience
module_config::validate_config_key(&key)?; module_config::validate_config_key(&key)?;
module_config::validate_config_value(&value)?;
// Read value from stdin or argument
let value_str = match value {
Some(v) if !stdin => v,
_ => {
// Read from stdin
use std::io::Read;
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer)
.context("Failed to read from stdin")?;
buffer
}
};
// Validate value
module_config::validate_config_value(&value_str)?;
let config_type = if temp { let config_type = if temp {
module_config::ConfigType::Temp module_config::ConfigType::Temp
} else { } else {
module_config::ConfigType::Persist module_config::ConfigType::Persist
}; };
module_config::set_config_value(&module_id, &key, &value, config_type) module_config::set_config_value(&module_id, &key, &value_str, config_type)
} }
ModuleConfigCmd::List => { ModuleConfigCmd::List => {
let config = module_config::merge_configs(&module_id)?; let config = module_config::merge_configs(&module_id)?;

View File

@@ -40,7 +40,10 @@ pub fn parse_bool_config(value: &str) -> bool {
} }
/// Validate config key /// Validate config key
/// Rejects keys with control characters, newlines, or excessive length /// Uses the same validation rules as module_id: ^[a-zA-Z][a-zA-Z0-9._-]+$
/// - Must start with a letter (a-zA-Z)
/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters
/// - Minimum length: 2 characters
pub fn validate_config_key(key: &str) -> Result<()> { pub fn validate_config_key(key: &str) -> Result<()> {
if key.is_empty() { if key.is_empty() {
bail!("Config key cannot be empty"); bail!("Config key cannot be empty");
@@ -54,27 +57,21 @@ pub fn validate_config_key(key: &str) -> Result<()> {
); );
} }
// Check for control characters and newlines // Use same pattern as module_id for consistency
for ch in key.chars() { let re = regex_lite::Regex::new(r"^[a-zA-Z][a-zA-Z0-9._-]+$")?;
if ch.is_control() { if !re.is_match(key) {
bail!( bail!(
"Config key contains control character: {:?} (U+{:04X})", "Invalid config key: '{}'. Must match /^[a-zA-Z][a-zA-Z0-9._-]+$/",
ch, key
ch as u32 );
);
}
}
// Reject keys with path separators to prevent path traversal
if key.contains('/') || key.contains('\\') {
bail!("Config key cannot contain path separators");
} }
Ok(()) Ok(())
} }
/// Validate config value /// Validate config value
/// Rejects values with control characters (except tab), newlines, or excessive length /// Only enforces maximum length - no character restrictions
/// Values are stored in binary format with length prefix, so any UTF-8 data is safe
pub fn validate_config_value(value: &str) -> Result<()> { pub fn validate_config_value(value: &str) -> Result<()> {
if value.len() > MAX_CONFIG_VALUE_LEN { if value.len() > MAX_CONFIG_VALUE_LEN {
bail!( bail!(
@@ -84,17 +81,7 @@ pub fn validate_config_value(value: &str) -> Result<()> {
); );
} }
// Check for control characters (allow tab but reject newlines and others) // No character restrictions - binary storage format handles all UTF-8 safely
for ch in value.chars() {
if ch.is_control() && ch != '\t' {
bail!(
"Config value contains invalid control character: {:?} (U+{:04X})",
ch,
ch as u32
);
}
}
Ok(()) Ok(())
} }