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

Provided by KDAB

Privacy Policy