util: precise errors infrastructure

Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/util/error/BUILD.bazel b/util/error/BUILD.bazel
new file mode 100644
index 0000000..eb7594e
--- /dev/null
+++ b/util/error/BUILD.bazel
@@ -0,0 +1,26 @@
+# Licensed under the Apache-2.0 license
+# SPDX-License-Identifier: Apache-2.0
+
+load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library")
+
+package(default_visibility = ["//visibility:public"])
+
+rust_library(
+    name = "error",
+    srcs = [
+        "flash.rs",
+        "ipc.rs",
+        "kernel.rs",
+        "lib.rs",
+    ],
+    crate_name = "util_error",
+    edition = "2024",
+    deps = [
+        "@pigweed//pw_status/rust:pw_status",
+    ],
+)
+
+rust_doc(
+    name = "error_doc",
+    crate = ":error",
+)
diff --git a/util/error/README.md b/util/error/README.md
new file mode 100644
index 0000000..94dced5
--- /dev/null
+++ b/util/error/README.md
@@ -0,0 +1,62 @@
+# util_error
+
+Structured error handling for OpenProt.
+
+This crate provides a mechanism for defining and using structured 32-bit error codes (`ErrorCode`) partitioned by 16-bit modules (`ErrorModule`).
+
+## Key Concepts
+
+### ErrorModule
+
+An `ErrorModule` is a 16-bit identifier that categorizes a set of error codes. It is recommended to use ASCII characters for the module ID to aid in debugging.
+
+```rust
+use util_error::ErrorModule;
+
+// Define a module with ID 'MY' (0x4d59)
+pub const MY_MODULE: ErrorModule = ErrorModule::new(0x4d59);
+```
+
+### ErrorCode
+
+An `ErrorCode` is a 32-bit value composed of:
+*   Upper 16 bits: The `ErrorModule` ID.
+*   Lower 16 bits: A module-specific error value.
+
+`ErrorCode` implements `core::error::Error`, `Display`, and `Debug`. It formats as a hex representation of the 32-bit value (e.g., `0x4b450001`).
+
+```rust
+use util_error::ErrorCode;
+
+// Create an error code under MY_MODULE
+pub const MY_ERROR: ErrorCode = MY_MODULE.error(1);
+```
+
+### Pigweed Integration
+
+`ErrorCode` supports integration with `pw_status::Error`. You can embed a Pigweed status into the lower 16 bits of the error code using `from_pw`.
+
+The lower 16 bits are partitioned as:
+*   Bits 5-15: Module-specific error code.
+*   Bits 0-4: Pigweed `pw_status::Error` (which is 5 bits).
+
+```rust
+use util_error::ErrorModule;
+use pw_status::Error;
+
+pub const MY_MODULE: ErrorModule = ErrorModule::new(0x4d59);
+
+// Create an error code that embeds pw_status::Error::InvalidArgument
+pub const MY_INVALID_ARG_ERROR: ErrorCode = MY_MODULE.from_pw(1, Error::InvalidArgument);
+```
+
+## Defined Modules
+
+The following modules are defined in this crate:
+
+| Module | ID (Hex) | ASCII | Description |
+| :--- | :--- | :--- | :--- |
+| `KERNEL_ERROR` | `0x4b45` | `KE` | Kernel-specific error codes (see [kernel.rs](file:///usr/local/google/home/cfrantz/src/openprot/errors/util/error/kernel.rs)). |
+| `FLASH_GENERIC` | `0x464c` | `FL` | Generic flash and SFDP errors (see [flash.rs](file:///usr/local/google/home/cfrantz/src/openprot/errors/util/error/flash.rs)). |
+| `FLASH_OPENTITAN`| `0x464f` | `FO` | OpenTitan-specific flash errors (see [flash.rs](file:///usr/local/google/home/cfrantz/src/openprot/errors/util/error/flash.rs)). |
+| `IPC_ERROR` | `0x4943` | `IC` | IPC-specific error codes (see [ipc.rs](file:///usr/local/google/home/cfrantz/src/openprot/errors/util/error/ipc.rs)). |
diff --git a/util/error/flash.rs b/util/error/flash.rs
new file mode 100644
index 0000000..976bfb4
--- /dev/null
+++ b/util/error/flash.rs
@@ -0,0 +1,63 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! Flash-specific error codes.
+
+use crate::{ErrorCode, ErrorModule};
+use pw_status::Error;
+
+// TODO: review the pw_status error codes.
+
+/// The generic flash error module.
+pub const FLASH_GENERIC: ErrorModule = ErrorModule::new(0x464c); //ascii `FL`.
+/// The flash device is busy.
+pub const FLASH_GENERIC_BUSY: ErrorCode = FLASH_GENERIC.from_pw(0, Error::Unavailable);
+/// The erase address is invalid (e.g., not page-aligned).
+pub const FLASH_GENERIC_ERASE_INVALID_ADDR: ErrorCode =
+    FLASH_GENERIC.from_pw(1, Error::InvalidArgument);
+/// The operation has bad alignment.
+pub const FLASH_GENERIC_BAD_ALIGNMENT: ErrorCode = FLASH_GENERIC.from_pw(2, Error::InvalidArgument);
+/// The read request is too long.
+pub const FLASH_GENERIC_READ_TOO_LONG: ErrorCode = FLASH_GENERIC.from_pw(3, Error::InvalidArgument);
+/// The program operation exceeds the hardware window size.
+pub const FLASH_GENERIC_PROGRAM_EXCEEDS_WINDOW_SIZE: ErrorCode =
+    FLASH_GENERIC.from_pw(4, Error::InvalidArgument);
+/// The program operation spans a hardware window boundary.
+pub const FLASH_GENERIC_PROGRAM_SPANS_WINDOW_BOUNDARY: ErrorCode =
+    FLASH_GENERIC.from_pw(5, Error::InvalidArgument);
+/// The address is out of bounds.
+pub const FLASH_GENERIC_ADDR_OUT_OF_BOUNDS: ErrorCode =
+    FLASH_GENERIC.from_pw(6, Error::InvalidArgument);
+/// The flash page size is invalid.
+pub const FLASH_GENERIC_INVALID_PAGE_SIZE: ErrorCode =
+    FLASH_GENERIC.from_pw(7, Error::InvalidArgument);
+/// The flash size is invalid.
+pub const FLASH_GENERIC_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(8, Error::InvalidArgument);
+/// The erase size is invalid.
+pub const FLASH_GENERIC_ERASE_INVALID_SIZE: ErrorCode =
+    FLASH_GENERIC.from_pw(9, Error::InvalidArgument);
+
+/// SFDP: Invalid memory density.
+pub const FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY: ErrorCode =
+    FLASH_GENERIC.from_pw(1024, Error::InvalidArgument);
+/// SFDP: Invalid signature.
+pub const FLASH_GENERIC_SFDP_INVALID_SIGNATURE: ErrorCode =
+    FLASH_GENERIC.from_pw(1025, Error::InvalidArgument);
+/// SFDP: No valid parameter header found.
+pub const FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND: ErrorCode =
+    FLASH_GENERIC.from_pw(1026, Error::InvalidArgument);
+/// SFDP: Parameters are too short.
+pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT: ErrorCode =
+    FLASH_GENERIC.from_pw(1027, Error::InvalidArgument);
+/// SFDP: Unsupported header major revision.
+pub const FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV: ErrorCode =
+    FLASH_GENERIC.from_pw(1028, Error::InvalidArgument);
+/// SFDP: Unsupported parameters major revision.
+pub const FLASH_GENERIC_SFDP_UNSUPPORTED_PARAMS_MAJOR_REV: ErrorCode =
+    FLASH_GENERIC.from_pw(1029, Error::InvalidArgument);
+/// SFDP: Parameters are too long.
+pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_LONG: ErrorCode =
+    FLASH_GENERIC.from_pw(1030, Error::InvalidArgument);
+
+/// The OpenTitan flash error module.
+pub const FLASH_OPENTITAN: ErrorModule = ErrorModule::new(0x464f); //ascii `FO`.
diff --git a/util/error/ipc.rs b/util/error/ipc.rs
new file mode 100644
index 0000000..e86db79
--- /dev/null
+++ b/util/error/ipc.rs
@@ -0,0 +1,20 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! IPC-specific error codes.
+
+use crate::{ErrorCode, ErrorModule};
+use pw_status::Error;
+
+/// The IPC error module.
+pub const IPC_ERROR: ErrorModule = ErrorModule::new(0x4943); // ascii `IC`
+/// The IPC response has a bad length.
+pub const IPC_ERROR_RSP_BAD_LEN: ErrorCode = IPC_ERROR.from_pw(1, Error::InvalidArgument);
+/// The IPC response is too large.
+pub const IPC_ERROR_RSP_TOO_LARGE: ErrorCode = IPC_ERROR.from_pw(2, Error::InvalidArgument);
+/// The IPC request is bad.
+pub const IPC_ERROR_BAD_REQ: ErrorCode = IPC_ERROR.from_pw(3, Error::InvalidArgument);
+/// The IPC request has a bad length.
+pub const IPC_ERROR_BAD_REQ_LEN: ErrorCode = IPC_ERROR.from_pw(4, Error::InvalidArgument);
+/// The IPC opcode is unknown.
+pub const IPC_ERROR_UNKNOWN_OP: ErrorCode = IPC_ERROR.from_pw(5, Error::Unknown);
diff --git a/util/error/kernel.rs b/util/error/kernel.rs
new file mode 100644
index 0000000..f0f82db
--- /dev/null
+++ b/util/error/kernel.rs
@@ -0,0 +1,41 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! Kernel-specific error codes.
+
+use crate::{ErrorCode, ErrorModule};
+
+/// The kernel error module.
+pub const KERNEL_ERROR: ErrorModule = ErrorModule::new(0x4b45); // ascii `KE`
+/// The operation was cancelled.
+pub const KERNEL_ERROR_CANCELLED: ErrorCode = KERNEL_ERROR.error(1);
+/// An unknown error occurred in the kernel.
+pub const KERNEL_ERROR_UNKNOWN: ErrorCode = KERNEL_ERROR.error(2);
+/// An invalid argument was provided to a kernel call.
+pub const KERNEL_ERROR_INVALID_ARGUMENT: ErrorCode = KERNEL_ERROR.error(3);
+/// The deadline for the operation was exceeded.
+pub const KERNEL_ERROR_DEADLINE_EXCEEDED: ErrorCode = KERNEL_ERROR.error(4);
+/// The requested resource was not found.
+pub const KERNEL_ERROR_NOT_FOUND: ErrorCode = KERNEL_ERROR.error(5);
+/// The resource already exists.
+pub const KERNEL_ERROR_ALREADY_EXISTS: ErrorCode = KERNEL_ERROR.error(6);
+/// Permission was denied for the operation.
+pub const KERNEL_ERROR_PERMISSION_DENIED: ErrorCode = KERNEL_ERROR.error(7);
+/// Resources have been exhausted.
+pub const KERNEL_ERROR_RESOURCE_EXHAUSTED: ErrorCode = KERNEL_ERROR.error(8);
+/// A precondition for the operation failed.
+pub const KERNEL_ERROR_FAILED_PRECONDITION: ErrorCode = KERNEL_ERROR.error(9);
+/// The operation was aborted.
+pub const KERNEL_ERROR_ABORTED: ErrorCode = KERNEL_ERROR.error(10);
+/// The value is out of range.
+pub const KERNEL_ERROR_OUT_OF_RANGE: ErrorCode = KERNEL_ERROR.error(11);
+/// The operation is unimplemented.
+pub const KERNEL_ERROR_UNIMPLEMENTED: ErrorCode = KERNEL_ERROR.error(12);
+/// An internal kernel error occurred.
+pub const KERNEL_ERROR_INTERNAL: ErrorCode = KERNEL_ERROR.error(13);
+/// The service or resource is unavailable.
+pub const KERNEL_ERROR_UNAVAILABLE: ErrorCode = KERNEL_ERROR.error(14);
+/// Data loss has occurred.
+pub const KERNEL_ERROR_DATA_LOSS: ErrorCode = KERNEL_ERROR.error(15);
+/// The caller is unauthenticated.
+pub const KERNEL_ERROR_UNAUTHENTICATED: ErrorCode = KERNEL_ERROR.error(16);
diff --git a/util/error/lib.rs b/util/error/lib.rs
new file mode 100644
index 0000000..f509408
--- /dev/null
+++ b/util/error/lib.rs
@@ -0,0 +1,122 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! Error code handling.
+
+#![no_std]
+
+use core::num::NonZero;
+
+mod flash;
+mod ipc;
+mod kernel;
+
+pub use flash::*;
+pub use ipc::*;
+pub use kernel::*;
+
+/// Represents an error module.
+///
+/// An error module is a non-zero 16-bit identifier that categorizes a set of
+/// error codes.
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct ErrorModule(pub NonZero<u16>);
+
+impl ErrorModule {
+    /// Creates a new `ErrorModule`.
+    ///
+    /// # Panics
+    /// Panics if `val` is zero.
+    pub const fn new(val: u16) -> Self {
+        match NonZero::new(val) {
+            Some(val) => Self(val),
+            None => panic!("ErrorModule must be non-zero"),
+        }
+    }
+
+    /// Creates an `ErrorCode` within this module.
+    ///
+    /// The resulting `ErrorCode` will have the module ID in the upper 16 bits
+    /// and the provided `code` in the lower 16 bits.
+    pub const fn error(self, code: u16) -> ErrorCode {
+        ErrorCode::new(((self.0.get() as u32) << 16) | (code as u32))
+    }
+
+    /// Creates an `ErrorCode` from a Pigweed status.
+    ///
+    /// This is a convenience method for creating error codes that incorporate
+    /// a Pigweed status.
+    pub const fn from_pw(self, code: u16, err: pw_status::Error) -> ErrorCode {
+        // pw_status::Error is 5 bits.
+        self.error((code << 5) | (err as u16))
+    }
+}
+
+/// A 32-bit error code.
+///
+/// An error code consists of a 16-bit module ID and a 16-bit module-specific
+/// error value.
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct ErrorCode(pub NonZero<u32>);
+
+impl ErrorCode {
+    /// Creates a new `ErrorCode`.
+    ///
+    /// # Panics
+    /// Panics if `val` is zero.
+    pub const fn new(val: u32) -> Self {
+        match NonZero::new(val) {
+            Some(val) => Self(val),
+            None => panic!("ErrorCode must be non-zero"),
+        }
+    }
+
+    /// Creates a kernel error code from a Pigweed status.
+    pub fn kernel_error(e: pw_status::Error) -> Self {
+        KERNEL_ERROR.error(e as u16)
+    }
+}
+
+impl From<ErrorCode> for u32 {
+    fn from(e: ErrorCode) -> u32 {
+        e.0.get()
+    }
+}
+
+/*
+ * TODO: decide if we want ufmt or not.
+use ufmt::{uDebug, uDisplay, uwrite};
+impl uDisplay for ErrorCode {
+    fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
+    where
+        W: ufmt::uWrite + ?Sized,
+    {
+        uwrite!(f, "0x{:x}", self.0.get())
+    }
+}
+
+impl uDebug for ErrorCode {
+    fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
+    where
+        W: ufmt::uWrite + ?Sized,
+    {
+        uDisplay::fmt(self, f)
+    }
+}
+*/
+
+impl core::fmt::Display for ErrorCode {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        write!(f, "0x{:x}", self.0.get())
+    }
+}
+
+impl core::fmt::Debug for ErrorCode {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        core::fmt::Display::fmt(self, f)
+    }
+}
+
+impl core::error::Error for ErrorCode {}