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);
#[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, 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>,
},
}
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::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}")
},
}
}
}
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 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;
}
debug_assert_ne!(
slot.at_space,
sys::AT_FILE,
"no verb in this plane reads a file, so a file-space offset cannot be about anything \
the caller can see"
);
let at = (slot.at_space == sys::AT_PATTERN)
.then(|| usize::try_from(slot.at).ok())
.flatten();
let about_a_file = !slot.path.is_null() && slot.path_len > 0;
if !about_a_file {
return Some(Detail {
text: name.to_owned(),
at,
});
}
let path = unsafe { std::slice::from_raw_parts(slot.path, slot.path_len) };
Some(Detail {
text: format!("{name} at {}", String::from_utf8_lossy(path)),
at,
})
}