use crate::{Device, Error, Transport};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
pub struct Parameters<'a, T: Transport>(&'a Device<T>, String);
impl<'a, T: Transport> Parameters<'a, T> {
pub(crate) fn new(device: &'a Device<T>, api_version: String) -> Self {
Self(device, api_version)
}
pub async fn list_definitions(
&self,
groups: Option<&[&str]>,
) -> Result<ParameterDefinitions, Error<T::Error>> {
let req = http::Request::builder()
.method(http::Method::GET)
.uri(
self.0
.uri_for_args(
"/axis-cgi/param.cgi",
ListParams {
action: "listdefinitions",
list_format: Some("xmlschema"),
groups,
},
)
.unwrap(),
)
.body(Vec::new())
.unwrap();
let (_resp, resp_body) = self.0.roundtrip(req, "text/xml").await?;
let resp_body =
std::str::from_utf8(resp_body.as_slice()).map_err(|_| Error::Other("invalid UTF-8"))?;
let params: ParameterDefinitions = quick_xml::de::from_str(resp_body)?;
Ok(params)
}
pub async fn list(
&self,
groups: Option<&[&str]>,
) -> Result<BTreeMap<String, String>, Error<T::Error>> {
let req = http::request::Builder::new()
.method(http::Method::GET)
.uri(
self.0
.uri_for_args(
"/axis-cgi/param.cgi",
ListParams {
action: "list",
list_format: None,
groups,
},
)
.unwrap(),
)
.body(Vec::new())
.unwrap();
let (_, body) = self.0.roundtrip(req, "text/plain").await?;
Ok(body
.as_slice()
.split(|byte| *byte == b'\n')
.filter_map(|line| {
let line = std::str::from_utf8(line).unwrap_or("");
let mut parts = line.splitn(2, '=');
match (parts.next(), parts.next()) {
(Some(key), Some(value)) => Some((key.to_string(), value.to_string())),
_ => None,
}
})
.collect())
}
pub async fn update<I: IntoIterator<Item = (K, V)>, K: AsRef<str>, V: AsRef<str>>(
&self,
parameters: I,
) -> Result<(), Error<T::Error>> {
let mut query_params: BTreeMap<String, String> = parameters
.into_iter()
.map(move |(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
.collect();
query_params.insert("action".into(), "update".into());
assert!(!query_params.is_empty());
let req = http::request::Builder::new()
.method(http::Method::GET)
.uri(
self.0
.uri_for_args("/axis-cgi/param.cgi", query_params)
.unwrap(),
)
.body(Vec::new())
.unwrap();
let (_, body) = self.0.roundtrip(req, "text/plain").await?;
if body.as_slice() == b"OK" {
Ok(())
} else if body.as_slice().starts_with(b"# ") {
Err(Error::Other("call failed for specific reason"))
} else {
Err(Error::Other("call failed for unknown reason"))
}
}
}
#[derive(Serialize)]
struct ListParams<'a> {
action: &'a str,
#[serde(skip_serializing_if = "Option::is_none", rename = "listformat")]
list_format: Option<&'a str>,
#[serde(
rename = "group",
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_list_params_groups"
)]
groups: Option<&'a [&'a str]>,
}
fn serialize_list_params_groups<S>(groups: &Option<&[&str]>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match groups {
Some(groups) => {
let groups = groups.join(",");
ser.serialize_str(&groups)
}
None => unreachable!(),
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterDefinitions {
#[serde(rename = "version")]
pub schema_version: String,
pub model: Option<String>,
pub firmware_version: Option<String>,
#[serde(rename = "group")]
pub groups: Vec<ParameterGroupDefinition>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterGroupDefinition {
pub name: String,
pub max_groups: Option<u32>,
#[serde(rename = "group", default)]
pub groups: Vec<ParameterGroupDefinition>,
#[serde(rename = "parameter", default)]
pub parameters: Vec<ParameterDefinition>,
}
impl ParameterGroupDefinition {
pub fn group(&self, name: &str) -> Option<&ParameterGroupDefinition> {
self.groups.iter().find(|g| g.name == name)
}
pub fn parameter(&self, name: &str) -> Option<&ParameterDefinition> {
self.parameters.iter().find(|g| g.name == name)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterDefinition {
pub name: String,
#[serde(rename = "value")]
pub current_value: Option<String>,
pub security_level: Option<u32>,
pub nice_name: Option<String>,
#[serde(rename = "type")]
pub parameter_type: Option<ParameterTypeDefinition>,
}
impl ParameterDefinition {
pub fn as_bool(&self) -> Option<bool> {
match (self.current_value.as_ref(), self.parameter_type.as_ref()) {
(
Some(value),
Some(ParameterTypeDefinition {
type_definition: TypeDefinition::Bool(td),
..
}),
) => {
if value == &td.true_value {
Some(true)
} else if value == &td.false_value {
Some(false)
} else {
None
}
}
_ => None,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct SecurityLevel {
pub create: AccessLevel,
pub delete: AccessLevel,
pub read: AccessLevel,
pub write: AccessLevel,
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum BadSecurityLevelError {
BadAccessLevel(BadAccessLevelError),
WrongLength(String),
}
impl fmt::Display for BadSecurityLevelError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BadSecurityLevelError::BadAccessLevel(BadAccessLevelError(c)) => {
write!(f, "expected access level digit, got '{}'", c)
}
BadSecurityLevelError::WrongLength(str) => {
write!(f, "expected 4 digits, got {:?}", str)
}
}
}
}
impl From<BadAccessLevelError> for BadSecurityLevelError {
fn from(l: BadAccessLevelError) -> Self {
BadSecurityLevelError::BadAccessLevel(l)
}
}
impl FromStr for SecurityLevel {
type Err = BadSecurityLevelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
fn next(
s: &str,
chars: &mut std::str::Chars,
) -> Result<AccessLevel, BadSecurityLevelError> {
chars
.next()
.ok_or_else(|| BadSecurityLevelError::WrongLength(s.into()))
.and_then(|c| AccessLevel::try_from(c).map_err(|e| e.into()))
}
let security_level = Self {
create: next(s, &mut chars)?,
delete: next(s, &mut chars)?,
read: next(s, &mut chars)?,
write: next(s, &mut chars)?,
};
if chars.next().is_none() {
Ok(security_level)
} else {
Err(BadSecurityLevelError::WrongLength(s.into()))
}
}
}
impl fmt::Display for SecurityLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use std::fmt::Write;
f.write_char(self.create.into())?;
f.write_char(self.delete.into())?;
f.write_char(self.read.into())?;
f.write_char(self.write.into())
}
}
impl<'de> serde::de::Deserialize<'de> for SecurityLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum AccessLevel {
Unprotected,
ViewerAccess,
OperatorAccess,
AdministratorAccess,
RootAccess,
}
impl From<AccessLevel> for char {
fn from(al: AccessLevel) -> Self {
match al {
AccessLevel::Unprotected => '0',
AccessLevel::ViewerAccess => '1',
AccessLevel::OperatorAccess => '4',
AccessLevel::AdministratorAccess => '6',
AccessLevel::RootAccess => '7',
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct BadAccessLevelError(char);
impl TryFrom<char> for AccessLevel {
type Error = BadAccessLevelError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'0' => Ok(AccessLevel::Unprotected),
'1' => Ok(AccessLevel::ViewerAccess),
'4' => Ok(AccessLevel::OperatorAccess),
'6' => Ok(AccessLevel::AdministratorAccess),
'7' => Ok(AccessLevel::RootAccess),
other => Err(BadAccessLevelError(other)),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterTypeDefinition {
#[serde(rename = "readonly")]
pub read_only: Option<bool>,
#[serde(rename = "writeonly")]
pub write_only: Option<bool>,
pub hidden: Option<bool>,
#[serde(rename = "const")]
pub constant: Option<bool>,
#[serde(rename = "nosync")]
pub no_sync: Option<bool>,
pub internal: Option<bool>,
#[serde(rename = "$value")]
pub type_definition: TypeDefinition,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TypeDefinition {
String(StringParameterDefinition),
Password(PasswordParameterDefinition),
Int(IntParameterDefinition),
Enum(EnumParameterDefinition),
Bool(BoolParameterDefinition),
Ip,
IpList,
Hostname,
TextArea,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StringParameterDefinition {
#[serde(rename = "maxlen")]
pub max_len: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasswordParameterDefinition {
#[serde(rename = "maxlen")]
pub max_len: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IntParameterDefinition {
pub min: Option<i64>,
pub max: Option<i64>,
#[serde(rename = "maxlen")]
pub max_len: Option<u8>,
pub range_entries: Option<Vec<IntParameterRangeDefinition>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IntParameterRangeDefinition {
pub value: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnumParameterDefinition {
#[serde(rename = "entry")]
pub values: Vec<EnumEntryDefinition>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnumEntryDefinition {
pub value: String,
pub nice_value: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BoolParameterDefinition {
#[serde(rename = "true")]
pub true_value: String,
#[serde(rename = "false")]
pub false_value: String,
}
#[cfg(test)]
mod tests {
#[test]
fn list() {
crate::test_with_devices(|test_device| async move {
let parameters = test_device.device.parameters();
let all_params = parameters.list(None).await?;
let brand_params = parameters.list(Some(&["root.Brand"])).await?;
let brand_and_firmware_params = parameters
.list(Some(&["root.Brand", "root.Properties.Firmware"]))
.await?;
assert!(all_params.len() > 0);
assert!(
all_params.len() > brand_params.len(),
"all_params.len() = {} is not greater than brand_params.len() = {}",
all_params.len(),
brand_params.len()
);
assert!(
all_params.len() > brand_and_firmware_params.len(),
"all_params.len() = {} is not greater than brand_and_firmware_params.len() = {}",
all_params.len(),
brand_and_firmware_params.len()
);
assert!(
brand_and_firmware_params.len() > brand_params.len(),
"brand_and_firmware_params.len() = {} is not greater than brand_params.len() = {}",
brand_and_firmware_params.len(),
brand_params.len()
);
Ok(())
});
}
#[test]
fn list_definitions() {
crate::test_with_devices(|test_device| async move {
let parameters = test_device.device.parameters();
let all_params = parameters.list_definitions(None).await?;
let brand_params = parameters.list_definitions(Some(&["root.Brand"])).await?;
let brand_and_firmware_params = parameters
.list_definitions(Some(&["root.Brand", "root.Properties.Firmware"]))
.await?;
assert_eq!(all_params.groups.len(), 1);
assert_eq!(brand_params.groups.len(), 1);
assert_eq!(brand_and_firmware_params.groups.len(), 1);
assert_eq!(all_params.model, brand_params.model);
assert_eq!(all_params.model, brand_and_firmware_params.model);
assert_eq!(all_params.firmware_version, brand_params.firmware_version);
assert_eq!(
all_params.firmware_version,
brand_and_firmware_params.firmware_version
);
assert!(all_params.groups[0].groups.len() > 2);
assert_eq!(brand_params.groups[0].groups.len(), 1);
assert_eq!(brand_and_firmware_params.groups[0].groups.len(), 2);
Ok(())
});
}
#[tokio::test]
async fn update() {
let device = crate::mock_device(|req| {
assert_eq!(req.method(), http::Method::GET);
assert_eq!(
req.uri().path_and_query().map(|pq| pq.as_str()),
Some("/axis-cgi/param.cgi?action=update&foo.bar=baz+quxx")
);
http::Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "text/plain")
.body(vec![b"OK".to_vec()])
});
let response = device
.parameters()
.update(vec![("foo.bar", "baz quxx")])
.await;
match response {
Ok(()) => {}
Err(e) => panic!("update should succeed: {}", e),
};
let device = crate::mock_device(|req| {
assert_eq!(req.method(), http::Method::GET);
assert_eq!(
req.uri().path_and_query().map(|pq| pq.as_str()),
Some("/axis-cgi/param.cgi?action=update&foo.bar=baz+quxx")
);
http::Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "text/plain")
.body(vec![
b"# Error: Error setting 'foo.bar' to 'baz quxx'!".to_vec()
])
});
let response = device
.parameters()
.update(vec![("foo.bar", "baz quxx")])
.await;
match response {
Err(crate::Error::Other(_)) => {}
Ok(()) => panic!("update should fail"),
Err(e) => panic!("update should fail with a different error: {}", e),
};
}
}