Skip to main content

rudof_rdf/rdf_core/utils/
regex.rs

1use std::{borrow::Cow, fmt::Display};
2
3use regex::{Regex, RegexBuilder};
4use thiserror::Error;
5
6/// Maximum size limit for compiled regular expressions.
7const REGEX_SIZE_LIMIT: usize = 1_000_000;
8
9/// A regular expression for RDF and SPARQL pattern matching.
10///
11/// This type wraps the Rust [`Regex`] type with SPARQL/XPath-compatible flag
12/// handling. It stores both the compiled regex and the original pattern with
13/// flags for display and debugging purposes.
14#[derive(Debug, Clone)]
15pub struct RDFRegex {
16    /// The compiled regular expression.
17    regex: Regex,
18    /// The source pattern string (after flag processing).
19    source: String,
20    /// Optional flags that were applied to this regex.
21    flags: Option<String>,
22}
23
24impl RDFRegex {
25    /// Creates a new regular expression with SPARQL-compatible flags.
26    ///
27    /// This constructor builds a regex following the XPath/SPARQL flag semantics,
28    /// which differ slightly from standard Rust regex flags. The implementation
29    /// is inspired by the Oxigraph SPARQL engine (https://github.com/oxigraph/oxigraph/blob/main/lib/spareval/src/eval.rs).
30    ///
31    /// # Arguments
32    ///
33    /// * `pattern` - The regular expression pattern string
34    /// * `flags` - Optional string containing flag characters (see below)
35    ///
36    /// # Supported Flags
37    ///
38    /// Following the [XPath specification](https://www.w3.org/TR/xpath-functions/#flags):
39    ///
40    /// - **`s`** (dot-all): Makes `.` match any character including newlines
41    /// - **`m`** (multi-line): Makes `^` and `$` match line boundaries, not just string boundaries
42    /// - **`i`** (case-insensitive): Performs case-insensitive matching
43    /// - **`x`** (ignore whitespace): Ignores unescaped whitespace and enables comments with `#`
44    /// - **`q`** (quote/literal): Treats the entire pattern as a literal string (escapes special characters)
45    ///
46    /// Flags can be combined, e.g., `"im"` for case-insensitive multi-line matching.
47    ///
48    /// # Size Limits
49    ///
50    /// The compiled regex is limited to [`REGEX_SIZE_LIMIT`] bytes. Patterns
51    /// exceeding this limit will fail with an error.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if:
56    /// - The pattern syntax is invalid (e.g., unbalanced parentheses, invalid escape sequences)
57    /// - An unsupported flag character is provided
58    /// - The compiled regex exceeds the size limit
59    pub fn new(pattern: &str, flags: Option<&str>) -> Result<Self, RDFRegexError> {
60        let mut pattern = Cow::Borrowed(pattern);
61        let flags = flags.unwrap_or_default();
62        if flags.contains('q') {
63            pattern = regex::escape(&pattern).into();
64        }
65        let mut regex_builder = RegexBuilder::new(&pattern);
66        regex_builder.size_limit(REGEX_SIZE_LIMIT);
67        for flag in flags.chars() {
68            match flag {
69                's' => {
70                    regex_builder.dot_matches_new_line(true);
71                },
72                'm' => {
73                    regex_builder.multi_line(true);
74                },
75                'i' => {
76                    regex_builder.case_insensitive(true);
77                },
78                'x' => {
79                    regex_builder.ignore_whitespace(true);
80                },
81                'q' => (),
82                _ => return Err(RDFRegexError::InvalidFlagOption(flag)),
83            }
84        }
85        let regex = regex_builder.build()?;
86        Ok(RDFRegex {
87            regex,
88            source: pattern.into_owned(),
89            flags: if flags.is_empty() {
90                None
91            } else {
92                Some(flags.to_string())
93            },
94        })
95    }
96
97    /// Tests whether the regex matches the given text.
98    ///
99    /// Returns `true` if the pattern matches any part of the input string,
100    /// following standard regex semantics (not anchored by default).
101    ///
102    /// # Arguments
103    ///
104    /// * `text` - The string to test against the pattern
105    pub fn is_match(&self, text: &str) -> bool {
106        self.regex.is_match(text)
107    }
108
109    /// Returns the flags used to create this regex, if any.
110    pub fn flags(&self) -> Option<&str> {
111        self.flags.as_deref()
112    }
113
114    /// Returns the source pattern string.
115    ///
116    /// For patterns created with the `q` (quote) flag, this returns the
117    /// escaped version of the original pattern.
118    pub fn source(&self) -> &str {
119        &self.source
120    }
121}
122
123/// Errors that can occur when creating or using RDF regular expressions.
124#[derive(Error, Debug)]
125pub enum RDFRegexError {
126    /// The regex pattern has invalid syntax.
127    #[error("Invalid regex pattern: {0}")]
128    InvalidPattern(#[from] regex::Error),
129
130    /// An unsupported flag character was provided.
131    ///
132    /// Valid flags are: `s`, `m`, `i`, `x`, `q`. Any other character
133    /// in the flags string will trigger this error.
134    #[error("Invalid regex flag option: {0}")]
135    InvalidFlagOption(char),
136}
137
138impl Display for RDFRegex {
139    /// Formats the regex in a compact notation showing pattern and flags.
140    ///
141    /// # Output format
142    /// - `/pattern/flags`
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(f, "/{}/{}", self.regex.as_str(), self.flags.as_deref().unwrap_or(""))
145    }
146}