#![allow(clippy::type_complexity)]
use rusp_lib as rusp;
use clap::{Parser, Subcommand};
use std::collections::HashMap;
use std::fs::File;
use std::io::{stdin, stdout, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use anyhow::Result;
use rusp::usp_decoder::{try_decode_msg, try_decode_record};
#[derive(PartialEq)]
enum OutputFormat {
Json,
CStr,
CArray,
Protobuf,
}
#[derive(Parser)]
#[command(
author,
version,
name = "rusp",
about = "The Rust USP toolkit, deprecated in 1.0, use rusp-run instead"
)]
struct Rusp {
#[arg(
long = "carray",
conflicts_with = "cstr",
conflicts_with = "json",
conflicts_with = "protobuf"
)]
carray: bool,
#[arg(
long = "json",
conflicts_with = "cstr",
conflicts_with = "carray",
conflicts_with = "protobuf"
)]
json: bool,
#[arg(
long = "cstr",
conflicts_with = "json",
conflicts_with = "carray",
conflicts_with = "protobuf"
)]
cstr: bool,
#[arg(
long = "protobuf",
conflicts_with = "json",
conflicts_with = "carray",
conflicts_with = "cstr"
)]
protobuf: bool,
#[command(subcommand)]
action: RuspAction,
}
#[derive(Subcommand)]
enum RuspAction {
#[command(name = "decode_msg")]
DecodeMsg {},
#[command(name = "decode_msg_files")]
DecodeMsgFiles {
#[arg(required = true)]
files: Vec<PathBuf>,
},
#[command(name = "decode_record")]
DecodeRecord {},
#[command(name = "decode_record_files")]
DecodeRecordFiles {
#[arg(required = true)]
files: Vec<PathBuf>,
},
#[command(name = "encode_msg")]
EncodeMsg {
_msgid: Option<String>,
#[arg(short = 'f', long = "file")]
_filename: Option<PathBuf>,
_command: Vec<String>,
},
#[command(name = "extract_msg")]
ExtractMsg {
in_file: PathBuf,
out_file: PathBuf,
},
#[command(name = "encode_no_session_record")]
EncodeNoSessionRecord {
#[arg(long = "version", default_value = "1.3")]
_version: Option<String>,
#[arg(long = "from", default_value = "doc::from")]
_from: Option<String>,
#[arg(long = "to", default_value = "doc::to")]
_to: Option<String>,
#[arg(short = 'f', long = "file")]
_filename: Option<PathBuf>,
},
#[command(name = "encode_session_record")]
EncodeSessionRecord {
#[arg(long = "version", default_value = "1.3")]
_version: Option<String>,
#[arg(long = "from", default_value = "doc::from")]
_from: Option<String>,
#[arg(long = "to", default_value = "doc::to")]
_to: String,
#[arg(long = "session_id", default_value = "1234")]
_session_id: u64,
#[arg(long = "sequence_id", default_value = "1")]
_sequence_id: u64,
#[arg(long = "expected_id", default_value = "2")]
_expected_id: u64,
#[arg(long = "retransmit_id", default_value = "0")]
_retransmit_id: u64,
#[arg(short = 'f', long = "file")]
_filename: Option<PathBuf>,
},
#[command(name = "create_mqtt_connect_record")]
CreateMQTTConnectRecord {
#[arg(long = "version", default_value = "1.3")]
_version: String,
#[arg(long = "from", default_value = "doc::from")]
_from: String,
#[arg(long = "to", default_value = "doc::to")]
_to: String,
#[arg(short = '4', long = "mqtt311")]
_mqtt311: bool,
#[arg(short = 's')]
_subscribed_topic: Option<String>,
#[arg(short = 'f', long = "file")]
_filename: Option<PathBuf>,
},
}
fn parse_key_val_json(s: &str) -> Result<HashMap<String, String>, String> {
serde_json::from_str::<HashMap<String, String>>(s).map_err(|e| e.to_string())
}
#[allow(dead_code)]
#[derive(PartialEq, Eq)]
enum OperateResponse {
OutputArgs(HashMap<String, String>),
CommandFailure(u32, String),
}
impl Default for OperateResponse {
fn default() -> Self {
Self::OutputArgs(HashMap::new())
}
}
#[derive(Parser, PartialEq, Eq)]
enum NotifyType {
OnBoardRequest {
oui: String,
product_class: String,
serial_number: String,
agent_supported_protocol_versions: String,
},
ValueChange {
param_path: String,
param_value: String,
},
Event {
obj_path: String,
event_name: String,
#[arg(value_parser = parse_key_val_json)]
params: HashMap<String, String>,
},
ObjectCreation {
obj_path: String,
#[arg(value_parser = parse_key_val_json)]
unique_keys: HashMap<String, String>,
},
ObjectDeletion {
obj_path: String,
},
OperationComplete {
obj_path: String,
command_name: String,
command_key: String,
#[structopt(skip)]
operation_resp: OperateResponse,
},
}
fn decode_msg_files(files: Vec<PathBuf>, format: &OutputFormat) -> Result<()> {
for file in files {
let fp = File::open(&file)?;
let mut buf_reader = BufReader::new(fp);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
let decoded = try_decode_msg(&contents)?;
write_msg(&decoded, get_out_stream(None)?, format)?;
}
Ok(())
}
fn decode_msg_stdin(format: &OutputFormat) -> Result<()> {
let mut contents = Vec::new();
stdin().read_to_end(&mut contents)?;
let decoded = try_decode_msg(&contents)?;
write_msg(&decoded, get_out_stream(None)?, format)
}
fn decode_record_files(files: Vec<PathBuf>, format: &OutputFormat) -> Result<()> {
for file in files {
let fp = File::open(&file)?;
let mut buf_reader = BufReader::new(fp);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
let decoded = try_decode_record(&contents)?;
write_record(&decoded, get_out_stream(None)?, format)?;
}
Ok(())
}
fn decode_record_stdin(format: &OutputFormat) -> Result<()> {
let mut contents = Vec::new();
stdin().read_to_end(&mut contents)?;
let decoded = try_decode_record(&contents)?;
write_record(&decoded, get_out_stream(None)?, format)
}
fn get_out_stream(filename: Option<PathBuf>) -> Result<Box<dyn Write>> {
if let Some(filename) = filename {
Ok(Box::new(File::create(filename)?))
} else {
Ok(Box::new(stdout()))
}
}
fn write_msg<W: Write>(msg: &rusp::usp::Msg, mut out: W, format: &OutputFormat) -> Result<()> {
match format {
OutputFormat::Json => {
out.write_all(&serde_json::to_string_pretty(&msg)?.into_bytes())?;
writeln!(out)
}
OutputFormat::CStr => out.write_all(&msg.to_c_str()?.into_bytes()),
OutputFormat::CArray => out.write_all(&msg.to_c_array()?.into_bytes()),
OutputFormat::Protobuf => out.write_all(&msg.to_vec()?),
}?;
Ok(())
}
fn write_record<W: Write>(
record: &rusp::usp_record::Record,
mut out: W,
format: &OutputFormat,
) -> Result<()> {
match format {
OutputFormat::Json => {
out.write_all(&serde_json::to_string_pretty(&record)?.into_bytes())?;
writeln!(out)
}
OutputFormat::CStr => out.write_all(&record.to_c_str()?.into_bytes()),
OutputFormat::CArray => out.write_all(&record.to_c_array()?.into_bytes()),
OutputFormat::Protobuf => out.write_all(&record.to_vec()?),
}?;
Ok(())
}
fn extract_msg(in_file: &Path, out_file: &Path, format: &OutputFormat) -> Result<()> {
use rusp::usp_record::mod_Record::OneOfrecord_type;
let fp = File::open(in_file)?;
let mut buf_reader = BufReader::new(fp);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
let record = try_decode_record(&contents)?;
match record.record_type {
OneOfrecord_type::no_session_context(context) => {
let msg = try_decode_msg(&context.payload)?;
let out = get_out_stream(Some(out_file.to_path_buf()))?;
write_msg(&msg, out, format)?;
}
OneOfrecord_type::session_context(_)
| OneOfrecord_type::websocket_connect(_)
| OneOfrecord_type::mqtt_connect(_)
| OneOfrecord_type::stomp_connect(_)
| OneOfrecord_type::uds_connect(_)
| OneOfrecord_type::disconnect(_)
| OneOfrecord_type::None => unreachable!(),
}
Ok(())
}
fn main() -> Result<()> {
let args = Rusp::parse();
println!(
"The rusp binary is deprecated and will be removed in future versions, please use rusp-run instead"
);
let format = {
if args.carray {
OutputFormat::CArray
} else if args.cstr {
OutputFormat::CStr
} else if args.protobuf {
OutputFormat::Protobuf
} else {
OutputFormat::Json
}
};
if format == OutputFormat::Protobuf {
return Err(anyhow::anyhow!(
"Support for protobuf output has been removed in rusp 1.0, use the new rusp-run command instead"
));
}
match args.action {
RuspAction::DecodeRecordFiles { files } => decode_record_files(files, &format),
RuspAction::DecodeRecord {} => decode_record_stdin(&format),
RuspAction::DecodeMsgFiles { files } => decode_msg_files(files, &format),
RuspAction::DecodeMsg {} => decode_msg_stdin(&format),
RuspAction::EncodeMsg {
_msgid,
_filename,
_command,
} => Err(anyhow::anyhow!(
"Support for encoding messages has been removed in rusp 0.96, use the new rusp-run command instead"
)),
RuspAction::ExtractMsg { in_file, out_file } => extract_msg(&in_file, &out_file, &format),
RuspAction::EncodeNoSessionRecord {
_version,
_from,
_to,
_filename,
} => Err(anyhow::anyhow!(
"Support for encoding messages has been removed in rusp 1.0, use the new rusp-run command instead"
)),
RuspAction::EncodeSessionRecord {
_version,
_from,
_to,
_filename,
_session_id,
_sequence_id,
_expected_id,
_retransmit_id,
} =>Err(anyhow::anyhow!(
"Support for encoding messages has been removed in rusp 1.0, use the new rusp-run command instead"
)),
RuspAction::CreateMQTTConnectRecord {
_version,
_from,
_to,
_mqtt311,
_subscribed_topic,
_filename,
} => Err(anyhow::anyhow!(
"Support for encoding messages has been removed in rusp 1.0, use the new rusp-run command instead"
)),
}?;
Ok(())
}