// ssip-client -- Speech Dispatcher client in Rust // Copyright (c) 2021-2022 Laurent Pelecq // // Licensed under the Apache License, Version 2.0 // or the MIT // license , 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 { 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 { 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(lines: &[String]) -> ClientResult where T: FromStr, { parse_single_value(lines)?.parse::().map_err(|_| { ClientError::Io(io::Error::new( io::ErrorKind::InvalidData, "invalid integer value", )) }) } pub(crate) fn parse_typed_lines(lines: &[String]) -> ClientResult> where T: FromStr, { lines .iter() .map(|line| T::from_str(line.as_str())) .collect::>>() } /// 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>, ) -> 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::() { 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>, ) -> 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::() { 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>, ) -> 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::() { 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::>(); let voices = super::parse_typed_lines::(&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(()) } }