add module config, migrate managedFeatures (#2965)

Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com>
This commit is contained in:
Ylarod
2025-11-20 21:50:34 +08:00
committed by ShirkNeko
parent 3d4e0e48b4
commit e3ef521de5
11 changed files with 881 additions and 46 deletions

View File

@@ -10,7 +10,7 @@ use anyhow::{Context, Result, anyhow, bail, ensure};
use const_format::concatcp;
use is_executable::is_executable;
use java_properties::PropertiesIter;
use log::{info, warn};
use log::{debug, info, warn};
use std::fs::{copy, rename};
use std::{
@@ -39,6 +39,63 @@ const INSTALL_MODULE_SCRIPT: &str = concatcp!(
"\n"
);
/// Validate module_id format and security
/// Module ID must match: ^[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_module_id(module_id: &str) -> Result<()> {
if module_id.is_empty() {
bail!("Module ID cannot be empty");
}
if module_id.len() < 2 {
bail!("Module ID too short: must be at least 2 characters");
}
if module_id.len() > 64 {
bail!(
"Module ID too long: {} characters (max: 64)",
module_id.len()
);
}
// Check first character: must be a letter
let first_char = module_id.chars().next().unwrap();
if !first_char.is_ascii_alphabetic() {
bail!(
"Module ID must start with a letter (a-zA-Z), got: '{}'",
first_char
);
}
// Check remaining characters: alphanumeric, dot, underscore, or hyphen
for (i, ch) in module_id.chars().enumerate() {
if i == 0 {
continue; // Already checked
}
if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '_' && ch != '-' {
bail!(
"Module ID contains invalid character '{}' at position {}. Only letters, digits, '.', '_', and '-' are allowed",
ch,
i
);
}
}
// Additional security checks
if module_id.contains("..") {
bail!("Module ID cannot contain '..' sequence");
}
if module_id == "." || module_id == ".." {
bail!("Module ID cannot be '.' or '..'");
}
Ok(())
}
/// Get common environment variables for script execution
pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> {
vec![
@@ -147,6 +204,41 @@ pub fn load_sepolicy_rule() -> Result<()> {
pub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
info!("exec {}", path.as_ref().display());
// Extract module_id from path if it matches /data/adb/modules/{id}/...
let module_id = path
.as_ref()
.strip_prefix(defs::MODULE_DIR)
.ok()
.and_then(|p| p.components().next())
.and_then(|c| c.as_os_str().to_str())
.map(|s| s.to_string());
// Validate and log module_id extraction
let validated_module_id = module_id
.as_ref()
.and_then(|id| match validate_module_id(id) {
Ok(_) => {
debug!("Module ID extracted from script path: '{}'", id);
Some(id.as_str())
}
Err(e) => {
warn!(
"Invalid module ID '{}' extracted from script path '{}': {}",
id,
path.as_ref().display(),
e
);
None
}
});
if module_id.is_none() {
debug!(
"Failed to extract module_id from script path '{}'. Script will run without KSU_MODULE environment variable.",
path.as_ref().display()
);
}
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
#[cfg(unix)]
{
@@ -165,6 +257,11 @@ pub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
.arg(path.as_ref())
.envs(get_common_script_envs());
// Set KSU_MODULE environment variable if module_id was validated successfully
if let Some(id) = validated_module_id {
command = command.env("KSU_MODULE", id);
}
let result = if wait {
command.status().map(|_| ())
} else {
@@ -276,6 +373,12 @@ pub fn prune_modules() -> Result<()> {
warn!("Failed to exec uninstaller: {e}");
}
// Clear module configs before removing module directory
if let Err(e) = crate::module_config::clear_module_configs(module_id) {
warn!("Failed to clear configs for {}: {}", module_id, e);
}
// Finally remove the module directory
if let Err(e) = remove_dir_all(module) {
warn!("Failed to remove {}: {}", module.display(), e);
}
@@ -363,6 +466,10 @@ fn _install_module(zip: &str) -> Result<()> {
};
let module_id = module_id.trim();
// Validate module_id format
validate_module_id(module_id)
.with_context(|| format!("Invalid module ID in module.prop: '{}'", module_id))?;
// Check if this module is a metamodule
let is_metamodule = metamodule::is_metamodule(&module_prop);
@@ -482,6 +589,8 @@ pub fn install_module(zip: &str) -> Result<()> {
}
pub fn undo_uninstall_module(id: &str) -> Result<()> {
validate_module_id(id)?;
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
@@ -497,6 +606,8 @@ pub fn undo_uninstall_module(id: &str) -> Result<()> {
}
pub fn uninstall_module(id: &str) -> Result<()> {
validate_module_id(id)?;
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
@@ -510,11 +621,15 @@ pub fn uninstall_module(id: &str) -> Result<()> {
}
pub fn run_action(id: &str) -> Result<()> {
validate_module_id(id)?;
let action_script_path = format!("/data/adb/modules/{id}/action.sh");
exec_script(&action_script_path, true)
}
pub fn enable_module(id: &str) -> Result<()> {
validate_module_id(id)?;
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
@@ -587,6 +702,15 @@ pub fn read_module_prop(module_path: &Path) -> Result<HashMap<String, String>> {
}
fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
// Load all module configs once to minimize I/O overhead
let all_configs = match crate::module_config::get_all_module_configs() {
Ok(configs) => configs,
Err(e) => {
warn!("Failed to load module configs: {}", e);
HashMap::new()
}
};
// first check enabled modules
let dir = std::fs::read_dir(path);
let Ok(dir) = dir else {
@@ -640,6 +764,32 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
module_prop_map.insert("action".to_owned(), action.to_string());
module_prop_map.insert("mount".to_owned(), need_mount.to_string());
// Apply module config overrides and extract managed features
if let Some(module_id) = module_prop_map.get("id")
&& let Some(config) = all_configs.get(module_id.as_str())
{
// Apply override.description
if let Some(desc) = config.get("override.description") {
module_prop_map.insert("description".to_owned(), desc.clone());
}
// Extract managed features from manage.* config entries
let managed_features: Vec<String> = config
.iter()
.filter_map(|(k, v)| {
if k.starts_with("manage.") && crate::module_config::parse_bool_config(v) {
k.strip_prefix("manage.").map(|f| f.to_string())
} else {
None
}
})
.collect();
if !managed_features.is_empty() {
module_prop_map.insert("managedFeatures".to_owned(), managed_features.join(","));
}
}
modules.push(module_prop_map);
}
@@ -653,45 +803,49 @@ pub fn list_modules() -> Result<()> {
}
/// Get all managed features from active modules
/// Modules can specify managedFeatures in their module.prop
/// Format: managedFeatures=feature1,feature2,feature3
/// Modules declare managed features via config system (manage.<feature>=true)
/// Returns: HashMap<ModuleId, Vec<ManagedFeature>>
pub fn get_managed_features() -> Result<HashMap<String, Vec<String>>> {
let mut managed_features_map: HashMap<String, Vec<String>> = HashMap::new();
foreach_active_module(|module_path| {
let prop_map = match read_module_prop(module_path) {
Ok(prop) => prop,
Err(e) => {
// Get module ID
let module_id = match module_path.file_name().and_then(|n| n.to_str()) {
Some(id) => id,
None => {
warn!(
"Failed to read module.prop for {}: {}",
module_path.display(),
e
"Failed to get module id from path: {}",
module_path.display()
);
return Ok(());
}
};
if let Some(features_str) = prop_map.get("managedFeatures") {
let module_id = prop_map
.get("id")
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string());
// Read module config
let config = match crate::module_config::merge_configs(module_id) {
Ok(c) => c,
Err(e) => {
warn!("Failed to merge configs for module '{}': {}", module_id, e);
return Ok(()); // Skip this module
}
};
info!("Module {} manages features: {}", module_id, features_str);
let mut feature_list = Vec::new();
for feature in features_str.split(',') {
let feature = feature.trim();
if !feature.is_empty() {
info!(" - Adding managed feature: {}", feature);
feature_list.push(feature.to_string());
// Extract manage.* config entries
let mut feature_list = Vec::new();
for (key, value) in config.iter() {
if key.starts_with("manage.") {
// Parse feature name
if let Some(feature_name) = key.strip_prefix("manage.")
&& crate::module_config::parse_bool_config(value)
{
info!("Module {} manages feature: {}", module_id, feature_name);
feature_list.push(feature_name.to_string());
}
}
}
if !feature_list.is_empty() {
managed_features_map.insert(module_id, feature_list);
}
if !feature_list.is_empty() {
managed_features_map.insert(module_id.to_string(), feature_list);
}
Ok(())