1use 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#[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#[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#[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 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#[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}