1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
//! X11 resource manager library.
//!
//! To open a database, it is recommended to use [`Database::new_from_default`], but that function
//! needs to do I/O. A wrapper to simplify usage is e.g. provided in the x11rb crate.
//!
//! This functionality is similar to what is available to C code through xcb-util-xrm and Xlib's
//! `Xrm*` function family. Not all their functionality is available in this library. Please open a
//! feature request if you need something that is not available.
//!
//! The code in this module is only available when the `resource_manager` feature of the library is
//! enabled.
#![cfg(feature = "std")]
use std::env::var_os;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use alloc::string::String;
use alloc::vec::Vec;
use crate::protocol::xproto::{GetPropertyReply, GetPropertyRequest};
mod matcher;
mod parser;
/// Maximum nesting of #include directives, same value as Xlib uses.
/// After following this many `#include` directives, further includes are ignored.
const MAX_INCLUSION_DEPTH: u8 = 100;
/// How tightly does the component of an entry match a query?
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Binding {
/// We have a tight match, meaning that the next component of the entry must match the query.
Tight,
/// We have a loose match, meaning that any number of components can be skipped before the next
/// match.
Loose,
}
/// A component of a database entry.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Component {
/// A string component
Normal(String), // Actually just a-z, A-Z, 0-9 and _ or - is allowed
/// A wildcard component ("?") that matches anything
Wildcard,
}
/// A single entry in the resource manager database.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Entry {
/// The components of the entry describe which queries it matches
components: Vec<(Binding, Component)>,
/// The value of the entry is what the caller gets after a match.
value: Vec<u8>,
}
mod work_around_constant_limitations {
// For GET_RESOURCE_DATABASE, we need Into::into() to use AtomEnum, but that is not const.
// This module exists to work around that.
pub(super) const ATOM_RESOURCE_MANAGER: u32 = 23;
pub(super) const ATOM_STRING: u32 = 31;
#[test]
fn constants_are_correct() {
use crate::protocol::xproto::AtomEnum;
assert_eq!(u32::from(AtomEnum::RESOURCE_MANAGER), ATOM_RESOURCE_MANAGER);
assert_eq!(u32::from(AtomEnum::STRING), ATOM_STRING);
}
}
/// A X11 resource database.
///
/// The recommended way to load a database is through [`Database::new_from_default`].
#[derive(Debug, Default, Clone)]
pub struct Database {
entries: Vec<Entry>,
}
impl Database {
/// The GetPropertyRequest to load the X11 resource database from the root window.
///
/// Copy this struct, set its `window` field to the root window of the first screen send the
/// resulting request to the X11 server. The reply can be passed to
/// [`Self::new_from_default`].
pub const GET_RESOURCE_DATABASE: GetPropertyRequest = GetPropertyRequest {
delete: false,
window: 0,
property: work_around_constant_limitations::ATOM_RESOURCE_MANAGER,
type_: work_around_constant_limitations::ATOM_STRING,
long_offset: 0,
// This is what Xlib does, so it must be correct (tm)
long_length: 100_000_000,
};
/// Create a new X11 resource database from the default locations.
///
/// The `reply` argument should come from [`Self::GET_RESOURCE_DATABASE`] with its `window`
/// field set to the window ID of the first root window. The `hostname` argument should be the
/// hostname of the running system.
///
/// The default location is a combination of two places. First, the following places are
/// searched for data:
/// - The `RESOURCE_MANAGER` property of the first screen's root window (See
/// [`Self::GET_RESOURCE_DATABASE`] and [`Self::new_from_get_property_reply`]).
/// - If not found, the file `$HOME/.Xresources` is loaded.
/// - If not found, the file `$HOME/.Xdefaults` is loaded.
///
/// The result of the above search of the above search is combined with:
/// - The contents of the file `$XENVIRONMENT`, if this environment variable is set.
/// - Otherwise, the contents of `$HOME/.Xdefaults-[hostname]`.
///
/// This function only returns an error if communication with the X11 server fails. All other
/// errors are ignored. It might be that an empty database is returned.
///
/// The behaviour of this function is mostly equivalent to Xlib's `XGetDefault()`. The
/// exception is that `XGetDefault()` does not load `$HOME/.Xresources`.
///
/// The behaviour of this function is equivalent to xcb-util-xrm's
/// `xcb_xrm_database_from_default()`.
pub fn new_from_default(reply: &GetPropertyReply, hostname: OsString) -> Self {
let cur_dir = Path::new(".");
// 1. Try to load the RESOURCE_MANAGER property
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) = var_os("HOME") {
// 2. Otherwise, try to load $HOME/.Xresources
let mut path = PathBuf::from(&home);
path.push(".Xresources");
let read_something = if let Ok(data) = std::fs::read(&path) {
parse_data_with_base_directory(&mut entries, &data, Path::new(&home), 0);
true
} else {
false
};
// Restore the path so it refers to $HOME again
let _ = path.pop();
if !read_something {
// 3. Otherwise, try to load $HOME/.Xdefaults
path.push(".Xdefaults");
if let Ok(data) = std::fs::read(&path) {
parse_data_with_base_directory(&mut entries, &data, Path::new(&home), 0);
}
}
}
entries
};
// 4. If XENVIRONMENT is specified, merge the database defined by that file
if let Some(xenv) = var_os("XENVIRONMENT") {
if let Ok(data) = std::fs::read(&xenv) {
let base = Path::new(&xenv).parent().unwrap_or(cur_dir);
parse_data_with_base_directory(&mut entries, &data, base, 0);
}
} else {
// 5. Load `$HOME/.Xdefaults-[hostname]`
let mut file = std::ffi::OsString::from(".Xdefaults-");
file.push(hostname);
let mut path = match var_os("HOME") {
Some(home) => PathBuf::from(home),
None => PathBuf::new(),
};
path.push(file);
if let Ok(data) = std::fs::read(&path) {
let base = path.parent().unwrap_or(cur_dir);
parse_data_with_base_directory(&mut entries, &data, base, 0);
}
}
Self { entries }
}
/// Construct a new X11 resource database from a [`GetPropertyReply`].
///
/// The reply should come from [`Self::GET_RESOURCE_DATABASE`] with its `window` field set to
/// the window ID of the first root window.
pub fn new_from_get_property_reply(reply: &GetPropertyReply) -> Option<Database> {
if reply.format == 8 && !reply.value.is_empty() {
Some(Database::new_from_data(&reply.value))
} else {
None
}
}
/// Construct a new X11 resource database from raw data.
///
/// This function parses data like `Some.Entry: Value\n#include "some_file"\n` and returns the
/// resulting resource database. Parsing cannot fail since unparsable lines are simply ignored.
///
/// See [`Self::new_from_data_with_base_directory`] for a version that allows to provide a path that
/// is used for resolving relative `#include` statements.
pub fn new_from_data(data: &[u8]) -> Self {
let mut entries = Vec::new();
parse_data_with_base_directory(&mut entries, data, Path::new("."), 0);
Self { entries }
}
/// Construct a new X11 resource database from raw data.
///
/// This function parses data like `Some.Entry: Value\n#include "some_file"\n` and returns the
/// resulting resource database. Parsing cannot fail since unparsable lines are simply ignored.
///
/// When a relative `#include` statement is encountered, the file to include is searched
/// relative to the given `base_path`.
pub fn new_from_data_with_base_directory(data: &[u8], base_path: impl AsRef<Path>) -> Self {
fn helper(data: &[u8], base_path: &Path) -> Database {
let mut entries = Vec::new();
parse_data_with_base_directory(&mut entries, data, base_path, 0);
Database { entries }
}
helper(data, base_path.as_ref())
}
/// Get a value from the resource database as a byte slice.
///
/// The given values describe a query to the resource database. `resource_class` can be an
/// empty string, but otherwise must contain the same number of components as `resource_name`.
/// Both strings may only contain alphanumeric characters or '-', '_', and '.'.
///
/// For example, this is how Xterm could query one of its settings if it where written in Rust
/// (see `man xterm`):
/// ```
/// use x11rb_protocol::resource_manager::Database;
/// fn get_pointer_shape(db: &Database) -> &[u8] {
/// db.get_bytes("XTerm.vt100.pointerShape", "XTerm.VT100.Cursor").unwrap_or(b"xterm")
/// }
/// ```
pub fn get_bytes(&self, resource_name: &str, resource_class: &str) -> Option<&[u8]> {
matcher::match_entry(&self.entries, resource_name, resource_class)
}
/// Get a value from the resource database as a byte slice.
///
/// The given values describe a query to the resource database. `resource_class` can be an
/// empty string, but otherwise must contain the same number of components as `resource_name`.
/// Both strings may only contain alphanumeric characters or '-', '_', and '.'.
///
/// If an entry is found that is not a valid utf8 `str`, `None` is returned.
///
/// For example, this is how Xterm could query one of its settings if it where written in Rust
/// (see `man xterm`):
/// ```
/// use x11rb_protocol::resource_manager::Database;
/// fn get_pointer_shape(db: &Database) -> &str {
/// db.get_string("XTerm.vt100.pointerShape", "XTerm.VT100.Cursor").unwrap_or("xterm")
/// }
/// ```
pub fn get_string(&self, resource_name: &str, resource_class: &str) -> Option<&str> {
std::str::from_utf8(self.get_bytes(resource_name, resource_class)?).ok()
}
/// Get a value from the resource database as a byte slice.
///
/// The given values describe a query to the resource database. `resource_class` can be an
/// empty string, but otherwise must contain the same number of components as `resource_name`.
/// Both strings may only contain alphanumeric characters or '-', '_', and '.'.
///
/// This function interprets "true", "on", "yes" as true-ish and "false", "off", "no" als
/// false-ish. Numbers are parsed and are true if they are not zero. Unknown values are mapped
/// to `None`.
///
/// For example, this is how Xterm could query one of its settings if it where written in Rust
/// (see `man xterm`):
/// ```
/// use x11rb_protocol::resource_manager::Database;
/// fn get_bell_is_urgent(db: &Database) -> bool {
/// db.get_bool("XTerm.vt100.bellIsUrgent", "XTerm.VT100.BellIsUrgent").unwrap_or(false)
/// }
/// ```
pub fn get_bool(&self, resource_name: &str, resource_class: &str) -> Option<bool> {
to_bool(self.get_string(resource_name, resource_class)?)
}
/// Get a value from the resource database and parse it.
///
/// The given values describe a query to the resource database. `resource_class` can be an
/// empty string, but otherwise must contain the same number of components as `resource_name`.
/// Both strings may only contain alphanumeric characters or '-', '_', and '.'.
///
/// If no value is found, `Ok(None)` is returned. Otherwise, the result from
/// [`FromStr::from_str]` is returned with `Ok(value)` replaced with `Ok(Some(value))`.
///
/// For example, this is how Xterm could query one of its settings if it where written in Rust
/// (see `man xterm`):
/// ```
/// use x11rb_protocol::resource_manager::Database;
/// fn get_print_attributes(db: &Database) -> u8 {
/// db.get_value("XTerm.vt100.printAttributes", "XTerm.VT100.PrintAttributes")
/// .ok().flatten().unwrap_or(1)
/// }
/// ```
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()
}
}
/// Parse the given data as a resource database.
///
/// The parsed entries are appended to `result`. `#include`s are resolved relative to the given
/// `base_path`. `depth` is the number of includes that we are already handling. This value is used
/// to prevent endless loops when a file (directly or indirectly) includes itself.
fn parse_data_with_base_directory(
result: &mut Vec<Entry>,
data: &[u8],
base_path: &Path,
depth: u8,
) {
if depth > MAX_INCLUSION_DEPTH {
return;
}
parser::parse_database(data, result, |path, entries| {
// Construct the name of the file to include
if let Ok(path) = std::str::from_utf8(path) {
let mut path_buf = PathBuf::from(base_path);
path_buf.push(path);
// Read the file contents
if let Ok(new_data) = std::fs::read(&path_buf) {
// Parse the file contents with the new base path
let new_base = path_buf.parent().unwrap_or(base_path);
parse_data_with_base_directory(entries, &new_data, new_base, depth + 1);
}
}
});
}
/// Parse a value to a boolean, returning `None` if this is not possible.
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" => Some(true),
b"on" => Some(true),
b"yes" => Some(true),
b"false" => Some(false),
b"off" => Some(false),
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:");
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.iter() {
let db = Database::new_from_data(input);
let result = db.get_value::<i32>("a", "a");
assert_eq!(result.unwrap().unwrap(), *expected);
}
}
}