Skip to main content

steel_core/permission/
context.rs

1use std::{cmp::Ordering, error::Error, fmt};
2
3use steel_utils::Identifier;
4
5use super::{PermissionKeyError, PermissionSegment};
6
7/// Rule-side context in which a permission entry applies.
8#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub enum PermissionRuleContext {
10    /// Applies in every runtime context.
11    Global,
12    /// Applies within one server domain.
13    Domain(PermissionDomain),
14    /// Applies within one loaded world.
15    World(Identifier),
16    /// Applies when a subsystem-provided key has one value.
17    Custom {
18        /// Context key owned by Steel or a future plugin.
19        key: PermissionContextKey,
20        /// Required active value.
21        value: PermissionContextValue,
22    },
23    /// Applies when every nested context matches.
24    All(PermissionRuleContexts),
25}
26
27/// Validated Steel domain name used by permission contexts.
28#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct PermissionDomain(String);
30
31impl PermissionDomain {
32    /// Parses a domain name using the command-visible world namespace grammar.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error when the domain is not a valid identifier namespace.
37    pub fn parse(value: impl Into<String>) -> Result<Self, PermissionRuleContextError> {
38        let value = value.into();
39        if value.is_empty() || !Identifier::validate_namespace(&value) {
40            return Err(PermissionRuleContextError::InvalidDomain(value));
41        }
42        Ok(Self(value))
43    }
44
45    /// Returns the validated domain name.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl fmt::Display for PermissionDomain {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter.write_str(self.as_str())
55    }
56}
57
58/// Validated value in Steel's unquoted permission-context expression syntax.
59#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct PermissionContextValue(String);
61
62impl PermissionContextValue {
63    /// Parses one custom context value.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error when the value cannot round-trip through an expression.
68    pub fn parse(value: impl Into<String>) -> Result<Self, PermissionRuleContextError> {
69        let value = value.into();
70        if value.is_empty()
71            || value
72                .chars()
73                .any(|character| character.is_whitespace() || "{},=".contains(character))
74        {
75            return Err(PermissionRuleContextError::InvalidValue(value));
76        }
77        Ok(Self(value))
78    }
79
80    /// Returns the validated context value.
81    #[must_use]
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85}
86
87impl fmt::Display for PermissionContextValue {
88    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89        formatter.write_str(self.as_str())
90    }
91}
92
93/// Canonically ordered AND-chain of permission rule contexts.
94#[derive(Clone, Debug, PartialEq, Eq, Hash)]
95pub struct PermissionRuleContexts {
96    contexts: Vec<PermissionRuleContext>,
97}
98
99impl PermissionRuleContexts {
100    const fn new(contexts: Vec<PermissionRuleContext>) -> Self {
101        Self { contexts }
102    }
103
104    fn into_vec(self) -> Vec<PermissionRuleContext> {
105        self.contexts
106    }
107
108    /// Returns the canonical context sequence.
109    pub fn iter(&self) -> impl Iterator<Item = &PermissionRuleContext> {
110        self.contexts.iter()
111    }
112}
113
114impl PermissionRuleContext {
115    /// Returns the global context.
116    #[must_use]
117    pub const fn global() -> Self {
118        Self::Global
119    }
120
121    /// Creates a domain-scoped rule context.
122    pub fn domain(domain: impl Into<String>) -> Result<Self, PermissionRuleContextError> {
123        PermissionDomain::parse(domain).map(Self::Domain)
124    }
125
126    /// Creates a loaded-world rule context.
127    #[must_use]
128    pub const fn world(world: Identifier) -> Self {
129        Self::World(world)
130    }
131
132    /// Creates a custom rule context.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error when `value` is empty.
137    pub fn custom(
138        key: PermissionContextKey,
139        value: impl Into<String>,
140    ) -> Result<Self, PermissionRuleContextError> {
141        let value = PermissionContextValue::parse(value)?;
142        Ok(Self::Custom { key, value })
143    }
144
145    /// Creates a canonical AND-chain of rule contexts.
146    ///
147    /// Global entries are omitted, nested chains are flattened, and duplicate
148    /// values are idempotent. Conflicting values for one context key are rejected.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error when a built-in or custom key receives multiple values.
153    pub fn all(
154        contexts: impl IntoIterator<Item = Self>,
155    ) -> Result<Self, PermissionRuleContextError> {
156        let mut flattened = Vec::new();
157        for context in contexts {
158            match context {
159                Self::Global => {}
160                Self::All(contexts) => {
161                    for context in contexts.into_vec() {
162                        push_unique_context(&mut flattened, context)?;
163                    }
164                }
165                context => push_unique_context(&mut flattened, context)?,
166            }
167        }
168        normalize_world_domain(&mut flattened)?;
169        flattened.sort_by(compare_rule_contexts);
170
171        Ok(match flattened.len() {
172            0 => Self::Global,
173            1 => match flattened.pop() {
174                Some(context) => context,
175                None => Self::Global,
176            },
177            _ => Self::All(PermissionRuleContexts::new(flattened)),
178        })
179    }
180
181    pub(super) fn matches(&self, context: &PermissionContext) -> bool {
182        match self {
183            Self::Global => true,
184            Self::Domain(domain) => context.domain.as_ref() == Some(domain),
185            Self::World(world) => context.world.as_ref() == Some(world),
186            Self::Custom { .. } => context.custom_contexts.contains(self),
187            Self::All(contexts) => contexts
188                .iter()
189                .all(|constraint| constraint.matches(context)),
190        }
191    }
192
193    pub(super) fn specificity(&self) -> usize {
194        match self {
195            Self::Global => 0,
196            Self::Domain(_) | Self::Custom { .. } => 1,
197            Self::World(_) => 2,
198            Self::All(contexts) => contexts.iter().map(Self::specificity).sum(),
199        }
200    }
201}
202
203impl fmt::Display for PermissionRuleContext {
204    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            Self::Global => formatter.write_str("global"),
207            Self::Domain(domain) => write!(formatter, "domain {}", domain.as_str()),
208            Self::World(world) => write!(formatter, "world {world}"),
209            Self::Custom { key, value } => {
210                write!(formatter, "{} {}", key.as_str(), value.as_str())
211            }
212            Self::All(contexts) => {
213                for (index, context) in contexts.iter().enumerate() {
214                    if index != 0 {
215                        formatter.write_str(" + ")?;
216                    }
217                    write!(formatter, "{context}")?;
218                }
219                Ok(())
220            }
221        }
222    }
223}
224
225fn normalize_world_domain(
226    contexts: &mut Vec<PermissionRuleContext>,
227) -> Result<(), PermissionRuleContextError> {
228    let domain = contexts.iter().find_map(|context| match context {
229        PermissionRuleContext::Domain(domain) => Some(domain.as_str()),
230        _ => None,
231    });
232    let world_domain = contexts.iter().find_map(|context| match context {
233        PermissionRuleContext::World(world) => Some(world.namespace.as_ref()),
234        _ => None,
235    });
236    let (Some(domain), Some(world_domain)) = (domain, world_domain) else {
237        return Ok(());
238    };
239    if domain != world_domain {
240        return Err(PermissionRuleContextError::WorldDomainMismatch {
241            domain: domain.to_owned(),
242            world_domain: world_domain.to_owned(),
243        });
244    }
245    contexts.retain(|context| !matches!(context, PermissionRuleContext::Domain(_)));
246    Ok(())
247}
248
249fn push_unique_context(
250    contexts: &mut Vec<PermissionRuleContext>,
251    context: PermissionRuleContext,
252) -> Result<(), PermissionRuleContextError> {
253    for existing in contexts.iter() {
254        match (&context, existing) {
255            (PermissionRuleContext::Domain(value), PermissionRuleContext::Domain(current)) => {
256                if value == current {
257                    return Ok(());
258                }
259                return Err(PermissionRuleContextError::DuplicateDomain);
260            }
261            (PermissionRuleContext::World(value), PermissionRuleContext::World(current)) => {
262                if value == current {
263                    return Ok(());
264                }
265                return Err(PermissionRuleContextError::DuplicateWorld);
266            }
267            (
268                PermissionRuleContext::Custom { key, value },
269                PermissionRuleContext::Custom {
270                    key: current_key,
271                    value: current_value,
272                },
273            ) if key == current_key => {
274                if value == current_value {
275                    return Ok(());
276                }
277                return Err(PermissionRuleContextError::DuplicateCustomKey(key.clone()));
278            }
279            _ => {}
280        }
281    }
282    if !contexts.contains(&context) {
283        contexts.push(context);
284    }
285    Ok(())
286}
287
288fn compare_rule_contexts(left: &PermissionRuleContext, right: &PermissionRuleContext) -> Ordering {
289    rule_context_rank(left)
290        .cmp(&rule_context_rank(right))
291        .then_with(|| match (left, right) {
292            (PermissionRuleContext::Domain(left), PermissionRuleContext::Domain(right)) => {
293                left.cmp(right)
294            }
295            (PermissionRuleContext::World(left), PermissionRuleContext::World(right)) => left
296                .namespace
297                .cmp(&right.namespace)
298                .then_with(|| left.path.cmp(&right.path)),
299            (
300                PermissionRuleContext::Custom {
301                    key: left_key,
302                    value: left_value,
303                },
304                PermissionRuleContext::Custom {
305                    key: right_key,
306                    value: right_value,
307                },
308            ) => left_key
309                .as_str()
310                .cmp(right_key.as_str())
311                .then_with(|| left_value.cmp(right_value)),
312            _ => Ordering::Equal,
313        })
314}
315
316const fn rule_context_rank(context: &PermissionRuleContext) -> u8 {
317    match context {
318        PermissionRuleContext::Domain(_) => 0,
319        PermissionRuleContext::World(_) => 1,
320        PermissionRuleContext::Custom { .. } => 2,
321        PermissionRuleContext::Global => 3,
322        PermissionRuleContext::All(_) => 4,
323    }
324}
325
326/// Active runtime context used to evaluate permission rules.
327#[derive(Clone, Debug, Default, PartialEq, Eq)]
328pub struct PermissionContext {
329    domain: Option<PermissionDomain>,
330    world: Option<Identifier>,
331    custom_contexts: Vec<PermissionRuleContext>,
332}
333
334impl PermissionContext {
335    /// Creates a context with no active scopes.
336    #[must_use]
337    pub fn global() -> Self {
338        Self::default()
339    }
340
341    /// Creates a context for a loaded world and its owning domain.
342    #[must_use]
343    pub fn for_world(world: Identifier) -> Self {
344        Self {
345            domain: Some(PermissionDomain(world.namespace.to_string())),
346            world: Some(world),
347            custom_contexts: Vec::new(),
348        }
349    }
350
351    /// Builds the active context represented by one rule-side expression.
352    ///
353    /// World namespaces are Steel domain names, matching command-visible world identifiers.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if a custom context value conflicts with another value.
358    pub fn from_rule_context(
359        rule_context: &PermissionRuleContext,
360    ) -> Result<Self, PermissionRuleContextError> {
361        let mut context = Self::global();
362        context.append_rule_context(rule_context)?;
363        Ok(context)
364    }
365
366    fn append_rule_context(
367        &mut self,
368        rule_context: &PermissionRuleContext,
369    ) -> Result<(), PermissionRuleContextError> {
370        match rule_context {
371            PermissionRuleContext::Global => {}
372            PermissionRuleContext::Domain(domain) => {
373                self.domain = Some(domain.clone());
374            }
375            PermissionRuleContext::World(world) => {
376                self.domain = Some(PermissionDomain(world.namespace.to_string()));
377                self.world = Some(world.clone());
378            }
379            PermissionRuleContext::Custom { key, value } => {
380                self.add_custom_context(key.clone(), value.as_str())?;
381            }
382            PermissionRuleContext::All(contexts) => {
383                for context in contexts.iter() {
384                    self.append_rule_context(context)?;
385                }
386            }
387        }
388        Ok(())
389    }
390
391    /// Adds one custom active context.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error for an empty value or a conflicting value for the same key.
396    pub fn with_custom_context(
397        mut self,
398        key: PermissionContextKey,
399        value: impl Into<String>,
400    ) -> Result<Self, PermissionRuleContextError> {
401        self.add_custom_context(key, value)?;
402        Ok(self)
403    }
404
405    /// Adds one custom active context in place.
406    ///
407    /// # Errors
408    ///
409    /// Returns an error for an empty value or a conflicting value for the same key.
410    pub fn add_custom_context(
411        &mut self,
412        key: PermissionContextKey,
413        value: impl Into<String>,
414    ) -> Result<(), PermissionRuleContextError> {
415        let context = PermissionRuleContext::custom(key, value)?;
416        let PermissionRuleContext::Custom { key, value } = &context else {
417            return Ok(());
418        };
419        for existing in &self.custom_contexts {
420            let PermissionRuleContext::Custom {
421                key: existing_key,
422                value: existing_value,
423            } = existing
424            else {
425                continue;
426            };
427            if existing_key != key {
428                continue;
429            }
430            if existing_value == value {
431                return Ok(());
432            }
433            return Err(PermissionRuleContextError::DuplicateCustomKey(key.clone()));
434        }
435        self.custom_contexts.push(context);
436        Ok(())
437    }
438}
439
440/// One custom permission context key.
441#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
442pub struct PermissionContextKey(String);
443
444impl PermissionContextKey {
445    /// Parses a local name or namespaced plugin context key.
446    ///
447    /// # Errors
448    ///
449    /// Returns an error when the key is not a valid segment or identifier-like name.
450    pub fn parse(value: impl Into<String>) -> Result<Self, PermissionContextKeyError> {
451        let value = value.into();
452        if value.contains(':') {
453            validate_namespaced_context_key(&value)?;
454            return Ok(Self(value));
455        }
456        PermissionSegment::parse(value.clone())
457            .map_err(PermissionContextKeyError::InvalidUnqualified)?;
458        Ok(Self(value))
459    }
460
461    /// Returns the validated context key.
462    #[must_use]
463    pub fn as_str(&self) -> &str {
464        &self.0
465    }
466}
467
468fn validate_namespaced_context_key(value: &str) -> Result<(), PermissionContextKeyError> {
469    let Some((namespace, path)) = value.split_once(':') else {
470        return Err(PermissionContextKeyError::InvalidFormat);
471    };
472    if namespace.is_empty() {
473        return Err(PermissionContextKeyError::EmptyNamespace);
474    }
475    if path.is_empty() {
476        return Err(PermissionContextKeyError::EmptyPath);
477    }
478    if path.contains(':') {
479        return Err(PermissionContextKeyError::InvalidFormat);
480    }
481    if namespace.split('.').any(str::is_empty) {
482        return Err(PermissionContextKeyError::InvalidNamespace);
483    }
484    if path.split(['.', '/']).any(str::is_empty) {
485        return Err(PermissionContextKeyError::InvalidPath);
486    }
487    if !Identifier::validate_namespace(namespace) {
488        return Err(PermissionContextKeyError::InvalidNamespace);
489    }
490    if !Identifier::validate_path(path) {
491        return Err(PermissionContextKeyError::InvalidPath);
492    }
493    Ok(())
494}
495
496/// Invalid rule-side or active permission context.
497#[derive(Clone, Debug, PartialEq, Eq)]
498pub enum PermissionRuleContextError {
499    /// A domain name is not a valid identifier namespace.
500    InvalidDomain(String),
501    /// A custom value cannot be represented by the expression syntax.
502    InvalidValue(String),
503    /// One chain binds two different domains.
504    DuplicateDomain,
505    /// One chain binds two different loaded worlds.
506    DuplicateWorld,
507    /// One custom key is bound to two different values.
508    DuplicateCustomKey(PermissionContextKey),
509    /// A world identifier and explicit domain name disagree.
510    WorldDomainMismatch {
511        /// Explicit domain constraint.
512        domain: String,
513        /// Domain implied by the world identifier.
514        world_domain: String,
515    },
516}
517
518impl fmt::Display for PermissionRuleContextError {
519    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
520        match self {
521            Self::InvalidDomain(domain) => {
522                write!(formatter, "invalid permission context domain '{domain}'")
523            }
524            Self::InvalidValue(value) => {
525                write!(formatter, "invalid permission context value '{value}'")
526            }
527            Self::DuplicateDomain => {
528                formatter.write_str("domain permission context cannot have multiple values")
529            }
530            Self::DuplicateWorld => {
531                formatter.write_str("world permission context cannot have multiple values")
532            }
533            Self::DuplicateCustomKey(key) => write!(
534                formatter,
535                "custom permission context key '{}' cannot have multiple values",
536                key.as_str()
537            ),
538            Self::WorldDomainMismatch {
539                domain,
540                world_domain,
541            } => write!(
542                formatter,
543                "permission context domain '{domain}' conflicts with world domain '{world_domain}'"
544            ),
545        }
546    }
547}
548
549impl Error for PermissionRuleContextError {}
550
551/// Invalid custom permission context key.
552#[derive(Clone, Debug, PartialEq, Eq)]
553pub enum PermissionContextKeyError {
554    /// A namespaced key does not use one `namespace:path` separator.
555    InvalidFormat,
556    /// The namespace before `:` is empty.
557    EmptyNamespace,
558    /// The path after `:` is empty.
559    EmptyPath,
560    /// The namespace is not a valid identifier namespace.
561    InvalidNamespace,
562    /// The path is not a valid identifier path.
563    InvalidPath,
564    /// An unqualified key is not a valid permission segment.
565    InvalidUnqualified(PermissionKeyError),
566}
567
568impl fmt::Display for PermissionContextKeyError {
569    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
570        match self {
571            Self::InvalidFormat => {
572                formatter.write_str("context key must be a name or namespaced id")
573            }
574            Self::EmptyNamespace => formatter.write_str("context key namespace is empty"),
575            Self::EmptyPath => formatter.write_str("context key path is empty"),
576            Self::InvalidNamespace => {
577                formatter.write_str("context key namespace contains invalid characters")
578            }
579            Self::InvalidPath => {
580                formatter.write_str("context key path contains invalid characters")
581            }
582            Self::InvalidUnqualified(source) => source.fmt(formatter),
583        }
584    }
585}
586
587impl Error for PermissionContextKeyError {}