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 | |
15 | use std::env::var_os; |
16 | use std::ffi::OsString; |
17 | use std::path::{Path, PathBuf}; |
18 | use std::str::FromStr; |
19 | |
20 | use alloc::string::String; |
21 | use alloc::vec::Vec; |
22 | |
23 | use crate::protocol::xproto::{GetPropertyReply, GetPropertyRequest}; |
24 | |
25 | mod matcher; |
26 | mod parser; |
27 | |
28 | /// Maximum nesting of #include directives, same value as Xlib uses. |
29 | /// After following this many `#include` directives, further includes are ignored. |
30 | const 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)] |
34 | enum 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)] |
44 | enum 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)] |
53 | pub(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 | |
60 | mod 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)] |
79 | pub struct Database { |
80 | entries: Vec<Entry>, |
81 | } |
82 | |
83 | impl 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. |
318 | fn 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. |
344 | fn 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)] |
360 | mod 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 | |