irrc 0.1.0

A client library for the IRRd query protocol
Documentation
use std::str::from_utf8;

use nom::{
    branch::alt,
    bytes::streaming::{tag, take_till, take_till1, take_until},
    character::{
        is_newline,
        streaming::{char, digit1, newline, space0},
    },
    combinator::{consumed, map, map_res, opt},
    sequence::{delimited, terminated},
    IResult,
};

use crate::error;

type ResponseResult = Result<Option<usize>, error::Response>;

const EOR: &[u8] = b"\nC\n";

fn resp_ok_data(input: &[u8]) -> IResult<&[u8], ResponseResult> {
    let (rem, _) = char('A')(input)?;
    let (rem, len) = terminated(map_res(map_res(digit1, from_utf8), str::parse), newline)(rem)?;
    Ok((rem, Ok(Some(len))))
}

fn resp_ok_none(input: &[u8]) -> IResult<&[u8], ResponseResult> {
    let (rem, _) = terminated(char('C'), newline)(input)?;
    Ok((rem, Ok(None)))
}

fn resp_err_not_found(input: &[u8]) -> IResult<&[u8], ResponseResult> {
    let (rem, _) = terminated(char('D'), newline)(input)?;
    Ok((rem, Err(error::Response::KeyNotFound)))
}

fn resp_err_not_unique(input: &[u8]) -> IResult<&[u8], ResponseResult> {
    let (rem, _) = terminated(char('E'), newline)(input)?;
    Ok((rem, Err(error::Response::KeyNotUnique)))
}

fn resp_err_other(input: &[u8]) -> IResult<&[u8], ResponseResult> {
    let (rem, _) = char('F')(input)?;
    let (rem, msg) = map_res(
        delimited(char(' '), take_till(is_newline), newline),
        from_utf8,
    )(rem)?;
    Ok((rem, Err(error::Response::Other(msg.to_owned()))))
}

pub(crate) fn response_status(input: &[u8]) -> IResult<&[u8], (usize, ResponseResult)> {
    map(
        consumed(alt((
            resp_ok_data,
            resp_ok_none,
            resp_err_not_found,
            resp_err_not_unique,
            resp_err_other,
        ))),
        |(consumed, result)| (consumed.len(), result),
    )(input)
}

pub(crate) fn end_of_response(input: &[u8]) -> IResult<&[u8], usize> {
    map(consumed(tag(EOR)), |(consumed, _): (&[u8], &[u8])| {
        consumed.len()
    })(input)
}

fn till_word_end(input: &[u8]) -> IResult<&[u8], &[u8]> {
    take_till1(|b| b == b' ' || b == b'\n')(input)
}

fn take_word_strip(input: &[u8]) -> IResult<&[u8], &[u8]> {
    let (mut remaining, result) = till_word_end(input)?;
    (remaining, _) = space0(remaining)?;
    Ok((remaining, result))
}

pub(crate) fn word(input: &[u8]) -> IResult<&[u8], (usize, &[u8])> {
    map(consumed(take_word_strip), |(consumed, word)| {
        (consumed.len(), word)
    })(input)
}

pub(crate) fn all(input: &[u8]) -> IResult<&[u8], (usize, &[u8])> {
    map(
        consumed(take_until(EOR)),
        |(consumed, data): (&[u8], &[u8])| (consumed.len(), data),
    )(input)
}

#[allow(clippy::unnecessary_wraps)]
pub(crate) const fn noop(input: &[u8]) -> IResult<&[u8], (usize, &[u8])> {
    Ok((input, (0, &[])))
}

pub(crate) fn paragraph(input: &[u8]) -> IResult<&[u8], (usize, &[u8])> {
    map(
        consumed(take_paragraph),
        |(consumed, paragraph): (&[u8], &[u8])| (consumed.len(), paragraph),
    )(input)
}

fn take_paragraph(input: &[u8]) -> IResult<&[u8], &[u8]> {
    let (remaining, _) = opt(newline)(input)?;
    let (remaining, result) = match take_until("\n\n")(remaining) {
        Ok((mut remaining, result)) => {
            (remaining, _) = newline(remaining)?;
            (remaining, result)
        }
        err @ Err(_) => match take_until::<_, _, (&[u8], _)>(EOR)(remaining) {
            Ok((remaining, result)) => (remaining, result),
            Err(_) => return err,
        },
    };
    Ok((remaining, result))
}

#[cfg(test)]
// TODO: remove `unknown_lints` dance when `clippy::ignored_unit_patterns` is stabilised
#[allow(unknown_lints)]
#[allow(clippy::ignored_unit_patterns)]
#[warn(unknown_lints)]
mod tests {
    use nom::Finish;
    use paste::paste;
    use proptest::prelude::*;

    use super::*;

    macro_rules! does_not_panic {
        ( $fn:ident ) => {
            proptest! {
                #[test]
                #[allow(unused_must_use)]
                fn does_not_panic(input in any::<Vec<u8>>()) {
                    $fn(&input);
                }
            }
        };
    }

