add module config, migrate managedFeatures (#2965)
Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com>
This commit is contained in:
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user