use std::fmt;
use thiserror::Error;
use winnow::Parser;
use winnow::error::{ContextError, ErrMode, ParseError, StrContext, StrContextValue};
use winnow::stream::{Offset, Stream};
use winnow::token::literal;
pub type NixUriResult<T> = Result<T, NixUriError>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum NixUriError {
#[error("parse error at byte {position}: expected {expected}")]
Parse {
position: usize,
expected: ParseExpected,
},
#[error("{0}")]
Unsupported(UnsupportedReason),
#[error("not a valid URL: {0}")]
InvalidUrl(String),
#[error("invalid value for `{field}`: {reason}")]
InvalidValue { field: &'static str, reason: String },
#[error("`{left}` and `{right}` are mutually exclusive")]
FieldConflict {
left: &'static str,
right: &'static str,
},
#[error("input `{input}` has no scheme; bare two-segment shorthand is not supported")]
MissingScheme { input: String },
#[error("indirect form accepts at most 3 segments, got {count}")]
TooManyIndirectSegments { count: usize },
#[error("URL parsing error: {0}")]
ServoUrl(#[from] url::ParseError),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ParseExpected {
Tag(&'static str),
Char(char),
Eof,
Alpha,
Digit,
HexDigit,
AlphaNumeric,
Space,
Multispace,
Description(&'static str),
Label(&'static str),
Alternatives,
Unknown,
Other(String),
}
impl fmt::Display for ParseExpected {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Tag(t) => write!(f, "tag `{t}`"),
Self::Char(c) => write!(f, "char `{c}`"),
Self::Eof => f.write_str("end of input"),
Self::Alpha => f.write_str("an alphabetic character"),
Self::Digit => f.write_str("a digit"),
Self::HexDigit => f.write_str("a hex digit"),
Self::AlphaNumeric => f.write_str("an alphanumeric character"),
Self::Space => f.write_str("a space"),
Self::Multispace => f.write_str("whitespace"),
Self::Description(d) => f.write_str(d),
Self::Label(s) => write!(f, "label `{s}`"),
Self::Alternatives => f.write_str("one of several alternatives"),
Self::Unknown => f.write_str("an unrecognised parser context"),
Self::Other(s) => f.write_str(s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnsupportedReason {
Param { name: String },
Field {
field: String,
only_supported_by: String,
},
UriType { ty: String },
TransportLayer { ty: String },
MissingParameter { ty: String, parameter: String },
Authority { scheme: &'static str },
}
impl fmt::Display for UnsupportedReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Param { name } => {
write!(
f,
"the parameter `{name}` is not supported by the flakeref type"
)
}
Self::Field {
field,
only_supported_by,
} => write!(f, "field `{field}` only supported by `{only_supported_by}`"),
Self::UriType { ty } => write!(f, "unknown URI type `{ty}`"),
Self::TransportLayer { ty } => write!(f, "unknown transport layer `{ty}`"),
Self::MissingParameter { ty, parameter } => write!(
f,
"FlakeRef type `{ty}` is missing required parameter `{parameter}`"
),
Self::Authority { scheme } => {
write!(f, "the `{scheme}:` scheme does not accept a URL authority")
}
}
}
}
pub(crate) fn tag<'i>(
expected: &'static str,
) -> impl Parser<&'i str, &'i str, ErrMode<ContextError>> {
literal(expected).context(StrContext::Expected(StrContextValue::StringLiteral(
expected,
)))
}
#[allow(dead_code)]
pub(crate) fn parse_error_from_winnow(
original: &str,
pe: &ParseError<&str, ContextError>,
) -> NixUriError {
let position = offset_within(original, pe.input()) + pe.offset();
NixUriError::Parse {
position,
expected: parse_expected_from_context(pe.inner()),
}
}
pub(crate) fn run_partial<'i, P, O>(
original: &'i str,
input: &'i str,
mut parser: P,
) -> Result<(&'i str, O), NixUriError>
where
P: Parser<&'i str, O, ErrMode<ContextError>>,
{
let mut current = input;
let start = current.checkpoint();
match parser.parse_next(&mut current) {
Ok(o) => Ok((current, o)),
Err(err_mode) => {
let inner = match err_mode {
ErrMode::Backtrack(e) | ErrMode::Cut(e) => e,
ErrMode::Incomplete(_) => {
unreachable!("complete parsers do not return Incomplete")
}
};
let local_offset = current.offset_from(&start);
let position = offset_within(original, input) + local_offset;
Err(NixUriError::Parse {
position,
expected: parse_expected_from_context(&inner),
})
}
}
}
pub(crate) fn parse_expected_from_context(err: &ContextError) -> ParseExpected {
for ctx in err.context() {
if let StrContext::Expected(value) = ctx {
return match value {
StrContextValue::StringLiteral(s) => ParseExpected::Tag(s),
StrContextValue::CharLiteral(c) => ParseExpected::Char(*c),
StrContextValue::Description(d) => description_to_expected(d),
_ => ParseExpected::Unknown,
};
}
}
for ctx in err.context() {
if let StrContext::Label(s) = ctx {
return ParseExpected::Label(s);
}
}
ParseExpected::Alternatives
}
fn description_to_expected(d: &'static str) -> ParseExpected {
match d {
"end of input" => ParseExpected::Eof,
"an alphabetic character" => ParseExpected::Alpha,
"a digit" => ParseExpected::Digit,
"a hex digit" => ParseExpected::HexDigit,
"an alphanumeric character" => ParseExpected::AlphaNumeric,
"a space" => ParseExpected::Space,
"whitespace" => ParseExpected::Multispace,
other => ParseExpected::Description(other),
}
}
fn offset_within(original: &str, slice: &str) -> usize {
let orig_start = original.as_ptr() as usize;
let orig_end = orig_start.saturating_add(original.len());
let s_start = slice.as_ptr() as usize;
debug_assert!(
s_start >= orig_start && s_start <= orig_end,
"offset_within: slice is not a sub-slice of original",
);
if s_start >= orig_start && s_start <= orig_end {
s_start - orig_start
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn context_iter_returns_innermost_first() {
let r: Result<&str, ParseError<&str, ContextError>> =
literal::<_, &str, ErrMode<ContextError>>("x")
.context(StrContext::Expected(StrContextValue::StringLiteral("x")))
.context(StrContext::Label("outer"))
.parse("abc");
let inner = r.unwrap_err().into_inner();
let labels: Vec<_> = inner.context().collect();
assert_eq!(labels.len(), 2);
match labels[0] {
StrContext::Expected(StrContextValue::StringLiteral(s)) => assert_eq!(*s, "x"),
other => panic!("expected innermost StringLiteral(\"x\"), got {other:?}"),
}
match labels[1] {
StrContext::Label(s) => assert_eq!(*s, "outer"),
other => panic!("expected outermost Label(\"outer\"), got {other:?}"),
}
}
#[test]
fn parse_position_reports_owner_repo_separator() {
let err = "github:n".parse::<crate::FlakeRef>().unwrap_err();
match err {
NixUriError::Parse { position, expected } => {
assert_eq!(position, 8, "expected offset 8, got {position}");
assert_eq!(
expected,
ParseExpected::Char('/'),
"expected Char('/'), got {expected:?}",
);
}
other => panic!("expected NixUriError::Parse, got {other:?}"),
}
}
#[test]
fn description_table_round_trips() {
assert_eq!(description_to_expected("end of input"), ParseExpected::Eof);
assert_eq!(
description_to_expected("an alphabetic character"),
ParseExpected::Alpha
);
assert_eq!(description_to_expected("a digit"), ParseExpected::Digit);
assert_eq!(
description_to_expected("a hex digit"),
ParseExpected::HexDigit
);
assert_eq!(
description_to_expected("an alphanumeric character"),
ParseExpected::AlphaNumeric,
);
assert_eq!(description_to_expected("a space"), ParseExpected::Space);
assert_eq!(
description_to_expected("whitespace"),
ParseExpected::Multispace
);
assert_eq!(
description_to_expected("not in the table"),
ParseExpected::Description("not in the table"),
);
}
#[test]
fn parse_expected_does_not_collapse_to_other() {
for input in [
"github:!",
"github:o/r?ref=invalid ref",
"garbage::scheme",
"github:",
"github:nixos/",
] {
let err = input.parse::<crate::FlakeRef>().unwrap_err();
if let NixUriError::Parse { expected, .. } = err {
assert!(
!matches!(expected, ParseExpected::Other(_)),
"input {input:?} produced ParseExpected::Other({expected:?})",
);
}
}
}
}