Expose error_code so consumers can read a code instead of parsing one

io_error_code was private, so the predicates built on it (is_halt,
is_skippable_title_stub, is_disc_level_no_key) were the only way to ask
anything about an io::Error's origin. A consumer that needs the code
itself — to report WHY a title failed rather than to branch on one of
three known cases — had no route to it: mux_stream returns an io::Error,
the typed Error is gone by then, and only the E<code> string prefix
survives.

That left every front-end to re-implement the prefix parse by hand,
which is precisely the string-matching 1.5.x spent its time removing.
One parser, exported.

No behaviour change: the function is unchanged and the three predicates
still call it.
This commit is contained in:
Matthew Jackson
2026-07-31 14:46:39 -07:00
parent e9811a1e01
commit d50a7173ad
3 changed files with 16 additions and 6 deletions
+12 -4
View File
@@ -1055,13 +1055,21 @@ pub type Result<T> = std::result::Result<T, Error>;
/// The numeric error code carried by an [`io::Error`](std::io::Error) that was /// The numeric error code carried by an [`io::Error`](std::io::Error) that was
/// produced from an [`Error`], or `None` if it carries none. /// produced from an [`Error`], or `None` if it carries none.
/// ///
/// Public because consumers need the code itself, not just the yes/no
/// predicates built on it below. `mux_stream` hands back an `io::Error`, and a
/// front-end reporting *why* a title failed had no way to recover the code
/// from it — the typed `Error` is gone by then and only the `E<code>` string
/// prefix survives. Parsing that prefix is this function's job; every consumer
/// re-implementing the parse is how the string-matching this crate spent 1.5.x
/// removing comes back.
///
/// [`From<Error> for io::Error`] is the ONLY path from a typed [`Error`] to an /// [`From<Error> for io::Error`] is the ONLY path from a typed [`Error`] to an
/// `io::Error` in this crate, and it stringifies (`io::Error::new(kind, msg)` /// `io::Error` in this crate, and it stringifies (`io::Error::new(kind, msg)`
/// where `msg` is the `Error`'s `E<code>[: …]` [`Display`](std::fmt::Display) /// where `msg` is the `Error`'s `E<code>[: …]` [`Display`](std::fmt::Display)
/// string) rather than boxing the typed value — no code path constructs an /// string) rather than boxing the typed value — no code path constructs an
/// `io::Error` that still holds a `crate::error::Error` via `get_ref`. So the /// `io::Error` that still holds a `crate::error::Error` via `get_ref`. So the
/// only recognised shape is the round-tripped `E<code>` message prefix. /// only recognised shape is the round-tripped `E<code>` message prefix.
fn io_error_code(e: &std::io::Error) -> Option<u16> { pub fn error_code(e: &std::io::Error) -> Option<u16> {
// Round-tripped: `From<Error> for io::Error` stringifies as "E<code>[: …]". // Round-tripped: `From<Error> for io::Error` stringifies as "E<code>[: …]".
let s = e.to_string(); let s = e.to_string();
let digits = s.strip_prefix('E')?; let digits = s.strip_prefix('E')?;
@@ -1097,7 +1105,7 @@ fn io_error_code(e: &std::io::Error) -> Option<u16> {
/// all of them and exited successfully. It is [`is_disc_level_no_key`]'s, and /// all of them and exited successfully. It is [`is_disc_level_no_key`]'s, and
/// fatal here. /// fatal here.
pub fn is_skippable_title_stub(e: &std::io::Error) -> bool { pub fn is_skippable_title_stub(e: &std::io::Error) -> bool {
matches!(io_error_code(e), Some(E_MKV_INVALID | E_CSS_KEY_MISSING)) matches!(error_code(e), Some(E_MKV_INVALID | E_CSS_KEY_MISSING))
} }
/// Whether an [`io::Error`](std::io::Error) is a cooperative user stop /// Whether an [`io::Error`](std::io::Error) is a cooperative user stop
@@ -1106,7 +1114,7 @@ pub fn is_skippable_title_stub(e: &std::io::Error) -> bool {
/// `completed = false`, and consumers preserve staging rather than quarantining. /// `completed = false`, and consumers preserve staging rather than quarantining.
/// Typed replacement for the consumers' `E<code>`-leading-token string match. /// Typed replacement for the consumers' `E<code>`-leading-token string match.
pub fn is_halt(e: &std::io::Error) -> bool { pub fn is_halt(e: &std::io::Error) -> bool {
io_error_code(e) == Some(E_HALTED) error_code(e) == Some(E_HALTED)
} }
/// Whether an [`io::Error`](std::io::Error) is a **disc-level** key failure — /// Whether an [`io::Error`](std::io::Error) is a **disc-level** key failure —
@@ -1126,7 +1134,7 @@ pub fn is_halt(e: &std::io::Error) -> bool {
/// "empty stub" notice and exited successfully. /// "empty stub" notice and exited successfully.
pub fn is_disc_level_no_key(e: &std::io::Error) -> bool { pub fn is_disc_level_no_key(e: &std::io::Error) -> bool {
matches!( matches!(
io_error_code(e), error_code(e),
Some(E_NO_DISC_KEY | E_KEYDB_LOAD | E_AACS_NO_KEYS | E_CSS_NO_DISC_KEY) Some(E_NO_DISC_KEY | E_KEYDB_LOAD | E_AACS_NO_KEYS | E_CSS_NO_DISC_KEY)
) )
} }
+1 -1
View File
@@ -217,7 +217,7 @@ mod tests {
// The three arms must be DISTINGUISHABLE, not merely non-Ok. Each // The three arms must be DISTINGUISHABLE, not merely non-Ok. Each
// carries its own numeric code through the "E<code>" prefix that // carries its own numeric code through the "E<code>" prefix that
// `From<Error> for io::Error` mints — the only shape `io_error_code` // `From<Error> for io::Error` mints — the only shape `error_code`
// recognises. A bare `ErrorKind` cannot be classified, which is how a // recognises. A bare `ErrorKind` cannot be classified, which is how a
// user cancel here used to read as a hard I/O failure. // user cancel here used to read as a hard I/O failure.
let lost = bounded_failure_to_result(BoundedError::WorkerLost) let lost = bounded_failure_to_result(BoundedError::WorkerLost)
+3 -1
View File
@@ -156,7 +156,9 @@ pub use session::{
// All fallible APIs return `Result<T, Error>`. `Error` is a typed enum with a // All fallible APIs return `Result<T, Error>`. `Error` is a typed enum with a
// numeric `code()`; **no English text in the library** — applications map // numeric `code()`; **no English text in the library** — applications map
// codes to localized messages. See `error.rs` for the full taxonomy. // codes to localized messages. See `error.rs` for the full taxonomy.
pub use error::{Error, Result, is_disc_level_no_key, is_halt, is_skippable_title_stub}; pub use error::{
Error, Result, error_code, is_disc_level_no_key, is_halt, is_skippable_title_stub,
};
// ─── Cooperative cancellation ─────────────────────────────────────────────── // ─── Cooperative cancellation ───────────────────────────────────────────────
// //