Skip to main content

iris_chat_core/
qr.rs

1use qrcode::{EcLevel, QrCode};
2
3#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
4pub struct QrCodeMatrix {
5    /// Square module count (one side of the matrix).
6    pub size: u32,
7    /// "1" = dark module, "0" = light module. Length == size * size.
8    /// We use a string instead of Vec<bool> to keep the FFI surface cheap.
9    pub modules: String,
10}
11
12/// Render `text` to a QR-code module matrix. Returns a square matrix encoded
13/// as `1`/`0` characters in row-major order. Returns `None` for inputs that
14/// don't fit at the medium error-correction level.
15#[uniffi::export]
16pub fn encode_text_qr(text: String) -> Option<QrCodeMatrix> {
17    let trimmed = text.trim();
18    if trimmed.is_empty() {
19        return None;
20    }
21    let code = QrCode::with_error_correction_level(trimmed.as_bytes(), EcLevel::M).ok()?;
22    let width = code.width();
23    let cells = code.to_colors();
24    let mut modules = String::with_capacity(width * width);
25    for color in cells {
26        modules.push(if color == qrcode::Color::Dark {
27            '1'
28        } else {
29            '0'
30        });
31    }
32    Some(QrCodeMatrix {
33        size: width as u32,
34        modules,
35    })
36}
37
38#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
39pub struct DeviceApprovalQrPayload {
40    pub owner_input: String,
41    pub device_input: String,
42}
43
44#[uniffi::export]
45pub fn encode_device_approval_qr(_owner_input: String, _device_input: String) -> String {
46    String::new()
47}
48
49#[uniffi::export]
50pub fn decode_device_approval_qr(raw: String) -> Option<DeviceApprovalQrPayload> {
51    nostr_double_ratchet::parse_compact_device_link_request(&raw)
52        .ok()
53        .map(|request| DeviceApprovalQrPayload {
54            owner_input: String::new(),
55            device_input: request.device_app_key_pubkey.to_hex(),
56        })
57}
58
59#[cfg(test)]
60mod tests {
61    use super::{decode_device_approval_qr, encode_device_approval_qr, DeviceApprovalQrPayload};
62    use nostr_sdk::Keys;
63
64    #[test]
65    fn removed_device_approval_qr_encoder_returns_empty() {
66        let encoded = encode_device_approval_qr("npub-owner".into(), "npub-device".into());
67        assert_eq!(encoded, "");
68    }
69
70    #[test]
71    fn compact_device_approval_qr_rejects_wrong_inputs() {
72        assert!(decode_device_approval_qr("".into()).is_none());
73        assert!(decode_device_approval_qr("npub1plainvalue".into()).is_none());
74        assert!(decode_device_approval_qr("https://example.com".into()).is_none());
75        assert!(decode_device_approval_qr("not-a-compact-link-code".into()).is_none());
76        assert!(
77            decode_device_approval_qr(format!("{}.{}.extra", "1".repeat(64), "1".repeat(64)))
78                .is_none()
79        );
80    }
81
82    #[test]
83    fn compact_device_link_qr_decodes_to_device() {
84        let device = Keys::generate();
85        let request = Keys::generate();
86        let encoded = format!(
87            "{}.{}",
88            device.public_key().to_hex(),
89            request.secret_key().to_secret_hex()
90        );
91
92        let decoded = decode_device_approval_qr(encoded.clone()).expect("decode compact request");
93        assert_eq!(
94            decoded,
95            DeviceApprovalQrPayload {
96                owner_input: String::new(),
97                device_input: device.public_key().to_hex(),
98            }
99        );
100
101        let prefixed = format!("prefix:{encoded}");
102        assert!(decode_device_approval_qr(prefixed).is_none());
103    }
104}