imsg-session 0.4.0

Session lifecycle — RFCOMM/iroh connect, OBEX handshake, reconnect, and MNS relay
Documentation
//! Shared fake-PBAP-server harness for `sync_contacts` tests, driven over a `tokio::io::duplex`
//! pair against a real (temp-dir) `Store`.

mod meta;
mod refresh;

use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use obex_core::headers::Header;
use obex_core::packet::{OpCode, Packet, PacketExtra};
use pbap_core::client::PbapClient;
use pbap_core::phonebook::PhonebookPath;
use secrecy::SecretBox;
use store::Store;

use super::{hex16, sync_contacts, Refresh, SyncReport};

const CONNECT_RSP: &[u8] = &[
    0xa0, 0x00, 0x1f, 0x10, 0x00, 0x0f, 0xa0, 0xcb, 0xdd, 0x20, 0x40, 0xd0, 0x4a, 0x00, 0x13, 0x79,
    0x61, 0x35, 0xf0, 0xf0, 0xc5, 0x11, 0xd8, 0x09, 0x66, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66,
];

const A: [u8; 16] = [0xAA; 16];
const B: [u8; 16] = [0xBB; 16];
const C: [u8; 16] = [0xCC; 16];
const D: [u8; 16] = [0xDD; 16];

async fn fake_store() -> anyhow::Result<(Store, tempfile::TempDir)> {
    let dir = tempfile::tempdir()?;
    let key: SecretBox<[u8; 32]> = SecretBox::new(Box::new([0u8; 32]));
    let s = Store::open(dir.path().join("test.db"), key).await?;
    Ok((s, dir))
}

/// Connects a `PbapClient` over an in-memory duplex, replies CONNECT then `responses` in order,
/// and runs `sync_contacts` against `db` concurrently. Returns once both sides finish.
async fn run_sync(db: &Store, responses: &[Bytes]) -> anyhow::Result<anyhow::Result<SyncReport>> {
    let (client_io, server_io) = tokio::io::duplex(8192);
    let (server_result, report) = futures::join!(
        async {
            let mut srv = obex_core::wrap(server_io);
            let _ = srv.next().await;
            srv.send(Bytes::from_static(CONNECT_RSP)).await?;
            for r in responses {
                let _ = srv.next().await;
                srv.send(r.clone()).await?;
            }
            Ok::<(), anyhow::Error>(())
        },
        async {
            let mut client = PbapClient::connect(client_io).await?;
            sync_contacts(&mut client, db, PhonebookPath::Pb).await
        },
    );
    server_result?;
    Ok(report)
}

fn tlv(tag: u8, value: &[u8]) -> Vec<u8> {
    let mut out = vec![tag, u8::try_from(value.len()).unwrap_or(u8::MAX)];
    out.extend_from_slice(value);
    out
}

fn metadata_rsp(db_id: [u8; 16], primary: [u8; 16], secondary: [u8; 16]) -> anyhow::Result<Bytes> {
    let mut app_params = tlv(0x0D, &db_id);
    app_params.extend(tlv(0x0A, &primary));
    app_params.extend(tlv(0x0B, &secondary));
    Ok(Packet {
        opcode: OpCode::Ok,
        extra: PacketExtra::None,
        headers: vec![Header::AppParams(Bytes::from(app_params))],
    }
    .encode()?)
}

fn body_rsp(body: &[u8]) -> anyhow::Result<Bytes> {
    Ok(Packet {
        opcode: OpCode::Ok,
        extra: PacketExtra::None,
        headers: vec![Header::EndOfBody(Bytes::copy_from_slice(body))],
    }
    .encode()?)
}

// collect_response maps any non-OK, non-CONTINUE opcode to PbapError::ServerError, which is
// what refresh_contacts counts as a failed pull
fn error_rsp() -> anyhow::Result<Bytes> {
    Ok(Packet { opcode: OpCode::NotFound, extra: PacketExtra::None, headers: vec![] }.encode()?)
}

fn list_body(handles: &[&str]) -> Vec<u8> {
    use std::fmt::Write as _;
    let mut body = String::from("<vCard-listing>");
    for h in handles {
        let _ = write!(body, "<card handle=\"{h}\" name=\"x\"/>");
    }
    body.push_str("</vCard-listing>");
    body.into_bytes()
}

fn vcard(uid: Option<&str>, name: &str, tel: &str) -> Vec<u8> {
    let uid_line = uid.map(|u| format!("UID:{u}\r\n")).unwrap_or_default();
    format!("BEGIN:VCARD\r\nVERSION:3.0\r\nFN:{name}\r\n{uid_line}TEL:{tel}\r\nEND:VCARD\r\n")
        .into_bytes()
}