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