Skip to main content

mcproto_types/
text_component.rs

1//! NBT-backed Minecraft text components.
2//!
3//! This module wraps the shared component model for the NBT representation
4//! used by the Minecraft protocol.
5
6use std::{
7    collections::HashMap,
8    error::Error,
9    fmt,
10    io::{self, Read, Write},
11};
12
13use fastnbt::{
14    SerOpts, Tag,
15    stream::{Parser, Value as StreamValue},
16};
17use mcproto_codec::{
18    error::{CodecError, CodecKind, CodecOperation, InvalidEncodingReason},
19    io::write_all_counted,
20};
21
22use crate::{TypeCodec, component::NbtComponent};
23
24/// The dynamically typed NBT value used by [`NbtComponent`].
25pub use fastnbt::Value as NbtValue;
26
27/// A text component encoded as an NBT tag.
28///
29/// Plain text-only components may use an NBT string tag. Components with
30/// styling, events, or other data use an NBT compound tag. See the
31/// [text component format] and [NBT specification].
32///
33/// [text component format]: https://minecraft.wiki/w/Text_component_format
34/// [NBT specification]: https://minecraft.wiki/w/NBT_format
35#[derive(Debug, Clone, PartialEq)]
36pub struct TextComponent(
37    /// The structured text component value.
38    pub NbtComponent,
39);
40
41impl TextComponent {
42    /// The maximum number of NBT bytes accepted while decoding a component.
43    pub const MAX_DECODE_BYTES: usize = 2 * 1024 * 1024;
44    /// The maximum number of NBT values accepted while decoding a component.
45    pub const MAX_DECODE_NODES: usize = 262_144;
46    const MAX_COMPONENT_DEPTH: usize = 512;
47
48    /// Creates a plain-text component.
49    pub fn text(value: impl Into<String>) -> Self {
50        Self(NbtComponent::text(value))
51    }
52}
53
54impl Default for TextComponent {
55    fn default() -> Self {
56        Self::text("")
57    }
58}
59
60impl From<NbtComponent> for TextComponent {
61    fn from(value: NbtComponent) -> Self {
62        Self(value)
63    }
64}
65
66impl From<String> for TextComponent {
67    fn from(value: String) -> Self {
68        Self::text(value)
69    }
70}
71
72impl From<&str> for TextComponent {
73    fn from(value: &str) -> Self {
74        Self::text(value)
75    }
76}
77
78impl TypeCodec for TextComponent {
79    fn encode(&self, writer: &mut impl Write) -> Result<(), CodecError> {
80        self.0
81            .validate_depth(Self::MAX_COMPONENT_DEPTH)
82            .map_err(|source| invalid_nbt(CodecOperation::Write, 0, source))?;
83        self.0
84            .validate_dynamic_depth(Self::MAX_COMPONENT_DEPTH)
85            .map_err(|source| invalid_nbt(CodecOperation::Write, 0, source))?;
86        let normalized = self.0.normalized_root_for_nbt();
87        let value = fastnbt::to_value(&normalized)
88            .map_err(|source| invalid_nbt(CodecOperation::Write, 0, source))?;
89        validate_nbt_strings(&value, CodecOperation::Write, 0)?;
90        encode_root(&value, writer)
91    }
92
93    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
94        let (value, bytes_processed) = decode_root(reader)?;
95        let component: NbtComponent = fastnbt::from_value(&value)
96            .map_err(|source| invalid_nbt(CodecOperation::Read, bytes_processed, source))?;
97        component
98            .validate_depth(Self::MAX_COMPONENT_DEPTH)
99            .map_err(|source| invalid_nbt(CodecOperation::Read, bytes_processed, source))?;
100        component
101            .validate_dynamic_depth(Self::MAX_COMPONENT_DEPTH)
102            .map_err(|source| invalid_nbt(CodecOperation::Read, bytes_processed, source))?;
103        Ok(Self(component.normalized_root_for_nbt()))
104    }
105}
106
107fn encode_root(value: &NbtValue, writer: &mut impl Write) -> Result<(), CodecError> {
108    let wrapper = HashMap::from([("", value)]);
109    let encoded = fastnbt::to_bytes_with_opts(&wrapper, SerOpts::network_nbt())
110        .map_err(|source| invalid_nbt(CodecOperation::Write, 0, source))?;
111
112    if encoded.len() < 5
113        || encoded[0] != Tag::Compound as u8
114        || encoded[2..4] != [0, 0]
115        || encoded.last() != Some(&(Tag::End as u8))
116        || !matches!(encoded[1], 8 | 10)
117    {
118        return Err(CodecError::invalid_encoding_for_operation(
119            CodecKind::TextComponent,
120            CodecOperation::Write,
121            0,
122            InvalidEncodingReason::InvalidNbt,
123        ));
124    }
125
126    write_all_counted(writer, &encoded[1..2], CodecKind::TextComponent, 0)?;
127    write_all_counted(
128        writer,
129        &encoded[4..encoded.len() - 1],
130        CodecKind::TextComponent,
131        1,
132    )
133}
134
135fn decode_root(reader: &mut impl Read) -> Result<(NbtValue, usize), CodecError> {
136    let mut reader = CountedReader::new(reader, TextComponent::MAX_DECODE_BYTES);
137    let mut root_tag = [0_u8; 1];
138    reader.read_exact(&mut root_tag).map_err(|source| {
139        CodecError::from_read_error(CodecKind::TextComponent, reader.processed, source)
140    })?;
141    if !matches!(root_tag[0], 8 | 10) {
142        return Err(CodecError::invalid_encoding(
143            CodecKind::TextComponent,
144            reader.processed,
145            InvalidEncodingReason::InvalidTextComponentRootTag { tag: root_tag[0] },
146        ));
147    }
148
149    // The stream parser requires a root name; network text components omit it.
150    let mut prefixed = PrefixReader {
151        prefix: &[root_tag[0], 0, 0],
152        inner: &mut reader,
153    };
154    let result = (|| {
155        let mut parser = Parser::new(&mut prefixed);
156        let first = parser.next().map_err(NbtTreeError::Parser)?;
157        let mut remaining_nodes = TextComponent::MAX_DECODE_NODES;
158        parse_stream_value(&mut parser, first, 1, &mut remaining_nodes)
159    })();
160
161    match result {
162        Ok(value) => Ok((value, prefixed.inner.processed)),
163        Err(error) => {
164            let processed = prefixed.inner.processed;
165            if prefixed.inner.limit_exceeded {
166                return Err(CodecError::invalid_encoding(
167                    CodecKind::TextComponent,
168                    processed,
169                    InvalidEncodingReason::TooLong {
170                        max_bytes: TextComponent::MAX_DECODE_BYTES,
171                    },
172                ));
173            }
174            match prefixed.inner.failure.take() {
175                Some(source) => Err(CodecError::from_read_error(
176                    CodecKind::TextComponent,
177                    processed,
178                    source,
179                )),
180                None => Err(invalid_nbt(CodecOperation::Read, processed, error)),
181            }
182        }
183    }
184}
185
186fn parse_stream_value<R: Read>(
187    parser: &mut Parser<R>,
188    value: StreamValue,
189    depth: usize,
190    remaining_nodes: &mut usize,
191) -> Result<NbtValue, NbtTreeError> {
192    *remaining_nodes = remaining_nodes
193        .checked_sub(1)
194        .ok_or(NbtTreeError::TooManyNodes)?;
195    if depth > TextComponent::MAX_COMPONENT_DEPTH {
196        return Err(NbtTreeError::TooDeep);
197    }
198    match value {
199        StreamValue::Byte(_, value) => Ok(NbtValue::Byte(value)),
200        StreamValue::Short(_, value) => Ok(NbtValue::Short(value)),
201        StreamValue::Int(_, value) => Ok(NbtValue::Int(value)),
202        StreamValue::Long(_, value) => Ok(NbtValue::Long(value)),
203        StreamValue::Float(_, value) => Ok(NbtValue::Float(value)),
204        StreamValue::Double(_, value) => Ok(NbtValue::Double(value)),
205        StreamValue::ByteArray(_, value) => Ok(NbtValue::ByteArray(fastnbt::ByteArray::new(value))),
206        StreamValue::String(_, value) => Ok(NbtValue::String(value)),
207        StreamValue::IntArray(_, value) => Ok(NbtValue::IntArray(fastnbt::IntArray::new(value))),
208        StreamValue::LongArray(_, value) => Ok(NbtValue::LongArray(fastnbt::LongArray::new(value))),
209        StreamValue::List(_, _, length) => {
210            let length = usize::try_from(length).map_err(|_| NbtTreeError::InvalidStructure)?;
211            // A declared length is untrusted and can exceed the remaining packet.
212            let mut values = Vec::with_capacity(length.min(1024));
213            for _ in 0..length {
214                let value = parser.next().map_err(NbtTreeError::Parser)?;
215                values.push(parse_stream_value(
216                    parser,
217                    value,
218                    depth + 1,
219                    remaining_nodes,
220                )?);
221            }
222            if !matches!(parser.next(), Ok(StreamValue::ListEnd)) {
223                return Err(NbtTreeError::InvalidStructure);
224            }
225            Ok(NbtValue::List(values))
226        }
227        StreamValue::Compound(_) => {
228            let mut values = HashMap::new();
229            loop {
230                let value = parser.next().map_err(NbtTreeError::Parser)?;
231                if matches!(value, StreamValue::CompoundEnd) {
232                    break;
233                }
234                let name = stream_name(&value).ok_or(NbtTreeError::InvalidStructure)?;
235                values.insert(
236                    name.to_owned(),
237                    parse_stream_value(parser, value, depth + 1, remaining_nodes)?,
238                );
239            }
240            Ok(NbtValue::Compound(values))
241        }
242        StreamValue::ListEnd | StreamValue::CompoundEnd => Err(NbtTreeError::InvalidStructure),
243    }
244}
245
246fn stream_name(value: &StreamValue) -> Option<&str> {
247    match value {
248        StreamValue::Byte(name, _)
249        | StreamValue::Short(name, _)
250        | StreamValue::Int(name, _)
251        | StreamValue::Long(name, _)
252        | StreamValue::Float(name, _)
253        | StreamValue::Double(name, _)
254        | StreamValue::ByteArray(name, _)
255        | StreamValue::String(name, _)
256        | StreamValue::List(name, _, _)
257        | StreamValue::Compound(name)
258        | StreamValue::IntArray(name, _)
259        | StreamValue::LongArray(name, _) => name.as_deref(),
260        StreamValue::ListEnd | StreamValue::CompoundEnd => None,
261    }
262}
263
264#[derive(Debug)]
265enum NbtTreeError {
266    Parser(fastnbt::stream::Error),
267    InvalidStructure,
268    TooDeep,
269    TooManyNodes,
270}
271
272impl fmt::Display for NbtTreeError {
273    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            Self::Parser(source) => source.fmt(formatter),
276            Self::InvalidStructure => formatter.write_str("invalid NBT tree structure"),
277            Self::TooDeep => formatter.write_str("NBT tree is nested too deeply"),
278            Self::TooManyNodes => formatter.write_str("NBT tree contains too many nodes"),
279        }
280    }
281}
282
283impl Error for NbtTreeError {
284    fn source(&self) -> Option<&(dyn Error + 'static)> {
285        match self {
286            Self::Parser(source) => Some(source),
287            _ => None,
288        }
289    }
290}
291
292fn invalid_nbt(
293    operation: CodecOperation,
294    bytes_processed: usize,
295    source: impl Error + Send + Sync + 'static,
296) -> CodecError {
297    CodecError::invalid_encoding_for_operation_with_source(
298        CodecKind::TextComponent,
299        operation,
300        bytes_processed,
301        InvalidEncodingReason::InvalidNbt,
302        source,
303    )
304}
305
306fn validate_nbt_strings(
307    root: &NbtValue,
308    operation: CodecOperation,
309    bytes_processed: usize,
310) -> Result<(), CodecError> {
311    let mut pending = vec![root];
312    while let Some(value) = pending.pop() {
313        match value {
314            NbtValue::String(value) => validate_nbt_string(value, operation, bytes_processed)?,
315            NbtValue::List(values) => pending.extend(values),
316            NbtValue::Compound(values) => {
317                for (name, value) in values {
318                    validate_nbt_string(name, operation, bytes_processed)?;
319                    pending.push(value);
320                }
321            }
322            _ => {}
323        }
324    }
325    Ok(())
326}
327
328fn validate_nbt_string(
329    value: &str,
330    operation: CodecOperation,
331    bytes_processed: usize,
332) -> Result<(), CodecError> {
333    let encoded_len = value.chars().try_fold(0_usize, |length, character| {
334        let width = match character as u32 {
335            0 => 2,
336            1..=0x7f => 1,
337            0x80..=0x7ff => 2,
338            0x800..=0xffff => 3,
339            _ => 6,
340        };
341        length.checked_add(width)
342    });
343    if !matches!(encoded_len, Some(length) if length <= u16::MAX as usize) {
344        return Err(CodecError::invalid_encoding_for_operation(
345            CodecKind::TextComponent,
346            operation,
347            bytes_processed,
348            InvalidEncodingReason::StringTooLong {
349                max_bytes: u16::MAX as usize,
350            },
351        ));
352    }
353    Ok(())
354}
355
356/// Tracks byte progress and limits for a reader, and records the first
357/// underlying I/O failure for later classification.
358///
359/// Like [`read_exact_counted`], interrupted operations are retried so callers
360/// never observe [`io::ErrorKind::Interrupted`], even when they call
361/// [`read`](Read::read) directly instead of [`Read::read_exact`].
362struct CountedReader<'a, R: ?Sized> {
363    inner: &'a mut R,
364    processed: usize,
365    byte_limit: usize,
366    limit_exceeded: bool,
367    failure: Option<io::Error>,
368}
369
370impl<'a, R: ?Sized> CountedReader<'a, R> {
371    fn new(inner: &'a mut R, byte_limit: usize) -> Self {
372        Self {
373            inner,
374            processed: 0,
375            byte_limit,
376            limit_exceeded: false,
377            failure: None,
378        }
379    }
380}
381
382impl<R: Read + ?Sized> Read for CountedReader<'_, R> {
383    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
384        loop {
385            if buffer.is_empty() {
386                return Ok(0);
387            }
388            let remaining = self.byte_limit.saturating_sub(self.processed);
389            if remaining == 0 {
390                self.limit_exceeded = true;
391                return Err(io::Error::other("NBT byte limit exceeded"));
392            }
393            let buffer_len = buffer.len().min(remaining);
394            match self.inner.read(&mut buffer[..buffer_len]) {
395                Ok(0) => {
396                    let error =
397                        io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of NBT");
398                    self.failure = Some(error);
399                    return Err(io::Error::new(
400                        io::ErrorKind::UnexpectedEof,
401                        "unexpected end of NBT",
402                    ));
403                }
404                Ok(read) => {
405                    self.processed += read;
406                    return Ok(read);
407                }
408                Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
409                Err(error) => {
410                    let returned = clone_io_error(&error);
411                    self.failure = Some(error);
412                    return Err(returned);
413                }
414            }
415        }
416    }
417}
418
419struct PrefixReader<'a, 'b, R: ?Sized> {
420    prefix: &'a [u8],
421    inner: &'a mut CountedReader<'b, R>,
422}
423
424impl<R: Read + ?Sized> Read for PrefixReader<'_, '_, R> {
425    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
426        if buffer.is_empty() {
427            return Ok(0);
428        }
429        if !self.prefix.is_empty() {
430            let read = buffer.len().min(self.prefix.len());
431            buffer[..read].copy_from_slice(&self.prefix[..read]);
432            self.prefix = &self.prefix[read..];
433            return Ok(read);
434        }
435        self.inner.read(buffer)
436    }
437}
438
439fn clone_io_error(error: &io::Error) -> io::Error {
440    match error.raw_os_error() {
441        Some(code) => io::Error::from_raw_os_error(code),
442        None => io::Error::new(error.kind(), error.to_string()),
443    }
444}