Skip to main content

steel_registry/
game_profile.rs

1//! Validated game profiles
2
3use std::io::{Cursor, Error, Result, Write};
4use std::ops::Deref;
5
6use serde::de::Error as _;
7use serde::{Deserialize, Deserializer};
8use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
9use steel_utils::codec::VarInt;
10use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
11use steel_utils::serial::{PrefixedRead as _, PrefixedWrite as _, ReadFrom, WriteTo};
12use uuid::Uuid;
13
14const MAX_PROPERTIES: usize = 16;
15const MAX_PROPERTY_NAME: usize = 64;
16const MAX_PROPERTY_VALUE: usize = 32_767;
17const MAX_PROPERTY_SIGNATURE: usize = 1_024;
18pub(crate) const MAX_PLAYER_NAME: usize = 16;
19
20/// A game profile property
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ProfileProperty {
23    name: String,
24    value: String,
25    signature: Option<String>,
26}
27
28impl ProfileProperty {
29    pub fn new(name: String, value: String, signature: Option<String>) -> Result<Self> {
30        validate_string(&name, MAX_PROPERTY_NAME, "Profile property name")?;
31        validate_string(&value, MAX_PROPERTY_VALUE, "Profile property value")?;
32        if let Some(signature) = &signature {
33            validate_string(
34                signature,
35                MAX_PROPERTY_SIGNATURE,
36                "Profile property signature",
37            )?;
38        }
39        Ok(Self {
40            name,
41            value,
42            signature,
43        })
44    }
45
46    #[must_use]
47    pub fn name(&self) -> &str {
48        &self.name
49    }
50
51    #[must_use]
52    pub fn value(&self) -> &str {
53        &self.value
54    }
55
56    #[must_use]
57    pub fn signature(&self) -> Option<&str> {
58        self.signature.as_deref()
59    }
60}
61
62/// Profile properties grouped by the first occurrence of each name
63#[derive(Debug, Default, Clone)]
64pub(crate) struct ProfileProperties(Vec<ProfileProperty>);
65
66impl ProfileProperties {
67    pub(crate) fn new(properties: Vec<ProfileProperty>) -> Result<Self> {
68        validate_properties(&properties)?;
69        let mut grouped = Vec::with_capacity(properties.len());
70        for property in properties {
71            let insertion_index = grouped
72                .iter()
73                .rposition(|existing: &ProfileProperty| existing.name() == property.name())
74                .map(|index| index + 1);
75            if let Some(index) = insertion_index {
76                grouped.insert(index, property);
77            } else {
78                grouped.push(property);
79            }
80        }
81        Ok(Self(grouped))
82    }
83}
84
85impl Deref for ProfileProperties {
86    type Target = [ProfileProperty];
87
88    fn deref(&self) -> &Self::Target {
89        &self.0
90    }
91}
92
93impl PartialEq for ProfileProperties {
94    fn eq(&self, other: &Self) -> bool {
95        if self.len() != other.len() {
96            return false;
97        }
98
99        let mut left_start = 0;
100        while let Some(first) = self.get(left_start) {
101            let name = first.name();
102            let left_end = property_group_end(self, left_start);
103            let Some(right_start) = other.iter().position(|property| property.name() == name)
104            else {
105                return false;
106            };
107            let right_end = property_group_end(other, right_start);
108            if self[left_start..left_end] != other[right_start..right_end] {
109                return false;
110            }
111            left_start = left_end;
112        }
113        true
114    }
115}
116
117impl Eq for ProfileProperties {}
118
119fn property_group_end(properties: &[ProfileProperty], start: usize) -> usize {
120    let Some(first) = properties.get(start) else {
121        return start;
122    };
123    properties[start + 1..]
124        .iter()
125        .position(|property| property.name() != first.name())
126        .map_or(properties.len(), |offset| start + offset + 1)
127}
128
129/// A player UUID with name and signed properties such as skin textures
130///
131/// Construction validates the vanilla network limits
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct GameProfile {
134    id: Uuid,
135    name: String,
136    properties: ProfileProperties,
137}
138
139impl GameProfile {
140    pub fn new(id: Uuid, name: String, properties: Vec<ProfileProperty>) -> Result<Self> {
141        validate_player_name(&name)?;
142        Ok(Self {
143            id,
144            name,
145            properties: ProfileProperties::new(properties)?,
146        })
147    }
148
149    #[must_use]
150    pub const fn id(&self) -> Uuid {
151        self.id
152    }
153
154    #[must_use]
155    pub fn name(&self) -> &str {
156        &self.name
157    }
158
159    #[must_use]
160    pub fn properties(&self) -> &[ProfileProperty] {
161        &self.properties
162    }
163
164    /// Writes the properties in their network form
165    /// Used by packets that send the name and properties separately from the id
166    pub fn write_properties(&self, writer: &mut impl Write) -> Result<()> {
167        write_properties_network(&self.properties, writer)
168    }
169}
170
171impl WriteTo for GameProfile {
172    fn write(&self, writer: &mut impl Write) -> Result<()> {
173        self.id.write(writer)?;
174        write_string(&self.name, MAX_PLAYER_NAME, writer)?;
175        write_properties_network(&self.properties, writer)
176    }
177}
178
179impl ReadFrom for GameProfile {
180    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
181        Self::new(
182            Uuid::read(data)?,
183            read_string(data, MAX_PLAYER_NAME)?,
184            read_properties_network(data)?,
185        )
186    }
187}
188
189/// Session server JSON shape that is validated into a game profile on deserialization
190#[derive(Deserialize)]
191struct GameProfileJson {
192    id: Uuid,
193    name: String,
194    #[serde(default)]
195    properties: Vec<ProfilePropertyJson>,
196}
197
198#[derive(Deserialize)]
199struct ProfilePropertyJson {
200    name: String,
201    value: String,
202    signature: Option<String>,
203}
204
205impl<'de> Deserialize<'de> for GameProfile {
206    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
207        let json = GameProfileJson::deserialize(deserializer)?;
208        let properties = json
209            .properties
210            .into_iter()
211            .map(|property| ProfileProperty::new(property.name, property.value, property.signature))
212            .collect::<Result<Vec<_>>>()
213            .map_err(D::Error::custom)?;
214        Self::new(json.id, json.name, properties).map_err(D::Error::custom)
215    }
216}
217
218pub(crate) fn validate_player_name(name: &str) -> Result<()> {
219    if name.encode_utf16().count() > MAX_PLAYER_NAME
220        || name
221            .chars()
222            .any(|character| character <= ' ' || character >= '\u{7f}')
223    {
224        return Err(Error::other("Invalid player profile name"));
225    }
226    Ok(())
227}
228
229pub(crate) fn validate_properties(properties: &[ProfileProperty]) -> Result<()> {
230    if properties.len() > MAX_PROPERTIES {
231        return Err(Error::other("A profile may contain at most 16 properties"));
232    }
233    Ok(())
234}
235
236pub(crate) fn validate_string(value: &str, max: usize, name: &str) -> Result<()> {
237    if value.encode_utf16().count() > max || value.len() > max.saturating_mul(3) {
238        return Err(Error::other(format!(
239            "{name} exceeds its {max}-character limit"
240        )));
241    }
242    Ok(())
243}
244
245pub(crate) fn write_properties_network(
246    properties: &[ProfileProperty],
247    writer: &mut impl Write,
248) -> Result<()> {
249    validate_properties(properties)?;
250    VarInt(properties.len() as i32).write(writer)?;
251    for property in properties {
252        write_string(&property.name, MAX_PROPERTY_NAME, writer)?;
253        write_string(&property.value, MAX_PROPERTY_VALUE, writer)?;
254        property.signature.is_some().write(writer)?;
255        if let Some(signature) = &property.signature {
256            write_string(signature, MAX_PROPERTY_SIGNATURE, writer)?;
257        }
258    }
259    Ok(())
260}
261
262pub(crate) fn read_properties_network(data: &mut Cursor<&[u8]>) -> Result<Vec<ProfileProperty>> {
263    let count = VarInt::read(data)?.0;
264    let count = usize::try_from(count)
265        .map_err(|_| Error::other(format!("Negative profile property count: {count}")))?;
266    if count > MAX_PROPERTIES {
267        return Err(Error::other("A profile may contain at most 16 properties"));
268    }
269    let mut properties = Vec::with_capacity(count);
270    for _ in 0..count {
271        properties.push(ProfileProperty::new(
272            read_string(data, MAX_PROPERTY_NAME)?,
273            read_string(data, MAX_PROPERTY_VALUE)?,
274            if bool::read(data)? {
275                Some(read_string(data, MAX_PROPERTY_SIGNATURE)?)
276            } else {
277                None
278            },
279        )?);
280    }
281    Ok(properties)
282}
283
284pub(crate) fn write_string(value: &str, max: usize, writer: &mut impl Write) -> Result<()> {
285    validate_string(value, max, "Profile string")?;
286    value.write_prefixed::<VarInt>(writer)
287}
288
289pub(crate) fn read_string(data: &mut Cursor<&[u8]>, max: usize) -> Result<String> {
290    let value = String::read_prefixed_bound::<VarInt>(data, max.saturating_mul(3))?;
291    validate_string(&value, max, "Profile string")?;
292    Ok(value)
293}
294
295impl ProfileProperty {
296    fn to_nbt_compound(&self) -> NbtCompound {
297        let mut compound = NbtCompound::new();
298        compound.insert("name", self.name().to_owned());
299        compound.insert("value", self.value().to_owned());
300        if let Some(signature) = self.signature() {
301            compound.insert("signature", signature.to_owned());
302        }
303        compound
304    }
305
306    fn from_nbt_compound(compound: &NbtCompound) -> Option<Self> {
307        Self::new(
308            compound.get("name")?.string()?.to_string(),
309            compound.get("value")?.string()?.to_string(),
310            match compound.get("signature") {
311                Some(tag) => Some(tag.string()?.to_string()),
312                None => None,
313            },
314        )
315        .ok()
316    }
317}
318
319impl HashComponent for ProfileProperty {
320    fn hash_component(&self, hasher: &mut ComponentHasher) {
321        let mut entries = Vec::with_capacity(3);
322        push_hash_entry(&mut entries, "name", self.name());
323        push_hash_entry(&mut entries, "value", self.value());
324        if let Some(signature) = self.signature() {
325            push_hash_entry(&mut entries, "signature", signature);
326        }
327        hash_entries(hasher, &mut entries);
328    }
329}
330
331pub(crate) fn insert_properties(compound: &mut NbtCompound, properties: &[ProfileProperty]) {
332    if !properties.is_empty() {
333        compound.insert(
334            "properties",
335            NbtList::Compound(
336                properties
337                    .iter()
338                    .map(ProfileProperty::to_nbt_compound)
339                    .collect(),
340            ),
341        );
342    }
343}
344
345pub(crate) fn read_properties(tag: Option<&NbtTag>) -> Option<Vec<ProfileProperty>> {
346    let Some(tag) = tag else {
347        return Some(Vec::new());
348    };
349    if let Some(list) = tag.list() {
350        let list = list.as_nbt_tags();
351        if list.len() > MAX_PROPERTIES {
352            return None;
353        }
354        return list
355            .iter()
356            .map(|tag| ProfileProperty::from_nbt_compound(tag.compound()?))
357            .collect();
358    }
359
360    let map = tag.compound()?;
361    if map.len() > MAX_PROPERTIES {
362        return None;
363    }
364    let mut properties = Vec::new();
365    for (name, values) in map.iter() {
366        let values = values.list()?;
367        match values {
368            NbtList::Empty => {}
369            NbtList::String(values) => {
370                for value in values {
371                    properties.push(
372                        ProfileProperty::new(name.to_string(), value.to_string(), None).ok()?,
373                    );
374                }
375            }
376            _ => return None,
377        }
378        if properties.len() > MAX_PROPERTIES {
379            return None;
380        }
381    }
382    Some(properties)
383}
384
385struct ProfilePropertyList<'a>(&'a [ProfileProperty]);
386
387impl HashComponent for ProfilePropertyList<'_> {
388    fn hash_component(&self, hasher: &mut ComponentHasher) {
389        hasher.start_list();
390        for property in self.0 {
391            hasher.put_component_hash(property);
392        }
393        hasher.end_list();
394    }
395}
396
397pub(crate) fn push_properties_hash_entry(
398    entries: &mut Vec<HashEntry>,
399    properties: &[ProfileProperty],
400) {
401    if !properties.is_empty() {
402        push_hash_entry(entries, "properties", &ProfilePropertyList(properties));
403    }
404}
405
406pub(crate) fn push_hash_entry<T: HashComponent + ?Sized>(
407    entries: &mut Vec<HashEntry>,
408    key: &str,
409    value: &T,
410) {
411    let mut key_hasher = ComponentHasher::new();
412    key_hasher.put_string(key);
413    let mut value_hasher = ComponentHasher::new();
414    value.hash_component(&mut value_hasher);
415    entries.push(HashEntry::new(key_hasher, value_hasher));
416}
417
418pub(crate) fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
419    sort_map_entries(entries);
420    hasher.start_map();
421    for entry in entries {
422        hasher.put_raw_bytes(&entry.key_bytes);
423        hasher.put_raw_bytes(&entry.value_bytes);
424    }
425    hasher.end_map();
426}
427
428#[cfg(test)]
429mod tests {
430    use std::io::Cursor;
431
432    use uuid::Uuid;
433
434    use super::{GameProfile, ProfileProperty, read_properties_network, write_properties_network};
435
436    #[test]
437    fn properties_use_authlib_multimap_order_and_equality() {
438        let first = GameProfile::new(
439            Uuid::nil(),
440            "Alex".to_owned(),
441            vec![
442                ProfileProperty::new("textures".to_owned(), "first".to_owned(), None)
443                    .expect("valid property"),
444                ProfileProperty::new("cape".to_owned(), "only".to_owned(), None)
445                    .expect("valid property"),
446                ProfileProperty::new("textures".to_owned(), "second".to_owned(), None)
447                    .expect("valid property"),
448            ],
449        )
450        .expect("valid profile");
451        let cross_key_reordered = GameProfile::new(
452            Uuid::nil(),
453            "Alex".to_owned(),
454            vec![
455                ProfileProperty::new("cape".to_owned(), "only".to_owned(), None)
456                    .expect("valid property"),
457                ProfileProperty::new("textures".to_owned(), "first".to_owned(), None)
458                    .expect("valid property"),
459                ProfileProperty::new("textures".to_owned(), "second".to_owned(), None)
460                    .expect("valid property"),
461            ],
462        )
463        .expect("valid profile");
464        let same_key_reordered = GameProfile::new(
465            Uuid::nil(),
466            "Alex".to_owned(),
467            vec![
468                ProfileProperty::new("textures".to_owned(), "second".to_owned(), None)
469                    .expect("valid property"),
470                ProfileProperty::new("textures".to_owned(), "first".to_owned(), None)
471                    .expect("valid property"),
472                ProfileProperty::new("cape".to_owned(), "only".to_owned(), None)
473                    .expect("valid property"),
474            ],
475        )
476        .expect("valid profile");
477
478        assert_eq!(
479            first
480                .properties()
481                .iter()
482                .map(|property| (property.name(), property.value()))
483                .collect::<Vec<_>>(),
484            vec![
485                ("textures", "first"),
486                ("textures", "second"),
487                ("cape", "only")
488            ]
489        );
490        assert_eq!(first, cross_key_reordered);
491        assert_ne!(first, same_key_reordered);
492
493        let mut network = Vec::new();
494        write_properties_network(first.properties(), &mut network)
495            .expect("properties should encode");
496        let decoded = read_properties_network(&mut Cursor::new(network.as_slice()))
497            .expect("properties should decode");
498        assert_eq!(decoded, first.properties());
499    }
500}