#![forbid(unsafe_code)]
use crate::attestation::AttestationError;
use crate::noc::NocError;
use crate::state_machine::action::Expectation;
use crate::state_machine::stage::Stage;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CommissioningError {
#[error("invalid commissioner config: {0}")]
InvalidConfig(&'static str),
#[error("unexpected response kind: expected {expected:?}, got {got:?}")]
UnexpectedResponseKind {
expected: Expectation,
got: Expectation,
},
#[error("response delivered out of order at stage {0:?}")]
OutOfOrderResponse(Stage),
#[error("device rejected stage {stage:?}: IM status {im_status:#x}")]
DeviceImStatus {
stage: Stage,
im_status: u16,
},
#[error("malformed response at stage {0:?}")]
MalformedResponse(Stage),
#[error("attestation verification failed: {0}")]
Attestation(#[from] AttestationError),
#[error("NOC issuance failed: {0}")]
Noc(#[from] NocError),
#[error("CASE session establishment failed")]
CaseEstablishmentFailed,
#[error("device does not support {needed:?} network type (credential/device mismatch)")]
NetworkFeatureUnsupported {
needed: NetworkKind,
},
#[error(
"network commissioning rejected at stage {stage:?}: \
networking_status {networking_status:#x}, \
debug_text={}, hint={remediation_hint:?}",
display_debug_text(debug_text.as_ref())
)]
NetworkRejected {
stage: Stage,
networking_status: u8,
debug_text: Option<String>,
remediation_hint: RemediationHint,
},
}
fn display_debug_text(text: Option<&String>) -> String {
match text {
None => "None".to_owned(),
Some(s) => {
let capped: String = s.chars().take(64).collect();
if capped.len() < s.len() {
format!("Some({capped:?}…)")
} else {
format!("Some({s:?})")
}
}
}
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum NetworkKind {
WiFi,
Thread,
Ethernet,
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum RemediationHint {
CheckPassphrase,
CheckSsid,
CheckRegulatoryRegion,
UpgradeSecurityMode,
DeviceNetworkSlotsFull,
DeviceIpStackFailure,
None,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state_machine::action::Expectation;
use crate::state_machine::stage::Stage;
#[test]
fn invalid_config_carries_message() {
let e = CommissioningError::InvalidConfig("missing IPK epoch key");
let msg = e.to_string();
assert!(msg.contains("missing IPK"), "{msg}");
}
#[test]
fn unexpected_response_kind_shows_both_sides() {
let e = CommissioningError::UnexpectedResponseKind {
expected: Expectation::ArmFailsafeResponse,
got: Expectation::AttestationResponse,
};
let msg = e.to_string();
assert!(msg.contains("ArmFailsafeResponse"), "{msg}");
assert!(msg.contains("AttestationResponse"), "{msg}");
}
#[test]
fn out_of_order_response_names_the_stage() {
let e = CommissioningError::OutOfOrderResponse(Stage::ArmFailsafe);
let msg = e.to_string();
assert!(msg.contains("ArmFailsafe"), "{msg}");
}
#[test]
fn device_im_status_includes_stage_and_status_code() {
let e = CommissioningError::DeviceImStatus {
stage: Stage::ArmFailsafe,
im_status: 0x0098,
};
let msg = e.to_string();
assert!(msg.contains("ArmFailsafe"), "{msg}");
assert!(msg.contains("0x98"), "{msg}");
}
#[test]
fn remediation_hint_is_copy_eq_hash() {
fn assert_copy<T: Copy + Eq + std::hash::Hash>() {}
assert_copy::<RemediationHint>();
assert_eq!(RemediationHint::None, RemediationHint::None);
assert_ne!(RemediationHint::None, RemediationHint::CheckPassphrase);
}
#[test]
fn network_rejected_display_caps_debug_text() {
let e = CommissioningError::NetworkRejected {
stage: Stage::NetworkSetup,
networking_status: 5,
debug_text: Some("s".repeat(300)),
remediation_hint: RemediationHint::CheckSsid,
};
let msg = e.to_string();
assert!(msg.contains(&format!("{:?}", "s".repeat(64))));
assert!(!msg.contains(&"s".repeat(65)));
assert!(msg.contains('…'));
let short = CommissioningError::NetworkRejected {
stage: Stage::NetworkSetup,
networking_status: 5,
debug_text: Some("bad ssid".into()),
remediation_hint: RemediationHint::CheckSsid,
};
assert!(short.to_string().contains("\"bad ssid\""));
assert!(!short.to_string().contains('…'));
}
#[test]
fn network_rejected_display_escapes_control_chars() {
let e = CommissioningError::NetworkRejected {
stage: Stage::NetworkSetup,
networking_status: 5,
debug_text: Some("evil\nFAKE LOG LINE\u{1b}[31mred".to_owned()),
remediation_hint: RemediationHint::CheckSsid,
};
let msg = e.to_string();
assert!(!msg.contains('\n'), "raw newline leaked into: {msg}");
assert!(!msg.contains('\u{1b}'), "raw ESC leaked into: {msg}");
assert!(msg.contains("\\n"), "newline must render escaped: {msg}");
assert!(msg.contains("\\u{1b}"), "ESC must render escaped: {msg}");
}
#[test]
fn network_feature_unsupported_wording_is_pinned() {
let e = CommissioningError::NetworkFeatureUnsupported {
needed: NetworkKind::Thread,
};
assert!(
e.to_string()
.contains("does not support Thread network type"),
"pinned substring changed: {e}"
);
}
}