1use std::io::{Cursor, Error, Result, Write};
4use std::ops::Deref;
5
6use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::Identifier;
9use steel_utils::UuidExt as _;
10use steel_utils::codec::VarInt;
11use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
12use steel_utils::serial::{PrefixedRead as _, PrefixedWrite as _, ReadFrom, WriteTo};
13use uuid::Uuid;
14
15const MAX_PROPERTIES: usize = 16;
16const MAX_PROPERTY_NAME: usize = 64;
17const MAX_PROPERTY_VALUE: usize = 32_767;
18const MAX_PROPERTY_SIGNATURE: usize = 1_024;
19const MAX_PLAYER_NAME: usize = 16;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ProfileProperty {
24 name: String,
25 value: String,
26 signature: Option<String>,
27}
28
29impl ProfileProperty {
30 pub fn new(name: String, value: String, signature: Option<String>) -> Result<Self> {
31 validate_string(&name, MAX_PROPERTY_NAME, "Profile property name")?;
32 validate_string(&value, MAX_PROPERTY_VALUE, "Profile property value")?;
33 if let Some(signature) = &signature {
34 validate_string(
35 signature,
36 MAX_PROPERTY_SIGNATURE,
37 "Profile property signature",
38 )?;
39 }
40 Ok(Self {
41 name,
42 value,
43 signature,
44 })
45 }
46
47 #[must_use]
48 pub fn name(&self) -> &str {
49 &self.name
50 }
51
52 #[must_use]
53 pub fn value(&self) -> &str {
54 &self.value
55 }
56
57 #[must_use]
58 pub fn signature(&self) -> Option<&str> {
59 self.signature.as_deref()
60 }
61
62 fn to_nbt_compound(&self) -> NbtCompound {
63 let mut compound = NbtCompound::new();
64 compound.insert("name", self.name.clone());
65 compound.insert("value", self.value.clone());
66 if let Some(signature) = &self.signature {
67 compound.insert("signature", signature.clone());
68 }
69 compound
70 }
71
72 fn from_nbt_compound(compound: &NbtCompound) -> Option<Self> {
73 Self::new(
74 compound.get("name")?.string()?.to_string(),
75 compound.get("value")?.string()?.to_string(),
76 match compound.get("signature") {
77 Some(tag) => Some(tag.string()?.to_string()),
78 None => None,
79 },
80 )
81 .ok()
82 }
83}
84
85impl HashComponent for ProfileProperty {
86 fn hash_component(&self, hasher: &mut ComponentHasher) {
87 let mut entries = Vec::with_capacity(3);
88 push_hash_entry(&mut entries, "name", &self.name);
89 push_hash_entry(&mut entries, "value", &self.value);
90 if let Some(signature) = &self.signature {
91 push_hash_entry(&mut entries, "signature", signature);
92 }
93 hash_entries(hasher, &mut entries);
94 }
95}
96
97#[derive(Debug, Default, Clone)]
99struct ProfileProperties(Vec<ProfileProperty>);
100
101impl ProfileProperties {
102 fn new(properties: Vec<ProfileProperty>) -> Result<Self> {
103 validate_properties(&properties)?;
104 let mut grouped = Vec::with_capacity(properties.len());
105 for property in properties {
106 let insertion_index = grouped
107 .iter()
108 .rposition(|existing: &ProfileProperty| existing.name() == property.name())
109 .map(|index| index + 1);
110 if let Some(index) = insertion_index {
111 grouped.insert(index, property);
112 } else {
113 grouped.push(property);
114 }
115 }
116 Ok(Self(grouped))
117 }
118}
119
120impl Deref for ProfileProperties {
121 type Target = [ProfileProperty];
122
123 fn deref(&self) -> &Self::Target {
124 &self.0
125 }
126}
127
128impl PartialEq for ProfileProperties {
129 fn eq(&self, other: &Self) -> bool {
130 if self.len() != other.len() {
131 return false;
132 }
133
134 let mut left_start = 0;
135 while let Some(first) = self.get(left_start) {
136 let name = first.name();
137 let left_end = property_group_end(self, left_start);
138 let Some(right_start) = other.iter().position(|property| property.name() == name)
139 else {
140 return false;
141 };
142 let right_end = property_group_end(other, right_start);
143 if self[left_start..left_end] != other[right_start..right_end] {
144 return false;
145 }
146 left_start = left_end;
147 }
148 true
149 }
150}
151
152impl Eq for ProfileProperties {}
153
154fn property_group_end(properties: &[ProfileProperty], start: usize) -> usize {
155 let Some(first) = properties.get(start) else {
156 return start;
157 };
158 properties[start + 1..]
159 .iter()
160 .position(|property| property.name() != first.name())
161 .map_or(properties.len(), |offset| start + offset + 1)
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct StoredGameProfile {
167 id: Uuid,
168 name: String,
169 properties: ProfileProperties,
170}
171
172impl StoredGameProfile {
173 pub fn new(id: Uuid, name: String, properties: Vec<ProfileProperty>) -> Result<Self> {
174 validate_player_name(&name)?;
175 Ok(Self {
176 id,
177 name,
178 properties: ProfileProperties::new(properties)?,
179 })
180 }
181
182 #[must_use]
183 pub const fn id(&self) -> Uuid {
184 self.id
185 }
186
187 #[must_use]
188 pub fn name(&self) -> &str {
189 &self.name
190 }
191
192 #[must_use]
193 pub fn properties(&self) -> &[ProfileProperty] {
194 &self.properties
195 }
196}
197
198#[derive(Debug, Default, Clone, PartialEq, Eq)]
200pub struct PartialProfile {
201 name: Option<String>,
202 id: Option<Uuid>,
203 properties: ProfileProperties,
204}
205
206impl PartialProfile {
207 pub fn new(
208 name: Option<String>,
209 id: Option<Uuid>,
210 properties: Vec<ProfileProperty>,
211 ) -> Result<Self> {
212 if let Some(name) = &name {
213 validate_player_name(name)?;
214 }
215 Ok(Self {
216 name,
217 id,
218 properties: ProfileProperties::new(properties)?,
219 })
220 }
221
222 #[must_use]
223 pub fn name(&self) -> Option<&str> {
224 self.name.as_deref()
225 }
226
227 #[must_use]
228 pub const fn id(&self) -> Option<Uuid> {
229 self.id
230 }
231
232 #[must_use]
233 pub fn properties(&self) -> &[ProfileProperty] {
234 &self.properties
235 }
236}
237
238#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
240pub enum PlayerModelType {
241 Slim,
242 #[default]
243 Wide,
244}
245
246impl PlayerModelType {
247 #[must_use]
248 pub const fn serialized_name(self) -> &'static str {
249 match self {
250 Self::Slim => "slim",
251 Self::Wide => "wide",
252 }
253 }
254
255 const fn from_serialized_name(name: &str) -> Option<Self> {
256 match name {
257 "slim" => Some(Self::Slim),
258 "wide" => Some(Self::Wide),
259 _ => None,
260 }
261 }
262}
263
264#[derive(Debug, Default, Clone, PartialEq, Eq)]
266pub struct PlayerSkinPatch {
267 texture: Option<Identifier>,
268 cape: Option<Identifier>,
269 elytra: Option<Identifier>,
270 model: Option<PlayerModelType>,
271}
272
273impl PlayerSkinPatch {
274 #[must_use]
275 pub const fn new(
276 texture: Option<Identifier>,
277 cape: Option<Identifier>,
278 elytra: Option<Identifier>,
279 model: Option<PlayerModelType>,
280 ) -> Self {
281 Self {
282 texture,
283 cape,
284 elytra,
285 model,
286 }
287 }
288
289 #[must_use]
290 pub const fn texture(&self) -> Option<&Identifier> {
291 self.texture.as_ref()
292 }
293
294 #[must_use]
295 pub const fn cape(&self) -> Option<&Identifier> {
296 self.cape.as_ref()
297 }
298
299 #[must_use]
300 pub const fn elytra(&self) -> Option<&Identifier> {
301 self.elytra.as_ref()
302 }
303
304 #[must_use]
305 pub const fn model(&self) -> Option<PlayerModelType> {
306 self.model
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
312pub enum ResolvableProfileContents {
313 DynamicName(String),
314 DynamicId(Uuid),
315 StaticFull(StoredGameProfile),
316 StaticPartial(PartialProfile),
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct ResolvableProfile {
322 contents: ResolvableProfileContents,
323 skin_patch: PlayerSkinPatch,
324}
325
326impl Default for ResolvableProfile {
327 fn default() -> Self {
328 Self::static_partial(PartialProfile::default(), PlayerSkinPatch::default())
329 }
330}
331
332impl ResolvableProfile {
333 pub fn dynamic_name(name: String, skin_patch: PlayerSkinPatch) -> Result<Self> {
334 validate_player_name(&name)?;
335 Ok(Self {
336 contents: ResolvableProfileContents::DynamicName(name),
337 skin_patch,
338 })
339 }
340
341 #[must_use]
342 pub const fn dynamic_id(id: Uuid, skin_patch: PlayerSkinPatch) -> Self {
343 Self {
344 contents: ResolvableProfileContents::DynamicId(id),
345 skin_patch,
346 }
347 }
348
349 #[must_use]
350 pub const fn static_full(profile: StoredGameProfile, skin_patch: PlayerSkinPatch) -> Self {
351 Self {
352 contents: ResolvableProfileContents::StaticFull(profile),
353 skin_patch,
354 }
355 }
356
357 #[must_use]
358 pub const fn static_partial(profile: PartialProfile, skin_patch: PlayerSkinPatch) -> Self {
359 Self {
360 contents: ResolvableProfileContents::StaticPartial(profile),
361 skin_patch,
362 }
363 }
364
365 #[must_use]
366 pub const fn contents(&self) -> &ResolvableProfileContents {
367 &self.contents
368 }
369
370 #[must_use]
371 pub const fn skin_patch(&self) -> &PlayerSkinPatch {
372 &self.skin_patch
373 }
374
375 fn from_partial(profile: PartialProfile, skin_patch: PlayerSkinPatch) -> Self {
376 if profile.properties.is_empty() {
377 match (&profile.name, profile.id) {
378 (Some(name), None) => {
379 return Self {
380 contents: ResolvableProfileContents::DynamicName(name.clone()),
381 skin_patch,
382 };
383 }
384 (None, Some(id)) => return Self::dynamic_id(id, skin_patch),
385 (Some(_), Some(_)) | (None, None) => {}
386 }
387 }
388 Self::static_partial(profile, skin_patch)
389 }
390
391 fn to_nbt_tag_ref(&self) -> NbtTag {
392 let mut compound = NbtCompound::new();
393 match &self.contents {
394 ResolvableProfileContents::StaticFull(profile) => {
395 compound.insert("id", uuid_to_nbt(profile.id));
396 compound.insert("name", profile.name.clone());
397 insert_properties(&mut compound, &profile.properties);
398 }
399 ResolvableProfileContents::DynamicName(name) => {
400 compound.insert("name", name.clone());
401 }
402 ResolvableProfileContents::DynamicId(id) => {
403 compound.insert("id", uuid_to_nbt(*id));
404 }
405 ResolvableProfileContents::StaticPartial(profile) => {
406 if let Some(name) = &profile.name {
407 compound.insert("name", name.clone());
408 }
409 if let Some(id) = profile.id {
410 compound.insert("id", uuid_to_nbt(id));
411 }
412 insert_properties(&mut compound, &profile.properties);
413 }
414 }
415 insert_skin_patch(&mut compound, &self.skin_patch);
416 NbtTag::Compound(compound)
417 }
418
419 fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
420 if let Some(name) = tag.string() {
421 return Self::dynamic_name(name.to_string(), PlayerSkinPatch::default()).ok();
422 }
423
424 let compound = tag.compound()?;
425 let name = match compound.get("name") {
426 Some(tag) => Some(tag.string()?.to_string()),
427 None => None,
428 };
429 let id = match compound.get("id") {
430 Some(tag) => Some(uuid_from_nbt(tag)?),
431 None => None,
432 };
433 let properties = read_properties(compound.get("properties"))?;
434 let skin_patch = read_skin_patch(compound)?;
435
436 if let (Some(id), Some(name)) = (id, &name) {
437 return Some(Self::static_full(
438 StoredGameProfile::new(id, name.clone(), properties).ok()?,
439 skin_patch,
440 ));
441 }
442 let partial = PartialProfile::new(name, id, properties).ok()?;
443 Some(Self::from_partial(partial, skin_patch))
444 }
445}
446
447impl WriteTo for ResolvableProfile {
448 fn write(&self, writer: &mut impl Write) -> Result<()> {
449 match &self.contents {
450 ResolvableProfileContents::StaticFull(profile) => {
451 true.write(writer)?;
452 write_full_profile(profile, writer)?;
453 }
454 ResolvableProfileContents::DynamicName(name) => {
455 false.write(writer)?;
456 write_partial_profile_fields(Some(name), None, &[], writer)?;
457 }
458 ResolvableProfileContents::DynamicId(id) => {
459 false.write(writer)?;
460 write_partial_profile_fields(None, Some(*id), &[], writer)?;
461 }
462 ResolvableProfileContents::StaticPartial(profile) => {
463 false.write(writer)?;
464 write_partial_profile(profile, writer)?;
465 }
466 }
467 write_skin_patch(&self.skin_patch, writer)
468 }
469}
470
471impl ReadFrom for ResolvableProfile {
472 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
473 let contents = if bool::read(data)? {
474 ResolvableProfileContents::StaticFull(read_full_profile(data)?)
475 } else {
476 let partial = read_partial_profile(data)?;
477 let patch = read_skin_patch_network(data)?;
478 return Ok(Self::from_partial(partial, patch));
479 };
480 Ok(Self {
481 contents,
482 skin_patch: read_skin_patch_network(data)?,
483 })
484 }
485}
486
487impl ToNbtTag for ResolvableProfile {
488 fn to_nbt_tag(self) -> NbtTag {
489 self.to_nbt_tag_ref()
490 }
491}
492
493impl FromNbtTag for ResolvableProfile {
494 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
495 Self::from_owned_nbt(&tag.to_owned())
496 }
497}
498
499impl HashComponent for ResolvableProfile {
500 fn hash_component(&self, hasher: &mut ComponentHasher) {
501 let mut entries = Vec::new();
502 match &self.contents {
503 ResolvableProfileContents::StaticFull(profile) => {
504 push_uuid_hash_entry(&mut entries, "id", profile.id);
505 push_hash_entry(&mut entries, "name", &profile.name);
506 push_properties_hash_entry(&mut entries, &profile.properties);
507 }
508 ResolvableProfileContents::DynamicName(name) => {
509 push_hash_entry(&mut entries, "name", name);
510 }
511 ResolvableProfileContents::DynamicId(id) => {
512 push_uuid_hash_entry(&mut entries, "id", *id);
513 }
514 ResolvableProfileContents::StaticPartial(profile) => {
515 if let Some(name) = &profile.name {
516 push_hash_entry(&mut entries, "name", name);
517 }
518 if let Some(id) = profile.id {
519 push_uuid_hash_entry(&mut entries, "id", id);
520 }
521 push_properties_hash_entry(&mut entries, &profile.properties);
522 }
523 }
524 if let Some(texture) = &self.skin_patch.texture {
525 push_hash_entry(&mut entries, "texture", texture);
526 }
527 if let Some(cape) = &self.skin_patch.cape {
528 push_hash_entry(&mut entries, "cape", cape);
529 }
530 if let Some(elytra) = &self.skin_patch.elytra {
531 push_hash_entry(&mut entries, "elytra", elytra);
532 }
533 if let Some(model) = self.skin_patch.model {
534 push_hash_entry(&mut entries, "model", model.serialized_name());
535 }
536 hash_entries(hasher, &mut entries);
537 }
538}
539
540fn validate_player_name(name: &str) -> Result<()> {
541 if name.encode_utf16().count() > MAX_PLAYER_NAME
542 || name
543 .chars()
544 .any(|character| character <= ' ' || character >= '\u{7f}')
545 {
546 return Err(Error::other("Invalid player profile name"));
547 }
548 Ok(())
549}
550
551fn validate_properties(properties: &[ProfileProperty]) -> Result<()> {
552 if properties.len() > MAX_PROPERTIES {
553 return Err(Error::other("A profile may contain at most 16 properties"));
554 }
555 Ok(())
556}
557
558fn validate_string(value: &str, max: usize, name: &str) -> Result<()> {
559 if value.encode_utf16().count() > max || value.len() > max.saturating_mul(3) {
560 return Err(Error::other(format!(
561 "{name} exceeds its {max}-character limit"
562 )));
563 }
564 Ok(())
565}
566
567fn uuid_to_nbt(id: Uuid) -> NbtTag {
568 NbtTag::IntArray(id.to_int_array().to_vec())
569}
570
571fn uuid_from_nbt(tag: &NbtTag) -> Option<Uuid> {
572 Uuid::from_int_array(tag.int_array()?)
573}
574
575fn insert_properties(compound: &mut NbtCompound, properties: &[ProfileProperty]) {
576 if !properties.is_empty() {
577 compound.insert(
578 "properties",
579 NbtList::Compound(
580 properties
581 .iter()
582 .map(ProfileProperty::to_nbt_compound)
583 .collect(),
584 ),
585 );
586 }
587}
588
589fn read_properties(tag: Option<&NbtTag>) -> Option<Vec<ProfileProperty>> {
590 let Some(tag) = tag else {
591 return Some(Vec::new());
592 };
593 if let Some(list) = tag.list() {
594 let list = list.as_nbt_tags();
595 if list.len() > MAX_PROPERTIES {
596 return None;
597 }
598 return list
599 .iter()
600 .map(|tag| ProfileProperty::from_nbt_compound(tag.compound()?))
601 .collect();
602 }
603
604 let map = tag.compound()?;
605 if map.len() > MAX_PROPERTIES {
606 return None;
607 }
608 let mut properties = Vec::new();
609 for (name, values) in map.iter() {
610 let values = values.list()?;
611 match values {
612 NbtList::Empty => {}
613 NbtList::String(values) => {
614 for value in values {
615 properties.push(
616 ProfileProperty::new(name.to_string(), value.to_string(), None).ok()?,
617 );
618 }
619 }
620 _ => return None,
621 }
622 if properties.len() > MAX_PROPERTIES {
623 return None;
624 }
625 }
626 Some(properties)
627}
628
629fn insert_skin_patch(compound: &mut NbtCompound, patch: &PlayerSkinPatch) {
630 if let Some(texture) = &patch.texture {
631 compound.insert("texture", texture.to_string());
632 }
633 if let Some(cape) = &patch.cape {
634 compound.insert("cape", cape.to_string());
635 }
636 if let Some(elytra) = &patch.elytra {
637 compound.insert("elytra", elytra.to_string());
638 }
639 if let Some(model) = patch.model {
640 compound.insert("model", model.serialized_name());
641 }
642}
643
644fn read_skin_patch(compound: &NbtCompound) -> Option<PlayerSkinPatch> {
645 Some(PlayerSkinPatch::new(
646 read_optional_identifier(compound.get("texture"))?,
647 read_optional_identifier(compound.get("cape"))?,
648 read_optional_identifier(compound.get("elytra"))?,
649 match compound.get("model") {
650 Some(tag) => Some(PlayerModelType::from_serialized_name(
651 &tag.string()?.to_string(),
652 )?),
653 None => None,
654 },
655 ))
656}
657
658#[expect(
659 clippy::option_option,
660 reason = "the outer option reports codec failure while the inner option represents an absent field"
661)]
662fn read_optional_identifier(tag: Option<&NbtTag>) -> Option<Option<Identifier>> {
663 match tag {
664 Some(tag) => Some(Some(tag.string()?.to_string().parse().ok()?)),
665 None => Some(None),
666 }
667}
668
669fn write_full_profile(profile: &StoredGameProfile, writer: &mut impl Write) -> Result<()> {
670 profile.id.write(writer)?;
671 write_string(&profile.name, MAX_PLAYER_NAME, writer)?;
672 write_properties_network(&profile.properties, writer)
673}
674
675fn read_full_profile(data: &mut Cursor<&[u8]>) -> Result<StoredGameProfile> {
676 StoredGameProfile::new(
677 Uuid::read(data)?,
678 read_string(data, MAX_PLAYER_NAME)?,
679 read_properties_network(data)?,
680 )
681}
682
683fn write_partial_profile(profile: &PartialProfile, writer: &mut impl Write) -> Result<()> {
684 write_partial_profile_fields(
685 profile.name.as_deref(),
686 profile.id,
687 &profile.properties,
688 writer,
689 )
690}
691
692fn write_partial_profile_fields(
693 name: Option<&str>,
694 id: Option<Uuid>,
695 properties: &[ProfileProperty],
696 writer: &mut impl Write,
697) -> Result<()> {
698 name.is_some().write(writer)?;
699 if let Some(name) = name {
700 write_string(name, MAX_PLAYER_NAME, writer)?;
701 }
702 id.is_some().write(writer)?;
703 if let Some(id) = id {
704 id.write(writer)?;
705 }
706 write_properties_network(properties, writer)
707}
708
709fn read_partial_profile(data: &mut Cursor<&[u8]>) -> Result<PartialProfile> {
710 let name = if bool::read(data)? {
711 Some(read_string(data, MAX_PLAYER_NAME)?)
712 } else {
713 None
714 };
715 let id = if bool::read(data)? {
716 Some(Uuid::read(data)?)
717 } else {
718 None
719 };
720 PartialProfile::new(name, id, read_properties_network(data)?)
721}
722
723fn write_properties_network(properties: &[ProfileProperty], writer: &mut impl Write) -> Result<()> {
724 validate_properties(properties)?;
725 VarInt(properties.len() as i32).write(writer)?;
726 for property in properties {
727 write_string(&property.name, MAX_PROPERTY_NAME, writer)?;
728 write_string(&property.value, MAX_PROPERTY_VALUE, writer)?;
729 property.signature.is_some().write(writer)?;
730 if let Some(signature) = &property.signature {
731 write_string(signature, MAX_PROPERTY_SIGNATURE, writer)?;
732 }
733 }
734 Ok(())
735}
736
737fn read_properties_network(data: &mut Cursor<&[u8]>) -> Result<Vec<ProfileProperty>> {
738 let count = VarInt::read(data)?.0;
739 let count = usize::try_from(count)
740 .map_err(|_| Error::other(format!("Negative profile property count: {count}")))?;
741 if count > MAX_PROPERTIES {
742 return Err(Error::other("A profile may contain at most 16 properties"));
743 }
744 let mut properties = Vec::with_capacity(count);
745 for _ in 0..count {
746 properties.push(ProfileProperty::new(
747 read_string(data, MAX_PROPERTY_NAME)?,
748 read_string(data, MAX_PROPERTY_VALUE)?,
749 if bool::read(data)? {
750 Some(read_string(data, MAX_PROPERTY_SIGNATURE)?)
751 } else {
752 None
753 },
754 )?);
755 }
756 Ok(properties)
757}
758
759fn write_skin_patch(patch: &PlayerSkinPatch, writer: &mut impl Write) -> Result<()> {
760 for texture in [&patch.texture, &patch.cape, &patch.elytra] {
761 texture.is_some().write(writer)?;
762 if let Some(texture) = texture {
763 texture.write(writer)?;
764 }
765 }
766 patch.model.is_some().write(writer)?;
767 if let Some(model) = patch.model {
768 (model == PlayerModelType::Slim).write(writer)?;
769 }
770 Ok(())
771}
772
773fn read_skin_patch_network(data: &mut Cursor<&[u8]>) -> Result<PlayerSkinPatch> {
774 let texture = read_optional_identifier_network(data)?;
775 let cape = read_optional_identifier_network(data)?;
776 let elytra = read_optional_identifier_network(data)?;
777 let model = if bool::read(data)? {
778 Some(if bool::read(data)? {
779 PlayerModelType::Slim
780 } else {
781 PlayerModelType::Wide
782 })
783 } else {
784 None
785 };
786 Ok(PlayerSkinPatch::new(texture, cape, elytra, model))
787}
788
789fn read_optional_identifier_network(data: &mut Cursor<&[u8]>) -> Result<Option<Identifier>> {
790 if bool::read(data)? {
791 Ok(Some(Identifier::read(data)?))
792 } else {
793 Ok(None)
794 }
795}
796
797fn write_string(value: &str, max: usize, writer: &mut impl Write) -> Result<()> {
798 validate_string(value, max, "Profile string")?;
799 value.write_prefixed::<VarInt>(writer)
800}
801
802fn read_string(data: &mut Cursor<&[u8]>, max: usize) -> Result<String> {
803 let value = String::read_prefixed_bound::<VarInt>(data, max.saturating_mul(3))?;
804 validate_string(&value, max, "Profile string")?;
805 Ok(value)
806}
807
808struct ProfilePropertyList<'a>(&'a [ProfileProperty]);
809
810impl HashComponent for ProfilePropertyList<'_> {
811 fn hash_component(&self, hasher: &mut ComponentHasher) {
812 hasher.start_list();
813 for property in self.0 {
814 hasher.put_component_hash(property);
815 }
816 hasher.end_list();
817 }
818}
819
820fn push_uuid_hash_entry(entries: &mut Vec<HashEntry>, key: &str, id: Uuid) {
821 let values = id.to_int_array();
822 let mut key_hasher = ComponentHasher::new();
823 key_hasher.put_string(key);
824 let mut value_hasher = ComponentHasher::new();
825 value_hasher.put_int_array(&values);
826 entries.push(HashEntry::new(key_hasher, value_hasher));
827}
828
829fn push_properties_hash_entry(entries: &mut Vec<HashEntry>, properties: &[ProfileProperty]) {
830 if !properties.is_empty() {
831 push_hash_entry(entries, "properties", &ProfilePropertyList(properties));
832 }
833}
834
835fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
836 let mut key_hasher = ComponentHasher::new();
837 key_hasher.put_string(key);
838 let mut value_hasher = ComponentHasher::new();
839 value.hash_component(&mut value_hasher);
840 entries.push(HashEntry::new(key_hasher, value_hasher));
841}
842
843fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
844 sort_map_entries(entries);
845 hasher.start_map();
846 for entry in entries {
847 hasher.put_raw_bytes(&entry.key_bytes);
848 hasher.put_raw_bytes(&entry.value_bytes);
849 }
850 hasher.end_map();
851}
852
853#[cfg(test)]
854mod tests {
855 use std::io::Cursor;
856
857 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
858 use steel_utils::Identifier;
859 use steel_utils::hash::HashComponent as _;
860 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
861 use uuid::Uuid;
862
863 use super::{
864 PlayerModelType, PlayerSkinPatch, ProfileProperty, ResolvableProfile,
865 ResolvableProfileContents, StoredGameProfile, read_properties_network,
866 write_properties_network,
867 };
868
869 fn parse(tag: simdnbt::owned::NbtTag) -> Option<ResolvableProfile> {
870 let mut bytes = Vec::new();
871 tag.write(&mut bytes);
872 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
873 ResolvableProfile::from_nbt_tag(borrowed.as_tag())
874 }
875
876 #[test]
877 fn full_profile_round_trips_both_codecs_and_hashes_primary_shape() {
878 let profile = ResolvableProfile::static_full(
879 StoredGameProfile::new(
880 Uuid::from_u128(1),
881 "Steve".to_owned(),
882 vec![
883 ProfileProperty::new(
884 "textures".to_owned(),
885 "value".to_owned(),
886 Some("signature".to_owned()),
887 )
888 .expect("valid property"),
889 ],
890 )
891 .expect("valid profile"),
892 PlayerSkinPatch::new(
893 Some(Identifier::vanilla_static("steve")),
894 None,
895 None,
896 Some(PlayerModelType::Wide),
897 ),
898 );
899 let nbt = profile.clone().to_nbt_tag();
900 assert_eq!(parse(nbt.clone()), Some(profile.clone()));
901 assert_eq!(profile.compute_hash(), nbt.compute_hash());
902
903 let mut network = Vec::new();
904 profile.write(&mut network).expect("profile should encode");
905 assert_eq!(
906 ResolvableProfile::read(&mut Cursor::new(network.as_slice()))
907 .expect("profile should decode"),
908 profile
909 );
910 }
911
912 #[test]
913 fn player_name_alternative_normalizes_to_dynamic_profile() {
914 let parsed = parse(simdnbt::owned::NbtTag::String("Alex".into()))
915 .expect("name alternative should decode");
916 assert_eq!(
917 parsed.contents(),
918 &ResolvableProfileContents::DynamicName("Alex".to_owned())
919 );
920 assert_eq!(parse(parsed.clone().to_nbt_tag()), Some(parsed));
921 }
922
923 #[test]
924 fn properties_use_authlib_multimap_order_and_equality() {
925 let first = StoredGameProfile::new(
926 Uuid::nil(),
927 "Alex".to_owned(),
928 vec![
929 ProfileProperty::new("textures".to_owned(), "first".to_owned(), None)
930 .expect("valid property"),
931 ProfileProperty::new("cape".to_owned(), "only".to_owned(), None)
932 .expect("valid property"),
933 ProfileProperty::new("textures".to_owned(), "second".to_owned(), None)
934 .expect("valid property"),
935 ],
936 )
937 .expect("valid profile");
938 let cross_key_reordered = StoredGameProfile::new(
939 Uuid::nil(),
940 "Alex".to_owned(),
941 vec![
942 ProfileProperty::new("cape".to_owned(), "only".to_owned(), None)
943 .expect("valid property"),
944 ProfileProperty::new("textures".to_owned(), "first".to_owned(), None)
945 .expect("valid property"),
946 ProfileProperty::new("textures".to_owned(), "second".to_owned(), None)
947 .expect("valid property"),
948 ],
949 )
950 .expect("valid profile");
951 let same_key_reordered = StoredGameProfile::new(
952 Uuid::nil(),
953 "Alex".to_owned(),
954 vec![
955 ProfileProperty::new("textures".to_owned(), "second".to_owned(), None)
956 .expect("valid property"),
957 ProfileProperty::new("textures".to_owned(), "first".to_owned(), None)
958 .expect("valid property"),
959 ProfileProperty::new("cape".to_owned(), "only".to_owned(), None)
960 .expect("valid property"),
961 ],
962 )
963 .expect("valid profile");
964
965 assert_eq!(
966 first
967 .properties()
968 .iter()
969 .map(|property| (property.name(), property.value()))
970 .collect::<Vec<_>>(),
971 vec![
972 ("textures", "first"),
973 ("textures", "second"),
974 ("cape", "only")
975 ]
976 );
977 assert_eq!(first, cross_key_reordered);
978 assert_ne!(first, same_key_reordered);
979
980 let mut network = Vec::new();
981 write_properties_network(first.properties(), &mut network)
982 .expect("properties should encode");
983 let decoded = read_properties_network(&mut Cursor::new(network.as_slice()))
984 .expect("properties should decode");
985 assert_eq!(decoded, first.properties());
986 }
987
988 #[test]
989 fn profile_limits_prevent_values_that_cannot_be_reencoded() {
990 assert!(
991 ResolvableProfile::dynamic_name("a".repeat(17), PlayerSkinPatch::default()).is_err()
992 );
993 let properties = (0..17)
994 .map(|index| {
995 ProfileProperty::new(format!("p{index}"), "value".to_owned(), None)
996 .expect("test property should be valid")
997 })
998 .collect();
999 assert!(StoredGameProfile::new(Uuid::nil(), "A".to_owned(), properties).is_err());
1000 }
1001}