use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::str::FromStr;
use tiny_std::{UnixStr, UnixString};
use crate::proto::xproto::GetPropertyReply;
mod matcher;
mod parser;
const MAX_INCLUSION_DEPTH: u8 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Binding {
Tight,
Loose,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Component {
Normal(String),
Wildcard,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Entry {
components: Vec<(Binding, Component)>,
value: Vec<u8>,
}
pub(crate) mod work_around_constant_limitations {
pub(crate) const ATOM_RESOURCE_MANAGER: u32 = 23;
pub(crate) const ATOM_STRING: u32 = 31;
#[test]
fn constants_are_correct() {
use crate::proto::xproto::AtomEnum;
assert_eq!(
u32::from(AtomEnum::RESOURCE_MANAGER.0),
ATOM_RESOURCE_MANAGER
);
assert_eq!(u32::from(AtomEnum::STRING.0), ATOM_STRING);
}
}
#[derive(Debug, Default, Clone)]
pub struct Database {
entries: Vec<Entry>,
}
impl Database {
pub fn new_from_default(
reply: &GetPropertyReply,
hostname: String,
home_dir: Option<&UnixStr>,
xenvironment: Option<&UnixStr>,
) -> Result<Self, tiny_std::Error> {
let cur_dir = String::from(".");
let mut entries = if let Some(db) = Self::new_from_get_property_reply(reply)? {
db.entries
} else {
let mut entries = Vec::new();
if let Some(home) = home_dir {
let mut path = String::from(home.as_str()?);
path.push_str("/.Xresources\0");
let read_something =
if let Ok(data) = tiny_std::fs::read(UnixStr::try_from_str(&path)?) {
parse_data_with_base_directory(&mut entries, &data, home, 0)?;
true
} else {
false
};
let _ = path.pop();
if !read_something {
path.push_str("/.Xdefaults\0");
if let Ok(data) = tiny_std::fs::read(UnixStr::try_from_str(&path)?) {
parse_data_with_base_directory(&mut entries, &data, home, 0)?;
}
}
}
entries
};
if let Some(xenv) = xenvironment {
if let Ok(data) = tiny_std::fs::read(xenv) {
let base = xenv
.as_str()?
.rsplit_once('/')
.map(|s| s.0)
.unwrap_or(&cur_dir);
let base_unix = UnixString::try_from_str(base)?;
parse_data_with_base_directory(&mut entries, &data, &base_unix, 0)?;
}
} else {
let mut file = String::from(".Xdefaults-");
file.push_str(&hostname);
let base_path = match home_dir {
Some(home) => home,
None => UnixStr::EMPTY,
};
let base_path_utf8 = base_path.as_str()?;
let path = format!("{base_path_utf8}/{file}\0");
let path_unix = UnixString::try_from_str(&path)?;
if let Ok(data) = tiny_std::fs::read(&path_unix) {
parse_data_with_base_directory(&mut entries, &data, base_path, 0)?;
}
}
Ok(Self { entries })
}
pub fn new_from_get_property_reply(
reply: &GetPropertyReply,
) -> Result<Option<Database>, tiny_std::Error> {
if reply.format == 8 && !reply.value.is_empty() {
Ok(Some(Database::new_from_data(&reply.value)?))
} else {
Ok(None)
}
}
pub fn new_from_data(data: &[u8]) -> Result<Self, tiny_std::Error> {
const THIS_DIR: &UnixStr = UnixStr::from_str_checked(".\0");
let mut entries = Vec::new();
parse_data_with_base_directory(&mut entries, data, THIS_DIR, 0)?;
Ok(Self { entries })
}
pub fn new_from_data_with_base_directory(
data: &[u8],
base_path: &UnixStr,
) -> Result<Self, tiny_std::Error> {
fn helper(data: &[u8], base_path: &UnixStr) -> Result<Database, tiny_std::Error> {
let mut entries = Vec::new();
parse_data_with_base_directory(&mut entries, data, base_path, 0)?;
Ok(Database { entries })
}
helper(data, base_path)
}
#[must_use]
pub fn get_bytes(&self, resource_name: &str, resource_class: &str) -> Option<&[u8]> {
matcher::match_entry(&self.entries, resource_name, resource_class)
}
#[must_use]
pub fn get_string(&self, resource_name: &str, resource_class: &str) -> Option<&str> {
core::str::from_utf8(self.get_bytes(resource_name, resource_class)?).ok()
}
#[must_use]
pub fn get_bool(&self, resource_name: &str, resource_class: &str) -> Option<bool> {
to_bool(self.get_string(resource_name, resource_class)?)
}
pub fn get_value<T>(
&self,
resource_name: &str,
resource_class: &str,
) -> Result<Option<T>, T::Err>
where
T: FromStr,
{
self.get_string(resource_name, resource_class)
.map(T::from_str)
.transpose()
}
}
fn parse_data_with_base_directory(
result: &mut Vec<Entry>,
data: &[u8],
base_path: &UnixStr,
depth: u8,
) -> Result<(), tiny_std::Error> {
if depth > MAX_INCLUSION_DEPTH {
return Ok(());
}
parser::parse_database(data, result, |path, entries| {
if let Ok(path) = core::str::from_utf8(path) {
let base_utf8 = base_path.as_str()?;
let extended = format!("{base_utf8}/{path}\0");
let mut file_buf = Vec::with_capacity(4096);
if let Ok(data) = tiny_std::fs::read(UnixStr::from_str_checked(&extended)) {
let new_base = extended.rsplit_once('/').map(|s| s.0).unwrap_or(base_utf8);
parse_data_with_base_directory(
entries,
&file_buf,
UnixStr::try_from_str(new_base)?,
depth + 1,
)
} else {
Ok(())
}
} else {
Ok(())
}
})
}
fn to_bool(data: &str) -> Option<bool> {
if let Ok(num) = i64::from_str(data) {
return Some(num != 0);
}
match data.to_lowercase().as_bytes() {
b"true" | b"on" | b"yes" => Some(true),
b"false" | b"off" | b"no" => Some(false),
_ => None,
}
}
#[cfg(test)]
mod test {
use super::{to_bool, Database};
#[test]
fn test_bool_true() {
let data = ["1", "10", "true", "TRUE", "on", "ON", "yes", "YES"];
for input in &data {
assert_eq!(Some(true), to_bool(input));
}
}
#[test]
fn test_bool_false() {
let data = ["0", "false", "FALSE", "off", "OFF", "no", "NO"];
for input in &data {
assert_eq!(Some(false), to_bool(input));
}
}
#[test]
fn test_bool_none() {
let data = ["", "abc"];
for input in &data {
assert_eq!(None, to_bool(input));
}
}
#[test]
fn test_parse_i32_fail() {
let db = Database::new_from_data(b"a:").unwrap();
assert_eq!(db.get_string("a", "a"), Some(""));
assert!(db.get_value::<i32>("a", "a").is_err());
}
#[test]
fn test_parse_i32_success() {
let data = [
(&b"a: 0"[..], 0),
(b"a: 1", 1),
(b"a: -1", -1),
(b"a: 100", 100),
];
for (input, expected) in &data {
let db = Database::new_from_data(input).unwrap();
let result = db.get_value::<i32>("a", "a");
assert_eq!(result.unwrap().unwrap(), *expected);
}
}
}