use core::fmt;
use crate::{BackendError, BackendErrorClass, Error};
pub const DIAGNOSTIC_SCHEMA: &str = "hisi-rf-error/v3";
pub const DIAGNOSTIC_TRACE_CAPACITY: usize = 4;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticCode {
AlreadyInitialized,
BackendInitialize,
BackendBusy,
OperationTimeout,
BackendTimeout,
OperationCancelled,
ResourceUnavailable,
UnsupportedSecurity,
ConnectionFailed,
BackendOther,
Protocol,
}
impl DiagnosticCode {
pub const fn as_str(self) -> &'static str {
match self {
Self::AlreadyInitialized => "radio.already_initialized",
Self::BackendInitialize => "backend.initialize",
Self::BackendBusy => "backend.busy",
Self::OperationTimeout => "operation.timeout",
Self::BackendTimeout => "backend.timeout",
Self::OperationCancelled => "operation.cancelled",
Self::ResourceUnavailable => "resource.unavailable",
Self::UnsupportedSecurity => "wifi.unsupported_security",
Self::ConnectionFailed => "wifi.connection_failed",
Self::BackendOther => "backend.other",
Self::Protocol => "radio.protocol",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticStage {
Initialize,
ControlPlane,
Connect,
Scan,
Authenticate,
Associate,
Sae,
Eapol,
Pmf,
Disconnect,
Runtime,
Operation,
Backend,
}
impl DiagnosticStage {
pub const fn as_str(self) -> &'static str {
match self {
Self::Initialize => "initialize",
Self::ControlPlane => "control_plane",
Self::Connect => "connect",
Self::Scan => "scan",
Self::Authenticate => "authenticate",
Self::Associate => "associate",
Self::Sae => "sae",
Self::Eapol => "eapol",
Self::Pmf => "pmf",
Self::Disconnect => "disconnect",
Self::Runtime => "runtime",
Self::Operation => "operation",
Self::Backend => "backend",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticTraceKind {
BackendStatus,
VendorStatus,
IeeeStatus,
HostapStatus,
DisconnectReason,
SupplicantContext,
DriverContext,
RuntimeCode,
ResourceRequired,
ResourceAvailable,
ResourceOwner,
LargestContiguous,
}
impl DiagnosticTraceKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::BackendStatus => "backend_status",
Self::VendorStatus => "vendor_status",
Self::IeeeStatus => "ieee_status",
Self::HostapStatus => "hostap_status",
Self::DisconnectReason => "disconnect_reason",
Self::SupplicantContext => "supplicant_context",
Self::DriverContext => "driver_context",
Self::RuntimeCode => "runtime_code",
Self::ResourceRequired => "resource_required",
Self::ResourceAvailable => "resource_available",
Self::ResourceOwner => "resource_owner",
Self::LargestContiguous => "largest_contiguous",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DiagnosticTraceEntry {
kind: DiagnosticTraceKind,
value: u32,
}
impl DiagnosticTraceEntry {
pub const fn kind(self) -> DiagnosticTraceKind {
self.kind
}
pub const fn value(self) -> u32 {
self.value
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DiagnosticTrace {
entries: [Option<DiagnosticTraceEntry>; DIAGNOSTIC_TRACE_CAPACITY],
len: u8,
truncated: bool,
}
impl DiagnosticTrace {
pub const fn new() -> Self {
Self {
entries: [None; DIAGNOSTIC_TRACE_CAPACITY],
len: 0,
truncated: false,
}
}
pub const fn len(self) -> usize {
self.len as usize
}
pub const fn is_empty(self) -> bool {
self.len == 0
}
pub const fn is_truncated(self) -> bool {
self.truncated
}
pub const fn get(self, index: usize) -> Option<DiagnosticTraceEntry> {
if index < self.len as usize {
self.entries[index]
} else {
None
}
}
pub(crate) fn push(&mut self, kind: DiagnosticTraceKind, value: u32) {
let index = self.len as usize;
if index < DIAGNOSTIC_TRACE_CAPACITY {
self.entries[index] = Some(DiagnosticTraceEntry { kind, value });
self.len += 1;
} else {
self.truncated = true;
}
}
}
impl Default for DiagnosticTrace {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecoveryAction {
UseExistingController,
Reinitialize,
WaitAndRetry,
RetryOperation,
ProvideResources,
SelectSupportedSecurity,
InspectNetworkAndRetry,
InspectBackendCode,
RecreateController,
}
impl RecoveryAction {
pub const fn as_str(self) -> &'static str {
match self {
Self::UseExistingController => "use_existing_controller",
Self::Reinitialize => "reinitialize",
Self::WaitAndRetry => "wait_and_retry",
Self::RetryOperation => "retry_operation",
Self::ProvideResources => "provide_resources",
Self::SelectSupportedSecurity => "select_supported_security",
Self::InspectNetworkAndRetry => "inspect_network_and_retry",
Self::InspectBackendCode => "inspect_backend_code",
Self::RecreateController => "recreate_controller",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Diagnostic {
code: DiagnosticCode,
stage: DiagnosticStage,
action: RecoveryAction,
backend_code: Option<u32>,
profile_revision: Option<&'static str>,
trace: DiagnosticTrace,
}
impl Diagnostic {
pub const fn schema(self) -> &'static str {
DIAGNOSTIC_SCHEMA
}
pub const fn code(self) -> DiagnosticCode {
self.code
}
pub const fn stage(self) -> DiagnosticStage {
self.stage
}
pub const fn action(self) -> RecoveryAction {
self.action
}
pub const fn backend_code(self) -> Option<u32> {
self.backend_code
}
pub const fn profile_revision(self) -> Option<&'static str> {
self.profile_revision
}
pub const fn trace(self) -> DiagnosticTrace {
self.trace
}
pub const fn docs_anchor(self) -> &'static str {
match self.code {
DiagnosticCode::AlreadyInitialized => "errors-radio-already-initialized",
DiagnosticCode::BackendInitialize => "errors-backend-initialize",
DiagnosticCode::BackendBusy => "errors-backend-busy",
DiagnosticCode::OperationTimeout => "errors-operation-timeout",
DiagnosticCode::BackendTimeout => "errors-backend-timeout",
DiagnosticCode::OperationCancelled => "errors-operation-cancelled",
DiagnosticCode::ResourceUnavailable => "errors-resource-unavailable",
DiagnosticCode::UnsupportedSecurity => "errors-wifi-unsupported-security",
DiagnosticCode::ConnectionFailed => "errors-wifi-connection-failed",
DiagnosticCode::BackendOther => "errors-backend-other",
DiagnosticCode::Protocol => "errors-radio-protocol",
}
}
pub fn write_json(self, output: &mut impl fmt::Write) -> fmt::Result {
write!(
output,
"{{\"schema\":\"{}\",\"code\":\"{}\",\"stage\":\"{}\",\"action\":\"{}\",\"backend_code\":",
self.schema(),
self.code.as_str(),
self.stage.as_str(),
self.action.as_str(),
)?;
match self.backend_code {
Some(code) => write!(output, "{code}"),
None => output.write_str("null"),
}?;
output.write_str(",\"profile_revision\":")?;
match self.profile_revision {
Some(revision) => write_json_string(output, revision)?,
None => output.write_str("null")?,
}
output.write_str(",\"trace\":[")?;
for index in 0..self.trace.len() {
if index != 0 {
output.write_str(",")?;
}
let entry = self.trace.get(index).expect("trace length is bounded");
write!(
output,
"{{\"kind\":\"{}\",\"value\":{}}}",
entry.kind().as_str(),
entry.value()
)?;
}
write!(
output,
"],\"trace_truncated\":{},\"docs\":\"{}\"}}",
self.trace.is_truncated(),
self.docs_anchor()
)
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} at {}; next action: {}",
self.code.as_str(),
self.stage.as_str(),
self.action.as_str(),
)?;
if let Some(code) = self.backend_code {
write!(formatter, "; backend code: 0x{code:08x}")?;
}
Ok(())
}
}
impl Error {
pub const fn diagnostic(self) -> Diagnostic {
match self {
Self::AlreadyInitialized => Diagnostic {
code: DiagnosticCode::AlreadyInitialized,
stage: DiagnosticStage::ControlPlane,
action: RecoveryAction::UseExistingController,
backend_code: None,
profile_revision: None,
trace: DiagnosticTrace::new(),
},
Self::Backend(error) => error.diagnostic(),
Self::Protocol => Diagnostic {
code: DiagnosticCode::Protocol,
stage: DiagnosticStage::ControlPlane,
action: RecoveryAction::RecreateController,
backend_code: None,
profile_revision: None,
trace: DiagnosticTrace::new(),
},
}
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.diagnostic().fmt(formatter)
}
}
impl BackendError {
pub const fn diagnostic(self) -> Diagnostic {
let (code, action) = match self.class() {
BackendErrorClass::Initialize => (
DiagnosticCode::BackendInitialize,
RecoveryAction::Reinitialize,
),
BackendErrorClass::Busy => (DiagnosticCode::BackendBusy, RecoveryAction::WaitAndRetry),
BackendErrorClass::OperationTimeout => (
DiagnosticCode::OperationTimeout,
RecoveryAction::RetryOperation,
),
BackendErrorClass::BackendTimeout => (
DiagnosticCode::BackendTimeout,
RecoveryAction::RetryOperation,
),
BackendErrorClass::Cancelled => (
DiagnosticCode::OperationCancelled,
RecoveryAction::RetryOperation,
),
BackendErrorClass::ResourceUnavailable => (
DiagnosticCode::ResourceUnavailable,
RecoveryAction::ProvideResources,
),
BackendErrorClass::UnsupportedSecurity => (
DiagnosticCode::UnsupportedSecurity,
RecoveryAction::SelectSupportedSecurity,
),
BackendErrorClass::Connect => (
DiagnosticCode::ConnectionFailed,
RecoveryAction::InspectNetworkAndRetry,
),
BackendErrorClass::Other => (
DiagnosticCode::BackendOther,
RecoveryAction::InspectBackendCode,
),
};
Diagnostic {
code,
stage: self.stage(),
action,
backend_code: Some(self.code()),
profile_revision: self.profile_revision(),
trace: self.trace(),
}
}
}
fn write_json_string(output: &mut impl fmt::Write, value: &str) -> fmt::Result {
output.write_str("\"")?;
for character in value.chars() {
match character {
'\"' => output.write_str("\\\"")?,
'\\' => output.write_str("\\\\")?,
'\n' => output.write_str("\\n")?,
'\r' => output.write_str("\\r")?,
'\t' => output.write_str("\\t")?,
control if control.is_control() => write!(output, "\\u{:04x}", control as u32)?,
character => output.write_char(character)?,
}
}
output.write_str("\"")
}
impl fmt::Display for BackendError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.diagnostic().fmt(formatter)
}
}
#[cfg(test)]
mod tests {
extern crate std;
use std::string::String;
use super::*;
#[test]
fn unknown_backend_codes_remain_lossless_and_actionable() {
let error = BackendError::new(BackendErrorClass::Other, 0xdeaf_0042);
let diagnostic = error.diagnostic();
assert_eq!(diagnostic.code(), DiagnosticCode::BackendOther);
assert_eq!(diagnostic.stage(), DiagnosticStage::Backend);
assert_eq!(diagnostic.action(), RecoveryAction::InspectBackendCode);
assert_eq!(diagnostic.backend_code(), Some(0xdeaf_0042));
}
#[test]
fn numeric_sources_remain_distinct_and_lossless() {
let diagnostic = BackendError::new(BackendErrorClass::Connect, 0x5732_1234)
.with_stage(DiagnosticStage::Associate)
.with_trace(DiagnosticTraceKind::VendorStatus, 8_030)
.with_trace(DiagnosticTraceKind::IeeeStatus, 30)
.with_trace(DiagnosticTraceKind::HostapStatus, (-17_i32) as u32)
.diagnostic();
let mut json = String::new();
diagnostic.write_json(&mut json).unwrap();
assert!(json.contains("\"kind\":\"vendor_status\",\"value\":8030"));
assert!(json.contains("\"kind\":\"ieee_status\",\"value\":30"));
assert!(json.contains("\"kind\":\"hostap_status\",\"value\":4294967279"));
}
#[test]
fn json_is_deterministic_and_contains_no_configuration_text() {
let mut json = String::new();
Error::Backend(BackendError::new(BackendErrorClass::BackendTimeout, 7))
.diagnostic()
.write_json(&mut json)
.unwrap();
assert_eq!(
json,
"{\"schema\":\"hisi-rf-error/v3\",\"code\":\"backend.timeout\",\"stage\":\"backend\",\"action\":\"retry_operation\",\"backend_code\":7,\"profile_revision\":null,\"trace\":[],\"trace_truncated\":false,\"docs\":\"errors-backend-timeout\"}"
);
assert!(!json.contains("ssid"));
assert!(!json.contains("passphrase"));
assert!(!json.contains("secret"));
}
#[test]
fn local_errors_do_not_invent_backend_codes() {
let diagnostic = Error::AlreadyInitialized.diagnostic();
assert_eq!(diagnostic.backend_code(), None);
assert_eq!(diagnostic.action(), RecoveryAction::UseExistingController);
assert_eq!(diagnostic.docs_anchor(), "errors-radio-already-initialized");
}
#[test]
fn backend_context_is_bounded_escaped_and_secret_free() {
let diagnostic = BackendError::new(BackendErrorClass::Connect, 30)
.with_stage(DiagnosticStage::Pmf)
.with_profile_revision("ws63-\"profile")
.with_trace(DiagnosticTraceKind::IeeeStatus, 30)
.with_trace(DiagnosticTraceKind::SupplicantContext, 0x445)
.diagnostic();
let mut json = String::new();
diagnostic.write_json(&mut json).unwrap();
assert_eq!(diagnostic.stage(), DiagnosticStage::Pmf);
assert_eq!(diagnostic.profile_revision(), Some("ws63-\"profile"));
assert_eq!(diagnostic.trace().len(), 2);
assert!(json.contains("ws63-\\\"profile"));
assert!(json.contains("\"kind\":\"ieee_status\",\"value\":30"));
assert!(!json.contains("ssid"));
assert!(!json.contains("passphrase"));
}
#[test]
fn trace_reports_capacity_truncation() {
let diagnostic = BackendError::new(BackendErrorClass::Other, 9)
.with_trace(DiagnosticTraceKind::BackendStatus, 1)
.with_trace(DiagnosticTraceKind::BackendStatus, 2)
.with_trace(DiagnosticTraceKind::BackendStatus, 3)
.with_trace(DiagnosticTraceKind::BackendStatus, 4)
.with_trace(DiagnosticTraceKind::BackendStatus, 5)
.diagnostic();
assert_eq!(diagnostic.trace().len(), DIAGNOSTIC_TRACE_CAPACITY);
assert!(diagnostic.trace().is_truncated());
}
#[test]
fn public_diagnostic_fixture_matrix_preserves_stage_class_and_context() {
let fixtures = [
(
BackendError::new(BackendErrorClass::Connect, 30)
.with_stage(DiagnosticStage::Associate)
.with_trace(DiagnosticTraceKind::IeeeStatus, 30),
DiagnosticCode::ConnectionFailed,
DiagnosticStage::Associate,
RecoveryAction::InspectNetworkAndRetry,
),
(
BackendError::new(BackendErrorClass::OperationTimeout, 0x45)
.with_stage(DiagnosticStage::Eapol),
DiagnosticCode::OperationTimeout,
DiagnosticStage::Eapol,
RecoveryAction::RetryOperation,
),
(
BackendError::new(BackendErrorClass::BackendTimeout, 0x46),
DiagnosticCode::BackendTimeout,
DiagnosticStage::Backend,
RecoveryAction::RetryOperation,
),
(
BackendError::new(BackendErrorClass::Cancelled, 0)
.with_stage(DiagnosticStage::ControlPlane),
DiagnosticCode::OperationCancelled,
DiagnosticStage::ControlPlane,
RecoveryAction::RetryOperation,
),
(
BackendError::new(BackendErrorClass::ResourceUnavailable, 4)
.with_stage(DiagnosticStage::Runtime)
.with_trace(DiagnosticTraceKind::ResourceRequired, 7)
.with_trace(DiagnosticTraceKind::ResourceAvailable, 3),
DiagnosticCode::ResourceUnavailable,
DiagnosticStage::Runtime,
RecoveryAction::ProvideResources,
),
(
BackendError::new(BackendErrorClass::BackendTimeout, 7)
.with_stage(DiagnosticStage::Runtime)
.with_trace(DiagnosticTraceKind::RuntimeCode, 7),
DiagnosticCode::BackendTimeout,
DiagnosticStage::Runtime,
RecoveryAction::RetryOperation,
),
];
for (error, code, stage, action) in fixtures {
let diagnostic = error.diagnostic();
assert_eq!(diagnostic.code(), code);
assert_eq!(diagnostic.stage(), stage);
assert_eq!(diagnostic.action(), action);
assert_eq!(diagnostic.backend_code(), Some(error.code()));
}
let resource = fixtures[4].0.diagnostic().trace();
assert_eq!(
resource.get(0).map(DiagnosticTraceEntry::kind),
Some(DiagnosticTraceKind::ResourceRequired)
);
assert_eq!(resource.get(0).map(DiagnosticTraceEntry::value), Some(7));
assert_eq!(
resource.get(1).map(DiagnosticTraceEntry::kind),
Some(DiagnosticTraceKind::ResourceAvailable)
);
assert_eq!(resource.get(1).map(DiagnosticTraceEntry::value), Some(3));
}
}