rsbinder 0.12.0

Pure-Rust Binder IPC for Linux, Android, and macOS — kernel binder plus a libbinder-compatible RPC (binder-over-socket) transport.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
// Copyright 2026 Jeff Kim <hiking90@gmail.com>
// SPDX-License-Identifier: Apache-2.0

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};

/// Options that are not part of the [`Uri`]. Set through
/// [`Client::open_with`]. An option that does not apply to the
/// transport is [`StatusCode::BadValue`] (logged), never ignored.
#[derive(Default)]
#[non_exhaustive]
pub struct ClientOptions {
    /// `tls://`: TLS client config (required) and the server name to
    /// verify (defaults to the URI host).
    #[cfg(feature = "rpc-tls")]
    pub tls: Option<std::sync::Arc<crate::rpc::rustls::ClientConfig>>,
    #[cfg(feature = "rpc-tls")]
    pub tls_server_name: Option<String>,
    /// RPC (android13plus profile): a non-empty id is refused with
    /// [`StatusCode::BadValue`]. It would open a second client session
    /// on a server session that another client session founded, with
    /// its own oneway numbering, binder addresses and lifetime (AOSP has
    /// no public entry for this either). Add a connection to a session
    /// with `RpcSession::add_outgoing_connection_with_config` on that
    /// session. Empty or `None` opens a new session.
    #[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>>,
    /// RPC (android13plus profile, every RPC transport): number of
    /// outgoing connections to open (AOSP `setupClient` fan-out).
    pub outgoing_connections: Option<u32>,
    /// RPC (android13plus profile, every RPC transport): number of
    /// incoming (callback) connections to open — AOSP
    /// `setMaxIncomingThreads`. Needed for the server to call this
    /// client's callbacks from outside a handler, and for any oneway
    /// callback. Each one is a further
    /// connection to the same endpoint (a `tls://` one is its own TLS
    /// session), because the android-13+ wire lets the server start a
    /// call only on a connection the client reads outside its own reply
    /// wait.
    ///
    /// Setting this `> 0` makes the resulting [`Client`] one that must be
    /// shut down explicitly (`client.session().unwrap().close_session()`):
    /// the serving threads keep the session alive, so dropping every
    /// handle reclaims nothing. Like every connection of the session, the
    /// loss of one ends the whole session — including the server's own
    /// reply timeout elapsing on a slow callback handler, which closes the
    /// founding connection and fires `binder_died` on every proxy although
    /// the handler was only slow. See
    // The target only exists with `rpc`, so only link it then.
    #[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>,
    /// RPC: FD transport mode to negotiate. Requesting
    /// [`FileDescriptorTransportMode::Unix`](crate::rpc::FileDescriptorTransportMode)
    /// is rejected on a transport that cannot carry fds — see
    /// [`Endpoint::supports_fd_passing`].
    #[cfg(feature = "rpc")]
    pub fd_mode: Option<crate::rpc::FileDescriptorTransportMode>,
    /// RPC: how long the server may go without answering before this end
    /// counts it as broken (`RpcSession::set_timeout`, plan 2-24 D4 and D6).
    ///
    /// - **Connecting**: it bounds each step of connecting as a whole —
    ///   `connect(2)` (for `tls://`, every address the host name resolves to
    ///   together: each try gets an even share of the time left but at least
    ///   2 s, or all of it when less is left, so an address refused at once
    ///   leaves its share to the next and one slower to accept than its share
    ///   is given up for the next), the TLS
    ///   handshake, and the android-13+ handshake — on the founding
    ///   connection and on every fan-out or incoming attach. A peer that
    ///   answers one byte at a time does not stretch a step. It is not a
    ///   budget for the phase as a whole, so `open` can take a multiple of
    ///   it before returning; what it guarantees is that no single step
    ///   waits on a silent peer forever. Three steps are out of its reach:
    ///   resolving the host name, which blocks in the platform's resolver;
    ///   on Linux and Android a
    ///   `unix://` or `unix-abstract://` connect into a listener whose
    ///   accept queue is full, which waits until the server accepts; and a
    ///   `vsock://` connect, which the kernel bounds by its own connect
    ///   timeout instead (`SO_VM_SOCKETS_CONNECT_TIMEOUT`, 2 s by default).
    /// - **The session**: applied as soon as the session exists, so it also
    ///   bounds the round trips `open` makes after that point (the r34
    ///   fd-mode negotiation, the `GET_MAX_THREADS` / `GET_SESSION_ID`
    ///   exchanges a multi-connection setup needs), and then every reply
    ///   wait, send and liveness check. An expired reply wait ends the
    ///   session.
    ///
    /// `None` (default) waits forever, so a peer that accepts the socket
    /// and then writes nothing hangs `open`. Set it whenever the peer is
    /// untrusted or merely unreliable. `Some(Duration::ZERO)` is no
    /// deadline either, as `RpcSession::set_timeout` treats it; unlike the
    /// deprecated `handshake_timeout` (`rpc` feature), `open` does not
    /// refuse it.
    pub timeout: Option<Duration>,
    /// RPC: deadline for the connection **handshake**, in place of
    /// [`timeout`](Self::timeout) for that phase. The r34 wire (no
    /// `?profile=`) has no handshake, so on a plain r34 endpoint other than
    /// `tls://` `open` refuses it with
    /// [`StatusCode::BadValue`], and it
    /// refuses `Some(Duration::ZERO)` the same way.
    #[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>,
    /// Kernel: `?driver=` equivalent. The device is fixed process-wide by
    /// whoever initializes `ProcessState` first, so a *different* path here
    /// is [`StatusCode::BadValue`] at
    /// [`open`](Client::open) — see
    /// [`ServeOptions::threads`](super::ServeOptions::threads) for the same
    /// rule on the server side.
    pub driver: Option<std::path::PathBuf>,
    /// Kernel: `?mmap=` equivalent — the size of the mapping this
    /// process receives into. A client is a receiver too: the reply to
    /// every call it makes is allocated out of *its* mapping, so a
    /// client expecting replies larger than the ~1 MB default raises it
    /// here. Unlike [`ServeOptions::mmap_size`](super::ServeOptions::mmap_size),
    /// this one takes effect — [`open`](Client::open) reads it before it
    /// initializes `ProcessState`. A *different* size than the one
    /// already in force is
    /// [`StatusCode::BadValue`], as with
    /// [`driver`](Self::driver).
    pub mmap_size: Option<usize>,
}

