use super::constants::{
DHCP_ACK, DHCP_ACTIVE_LEASE_QUERY, DHCP_BULK_LEASE_QUERY, DHCP_DECLINE, DHCP_DISCOVER,
DHCP_FORCE_RENEW, DHCP_INFORM, DHCP_LEASE_ACTIVE, DHCP_LEASE_QUERY, DHCP_LEASE_QUERY_DONE,
DHCP_LEASE_QUERY_STATUS, DHCP_LEASE_UNASSIGNED, DHCP_LEASE_UNKNOWN, DHCP_NAK, DHCP_OFFER,
DHCP_RELEASE, DHCP_REQUEST, DHCP_TLS,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DhcpMessageType {
Discover,
Offer,
Request,
Decline,
Ack,
Nak,
Release,
Inform,
ForceRenew,
LeaseQuery,
LeaseUnassigned,
LeaseUnknown,
LeaseActive,
BulkLeaseQuery,
LeaseQueryDone,
ActiveLeaseQuery,
LeaseQueryStatus,
DhcpTls,
Unknown(u8),
}
impl DhcpMessageType {
pub const fn from_code(code: u8) -> Self {
match code {
DHCP_DISCOVER => Self::Discover,
DHCP_OFFER => Self::Offer,
DHCP_REQUEST => Self::Request,
DHCP_DECLINE => Self::Decline,
DHCP_ACK => Self::Ack,
DHCP_NAK => Self::Nak,
DHCP_RELEASE => Self::Release,
DHCP_INFORM => Self::Inform,
DHCP_FORCE_RENEW => Self::ForceRenew,
DHCP_LEASE_QUERY => Self::LeaseQuery,
DHCP_LEASE_UNASSIGNED => Self::LeaseUnassigned,
DHCP_LEASE_UNKNOWN => Self::LeaseUnknown,
DHCP_LEASE_ACTIVE => Self::LeaseActive,
DHCP_BULK_LEASE_QUERY => Self::BulkLeaseQuery,
DHCP_LEASE_QUERY_DONE => Self::LeaseQueryDone,
DHCP_ACTIVE_LEASE_QUERY => Self::ActiveLeaseQuery,
DHCP_LEASE_QUERY_STATUS => Self::LeaseQueryStatus,
DHCP_TLS => Self::DhcpTls,
value => Self::Unknown(value),
}
}
pub const fn code(self) -> u8 {
match self {
Self::Discover => DHCP_DISCOVER,
Self::Offer => DHCP_OFFER,
Self::Request => DHCP_REQUEST,
Self::Decline => DHCP_DECLINE,
Self::Ack => DHCP_ACK,
Self::Nak => DHCP_NAK,
Self::Release => DHCP_RELEASE,
Self::Inform => DHCP_INFORM,
Self::ForceRenew => DHCP_FORCE_RENEW,
Self::LeaseQuery => DHCP_LEASE_QUERY,
Self::LeaseUnassigned => DHCP_LEASE_UNASSIGNED,
Self::LeaseUnknown => DHCP_LEASE_UNKNOWN,
Self::LeaseActive => DHCP_LEASE_ACTIVE,
Self::BulkLeaseQuery => DHCP_BULK_LEASE_QUERY,
Self::LeaseQueryDone => DHCP_LEASE_QUERY_DONE,
Self::ActiveLeaseQuery => DHCP_ACTIVE_LEASE_QUERY,
Self::LeaseQueryStatus => DHCP_LEASE_QUERY_STATUS,
Self::DhcpTls => DHCP_TLS,
Self::Unknown(value) => value,
}
}
}
#[cfg(test)]
pub(crate) const REGISTERED_MESSAGE_TYPES: [DhcpMessageType; 18] = [
DhcpMessageType::Discover,
DhcpMessageType::Offer,
DhcpMessageType::Request,
DhcpMessageType::Decline,
DhcpMessageType::Ack,
DhcpMessageType::Nak,
DhcpMessageType::Release,
DhcpMessageType::Inform,
DhcpMessageType::ForceRenew,
DhcpMessageType::LeaseQuery,
DhcpMessageType::LeaseUnassigned,
DhcpMessageType::LeaseUnknown,
DhcpMessageType::LeaseActive,
DhcpMessageType::BulkLeaseQuery,
DhcpMessageType::LeaseQueryDone,
DhcpMessageType::ActiveLeaseQuery,
DhcpMessageType::LeaseQueryStatus,
DhcpMessageType::DhcpTls,
];
impl From<DhcpMessageType> for u8 {
fn from(value: DhcpMessageType) -> Self {
value.code()
}
}
pub(crate) fn message_type_summary(message_type: DhcpMessageType) -> String {
match message_type {
DhcpMessageType::Discover => "discover".to_string(),
DhcpMessageType::Offer => "offer".to_string(),
DhcpMessageType::Request => "request".to_string(),
DhcpMessageType::Decline => "decline".to_string(),
DhcpMessageType::Ack => "ack".to_string(),
DhcpMessageType::Nak => "nak".to_string(),
DhcpMessageType::Release => "release".to_string(),
DhcpMessageType::Inform => "inform".to_string(),
DhcpMessageType::ForceRenew => "forcerenew".to_string(),
DhcpMessageType::LeaseQuery => "leasequery".to_string(),
DhcpMessageType::LeaseUnassigned => "leaseunassigned".to_string(),
DhcpMessageType::LeaseUnknown => "leaseunknown".to_string(),
DhcpMessageType::LeaseActive => "leaseactive".to_string(),
DhcpMessageType::BulkLeaseQuery => "bulkleasequery".to_string(),
DhcpMessageType::LeaseQueryDone => "leasequerydone".to_string(),
DhcpMessageType::ActiveLeaseQuery => "activeleasequery".to_string(),
DhcpMessageType::LeaseQueryStatus => "leasequerystatus".to_string(),
DhcpMessageType::DhcpTls => "dhcptls".to_string(),
DhcpMessageType::Unknown(value) => format!("unknown({value})"),
}
}
#[cfg(test)]
mod message_type_tests {
use super::{DhcpMessageType, REGISTERED_MESSAGE_TYPES};
const REGISTERED_CODES: [u8; 18] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
];
#[test]
fn dhcp_message_type_registry_is_complete() {
assert_eq!(
REGISTERED_MESSAGE_TYPES.len(),
REGISTERED_CODES.len(),
"registered message type table must cover every IANA codepoint",
);
for (message_type, expected_code) in
REGISTERED_MESSAGE_TYPES.into_iter().zip(REGISTERED_CODES)
{
assert_eq!(
message_type.code(),
expected_code,
"{message_type:?} must encode to its IANA codepoint",
);
assert_eq!(
DhcpMessageType::from_code(expected_code),
message_type,
"code {expected_code} must decode to {message_type:?}",
);
assert_eq!(u8::from(message_type), expected_code);
assert!(
!matches!(message_type, DhcpMessageType::Unknown(_)),
"{message_type:?} is a registered type and must not be Unknown",
);
}
assert_eq!(
REGISTERED_CODES,
core::array::from_fn::<u8, 18, _>(|i| (i + 1) as u8)
);
}
#[test]
fn dhcp_message_type_unknown_values_roundtrip() {
let registered: std::collections::HashSet<u8> =
REGISTERED_MESSAGE_TYPES.iter().map(|m| m.code()).collect();
for code in 0u8..=255 {
let message_type = DhcpMessageType::from_code(code);
if registered.contains(&code) {
assert!(!matches!(message_type, DhcpMessageType::Unknown(_)));
continue;
}
assert_eq!(
message_type,
DhcpMessageType::Unknown(code),
"unregistered code {code} must be preserved as Unknown",
);
assert_eq!(message_type.code(), code);
assert_eq!(u8::from(message_type), code);
}
}
#[test]
fn dhcp_leasequery_message_types_roundtrip() {
use super::super::Dhcp;
let leasequery_family = [
(DhcpMessageType::LeaseQuery, 10u8),
(DhcpMessageType::LeaseUnassigned, 11),
(DhcpMessageType::LeaseUnknown, 12),
(DhcpMessageType::LeaseActive, 13),
(DhcpMessageType::BulkLeaseQuery, 14),
(DhcpMessageType::LeaseQueryDone, 15),
(DhcpMessageType::ActiveLeaseQuery, 16),
(DhcpMessageType::LeaseQueryStatus, 17),
(DhcpMessageType::DhcpTls, 18),
];
for (message_type, code) in leasequery_family {
assert_eq!(message_type.code(), code, "{message_type:?} codepoint");
assert_eq!(DhcpMessageType::from_code(code), message_type);
assert!(!matches!(message_type, DhcpMessageType::Unknown(_)));
let dhcp = Dhcp::new()
.op(super::super::BOOTP_REPLY)
.message_type(message_type);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcp::decode(&bytes).unwrap();
assert_eq!(
parsed.message_type_value(),
Some(message_type),
"code {code} must decode from option 53 in a full packet",
);
let recompiled = crate::Packet::from_layer(parsed)
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(recompiled, bytes, "code {code} must re-compile identically");
}
}
#[test]
fn dhcp_message_type_matrix_full_packet_roundtrip() {
use super::super::Dhcp;
for message_type in REGISTERED_MESSAGE_TYPES {
let dhcp = Dhcp::new()
.op(super::super::BOOTP_REPLY)
.message_type(message_type);
let bytes = crate::Packet::from_layer(dhcp)
.compile()
.unwrap()
.as_bytes()
.to_vec();
let parsed = Dhcp::decode(&bytes).unwrap();
assert_eq!(
parsed.message_type_value(),
Some(message_type),
"{message_type:?} must survive option-53 decode in a full packet",
);
assert!(
!matches!(
parsed.message_type_value(),
Some(DhcpMessageType::Unknown(_))
),
"{message_type:?} must decode to a registered type, not Unknown",
);
let recompiled = crate::Packet::from_layer(parsed)
.compile()
.unwrap()
.as_bytes()
.to_vec();
assert_eq!(
recompiled, bytes,
"{message_type:?} must re-encode to identical bytes",
);
}
}
}