ksud: config set support read from stdin, and less restriction
This commit is contained in:
@@ -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)?;
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user