use fluent_uri::Uri;
use lalrpop_util::ParseError as LalrParseError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{Display, Formatter};
use thiserror::Error;
pub const MAX_FILTER_DEPTH: usize = 64;
#[derive(Debug, Error)]
pub enum FilterActionError {
#[error("attrPath '{0}' has more than one sub-attribute segment")]
InvalidAttrPath(String),
#[error("invalid comparison value: {0}")]
InvalidCompValue(#[from] serde_json::Error),
#[error("filter nesting depth exceeds maximum of {MAX_FILTER_DEPTH} (at depth {0})")]
DepthExceeded(usize),
}
pub type ParseError = LalrParseError<usize, String, FilterActionError>;
#[derive(Debug, Error)]
#[error("invalid SCIM filter {raw:?}: {error}")]
pub struct InvalidFilterError {
pub raw: String,
pub error: ParseError,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
Attr(AttrExp),
ValuePath(ValuePath),
Not(Box<Filter>),
And(Box<Filter>, Box<Filter>),
Or(Box<Filter>, Box<Filter>),
}
impl Display for Filter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Filter::Attr(e) => write!(f, "{e}"),
Filter::ValuePath(vp) => write!(f, "{}[{}]", vp.attr, vp.filter),
Filter::Not(inner) => write!(f, "not ({inner})"),
Filter::And(lhs, rhs) => {
fmt_and_operand_filter(lhs, f)?;
write!(f, " and ")?;
fmt_and_operand_filter(rhs, f)
}
Filter::Or(lhs, rhs) => write!(f, "{lhs} or {rhs}"),
}
}
}
fn fmt_and_operand_filter(operand: &Filter, f: &mut Formatter<'_>) -> std::fmt::Result {
if matches!(operand, Filter::Or(_, _)) {
write!(f, "({operand})")
} else {
write!(f, "{operand}")
}
}
impl Serialize for Filter {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Filter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug)]
pub enum MaybeFilter {
Valid(Filter),
Invalid(InvalidFilterError),
}
impl<'de> Deserialize<'de> for MaybeFilter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.parse::<Filter>() {
Ok(f) => Ok(MaybeFilter::Valid(f)),
Err(e) => Ok(MaybeFilter::Invalid(InvalidFilterError {
raw: s,
error: e,
})),
}
}
}
impl std::str::FromStr for Filter {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = crate::filter_parser::FilterParser::new()
.parse(s.trim())
.map_err(|e| e.map_token(|t| t.to_string()))?;
if let Some(depth) = filter_depth_exceeds(&parsed, MAX_FILTER_DEPTH) {
drop_filter_iteratively(parsed);
return Err(LalrParseError::User {
error: FilterActionError::DepthExceeded(depth),
});
}
Ok(parsed)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValuePath {
pub attr: AttrPath,
pub filter: Box<ValFilter>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValFilter {
Attr(AttrExp),
Not(Box<ValFilter>),
And(Box<ValFilter>, Box<ValFilter>),
Or(Box<ValFilter>, Box<ValFilter>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AttrExp {
Present(AttrPath),
Comparison(AttrPath, CompareOp, CompValue),
}
#[derive(Debug, Clone, PartialEq)]
pub struct AttrPath {
pub uri: Option<String>,
pub name: String,
pub sub_attr: Option<String>,
}
impl AttrPath {
pub fn with_name(name: impl Into<String>) -> Self {
Self {
uri: None,
name: name.into(),
sub_attr: None,
}
}
pub fn with_sub_attr(name: impl Into<String>, sub_attr: impl Into<String>) -> Self {
Self {
uri: None,
name: name.into(),
sub_attr: Some(sub_attr.into()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompareOp {
Eq,
Ne,
Co,
Sw,
Ew,
Gt,
Lt,
Ge,
Le,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompValue {
False,
Null,
True,
Number(serde_json::Number),
Str(String),
}
impl From<String> for CompValue {
fn from(s: String) -> Self {
CompValue::Str(s)
}
}
impl From<&str> for CompValue {
fn from(s: &str) -> Self {
CompValue::Str(s.to_owned())
}
}
pub(crate) fn parse_attr_path(s: &str) -> Result<AttrPath, FilterActionError> {
let (uri, rest) = if let Some((before, after)) = s.rsplit_once(':') {
if Uri::parse(before).is_ok() {
(Some(before.to_owned()), after)
} else {
(None, s)
}
} else {
(None, s)
};
if rest.chars().filter(|&c| c == '.').count() > 1 {
return Err(FilterActionError::InvalidAttrPath(s.to_string()));
}
let (name, sub_attr) = rest
.split_once('.')
.map_or((rest, None), |(name, sub_attr)| {
(name, Some(sub_attr.to_string()))
});
Ok(AttrPath {
uri,
name: name.to_string(),
sub_attr,
})
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatchPath {
Attr(AttrPath),
Value(PatchValuePath),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PatchValuePath {
pub attr: AttrPath,
pub filter: ValFilter,
pub sub_attr: Option<String>,
}
impl Display for AttrPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(uri) = &self.uri {
write!(f, "{uri}:")?;
}
write!(f, "{}", self.name)?;
if let Some(sub) = &self.sub_attr {
write!(f, ".{sub}")?;
}
Ok(())
}
}
impl Display for CompareOp {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
CompareOp::Eq => "eq",
CompareOp::Ne => "ne",
CompareOp::Co => "co",
CompareOp::Sw => "sw",
CompareOp::Ew => "ew",
CompareOp::Gt => "gt",
CompareOp::Lt => "lt",
CompareOp::Ge => "ge",
CompareOp::Le => "le",
};
write!(f, "{s}")
}
}
impl Display for CompValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CompValue::False => write!(f, "false"),
CompValue::Null => write!(f, "null"),
CompValue::True => write!(f, "true"),
CompValue::Number(n) => write!(f, "{n}"),
CompValue::Str(s) => {
let encoded = serde_json::to_string(s).expect("string serialization never fails");
write!(f, "{encoded}")
}
}
}
}
impl Display for AttrExp {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AttrExp::Present(path) => write!(f, "{path} pr"),
AttrExp::Comparison(path, op, val) => write!(f, "{path} {op} {val}"),
}
}
}
impl Display for ValFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ValFilter::Attr(e) => write!(f, "{e}"),
ValFilter::Not(inner) => write!(f, "not ({inner})"),
ValFilter::And(lhs, rhs) => {
fmt_and_operand_val_filter(lhs, f)?;
write!(f, " and ")?;
fmt_and_operand_val_filter(rhs, f)
}
ValFilter::Or(lhs, rhs) => write!(f, "{lhs} or {rhs}"),
}
}
}
fn fmt_and_operand_val_filter(operand: &ValFilter, f: &mut Formatter<'_>) -> std::fmt::Result {
if matches!(operand, ValFilter::Or(_, _)) {
write!(f, "({operand})")
} else {
write!(f, "{operand}")
}
}
impl Display for PatchValuePath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}[{}]", self.attr, self.filter)?;
if let Some(sub) = &self.sub_attr {
write!(f, ".{sub}")?;
}
Ok(())
}
}
impl Display for PatchPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PatchPath::Attr(p) => write!(f, "{p}"),
PatchPath::Value(vp) => write!(f, "{vp}"),
}
}
}
impl Serialize for PatchPath {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PatchPath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl std::str::FromStr for PatchPath {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = crate::filter_parser::PathParser::new()
.parse(s.trim())
.map_err(|e| e.map_token(|t| t.to_string()))?;
match parsed {
PatchPath::Attr(a) => Ok(PatchPath::Attr(a)),
PatchPath::Value(vp) => match val_filter_depth_exceeds(&vp.filter, MAX_FILTER_DEPTH) {
Some(depth) => {
drop_val_filter_iteratively(vp.filter);
Err(LalrParseError::User {
error: FilterActionError::DepthExceeded(depth),
})
}
None => Ok(PatchPath::Value(vp)),
},
}
}
}
fn filter_depth_exceeds(root: &Filter, limit: usize) -> Option<usize> {
let mut worklist: Vec<(&Filter, usize)> = vec![(root, 1)];
while let Some((node, depth)) = worklist.pop() {
if depth > limit {
return Some(depth);
}
match node {
Filter::Attr(_) => {}
Filter::ValuePath(vp) => {
if let Some(d) = val_filter_depth_exceeds(&vp.filter, limit) {
return Some(d);
}
}
Filter::Not(inner) => worklist.push((inner, depth + 1)),
Filter::And(lhs, rhs) | Filter::Or(lhs, rhs) => {
worklist.push((lhs, depth + 1));
worklist.push((rhs, depth + 1));
}
}
}
None
}
fn val_filter_depth_exceeds(root: &ValFilter, limit: usize) -> Option<usize> {
let mut worklist: Vec<(&ValFilter, usize)> = vec![(root, 1)];
while let Some((node, depth)) = worklist.pop() {
if depth > limit {
return Some(depth);
}
match node {
ValFilter::Attr(_) => {}
ValFilter::Not(inner) => worklist.push((inner, depth + 1)),
ValFilter::And(lhs, rhs) | ValFilter::Or(lhs, rhs) => {
worklist.push((lhs, depth + 1));
worklist.push((rhs, depth + 1));
}
}
}
None
}
fn drop_filter_iteratively(root: Filter) {
let mut stack: Vec<Filter> = vec![root];
while let Some(node) = stack.pop() {
match node {
Filter::Attr(_) => {}
Filter::ValuePath(vp) => drop_val_filter_iteratively(*vp.filter),
Filter::Not(inner) => stack.push(*inner),
Filter::And(lhs, rhs) | Filter::Or(lhs, rhs) => {
stack.push(*lhs);
stack.push(*rhs);
}
}
}
}
fn drop_val_filter_iteratively(root: ValFilter) {
let mut stack: Vec<ValFilter> = vec![root];
while let Some(node) = stack.pop() {
match node {
ValFilter::Attr(_) => {}
ValFilter::Not(inner) => stack.push(*inner),
ValFilter::And(lhs, rhs) | ValFilter::Or(lhs, rhs) => {
stack.push(*lhs);
stack.push(*rhs);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test]
fn test_scim_filter_simple_eq() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"userName eq "bjensen""#)
.unwrap();
assert_eq!(
f,
Filter::Attr(AttrExp::Comparison(
AttrPath {
uri: None,
name: "userName".into(),
sub_attr: None
},
CompareOp::Eq,
CompValue::Str("bjensen".into()),
))
);
}
#[test]
fn test_scim_filter_case_insensitive_op() {
let f1 = crate::filter_parser::FilterParser::new()
.parse(r#"userName Eq "john""#)
.unwrap();
let f2 = crate::filter_parser::FilterParser::new()
.parse(r#"userName eq "john""#)
.unwrap();
assert_eq!(f1, f2);
}
#[test]
fn test_scim_filter_pr() {
let f = crate::filter_parser::FilterParser::new()
.parse("title pr")
.unwrap();
assert_eq!(
f,
Filter::Attr(AttrExp::Present(AttrPath {
uri: None,
name: "title".into(),
sub_attr: None,
}))
);
}
#[test]
fn test_scim_filter_sub_attr() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"name.familyName co "O'Malley""#)
.unwrap();
assert_eq!(
f,
Filter::Attr(AttrExp::Comparison(
AttrPath {
uri: None,
name: "name".into(),
sub_attr: Some("familyName".into()),
},
CompareOp::Co,
CompValue::Str("O'Malley".into()),
))
);
}
#[test]
fn test_scim_filter_uri_prefix() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J""#)
.unwrap();
assert_eq!(
f,
Filter::Attr(AttrExp::Comparison(
AttrPath {
uri: Some("urn:ietf:params:scim:schemas:core:2.0:User".into()),
name: "userName".into(),
sub_attr: None,
},
CompareOp::Sw,
CompValue::Str("J".into()),
))
);
}
#[test]
fn test_scim_filter_and() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"title pr and userType eq "Employee""#)
.unwrap();
assert_eq!(
f,
Filter::And(
Box::new(Filter::Attr(AttrExp::Present(AttrPath {
uri: None,
name: "title".into(),
sub_attr: None,
}))),
Box::new(Filter::Attr(AttrExp::Comparison(
AttrPath {
uri: None,
name: "userType".into(),
sub_attr: None
},
CompareOp::Eq,
CompValue::Str("Employee".into()),
))),
)
);
}
#[test]
fn test_scim_filter_or() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"title pr or userType eq "Intern""#)
.unwrap();
assert_eq!(
f,
Filter::Or(
Box::new(Filter::Attr(AttrExp::Present(AttrPath {
uri: None,
name: "title".into(),
sub_attr: None,
}))),
Box::new(Filter::Attr(AttrExp::Comparison(
AttrPath {
uri: None,
name: "userType".into(),
sub_attr: None
},
CompareOp::Eq,
CompValue::Str("Intern".into()),
))),
)
);
}
#[test]
fn test_scim_filter_and_precedence_over_or() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"title pr and userType eq "Employee" or emails pr"#)
.unwrap();
assert!(matches!(f, Filter::Or(_, _)));
if let Filter::Or(left, _) = f {
assert!(matches!(*left, Filter::And(_, _)));
}
}
#[test]
fn test_scim_filter_not() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"not (emails co "example.com")"#)
.unwrap();
assert!(matches!(f, Filter::Not(_)));
}
#[test]
fn test_scim_filter_grouping() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")"#)
.unwrap();
assert!(matches!(f, Filter::And(_, _)));
}
#[test]
fn test_scim_filter_value_path() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"emails[type eq "work" and value co "@example.com"]"#)
.unwrap();
assert!(matches!(f, Filter::ValuePath(_)));
if let Filter::ValuePath(vp) = f {
assert_eq!(vp.attr.name, "emails");
assert!(matches!(*vp.filter, ValFilter::And(_, _)));
}
}
#[test_case(r#"userName eq "bjensen""# ; "simple_eq")]
#[test_case(r#"name.familyName co "O'Malley""# ; "sub_attr_co")]
#[test_case(r#"userName sw "J""# ; "sw")]
#[test_case(r#"urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J""# ; "uri_prefix_sw")]
#[test_case("title pr" ; "pr")]
#[test_case(r#"meta.lastModified gt "2011-05-13T04:42:34Z""# ; "datetime_gt")]
#[test_case(r#"meta.lastModified ge "2011-05-13T04:42:34Z""# ; "datetime_ge")]
#[test_case(r#"meta.lastModified lt "2011-05-13T04:42:34Z""# ; "datetime_lt")]
#[test_case(r#"meta.lastModified le "2011-05-13T04:42:34Z""# ; "datetime_le")]
#[test_case(r#"title pr and userType eq "Employee""# ; "and")]
#[test_case(r#"title pr or userType eq "Intern""# ; "or")]
#[test_case(r#"schemas eq "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User""# ; "urn_value")]
#[test_case(r#"userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")"# ; "and_grouped_or")]
#[test_case(r#"userType ne "Employee" and not (emails co "example.com" or emails.value co "example.org")"# ; "and_not_grouped_or")]
#[test_case(r#"userType eq "Employee" and (emails.type eq "work")"# ; "and_grouped_sub_attr")]
#[test_case(r#"userType eq "Employee" and emails[type eq "work" and value co "@example.com"]"# ; "and_value_path")]
#[test_case(r#"emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]"# ; "two_value_paths_or")]
fn filter_parses_and_round_trips(s: &str) {
let f: Filter = s.parse().unwrap_or_else(|e| panic!("parse {s:?}: {e:?}"));
let displayed = f.to_string();
let reparsed: Filter = displayed
.parse()
.unwrap_or_else(|e| panic!("re-parse of {displayed:?}: {e:?}"));
assert_eq!(f, reparsed, "round-trip mismatch for {s:?}");
}
#[test]
fn test_scim_patch_path_attr() {
let p = crate::filter_parser::PathParser::new()
.parse("userName")
.unwrap();
assert_eq!(
p,
PatchPath::Attr(AttrPath {
uri: None,
name: "userName".into(),
sub_attr: None
})
);
}
#[test]
fn test_scim_patch_path_sub_attr() {
let p = crate::filter_parser::PathParser::new()
.parse("name.familyName")
.unwrap();
assert_eq!(
p,
PatchPath::Attr(AttrPath {
uri: None,
name: "name".into(),
sub_attr: Some("familyName".into()),
})
);
}
#[test]
fn test_scim_patch_path_value_path() {
let p = crate::filter_parser::PathParser::new()
.parse(r#"emails[type eq "work"]"#)
.unwrap();
let PatchPath::Value(vp) = p else {
panic!("expected PatchPath::Value");
};
assert_eq!(vp.attr.name, "emails");
assert!(vp.sub_attr.is_none());
}
#[test]
fn test_scim_patch_path_value_path_sub_attr() {
let p = crate::filter_parser::PathParser::new()
.parse(r#"emails[type eq "work"].value"#)
.unwrap();
let PatchPath::Value(vp) = p else {
panic!("expected PatchPath::Value");
};
assert_eq!(vp.attr.name, "emails");
assert_eq!(vp.sub_attr.as_deref(), Some("value"));
}
#[test_case(r#"emails[type eq "work"].value.extra"# ; "dotted_sub_attr")]
#[test_case(r#"emails[type eq "work"].urn:foo:bar"# ; "trailing_urn")]
#[test_case("name.family.given" ; "too_many_sub_attrs")]
fn path_rejected(s: &str) {
assert!(crate::filter_parser::PathParser::new().parse(s).is_err());
}
#[test_case(r#"userName eq "\q""# ; "unknown_escape")]
#[test_case(r#"userName eq "\uGHIJ""# ; "invalid_unicode_escape")]
fn filter_comp_value_rejected(s: &str) {
assert!(crate::filter_parser::FilterParser::new().parse(s).is_err());
}
#[test_case("name." ; "empty_sub_attr")]
#[test_case("name.family.given pr" ; "too_many_sub_attrs")]
#[test_case(r#"emails[name.family.given eq "x"]"# ; "value_path_inner_too_many_sub_attrs")]
fn invalid_attr_path_rejected(s: &str) {
assert!(crate::filter_parser::FilterParser::new().parse(s).is_err());
}
#[test_case(r#"userName eq"bjensen""# ; "missing_space_before_comp_value")]
#[test_case(r#"emails[type eq"work"]"# ; "missing_space_in_value_path")]
#[test_case(r#"title prand userType eq "Employee""# ; "missing_space_before_and")]
fn filter_requires_required_spaces(s: &str) {
assert!(crate::filter_parser::FilterParser::new().parse(s).is_err());
}
#[test]
fn test_comp_value_surrogate_pair_handled() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"userName eq "\uD800\uDC00""#)
.unwrap();
if let Filter::Attr(AttrExp::Comparison(_, _, CompValue::Str(s))) = f {
assert_eq!(s, "\u{10000}");
} else {
panic!("unexpected parse result");
}
}
#[test]
fn test_scim_patch_path_complex_filter() {
let p = crate::filter_parser::PathParser::new()
.parse(r#"emails[type eq "work" and value co "@example.com"].value"#)
.unwrap();
let PatchPath::Value(vp) = p else {
panic!("expected PatchPath::Value");
};
let ValFilter::And(_, _) = vp.filter else {
panic!("expected ValFilter::And, got {:?}", vp.filter);
};
assert_eq!(vp.sub_attr.as_deref(), Some("value"));
}
#[test]
fn test_val_filter_parenthesized_grouping() {
let f = crate::filter_parser::FilterParser::new()
.parse(r#"emails[(type eq "work") and value pr]"#)
.unwrap();
assert!(matches!(f, Filter::ValuePath(_)));
if let Filter::ValuePath(vp) = f {
assert!(matches!(*vp.filter, ValFilter::And(_, _)));
}
}
#[test]
fn test_filter_from_str() {
let f: Filter = r#"userName eq "bjensen""#.parse().unwrap();
assert!(matches!(f, Filter::Attr(AttrExp::Comparison(..))));
}
#[test]
fn test_patch_path_from_str() {
let p: PatchPath = r#"emails[type eq "work"].value"#.parse().unwrap();
assert!(matches!(p, PatchPath::Value(_)));
}
#[test]
fn test_filter_from_str_error() {
assert!(r#"not a valid filter !!!"#.parse::<Filter>().is_err());
}
#[test_case("userName" ; "simple_attr")]
#[test_case("name.familyName" ; "sub_attr")]
#[test_case(r#"emails[type eq "work"]"# ; "value_path")]
#[test_case(r#"emails[type eq "work"].value"# ; "value_path_sub_attr")]
#[test_case(r#"emails[type eq "work" and value co "@example.com"].value"# ; "complex_filter")]
fn patch_path_round_trips(s: &str) {
let p: PatchPath = s.parse().unwrap_or_else(|e| panic!("parse {s:?}: {e:?}"));
let displayed = p.to_string();
let reparsed: PatchPath = displayed
.parse()
.unwrap_or_else(|e| panic!("re-parse of {displayed:?}: {e:?}"));
assert_eq!(p, reparsed, "round-trip mismatch for {s:?}");
}
#[test]
fn test_display_attr_path() {
let p = AttrPath {
uri: Some("urn:ietf:params:scim:schemas:core:2.0:User".into()),
name: "userName".into(),
sub_attr: None,
};
assert_eq!(
p.to_string(),
"urn:ietf:params:scim:schemas:core:2.0:User:userName"
);
}
#[test]
fn test_display_comp_value_string_escaping() {
let v = CompValue::Str("foo\nbar".into());
assert_eq!(v.to_string(), r#""foo\nbar""#);
}
#[test]
fn test_display_and_wraps_or_children() {
let f: Filter = r#"(title pr or userType eq "Intern") and emails pr"#
.parse()
.unwrap();
let s = f.to_string();
let reparsed: Filter = s.parse().unwrap();
assert_eq!(f, reparsed);
assert!(s.contains('('), "expected parens in {s:?}");
}
fn assert_depth_exceeded<T: std::fmt::Debug>(res: Result<T, ParseError>) {
match res {
Err(LalrParseError::User {
error: FilterActionError::DepthExceeded(_),
}) => {}
Err(other) => panic!("expected ParseError::User(DepthExceeded), got {other:?}"),
Ok(ok) => panic!("expected rejection, but parse succeeded: {ok:?}"),
}
}
fn not_chain(depth: usize) -> String {
assert!(depth >= 1);
let nots = depth - 1;
format!("{}title pr{}", "not (".repeat(nots), ")".repeat(nots))
}
fn val_not_chain(depth: usize) -> String {
assert!(depth >= 1);
let nots = depth - 1;
format!(
r#"{}type eq "work"{}"#,
"not (".repeat(nots),
")".repeat(nots)
)
}
#[test]
fn not_chain_within_limit_parses() {
let s = not_chain(MAX_FILTER_DEPTH);
s.parse::<Filter>()
.unwrap_or_else(|e| panic!("expected success at depth {MAX_FILTER_DEPTH}: {e:?}"));
}
#[test]
fn not_chain_exceeding_limit_rejected() {
let s = not_chain(MAX_FILTER_DEPTH + 5);
assert_depth_exceeded(s.parse::<Filter>());
}
#[test]
fn and_chain_within_limit_parses() {
let n = MAX_FILTER_DEPTH;
let mut s = String::from("title pr");
for _ in 0..n - 1 {
s.push_str(" and title pr");
}
s.parse::<Filter>()
.unwrap_or_else(|e| panic!("expected success with {n} leaves: {e:?}"));
}
#[test]
fn and_chain_exceeding_limit_rejected() {
let n = MAX_FILTER_DEPTH + 10;
let mut s = String::from("title pr");
for _ in 0..n - 1 {
s.push_str(" and title pr");
}
assert_depth_exceeded(s.parse::<Filter>());
}
#[test]
fn or_chain_exceeding_limit_rejected() {
let n = MAX_FILTER_DEPTH + 10;
let mut s = String::from("title pr");
for _ in 0..n - 1 {
s.push_str(" or title pr");
}
assert_depth_exceeded(s.parse::<Filter>());
}
#[test]
fn value_path_inner_depth_bounded() {
let s = format!("emails[{}]", val_not_chain(MAX_FILTER_DEPTH + 5));
assert_depth_exceeded(s.parse::<Filter>());
}
#[test]
fn patch_path_value_path_depth_bounded() {
let s = format!("emails[{}]", val_not_chain(MAX_FILTER_DEPTH + 5));
assert_depth_exceeded(s.parse::<PatchPath>());
}
#[test]
fn patch_path_plain_attr_not_affected() {
let p: PatchPath = "name.familyName".parse().unwrap();
assert!(matches!(p, PatchPath::Attr(_)));
}
#[test]
fn deserialize_rejects_deep_nesting() {
let raw = not_chain(MAX_FILTER_DEPTH + 5);
let json = serde_json::to_string(&raw).unwrap();
let err = serde_json::from_str::<Filter>(&json).unwrap_err();
assert!(
err.to_string().contains("depth"),
"expected depth error, got: {err}"
);
}
#[test]
fn display_round_trip_at_limit_does_not_overflow() {
let s = not_chain(MAX_FILTER_DEPTH);
let f: Filter = s.parse().unwrap();
let displayed = f.to_string();
let reparsed: Filter = displayed.parse().unwrap();
assert_eq!(f, reparsed);
let _ = format!("{f:?}");
let _ = serde_json::to_string(&f).unwrap();
}
#[test]
fn extremely_deep_input_rejected_without_panic() {
let s = not_chain(10_000);
assert_depth_exceeded(s.parse::<Filter>());
}
}