1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use tokio::io;
5use toml::ser::Error as TomlSerializeError;
6use uuid::Uuid;
7
8use crate::permission::{
9 PermissionEntry, PermissionMetadataEntry, PermissionMetadataExpression, PermissionMetadataSet,
10 PermissionMetadataValue, PermissionRuleExpression, PermissionSegment, PermissionSet,
11 PermissionState, PermissionSubjectIndex, PermissionSubjectState,
12};
13
14#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
15#[serde(default, deny_unknown_fields)]
16pub(super) struct PlayerPermissionsFile {
17 pub(super) players: BTreeMap<String, PlayerPermissionEntryFile>,
18}
19
20#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
21#[serde(default, deny_unknown_fields)]
22pub(super) struct PlayerPermissionEntryFile {
23 pub(super) groups: Vec<String>,
24 pub(super) allow: Vec<String>,
25 pub(super) deny: Vec<String>,
26 pub(super) metadata: Vec<PlayerPermissionMetadataEntryFile>,
27}
28
29#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
30#[serde(deny_unknown_fields)]
31pub(super) struct PlayerPermissionMetadataEntryFile {
32 pub(super) key: String,
33 pub(super) value: PermissionMetadataValue,
34}
35
36impl PlayerPermissionsFile {
37 pub(super) fn from_subject_index(subjects: &PermissionSubjectIndex) -> Self {
38 let mut file = Self::default();
39 for (uuid, state) in subjects.entries() {
40 set_permission_subject(&mut file, uuid, state);
41 }
42 file
43 }
44
45 pub(super) fn validate(&self) -> io::Result<()> {
46 for (uuid, entry) in &self.players {
47 let uuid = parse_uuid(uuid)?;
48 entry.validate(uuid)?;
49 }
50 Ok(())
51 }
52
53 pub(super) fn into_subject_index(self) -> io::Result<PermissionSubjectIndex> {
54 let mut subjects = PermissionSubjectIndex::new();
55 for (uuid_text, entry) in self.players {
56 let uuid = parse_uuid(&uuid_text)?;
57 if subjects.get(uuid).is_some() {
58 return Err(io::Error::new(
59 io::ErrorKind::InvalidData,
60 format!("duplicate player permission UUID '{uuid_text}' resolves to {uuid}"),
61 ));
62 }
63 subjects.set(uuid, entry.into_subject_state(uuid)?);
64 }
65 Ok(subjects)
66 }
67}
68
69impl PlayerPermissionEntryFile {
70 fn validate(&self, uuid: Uuid) -> io::Result<()> {
71 validate_groups(uuid, &self.groups)?;
72 for expression in &self.allow {
73 parse_permission_expression(uuid, expression, "allow")?;
74 }
75 for expression in &self.deny {
76 parse_permission_expression(uuid, expression, "deny")?;
77 }
78 for entry in &self.metadata {
79 parse_metadata_expression(uuid, &entry.key)?;
80 }
81 Ok(())
82 }
83
84 fn from_subject_state(state: &PermissionSubjectState) -> Self {
85 let mut allow = Vec::new();
86 let mut deny = Vec::new();
87 for entry in state.overrides().entries() {
88 let expression =
89 PermissionRuleExpression::new(entry.key().clone(), entry.context().clone())
90 .to_string();
91 match entry.state() {
92 PermissionState::Allow => allow.push(expression),
93 PermissionState::Deny => deny.push(expression),
94 }
95 }
96 Self {
97 groups: state.groups().to_vec(),
98 allow,
99 deny,
100 metadata: state
101 .metadata_overrides()
102 .entries()
103 .iter()
104 .map(|entry| PlayerPermissionMetadataEntryFile {
105 key: PermissionMetadataExpression::new(
106 entry.key().clone(),
107 entry.context().clone(),
108 )
109 .to_string(),
110 value: entry.value().clone(),
111 })
112 .collect(),
113 }
114 }
115
116 fn into_subject_state(self, uuid: Uuid) -> io::Result<PermissionSubjectState> {
117 validate_groups(uuid, &self.groups)?;
118 let mut overrides = PermissionSet::new();
119 for expression in self.allow {
120 let expression = parse_permission_expression(uuid, &expression, "allow")?;
121 let (key, context) = expression.into_parts();
122 overrides.push(PermissionEntry::allow_with_context(key, context));
123 }
124 for expression in self.deny {
125 let expression = parse_permission_expression(uuid, &expression, "deny")?;
126 let (key, context) = expression.into_parts();
127 overrides.push(PermissionEntry::deny_with_context(key, context));
128 }
129 let mut metadata = PermissionMetadataSet::new();
130 for entry in self.metadata {
131 let expression = parse_metadata_expression(uuid, &entry.key)?;
132 let (key, context) = expression.into_parts();
133 metadata.push(PermissionMetadataEntry::new_with_context(
134 key,
135 context,
136 entry.value,
137 ));
138 }
139 Ok(PermissionSubjectState::new_with_metadata(
140 self.groups,
141 overrides,
142 metadata,
143 ))
144 }
145}
146
147fn parse_uuid(value: &str) -> io::Result<Uuid> {
148 Uuid::parse_str(value).map_err(|error| {
149 io::Error::new(
150 io::ErrorKind::InvalidData,
151 format!("invalid player permission UUID '{value}': {error}"),
152 )
153 })
154}
155
156fn validate_groups(uuid: Uuid, groups: &[String]) -> io::Result<()> {
157 for group in groups {
158 PermissionSegment::parse(group.as_str()).map_err(|error| {
159 io::Error::new(
160 io::ErrorKind::InvalidData,
161 format!("invalid permission group '{group}' for {uuid}: {error}"),
162 )
163 })?;
164 }
165 Ok(())
166}
167
168fn parse_permission_expression(
169 uuid: Uuid,
170 expression: &str,
171 state: &str,
172) -> io::Result<PermissionRuleExpression> {
173 PermissionRuleExpression::parse(expression).map_err(|error| {
174 io::Error::new(
175 io::ErrorKind::InvalidData,
176 format!("invalid {state} permission expression for {uuid}: {error}"),
177 )
178 })
179}
180
181fn parse_metadata_expression(
182 uuid: Uuid,
183 expression: &str,
184) -> io::Result<PermissionMetadataExpression> {
185 PermissionMetadataExpression::parse(expression).map_err(|error| {
186 io::Error::new(
187 io::ErrorKind::InvalidData,
188 format!("invalid permission metadata expression for {uuid}: {error}"),
189 )
190 })
191}
192
193pub(super) fn set_permission_subject(
194 file: &mut PlayerPermissionsFile,
195 uuid: Uuid,
196 state: &PermissionSubjectState,
197) {
198 if state.is_empty() {
199 file.players.remove(&uuid.to_string());
200 return;
201 }
202 file.players.insert(
203 uuid.to_string(),
204 PlayerPermissionEntryFile::from_subject_state(state),
205 );
206}
207
208pub(super) fn serialize_player_permissions_file(
209 file: &PlayerPermissionsFile,
210) -> Result<String, TomlSerializeError> {
211 let mut output = String::new();
212 if file.players.is_empty() {
213 output.push_str("players = {}\n");
214 return Ok(output);
215 }
216
217 for (uuid, entry) in &file.players {
218 output.push_str("[players.");
219 output.push_str(&toml_value(uuid)?);
220 output.push_str("]\n");
221 push_player_permission_entry(&mut output, entry)?;
222 output.push('\n');
223 }
224 Ok(output)
225}
226
227fn push_player_permission_entry(
228 output: &mut String,
229 entry: &PlayerPermissionEntryFile,
230) -> Result<(), TomlSerializeError> {
231 push_string_array_field(output, "groups", &entry.groups)?;
232 push_string_array_field(output, "allow", &entry.allow)?;
233 push_string_array_field(output, "deny", &entry.deny)?;
234 push_permission_metadata_entries(output, &entry.metadata)
235}
236
237fn push_string_array_field(
238 output: &mut String,
239 key: &str,
240 values: &[String],
241) -> Result<(), TomlSerializeError> {
242 if values.is_empty() {
243 output.push_str(key);
244 output.push_str(" = []\n");
245 return Ok(());
246 }
247
248 output.push_str(key);
249 output.push_str(" = [\n");
250 for value in values {
251 output.push_str(" ");
252 output.push_str(&toml_value(value)?);
253 output.push_str(",\n");
254 }
255 output.push_str("]\n");
256 Ok(())
257}
258
259fn push_permission_metadata_entries(
260 output: &mut String,
261 metadata: &[PlayerPermissionMetadataEntryFile],
262) -> Result<(), TomlSerializeError> {
263 if metadata.is_empty() {
264 output.push_str("metadata = []\n");
265 return Ok(());
266 }
267
268 output.push_str("metadata = [\n");
269 for entry in metadata {
270 output.push_str(" { key = ");
271 output.push_str(&toml_value(&entry.key)?);
272 output.push_str(", value = ");
273 output.push_str(&permission_metadata_value_toml(&entry.value)?);
274 output.push_str(" },\n");
275 }
276 output.push_str("]\n");
277 Ok(())
278}
279
280fn toml_value<T: Serialize + ?Sized>(value: &T) -> Result<String, TomlSerializeError> {
281 #[derive(Serialize)]
282 struct Field<'a, T: Serialize + ?Sized> {
283 value: &'a T,
284 }
285
286 let serialized = toml::to_string(&Field { value })?;
287 let serialized = serialized.trim_end();
288 Ok(serialized
289 .strip_prefix("value = ")
290 .unwrap_or(serialized)
291 .to_owned())
292}
293
294fn permission_metadata_value_toml(
295 value: &PermissionMetadataValue,
296) -> Result<String, TomlSerializeError> {
297 match value {
298 PermissionMetadataValue::Bool(value) => toml_value(value),
299 PermissionMetadataValue::Integer(value) => toml_value(value),
300 PermissionMetadataValue::String(value) => toml_value(value),
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::permission::{
308 PermissionKey, PermissionMetadataValue, PermissionRuleContext,
309 parse_permission_metadata_key,
310 };
311
312 fn key(value: &str) -> PermissionKey {
313 match PermissionKey::parse(value) {
314 Ok(key) => key,
315 Err(error) => panic!("test permission key should parse: {error}"),
316 }
317 }
318
319 #[test]
320 fn subject_file_round_trip_preserves_groups_and_contextual_rules() {
321 let uuid = Uuid::from_u128(1);
322 let mut overrides = PermissionSet::new();
323 overrides.allow(key("steel.fly"));
324 overrides.deny_in(
325 key("steel.build"),
326 PermissionRuleContext::domain("survival")
327 .unwrap_or_else(|error| panic!("test domain should parse: {error}")),
328 );
329 let metadata_key = match parse_permission_metadata_key("plugin:max_homes") {
330 Ok(key) => key,
331 Err(error) => panic!("test metadata key should parse: {error}"),
332 };
333 let mut metadata = PermissionMetadataSet::new();
334 metadata.set_in(
335 metadata_key,
336 PermissionRuleContext::domain("survival")
337 .unwrap_or_else(|error| panic!("test domain should parse: {error}")),
338 PermissionMetadataValue::Integer(5),
339 );
340 let state = PermissionSubjectState::new_with_metadata(
341 vec!["retired_group".to_owned()],
342 overrides,
343 metadata,
344 );
345 let mut file = PlayerPermissionsFile::default();
346 set_permission_subject(&mut file, uuid, &state);
347
348 let serialized = match serialize_player_permissions_file(&file) {
349 Ok(serialized) => serialized,
350 Err(error) => panic!("subject file should serialize: {error}"),
351 };
352
353 assert!(serialized.contains("{ key = \"plugin:max_homes{domain=survival}\", value = 5 }"));
354 let parsed = match toml::from_str::<PlayerPermissionsFile>(&serialized) {
355 Ok(parsed) => parsed,
356 Err(error) => panic!("subject file should parse: {error}"),
357 };
358 let parsed = match parsed.into_subject_index() {
359 Ok(parsed) => parsed,
360 Err(error) => panic!("subjects should validate: {error}"),
361 };
362 let Some(parsed) = parsed.get(uuid) else {
363 panic!("subject should exist");
364 };
365
366 assert_eq!(parsed, &state);
367 }
368
369 #[test]
370 fn subject_file_rejects_invalid_group_names() {
371 let uuid = Uuid::from_u128(2);
372 let mut file = PlayerPermissionsFile::default();
373 file.players.insert(
374 uuid.to_string(),
375 PlayerPermissionEntryFile {
376 groups: vec!["Admin Group".to_owned()],
377 ..PlayerPermissionEntryFile::default()
378 },
379 );
380
381 let error = file.validate();
382 assert!(error.is_err_and(|error| {
383 error
384 .to_string()
385 .contains("invalid permission group 'Admin Group'")
386 }));
387 }
388
389 #[test]
390 fn empty_subject_state_removes_the_file_entry() {
391 let uuid = Uuid::from_u128(3);
392 let mut file = PlayerPermissionsFile::default();
393 file.players
394 .insert(uuid.to_string(), PlayerPermissionEntryFile::default());
395
396 set_permission_subject(&mut file, uuid, &PermissionSubjectState::default());
397
398 assert!(file.players.is_empty());
399 }
400
401 #[test]
402 fn subject_file_rejects_duplicate_uuid_spellings() {
403 let uuid = Uuid::from_u128(4);
404 let mut file = PlayerPermissionsFile::default();
405 file.players.insert(
406 uuid.to_string(),
407 PlayerPermissionEntryFile {
408 groups: vec!["op".to_owned()],
409 ..PlayerPermissionEntryFile::default()
410 },
411 );
412 file.players.insert(
413 uuid.simple().to_string(),
414 PlayerPermissionEntryFile {
415 groups: vec!["builder".to_owned()],
416 ..PlayerPermissionEntryFile::default()
417 },
418 );
419
420 let error = file.into_subject_index();
421
422 assert!(error.is_err_and(|error| {
423 error
424 .to_string()
425 .contains("duplicate player permission UUID")
426 }));
427 }
428}