/// A resolver for named services on one endpoint: the system service
/// manager (`binder://`) or one RPC session.
///
/// Proxies returned by [`get`](Self::get) stay valid after the `Client`
/// is dropped — an RPC proxy holds its session; keep the `Client` only
/// to issue more lookups on the same session. The kernel form also
/// starts the binder thread pool so callbacks, death notifications and
/// registration waits are delivered promptly.
///
/// **One exception to "just drop it".** A client opened with
/// [`ClientOptions::incoming_connections`] `> 0` owns the threads
/// serving those connections, and each of them holds the session alive:
/// dropping the `Client` *and* every proxy reclaims nothing, so the
/// threads, the session and both ends' sockets survive until the process
/// exits. Call `client.session().unwrap().close_session()` when you are done
/// with such a client.
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),
}

/// Merges a [`ClientOptions`] field with its URI key; differing values are `BadValue`.
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> {
    // Here and not in `open_staged`: the reconnect helper validates once at `build`.
    uri.validate()?;
    open_staged(uri, o).map_err(|(_, code)| code)
}

/// Where [`open_staged`] failed: `Setup` does no I/O and fails again; `Connect` may recover.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OpenStage {
    Setup,
    // Opening the kernel endpoint does no transport I/O; only RPC connects.
    #[cfg(feature = "rpc")]
    Connect,
}

/// [`new_client`] reporting which stage failed; the reconnect helper retries only `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)
}

/// The kernel endpoint and every option check; RPC endpoints end here without the `rpc` feature.
#[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,
            })
        }
        // RPC endpoints reach here only without the `rpc` feature (`open_staged`).
        _ => {
            log::error!(
                "rsbinder::Client::open: {:?} needs the `rpc` feature",
                uri.endpoint
            );
            Err(StatusCode::InvalidOperation)
        }
    }
}

/// Every check an RPC open makes before any I/O, and the config it then connects with.
#[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
    };

    // Refuse zero here, where the option can be named; its downstream consumers reject it too.
    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);
    // `is_some()`, not the value: a set option this endpoint lacks is `BadValue`, never ignored.
    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);
    }
    // r34 has no handshake to bound (see `ClientOptions::handshake_timeout`); `tls://` does.
    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);
    }

    // TLS only on `tls://`: elsewhere it would connect in plaintext the caller thinks encrypted.
    #[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);
    }
    // The session layer's own pre-connect refusal, made here so it counts as setup.
    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))
}

