Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,618 @@
//! Match Xrm entries against a query.
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use std::cmp::Ordering;
use super::parser::parse_query;
use super::{Binding, Component, Entry};
mod zip_longest {
/// Given two slices, produce an iterator that zips the two slices.
///
/// Compared to std::iter::Iterator::zip(), this iterator does not stop at the end of the
/// shorter of the two slices, but instead continues to the end of the longer slice. To make
/// this possible, the individual items are wrapped in `Option`.
///
/// See tests below to make this clearer.
pub(super) fn zip_longest<'a, T>(
a: &'a [T],
b: &'a [T],
) -> impl Iterator<Item = (Option<&'a T>, Option<&'a T>)> + 'a {
ZipLongest {
a: a.iter(),
b: b.iter(),
}
}
#[derive(Debug)]
struct ZipLongest<A, B> {
a: A,
b: B,
}
impl<A, B> Iterator for ZipLongest<A, B>
where
A: Iterator,
B: Iterator,
{
type Item = (Option<A::Item>, Option<B::Item>);
fn next(&mut self) -> Option<Self::Item> {
match (self.a.next(), self.b.next()) {
(None, None) => None,
(a, b) => Some((a, b)),
}
}
}
#[cfg(test)]
mod test_zip_longest {
use super::zip_longest;
use alloc::vec::Vec;
#[test]
fn empty() {
let (a, b): ([u8; 0], [u8; 0]) = ([], []);
let res = zip_longest(&a, &b).collect::<Vec<_>>();
assert_eq!(res, []);
}
#[test]
fn same_length() {
let a = [0, 1, 2];
let b = [4, 5, 6];
let expected = [
(Some(&0), Some(&4)),
(Some(&1), Some(&5)),
(Some(&2), Some(&6)),
];
let res = zip_longest(&a, &b).collect::<Vec<_>>();
assert_eq!(res, expected);
}
#[test]
fn first_shorter() {
let a = [0, 1];
let b = [4, 5, 6, 7];
let expected = [
(Some(&0), Some(&4)),
(Some(&1), Some(&5)),
(None, Some(&6)),
(None, Some(&7)),
];
let res = zip_longest(&a, &b).collect::<Vec<_>>();
assert_eq!(res, expected);
}
#[test]
fn second_shorter() {
let a = [0, 1, 2, 3];
let b = [4, 5];
let expected = [
(Some(&0), Some(&4)),
(Some(&1), Some(&5)),
(Some(&2), None),
(Some(&3), None),
];
let res = zip_longest(&a, &b).collect::<Vec<_>>();
assert_eq!(res, expected);
}
}
}
/// Info how a specific component was matched.
///
/// This information is used to decide which of two matches is "better" in `compare_matches()`.
#[derive(Debug, Copy, Clone)]
enum HowMatched {
/// The component matched the instance of the query
Instance,
/// The component matched the class of the query
Class,
/// The component is a wildcard and thus matched by default
Wildcard,
}
/// Info on how an (unskipped) component of the query was matched
///
/// This information is used to decide which of two matches is "better" in `compare_matches()`.
#[derive(Debug, Copy, Clone)]
struct MatchComponent {
preceding_binding: Binding,
how_matched: HowMatched,
}
/// Info how a (possibly skipped) component of the query was matched
///
/// This information is used to decide which of two matches is "better" in `compare_matches()`.
#[derive(Debug, Copy, Clone)]
enum MatchKind {
/// The component was skipped via a loose binding ("*")
SkippedViaLooseBinding,
/// The component was matched against the entry.
Matched(MatchComponent),
}
impl MatchKind {
/// Create a new `MatchKind::Match` with the given entries.
fn new_match(preceding_binding: Binding, how_matched: HowMatched) -> Self {
Self::Matched(MatchComponent {
preceding_binding,
how_matched,
})
}
}
fn check_match(entry: &Entry, resource: &[String], class: &[String]) -> Vec<Vec<MatchKind>> {
/// Current state of the matching machinery
#[derive(Debug, Default)]
struct MatchState {
/// Index into the entry on where we have to continue matching
index: usize,
/// How did we get to this state?
history: Vec<MatchKind>,
}
impl MatchState {
/// Record that a component was skipped via a loose binding (`*`).
fn skip_loose(&self) -> Self {
let mut history = self.history.clone();
history.push(MatchKind::SkippedViaLooseBinding);
Self {
index: self.index,
history,
}
}
/// Record that a component was matched in the given way.
fn step(mut self, kind: MatchKind) -> Self {
self.history.push(kind);
self.index += 1;
self
}
}
// The idea is to check if a nondeterministic finite automaton accepts a given
// word. We have a set of current states. This describes where in the
// entry we are while trying to match. When we match a component, we go to the next
// component in the entry (index + 1, `MatchState::step()`). When we have a loose binding, we
// can accept the current component by staying in the same state (index,
// `MatchState::skip_loose()`).
let mut states = vec![MatchState::default()];
// Go through the components and match them against the query
for (resource, class) in zip_longest::zip_longest(resource, class) {
let mut next_states = Vec::new();
for state in states.into_iter() {
if state.index == entry.components.len() {
// We are at the end of the entry and thus cannot continue this match.
// We drop this match state.
continue;
}
let binding = entry.components[state.index].0;
match binding {
// We have to match here, no way around that.
Binding::Tight => {}
// We could "eat" this with the loose binding by staying in the state
Binding::Loose => next_states.push(state.skip_loose()),
}
// Does the component match?
let kind = match entry.components[state.index].1 {
Component::Wildcard => Some(MatchKind::new_match(binding, HowMatched::Wildcard)),
Component::Normal(ref s) => {
if Some(s) == resource {
Some(MatchKind::new_match(binding, HowMatched::Instance))
} else if Some(s) == class {
Some(MatchKind::new_match(binding, HowMatched::Class))
} else {
None
}
}
};
if let Some(kind) = kind {
// Yes, the component matches and we go to the next state
next_states.push(state.step(kind));
}
}
states = next_states;
}
// We have a match if we reached the end of the components
states
.into_iter()
.filter(|s| s.index == entry.components.len())
.map(|s| s.history)
.collect()
}
/// Compare two matches and decide which one of the two is better (`Ordering::Greater`)
fn compare_matches(match1: &[MatchKind], match2: &[MatchKind]) -> Ordering {
use Binding::*;
use HowMatched::*;
use MatchKind::*;
fn rule1(match1: &MatchKind, match2: &MatchKind) -> Ordering {
// Precedence rule #1: Matching components (including wildcard '?') outweighs loose bindings ('*')
if let Matched(_) = match1 {
if let SkippedViaLooseBinding = match2 {
return Ordering::Greater;
}
}
Ordering::Equal
}
fn rule2(match1: &MatchKind, match2: &MatchKind) -> Ordering {
// Precedence rule #2a: Matching instance outweighs both matching class and wildcard
if let Matched(MatchComponent {
how_matched: Instance,
preceding_binding: _,
}) = match1
{
if let Matched(MatchComponent {
how_matched: Class,
preceding_binding: _,
}) = match2
{
return Ordering::Greater;
}
if let Matched(MatchComponent {
how_matched: Wildcard,
..
}) = match2
{
return Ordering::Greater;
}
}
// Precedence rule #2b: Matching class outweighs wildcard
if let Matched(MatchComponent {
how_matched: Class, ..
}) = match1
{
if let Matched(MatchComponent {
how_matched: Wildcard,
..
}) = match2
{
return Ordering::Greater;
}
}
Ordering::Equal
}
fn rule3(match1: &MatchKind, match2: &MatchKind) -> Ordering {
// Precedence rule #3: A preceding exact match outweights a preceding '*'
if let Matched(MatchComponent {
preceding_binding: Tight,
..
}) = match1
{
if let Matched(MatchComponent {
preceding_binding: Loose,
..
}) = match2
{
return Ordering::Greater;
}
}
Ordering::Equal
}
assert_eq!(
match1.len(),
match2.len(),
"Both matches should have the same length (which is guaranteed by the current \
implementation of check_match())"
);
for (m1, m2) in match1.iter().zip(match2.iter()) {
let ordering = rule1(m1, m2)
.then_with(|| rule1(m2, m1).reverse())
.then_with(|| rule2(m1, m2))
.then_with(|| rule2(m2, m1).reverse())
.then_with(|| rule3(m1, m2))
.then_with(|| rule3(m2, m1).reverse());
if ordering != Ordering::Equal {
return ordering;
}
}
Ordering::Equal
}
/// Find the best match for the given query in the database, returning `None` when nothing matches.
pub(crate) fn match_entry<'a>(
database: &'a [Entry],
resource: &str,
class: &str,
) -> Option<&'a [u8]> {
let resource = parse_query(resource.as_bytes())?;
let class = parse_query(class.as_bytes())?;
database
.iter()
// Filter for entries that match the query (and record some info on how they match)
.flat_map(|entry| {
let matches = check_match(entry, &resource, &class);
let best_match = matches
.into_iter()
.max_by(|match1, match2| compare_matches(match1, match2));
best_match.map(|m| (entry, m))
})
.max_by(|(_, match1), (_, match2)| compare_matches(match1, match2))
.map(|(entry, _)| &entry.value[..])
}
#[cfg(test)]
mod test {
use super::super::parser::parse_database;
use super::match_entry;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::eprintln;
// Most tests in here are based on [1], which is: Copyright © 2016 Ingo Bürk
// [1]: https://github.com/Airblader/xcb-util-xrm/blob/master/tests/tests_match.c
#[test]
fn test_matches() {
let tests = [
// Non-matches / Errors
(&b""[..], "", "", None),
// Xlib returns the match here, despite the query violating the specs (different number
// of components in the query)
(
b"First.second: 1",
"First.second",
"First.second.third",
None,
),
(b"", "First.second", "", None),
(b"First.second: 1", "First.third", "", None),
(b"First.second: 1", "First", "", None),
(b"First: 1", "First.second", "", None),
(b"First.?.fourth: 1", "First.second.third.fourth", "", None),
(b"First*?.third: 1", "First.third", "", None),
(b"First: 1", "first", "", None),
(b"First: 1", "", "first", None),
// Duplicate entries
(
b"First: 1\nFirst: 2\nFirst: 3\n",
"First",
"",
Some(&b"3"[..]),
),
(
b"First: 1\nSecond: 2\nSecond: 3\nThird: 4\n",
"Second",
"",
Some(b"3"),
),
// Basic matching
(b"First: 1", "First", "", Some(b"1")),
(b"First.second: 1", "First.second", "", Some(b"1")),
(b"?.second: 1", "First.second", "", Some(b"1")),
(b"First.?.third: 1", "First.second.third", "", Some(b"1")),
(
b"First.?.?.fourth: 1",
"First.second.third.fourth",
"",
Some(b"1"),
),
(b"*second: 1", "First.second", "", Some(b"1")),
(b".second: 1", "First.second", "", None),
(b"*third: 1", "First.second.third", "", Some(b"1")),
(b"First*second: 1", "First.second", "", Some(b"1")),
(b"First*third: 1", "First.second.third", "", Some(b"1")),
(
b"First*fourth: 1",
"First.second.third.fourth",
"",
Some(b"1"),
),
(b"First*?.third: 1", "First.second.third", "", Some(b"1")),
(b"First: 1", "Second", "First", Some(b"1")),
(
b"First.second: 1",
"First.third",
"first.second",
Some(b"1"),
),
(
b"First.second.third: 1",
"First.third.third",
"first.second.fourth",
Some(b"1"),
),
(
b"First*third*fifth: 1",
"First.second.third.fourth.third.fifth",
"",
Some(b"1"),
),
(b"First: x\\\ny", "First", "", Some(b"xy")),
(b"! First: x", "First", "", None),
(b"# First: x", "First", "", None),
(b"First:", "First", "", Some(b"")),
(b"First: ", "First", "", Some(b"")),
(b"First: \t ", "First", "", Some(b"")),
// Consecutive bindings
(b"*.bar: 1", "foo.foo.bar", "", Some(b"1")),
(b"...bar: 1", "foo.bar", "", None),
(b"...bar: 1", "foo.foo.foo.bar", "", None),
(b"***bar: 1", "foo.bar", "", Some(b"1")),
(b".*.bar: 1", "foo.bar", "", Some(b"1")),
(b".*.bar: 1", "foo.foo.bar", "", Some(b"1")),
(b"..*bar: 1", "foo.foo.foo.foo.bar", "", Some(b"1")),
(b"a.*.z: 1", "a.b.c.d.e.f.z", "", Some(b"1")),
(b"a...z: 1", "a.z", "", Some(b"1")),
(b"a...z: 1", "a.b.z", "", None),
// Matching among multiple entries
(b"First: 1\nSecond: 2\n", "First", "", Some(b"1")),
(b"First: 1\nSecond: 2\n", "Second", "", Some(b"2")),
// Greediness
(b"a*c.e: 1", "a.b.c.d.c.e", "", Some(b"1")),
(b"a*c.e: 1", "a.b.c.c.e", "", Some(b"1")),
(b"a*?.e: 1", "a.b.c.e", "", Some(b"1")),
(b"a*c*e: 1", "a.b.c.d.c.d.e.d.e", "", Some(b"1")),
// Precedence rules
// Rule 1
(
b"First.second.third: 1\nFirst*third: 2\n",
"First.second.third",
"",
Some(b"1"),
),
(
b"First*third: 2\nFirst.second.third: 1\n",
"First.second.third",
"",
Some(b"1"),
),
(
b"First.second.third: 1\nFirst*third: 2\n",
"x.x.x",
"First.second.third",
Some(b"1"),
),
(
b"First*third: 2\nFirst.second.third: 1\n",
"x.x.x",
"First.second.third",
Some(b"1"),
),
// Rule 2
(
b"First.second: 1\nFirst.third: 2\n",
"First.second",
"First.third",
Some(b"1"),
),
(
b"First.third: 2\nFirst.second: 1\n",
"First.second",
"First.third",
Some(b"1"),
),
(
b"First.second.third: 1\nFirst.?.third: 2\n",
"First.second.third",
"",
Some(b"1"),
),
(
b"First.?.third: 2\nFirst.second.third: 1\n",
"First.second.third",
"",
Some(b"1"),
),
(
b"First.second.third: 1\nFirst.?.third: 2\n",
"x.x.x",
"First.second.third",
Some(b"1"),
),
(
b"First.?.third: 2\nFirst.second.third: 1\n",
"x.x.x",
"First.second.third",
Some(b"1"),
),
// Rule 3
(
b"First.second: 1\nFirst*second: 2\n",
"First.second",
"",
Some(b"1"),
),
(
b"First*second: 2\nFirst.second: 1\n",
"First.second",
"",
Some(b"1"),
),
// Some real world examples. May contain duplicates to the above tests.
// From the specification:
// https://tronche.com/gui/x/xlib/resource-manager/matching-rules.html
(
b"xmh*Paned*activeForeground: red\n\
*incorporate.Foreground: blue\n\
xmh.toc*Command*activeForeground: green\n\
xmh.toc*?.Foreground: white\n\
xmh.toc*Command.activeForeground: black",
"xmh.toc.messagefunctions.incorporate.activeForeground",
"Xmh.Paned.Box.Command.Foreground",
Some(b"black"),
),
(
b"urxvt*background: [95]#000",
"urxvt.background",
"",
Some(b"[95]#000"),
),
(
b"urxvt*scrollBar_right:true",
"urxvt.scrollBar_right",
"",
Some(b"true"),
),
(
b"urxvt*cutchars: '\"'()*<>[]{|}",
"urxvt.cutchars",
"",
Some(b"'\"'()*<>[]{|}"),
),
(
b"urxvt.keysym.Control-Shift-Up: perl:font:increment",
"urxvt.keysym.Control-Shift-Up",
"",
Some(b"perl:font:increment"),
),
(
b"rofi.normal: #000000, #000000, #000000, #000000",
"rofi.normal",
"",
Some(b"#000000, #000000, #000000, #000000"),
),
// Own tests
(b"*foo.bar: 1", "bar", "", None),
(
b"First.Second.Third: 1\nFirst.Second: 2",
"First.Second.Third",
"First.Second",
Some(b"1"),
),
(
b"First.Second.Third: 1\nFirst.Second: 2",
"First.Second",
"First.Second.Third",
Some(b"1"),
),
];
let mut failures = 0;
for &(data, resource, class, expected) in tests.iter() {
let mut entries = Vec::new();
parse_database(data, &mut entries, |_, _| unreachable!());
let result = match_entry(&entries, resource, class);
if result != expected {
eprintln!(
"While testing resource '{resource}' and class '{class}' with the following input:"
);
eprintln!("{}", print_string(data));
eprintln!("Expected: {:?}", expected.map(print_string));
eprintln!("Got: {:?}", result.map(print_string));
eprintln!();
failures += 1;
}
}
if failures != 0 {
panic!("Had {} failures", failures)
}
}
fn print_string(data: &[u8]) -> String {
std::str::from_utf8(data)
.map(|s| s.to_string())
.unwrap_or_else(|_| format!("{data:?}"))
}
}

