use std::borrow::Cow;
use crate::proto::{
self,
app::{AppStateResponse, GetErrorResponse, LockStatusResponse},
desktop::Status,
gpio::{GetOtgModeResponse, GetPinModeResponse, ReadPinResponse},
gui::ScreenFrame,
main::Content,
property::GetResponse,
storage::{InfoResponse, TimestampResponse, file::FileType},
system::{
DateTime, DeviceInfoResponse, PowerInfoResponse, ProtobufVersionResponse, UpdateResponse,
},
};
macro_rules! define_into_impl {
($enum_name:ident $variant:ident $typ:ty) => {
impl std::convert::TryFrom<$enum_name> for $typ {
type Error = crate::error::Error;
fn try_from(value: $enum_name) -> Result<$typ, Self::Error> {
match value {
$enum_name::$variant(x) => Ok(x),
other => Err(crate::error::Error::UnexpectedResponse {
expected: stringify!($variant),
actual: other.kind(),
}),
}
}
}
};
($enum_name:ident $variant:ident) => {};
}
macro_rules! define_into_enum {
(
$(#[$enum_meta:meta])*
$vis:vis enum $enum_name:ident {
$(
$(#[$variant_meta:meta])*
$variant:ident $( ( $typ:ty ) )?
),* $(,)?
}
) => {
$(#[$enum_meta])*
$vis enum $enum_name {
$(
$(#[$variant_meta])*
#[doc = stringify!($enum_name::$variant)]
$variant $( ( $typ ) )?,
)*
}
$(
define_into_impl!($enum_name $variant $( $typ)?);
)*
};
}
define_into_enum! {
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum Response {
Empty,
Ping(Vec<u8>),
SystemDeviceInfo(DeviceInfoResponse),
SystemGetDatetime(Option<DateTime>),
SystemProtobufVersion(ProtobufVersionResponse),
SystemUpdate(UpdateResponse),
SystemPowerInfo(PowerInfoResponse),
StorageInfo(InfoResponse),
StorageTimestamp(TimestampResponse),
StorageStat(Option<u32>),
StorageList(Vec<ReadDirItem>),
StorageRead(Option<Cow<'static, [u8]>>),
StorageMd5sum(String),
AppLockStatus(LockStatusResponse),
AppGetError(GetErrorResponse),
GuiScreenFrame(ScreenFrame),
GpioGetPinMode(GetPinModeResponse),
GpioReadPin(ReadPinResponse),
GpioGetOtgMode(GetOtgModeResponse),
AppState(AppStateResponse),
PropertyGet(GetResponse),
DesktopStatus(Status),
}
}
#[derive(Debug, PartialEq)]
pub enum ReadDirItem {
Dir(String),
File(String, u32, Option<String>),
}
impl Response {
fn kind(&self) -> &'static str {
match self {
Self::Empty => "Empty",
Self::Ping(_) => "Ping",
Self::SystemDeviceInfo(_) => "SystemDeviceInfo",
Self::SystemGetDatetime(_) => "SystemGetDatetime",
Self::SystemProtobufVersion(_) => "SystemProtobufVersion",
Self::SystemUpdate(_) => "SystemUpdate",
Self::SystemPowerInfo(_) => "SystemPowerInfo",
Self::StorageInfo(_) => "StorageInfo",
Self::StorageTimestamp(_) => "StorageTimestamp",
Self::StorageStat(_) => "StorageStat",
Self::StorageList(_) => "StorageList",
Self::StorageRead(_) => "StorageRead",
Self::StorageMd5sum(_) => "StorageMd5sum",
Self::AppLockStatus(_) => "AppLockStatus",
Self::AppGetError(_) => "AppGetError",
Self::GuiScreenFrame(_) => "GuiScreenFrame",
Self::GpioGetPinMode(_) => "GpioGetPinMode",
Self::GpioReadPin(_) => "GpioReadPin",
Self::GpioGetOtgMode(_) => "GpioGetOtgMode",
Self::AppState(_) => "AppState",
Self::PropertyGet(_) => "PropertyGet",
Self::DesktopStatus(_) => "DesktopStatus",
}
}
}
fn decode_storage_file_type(raw: i32) -> Result<FileType, crate::error::Error> {
FileType::try_from(raw).map_err(|_| crate::error::Error::InvalidStorageFileType(raw))
}
impl TryFrom<proto::Main> for Response {
type Error = crate::error::Error;
fn try_from(value: proto::Main) -> Result<Self, Self::Error> {
use Response::*;
let content = value.content;
match content {
None | Some(Content::Empty(_)) => Ok(Empty),
Some(x) => match x {
Content::SystemPingResponse(r) => Ok(Ping(r.data)),
Content::SystemDeviceInfoResponse(r) => Ok(SystemDeviceInfo(r)),
Content::SystemGetDatetimeResponse(r) => Ok(SystemGetDatetime(r.datetime)),
Content::SystemProtobufVersionResponse(r) => Ok(SystemProtobufVersion(r)),
Content::SystemUpdateResponse(r) => Ok(SystemUpdate(r)),
Content::SystemPowerInfoResponse(r) => Ok(SystemPowerInfo(r)),
Content::StorageInfoResponse(r) => Ok(StorageInfo(r)),
Content::StorageTimestampResponse(r) => Ok(StorageTimestamp(r)),
Content::StorageStatResponse(r) => Ok(StorageStat(r.file.map(|x| x.size))),
Content::StorageListResponse(r) => {
let items = r
.file
.into_iter()
.map(|file| {
Ok(match decode_storage_file_type(file.r#type)? {
FileType::File => ReadDirItem::File(
file.name,
file.size,
if file.md5sum.is_empty() {
None
} else {
Some(file.md5sum)
},
),
FileType::Dir => ReadDirItem::Dir(file.name),
})
})
.collect::<Result<Vec<_>, crate::error::Error>>()?;
Ok(StorageList(items))
}
Content::StorageReadResponse(r) => {
let data = match r.file {
None => None,
Some(file) => match decode_storage_file_type(file.r#type)? {
FileType::File => Some(file.data.into()),
FileType::Dir => {
return Err(crate::error::Error::InvalidRpcPayload(
"storage read response contained a directory entry",
));
}
},
};
Ok(StorageRead(data))
}
Content::StorageMd5sumResponse(r) => Ok(StorageMd5sum(r.md5sum)),
Content::AppLockStatusResponse(r) => Ok(AppLockStatus(r)),
Content::AppGetErrorResponse(r) => Ok(AppGetError(r)),
Content::GuiScreenFrame(r) => Ok(GuiScreenFrame(r)),
Content::GpioGetPinModeResponse(r) => Ok(GpioGetPinMode(r)),
Content::GpioReadPinResponse(r) => Ok(GpioReadPin(r)),
Content::GpioGetOtgModeResponse(r) => Ok(GpioGetOtgMode(r)),
Content::AppStateResponse(r) => Ok(AppState(r)),
Content::PropertyGetResponse(r) => Ok(PropertyGet(r)),
Content::DesktopStatus(r) => Ok(DesktopStatus(r)),
_ => Err(crate::error::Error::UnsupportedRpcContent),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::{
self,
main::Content,
storage::{self, file::FileType},
system,
};
#[test]
fn converts_ping_response() {
let message = proto::Main {
command_id: 1,
command_status: proto::CommandStatus::Ok.into(),
has_next: false,
content: Some(Content::SystemPingResponse(system::PingResponse {
data: vec![1, 2, 3, 4],
})),
};
let response = Response::try_from(message).expect("ping response should decode");
assert_eq!(response, Response::Ping(vec![1, 2, 3, 4]));
}
#[test]
fn rejects_request_messages() {
let message = proto::Main {
command_id: 1,
command_status: proto::CommandStatus::Ok.into(),
has_next: false,
content: Some(Content::SystemPingRequest(system::PingRequest {
data: vec![1, 2, 3, 4],
})),
};
let error = Response::try_from(message).expect_err("request variants are not responses");
assert!(matches!(error, crate::error::Error::UnsupportedRpcContent));
}
#[test]
fn rejects_unknown_storage_file_types() {
let message = proto::Main {
command_id: 1,
command_status: proto::CommandStatus::Ok.into(),
has_next: false,
content: Some(Content::StorageListResponse(storage::ListResponse {
file: vec![storage::File {
r#type: 99,
name: "bad".to_string(),
size: 1,
data: Vec::new(),
md5sum: String::new(),
}],
})),
};
let error = Response::try_from(message).expect_err("unknown file types should fail");
assert!(matches!(
error,
crate::error::Error::InvalidStorageFileType(99)
));
}
#[test]
fn typed_conversions_report_variant_mismatches() {
let error = Vec::<u8>::try_from(Response::StorageMd5sum("abc".to_string()))
.expect_err("mismatched conversions should fail");
assert!(matches!(
error,
crate::error::Error::UnexpectedResponse {
expected: "Ping",
actual: "StorageMd5sum",
}
));
}
#[test]
fn reads_file_payloads() {
let message = proto::Main {
command_id: 1,
command_status: proto::CommandStatus::Ok.into(),
has_next: false,
content: Some(Content::StorageReadResponse(storage::ReadResponse {
file: Some(storage::File {
r#type: FileType::File.into(),
name: "note.txt".to_string(),
size: 4,
data: b"test".to_vec(),
md5sum: String::new(),
}),
})),
};
let response = Response::try_from(message).expect("storage read should decode");
assert_eq!(
response,
Response::StorageRead(Some(Cow::Owned(b"test".to_vec())))
);
}
}