use super::{Command, Result};
use tokio::{
io::{AsyncWriteExt, BufWriter},
process::ChildStdin,
sync::mpsc::Receiver,
};
pub(super) async fn transmit<'a>(stream: ChildStdin, mut channel: Receiver<Command>) -> Result<()> {
let mut writer = BufWriter::new(stream);
loop {
match channel.recv().await {
None => {
println!("🛈 Pinentry: Command channel closed");
break;
}
Some(cmd) => {
let lines: Vec<Vec<u8>> = cmd.into();
for mut line in lines {
line.push(b'\n');
writer.write(&line).await?;
writer.flush().await?;
}
}
}
}
Ok(())
}
impl From<Command> for Vec<Vec<u8>> {
fn from(cmd: Command) -> Self {
match cmd {
Command::Title(s) => CmdType::Text("SETTITLE", s),
Command::Description(s) => CmdType::Text("SETDESC", s),
Command::Prompt(s) => CmdType::Text("SETPROMPT", s),
Command::OKButton(s) => CmdType::Text("SETOK", s),
Command::CancelButton(s) => CmdType::Text("SETCANCEL", s),
Command::NotOKButton(s) => CmdType::Text("SETNOTOK", s),
Command::TimeOut(t) => CmdType::Number("SETTIMEOUT", t),
Command::Data(data) => CmdType::Data("D", data),
Command::End => CmdType::Plain("END"),
Command::Cancel => CmdType::Plain("CAN"),
Command::Password => CmdType::Plain("GETPIN"),
Command::Confirm => CmdType::Plain("CONFIRM"),
Command::Notify => CmdType::Plain("MESSAGE"),
Command::Exit => CmdType::Plain("BYE"),
}
.into()
}
}
enum CmdType {
Plain(&'static str),
Text(&'static str, String),
Number(&'static str, i64),
Data(&'static str, Vec<u8>),
}
impl From<CmdType> for Vec<Vec<u8>> {
fn from(data: CmdType) -> Self {
match data {
CmdType::Text(cmd, text) => text.export(cmd),
CmdType::Data(cmd, data) => data.segment(cmd),
CmdType::Plain(cmd) => ().export(cmd),
CmdType::Number(cmd, num) => num.export(cmd),
}
}
}
trait AssuanEscape {
fn escape(self) -> Vec<u8>;
}
impl<'a> AssuanEscape for &'a [u8] {
fn escape(self) -> Vec<u8> {
let mut dest = Vec::with_capacity(self.len());
for byte in self {
match byte {
b'%' => dest.extend_from_slice(b"%25"),
b'\r' => dest.extend_from_slice(b"%0D"),
b'\n' => dest.extend_from_slice(b"%0A"),
_ => dest.push(*byte),
}
}
dest
}
}
trait AssuanSegmented {
fn segment(self, cmd: &'static str) -> Vec<Vec<u8>>;
}
impl<T: Into<Vec<u8>>> AssuanSegmented for T {
fn segment(self, cmd: &'static str) -> Vec<Vec<u8>> {
const MAX_CHUNK_SIZE: usize = 999;
let cmd = format!("{cmd} ").into_bytes();
let sub_chunk_size = MAX_CHUNK_SIZE - cmd.len();
self.into()
.escape()
.chunks(sub_chunk_size)
.map(|chunk| cmd.iter().chain(chunk).map(|x| *x).collect())
.collect()
}
}
trait AssuanSingleLine {
fn export(&self, cmd: &'static str) -> Vec<Vec<u8>>;
}
impl AssuanSingleLine for () {
fn export(&self, cmd: &'static str) -> Vec<Vec<u8>> {
vec![format!("{cmd}").into_bytes()]
}
}
impl AssuanSingleLine for i64 {
fn export(&self, cmd: &'static str) -> Vec<Vec<u8>> {
vec![format!("{cmd} {self}").into_bytes()]
}
}
impl AssuanSingleLine for String {
fn export(&self, cmd: &'static str) -> Vec<Vec<u8>> {
let mut line = format!("{cmd} ").into_bytes();
line.extend_from_slice(&self.as_bytes().escape());
vec![line]
}
}