use std::fmt;
use crate::sys;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Status(i32);
impl Status {
pub const DECLINED: Self = Self(sys::STALE);
pub const OUT_OF_MEMORY: Self = Self(sys::OOM);
pub const INVALID: Self = Self(sys::INVALID);
#[must_use]
pub const fn code(self) -> i32 {
self.0
}
#[must_use]
pub fn message(self) -> &'static str {
sys::status_message(self.0)
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = self.message();
if text.is_empty() {
return write!(f, "status {}", self.0);
}
write!(f, "{text} (status {})", self.0)
}
}
impl fmt::Debug for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Status({}: {})", self.0, self.message())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Answer<T> {
Given(T),
Declined,
}
impl<T> Answer<T> {
pub fn given(self) -> Option<T> {
match self {
Self::Given(value) => Some(value),
Self::Declined => None,
}
}
#[must_use]
pub fn is_declined(&self) -> bool {
matches!(self, Self::Declined)
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Answer<U> {
match self {
Self::Given(value) => Answer::Given(f(value)),
Self::Declined => Answer::Declined,
}
}
}
impl<T> From<Answer<T>> for Option<T> {
fn from(answer: Answer<T>) -> Self {
answer.given()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Pattern {
pattern: String,
status: Status,
detail: Option<String>,
},
Search {
status: Status,
detail: Option<String>,
},
Groups {
pattern: String,
status: Status,
detail: Option<String>,
},
OutOfMemory {
detail: Option<String>,
},
Abi {
expected: u32,
found: u32,
},
NotCharBoundary {
offset: usize,
},
Inconsistent {
message: String,
},
NeedsPcre {
pattern: String,
},
Syntax {
pattern: String,
at: usize,
status: Status,
detail: Option<String>,
},
NothingLexable {
offered: usize,
},
BadWindow {
start: usize,
end: usize,
},
Plane {
plane: &'static str,
status: Status,
detail: Option<String>,
},
Unsettled {
plane: &'static str,
offered: usize,
wanted: usize,
},
}
impl Error {
#[must_use]
pub fn is_out_of_memory(&self) -> bool {
matches!(self, Self::OutOfMemory { .. })
}
#[must_use]
pub fn status(&self) -> Option<Status> {
match self {
Self::Pattern { status, .. }
| Self::Syntax { status, .. }
| Self::Search { status, .. }
| Self::Plane { status, .. }
| Self::Groups { status, .. } => Some(*status),
Self::NeedsPcre { .. } => Some(Status::DECLINED),
Self::OutOfMemory { .. } => Some(Status::OUT_OF_MEMORY),
_ => None,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NeedsPcre { pattern } => write!(
f,
"the linear grammar cannot express `{pattern}`, but the PCRE2 arm can: \
compiling it with RegexBuilder::pcre(true) accepts this pattern. That arm \
is not linear in the length of the text, which is why it is opt-in."
),
Self::Syntax {
pattern,
at,
status,
detail,
} => {
write!(f, "cannot compile pattern `{pattern}`: at byte {at}, ")?;
write_reason(f, *status, detail.as_deref())
},
Self::Pattern {
pattern,
status,
detail,
} => {
write!(f, "cannot compile pattern `{pattern}`: ")?;
write_reason(f, *status, detail.as_deref())
},
Self::Search { status, detail } => {
write!(f, "search failed: ")?;
write_reason(f, *status, detail.as_deref())
},
Self::Groups {
pattern,
status,
detail,
} => {
write!(
f,
"the capture engine will not compile `{pattern}`, so group detail is \
unavailable for its matches (searching still works): "
)?;
write_reason(f, *status, detail.as_deref())
},
Self::OutOfMemory { detail } => {
write!(f, "the engine ran out of memory: ")?;
write_reason(f, Status::OUT_OF_MEMORY, detail.as_deref())
},
Self::Abi { expected, found } => write!(
f,
"irregex ABI mismatch: this crate speaks ABI {expected}, but the linked \
library reports ABI {found}. Link a matching pair, or unset IRGX_LIB_DIR."
),
Self::NotCharBoundary { offset } => write!(
f,
"the engine reported a match boundary at byte {offset}, which is inside a \
UTF-8 codepoint; that span cannot slice the searched string. A pattern \
compiled with unicode(false) matches bytes, so this is the byte semantics \
you asked for showing through."
),
Self::Inconsistent { message } => {
write!(f, "internal disagreement in the engine: {message}")
},
Self::NothingLexable { offered } => write!(
f,
"none of the {offered} patterns offered could be determinized as an anchored \
automaton, so the munch has nothing to scan with. Compiling them one at a time \
with Regex::new says which, and why."
),
Self::BadWindow { start, end } => write!(
f,
"the search window ends at byte {end}, before it starts at {start}. \
Bounds are not clamped, because a miscomputed one is worth hearing about."
),
Self::Plane {
plane,
status,
detail,
} => {
write!(f, "the {plane} plane could not answer: ")?;
write_reason(f, *status, detail.as_deref())
},
Self::Unsettled {
plane,
offered,
wanted,
} => write!(
f,
"the {plane} plane asked for {wanted} rows after being offered {offered}, and \
kept growing across every retry. The answer is over something that is still \
being written; ask again when it has settled."
),
}
}
}
fn write_reason(f: &mut fmt::Formatter<'_>, status: Status, detail: Option<&str>) -> fmt::Result {
match detail {
Some(name) => write!(f, "{name}; {status}"),
None => write!(f, "{status}"),
}
}
impl std::error::Error for Error {}
pub(crate) fn fault(status: i32, build: impl FnOnce(Status, Option<String>) -> Error) -> Error {
debug_assert!(status < 0, "a non-negative status is not a failure");
let detail = last_fault();
if status == sys::OOM {
return Error::OutOfMemory {
detail: detail.map(|found| found.text),
};
}
build(Status(status), detail.map(|found| found.text))
}
pub(crate) fn plane_fault(status: i32, plane: &'static str) -> Error {
debug_assert_ne!(status, sys::STALE, "a declinature is not a plane failure");
fault(status, |status, detail| Error::Plane {
plane,
status,
detail,
})
}
pub(crate) fn compile_refusal(status: i32, pattern: &str) -> Error {
debug_assert!(status < 0, "a non-negative status is not a refusal");
if status == sys::STALE {
return Error::NeedsPcre {
pattern: pattern.to_owned(),
};
}
let detail = last_fault();
if status == sys::OOM {
return Error::OutOfMemory {
detail: detail.map(|found| found.text),
};
}
let at = detail
.as_ref()
.filter(|_| status == sys::INVALID)
.and_then(|found| found.at)
.filter(|at| pattern.is_char_boundary(*at));
let (pattern, detail) = (pattern.to_owned(), detail.map(|found| found.text));
match at {
Some(at) => Error::Syntax {
pattern,
at,
status: Status(status),
detail,
},
None => Error::Pattern {
pattern,
status: Status(status),
detail,
},
}
}
struct Detail {
text: String,
at: Option<usize>,
}
fn last_fault() -> Option<Detail> {
let mut slot = sys::Fault::default();
if unsafe { sys::irgx_last_fault(&raw mut slot) } != sys::MATCH {
return None;
}
if slot.name.is_null() {
return None;
}
let name = unsafe { std::ffi::CStr::from_ptr(slot.name) }
.to_str()
.ok()?;
if name.is_empty() {
return None;
}
let offset = usize::try_from(slot.at).ok();
let at = (slot.at_space == sys::AT_PATTERN)
.then_some(offset)
.flatten();
let path = (!slot.path.is_null() && slot.path_len > 0).then(|| {
let bytes = unsafe { std::slice::from_raw_parts(slot.path, slot.path_len) };
String::from_utf8_lossy(bytes)
});
let text = match (path, slot.at_space == sys::AT_FILE, offset) {
(Some(path), true, Some(offset)) => format!("{name} at {path}:{offset}"),
(Some(path), _, _) => format!("{name} at {path}"),
(None, _, _) => name.to_owned(),
};
Some(Detail { text, at })
}