mcproto-types 0.5.0

Minecraft protocol types.
Documentation
//! Integration tests for VarInt-prefixed Minecraft protocol strings.

use std::io::{self, Write};

use mcproto_codec::{
    error::{CodecErrorKind, CodecKind, CodecOperation, InvalidEncodingReason},
    varint::VarIntWrite,
};
use mcproto_types::{TypeCodec, basic::PrefixedString};

fn assert_roundtrip(value: &str, expected: &[u8]) {
    let value = PrefixedString(value.to_owned());
    let mut encoded = Vec::new();
    value.encode(&mut encoded).unwrap();
    assert_eq!(encoded, expected);

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

#[test]
fn empty_string() {
    assert_roundtrip("", &[0x00]);
}

#[test]
fn ascii_string() {
    assert_roundtrip(
        "Hello, world!",
        &[
            0x0d, b'H', b'e', b'l', b'l', b'o', b',', b' ', b'w', b'o', b'r', b'l', b'd', b'!',
        ],
    );
}

#[test]
fn chinese_character_uses_three_utf8_bytes_and_one_utf16_code_unit() {
    assert_eq!("".encode_utf16().count(), 1);
    assert_roundtrip("", &[0x03, 0xe4, 0xb8, 0xad]);
}

#[test]
fn emoji_uses_four_utf8_bytes_and_two_utf16_code_units() {
    assert_eq!("😀".encode_utf16().count(), 2);
    assert_roundtrip("😀", &[0x04, 0xf0, 0x9f, 0x98, 0x80]);
}

#[test]
fn nul_uses_standard_utf8_instead_of_modified_utf8() {
    assert_roundtrip("\0", &[0x01, 0x00]);
}

#[test]
fn maximum_utf16_length_is_accepted() {
    let value = PrefixedString("a".repeat(PrefixedString::MAX_UTF16_CODE_UNITS));
    let mut encoded = Vec::new();
    value.encode(&mut encoded).unwrap();

    assert_eq!(&encoded[..3], &[0xff, 0xff, 0x01]);
    assert_eq!(encoded.len(), PrefixedString::MAX_UTF16_CODE_UNITS + 3);

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

#[test]
fn maximum_utf8_byte_length_is_accepted() {
    let value = PrefixedString("".repeat(PrefixedString::MAX_UTF16_CODE_UNITS));
    assert_eq!(value.0.len(), PrefixedString::MAX_BYTES);

    let mut encoded = Vec::new();
    value.encode(&mut encoded).unwrap();
    let mut input = encoded.as_slice();
    assert_eq!(PrefixedString::decode(&mut input).unwrap(), value);
}

#[test]
fn encode_rejects_too_many_utf16_code_units_before_writing() {
    let value = PrefixedString("a".repeat(PrefixedString::MAX_UTF16_CODE_UNITS + 1));
    let mut output = Vec::new();
    let error = value.encode(&mut output).unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::TooManyUtf16CodeUnits {
            max_code_units: PrefixedString::MAX_UTF16_CODE_UNITS,
        })
    );
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Write);
    assert_eq!(error.bytes_processed(), 0);
    assert!(output.is_empty());
}

#[test]
fn encode_rejects_too_many_utf8_bytes_before_writing() {
    let value = PrefixedString("".repeat(PrefixedString::MAX_UTF16_CODE_UNITS + 1));
    assert!(value.0.len() > PrefixedString::MAX_BYTES);

    let mut output = Vec::new();
    let error = value.encode(&mut output).unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::StringTooLong {
            max_bytes: PrefixedString::MAX_BYTES,
        })
    );
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Write);
    assert_eq!(error.bytes_processed(), 0);
    assert!(output.is_empty());
}