    macro_rules! assert_incomplete_parse {
        ( $fn:ident { $( $desc:ident: $input:literal ),* $(,)? } ) => {
            paste! {
                $(
                    #[test]
                    fn [<$desc _is_incomplete>]() {
                        let input = dbg!($input);
                        assert!($fn(input).unwrap_err().is_incomplete())
                    }
                )*
            }
        }
    }

    macro_rules! assert_error_kind {
        ( $fn:ident { $( $desc:ident: $input:literal => $kind:ident ),* $(,)? } ) => {
            paste! {
                $(
                    #[test]
                    fn [<$desc _is_ $kind:snake _error>]() {
                        let input = dbg!($input);
                        let err = $fn(input).finish().unwrap_err();
                        assert_eq!(err.code, nom::error::ErrorKind::$kind)
                    }
                )*
            }
        }
    }

    macro_rules! assert_parse_result {
        ( $fn:ident { $( $desc:ident: $input:literal => ( $consumed:literal, $result:expr ) ),* $(,)? } ) => {
            paste! {
                $(
                    #[test]
                    fn [<$desc _is_valid_result>]() {
                        let input = dbg!($input);
                        let (_, (consumed, result)) = $fn(input).unwrap();
                        assert_eq!(consumed, $consumed);
                        assert_eq!(result, $result);
                    }
                )*
            }
        }
    }

    mod status {
        use super::*;

        does_not_panic!(response_status);

        assert_incomplete_parse!(response_status {
            empty: b"",
            unterminated_ok_none: b"C",
            unterminated_err_not_found: b"D",
            unterminated_err_not_unique: b"E",
            ok_data_no_length: b"A",
            unterminated_ok_data: b"A1",
            err_other_no_msg: b"F",
            unterminated_err_other: b"F foo",
        });

        assert_error_kind!(
            response_status {
                null: b"\n" => Char,
                unknown_status: b"Z" => Char,
                missing_length: b"A\n" => Char,
                invalid_length: b"Afoo" => Char,
                unexpected_length: b"C1" => Char,
                missing_err_msg: b"F\n" => Char,
                missing_err_msg_delimiter: b"Fmsg" => Char,
                non_utf8_err_msg: b"F \xc0\n" => MapRes,
            }
        );

        assert_parse_result!(
            response_status {
                ok_data_nil_length: b"A0\n" => (3, Ok(Some(0))),
                ok_data_with_length: b"A101\n" => (5, Ok(Some(101))),
                ok_none: b"C\n" => (2, Ok(None)),
                err_not_found: b"D\n" => (2, Err(error::Response::KeyNotFound)),
                err_not_unique: b"E\n" => (2, Err(error::Response::KeyNotUnique)),
                err_other: b"F foo\n" => (6, Err(error::Response::Other("foo".to_string()))),
            }
        );
    }

    mod end_of_response {
        use super::*;

        does_not_panic!(end_of_response);

        assert_incomplete_parse!(end_of_response {
            empty: b"",
            partial: b"\nC",
        });

        assert_error_kind!(
            end_of_response {
                missing_newline: b"C" => Tag,
                missing_char: b"\n\n" => Tag,
            }
        );

        #[test]
        fn eor() {
            let empty: &[u8] = b"";
            assert_eq!(end_of_response(EOR), Ok((empty, 3)));
        }

        proptest! {
            #[test]
            fn parses_with_arbitary_trailing_input(
                input in proptest::string::bytes_regex("\nC\n.*").unwrap(),
            ) {
                assert_eq!(end_of_response(&input), Ok((&input[3..], 3)));
            }

            #[test]
            fn fails_with_arbitary_leading_input(
                input in proptest::string::bytes_regex("[^\n].*").unwrap()
            ) {
                assert_eq!(
                    end_of_response(&input).finish().unwrap_err().code,
                    nom::error::ErrorKind::Tag,
                );
            }
        }
    }

    mod word {
        use super::*;

        does_not_panic!(word);

        assert_incomplete_parse!(word {
            empty: b"",
            unterminated: b"foo",
        });

        assert_error_kind!(
            word {
                leading_space: b" foo" => TakeTill1,
                leading_newline: b"\nfoo" => TakeTill1,
            }
        );

        assert_parse_result!(
            word {
                trailing_space: b"foo bar" => (4, b"foo"),
                trailing_newline: b"foo\n" => (3, b"foo"),
            }
        );
    }

    mod paragraph {
        use super::*;

        does_not_panic!(paragraph);

        assert_incomplete_parse!(all {
            empty: b"",
            unterminated: b"foo",
        });
    }

    mod all {
        use super::*;

        does_not_panic!(all);

        assert_incomplete_parse!(all {
            empty: b"",
            unterminated: b"foo",
        });

        assert_parse_result!(
            all {
                terminated: b"foo bar baz\nC\n" => (11, b"foo bar baz"),
            }
        );
    }

    mod noop {
        use super::*;

        does_not_panic!(noop);

        assert_parse_result!(noop {
            empty: b"" => (0, b""),
            terminated: b"foo bar baz\nC\n" => (0, b""),
        });
    }
}