use int_enum::IntEnum;
use crate::errors::OptionReplyError;
use crate::info::InformationRequest;
use crate::io::option_request::OptionRequestRaw;
#[repr(u32)]
#[derive(Debug, IntEnum)]
enum OptionRequestType {
ExportName = 1,
Abort = 2,
List = 3,
PeekExport = 4,
StartTLS = 5,
Info = 6,
Go = 7,
StructuredReply = 8,
ListMetaContext = 9,
SetMetaContext = 10,
ExtendedHeaders = 11,
}
#[derive(Debug)]
pub(crate) enum OptionRequest {
ExportName(String),
Abort,
List,
StartTLS,
Info(String, Vec<InformationRequest>),
Go(String, Vec<InformationRequest>),
StructuredReply,
ListMetaContext,
SetMetaContext(Vec<u8>),
ExtendedHeaders(Vec<u8>),
}
fn parse_info_payload(data: &[u8]) -> Result<(String, Vec<InformationRequest>), OptionReplyError> {
let Some((&name_length, rest)) = data.split_first() else {
return Ok((String::new(), vec![]));
};
let (name, rest) = if name_length == 0 {
(String::new(), rest)
} else {
let Some((name, rest)) = rest.split_at_checked(name_length as usize) else {
return Err(OptionReplyError::Invalid);
};
(
String::from_utf8(name.into()).map_err(|_| OptionReplyError::Invalid)?,
rest,
)
};
let Some((&count, rest)) = rest.split_first() else {
return Err(OptionReplyError::Invalid);
};
if count == 0 {
return Ok((name, vec![]));
}
let count = count as usize;
let (chunks, remainder) = rest.as_chunks::<2>();
if !remainder.is_empty() || chunks.len() != count {
return Err(OptionReplyError::Invalid);
}
let mut buff = Vec::with_capacity(count);
for &chunk in chunks {
match InformationRequest::try_from(u16::from_be_bytes(chunk)) {
Ok(info_request) => buff.push(info_request),
Err(_) => return Err(OptionReplyError::Unsupported),
}
}
Ok((name, buff))
}
impl TryFrom<&OptionRequestRaw> for OptionRequest {
type Error = OptionReplyError;
fn try_from(raw: &OptionRequestRaw) -> Result<Self, Self::Error> {
if raw.magic != crate::magic::NBD_IHAVEOPT {
return Err(OptionReplyError::Invalid);
}
let option_type = match OptionRequestType::try_from(raw.option) {
Ok(option_type) => option_type,
Err(_) => {
return Err(OptionReplyError::Unsupported);
}
};
let option = match option_type {
OptionRequestType::PeekExport => return Err(OptionReplyError::Unsupported),
OptionRequestType::ExportName => {
let name =
String::from_utf8(raw.data.clone()).map_err(|_| OptionReplyError::Invalid)?;
OptionRequest::ExportName(name)
}
OptionRequestType::Abort => OptionRequest::Abort,
OptionRequestType::List => OptionRequest::List,
OptionRequestType::StartTLS => OptionRequest::StartTLS,
OptionRequestType::StructuredReply => OptionRequest::StructuredReply,
OptionRequestType::ListMetaContext => OptionRequest::ListMetaContext,
OptionRequestType::Info => {
let (name, info_requests) = parse_info_payload(&raw.data)?;
OptionRequest::Info(name, info_requests)
}
OptionRequestType::Go => {
let (name, info_requests) = parse_info_payload(&raw.data)?;
OptionRequest::Go(name, info_requests)
}
OptionRequestType::SetMetaContext => OptionRequest::SetMetaContext(raw.data.clone()),
OptionRequestType::ExtendedHeaders => OptionRequest::ExtendedHeaders(raw.data.clone()),
};
Ok(option)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::magic;
#[test]
fn test_option_request_type_conversion() {
assert_eq!(OptionRequestType::ExportName as u32, 1);
assert_eq!(OptionRequestType::Abort as u32, 2);
assert_eq!(OptionRequestType::List as u32, 3);
assert_eq!(OptionRequestType::PeekExport as u32, 4);
assert_eq!(OptionRequestType::StartTLS as u32, 5);
assert_eq!(OptionRequestType::Info as u32, 6);
assert_eq!(OptionRequestType::Go as u32, 7);
assert_eq!(OptionRequestType::StructuredReply as u32, 8);
assert_eq!(OptionRequestType::ListMetaContext as u32, 9);
assert_eq!(OptionRequestType::SetMetaContext as u32, 10);
assert_eq!(OptionRequestType::ExtendedHeaders as u32, 11);
match OptionRequestType::try_from(1u32) {
Ok(request_type) => assert!(matches!(request_type, OptionRequestType::ExportName)),
Err(_) => panic!("Failed to convert 1 to OptionRequestType::ExportName"),
}
match OptionRequestType::try_from(7u32) {
Ok(request_type) => assert!(matches!(request_type, OptionRequestType::Go)),
Err(_) => panic!("Failed to convert 7 to OptionRequestType::Go"),
}
assert!(OptionRequestType::try_from(99u32).is_err());
}
#[test]
fn test_parse_info_payload_empty() {
let result = parse_info_payload(&[]);
assert!(result.is_ok());
let (name, requests) = result.unwrap();
assert_eq!(name, "");
assert!(requests.is_empty());
}
#[test]
fn test_parse_info_payload_with_name() {
let mut data = vec![5]; data.extend_from_slice(b"hello"); data.push(0);
let result = parse_info_payload(&data);
assert!(result.is_ok());
let (name, requests) = result.unwrap();
assert_eq!(name, "hello");
assert!(requests.is_empty());
}
#[test]
fn test_try_from_raw_invalid_magic() {
let raw = OptionRequestRaw {
magic: 0x123456789, option: 1, data: b"export1".to_vec(),
};
let result = OptionRequest::try_from(&raw);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), OptionReplyError::Invalid));
}
#[test]
fn test_try_from_raw_export_name() {
let raw = OptionRequestRaw {
magic: magic::NBD_IHAVEOPT,
option: 1, data: b"export1".to_vec(),
};
let result = OptionRequest::try_from(&raw);
assert!(result.is_ok());
match result.unwrap() {
OptionRequest::ExportName(name) => assert_eq!(name, "export1"),
_ => panic!("Wrong variant returned for ExportName"),
}
}
#[test]
fn test_try_from_raw_unsupported() {
let raw = OptionRequestRaw {
magic: magic::NBD_IHAVEOPT,
option: 99, data: vec![],
};
let result = OptionRequest::try_from(&raw);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), OptionReplyError::Unsupported));
}
}