mcproto-types 0.2.1

Minecraft protocol types.
Documentation
use std::{
    error::Error as _,
    io::{self, Read, Write},
};

use mcproto_codec::{
    error::{CodecErrorKind, CodecKind, CodecOperation, InvalidEncodingReason},
    varint::VarIntWrite,
};
use mcproto_types::{
    TypeCodec,
    component::{Content, JsonComponent},
    json_text_component::JsonTextComponent,
};

fn roundtrip(value: JsonTextComponent) -> Vec<u8> {
    let mut encoded = Vec::new();
    value.encode(&mut encoded).unwrap();

    let mut input = encoded.as_slice();
    assert_eq!(JsonTextComponent::decode(&mut input).unwrap(), value);
    assert!(input.is_empty());
    encoded
}

fn encode_raw_json(json: &str) -> Vec<u8> {
    let mut encoded = Vec::new();
    encoded.write_varint(json.len() as i32).unwrap();
    encoded.extend_from_slice(json.as_bytes());
    encoded
}

#[test]
fn plain_text_is_a_json_string() {
    assert_eq!(
        roundtrip(JsonTextComponent::text("Hello")),
        [7, b'"', b'H', b'e', b'l', b'l', b'o', b'"']
    );
}

#[test]
fn object_component_roundtrips() {
    roundtrip(
        JsonTextComponent::from_json_str(r#"{"text":"Hello","bold":true,"color":"gold"}"#).unwrap(),
    );
}

#[test]
fn array_component_roundtrips() {
    roundtrip(
        JsonTextComponent::from_json_str(r#"[{"text":"Hello"},{"text":" world","italic":true}]"#)
            .unwrap(),
    );
}

#[test]
fn chinese_uses_standard_utf8() {
    let encoded = roundtrip(JsonTextComponent::text(""));
    assert_eq!(encoded, [5, b'"', 0xe4, 0xb8, 0xad, b'"']);
}

#[test]
fn emoji_uses_four_byte_standard_utf8() {
    let encoded = roundtrip(JsonTextComponent::text("😀"));
    assert_eq!(encoded, [6, b'"', 0xf0, 0x9f, 0x98, 0x80, b'"']);
}

#[test]
fn json_escaping_is_counted_as_wire_text() {
    assert_eq!(
        roundtrip(JsonTextComponent::text("\n")),
        [4, b'"', b'\\', b'n', b'"']
    );
}

#[test]
fn decoder_accepts_values_above_the_vanilla_encoding_limit() {
    let text = "a".repeat(JsonTextComponent::MAX_ENCODE_UTF16_CODE_UNITS + 1);
    let json = format!("\"{text}\"");
    let encoded = encode_raw_json(&json);

    assert_eq!(
        JsonTextComponent::decode(&mut encoded.as_slice()).unwrap(),
        JsonTextComponent::text(text)
    );
}

#[test]
fn decoder_accepts_the_document_length_boundary() {
    let text = "a".repeat(JsonTextComponent::MAX_DECODE_UTF16_CODE_UNITS - 2);
    let json = format!("\"{text}\"");
    assert_eq!(
        json.encode_utf16().count(),
        JsonTextComponent::MAX_DECODE_UTF16_CODE_UNITS
    );
    let encoded = encode_raw_json(&json);

    assert_eq!(
        JsonTextComponent::decode(&mut encoded.as_slice()).unwrap(),
        JsonTextComponent::text(text)
    );
}

#[test]
fn decoder_rejects_too_many_utf16_code_units() {
    let text = "a".repeat(JsonTextComponent::MAX_DECODE_UTF16_CODE_UNITS - 1);
    let json = format!("\"{text}\"");
    let encoded = encode_raw_json(&json);
    let error = JsonTextComponent::decode(&mut encoded.as_slice()).unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::TooManyUtf16CodeUnits {
            max_code_units: JsonTextComponent::MAX_DECODE_UTF16_CODE_UNITS,
        })
    );
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn decoder_rejects_declared_byte_length_above_limit_before_allocating() {
    let mut encoded = Vec::new();
    encoded
        .write_varint((JsonTextComponent::MAX_DECODE_BYTES + 1) as i32)
        .unwrap();
    let error = JsonTextComponent::decode(&mut encoded.as_slice()).unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::StringTooLong {
            max_bytes: JsonTextComponent::MAX_DECODE_BYTES,
        })
    );
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn encoder_accepts_the_vanilla_document_length_boundary() {
    let text = "a".repeat(JsonTextComponent::MAX_ENCODE_UTF16_CODE_UNITS - 2);
    let value = JsonTextComponent::text(text);
    let encoded = roundtrip(value);
    assert_eq!(
        encoded.len(),
        JsonTextComponent::MAX_ENCODE_UTF16_CODE_UNITS + 3
    );
    assert!(encoded.len() <= JsonTextComponent::MAX_ENCODE_ENCODED_BYTES);
}

#[test]
fn encoder_rejects_above_the_vanilla_utf16_limit_before_writing() {
    let text = "a".repeat(JsonTextComponent::MAX_ENCODE_UTF16_CODE_UNITS - 1);
    let mut output = Vec::new();
    let error = JsonTextComponent::text(text)
        .encode(&mut output)
        .unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::TooManyUtf16CodeUnits {
            max_code_units: JsonTextComponent::MAX_ENCODE_UTF16_CODE_UNITS,
        })
    );
    assert_eq!(error.operation(), CodecOperation::Write);
    assert_eq!(error.bytes_processed(), 0);
    assert!(output.is_empty());
}

