use std::time::Duration;
#[cfg(feature = "rpc")]
use super::uri::WireProfile;
use super::uri::{Endpoint, Uri};
use crate::error::{Result, StatusCode};
use crate::{FromIBinder, SIBinder, Strong};
#[derive(Default)]
#[non_exhaustive]
pub struct ClientOptions {
#[cfg(feature = "rpc-tls")]
pub tls: Option<std::sync::Arc<crate::rpc::rustls::ClientConfig>>,
#[cfg(feature = "rpc-tls")]
pub tls_server_name: Option<String>,
#[deprecated(
since = "0.12.0",
note = "a non-empty id is refused with BadValue: open a new session, then add connections with `RpcSession::add_outgoing_connection_with_config` on it"
)]
pub session_id: Option<Vec<u8>>,
pub outgoing_connections: Option<u32>,
#[cfg_attr(
feature = "rpc",
doc = "[`RpcClientConfig::incoming_connections`](crate::rpc::RpcClientConfig::incoming_connections)."
)]
#[cfg_attr(
not(feature = "rpc"),
doc = "`RpcClientConfig::incoming_connections` (`rpc` feature)."
)]
pub incoming_connections: Option<u32>,
#[cfg(feature = "rpc")]
pub fd_mode: Option<crate::rpc::FileDescriptorTransportMode>,
pub timeout: Option<Duration>,
#[cfg(feature = "rpc")]
#[deprecated(
since = "0.12.0",
note = "`timeout` bounds each connect and handshake step too; set it instead (plan 2-24 D6)"
)]
pub handshake_timeout: Option<Duration>,
pub driver: Option<std::path::PathBuf>,
pub mmap_size: Option<usize>,
}
pub struct Client {
endpoint: Endpoint,
inner: Inner,
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("Client");
match &self.inner {
Inner::Kernel => d.field("transport", &"kernel"),
#[cfg(feature = "rpc")]
Inner::Rpc(_) => d.field("transport", &"rpc"),
};
d.finish()
}
}
impl std::fmt::Debug for ClientOptions {
#[expect(
deprecated,
reason = "shows `session_id` and `handshake_timeout` while they are still read"
)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("ClientOptions");
#[cfg(feature = "rpc-tls")]
d.field("tls", &self.tls.is_some())
.field("tls_server_name", &self.tls_server_name);
d.field("session_id", &self.session_id.as_ref().map(|v| v.len()))
.field("outgoing_connections", &self.outgoing_connections)
.field("incoming_connections", &self.incoming_connections);
#[cfg(feature = "rpc")]
d.field("fd_mode", &self.fd_mode)
.field("handshake_timeout", &self.handshake_timeout);
d.field("timeout", &self.timeout)
.field("driver", &self.driver)
.field("mmap_size", &self.mmap_size)
.finish()
}
}
enum Inner {
Kernel,
#[cfg(feature = "rpc")]
Rpc(crate::rpc::RpcSession),
}
fn one_source<T: PartialEq + std::fmt::Debug>(
what: &str,
from_option: Option<T>,
from_uri: Option<T>,
) -> Result<Option<T>> {
match (from_option, from_uri) {
(Some(o), Some(u)) if o != u => {
log::error!(
"rsbinder::Client::open: ClientOptions::{what}={o:?} conflicts with the URI's \
{u:?} — give the value once"
);
Err(StatusCode::BadValue)
}
(o, u) => Ok(o.or(u)),
}
}
pub(super) fn new_client(uri: Uri, o: ClientOptions) -> Result<Client> {
uri.validate()?;
open_staged(uri, o).map_err(|(_, code)| code)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OpenStage {
Setup,
#[cfg(feature = "rpc")]
Connect,
}
pub(crate) fn open_staged(
uri: Uri,
o: ClientOptions,
) -> std::result::Result<Client, (OpenStage, StatusCode)> {
let setup = |code| (OpenStage::Setup, code);
#[cfg(feature = "rpc")]
if !uri.endpoint.is_kernel() {
let cfg = rpc_setup(&uri, &o).map_err(setup)?;
let session = rpc_connect(cfg, &uri, &o).map_err(|code| (OpenStage::Connect, code))?;
return Ok(Client {
endpoint: uri.endpoint.clone(),
inner: Inner::Rpc(session),
});
}
kernel_open(uri, o).map_err(setup)
}
#[expect(
deprecated,
reason = "refuses `session_id` and `handshake_timeout` where they do not apply"
)]
fn kernel_open(uri: Uri, o: ClientOptions) -> Result<Client> {
if uri.service.is_some() {
log::error!("rsbinder::Client::open: a service name is not allowed here (use connect)");
return Err(StatusCode::BadValue);
}
let reject = |what: &str| {
log::error!(
"rsbinder::Client::open: option `{what}` does not apply to {:?}",
uri.endpoint
);
StatusCode::BadValue
};
match &uri.endpoint {
Endpoint::Kernel(k) => {
if o.session_id.is_some()
|| o.outgoing_connections.is_some()
|| o.incoming_connections.is_some()
|| o.timeout.is_some()
{
return Err(reject(
"session_id/outgoing_connections/incoming_connections/timeout",
));
}
#[cfg(feature = "rpc")]
if o.fd_mode.is_some() || o.handshake_timeout.is_some() {
return Err(reject("fd_mode/handshake_timeout"));
}
#[cfg(feature = "rpc-tls")]
if o.tls.is_some() || o.tls_server_name.is_some() {
return Err(reject("tls/tls_server_name"));
}
let driver = one_source("driver", o.driver.as_deref(), k.driver())?;
let mmap_size = one_source("mmap_size", o.mmap_size, k.mmap_size())?;
super::server::kernel_init(driver, k.threads(), mmap_size)?;
crate::ProcessState::start_thread_pool();
Ok(Client {
endpoint: uri.endpoint.clone(),
inner: Inner::Kernel,
})
}
_ => {
log::error!(
"rsbinder::Client::open: {:?} needs the `rpc` feature",
uri.endpoint
);
Err(StatusCode::InvalidOperation)
}
}
}
#[cfg(feature = "rpc")]
#[expect(
deprecated,
reason = "forwards `handshake_timeout` to the config's own deprecated setter"
)]
fn rpc_setup<'a>(uri: &'a Uri, o: &'a ClientOptions) -> Result<crate::rpc::RpcClientConfig<'a>> {
if uri.service.is_some() {
log::error!("rsbinder::Client::open: a service name is not allowed here (use connect)");
return Err(StatusCode::BadValue);
}
if o.driver.is_some() || o.mmap_size.is_some() {
log::error!(
"rsbinder::Client::open: option `driver/mmap_size` does not apply to {:?}",
uri.endpoint
);
return Err(StatusCode::BadValue);
}
if o.fd_mode == Some(crate::rpc::FileDescriptorTransportMode::Unix)
&& !uri.endpoint.supports_fd_passing()
{
log::error!(
"rsbinder::Client::open: option `fd_mode` cannot request Unix fd passing \
on {:?} — only Unix-domain sockets carry SCM_RIGHTS",
uri.endpoint
);
return Err(StatusCode::BadValue);
}
#[cfg(feature = "rpc-tls")]
let reject_option = |what: &str, endpoint: &Endpoint| {
log::error!("rsbinder::Client::open: option `{what}` does not apply to {endpoint:?}");
StatusCode::BadValue
};
crate::rpc::session::reject_zero_handshake_timeout(
o.handshake_timeout,
"ClientOptions::handshake_timeout",
)?;
let versioned = match uri.wire {
WireProfile::R34 => None,
WireProfile::Android13Plus(v) => Some(v.get()),
};
let fan_out = o.outgoing_connections.unwrap_or(1).max(1);
let incoming = o.incoming_connections.unwrap_or(0);
let multi_conn = o.outgoing_connections.is_some() || o.incoming_connections.is_some();
if versioned.is_none() && (o.session_id.is_some() || multi_conn) {
log::error!(
"rsbinder::Client::open: session_id/outgoing_connections/incoming_connections \
need `WireProfile::Android13Plus` (`?profile=android13plus`) ({:?})",
uri.endpoint
);
return Err(StatusCode::BadValue);
}
if versioned.is_none()
&& o.handshake_timeout.is_some()
&& !matches!(uri.endpoint, Endpoint::Tls { .. })
{
log::error!(
"rsbinder::Client::open: handshake_timeout needs `WireProfile::Android13Plus` \
(`?profile=android13plus`) or a TLS endpoint ({:?})",
uri.endpoint
);
return Err(StatusCode::BadValue);
}
#[cfg(feature = "rpc-tls")]
if !matches!(uri.endpoint, Endpoint::Tls { .. })
&& (o.tls.is_some() || o.tls_server_name.is_some())
{
return Err(reject_option("tls/tls_server_name", &uri.endpoint));
}
let mut cfg = client_config(&uri.endpoint, o, versioned.unwrap_or(0))?;
if let Some(m) = o.fd_mode {
cfg = cfg.fd_mode(m);
}
if let Some(t) = o.timeout {
cfg = cfg.timeout(t);
}
if let Some(t) = o.handshake_timeout {
cfg = cfg.handshake_timeout(t);
}
if o.session_id.as_deref().is_some_and(|id| !id.is_empty()) {
log::error!(
"rsbinder::Client::open: session_id would open a second client session on a server \
session another one founded; add the connection with \
`RpcSession::add_outgoing_connection_with_config` on that session instead"
);
return Err(StatusCode::BadValue);
}
Ok(cfg
.outgoing_connections(fan_out)
.incoming_connections(incoming))
}
#[cfg(feature = "rpc")]
fn rpc_connect(
cfg: crate::rpc::RpcClientConfig<'_>,
uri: &Uri,
o: &ClientOptions,
) -> Result<crate::rpc::RpcSession> {
use crate::rpc::{AddressSpace, RpcSession};
if uri.wire != WireProfile::R34 {
return RpcSession::setup_client_android13plus_with_config(cfg);
}
let session =
RpcSession::new(cfg.connect_once()?, AddressSpace::Initiator).map_err(StatusCode::from)?;
session.set_timeout(o.timeout);
if let Some(mode) = o.fd_mode {
session.negotiate_fd_transport(mode)?;
}
Ok(session)
}
#[cfg(feature = "rpc")]
fn client_config<'a>(
endpoint: &'a Endpoint,
o: &'a ClientOptions,
max_version: u32,
) -> Result<crate::rpc::RpcClientConfig<'a>> {
use crate::rpc::RpcClientConfig;
let unix_addr_refused = |e: std::io::Error| {
log::error!("rsbinder::Client::open: {endpoint:?} is not a Unix socket address: {e}");
StatusCode::from(e)
};
match endpoint {
Endpoint::Kernel(_) => unreachable!("kernel handled by caller"),
Endpoint::Unix(path) => {
std::os::unix::net::SocketAddr::from_pathname(path).map_err(unix_addr_refused)?;
Ok(RpcClientConfig::unix(path, max_version))
}
Endpoint::UnixAbstract(name) => {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
#[cfg(target_os = "android")]
use std::os::android::net::SocketAddrExt;
#[cfg(target_os = "linux")]
use std::os::linux::net::SocketAddrExt;
std::os::unix::net::SocketAddr::from_abstract_name(name)
.map_err(unix_addr_refused)?;
Ok(RpcClientConfig::unix_abstract(name, max_version))
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
let _ = name;
log::error!("rsbinder::Client::open: abstract Unix sockets are Linux/Android only");
Err(StatusCode::InvalidOperation)
}
}
Endpoint::Vsock { cid, port } => {
#[cfg(all(feature = "rpc-vsock", any(target_os = "linux", target_os = "android")))]
{
Ok(RpcClientConfig::vsock(*cid, *port, max_version))
}
#[cfg(not(all(
feature = "rpc-vsock",
any(target_os = "linux", target_os = "android")
)))]
{
let _ = (cid, port);
log::error!(
"rsbinder::Client::open: vsock needs the `rpc-vsock` feature (Linux/Android)"
);
Err(StatusCode::InvalidOperation)
}
}
Endpoint::Tls { host, port } => {
#[cfg(feature = "rpc-tls")]
{
let tls = o.tls.clone().ok_or_else(|| {
log::error!(
"rsbinder::Client::open: a TLS endpoint requires `ClientOptions::tls`"
);
StatusCode::BadValue
})?;
let name = o.tls_server_name.as_deref().unwrap_or(host);
if rustls::pki_types::ServerName::try_from(name.to_string()).is_err() {
log::error!("rsbinder::Client::open: invalid TLS server name {name:?}");
return Err(StatusCode::RpcError);
}
Ok(RpcClientConfig::tls(host, *port, name, tls, max_version))
}
#[cfg(not(feature = "rpc-tls"))]
{
let _ = (host, port, o);
log::error!("rsbinder::Client::open: `tls://` needs the `rpc-tls` feature");
Err(StatusCode::InvalidOperation)
}
}
}
}
impl Client {
pub fn open(uri: impl Into<Uri>) -> Result<Client> {
new_client(uri.into(), ClientOptions::default())
}
pub fn open_with(
uri: impl Into<Uri>,
f: impl FnOnce(&mut ClientOptions, &Endpoint),
) -> Result<Client> {
let uri = uri.into();
let mut o = ClientOptions::default();
f(&mut o, &uri.endpoint);
new_client(uri, o)
}
pub fn endpoint(&self) -> &Endpoint {
&self.endpoint
}
pub fn get<T: FromIBinder + ?Sized>(&self, name: &str) -> Result<Strong<T>> {
FromIBinder::try_from(self.binder(name)?)
}
pub fn try_get<T: FromIBinder + ?Sized>(&self, name: &str) -> Result<Option<Strong<T>>> {
match &self.inner {
Inner::Kernel => crate::hub::try_get_interface(name),
#[cfg(feature = "rpc")]
Inner::Rpc(s) => match s.get_service(name) {
Ok(b) => FromIBinder::try_from(b).map(Some),
Err(StatusCode::NameNotFound) => Ok(None),
Err(e) => Err(e),
},
}
}
pub fn binder(&self, name: &str) -> Result<SIBinder> {
match &self.inner {
Inner::Kernel => crate::hub::default()?
.wait_for_service(name)
.ok_or(StatusCode::NameNotFound),
#[cfg(feature = "rpc")]
Inner::Rpc(s) => s.get_service(name),
}
}
#[cfg_attr(
feature = "rpc",
doc = "session has *now* — see [`RpcSession::caps`](crate::rpc::RpcSession::caps)"
)]
#[cfg_attr(
not(feature = "rpc"),
doc = "session has *now* — see `RpcSession::caps` (`rpc` feature)"
)]
pub fn caps(&self) -> crate::TransportCaps {
match &self.inner {
Inner::Kernel => crate::TransportCaps::KERNEL,
#[cfg(feature = "rpc")]
Inner::Rpc(s) => s.caps(),
}
}
#[cfg(feature = "rpc")]
pub fn session(&self) -> Option<&crate::rpc::RpcSession> {
match &self.inner {
Inner::Rpc(s) => Some(s),
Inner::Kernel => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "rpc")]
#[test]
#[allow(deprecated)] fn open_reports_the_stage_that_failed() {
use super::super::uri::WireVersion;
let open = |uri: Uri, f: fn(&mut ClientOptions)| {
let mut o = ClientOptions::default();
f(&mut o);
open_staged(uri, o).map(|_| ())
};
let missing = Uri::new(Endpoint::unix("/nonexistent/rsbinder-open-stage.sock"));
assert_eq!(
open(missing.clone(), |o| o.incoming_connections = Some(1)),
Err((OpenStage::Setup, StatusCode::BadValue)),
"multi-connection options need WireProfile::Android13Plus"
);
assert_eq!(
open(
missing
.clone()
.with_wire(WireProfile::Android13Plus(WireVersion::MAX)),
|o| {
o.session_id = Some(vec![1; 32]);
}
),
Err((OpenStage::Setup, StatusCode::BadValue)),
"a session_id would open a second client session on one server session"
);
assert_eq!(
open(missing, |_| {}),
Err((OpenStage::Connect, StatusCode::NameNotFound)),
"ENOENT on connect is the transport, not the configuration"
);
}
#[test]
fn a_setting_given_twice_must_agree() {
assert_eq!(one_source::<usize>("mmap_size", None, None), Ok(None));
assert_eq!(one_source("mmap_size", Some(8192), None), Ok(Some(8192)));
assert_eq!(one_source("mmap_size", None, Some(8192)), Ok(Some(8192)));
assert_eq!(
one_source("mmap_size", Some(8192), Some(8192)),
Ok(Some(8192))
);
assert_eq!(
one_source("mmap_size", Some(8192), Some(4096)),
Err(StatusCode::BadValue)
);
assert_eq!(
one_source("driver", Some("/dev/binder"), Some("/dev/vndbinder")),
Err(StatusCode::BadValue)
);
}
}