#[test]
fn decode_rejects_declared_byte_length_above_limit_before_reading_payload() {
    let mut encoded = Vec::new();
    encoded
        .write_varint((PrefixedString::MAX_BYTES + 1) as i32)
        .unwrap();

    let error = PrefixedString::decode(&mut encoded.as_slice()).unwrap_err();
    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::StringTooLong {
            max_bytes: PrefixedString::MAX_BYTES,
        })
    );
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn decode_rejects_negative_length() {
    let encoded = [0xff, 0xff, 0xff, 0xff, 0x0f];
    let error = PrefixedString::decode(&mut encoded.as_slice()).unwrap_err();

    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::NegativeLength { value: -1 })
    );
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn truncated_payload_is_unexpected_eof() {
    let encoded = [0x05, b'H', b'e'];
    let error = PrefixedString::decode(&mut encoded.as_slice()).unwrap_err();

    assert_eq!(error.kind(), CodecErrorKind::UnexpectedEof);
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), encoded.len());
    assert_eq!(
        error.io_error().unwrap().kind(),
        io::ErrorKind::UnexpectedEof
    );
}

#[test]
fn invalid_utf8_is_rejected() {
    let encoded = [0x02, 0xc3, 0x28];
    let error = PrefixedString::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.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), encoded.len());
}

#[test]
fn decode_rejects_too_many_utf16_code_units() {
    let payload = "a".repeat(PrefixedString::MAX_UTF16_CODE_UNITS + 1);
    let mut encoded = Vec::new();
    encoded.write_varint(payload.len() as i32).unwrap();
    encoded.extend_from_slice(payload.as_bytes());

    let error = PrefixedString::decode(&mut encoded.as_slice()).unwrap_err();
    assert_eq!(
        error.kind(),
        CodecErrorKind::InvalidEncoding(InvalidEncodingReason::TooManyUtf16CodeUnits {
            max_code_units: PrefixedString::MAX_UTF16_CODE_UNITS,
        })
    );
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), encoded.len());
}

struct PayloadFailingWriter {
    writes: usize,
}

impl Write for PayloadFailingWriter {
    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
        if self.writes == 0 {
            self.writes += 1;
            Ok(buffer.len())
        } else {
            Err(io::Error::other("payload write failed"))
        }
    }

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

#[test]
fn payload_write_error_is_attributed_to_string() {
    let mut writer = PayloadFailingWriter { writes: 0 };
    let error = PrefixedString("a".to_owned())
        .encode(&mut writer)
        .unwrap_err();

    assert_eq!(error.kind(), CodecErrorKind::Io);
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.operation(), CodecOperation::Write);
    assert_eq!(error.bytes_processed(), 1);
    assert_eq!(error.io_error().unwrap().kind(), io::ErrorKind::Other);
}

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_prefix_write_preserves_varint_and_string_context() {
    let value = PrefixedString("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::String));
    assert_eq!(error.operation(), CodecOperation::Write);
    assert_eq!(error.bytes_processed(), 1);
    assert!(error.to_string().contains("while processing String"));
}

#[test]
fn partial_payload_write_includes_prefix_and_payload_progress() {
    let value = PrefixedString("hello".to_owned());
    let error = value
        .encode(&mut FailAfterWriter { remaining: 3 })
        .unwrap_err();

    assert_eq!(error.kind(), CodecErrorKind::Io);
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.context(), None);
    assert_eq!(error.operation(), CodecOperation::Write);
    assert_eq!(error.bytes_processed(), 3);
}

#[test]
fn truncated_prefix_preserves_varint_and_string_context() {
    let encoded = [0x80];
    let error = PrefixedString::decode(&mut encoded.as_slice()).unwrap_err();

    assert_eq!(error.kind(), CodecErrorKind::UnexpectedEof);
    assert_eq!(error.codec(), CodecKind::VarInt);
    assert_eq!(error.context(), Some(CodecKind::String));
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), 1);
}

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

impl io::Read for PartialErrorReader {
    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
        if self.input.is_empty() {
            return Err(io::Error::other("injected payload 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_includes_prefix_and_payload_progress() {
    let mut reader = PartialErrorReader {
        input: &[0x05, b'H', b'e'],
    };
    let error = PrefixedString::decode(&mut reader).unwrap_err();

    assert_eq!(error.kind(), CodecErrorKind::Io);
    assert_eq!(error.codec(), CodecKind::String);
    assert_eq!(error.context(), None);
    assert_eq!(error.operation(), CodecOperation::Read);
    assert_eq!(error.bytes_processed(), 3);
}