#[test]
fn encoder_rejects_above_the_vanilla_byte_limit_before_writing() {
    let text = "".repeat(JsonTextComponent::MAX_ENCODE_UTF16_CODE_UNITS);
    let mut output = Vec::new();
    let error = JsonTextComponent::text(text)
        .encode(&mut output)
        .unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::StringTooLong {
            max_bytes: JsonTextComponent::MAX_ENCODE_BYTES,
        })
    );
    assert!(output.is_empty());
}

#[test]
fn negative_length_is_rejected() {
    let encoded = [0xff, 0xff, 0xff, 0xff, 0x0f];
    let error = JsonTextComponent::decode(&mut encoded.as_slice()).unwrap_err();
    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::NegativeLength { value: -1 })
    );
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn truncated_payload_is_unexpected_eof() {
    let encoded = [5, b'"', b'h'];
    let error = JsonTextComponent::decode(&mut encoded.as_slice()).unwrap_err();
    assert_eq!(error.kind(), CodecErrorKind::UnexpectedEof);
    assert_eq!(error.codec(), CodecKind::JsonTextComponent);
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn truncated_prefix_preserves_varint_context() {
    let error = JsonTextComponent::decode(&mut [0x80].as_slice()).unwrap_err();
    assert_eq!(error.kind(), CodecErrorKind::UnexpectedEof);
    assert_eq!(error.codec(), CodecKind::VarInt);
    assert_eq!(error.context(), Some(CodecKind::JsonTextComponent));
    assert_eq!(error.bytes_processed(), 1);
}

#[test]
fn invalid_utf8_is_rejected() {
    let encoded = [2, 0xc3, 0x28];
    let error = JsonTextComponent::decode(&mut encoded.as_slice()).unwrap_err();
    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::InvalidUtf8 {
            valid_up_to: 0,
            error_len: Some(1),
        })
    );
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn invalid_json_is_rejected_with_source() {
    let encoded = encode_raw_json("{\"text\":}");
    let error = JsonTextComponent::decode(&mut encoded.as_slice()).unwrap_err();
    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::InvalidJson)
    );
    assert_eq!(error.codec(), CodecKind::JsonTextComponent);
    assert_eq!(error.bytes_processed(), encoded.len());
    assert!(error.source().is_some());
}

#[test]
fn decoding_consumes_only_one_component() {
    let mut encoded = encode_raw_json("\"hi\"");
    encoded.push(0xaa);
    let mut input = encoded.as_slice();
    assert_eq!(
        JsonTextComponent::decode(&mut input).unwrap(),
        JsonTextComponent::text("hi")
    );
    assert_eq!(input, [0xaa]);
}

struct FailAfterWriter {
    remaining: usize,
}

impl Write for FailAfterWriter {
    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
        if self.remaining == 0 {
            return Err(io::Error::other("injected write failure"));
        }
        let written = buffer.len().min(self.remaining);
        self.remaining -= written;
        Ok(written)
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[test]
fn partial_payload_write_reports_exact_progress() {
    let error = JsonTextComponent::text("hello")
        .encode(&mut FailAfterWriter { remaining: 4 })
        .unwrap_err();
    assert_eq!(error.kind(), CodecErrorKind::Io);
    assert_eq!(error.codec(), CodecKind::JsonTextComponent);
    assert_eq!(error.bytes_processed(), 4);
}

#[test]
fn partial_prefix_write_preserves_varint_context() {
    let value = JsonTextComponent::text("a".repeat(128));
    let error = value
        .encode(&mut FailAfterWriter { remaining: 1 })
        .unwrap_err();
    assert_eq!(error.kind(), CodecErrorKind::Io);
    assert_eq!(error.codec(), CodecKind::VarInt);
    assert_eq!(error.context(), Some(CodecKind::JsonTextComponent));
    assert_eq!(error.bytes_processed(), 1);
}

struct FailAfterReader {
    input: &'static [u8],
}

impl Read for FailAfterReader {
    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
        if self.input.is_empty() {
            return Err(io::Error::other("injected read failure"));
        }
        let read = buffer.len().min(self.input.len());
        buffer[..read].copy_from_slice(&self.input[..read]);
        self.input = &self.input[read..];
        Ok(read)
    }
}

#[test]
fn partial_payload_read_reports_exact_progress() {
    let mut reader = FailAfterReader {
        input: &[5, b'"', b'h'],
    };
    let error = JsonTextComponent::decode(&mut reader).unwrap_err();
    assert_eq!(error.kind(), CodecErrorKind::Io);
    assert_eq!(error.codec(), CodecKind::JsonTextComponent);
    assert_eq!(error.bytes_processed(), 3);
}

#[test]
fn parses_json_constructor() {
    assert_eq!(
        JsonTextComponent::from_json_str("{\"text\":\"hello\"}").unwrap(),
        JsonTextComponent(JsonComponent::object(Content::Text {
            text: "hello".to_owned(),
        }))
    );
}