use log::{error, warn};
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};
use crate::errors::*;
const RESTORECON_PROPERTY: &str = "selinux.restorecon_recursive";
const MAX_IMPORT_DEPTH: u8 = 8;
const MAX_TOTAL_LOADS: u32 = 1_000;
fn check_permissions(_key: &str, _value: &str, _context: &str) {
}
pub(crate) const MAX_LINE_LEN: usize = 64 * 1024;
pub(crate) fn read_bounded_line(
reader: &mut impl BufRead,
buf: &mut Vec<u8>,
) -> std::io::Result<(usize, bool)> {
buf.clear();
let read = Read::take(&mut *reader, MAX_LINE_LEN as u64 + 1).read_until(b'\n', buf)?;
if buf.len() <= MAX_LINE_LEN || buf.ends_with(b"\n") {
return Ok((read, false));
}
buf.truncate(MAX_LINE_LEN);
let mut total = read;
loop {
let available = reader.fill_buf()?;
if available.is_empty() {
return Ok((total, true)); }
match available.iter().position(|&b| b == b'\n') {
Some(nl) => {
reader.consume(nl + 1);
return Ok((total + nl + 1, true));
}
None => {
let n = available.len();
reader.consume(n);
total += n;
}
}
}
}
pub fn load_properties_from_file(
filename: &Path,
filter: Option<&str>,
context: &str,
properties: &mut HashMap<String, String>,
) -> Result<()> {
let mut visited = HashSet::new();
let mut loads = 0u32;
load_properties_impl(
filename,
filter,
context,
properties,
0,
&mut visited,
&mut loads,
)
}
fn expand_import_path(raw: &str, properties: &HashMap<String, String>) -> Option<String> {
let mut out = String::with_capacity(raw.len());
let mut rest = raw;
while let Some(dollar) = rest.find('$') {
out.push_str(&rest[..dollar]);
let after = &rest[dollar + 1..];
if let Some(tail) = after.strip_prefix('$') {
out.push('$');
rest = tail;
continue;
}
let body = after.strip_prefix('{')?;
let end = body.find('}')?;
let reference = &body[..end];
let (name, default) = match reference.split_once(":-") {
Some((name, default)) => (name, Some(default)),
None => (reference, None),
};
match properties.get(name).map(String::as_str) {
Some(value) if !value.is_empty() => out.push_str(value),
_ => out.push_str(default?),
}
rest = &body[end + 1..];
}
out.push_str(rest);
Some(out)
}
#[allow(clippy::too_many_arguments)]
fn load_properties_impl(
filename: &Path,
filter: Option<&str>,
context: &str,
properties: &mut HashMap<String, String>,
depth: u8,
visited: &mut HashSet<PathBuf>,
loads: &mut u32,
) -> Result<()> {
if depth > MAX_IMPORT_DEPTH {
return Err(Error::Parse(format!(
"import nesting deeper than {MAX_IMPORT_DEPTH} levels at {filename:?}"
)));
}
*loads += 1;
if *loads > MAX_TOTAL_LOADS {
return Err(Error::LimitExceeded(format!(
"more than {MAX_TOTAL_LOADS} file loads in one pass (import amplification) at {filename:?}"
)));
}
let canonical = std::fs::canonicalize(filename).unwrap_or_else(|_| filename.to_path_buf());
if !visited.insert(canonical.clone()) {
warn!("{filename:?} is already being loaded (import cycle) — skipping");
return Ok(());
}
let result = load_properties_body(
filename, &canonical, filter, context, properties, depth, visited, loads,
);
visited.remove(&canonical);
result
}
#[allow(clippy::too_many_arguments)]
fn load_properties_body(
filename: &Path,
canonical: &Path,
filter: Option<&str>,
context: &str,
properties: &mut HashMap<String, String>,
depth: u8,
visited: &mut HashSet<PathBuf>,
loads: &mut u32,
) -> Result<()> {
let file =
File::open(canonical).context_with_location(format!("Failed to open {filename:?}"))?;
let mut reader = BufReader::new(file);
let filter = filter.filter(|s| !s.is_empty());
let mut raw_line = Vec::new();
let mut line_count = 0usize;
loop {
let (read, truncated) = read_bounded_line(&mut reader, &mut raw_line)
.with_context_location(|| {
format!("Failed to read line {} of {filename:?}", line_count + 1)
})?;
if read == 0 {
break;
}
line_count += 1;
if truncated {
warn!(
"Line {line_count} of {filename:?}: skipping line longer than {MAX_LINE_LEN} bytes"
);
continue;
}
let Ok(line) = std::str::from_utf8(&raw_line) else {
warn!("Line {line_count} of {filename:?}: skipping non-UTF-8 line");
continue;
};
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if filter.is_none() {
if let Some(import_path) = line.strip_prefix("import ") {
match expand_import_path(import_path.trim(), properties) {
Some(expanded) => {
if let Err(e) = load_properties_impl(
Path::new(&expanded),
None,
context,
properties,
depth + 1,
visited,
loads,
) {
if matches!(e, Error::LimitExceeded(_)) {
return Err(e);
}
warn!(
"Line {line_count} of {filename:?}: couldn't load import {expanded:?}: {e}"
);
}
}
None => warn!(
"Line {line_count} of {filename:?}: couldn't expand import path {import_path:?}"
),
}
continue;
}
}
let (key, value) = match line.find('=') {
Some(pos) => (line[..pos].trim_end(), line[pos + 1..].trim()),
None => continue,
};
if key.is_empty() {
warn!("Line {line_count} of {filename:?}: ignoring entry with empty key");
continue;
}
if let Some(filter) = filter {
if let Some(prefix) = filter.strip_suffix('*') {
if !key.starts_with(prefix) {
continue;
}
} else if key != filter {
continue;
}
}
if key.starts_with("ctl.") || key == "sys.powerctl" || key == RESTORECON_PROPERTY {
error!("Line {line_count} of {filename:?}: Ignoring disallowed property '{key}' with special meaning");
continue;
}
check_permissions(key, value, context);
if let Some(old_value) = properties.insert(key.to_string(), value.to_string()) {
warn!(
"Line {line_count} of {filename:?}: Overriding previous property '{key}':'{old_value}' with new value '{value}'"
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#[cfg(not(target_os = "android"))]
use super::*;
#[cfg(not(target_os = "android"))]
#[test]
fn test_load_properties_from_file() {
let mut properties = HashMap::new();
load_properties_from_file(
Path::new("tests/android/system_build.prop"),
None,
"u:r:init:s0",
&mut properties,
)
.unwrap();
assert_eq!(
properties.get("persist.sys.usb.config"),
Some(&"adb".to_string())
);
}
#[cfg(not(target_os = "android"))]
struct TempDir(PathBuf);
#[cfg(not(target_os = "android"))]
impl TempDir {
fn new(label: &str) -> Self {
let dir = std::env::temp_dir().join(format!("{label}_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
}
#[cfg(not(target_os = "android"))]
impl Drop for TempDir {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
#[cfg(not(target_os = "android"))]
#[test]
fn test_import_recursion_and_expansion() {
use std::io::Write;
let tmp = TempDir::new("rsprops_import_test");
let dir = &tmp.0;
let imported = dir.join("imported.prop");
writeln!(File::create(&imported).unwrap(), "from.import=1").unwrap();
let root = dir.join("root.prop");
{
let mut f = File::create(&root).unwrap();
writeln!(f, "ro.base={}", dir.display()).unwrap();
writeln!(f, "import ${{ro.base}}/imported.prop").unwrap();
writeln!(f, "import {}/missing.prop", dir.display()).unwrap();
writeln!(f, "after.import=2").unwrap();
}
let mut properties = HashMap::new();
load_properties_from_file(&root, None, "u:r:init:s0", &mut properties).unwrap();
assert_eq!(properties.get("from.import"), Some(&"1".to_string()));
assert_eq!(properties.get("after.import"), Some(&"2".to_string()));
}
#[cfg(not(target_os = "android"))]
#[test]
fn test_duplicate_import_reapplies_last_wins() {
use std::io::Write;
let tmp = TempDir::new("rsprops_lastwins_test");
let dir = &tmp.0;
let overlay = dir.join("overlay.prop");
writeln!(File::create(&overlay).unwrap(), "key=from_overlay").unwrap();
let root = dir.join("root.prop");
{
let mut f = File::create(&root).unwrap();
writeln!(f, "import {}", overlay.display()).unwrap();
writeln!(f, "key=local").unwrap();
writeln!(f, "import {}", overlay.display()).unwrap();
}
let mut properties = HashMap::new();
load_properties_from_file(&root, None, "u:r:init:s0", &mut properties).unwrap();
assert_eq!(properties.get("key"), Some(&"from_overlay".to_string()));
}
#[cfg(not(target_os = "android"))]
#[test]
fn test_expand_props_aosp_parity() {
let mut props = HashMap::new();
props.insert("a".to_string(), "va".to_string());
props.insert("empty".to_string(), String::new());
assert_eq!(expand_import_path("x$$y", &props).as_deref(), Some("x$y"));
assert_eq!(
expand_import_path("/${a}/f", &props).as_deref(),
Some("/va/f")
);
assert_eq!(
expand_import_path("${miss:-d}", &props).as_deref(),
Some("d")
);
assert_eq!(
expand_import_path("${empty:-d}", &props).as_deref(),
Some("d")
);
assert_eq!(expand_import_path("${miss}", &props), None);
assert_eq!(expand_import_path("${empty}", &props), None);
assert_eq!(expand_import_path("a$b", &props), None);
assert_eq!(expand_import_path("${a", &props), None);
}
#[cfg(not(target_os = "android"))]
#[test]
fn test_read_bounded_line_truncates_giant_line() {
let mut data = vec![b'x'; MAX_LINE_LEN + 100];
data.push(b'\n');
data.extend_from_slice(b"next=1\n");
let mut reader = BufReader::new(std::io::Cursor::new(data));
let mut buf = Vec::new();
let (read, truncated) = read_bounded_line(&mut reader, &mut buf).unwrap();
assert!(truncated);
assert_eq!(buf.len(), MAX_LINE_LEN);
assert_eq!(read, MAX_LINE_LEN + 101);
let (_, truncated) = read_bounded_line(&mut reader, &mut buf).unwrap();
assert!(!truncated);
assert_eq!(&buf[..], b"next=1\n");
let (read, _) = read_bounded_line(&mut reader, &mut buf).unwrap();
assert_eq!(read, 0); }
#[cfg(not(target_os = "android"))]
#[test]
fn test_import_cycle_is_cut() {
use std::io::Write;
let tmp = TempDir::new("rsprops_cycle_test");
let dir = &tmp.0;
let a = dir.join("a.prop");
let b = dir.join("b.prop");
writeln!(File::create(&a).unwrap(), "import {}", b.display()).unwrap();
writeln!(File::create(&b).unwrap(), "import {}\nkey=v", a.display()).unwrap();
let mut properties = HashMap::new();
load_properties_from_file(&a, None, "u:r:init:s0", &mut properties).unwrap();
assert_eq!(properties.get("key"), Some(&"v".to_string()));
}
}