/// The I/O half of an RPC open: connect, handshake, and on r34 the fd-mode transaction.
#[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 {
        // One connection each, as AOSP `setupClient` calls `connectAndInit`, on every transport.
        return RpcSession::setup_client_android13plus_with_config(cfg);
    }

    // r34 wire: no handshake, so the session is built on the connection itself.
    let session =
        RpcSession::new(cfg.connect_once()?, AddressSpace::Initiator).map_err(StatusCode::from)?;
    // Before the negotiation below: that transaction reads this value when it runs.
    session.set_timeout(o.timeout);
    // r34 negotiates the FD mode by a transaction after connect (versioned: in the handshake).
    if let Some(mode) = o.fd_mode {
        session.negotiate_fd_transport(mode)?;
    }
    Ok(session)
}

/// Session config offering `max_version` (ignored on r34); refuses what the build lacks early.
#[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) => {
            // `UnixStream::connect`'s refusal (same code), made before connecting.
            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;
                // `UnixTransport::connect_abstract`'s refusal (same code), made before connecting.
                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);
                // `TlsTransport::connect_stream`'s refusal (same code), made before connecting.
                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 {
    /// Open a resolver on `uri`, which must not name a service (that is
    /// [`connect`](super::connect)). See [`ClientOptions`] for what is not
    /// part of the [`Uri`].
    pub fn open(uri: impl Into<Uri>) -> Result<Client> {
        new_client(uri.into(), ClientOptions::default())
    }

    /// [`open`](Self::open) with [`ClientOptions`]. The closure also sees
    /// the [`Endpoint`], so an option that applies to only some transports
    /// can be set conditionally when `uri` came from a string.
    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)
    }

    /// The endpoint this client is connected to. Mirrors
    /// [`Server::endpoint`](super::Server::endpoint).
    pub fn endpoint(&self) -> &Endpoint {
        &self.endpoint
    }

    /// Resolve `name` and cast it to `T`. Kernel: blocks until the
    /// service is registered (AOSP `waitForService`); RPC: one lookup in
    /// the server's directory ([`StatusCode::NameNotFound`] if absent).
    pub fn get<T: FromIBinder + ?Sized>(&self, name: &str) -> Result<Strong<T>> {
        FromIBinder::try_from(self.binder(name)?)
    }

    /// Non-waiting form of [`get`](Self::get): `Ok(None)` when `name` is
    /// not (yet) registered.
    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),
            },
        }
    }

    /// Resolve `name` as an untyped binder (waiting semantics of
    /// [`get`](Self::get)).
    pub fn binder(&self, name: &str) -> Result<SIBinder> {
        match &self.inner {
            // The service manager's own failure, not `NameNotFound`: the name was never looked up.
            Inner::Kernel => crate::hub::default()?
                .wait_for_service(name)
                .ok_or(StatusCode::NameNotFound),
            #[cfg(feature = "rpc")]
            Inner::Rpc(s) => s.get_service(name),
        }
    }

    /// What this client's transport can do, as a
    /// [`TransportCaps`](crate::TransportCaps) set.
    ///
    /// Kernel is always the full set. An RPC client reports what its
    // The target only exists with `rpc`, so only link it then.
    #[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)"
    )]
    /// for why that is a snapshot, and
    /// [`Endpoint::static_caps`] for what the transport could offer at
    /// best.
    ///
    /// ```no_run
    /// # fn f() -> rsbinder::Result<()> {
    /// use rsbinder::TransportCaps;
    /// let client = rsbinder::Client::open(rsbinder::Endpoint::unix("/tmp/x.sock"))?;
    /// // Fails here, naming the option to set, rather than on the first
    /// // callback.
    /// client.caps().require(TransportCaps::CALLBACKS, "event subscription")?;
    /// # Ok(()) }
    /// ```
    pub fn caps(&self) -> crate::TransportCaps {
        match &self.inner {
            Inner::Kernel => crate::TransportCaps::KERNEL,
            #[cfg(feature = "rpc")]
            Inner::Rpc(s) => s.caps(),
        }
    }

    /// The underlying RPC session (`None` for kernel binder).
    #[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::*;

    /// A refusal that does no I/O is `Setup`; a socket that is not there yet is `Connect`.
    #[cfg(feature = "rpc")]
    #[test]
    #[allow(deprecated)] // Sets `session_id` to pin its refusal.
    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"
        );
    }

    /// Option vs URI key: agreeing or single values pass; a conflict is refused, not resolved.
    #[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)
        );
    }
}