| 1 | //! Byte transports. Device protocols only see the [`Transport`] trait, so they can be tested |
| 2 | //! against a scripted mock instead of real buds. |
| 3 | |
| 4 | use std::io; |
| 5 | use std::time::Instant; |
| 6 | |
| 7 | use crate::debug; |
| 8 | use crate::error::{Error, Result}; |
| 9 | |
| 10 | pub mod rfcomm; |
| 11 | pub mod sdp; |
| 12 | mod socket; |
| 13 | |
| 14 | #[cfg(test)] |
| 15 | pub(crate) mod mock; |
| 16 | |
| 17 | /// A bidirectional byte stream to the buds. |
| 18 | pub trait Transport: Send { |
| 19 | /// Send all of `bytes`. |
| 20 | fn send(&mut self, bytes: &[u8]) -> Result<()>; |
| 21 | |
| 22 | /// The next bytes from the buds, or `None` once `deadline` passes with nothing to read. |
| 23 | fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>>; |
| 24 | } |
| 25 | |
| 26 | /// Wraps a transport and prints every chunk sent and received (`--debug`). |
| 27 | pub struct Hexdump { |
| 28 | inner: Box<dyn Transport>, |
| 29 | start: Instant, |
| 30 | } |
| 31 | |
| 32 | impl Hexdump { |
| 33 | pub fn new(inner: Box<dyn Transport>) -> Self { |
| 34 | Self { |
| 35 | inner, |
| 36 | start: Instant::now(), |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | fn print(&self, direction: &str, bytes: &[u8]) { |
| 41 | let elapsed = self.start.elapsed().as_secs_f64(); |
| 42 | eprintln!("{elapsed:>7.3} {direction} {}", debug::hex(bytes)); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | impl Transport for Hexdump { |
| 47 | fn send(&mut self, bytes: &[u8]) -> Result<()> { |
| 48 | self.print("TX", bytes); |
| 49 | self.inner.send(bytes) |
| 50 | } |
| 51 | |
| 52 | fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>> { |
| 53 | let received = self.inner.recv(deadline)?; |
| 54 | if let Some(bytes) = &received { |
| 55 | self.print("RX", bytes); |
| 56 | } |
| 57 | Ok(received) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /// What an OS error on a Bluetooth socket means for budctl. |
| 62 | /// `may_be_busy` is set only for the RFCOMM connect, where a refusal means another client |
| 63 | /// holds the channel. Spec: transport.md#errno |
| 64 | pub(crate) fn classify(error: io::Error, context: &'static str, may_be_busy: bool) -> Error { |
| 65 | let detail = || format!("{context}: {error}"); |
| 66 | match error.raw_os_error() { |
| 67 | Some(libc::EBUSY | libc::EADDRINUSE | libc::ECONNREFUSED) if may_be_busy => Error::ChannelBusy(detail()), |
| 68 | Some( |
| 69 | libc::EHOSTDOWN |
| 70 | | libc::EHOSTUNREACH |
| 71 | | libc::ENOTCONN |
| 72 | | libc::ECONNRESET |
| 73 | | libc::ECONNABORTED |
| 74 | | libc::EPIPE |
| 75 | | libc::ENODEV |
| 76 | | libc::ENETDOWN, |
| 77 | ) => Error::NotConnected(detail()), |
| 78 | Some(libc::ETIMEDOUT | libc::EINPROGRESS | libc::EAGAIN) => Error::Timeout(detail()), |
| 79 | _ => Error::Os { context, source: error }, |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | #[cfg(test)] |
| 84 | mod tests { |
| 85 | use super::*; |
| 86 | use crate::error::Kind; |
| 87 | |
| 88 | fn kind_of(errno: i32, may_be_busy: bool) -> Kind { |
| 89 | classify(io::Error::from_raw_os_error(errno), "test", may_be_busy).kind() |
| 90 | } |
| 91 | |
| 92 | #[test] |
| 93 | fn errno_maps_to_the_documented_kinds() { |
| 94 | assert_eq!(kind_of(libc::ECONNREFUSED, true), Kind::Busy); |
| 95 | assert_eq!(kind_of(libc::EBUSY, true), Kind::Busy); |
| 96 | assert_eq!(kind_of(libc::ECONNREFUSED, false), Kind::NotFound); |
| 97 | assert_eq!(kind_of(libc::EHOSTDOWN, true), Kind::NotFound); |
| 98 | assert_eq!(kind_of(libc::ECONNRESET, false), Kind::NotFound); |
| 99 | assert_eq!(kind_of(libc::EINPROGRESS, true), Kind::Failed); |
| 100 | assert_eq!(kind_of(libc::ETIMEDOUT, false), Kind::Failed); |
| 101 | assert_eq!(kind_of(libc::EACCES, false), Kind::NotFound); |
| 102 | } |
| 103 | } |