qubit-text-codec 0.1.0

Buffer-oriented text encoding and decoding utilities for Rust
Documentation
use qubit_text_codec::{
    Charset,
    CharsetCodec,
    CharsetDecodeError,
    CharsetDecodeErrorKind,
    CharsetDecodeResult,
    CharsetDecoder,
    CharsetEncodeError,
    CharsetEncodeErrorKind,
    CharsetEncodeResult,
    Coder,
    CoderStatus,
    DecodeStatus,
    MalformedAction,
    Utf8Codec,
    Utf32U32Codec,
};

#[derive(Clone, Copy, Debug, Default)]
struct IncompleteErrorCodec;

impl CharsetCodec for IncompleteErrorCodec {
    type Unit = u8;
    fn charset(&self) -> Charset {
        Charset::ASCII
    }

    fn max_units_per_char(&self) -> usize {
        1
    }

    fn decode_one(&self, _input: &[u8], index: usize) -> CharsetDecodeResult<DecodeStatus> {
        let kind = CharsetDecodeErrorKind::IncompleteSequence {
            required: index + 1,
            available: 0,
        };
        Err(CharsetDecodeError::new(Charset::ASCII, kind, index))
    }

    fn encode_one(&self, _ch: char, _output: &mut [u8], index: usize) -> CharsetEncodeResult<usize> {
        let kind = CharsetEncodeErrorKind::BufferTooSmall {
            required: index + 1,
            available: 0,
        };
        Err(CharsetEncodeError::new(Charset::ASCII, kind, index))
    }
}

#[test]
fn test_charset_decoder_exposes_configuration_and_bounds() {
    let mut decoder = CharsetDecoder::new(Utf8Codec);

    assert_eq!(Charset::UTF_8, decoder.codec().charset());
    assert_eq!(Charset::UTF_8, decoder.codec_mut().charset());
    assert_eq!(MalformedAction::Replace, decoder.malformed_action());
    assert_eq!('\u{fffd}', decoder.replacement());
    assert_eq!(Some(3), decoder.max_output_len(3));

    let decoder_with_replacement = CharsetDecoder::new(Utf8Codec).with_replacement('!');
    assert_eq!('!', decoder_with_replacement.replacement());

    decoder.set_replacement('?');
    decoder.set_malformed_action(MalformedAction::Ignore);

    assert_eq!('?', decoder.replacement());
    assert_eq!(MalformedAction::Ignore, decoder.malformed_action());
}

#[test]
fn test_charset_decoder_replaces_reports_and_ignores_malformed_input() {
    let input = [b'A', 0x80, b'B'];
    let mut output = ['\0'; 3];
    let mut decoder = CharsetDecoder::new(Utf8Codec);

    let progress = decoder
        .convert(&input, 0, &mut output, 0)
        .expect("default malformed action replaces");

    assert_eq!(CoderStatus::Complete, progress.status());
    assert_eq!(3, progress.read());
    assert_eq!(3, progress.written());
    assert_eq!(['A', '\u{fffd}', 'B'], output);

    decoder.set_malformed_action(MalformedAction::Ignore);
    let mut ignored_output = ['\0'; 2];
    let progress = decoder
        .convert(&input, 0, &mut ignored_output, 0)
        .expect("ignore malformed input");

    assert_eq!(CoderStatus::Complete, progress.status());
    assert_eq!(3, progress.read());
    assert_eq!(2, progress.written());
    assert_eq!(['A', 'B'], ignored_output);

    decoder.set_malformed_action(MalformedAction::Report);
    let error = decoder
        .convert(&input, 1, &mut output, 0)
        .expect_err("report malformed input");

    assert_eq!(
        CharsetDecodeErrorKind::MalformedSequence { value: Some(0x80) },
        error.kind()
    );
    assert_eq!(1, error.index());
}

#[test]
fn test_charset_decoder_reports_invalid_indices_capacity_and_need_input() {
    let input = b"AB";
    let mut output = ['\0'; 1];
    let mut decoder = CharsetDecoder::new(Utf8Codec);

    let error = decoder
        .convert(input, input.len() + 1, &mut output, 0)
        .expect_err("input index outside input slice");
    assert_eq!(CharsetDecodeErrorKind::MalformedSequence { value: None }, error.kind());
    assert_eq!(input.len() + 1, error.index());

    let beyond_output = output.len() + 1;
    let progress = decoder
        .convert(input, 0, &mut output, beyond_output)
        .expect("output index beyond output slice needs more output");
    assert!(matches!(progress.status(), CoderStatus::NeedOutput { .. }));
    assert_eq!(0, progress.read());
    assert_eq!(0, progress.written());

    let progress = decoder
        .convert(input, 0, &mut output, 0)
        .expect("decoder stops when output buffer fills");
    assert!(matches!(progress.status(), CoderStatus::NeedOutput { .. }));
    assert_eq!(1, progress.read());
    assert_eq!(1, progress.written());

    let progress = decoder
        .convert(&[0xe4], 0, &mut output, 0)
        .expect("incomplete UTF-8 prefix needs input");
    assert!(matches!(progress.status(), CoderStatus::NeedInput { .. }));
    assert_eq!(0, progress.read());
    assert_eq!(0, progress.written());
}

#[test]
fn test_charset_decoder_replaces_malformed_ranges_and_invalid_scalars() {
    let mut decoder = CharsetDecoder::new(Utf8Codec);
    decoder.set_replacement('?');
    let mut output = ['\0'; 2];

    let progress = decoder
        .convert(&[0xe4, b' ', b'A'], 0, &mut output, 0)
        .expect("invalid continuation is one malformed range");

    assert_eq!(CoderStatus::Complete, progress.status());
    assert_eq!(3, progress.read());
    assert_eq!(2, progress.written());
    assert_eq!(['?', 'A'], output);

    let mut utf32_decoder = CharsetDecoder::new(Utf32U32Codec);
    let mut scalar_output = ['\0'; 1];
    let progress = utf32_decoder
        .convert(&[0x110000], 0, &mut scalar_output, 0)
        .expect("invalid scalar is replaced by default");

    assert_eq!(CoderStatus::Complete, progress.status());
    assert_eq!(1, progress.read());
    assert_eq!(1, progress.written());
    assert_eq!('\u{fffd}', scalar_output[0]);
}

#[test]
fn test_charset_decoder_propagates_non_policy_decoding_errors() {
    let input = [0_u8];
    let mut output = ['\0'; 1];
    let mut decoder = CharsetDecoder::new(IncompleteErrorCodec);

    let error = decoder
        .convert(&input, 0, &mut output, 0)
        .expect_err("incomplete-sequence error is not absorbed");

    assert!(matches!(
        error.kind(),
        CharsetDecodeErrorKind::IncompleteSequence { .. },
    ));
    assert_eq!(0, error.index());
}