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                .ok()
116                .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
117                .map_or_else(Vec::new, |targets| {
118                    context.source().user_permission_rule_suggestions(targets)
119                }),
120            PermissionSuggestionScope::GroupOwned => context
121                .argument("group")
122                .ok()
123                .and_then(|value| value.downcast_ref::<PermissionGroupName>())
124                .map_or_else(Vec::new, |group| {
125                    context
126                        .source()
127                        .group_permission_rule_suggestions(group.as_str())
128                }),
129        };
130        suggest_expression(builder, context.source(), expressions);
131    }
132
133    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
134        permission_expression_argument()
135    }
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub(super) struct PermissionMetadataParser {
140    suggestions: PermissionSuggestionScope,
141}
142
143impl PermissionMetadataParser {
144    pub(super) const fn all() -> Self {
145        Self {
146            suggestions: PermissionSuggestionScope::All,
147        }
148    }
149
150    pub(super) const fn user_owned() -> Self {
151        Self {
152            suggestions: PermissionSuggestionScope::UserOwned,
153        }
154    }
155
156    pub(super) const fn group_owned() -> Self {
157        Self {
158            suggestions: PermissionSuggestionScope::GroupOwned,
159        }
160    }
161}
162
163// SAFETY: This Steel-owned key uniquely identifies the concrete parser.
164unsafe impl DowncastType for PermissionMetadataParser {
165    const TYPE_KEY: DowncastTypeKey =
166        DowncastTypeKey::new("steel:command/parser/permission_metadata");
167}
168
169impl SteelArgumentParser for PermissionMetadataParser {
170    type Value = PermissionMetadataExpression;
171
172    fn parse(
173        &self,
174        reader: &mut StringReader<'_>,
175        _source: &dyn CommandArgumentSource,
176    ) -> Result<Self::Value, CommandSyntaxError> {
177        let value = reader.read_unquoted_token();
178        PermissionMetadataExpression::parse(value).map_err(|error| {
179            reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
180                error.to_string().into(),
181            )))
182        })
183    }
184
185    fn list_suggestions(
186        &self,
187        context: &dyn SteelArgumentSuggestionContext,
188        builder: &mut SuggestionsBuilder<'_>,
189    ) {
190        let expressions = match self.suggestions {
191            PermissionSuggestionScope::All => context.source().permission_metadata_suggestions(),
192            PermissionSuggestionScope::UserOwned => context
193                .argument("targets")
194                .ok()
195                .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
196                .map_or_else(Vec::new, |targets| {
197                    context
198                        .source()
199                        .user_permission_metadata_suggestions(targets)
200                }),
201            PermissionSuggestionScope::GroupOwned => context
202                .argument("group")
203                .ok()
204                .and_then(|value| value.downcast_ref::<PermissionGroupName>())
205                .map_or_else(Vec::new, |group| {
206                    context
207                        .source()
208                        .group_permission_metadata_suggestions(group.as_str())
209                }),
210        };
211        suggest_expression(builder, context.source(), expressions);
212    }
213
214    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
215        permission_expression_argument()
216    }
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub(super) struct PermissionGroupParser {
221    pub(super) require_existing: bool,
222}
223
224// SAFETY: This Steel-owned key uniquely identifies the concrete parser.
225unsafe impl DowncastType for PermissionGroupParser {
226    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/permission_group");
227}
228
229impl SteelArgumentParser for PermissionGroupParser {
230    type Value = PermissionGroupName;
231
232    fn parse(
233        &self,
234        reader: &mut StringReader<'_>,
235        source: &dyn CommandArgumentSource,
236    ) -> Result<Self::Value, CommandSyntaxError> {
237        let value = reader.read_unquoted_string();
238        PermissionSegment::parse(value).map_err(|error| {
239            reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
240                error.to_string().into(),
241            )))
242        })?;
243        if self.require_existing
244            && !source
245                .permission_group_names()
246                .iter()
247                .any(|group| group == value)
248        {
249            return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
250                format!("Unknown permission group '{value}'").into(),
251            ))));
252        }
253        Ok(PermissionGroupName(value.into()))
254    }
255
256    fn list_suggestions(
257        &self,
258        context: &dyn SteelArgumentSuggestionContext,
259        builder: &mut SuggestionsBuilder<'_>,
260    ) {
261        let prefix = builder.remaining_lowercase().to_owned();
262        for group in context.source().permission_group_names() {
263            if group.to_lowercase().starts_with(&prefix) {
264                builder.suggest(group);
265            }
266        }
267    }
268
269    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
270        permission_group_argument()
271    }
272}
273
274const fn permission_expression_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>)
275{
276    // Permission expressions contain characters that Brigadier's word parser
277    // treats as delimiters. These arguments are terminal, so a greedy string
278    // lets the client cover Steel's full no-whitespace expression syntax.
279    (
280        ProtocolArgumentType::String {
281            behavior: ArgumentStringTypeBehavior::GreedyPhrase,
282        },
283        Some(ProtocolSuggestionType::AskServer),
284    )
285}
286
287const fn permission_group_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
288    (
289        ProtocolArgumentType::String {
290            behavior: ArgumentStringTypeBehavior::SingleWord,
291        },
292        Some(ProtocolSuggestionType::AskServer),
293    )
294}
295
296fn suggest_expression(
297    builder: &mut SuggestionsBuilder<'_>,
298    source: &dyn CommandArgumentSource,
299    expressions: Vec<String>,
300) {
301    let prefix = builder.remaining();
302    for expression in &expressions {
303        if expression.starts_with(prefix) {
304            builder.suggest(expression.clone());
305        }
306    }
307
308    let Some((base, context_prefix)) = prefix.split_once('{') else {
309        return;
310    };
311    if base.is_empty() || context_prefix.ends_with('}') {
312        return;
313    }
314
315    let known_contexts = known_custom_contexts(
316        expressions
317            .iter()
318            .chain(source.permission_rule_suggestions().iter())
319            .chain(source.permission_metadata_suggestions().iter()),
320    );
321    suggest_context(builder, source, base, context_prefix, &known_contexts);
322}
323
324fn suggest_context(
325    builder: &mut SuggestionsBuilder<'_>,
326    source: &dyn CommandArgumentSource,
327    base: &str,
328    context_prefix: &str,
329    known_contexts: &BTreeMap<String, BTreeSet<String>>,
330) {
331    let (completed, current) = context_prefix
332        .rsplit_once(',')
333        .map_or(("", context_prefix), |(completed, current)| {
334            (completed, current)
335        });
336    let completed_keys = completed
337        .split(',')
338        .filter_map(|entry| entry.split_once('=').map(|(key, _)| key))
339        .collect::<BTreeSet<_>>();
340    let entry_prefix = if completed.is_empty() {
341        format!("{base}{{")
342    } else {
343        format!("{base}{{{completed},")
344    };
345
346    let Some((key, value_prefix)) = current.split_once('=') else {
347        for key in ["domain", "world"]
348            .into_iter()
349            .chain(known_contexts.keys().map(String::as_str))
350        {
351            if !completed_keys.contains(key) && key.starts_with(current) {
352                builder.suggest(format!("{entry_prefix}{key}="));
353            }
354        }
355        return;
356    };
357
358    let values = match key {
359        "domain" => source
360            .domain_names()
361            .into_iter()
362            .map(str::to_owned)
363            .collect(),
364        "world" => source.permission_context_world_names(),
365        custom => known_contexts
366            .get(custom)
367            .map_or_else(Vec::new, |values| values.iter().cloned().collect()),
368    };
369    for value in values {
370        if value.starts_with(value_prefix) {
371            builder.suggest(format!("{entry_prefix}{key}={value}}}"));
372        }
373    }
374}
375
376fn known_custom_contexts<'a>(
377    expressions: impl Iterator<Item = &'a String>,
378) -> BTreeMap<String, BTreeSet<String>> {
379    let mut contexts = BTreeMap::new();
380    for expression in expressions {
381        if let Ok(expression) = PermissionRuleExpression::parse(expression) {
382            collect_custom_contexts(expression.context(), &mut contexts);
383        } else if let Ok(expression) = PermissionMetadataExpression::parse(expression) {
384            collect_custom_contexts(expression.context(), &mut contexts);
385        }
386    }
387    contexts
388}
389
390fn collect_custom_contexts(
391    context: &PermissionRuleContext,
392    contexts: &mut BTreeMap<String, BTreeSet<String>>,
393) {
394    match context {
395        PermissionRuleContext::Custom { key, value } => {
396            contexts
397                .entry(key.as_str().to_owned())
398                .or_default()
399                .insert(value.as_str().to_owned());
400        }
401        PermissionRuleContext::All(nested) => {
402            for context in nested.iter() {
403                collect_custom_contexts(context, contexts);
404            }
405        }
406        PermissionRuleContext::Global
407        | PermissionRuleContext::Domain(_)
408        | PermissionRuleContext::World(_) => {}
409    }
410}