yuri 0.1.0

no_std alloc-free URI Parser
Documentation
//! URI Authority

use logos::{Lexer, Logos};

use crate::UserInfo;
use crate::{error::AuthorityError, Authority};

#[derive(Debug, Logos)]
pub(super) enum AuthorityToken<'uri> {
    #[token("@", priority = 200)]
    At,

    #[regex(r"\[[a-f0-9:]+\]", |lex| lex.slice(), priority = 250)]
    MaybeIpv6Hostname(&'uri str),

    #[token(":", priority = 200)]
    Colon,

    #[regex(r"[/?#]", priority = 200)]
    MaybePathStart(&'uri str),

    #[regex(r"[^@:/?#]+", |lex| lex.slice(), priority = 100)]
    MaybeSomethingElse(&'uri str),
}

#[derive(Debug, PartialEq)]
enum Stage {
    Nowhere,
    WantAt,
    SeenFirstBit,
    WantSecondBit,
    WantHost,
    GotHost,
    WantPort,
    GotPort,
}

pub(super) fn parse_authority<'uri>(
    lexer: &mut Lexer<'uri, AuthorityToken<'uri>>,
) -> Result<(Authority<'uri>, Option<&'uri str>), AuthorityError<'uri>> {
    let mut stage: Stage = Stage::Nowhere;
    let mut host: Option<&'uri str> = None;
    let mut first_bit: Option<&'uri str> = None;
    let mut second_bit: Option<&'uri str> = None;
    let mut port: Option<u16> = None;
    let mut carry: Option<&'uri str> = None;

    while let Some(token) = lexer.next() {
        match token {
            Ok(AuthorityToken::At) if stage == Stage::WantSecondBit || stage == Stage::WantAt => {
                stage = Stage::WantHost;
            }
            Ok(AuthorityToken::Colon) if stage == Stage::SeenFirstBit => {
                stage = Stage::WantSecondBit;
            }
            Ok(AuthorityToken::Colon) if stage == Stage::GotHost => {
                stage = Stage::WantPort;
            }
            Ok(AuthorityToken::MaybePathStart(start)) => {
                carry = Some(start);
                break;
            }
            Ok(AuthorityToken::MaybeSomethingElse(something)) if stage == Stage::Nowhere => {
                first_bit = Some(something);
                stage = Stage::SeenFirstBit;
            }
            Ok(AuthorityToken::MaybeIpv6Hostname(something)) if stage == Stage::Nowhere => {
                first_bit = Some(something);
                stage = Stage::SeenFirstBit;
            }
            Ok(AuthorityToken::MaybeSomethingElse(something)) if stage == Stage::WantSecondBit => {
                second_bit = Some(something);
                stage = Stage::WantAt;
            }
            Ok(AuthorityToken::MaybeSomethingElse(something)) if stage == Stage::WantHost => {
                host = Some(something);
                stage = Stage::GotHost;
            }
            Ok(AuthorityToken::MaybeIpv6Hostname(maybehost)) if stage == Stage::WantHost => {
                host = Some(maybehost);
                stage = Stage::GotHost;
            }
            Ok(AuthorityToken::MaybeSomethingElse(something)) if stage == Stage::WantPort => {
                port = Some(something.parse().map_err(|_| AuthorityError::InvalidPort)?);
                stage = Stage::GotPort;
            }
            _ => {
                let cut_slice = &lexer.source()[lexer.span().start..];
                let cut_span = &lexer.source()[lexer.span().start..lexer.span().end];

                let detail = crate::error::ParsingDetail {
                    component: "authority",
                    span_start: lexer.span().start,
                    span_end: lexer.span().end,
                    source: lexer.source(),
                    clipped_span: cut_span,
                    clipped_remaining: cut_slice,
                };
                return Err(AuthorityError::ParsingDetailed(detail));
            }
        }
    }

    if stage == Stage::Nowhere {
        return Err(AuthorityError::ParsedNothing);
    }

    if host.is_none() {
        if let Some(first_bit) = first_bit {
            if let Some(second_bit) = second_bit {
                port = Some(
                    second_bit
                        .parse()
                        .map_err(|_| AuthorityError::InvalidPort)?,
                );
            }
            return Ok((
                Authority {
                    userinfo: None,
                    raw_host: first_bit,
                    port,
                },
                carry,
            ));
        }
        return Err(AuthorityError::MissingHost);
    }

    if let Some(host) = host {
        let userinfo = if let Some(first_bit) = first_bit {
            Some(UserInfo {
                raw_user: first_bit,
                raw_authorization: second_bit,
            })
        } else {
            None
        };
        return Ok((
            Authority {
                userinfo,
                raw_host: host,
                port,
            },
            carry,
        ));
    }

    Err(AuthorityError::InvalidAuthority)
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::error::ParsingDetail;
    use rstest::rstest;

    const fn userinfo(user: &'static str, auth: Option<&'static str>) -> Option<UserInfo<'static>> {
        Some(UserInfo {
            raw_user: user,
            raw_authorization: auth,
        })
    }
    const fn auth(
        userinfo: Option<UserInfo<'static>>,
        host: &'static str,
        port: Option<u16>,
    ) -> Authority<'static> {
        Authority {
            userinfo,
            raw_host: host,
            port,
        }
    }

    #[rstest]
    #[case(
        "user:authorization@foo.test:8181/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(userinfo("user", Some("authorization")), "foo.test", Some(8181)), Some("/")))
    )]
    #[case(
        "user:authorization@[2001:db8::7]:8181/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(userinfo("user", Some("authorization")), "[2001:db8::7]", Some(8181)), Some("/")))
    )]
    #[case(
        "user:authorization@foo.test:/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(userinfo("user", Some("authorization")), "foo.test", None), Some("/")))
    )]
    #[case(
        "user:authorization@foo.test/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(userinfo("user", Some("authorization")), "foo.test", None), Some("/")))
    )]
    #[case(
        "user:@foo.test/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(userinfo("user", None), "foo.test", None), Some("/")))
    )]
    #[case(
        "foo.test/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(None, "foo.test", None), Some("/")))
    )]
    #[case(
        "foo.test:800/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Ok((auth(None, "foo.test", Some(800)), Some("/")))
    )]
    #[case(
        "foo.test:800?foo=bar",
        "foo=bar",
        Ok((auth(None, "foo.test", Some(800)), Some("?")))
    )]
    #[case(
        "[2001:db8::7]:800?foo=bar",
        "foo=bar",
        Ok((auth(None, "[2001:db8::7]", Some(800)), Some("?")))
    )]
    #[case(
        "foo.test:800#foobar",
        "foobar",
        Ok((auth(None, "foo.test", Some(800)), Some("#")))
    )]
    #[case(
        ":800/path/nowhere?foo=bar", "800/path/nowhere?foo=bar",
        Err(AuthorityError::ParsingDetailed(ParsingDetail { component: "authority", span_start: 0, span_end: 1, source: ":800/path/nowhere?foo=bar", clipped_span: ":", clipped_remaining: ":800/path/nowhere?foo=bar" }))
    )]
    #[case(
        "/path/nowhere?foo=bar",
        "path/nowhere?foo=bar",
        Err(AuthorityError::ParsedNothing)
    )]
    fn t_authority(
        #[case] s: &'static str,
        #[case] remaining: &'static str,
        #[case] expected: Result<(Authority<'static>, Option<&str>), AuthorityError<'static>>,
    ) {
        let mut lexer = AuthorityToken::lexer(s);
        let a = parse_authority(&mut lexer);
        assert_eq!(expected, a);
        assert_eq!(remaining, lexer.remainder());
    }
}