//! Share-code wrapping: carrying the host's stable friend id alongside the //! one-shot video ticket. //! //! A bare video ticket identifies only the host's *ephemeral* video endpoint, //! so two people who meet over one can't learn each other's stable friend id — //! the thing the friends system needs. The GUI host therefore wraps its ticket //! with its control-plane [`EndpointId`]; the viewer unwraps it, dials the //! video ticket as before, and now also knows who to befriend (and announces //! itself back over the control plane so the host learns the viewer in turn). //! //! Format: `pixelpassF1:.`. Both the id and the //! ticket are base32 text with no `.`, so a single `.` separator is //! unambiguous. [`unwrap`] is lenient: anything without the prefix is treated //! as a bare ticket, so a plain CLI ticket pasted into the GUI still works (it //! just offers no friend option). The host name isn't carried here — the //! viewer's announcement triggers a name exchange over the control plane. use std::str::FromStr; use iroh::EndpointId; /// Prefix marking a wrapped friend code. The `F1` is the wrap-format version, /// bumped if the layout ever changes. const MAGIC: &str = "pixelpassF1:"; /// Wrap a bare ticket with the host's control id, for display/copy/QR. pub fn wrap(host_id: EndpointId, ticket: &str) -> String { format!("{MAGIC}{host_id}.{ticket}") } /// Split an input into `(host control id if it was a wrapped code, bare /// ticket)`. A bare or unrecognised input yields `(None, trimmed input)` so the /// viewer path stays identical to before for plain tickets. pub fn unwrap(code: &str) -> (Option, String) { let code = code.trim(); if let Some(rest) = code.strip_prefix(MAGIC) && let Some((id_str, ticket)) = rest.split_once('.') && let Ok(id) = EndpointId::from_str(id_str) && !ticket.is_empty() { return (Some(id), ticket.to_string()); } (None, code.to_string()) } #[cfg(test)] mod tests { use super::*; fn sample_id() -> EndpointId { iroh::SecretKey::generate().public() } #[test] fn wrap_unwrap_round_trips() { let id = sample_id(); let ticket = "endpointaabwxjexzensznfvuudiapn5tyzws3angd2merarm"; let code = wrap(id, ticket); let (got_id, got_ticket) = unwrap(&code); assert_eq!(got_id, Some(id)); assert_eq!(got_ticket, ticket); } #[test] fn bare_ticket_passes_through() { let ticket = "endpointaabwxjexzensznfvuudiapn5tyzws3angd2merarm"; let (id, got) = unwrap(ticket); assert_eq!(id, None); assert_eq!(got, ticket); } #[test] fn trims_surrounding_whitespace() { let ticket = "endpointaabwxjex"; let (id, got) = unwrap(&format!(" {} ", wrap(sample_id(), ticket))); assert!(id.is_some()); assert_eq!(got, ticket); } #[test] fn malformed_wrapped_code_falls_back_to_bare() { // Prefix present but the id isn't a valid EndpointId → treat the whole // thing as a (doomed) bare ticket rather than panicking. let (id, got) = unwrap("pixelpassF1:not-an-id.endpointaa"); assert_eq!(id, None); assert_eq!(got, "pixelpassF1:not-an-id.endpointaa"); } }