// Copyright 2021-2023 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! String helpers. use std::borrow::Borrow; use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt; use std::fmt::Debug; use std::ops::Deref; use either::Either; use thiserror::Error; /// Error occurred during pattern string parsing. #[derive(Debug, Error)] pub enum StringPatternParseError { /// Unknown pattern kind is specified. #[error("Invalid string pattern kind `{0}:`")] InvalidKind(String), /// Failed to parse glob pattern. #[error(transparent)] GlobPattern(glob::PatternError), /// Failed to parse regular expression. #[error(transparent)] Regex(regex::Error), } /// A wrapper for [`glob::Pattern`] with a more concise Debug impl #[derive(Clone)] pub struct GlobPattern(pub glob::Pattern); impl GlobPattern { fn as_str(&self) -> &str { self.0.as_str() } } impl Debug for GlobPattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("GlobPattern").field(&self.as_str()).finish() } } fn parse_glob(src: &str) -> Result { glob::Pattern::new(src) .map(GlobPattern) .map_err(StringPatternParseError::GlobPattern) } /// Pattern to be tested against string property like commit description or /// bookmark name. #[derive(Clone, Debug)] pub enum StringPattern { /// Matches strings exactly. Exact(String), /// Matches strings case‐insensitively. ExactI(String), /// Matches strings that contain a substring. Substring(String), /// Matches strings that case‐insensitively contain a substring. SubstringI(String), /// Matches with a Unix‐style shell wildcard pattern. Glob(GlobPattern), /// Matches with a case‐insensitive Unix‐style shell wildcard pattern. GlobI(GlobPattern), /// Matches substrings with a regular expression. Regex(regex::Regex), /// Matches substrings with a case‐insensitive regular expression. RegexI(regex::Regex), } impl StringPattern { /// Pattern that matches any string. pub const fn everything() -> Self { StringPattern::Substring(String::new()) } /// Parses the given string as a [`StringPattern`]. Everything before the /// first ":" is considered the string's prefix. If the prefix is /// "exact[-i]:", "glob[-i]:", or "substring[-i]:", a pattern of the /// specified kind is returned. Returns an error if the string has an /// unrecognized prefix. Otherwise, a `StringPattern::Exact` is /// returned. pub fn parse(src: &str) -> Result { if let Some((kind, pat)) = src.split_once(':') { StringPattern::from_str_kind(pat, kind) } else { Ok(StringPattern::exact(src)) } } /// Constructs a pattern that matches exactly. pub fn exact(src: impl Into) -> Self { StringPattern::Exact(src.into()) } /// Constructs a pattern that matches case‐insensitively. pub fn exact_i(src: impl Into) -> Self { StringPattern::ExactI(src.into()) } /// Constructs a pattern that matches a substring. pub fn substring(src: impl Into) -> Self { StringPattern::Substring(src.into()) } /// Constructs a pattern that case‐insensitively matches a substring. pub fn substring_i(src: impl Into) -> Self { StringPattern::SubstringI(src.into()) } /// Parses the given string as a glob pattern. pub fn glob(src: &str) -> Result { // TODO: might be better to do parsing and compilation separately since // not all backends would use the compiled pattern object. // TODO: if no meta character found, it can be mapped to Exact. Ok(StringPattern::Glob(parse_glob(src)?)) } /// Parses the given string as a case‐insensitive glob pattern. pub fn glob_i(src: &str) -> Result { Ok(StringPattern::GlobI(parse_glob(src)?)) } /// Parses the given string as a regular expression. pub fn regex(src: &str) -> Result { let pattern = regex::Regex::new(src).map_err(StringPatternParseError::Regex)?; Ok(StringPattern::Regex(pattern)) } /// Parses the given string as a case-insensitive regular expression. pub fn regex_i(src: &str) -> Result { let pattern = regex::RegexBuilder::new(src) .case_insensitive(true) .build() .map_err(StringPatternParseError::Regex)?; Ok(StringPattern::RegexI(pattern)) } /// Parses the given string as a pattern of the specified `kind`. pub fn from_str_kind(src: &str, kind: &str) -> Result { match kind { "exact" => Ok(StringPattern::exact(src)), "exact-i" => Ok(StringPattern::exact_i(src)), "substring" => Ok(StringPattern::substring(src)), "substring-i" => Ok(StringPattern::substring_i(src)), "glob" => StringPattern::glob(src), "glob-i" => StringPattern::glob_i(src), "regex" => StringPattern::regex(src), "regex-i" => StringPattern::regex_i(src), _ => Err(StringPatternParseError::InvalidKind(kind.to_owned())), } } /// Returns true if this pattern matches input strings exactly. pub fn is_exact(&self) -> bool { self.as_exact().is_some() } /// Returns a literal pattern if this should match input strings exactly. /// /// This can be used to optimize map lookup by exact key. pub fn as_exact(&self) -> Option<&str> { // TODO: Handle trivial case‐insensitive patterns here? It might make people // expect they can use case‐insensitive patterns in contexts where they // generally can’t. match self { StringPattern::Exact(literal) => Some(literal), _ => None, } } /// Returns the original string of this pattern. pub fn as_str(&self) -> &str { match self { StringPattern::Exact(literal) => literal, StringPattern::ExactI(literal) => literal, StringPattern::Substring(needle) => needle, StringPattern::SubstringI(needle) => needle, StringPattern::Glob(pattern) => pattern.as_str(), StringPattern::GlobI(pattern) => pattern.as_str(), StringPattern::Regex(pattern) => pattern.as_str(), StringPattern::RegexI(pattern) => pattern.as_str(), } } /// Converts this pattern to a glob string. Returns `None` if the pattern /// can't be represented as a glob. pub fn to_glob(&self) -> Option> { // TODO: Handle trivial case‐insensitive patterns here? It might make people // expect they can use case‐insensitive patterns in contexts where they // generally can’t. match self { StringPattern::Exact(literal) => Some(glob::Pattern::escape(literal).into()), StringPattern::Substring(needle) => { if needle.is_empty() { Some("*".into()) } else { Some(format!("*{}*", glob::Pattern::escape(needle)).into()) } } StringPattern::Glob(pattern) => Some(pattern.as_str().into()), StringPattern::ExactI(_) => None, StringPattern::SubstringI(_) => None, StringPattern::GlobI(_) => None, StringPattern::Regex(_) => None, StringPattern::RegexI(_) => None, } } /// Returns true if this pattern matches the `haystack`. /// /// When matching against a case‐insensitive pattern, only ASCII case /// differences are currently folded. This may change in the future. pub fn matches(&self, haystack: &str) -> bool { // TODO: Unicode case folding is complicated and can be // locale‐specific. The `glob` crate and Gitoxide only deal with ASCII // case folding, so we do the same here; a more elaborate case folding // system will require making sure those behave in a matching manner // where relevant. That said, regex patterns are unicode-aware by // default, so we already have some inconsistencies. // // Care will need to be taken regarding normalization and the choice of an // appropriate case‐insensitive comparison scheme (`toNFKC_Casefold`?) to ensure // that it is compatible with the standard case‐insensitivity of haystack // components (like internationalized domain names in email addresses). The // availability of normalization and case folding schemes in database backends // will also need to be considered. A locale‐specific case folding // scheme would likely not be appropriate for Jujutsu. // // For some discussion of this topic, see: // match self { StringPattern::Exact(literal) => haystack == literal, StringPattern::ExactI(literal) => haystack.eq_ignore_ascii_case(literal), StringPattern::Substring(needle) => haystack.contains(needle), StringPattern::SubstringI(needle) => haystack .to_ascii_lowercase() .contains(&needle.to_ascii_lowercase()), StringPattern::Glob(pattern) => pattern.0.matches(haystack), StringPattern::GlobI(pattern) => pattern.0.matches_with( haystack, glob::MatchOptions { case_sensitive: false, ..glob::MatchOptions::new() }, ), // Regex and RegexI are identical here, but callers might want to // translate these to backend-specific query differently. StringPattern::Regex(pattern) => pattern.is_match(haystack), StringPattern::RegexI(pattern) => pattern.is_match(haystack), } } /// Iterates entries of the given `map` whose string keys match this /// pattern. pub fn filter_btree_map<'a, 'b, K: Borrow + Ord, V>( &'b self, map: &'a BTreeMap, ) -> impl Iterator + use<'a, 'b, K, V> { self.filter_btree_map_with(map, |key| key, |key| key) } /// Iterates entries of the given `map` whose string-like keys match this /// pattern. /// /// The borrowed key type is constrained by the `Deref::Target`. It must be /// convertible to/from `str`. pub fn filter_btree_map_as_deref<'a, 'b, K, V>( &'b self, map: &'a BTreeMap, ) -> impl Iterator + use<'a, 'b, K, V> where K: Borrow + Deref + Ord, K::Target: AsRef + Ord, str: AsRef, { self.filter_btree_map_with(map, AsRef::as_ref, AsRef::as_ref) } fn filter_btree_map_with<'a, 'b, K, Q, V, FromKey, ToKey>( &'b self, map: &'a BTreeMap, from_key: FromKey, to_key: ToKey, // TODO: Q, FromKey, and ToKey don't have to be captured, but // "currently, all type parameters are required to be mentioned in the // precise captures list" as of rustc 1.85.0. ) -> impl Iterator + use<'a, 'b, K, Q, V, FromKey, ToKey> where K: Borrow + Ord, Q: Ord + ?Sized, FromKey: Fn(&Q) -> &str, ToKey: Fn(&str) -> &Q, { if let Some(key) = self.as_exact() { Either::Left(map.get_key_value(to_key(key)).into_iter()) } else { Either::Right( map.iter() .filter(move |&(key, _)| self.matches(from_key(key.borrow()))), ) } } } impl fmt::Display for StringPattern { /// Shows the original string of this pattern. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_str()) } } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; #[test] fn test_string_pattern_to_glob() { assert_eq!(StringPattern::everything().to_glob(), Some("*".into())); assert_eq!(StringPattern::exact("a").to_glob(), Some("a".into())); assert_eq!(StringPattern::exact("*").to_glob(), Some("[*]".into())); assert_eq!( StringPattern::glob("*").unwrap().to_glob(), Some("*".into()) ); assert_eq!( StringPattern::Substring("a".into()).to_glob(), Some("*a*".into()) ); assert_eq!( StringPattern::Substring("*".into()).to_glob(), Some("*[*]*".into()) ); } #[test] fn test_parse() { // Parse specific pattern kinds. assert_matches!( StringPattern::parse("exact:foo"), Ok(StringPattern::Exact(s)) if s == "foo" ); assert_matches!( StringPattern::from_str_kind("foo", "exact"), Ok(StringPattern::Exact(s)) if s == "foo" ); assert_matches!( StringPattern::parse("glob:foo*"), Ok(StringPattern::Glob(p)) if p.as_str() == "foo*" ); assert_matches!( StringPattern::from_str_kind("foo*", "glob"), Ok(StringPattern::Glob(p)) if p.as_str() == "foo*" ); assert_matches!( StringPattern::parse("substring:foo"), Ok(StringPattern::Substring(s)) if s == "foo" ); assert_matches!( StringPattern::from_str_kind("foo", "substring"), Ok(StringPattern::Substring(s)) if s == "foo" ); assert_matches!( StringPattern::parse("substring-i:foo"), Ok(StringPattern::SubstringI(s)) if s == "foo" ); assert_matches!( StringPattern::from_str_kind("foo", "substring-i"), Ok(StringPattern::SubstringI(s)) if s == "foo" ); assert_matches!( StringPattern::parse("regex:foo"), Ok(StringPattern::Regex(p)) if p.as_str() == "foo" ); assert_matches!( StringPattern::from_str_kind("foo", "regex"), Ok(StringPattern::Regex(p)) if p.as_str() == "foo" ); assert_matches!( StringPattern::parse("regex-i:foo"), Ok(StringPattern::RegexI(p)) if p.as_str() == "foo" ); assert_matches!( StringPattern::from_str_kind("foo", "regex-i"), Ok(StringPattern::RegexI(p)) if p.as_str() == "foo" ); // Parse a pattern that contains a : itself. assert_matches!( StringPattern::parse("exact:foo:bar"), Ok(StringPattern::Exact(s)) if s == "foo:bar" ); // If no kind is specified, the input is treated as an exact pattern. assert_matches!( StringPattern::parse("foo"), Ok(StringPattern::Exact(s)) if s == "foo" ); // Parsing an unknown prefix results in an error. assert_matches!( StringPattern::parse("unknown-prefix:foo"), Err(StringPatternParseError::InvalidKind(_)) ); } }