Skip to main content

mcproto_types/profile/
game_profile.rs

1//! Minecraft player game profiles.
2
3use std::{fmt, io::Read};
4
5use mcproto_codec::{
6    error::{CodecError, CodecKind, InvalidEncodingReason},
7    varint::{VarIntRead, VarIntWrite},
8};
9
10use crate::{
11    TypeCodec, TypeStructCodec,
12    basic::{Uuid, decode_prefixed_string, encode_prefixed_string},
13    contextual::PrefixedOptional,
14};
15
16/// Maximum number of properties in a game profile.
17pub const MAX_GAME_PROFILE_PROPERTIES: usize = 16;
18
19/// A protocol string with a game-profile-specific UTF-16 length limit.
20///
21/// Use the aliases [`GameProfileUsername`], [`GameProfilePropertyName`],
22/// [`GameProfilePropertyValue`], and [`GameProfilePropertySignature`] rather
23/// than spelling the const generic directly.
24#[repr(transparent)]
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
26pub struct GameProfileString<const MAX_UTF16_CODE_UNITS: usize>(String);
27
28impl<const MAX_UTF16_CODE_UNITS: usize> GameProfileString<MAX_UTF16_CODE_UNITS> {
29    /// Maximum number of UTF-16 code units accepted by this string type.
30    pub const MAX_UTF16_CODE_UNITS: usize = MAX_UTF16_CODE_UNITS;
31    /// Maximum UTF-8 payload size accepted by this string type.
32    pub const MAX_BYTES: usize = MAX_UTF16_CODE_UNITS.saturating_mul(3);
33
34    /// Creates a string after checking its UTF-16 code-unit limit.
35    pub fn new(value: impl Into<String>) -> Result<Self, GameProfileStringTooLong> {
36        let value = value.into();
37        let actual_code_units = value.encode_utf16().count();
38        if actual_code_units > MAX_UTF16_CODE_UNITS {
39            return Err(GameProfileStringTooLong {
40                max_code_units: MAX_UTF16_CODE_UNITS,
41                actual_code_units,
42            });
43        }
44        Ok(Self(value))
45    }
46
47    /// Returns the string value.
48    #[must_use]
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52
53    /// Extracts the owned string.
54    #[must_use]
55    pub fn into_inner(self) -> String {
56        self.0
57    }
58}
59
60impl<const MAX_UTF16_CODE_UNITS: usize> AsRef<str> for GameProfileString<MAX_UTF16_CODE_UNITS> {
61    fn as_ref(&self) -> &str {
62        self.as_str()
63    }
64}
65
66impl<const MAX_UTF16_CODE_UNITS: usize> fmt::Display for GameProfileString<MAX_UTF16_CODE_UNITS> {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        formatter.write_str(self.as_str())
69    }
70}
71
72impl<const MAX_UTF16_CODE_UNITS: usize> TryFrom<String>
73    for GameProfileString<MAX_UTF16_CODE_UNITS>
74{
75    type Error = GameProfileStringTooLong;
76
77    fn try_from(value: String) -> Result<Self, Self::Error> {
78        Self::new(value)
79    }
80}
81
82impl<const MAX_UTF16_CODE_UNITS: usize> TryFrom<&str> for GameProfileString<MAX_UTF16_CODE_UNITS> {
83    type Error = GameProfileStringTooLong;
84
85    fn try_from(value: &str) -> Result<Self, Self::Error> {
86        Self::new(value)
87    }
88}
89
90impl<const MAX_UTF16_CODE_UNITS: usize> From<GameProfileString<MAX_UTF16_CODE_UNITS>> for String {
91    fn from(value: GameProfileString<MAX_UTF16_CODE_UNITS>) -> Self {
92        value.into_inner()
93    }
94}
95
96impl<const MAX_UTF16_CODE_UNITS: usize> TypeCodec for GameProfileString<MAX_UTF16_CODE_UNITS> {
97    fn encode(&self, writer: &mut impl std::io::Write) -> Result<(), CodecError> {
98        encode_prefixed_string(
99            &self.0,
100            writer,
101            CodecKind::String,
102            Self::MAX_BYTES,
103            MAX_UTF16_CODE_UNITS,
104        )
105    }
106
107    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
108        decode_prefixed_string(
109            reader,
110            CodecKind::String,
111            Self::MAX_BYTES,
112            MAX_UTF16_CODE_UNITS,
113        )
114        .map(|(value, _)| Self(value))
115    }
116}
117
118/// Error returned when a game-profile string exceeds its field limit.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub struct GameProfileStringTooLong {
121    /// Maximum permitted number of UTF-16 code units.
122    pub max_code_units: usize,
123    /// Number of UTF-16 code units in the rejected value.
124    pub actual_code_units: usize,
125}
126
127impl fmt::Display for GameProfileStringTooLong {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(
130            formatter,
131            "game-profile string contains {} UTF-16 code units; maximum is {}",
132            self.actual_code_units, self.max_code_units
133        )
134    }
135}
136
137impl std::error::Error for GameProfileStringTooLong {}
138
139/// A game profile username, limited to 16 UTF-16 code units.
140pub type GameProfileUsername = GameProfileString<16>;
141/// A game profile property name, limited to 64 UTF-16 code units.
142pub type GameProfilePropertyName = GameProfileString<64>;
143/// A game profile property value, limited to 32,767 UTF-16 code units.
144pub type GameProfilePropertyValue = GameProfileString<32767>;
145/// A game profile property signature, limited to 1024 UTF-16 code units.
146pub type GameProfilePropertySignature = GameProfileString<1024>;
147
148/// One property attached to a [`GameProfile`].
149#[derive(Debug, Clone, PartialEq, Eq, Hash, TypeStructCodec)]
150#[type_struct_codec(kind = GameProfileProperty)]
151pub struct GameProfileProperty {
152    /// Property name, commonly `textures`.
153    pub name: GameProfilePropertyName,
154    /// Property value, commonly Base64-encoded JSON.
155    pub value: GameProfilePropertyValue,
156    /// Optional cryptographic signature for the property value.
157    pub signature: PrefixedOptional<GameProfilePropertySignature>,
158}
159
160impl GameProfileProperty {
161    /// Creates an unsigned game profile property.
162    #[must_use]
163    pub const fn new(name: GameProfilePropertyName, value: GameProfilePropertyValue) -> Self {
164        Self {
165            name,
166            value,
167            signature: PrefixedOptional::none(),
168        }
169    }
170}
171
172/// A length-prefixed game profile property list containing at most 16 entries.
173#[repr(transparent)]
174#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
175pub struct GameProfileProperties(Vec<GameProfileProperty>);
176
177impl GameProfileProperties {
178    /// Maximum number of properties permitted by the protocol.
179    pub const MAX_LEN: usize = MAX_GAME_PROFILE_PROPERTIES;
180
181    /// Creates a property list after enforcing the 16-entry limit.
182    pub fn new(properties: Vec<GameProfileProperty>) -> Result<Self, TooManyGameProfileProperties> {
183        if properties.len() > Self::MAX_LEN {
184            return Err(TooManyGameProfileProperties {
185                actual: properties.len(),
186            });
187        }
188        Ok(Self(properties))
189    }
190
191    /// Returns the number of properties.
192    #[must_use]
193    pub const fn len(&self) -> usize {
194        self.0.len()
195    }
196
197    /// Returns whether the property list is empty.
198    #[must_use]
199    pub const fn is_empty(&self) -> bool {
200        self.0.is_empty()
201    }
202
203    /// Returns the properties as a slice.
204    #[must_use]
205    pub const fn as_slice(&self) -> &[GameProfileProperty] {
206        self.0.as_slice()
207    }
208
209    /// Extracts the property vector.
210    #[must_use]
211    pub fn into_vec(self) -> Vec<GameProfileProperty> {
212        self.0
213    }
214
215    /// Adds one property if the protocol limit has not been reached.
216    pub fn push(
217        &mut self,
218        property: GameProfileProperty,
219    ) -> Result<(), TooManyGameProfileProperties> {
220        if self.len() == Self::MAX_LEN {
221            return Err(TooManyGameProfileProperties {
222                actual: self.len() + 1,
223            });
224        }
225        self.0.push(property);
226        Ok(())
227    }
228}
229
230impl TryFrom<Vec<GameProfileProperty>> for GameProfileProperties {
231    type Error = TooManyGameProfileProperties;
232
233    fn try_from(value: Vec<GameProfileProperty>) -> Result<Self, Self::Error> {
234        Self::new(value)
235    }
236}
237
238impl From<GameProfileProperties> for Vec<GameProfileProperty> {
239    fn from(value: GameProfileProperties) -> Self {
240        value.into_vec()
241    }
242}
243
244impl TypeCodec for GameProfileProperties {
245    fn encode(&self, writer: &mut impl std::io::Write) -> Result<(), CodecError> {
246        writer
247            .write_varint(self.len() as i32)
248            .map_err(|error| error.with_context(CodecKind::PrefixedArray))?;
249        for property in &self.0 {
250            property
251                .encode(writer)
252                .map_err(|error| error.with_context(CodecKind::PrefixedArray))?;
253        }
254        Ok(())
255    }
256
257    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
258        let (length, prefix_size) = reader
259            .read_varint_with_size()
260            .map_err(|error| error.with_context(CodecKind::PrefixedArray))?;
261        if length < 0 {
262            return Err(CodecError::invalid_encoding(
263                CodecKind::PrefixedArray,
264                prefix_size,
265                InvalidEncodingReason::NegativeLength { value: length },
266            ));
267        }
268        let length = length as usize;
269        if length > Self::MAX_LEN {
270            return Err(CodecError::invalid_encoding(
271                CodecKind::PrefixedArray,
272                prefix_size,
273                InvalidEncodingReason::LengthOutOfRange {
274                    max: Self::MAX_LEN,
275                    actual: length,
276                },
277            ));
278        }
279
280        let mut properties = Vec::with_capacity(length);
281        for _ in 0..length {
282            properties.push(
283                GameProfileProperty::decode(reader)
284                    .map_err(|error| error.with_context(CodecKind::PrefixedArray))?,
285            );
286        }
287        Ok(Self(properties))
288    }
289}
290
291/// Error returned when a game profile contains more than 16 properties.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
293pub struct TooManyGameProfileProperties {
294    /// Number of properties in the rejected list.
295    pub actual: usize,
296}
297
298impl fmt::Display for TooManyGameProfileProperties {
299    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
300        write!(
301            formatter,
302            "game profile contains {} properties; maximum is {}",
303            self.actual, MAX_GAME_PROFILE_PROPERTIES
304        )
305    }
306}
307
308impl std::error::Error for TooManyGameProfileProperties {}
309
310/// A Minecraft player profile.
311///
312/// The protocol encodes a UUID, a username limited to 16 UTF-16 code units,
313/// and a VarInt-prefixed list of at most 16 [`GameProfileProperty`] values.
314///
315/// # Examples
316///
317/// ```
318/// use mcproto_types::{
319///     GameProfile, GameProfileProperty, GameProfilePropertyName,
320///     GameProfilePropertyValue, GameProfileUsername, TypeCodec, Uuid,
321/// };
322///
323/// let mut profile = GameProfile::new(
324///     Uuid::from_bytes([0; 16]),
325///     GameProfileUsername::new("Player")?,
326/// );
327/// profile.properties.push(GameProfileProperty::new(
328///     GameProfilePropertyName::new("textures")?,
329///     GameProfilePropertyValue::new("base64-value")?,
330/// ))?;
331///
332/// let mut encoded = Vec::new();
333/// profile.encode(&mut encoded)?;
334/// let mut input = encoded.as_slice();
335/// assert_eq!(GameProfile::decode(&mut input)?, profile);
336/// # Ok::<(), Box<dyn std::error::Error>>(())
337/// ```
338///
339/// See the official [Game Profile] protocol documentation.
340///
341/// [Game Profile]: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Game_Profile
342#[derive(Debug, Clone, PartialEq, Eq, Hash, TypeStructCodec)]
343#[type_struct_codec(kind = GameProfile)]
344pub struct GameProfile {
345    /// Universally unique player identifier.
346    pub uuid: Uuid,
347    /// Player username, limited to 16 UTF-16 code units.
348    pub username: GameProfileUsername,
349    /// Signed or unsigned player properties, limited to 16 entries.
350    pub properties: GameProfileProperties,
351}
352
353impl GameProfile {
354    /// Creates a game profile with no properties.
355    #[must_use]
356    pub const fn new(uuid: Uuid, username: GameProfileUsername) -> Self {
357        Self {
358            uuid,
359            username,
360            properties: GameProfileProperties(Vec::new()),
361        }
362    }
363}