Skip to main content

steel_core/command/execution/
permission.rs

1//! Permission command arguments and discovery-only suggestions.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use steel_protocol::packets::game::{
6    ArgumentStringTypeBehavior, ArgumentType as ProtocolArgumentType,
7    SuggestionType as ProtocolSuggestionType,
8};
9use steel_utils::{DowncastType, DowncastTypeKey};
10
11use crate::{
12    command::brigadier::{
13        CommandSyntaxError, CommandSyntaxErrorKind, StringReader, SuggestionsBuilder,
14    },
15    permission::{
16        PermissionMetadataExpression, PermissionRuleContext, PermissionRuleExpression,
17        PermissionSegment,
18    },
19};
20
21use super::{
22    CommandArgumentSource,
23    argument::{SteelArgumentParser, SteelArgumentSuggestionContext},
24};
25
26// SAFETY: This Steel-owned key uniquely identifies the concrete parsed value.
27unsafe impl DowncastType for PermissionRuleExpression {
28    const TYPE_KEY: DowncastTypeKey =
29        DowncastTypeKey::new("steel:command/value/permission_rule_expression");
30}
31
32// SAFETY: This Steel-owned key uniquely identifies the concrete parsed value.
33unsafe impl DowncastType for PermissionMetadataExpression {
34    const TYPE_KEY: DowncastTypeKey =
35        DowncastTypeKey::new("steel:command/value/permission_metadata_expression");
36}
37
38/// Validated permission group name.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub(crate) struct PermissionGroupName(Box<str>);
41
42impl PermissionGroupName {
43    pub(crate) fn as_str(&self) -> &str {
44        &self.0
45    }
46}
47
48// SAFETY: This Steel-owned key uniquely identifies the concrete parsed value.
49unsafe impl DowncastType for PermissionGroupName {
50    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/value/permission_group");
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54enum PermissionSuggestionScope {
55    All,
56    UserOwned,
57    GroupOwned,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub(super) struct PermissionRuleParser {
62    suggestions: PermissionSuggestionScope,
63}
64
65impl PermissionRuleParser {
66    pub(super) const fn all() -> Self {
67        Self {
68            suggestions: PermissionSuggestionScope::All,
69        }
70    }
71
72    pub(super) const fn user_owned() -> Self {
73        Self {
74            suggestions: PermissionSuggestionScope::UserOwned,
75        }
76    }
77
78    pub(super) const fn group_owned() -> Self {
79        Self {
80            suggestions: PermissionSuggestionScope::GroupOwned,
81        }
82    }
83}
84
85// SAFETY: This Steel-owned key uniquely identifies the concrete parser.
86unsafe impl DowncastType for PermissionRuleParser {
87    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/permission_rule");
88}
89
90impl SteelArgumentParser for PermissionRuleParser {
91    type Value = PermissionRuleExpression;
92
93    fn parse(
94        &self,
95        reader: &mut StringReader<'_>,
96        _source: &dyn CommandArgumentSource,
97    ) -> Result<Self::Value, CommandSyntaxError> {
98        let value = reader.read_unquoted_token();
99        PermissionRuleExpression::parse(value).map_err(|error| {
100            reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
101                error.to_string().into(),
102            )))
103        })
104    }
105
106    fn list_suggestions(
107        &self,
108        context: &dyn SteelArgumentSuggestionContext,
109        builder: &mut SuggestionsBuilder<'_>,
110    ) {
111        let expressions = match self.suggestions {
112            PermissionSuggestionScope::All => context.source().permission_rule_suggestions(),
113            PermissionSuggestionScope::UserOwned => context
114                .argument("targets")
115                .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
116                .map_or_else(Vec::new, |targets| {
117                    context.source().user_permission_rule_suggestions(targets)
118                }),
119            PermissionSuggestionScope::GroupOwned => context
120                .argument("group")
121                .and_then(|value| value.downcast_ref::<PermissionGroupName>())
122                .map_or_else(Vec::new, |group| {
123                    context
124                        .source()
125                        .group_permission_rule_suggestions(group.as_str())
126                }),
127        };
128        suggest_expression(builder, context.source(), expressions);
129    }
130
131    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
132        permission_expression_argument()
133    }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub(super) struct PermissionMetadataParser {
138    suggestions: PermissionSuggestionScope,
139}
140
141impl PermissionMetadataParser {
142    pub(super) const fn all() -> Self {
143        Self {
144            suggestions: PermissionSuggestionScope::All,
145        }
146    }
147
148    pub(super) const fn user_owned() -> Self {
149        Self {
150            suggestions: PermissionSuggestionScope::UserOwned,
151        }
152    }
153
154    pub(super) const fn group_owned() -> Self {
155        Self {
156            suggestions: PermissionSuggestionScope::GroupOwned,
157        }
158    }
159}
160
161// SAFETY: This Steel-owned key uniquely identifies the concrete parser.
162unsafe impl DowncastType for PermissionMetadataParser {
163    const TYPE_KEY: DowncastTypeKey =
164        DowncastTypeKey::new("steel:command/parser/permission_metadata");
165}
166
167impl SteelArgumentParser for PermissionMetadataParser {
168    type Value = PermissionMetadataExpression;
169
170    fn parse(
171        &self,
172        reader: &mut StringReader<'_>,
173        _source: &dyn CommandArgumentSource,
174    ) -> Result<Self::Value, CommandSyntaxError> {
175        let value = reader.read_unquoted_token();
176        PermissionMetadataExpression::parse(value).map_err(|error| {
177            reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
178                error.to_string().into(),
179            )))
180        })
181    }
182
183    fn list_suggestions(
184        &self,
185        context: &dyn SteelArgumentSuggestionContext,
186        builder: &mut SuggestionsBuilder<'_>,
187    ) {
188        let expressions = match self.suggestions {
189            PermissionSuggestionScope::All => context.source().permission_metadata_suggestions(),
190            PermissionSuggestionScope::UserOwned => context
191                .argument("targets")
192                .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
193                .map_or_else(Vec::new, |targets| {
194                    context
195                        .source()
196                        .user_permission_metadata_suggestions(targets)
197                }),
198            PermissionSuggestionScope::GroupOwned => context
199                .argument("group")
200                .and_then(|value| value.downcast_ref::<PermissionGroupName>())
201                .map_or_else(Vec::new, |group| {
202                    context
203                        .source()
204                        .group_permission_metadata_suggestions(group.as_str())
205                }),
206        };
207        suggest_expression(builder, context.source(), expressions);
208    }
209
210    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
211        permission_expression_argument()
212    }
213}
214
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub(super) struct PermissionGroupParser {
217    pub(super) require_existing: bool,
218}
219
220// SAFETY: This Steel-owned key uniquely identifies the concrete parser.
221unsafe impl DowncastType for PermissionGroupParser {
222    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/permission_group");
223}
224
225impl SteelArgumentParser for PermissionGroupParser {
226    type Value = PermissionGroupName;
227
228    fn parse(
229        &self,
230        reader: &mut StringReader<'_>,
231        source: &dyn CommandArgumentSource,
232    ) -> Result<Self::Value, CommandSyntaxError> {
233        let value = reader.read_unquoted_string();
234        PermissionSegment::parse(value).map_err(|error| {
235            reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
236                error.to_string().into(),
237            )))
238        })?;
239        if self.require_existing
240            && !source
241                .permission_group_names()
242                .iter()
243                .any(|group| group == value)
244        {
245            return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
246                format!("Unknown permission group '{value}'").into(),
247            ))));
248        }
249        Ok(PermissionGroupName(value.into()))
250    }
251
252    fn list_suggestions(
253        &self,
254        context: &dyn SteelArgumentSuggestionContext,
255        builder: &mut SuggestionsBuilder<'_>,
256    ) {
257        let prefix = builder.remaining_lowercase().to_owned();
258        for group in context.source().permission_group_names() {
259            if group.to_lowercase().starts_with(&prefix) {
260                builder.suggest(group);
261            }
262        }
263    }
264
265    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
266        permission_group_argument()
267    }
268}
269
270const fn permission_expression_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>)
271{
272    // Permission expressions contain characters that Brigadier's word parser
273    // treats as delimiters. These arguments are terminal, so a greedy string
274    // lets the client cover Steel's full no-whitespace expression syntax.
275    (
276        ProtocolArgumentType::String {
277            behavior: ArgumentStringTypeBehavior::GreedyPhrase,
278        },
279        Some(ProtocolSuggestionType::AskServer),
280    )
281}
282
283const fn permission_group_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
284    (
285        ProtocolArgumentType::String {
286            behavior: ArgumentStringTypeBehavior::SingleWord,
287        },
288        Some(ProtocolSuggestionType::AskServer),
289    )
290}
291
292fn suggest_expression(
293    builder: &mut SuggestionsBuilder<'_>,
294    source: &dyn CommandArgumentSource,
295    expressions: Vec<String>,
296) {
297    let prefix = builder.remaining();
298    for expression in &expressions {
299        if expression.starts_with(prefix) {
300            builder.suggest(expression.clone());
301        }
302    }
303
304    let Some((base, context_prefix)) = prefix.split_once('{') else {
305        return;
306    };
307    if base.is_empty() || context_prefix.ends_with('}') {
308        return;
309    }
310
311    let known_contexts = known_custom_contexts(
312        expressions
313            .iter()
314            .chain(source.permission_rule_suggestions().iter())
315            .chain(source.permission_metadata_suggestions().iter()),
316    );
317    suggest_context(builder, source, base, context_prefix, &known_contexts);
318}
319
320fn suggest_context(
321    builder: &mut SuggestionsBuilder<'_>,
322    source: &dyn CommandArgumentSource,
323    base: &str,
324    context_prefix: &str,
325    known_contexts: &BTreeMap<String, BTreeSet<String>>,
326) {
327    let (completed, current) = context_prefix
328        .rsplit_once(',')
329        .map_or(("", context_prefix), |(completed, current)| {
330            (completed, current)
331        });
332    let completed_keys = completed
333        .split(',')
334        .filter_map(|entry| entry.split_once('=').map(|(key, _)| key))
335        .collect::<BTreeSet<_>>();
336    let entry_prefix = if completed.is_empty() {
337        format!("{base}{{")
338    } else {
339        format!("{base}{{{completed},")
340    };
341
342    let Some((key, value_prefix)) = current.split_once('=') else {
343        for key in ["domain", "world"]
344            .into_iter()
345            .chain(known_contexts.keys().map(String::as_str))
346        {
347            if !completed_keys.contains(key) && key.starts_with(current) {
348                builder.suggest(format!("{entry_prefix}{key}="));
349            }
350        }
351        return;
352    };
353
354    let values = match key {
355        "domain" => source
356            .domain_names()
357            .into_iter()
358            .map(str::to_owned)
359            .collect(),
360        "world" => source.permission_context_world_names(),
361        custom => known_contexts
362            .get(custom)
363            .map_or_else(Vec::new, |values| values.iter().cloned().collect()),
364    };
365    for value in values {
366        if value.starts_with(value_prefix) {
367            builder.suggest(format!("{entry_prefix}{key}={value}}}"));
368        }
369    }
370}
371
372fn known_custom_contexts<'a>(
373    expressions: impl Iterator<Item = &'a String>,
374) -> BTreeMap<String, BTreeSet<String>> {
375    let mut contexts = BTreeMap::new();
376    for expression in expressions {
377        if let Ok(expression) = PermissionRuleExpression::parse(expression) {
378            collect_custom_contexts(expression.context(), &mut contexts);
379        } else if let Ok(expression) = PermissionMetadataExpression::parse(expression) {
380            collect_custom_contexts(expression.context(), &mut contexts);
381        }
382    }
383    contexts
384}
385
386fn collect_custom_contexts(
387    context: &PermissionRuleContext,
388    contexts: &mut BTreeMap<String, BTreeSet<String>>,
389) {
390    match context {
391        PermissionRuleContext::Custom { key, value } => {
392            contexts
393                .entry(key.as_str().to_owned())
394                .or_default()
395                .insert(value.as_str().to_owned());
396        }
397        PermissionRuleContext::All(nested) => {
398            for context in nested.iter() {
399                collect_custom_contexts(context, contexts);
400            }
401        }
402        PermissionRuleContext::Global
403        | PermissionRuleContext::Domain(_)
404        | PermissionRuleContext::World(_) => {}
405    }
406}