You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

362 lines
12 KiB

// ssip-client -- Speech Dispatcher client in Rust
// Copyright (c) 2021-2022 Laurent Pelecq
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.
use log::debug;
use std::io::{self, BufRead, Write};
#[cfg(feature = "tokio")]
use tokio::io::{
AsyncWrite, AsyncWriteExt,
AsyncBufRead, AsyncBufReadExt,
};
#[cfg(feature = "async-std")]
use async_std::io::{
Read as AsyncReadStd,
BufRead as AsyncBufReadStd,
Write as AsyncWriteStd,
ReadExt,
WriteExt,
prelude::BufReadExt,
};
use std::str::FromStr;
use crate::types::{ClientError, ClientResult, ClientStatus, EventId, StatusLine};
macro_rules! invalid_input {
($msg:expr) => {
ClientError::from(io::Error::new(io::ErrorKind::InvalidInput, $msg))
};
($fmt:expr, $($arg:tt)*) => {
invalid_input!(format!($fmt, $($arg)*).as_str())
};
}
/// Return the only string in the list or an error if there is no line or too many.
pub(crate) fn parse_single_value(lines: &[String]) -> ClientResult<String> {
match lines.len() {
0 => Err(ClientError::TooFewLines),
1 => Ok(lines[0].to_string()),
_ => Err(ClientError::TooManyLines),
}
}
/// Convert two lines of the response in an event id
pub(crate) fn parse_event_id(lines: &[String]) -> ClientResult<EventId> {
match lines.len() {
0 | 1 => Err(ClientError::TooFewLines),
2 => Ok(EventId::new(&lines[0], &lines[1])),
_ => Err(ClientError::TooManyLines),
}
}
/// Parse single integer value
pub(crate) fn parse_single_integer<T>(lines: &[String]) -> ClientResult<T>
where
T: FromStr,
{
parse_single_value(lines)?.parse::<T>().map_err(|_| {
ClientError::Io(io::Error::new(
io::ErrorKind::InvalidData,
"invalid integer value",
))
})
}
pub(crate) fn parse_typed_lines<T>(lines: &[String]) -> ClientResult<Vec<T>>
where
T: FromStr<Err = ClientError>,
{
lines
.iter()
.map(|line| T::from_str(line.as_str()))
.collect::<ClientResult<Vec<T>>>()
}
/// Write lines separated by CRLF.
pub(crate) fn write_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> {
for line in lines.iter() {
debug!("SSIP(out): {}", line);
output.write_all(line.as_bytes())?;
output.write_all(b"\r\n")?;
}
Ok(())
}
/// Write lines (asyncronously) separated by CRLF.
#[cfg(feature = "tokio")]
pub(crate) async fn write_lines_tokio(output: &mut (dyn AsyncWrite + Unpin), lines: &[&str]) -> ClientResult<()> {
for line in lines.iter() {
debug!("SSIP(out): {}", line);
output.write_all(line.as_bytes()).await?;
output.write_all(b"\r\n").await?;
}
Ok(())
}
/// Write lines (asyncronously) separated by CRLF.
#[cfg(feature = "async-std")]
pub(crate) async fn write_lines_async_std(output: &mut (dyn AsyncWriteStd + Unpin), lines: &[&str]) -> ClientResult<()> {
for line in lines.iter() {
debug!("SSIP(out): {}", line);
output.write_all(line.as_bytes()).await?;
output.write_all(b"\r\n").await?;
}
Ok(())
}
/// Write lines separated by CRLF and flush the output.
pub(crate) fn flush_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> {
write_lines(output, lines)?;
output.flush()?;
Ok(())
}
/// Write lines separated by CRLF and flush the output asyncronously.
#[cfg(feature = "tokio")]
pub(crate) async fn flush_lines_tokio(output: &mut (dyn AsyncWrite + Unpin), lines: &[&str]) -> ClientResult<()> {
write_lines_tokio(output, lines).await?;
output.flush().await?;
Ok(())
}
/// Write lines separated by CRLF and flush the output asyncronously.
#[cfg(feature = "async-std")]
pub(crate) async fn flush_lines_async_std(output: &mut (dyn AsyncWriteStd + Unpin), lines: &[&str]) -> ClientResult<()> {
write_lines_async_std(output, lines).await?;
output.flush().await?;
Ok(())
}
/// Strip prefix if found
fn strip_prefix(line: &str, prefix: &str) -> String {
line.strip_prefix(prefix).unwrap_or(line).to_string()
}
/// Parse the status line "OK msg" or "ERR msg"
fn parse_status_line(code: u16, line: &str) -> ClientStatus {
if (300..700).contains(&code) {
const TOKEN_ERR: &str = "ERR ";
let message = strip_prefix(line, TOKEN_ERR);
Err(ClientError::Ssip(StatusLine { code, message }))
} else {
const TOKEN_OK: &str = "OK ";
let message = strip_prefix(line, TOKEN_OK);
Ok(StatusLine { code, message })
}
}
/// Read lines from server until a status line is found.
#[cfg(feature = "tokio")]
pub(crate) async fn receive_answer_tokio(
input: &mut (dyn AsyncBufRead + Unpin),
mut lines: Option<&mut Vec<String>>,
) -> ClientStatus {
loop {
let mut line = String::new();
input.read_line(&mut line).await.map_err(ClientError::Io)?;
debug!("SSIP(in): {}", line.trim_end());
match line.chars().nth(3) {
Some(ch) => match ch {
' ' => match line[0..3].parse::<u16>() {
Ok(code) => return parse_status_line(code, line[4..].trim_end()),
Err(err) => return Err(invalid_input!(err.to_string())),
},
'-' => match lines {
Some(ref mut lines) => lines.push(line[4..].trim_end().to_string()),
None => return Err(invalid_input!("unexpected line: {}", line)),
},
ch => {
return Err(invalid_input!("expecting space or dash, got {}.", ch));
}
},
None if line.is_empty() => return Err(invalid_input!("empty line")),
None => return Err(invalid_input!("line too short: {}", line)),
}
}
}
/// Read lines from server until a status line is found.
#[cfg(feature = "async-std")]
pub(crate) async fn receive_answer_async_std(
input: &mut (dyn AsyncBufReadStd + Unpin),
mut lines: Option<&mut Vec<String>>,
) -> ClientStatus {
loop {
let mut line = String::new();
input.read_line(&mut line).await.map_err(ClientError::Io)?;
debug!("SSIP(in): {}", line.trim_end());
match line.chars().nth(3) {
Some(ch) => match ch {
' ' => match line[0..3].parse::<u16>() {
Ok(code) => return parse_status_line(code, line[4..].trim_end()),
Err(err) => return Err(invalid_input!(err.to_string())),
},
'-' => match lines {
Some(ref mut lines) => lines.push(line[4..].trim_end().to_string()),
None => return Err(invalid_input!("unexpected line: {}", line)),
},
ch => {
return Err(invalid_input!("expecting space or dash, got {}.", ch));
}
},
None if line.is_empty() => return Err(invalid_input!("empty line")),
None => return Err(invalid_input!("line too short: {}", line)),
}
}
}
/// Read lines from server until a status line is found asyncronously.
pub(crate) fn receive_answer(
input: &mut dyn BufRead,
mut lines: Option<&mut Vec<String>>,
) -> ClientStatus {
loop {
let mut line = String::new();
input.read_line(&mut line).map_err(ClientError::Io)?;
debug!("SSIP(in): {}", line.trim_end());
match line.chars().nth(3) {
Some(ch) => match ch {
' ' => match line[0..3].parse::<u16>() {
Ok(code) => return parse_status_line(code, line[4..].trim_end()),
Err(err) => return Err(invalid_input!(err.to_string())),
},
'-' => match lines {
Some(ref mut lines) => lines.push(line[4..].trim_end().to_string()),
None => return Err(invalid_input!("unexpected line: {}", line)),
},
ch => {
return Err(invalid_input!("expecting space or dash, got {}.", ch));
}
},
None if line.is_empty() => return Err(invalid_input!("empty line")),
None => return Err(invalid_input!("line too short: {}", line)),
}
}
}
#[cfg(test)]
mod tests {
use std::io::BufReader;
use super::{receive_answer, ClientError, ClientResult};
use crate::types::SynthesisVoice;
#[test]
fn single_ok_status_line() {
let mut input = BufReader::new("208 OK CLIENT NAME SET\r\n".as_bytes());
let status = receive_answer(&mut input, None).unwrap();
assert_eq!(208, status.code);
assert_eq!("CLIENT NAME SET", status.message);
}
#[test]
fn single_success_status_line() {
let mut input = BufReader::new("231 HAPPY HACKING\r\n".as_bytes());
let status = receive_answer(&mut input, None).unwrap();
assert_eq!(231, status.code);
assert_eq!("HAPPY HACKING", status.message);
}
#[test]
fn single_err_status_line() {
let mut input = BufReader::new("409 ERR RATE TOO HIGH\r\n".as_bytes());
match receive_answer(&mut input, None).err().unwrap() {
ClientError::Ssip(status) => {
assert_eq!(409, status.code);
assert_eq!("RATE TOO HIGH", status.message);
}
err => panic!("{}: invalid error", err),
}
}
#[test]
fn multi_lines() {
let mut input = BufReader::new(
"249-afrikaans\taf\tnone\r\n249-en-rhotic\ten\tr\r\n249 OK VOICE LIST SENT\r\n"
.as_bytes(),
);
let mut lines = Vec::new();
let status = receive_answer(&mut input, Some(&mut lines)).unwrap();
assert_eq!(249, status.code);
assert_eq!("VOICE LIST SENT", status.message);
assert_eq!(
vec!["afrikaans\taf\tnone", "en-rhotic\ten\tr"],
lines.as_slice()
);
}
#[test]
fn parse_single_value() -> ClientResult<()> {
let no_lines = Vec::new();
assert!(matches!(
super::parse_single_value(&no_lines),
Err(ClientError::TooFewLines)
));
let one = String::from("one");
let one_line = vec![one.to_owned()];
assert_eq!(one, super::parse_single_value(&one_line)?);
let two_lines = vec![one, String::from("two")];
assert!(matches!(
super::parse_single_value(&two_lines),
Err(ClientError::TooManyLines)
));
Ok(())
}
#[test]
fn parse_event_id() -> ClientResult<()> {
let no_lines = Vec::new();
assert!(matches!(
super::parse_event_id(&no_lines),
Err(ClientError::TooFewLines)
));
let one_line = vec![String::from("one")];
assert!(matches!(
super::parse_event_id(&one_line),
Err(ClientError::TooFewLines)
));
let mid = String::from("message");
let cid = String::from("client");
let two_lines = vec![mid.to_owned(), cid.to_owned()];
let event_id = super::parse_event_id(&two_lines)?;
assert_eq!(mid, event_id.message);
assert_eq!(cid, event_id.client);
let three_lines = vec![
String::from("one"),
String::from("two"),
String::from("three"),
];
assert!(matches!(
super::parse_event_id(&three_lines),
Err(ClientError::TooManyLines)
));
Ok(())
}
#[test]
fn parse_synthesis_voices() -> ClientResult<()> {
let lines = ["en", "afrikaans\taf", "lancashire\ten\tuk-north"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let voices = super::parse_typed_lines::<SynthesisVoice>(&lines)?;
assert_eq!(3, voices.len());
assert_eq!("en", voices[0].name.as_str());
assert_eq!(Some(String::from("af")), voices[1].language);
assert_eq!(Some(String::from("uk-north")), voices[2].dialect);
Ok(())
}
}