Krolyxon
Commit cf9c69a

rmptuisrcmpdconnection.rs

156 lines · 5.0 KB · Rust

1//! An asynchronous MPD connection over TCP or a Unix socket.
2
3use std::{fmt, path::PathBuf};
4
5use tokio::{
6 io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufStream},
7 net::TcpStream,
8};
9
10use super::{
11 error::{MpdError, MpdResult},
12 protocol::{self, Command, Line, Pair},
13};
14
15/// Where the server listens.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum Address {
18 /// `host:port`, with IPv6 hosts in brackets.
19 Tcp(String),
20 Unix(PathBuf),
21}
22
23impl fmt::Display for Address {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Tcp(addr) => f.write_str(addr),
27 Self::Unix(path) => write!(f, "{}", path.display()),
28 }
29 }
30}
31
32trait Socket: AsyncRead + AsyncWrite + Unpin + Send {}
33impl<T: AsyncRead + AsyncWrite + Unpin + Send> Socket for T {}
34
35pub struct Connection {
36 stream: BufStream<Box<dyn Socket>>,
37 version: String,
38 line: Vec<u8>,
39}
40
41impl fmt::Debug for Connection {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.debug_struct("Connection")
44 .field("version", &self.version)
45 .finish_non_exhaustive()
46 }
47}
48
49impl Connection {
50 /// Connects, reads the greeting and authenticates if `password` is set.
51 pub async fn connect(address: &Address, password: Option<&str>) -> MpdResult<Self> {
52 let socket: Box<dyn Socket> = match address {
53 Address::Tcp(addr) => {
54 let stream = TcpStream::connect(addr).await?;
55 stream.set_nodelay(true)?;
56 Box::new(stream)
57 }
58 #[cfg(unix)]
59 Address::Unix(path) => Box::new(tokio::net::UnixStream::connect(path).await?),
60 #[cfg(not(unix))]
61 Address::Unix(_) => {
62 return Err(MpdError::Protocol(
63 "Unix sockets are not supported on this platform".into(),
64 ));
65 }
66 };
67 let mut conn = Self {
68 stream: BufStream::new(socket),
69 version: String::new(),
70 line: Vec::new(),
71 };
72 let greeting = conn.read_line().await?;
73 conn.version = protocol::parse_greeting(&greeting)?;
74 if let Some(password) = password.filter(|p| !p.is_empty()) {
75 conn.execute(&Command::new("password").arg(password))
76 .await?;
77 }
78 Ok(conn)
79 }
80
81 /// The protocol version announced by the server.
82 pub fn version(&self) -> &str {
83 &self.version
84 }
85
86 /// Runs one command and collects its response.
87 pub async fn execute(&mut self, command: &Command) -> MpdResult<Vec<Pair>> {
88 self.send(command.as_str()).await?;
89 self.read_response().await
90 }
91
92 /// Runs commands atomically as a command list, returning one response per
93 /// command.
94 pub async fn execute_list(&mut self, commands: &[Command]) -> MpdResult<Vec<Vec<Pair>>> {
95 let mut request = String::from("command_list_ok_begin\n");
96 for command in commands {
97 request.push_str(command.as_str());
98 request.push('\n');
99 }
100 request.push_str("command_list_end");
101 self.send(&request).await?;
102
103 let mut responses = Vec::with_capacity(commands.len());
104 let mut current = Vec::new();
105 loop {
106 let line = self.read_line().await?;
107 match protocol::parse_line(&line)? {
108 Line::Pair(pair) => current.push(pair),
109 Line::ListOk => responses.push(std::mem::take(&mut current)),
110 Line::Ok => return Ok(responses),
111 }
112 }
113 }
114
115 /// Runs commands as a list when there is more than one, discarding output.
116 pub async fn execute_all(&mut self, commands: &[Command]) -> MpdResult<()> {
117 match commands {
118 [] => Ok(()),
119 [command] => self.execute(command).await.map(drop),
120 _ => self.execute_list(commands).await.map(drop),
121 }
122 }
123
124 async fn send(&mut self, request: &str) -> MpdResult<()> {
125 self.stream.write_all(request.as_bytes()).await?;
126 self.stream.write_all(b"\n").await?;
127 self.stream.flush().await?;
128 Ok(())
129 }
130
131 async fn read_response(&mut self) -> MpdResult<Vec<Pair>> {
132 let mut pairs = Vec::new();
133 loop {
134 let line = self.read_line().await?;
135 match protocol::parse_line(&line)? {
136 Line::Pair(pair) => pairs.push(pair),
137 Line::Ok => return Ok(pairs),
138 Line::ListOk => {
139 return Err(MpdError::Protocol("unexpected list_OK".into()));
140 }
141 }
142 }
143 }
144
145 async fn read_line(&mut self) -> MpdResult<String> {
146 self.line.clear();
147 if self.stream.read_until(b'\n', &mut self.line).await? == 0 {
148 return Err(MpdError::Closed);
149 }
150 if self.line.last() == Some(&b'\n') {
151 self.line.pop();
152 }
153 // Tags are UTF-8 by protocol; tolerate broken files instead of failing.
154 Ok(String::from_utf8_lossy(&self.line).into_owned())
155 }
156}