chunkedge_binary/
text_component.rs

1use std::borrow::Cow;
2use std::io::Write;
3
4use anyhow::ensure;
5use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
6use chunkedge_nbt::Tag;
7use chunkedge_text::{IntoText, Text};
8
9use crate::{Decode, Encode};
10
11#[derive(Clone, Debug, PartialEq)]
12#[repr(transparent)] // if you change this you have to remove the unsafe code!
13pub struct TextComponent {
14    pub text: Text,
15}
16
17impl TextComponent {
18    /// Zero-copy cast from a `Cow<Text>` to a `Cow<TextComponent>`.
19    ///
20    /// # Safety
21    /// This is safe because `TextComponent` is #[repr(transparent)] wrapper
22    /// around Text.
23    pub fn from_cow_text<'a>(cow: Cow<'a, Text>) -> Cow<'a, TextComponent> {
24        match cow {
25            Cow::Borrowed(b) => {
26                // SAFETY: TextComponent has the exact same memory layout as Text.
27                let ptr = b as *const Text as *const TextComponent;
28                Cow::Borrowed(unsafe { &*ptr })
29            }
30            Cow::Owned(o) => Cow::Owned(TextComponent { text: o }),
31        }
32    }
33
34    pub fn as_text(&self) -> &Text {
35        &self.text
36    }
37}
38
39impl<'a> IntoText<'a> for TextComponent {
40    fn into_cow_text(self) -> Cow<'a, Text> {
41        // Since we wrap Text, we just return it.
42        Cow::Owned(self.text)
43    }
44}
45
46pub trait IntoTextComponent<'a> {
47    fn into_text_component(self) -> TextComponent;
48    fn into_cow_text_component(self) -> Cow<'a, TextComponent>;
49}
50
51impl<'a, T: IntoText<'a>> IntoTextComponent<'a> for T {
52    fn into_text_component(self) -> TextComponent {
53        TextComponent {
54            text: self.into_cow_text().into_owned(),
55        }
56    }
57
58    fn into_cow_text_component(self) -> Cow<'a, TextComponent> {
59        let cow = self.into_cow_text();
60        TextComponent::from_cow_text(cow)
61    }
62}
63
64impl From<Text> for TextComponent {
65    fn from(value: Text) -> Self {
66        TextComponent { text: value }
67    }
68}
69
70impl Encode for TextComponent {
71    fn encode(&self, mut w: impl Write) -> anyhow::Result<()> {
72        if self.text.is_plain() {
73            // Encode as NBT String
74            let mut w = w;
75            w.write_u8(Tag::String as u8)?;
76
77            let chunkedge_text::TextContent::Text { text: string } = &self.text.content else {
78                // is_plain should mean this is unreachable
79                unreachable!()
80            };
81
82            let len = string.len();
83
84            match u16::try_from(len) {
85                Ok(n) => w.write_u16::<BigEndian>(n)?,
86                Err(_) => {
87                    return Err(anyhow::anyhow!(
88                        "string of length {len} exceeds maximum of u16::MAX"
89                    ));
90                }
91            }
92
93            // Write string bytes... (placeholder for `to_modified_utf8`)
94            w.write_all(string.as_bytes())?;
95            Ok(())
96        } else {
97            // Encode as Compound
98            w.write_u8(Tag::Compound as u8)?;
99            self.text.encode(&mut w)
100        }
101    }
102}
103
104impl Decode<'_> for TextComponent {
105    fn decode(r: &mut &'_ [u8]) -> anyhow::Result<Self> {
106        let tag_id = r.read_u8()?;
107        match tag_id {
108            val if val == Tag::String as u8 => Ok(TextComponent {
109                text: {
110                    let len = r.read_u16::<BigEndian>()?.into();
111                    ensure!(
112                        len <= r.len(),
113                        "string of length {} exceeds remainder of input {}",
114                        len,
115                        r.len()
116                    );
117
118                    let (left, right) = r.split_at(len);
119
120                    *r = right; // make sure reader cusor is correctly possitioned
121
122                    String::from_utf8_lossy(left).into_owned().into()
123                },
124            }),
125            val if val == Tag::Compound as u8 => {
126                // Standard Text decode
127                Ok(TextComponent {
128                    text: Decode::decode(r)?,
129                })
130            }
131            _ => Err(anyhow::anyhow!(
132                "unexpected tag ID {tag_id} when decoding TextComponent"
133            )),
134        }
135    }
136}