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 std::path::PathBuf;
@@ -322,8 +322,11 @@ enum ModuleConfigCmd {
Set {
/// config key
key: String,
/// config value
value: String,
/// config value (omit to read from stdin)
value: Option<String>,
/// read value from stdin (default if value not provided)
#[arg(long)]
stdin: bool,
/// use temporary config (cleared on reboot)
#[arg(short, long)]
temp: bool,
@@ -576,17 +579,32 @@ pub fn run() -> Result<()> {
None => anyhow::bail!("Key '{key}' not found"),
}
}
ModuleConfigCmd::Set { key, value, temp } => {
// Validate input at CLI layer for better user experience
ModuleConfigCmd::Set { key, value, stdin, temp } => {
// Validate key at CLI layer for better user experience
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 {
module_config::ConfigType::Temp
} else {
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 => {
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
/// 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<()> {
if key.is_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
for ch in key.chars() {
if ch.is_control() {
// Use same pattern as module_id for consistency
let re = regex_lite::Regex::new(r"^[a-zA-Z][a-zA-Z0-9._-]+$")?;
if !re.is_match(key) {
bail!(
"Config key contains control character: {:?} (U+{:04X})",
ch,
ch as u32
"Invalid config key: '{}'. Must match /^[a-zA-Z][a-zA-Z0-9._-]+$/",
key
);
}
}
// Reject keys with path separators to prevent path traversal
if key.contains('/') || key.contains('\\') {
bail!("Config key cannot contain path separators");
}
Ok(())
}
/// 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<()> {
if value.len() > MAX_CONFIG_VALUE_LEN {
bail!(
@@ -84,17 +81,7 @@ pub fn validate_config_value(value: &str) -> Result<()> {
);
}
// Check for control characters (allow tab but reject newlines and others)
for ch in value.chars() {
if ch.is_control() && ch != '\t' {
bail!(
"Config value contains invalid control character: {:?} (U+{:04X})",
ch,
ch as u32
);
}
}
// No character restrictions - binary storage format handles all UTF-8 safely
Ok(())
}