View File

@@ -0,0 +1,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 = 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);
}
}
}

View File

@@ -0,0 +1,697 @@
//! Code for parsing resource management things
use super::{Binding, Component, Entry};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
// =======================
// Common helper functions
// =======================
/// Check if a character (well, u8) is an octal digit
fn is_octal_digit(c: u8) -> bool {
matches!(c, b'0' | b'1' | b'2' | b'3' | b'4' | b'5' | b'6' | b'7')
}
/// Find the longest prefix of the given data where the given callback returns true
fn parse_with_matcher<M>(data: &[u8], matcher: M) -> (&[u8], &[u8])
where
M: Fn(u8) -> bool,
{
let end = data
.iter()
.enumerate()
.find(|(_, &c)| !matcher(c))
.map(|(idx, _)| idx)
.unwrap_or(data.len());
(&data[..end], &data[end..])
}
/// Check if a character is allowed in a quark name
fn allowed_in_quark_name(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'-' || c == b'_'
}
/// Find the longest prefix satisfying allowed_in_quark_name().
/// This returns (Some(prefix), remaining) if a prefix is found, else (None, data).
fn next_component(data: &[u8]) -> (Option<&[u8]>, &[u8]) {
let (prefix, remaining) = parse_with_matcher(data, allowed_in_quark_name);
match prefix {
[] => (None, remaining),
prefix => (Some(prefix), remaining),
}
}
// =========================
// Parser for resource files
// =========================
/// Skip to the next end of line in the given data
fn skip_to_eol(data: &[u8]) -> &[u8] {
parse_with_matcher(data, |c| c != b'\n').1
}
/// Skip all spaces in the given data
fn skip_spaces(data: &[u8]) -> &[u8] {
parse_with_matcher(data, |c| c == b' ').1
}
/// Skip the given text. Returns `None` if the text was not found
fn skip_text<'a>(data: &'a [u8], text: &[u8]) -> Option<&'a [u8]> {
if data.starts_with(text) {
Some(&data[text.len()..])
} else {
None
}
}
/// Parse a single `Component` from the data. This can either be a wildcard ("?") or a
/// component made up of characters accepted by `allowed_in_quark_name`.
fn next_component_name(data: &[u8]) -> (Option<Component>, &[u8]) {
if data.first() == Some(&b'?') {
(Some(Component::Wildcard), &data[1..])
} else {
let (comp, remaining) = next_component(data);
let comp = comp.map(|s| {
let s = std::str::from_utf8(s).expect("ascii-only");
Component::Normal(s.to_string())
});
(comp, remaining)
}
}
/// Parse a resource like "foo.?*baz" (wildcards allowed)
fn parse_components(data: &[u8]) -> (Vec<(Binding, Component)>, &[u8]) {
fn parse_binding(mut data: &[u8]) -> (Binding, &[u8]) {
let mut binding = Binding::Tight;
loop {
match data.first() {
Some(&b'*') => binding = Binding::Loose,
Some(&b'.') => {}
_ => break,
}
data = &data[1..];
}
(binding, data)
}
let mut data = data;
let mut result = Vec::new();
loop {
let (binding, remaining) = parse_binding(data);
if let (Some(component), remaining) = next_component_name(remaining) {
data = remaining;
result.push((binding, component));
} else {
break;
}
}
(result, data)
}
/// Parse a full entry from the data. This begins with components (see `parse_components()`),
/// then after a colon (":") comes the value. The value may contain escape sequences.
fn parse_entry(data: &[u8]) -> (Result<Entry, ()>, &[u8]) {
let (components, data) = parse_components(data);
match components.last() {
// Empty components are not allowed
None => return (Err(()), skip_to_eol(data)),
// The last component may not be a wildcard
Some((_, Component::Wildcard)) => return (Err(()), skip_to_eol(data)),
_ => {}
}
let data = skip_spaces(data);
// next comes a colon
let data = match data.split_first() {
Some((&b':', data)) => data,
_ => return (Err(()), skip_to_eol(data)),
};
// skip more spaces and let \ escape line breaks
let mut data = data;
loop {
let (_, remaining) = parse_with_matcher(data, |c| c == b' ' || c == b'\t');
if remaining.get(..2) == Some(&b"\\\n"[..]) {
data = &remaining[2..];
} else {
data = remaining;
break;
}
}
// Parse the value, decoding escape sequences. The most complicated case are octal escape
// sequences like \123.
let mut value = Vec::new();
let mut index = 0;
let mut octal = None;
while let Some(&b) = data.get(index) {
index += 1;
if b == b'\n' {
break;
}
if let Some(oct) = octal {
if is_octal_digit(b) {
// We are currently parsing an octal; add the new character
match oct {
(x, None) => octal = Some((x, Some(b))),
(x, Some(y)) => {
let (x, y, z) = (x - b'0', y - b'0', b - b'0');
let decoded = (x * 8 + y) * 8 + z;
value.push(decoded);
octal = None;
}
}
continue;
} else {
// Not an octal sequence; add the collected characters to the output
value.push(b'\\');
value.push(oct.0);
if let Some(oct2) = oct.1 {
value.push(oct2);
}
octal = None;
// Fall through to the parsing code below
}
}
if b != b'\\' {
value.push(b);
} else {
match data.get(index) {
None => {
value.push(b);
// Keep index as-is. This is to counter the += 1 below.
index -= 1;
}
Some(b' ') => value.push(b' '),
Some(b'\t') => value.push(b'\t'),
Some(b'n') => value.push(b'\n'),
Some(b'\\') => value.push(b'\\'),
Some(b'\n') => { /* Continue parsing next line */ }
Some(&x) if is_octal_digit(x) => octal = Some((x, None)),
Some(&x) => {
value.push(b);
value.push(x);
}
}
index += 1;
}
}
let entry = Entry { components, value };
(Ok(entry), &data[index..])
}
/// Parse the contents of a database
pub(crate) fn parse_database<F>(mut data: &[u8], result: &mut Vec<Entry>, mut include_callback: F)
where
for<'r> F: FnMut(&'r [u8], &mut Vec<Entry>),
{
// Iterate over lines
while let Some(first) = data.first() {
match first {
// Skip empty lines
b'\n' => data = &data[1..],
// Comment, skip the line
b'!' => data = skip_to_eol(data),
b'#' => {
let remaining = skip_spaces(&data[1..]);
// Skip to the next line for the next loop iteration. The rest of the code here
// tried to parse the line.
data = skip_to_eol(remaining);
// Only #include is defined
if let Some(remaining) = skip_text(remaining, b"include") {
let (_, remaining) = parse_with_matcher(remaining, |c| c == b' ');
// Find the text enclosed in quotation marks
if let Some(b'\"') = remaining.first() {
let (file, remaining) =
parse_with_matcher(&remaining[1..], |c| c != b'"' && c != b'\n');
if let Some(b'\"') = remaining.first() {
// Okay, we found a well-formed include directive.
include_callback(file, result);
}
}
}
}
_ => {
let (entry, remaining) = parse_entry(data);
data = remaining;
// Add the entry to the result if we parsed one; ignore errors
result.extend(entry.ok());
}
}
}
}
/// Parse a resource query like "foo.bar.baz" (no wildcards allowed, no bindings allowed)
pub(crate) fn parse_query(data: &[u8]) -> Option<Vec<String>> {
let mut data = data;
let mut result = Vec::new();
while let (Some(component), remaining) = next_component(data) {
data = remaining;
while let Some(&b'.') = data.first() {
data = &data[1..];
}
let component = std::str::from_utf8(component).expect("ascii-only");
result.push(component.to_string());
}
if data.is_empty() {
Some(result)
} else {
None
}
}
#[cfg(test)]
mod test {
use super::{parse_database, parse_entry, parse_query, Binding, Component, Entry};
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use std::eprintln;
// Most tests in here are based on [1], which is: Copyright © 2016 Ingo Bürk
// [1]: https://github.com/Airblader/xcb-util-xrm/blob/master/tests/tests_parser.c
#[test]
fn test_parse_query_success() {
let tests = [
(
&b"First.second"[..],
vec!["First".to_string(), "second".to_string()],
),
(b"", Vec::new()),
(
b"urxvt.scrollBar_right",
vec!["urxvt".to_string(), "scrollBar_right".to_string()],
),
(
b"urxvt.Control-Shift-Up",
vec!["urxvt".to_string(), "Control-Shift-Up".to_string()],
),
];
for (data, expected) in tests.iter() {
let result = parse_query(data);
assert_eq!(result.as_ref(), Some(expected), "while parsing {data:?}");
}
}
#[test]
fn test_parse_query_error() {
let tests = [
&b"First.second: on"[..],
b"First*second",
b"First.?.second",
b"*second",
b"?.second",
];
for data in tests.iter() {
let result = parse_query(data);
assert!(
result.is_none(),
"Unexpected success parsing '{data:?}': {result:?}"
);
}
}
#[test]
fn test_parse_entry_success() {
let tests = [
// Basics
(
&b"First: 1"[..],
vec![(Binding::Tight, Component::Normal("First".to_string()))],
&b"1"[..],
),
(
b"First.second: 1",
vec![
(Binding::Tight, Component::Normal("First".to_string())),
(Binding::Tight, Component::Normal("second".to_string())),
],
b"1",
),
(
b"First..second: 1",
vec![
(Binding::Tight, Component::Normal("First".to_string())),
(Binding::Tight, Component::Normal("second".to_string())),
],
b"1",
),
// Wildcards
(
b"?.second: 1",
vec![
(Binding::Tight, Component::Wildcard),
(Binding::Tight, Component::Normal("second".to_string())),
],
b"1",
),
(
b"First.?.third: 1",
vec![
(Binding::Tight, Component::Normal("First".to_string())),
(Binding::Tight, Component::Wildcard),
(Binding::Tight, Component::Normal("third".to_string())),
],
b"1",
),
// Loose bindings
(
b"*second: 1",
vec![(Binding::Loose, Component::Normal("second".to_string()))],
b"1",
),
(
b"First*third: 1",
vec![
(Binding::Tight, Component::Normal("First".to_string())),
(Binding::Loose, Component::Normal("third".to_string())),
],
b"1",
),
(
b"First**third: 1",
vec![
(Binding::Tight, Component::Normal("First".to_string())),
(Binding::Loose, Component::Normal("third".to_string())),
],
b"1",
),
// Combinations
(
b"First*?.fourth: 1",
vec![
(Binding::Tight, Component::Normal("First".to_string())),
(Binding::Loose, Component::Wildcard),
(Binding::Tight, Component::Normal("fourth".to_string())),
],
b"1",
),
// Values
(
b"First: 1337",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"1337",
),
(
b"First: -1337",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"-1337",
),
(
b"First: 13.37",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"13.37",
),
(
b"First: value",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"value",
),
(
b"First: #abcdef",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"#abcdef",
),
(
b"First: { key: 'value' }",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"{ key: 'value' }",
),
(
b"First: x?y",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x?y",
),
(
b"First: x*y",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x*y",
),
// Whitespace
(
b"First: x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x",
),
(
b"First: x ",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x ",
),
(
b"First: x ",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x ",
),
(
b"First:x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x",
),
(
b"First: \t x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x",
),
(
b"First: \t x \t",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x \t",
),
// Special characters
(
b"First: \\ x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b" x",
),
(
b"First: x\\ x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x x",
),
(
b"First: \\\tx",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\tx",
),
(
b"First: \\011x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\tx",
),
(
b"First: x\\\\x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x\\x",
),
(
b"First: x\\nx",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"x\nx",
),
(
b"First: \\080",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\\080",
),
(
b"First: \\00a",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\\00a",
),
// Own tests
// Some more escape tests, e.g. escape at end of input
(
b"First: \\",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\\",
),
(
b"First: \\xxx",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\\xxx",
),
(
b"First: \\1xx",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\\1xx",
),
(
b"First: \\10x",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\\10x",
),
(
b"First: \\100",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"@",
),
(
b"First: \\n",
vec![(Binding::Tight, Component::Normal("First".to_string()))],
b"\n",
),
];
for (data, resource, value) in tests.iter() {
run_entry_test(data, resource, value);
}
}
#[test]
fn test_parse_entry_error() {
let tests = [
&b": 1"[..],
b"?: 1",
b"First",
b"First second",
b"First.?: 1",
b"F\xc3\xb6rst: 1",
b"F~rst: 1",
];
for data in tests.iter() {
match parse_entry(data) {
(Ok(v), _) => panic!("Unexpected success parsing '{:?}': {:?}", data, v),
(Err(_), b"") => {}
(Err(_), remaining) => panic!(
"Unexpected remaining data parsing '{:?}': {:?}",
data, remaining
),
}
}
}
#[test]
fn test_parse_large_value() {
let value = vec![b'x'; 1025];
let mut data = b"First: ".to_vec();
data.extend(&value);
let resource = (Binding::Tight, Component::Normal("First".to_string()));
run_entry_test(&data, &[resource], &value);
}
#[test]
fn test_parse_large_resource() {
let x = vec![b'x'; 1025];
let y = vec![b'y'; 1025];
let mut data = x.clone();
data.push(b'.');
data.extend(&y);
data.extend(b": 1");
let resource = [
(
Binding::Tight,
Component::Normal(String::from_utf8(x).unwrap()),
),
(
Binding::Tight,
Component::Normal(String::from_utf8(y).unwrap()),
),
];
run_entry_test(&data, &resource, b"1");
}
#[test]
fn test_parse_database() {
let expected_entry = Entry {
components: vec![(Binding::Tight, Component::Normal("First".to_string()))],
value: b"1".to_vec(),
};
let tests = [
(&b"First: 1\n\n\n"[..], vec![expected_entry.clone()]),
(b"First: 1\n!Foo", vec![expected_entry.clone()]),
(b"!First: 1\nbar\n\n\n", Vec::new()),
(b"!bar\nFirst: 1\nbaz", vec![expected_entry.clone()]),
(b"First :\\\n \\\n\\\n1\n", vec![expected_entry]),
(
b"First: \\\n 1\\\n2\n",
vec![Entry {
components: vec![(Binding::Tight, Component::Normal("First".to_string()))],
value: b"12".to_vec(),
}],
),
];
let mut success = true;
for (data, expected) in tests.iter() {
let mut result = Vec::new();
parse_database(data, &mut result, |_, _| unreachable!());
if &result != expected {
eprintln!("While testing {data:?}");
eprintln!("Expected: {expected:?}");
eprintln!("Got: {result:?}");
eprintln!();
success = false;
}
}
if !success {
panic!()
}
}
#[test]
fn test_include_parsing() {
let tests = [
(&b"#include\"test\""[..], vec![&b"test"[..]]),
(b"#include\"test", Vec::new()),
(b"#include\"", Vec::new()),
(b"#include", Vec::new()),
(b"#includ", Vec::new()),
(b"#in", Vec::new()),
(b"# foo", Vec::new()),
(
b"# include \" test \" \n#include \"foo\"",
vec![b" test ", b"foo"],
),
];
let mut success = true;
for (data, expected) in tests.iter() {
let mut result = Vec::new();
let mut calls = Vec::new();
parse_database(data, &mut result, |file, _| calls.push(file.to_vec()));
if &calls != expected {
eprintln!("While testing {data:?}");
eprintln!("Expected: {expected:?}");
eprintln!("Got: {calls:?}");
eprintln!();
success = false;
}
}
if !success {
panic!()
}
}
#[test]
fn test_include_additions() {
let entry = Entry {
components: Vec::new(),
value: b"42".to_vec(),
};
let mut result = Vec::new();
parse_database(b"#include\"test\"", &mut result, |file, result| {
assert_eq!(file, b"test");
result.push(entry.clone());
});
assert_eq!(result, [entry]);
}
fn run_entry_test(data: &[u8], resource: &[(Binding, Component)], value: &[u8]) {
match parse_entry(data) {
(Ok(result), remaining) => {
assert_eq!(remaining, b"", "failed to parse {data:?}");
assert_eq!(
result.components, resource,
"incorrect components when parsing {data:?}",
);
assert_eq!(result.value, value, "incorrect value when parsing {data:?}");
}
(Err(err), _) => panic!("Failed to parse '{:?}': {:?}", data, err),
}
}
}