Skip to main content

plat/
linux_portal.rs

1// linux_portal -- xdg-desktop-portal over a minimal D-Bus client (raw socket, SASL EXTERNAL,
2// little-endian marshalling): the file dialog, OpenURI / OpenDirectory, Print
3//                                                            [src/plat/linux/portal.s]
4//
5//   file_dialog(flags, path, exts) -> (1 chosen / 0 cancelled / -1 no portal, index of the
6//        chosen extension entry).  Same contract as on Windows (win_filedlg.rs): flags FD_SAVE
7//        | index << FD_EXT_SHIFT (FD_FOLDER: OpenFile with "directory", portal version 3),
8//        path in / out (UTF-8, '/'; at most PATH_CAP - 1 bytes out),
9//        exts ["png=PNG-Bild", "jpg;jpeg=JPEG-Bild"] (an empty entry ends the list).
10//   open_url(url) -> 0 / -1         a web page in the default application: OpenURI.OpenURI;
11//                                   jfgui_OPEN_DRYRUN=1 only prints it
12//   open_folder(path) -> 0 / -1     show a folder / a file in the file manager:
13//        OpenURI.OpenDirectory with an O_PATH descriptor (SCM_RIGHTS), fallback
14//        OpenURI.OpenURI("file://...").  The Response is awaited for OF_WAIT ms (the window
15//        keeps being serviced): code 0 -> 0, 1 / 2 -> -1; without an answer by then the
16//        request stays alive on its connection until the next call and 0 is returned.  No
17//        bus / no portal / no such path -> -1.  jfgui_OPEN_DRYRUN=1: printed, nothing sent.
18//   (crate) portal_print_prepare / portal_print_send / portal_print_end  org.freedesktop.portal
19//        .Print for linux_print.rs (PreparePrint with the job's paper / orientation, the page
20//        setup of the answer; Print with the PDF as a descriptor)
21//
22// The dialogs belong to the portal's process.  While one is open the display keeps being
23// serviced (wait); input events of that time are dropped, window events (resize, close,
24// focus, dpi, paint) are kept and queued again afterwards.
25//
26// State: the assembly's globals (receive buffer, body / message builders, token, request
27// path, ...) are one Bus record (module state, UI thread only).  Messages are byte-identical
28// to the assembly's (serials, field order, padding, token "jfgui<n>").
29
30use alloc::format;
31use alloc::string::String;
32use alloc::vec;
33use alloc::vec::Vec;
34use core::cell::RefCell;
35
36use rt::os::{syscall6, AT_FDCWD, O_CLOEXEC, O_PATH, SYS_CLOSE, SYS_CONNECT, SYS_GETUID, SYS_OPENAT, SYS_READ, SYS_SENDMSG, SYS_SOCKET, SYS_WRITE};
37
38use crate::{
39    BK_X11, E_CLOSE, E_DPI, E_FOCUS_IN, E_FOCUS_OUT, E_PAINT, E_RESIZE, Event, FD_EXT_SHIFT, FD_FOLDER, FD_SAVE, PAPER_N,
40    PJF_LANDSCAPE, PPage, PrintJob,
41};
42
43const AF_UNIX: u16 = 1;
44const SOCK_STREAM_CLOEXEC: usize = 1 | 0x80000;
45const POLLIN: u16 = 1;
46const EINTR: isize = -4;
47const MSG_NOSIGNAL: usize = 0x4000;
48
49const MB_CAP: usize = 32768; // body builder (file filters: every letter as [xX])
50const MM_CAP: usize = MB_CAP + 4096; // whole message
51const RB_CAP: usize = 65536; // receive buffer
52const URI_CAP: usize = 2048;
53const UID_CAP: usize = 16;
54const HDR_CAP: usize = MM_CAP - MB_CAP - 32; // header fields end below this
55const T_HELLO: i32 = 2000; // ms
56const T_CALL: i32 = 20000; // ms until the portal answers the call (activation)
57const KEEP_MAX: usize = 32; // window events kept while the dialog is open
58const MSG_CALL: u8 = 1;
59const MSG_RETURN: u8 = 2;
60const MSG_ERROR: u8 = 3;
61const MSG_SIGNAL: u8 = 4;
62const HF_PATH: u8 = 1;
63const HF_MEMBER: u8 = 3;
64const HF_REPLY: u8 = 5;
65const MP_NAME_END: usize = 200; // req_path: the name part ends here (256 bytes total)
66const SKIP_DEPTH_MAX: u32 = 32;
67const OF_CAP: usize = 1024; // open_folder: absolute path
68const OF_WAIT: u32 = 1500; // ms to wait for the portal's Response
69const PRN_T_DIALOG: u32 = 3_600_000; // ms the user may spend in the print dialog
70const PRN_T_PRINT: u32 = 600_000; // ms until the portal answers Print
71/// file_dialog: the path capacity (FD_PATH of ui_filedlg.s)
72pub const PATH_CAP: usize = 1040;
73
74const S_BUS_NAME: &[u8] = b"org.freedesktop.DBus";
75const S_BUS_PATH: &[u8] = b"/org/freedesktop/DBus";
76const S_HELLO: &[u8] = b"Hello";
77const S_ADDMATCH: &[u8] = b"AddMatch";
78const S_MATCH1: &[u8] = b"type='signal',interface='org.freedesktop.portal.Request',member='Response',path='";
79// the portal went away (crashed, restarted by an update): its Response never comes
80const S_MATCH_OWNER: &[u8] =
81    b"type='signal',sender='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.freedesktop.portal.Desktop'";
82const S_NAME_OWNER_CHANGED: &[u8] = b"NameOwnerChanged";
83const S_PORTAL_NAME: &[u8] = b"org.freedesktop.portal.Desktop";
84const S_PORTAL_PATH: &[u8] = b"/org/freedesktop/portal/desktop";
85const S_FILECHOOSER: &[u8] = b"org.freedesktop.portal.FileChooser";
86const S_RESPONSE: &[u8] = b"Response";
87const S_OPENURI: &[u8] = b"org.freedesktop.portal.OpenURI";
88const S_PRN_IFACE: &[u8] = b"org.freedesktop.portal.Print";
89const S_ALL_FILES: &[u8] = "Alle unterstützten Dateien".as_bytes();
90const S_HEXD: &[u8] = b"0123456789abcdef";
91const S_HEXU: &[u8] = b"0123456789ABCDEF";
92
93// PAPER_*: mm (portrait), PWG name, display name
94static PRN_PAPERS: [(f64, f64, &[u8], &[u8]); 5] = [
95    (210.0, 297.0, b"iso_a4", b"A4"),
96    (215.9, 279.4, b"na_letter", b"US Letter"),
97    (148.0, 210.0, b"iso_a5", b"A5"),
98    (215.9, 355.6, b"na_legal", b"US Legal"),
99    (297.0, 420.0, b"iso_a3", b"A3"),
100];
101static PRN_DKEYS: [&[u8]; 6] = [b"Width", b"Height", b"MarginTop", b"MarginBottom", b"MarginLeft", b"MarginRight"];
102const PRN_MM_PT: f64 = 2.834645669291339; // 72 / 25.4
103
104/// a header field value of the current message (msg_field)
105enum Field {
106    None,
107    Str(usize, usize), // bytes [start, end) of rbuf (NUL at end)
108    U32(u32),
109}
110
111struct Bus {
112    fd: i32,
113    rbuf: Vec<u8>, // RB_CAP + 16
114    rlen: usize,
115    mlen: usize, // current message length (msg_ready)
116    mhdr: usize, // its body offset
117    rp: usize,   // read position
118    body: Vec<u8>,
119    b_over: bool, // the body did not fit: msg_send refuses it
120    msg: Vec<u8>,
121    m_path: &'static [u8],
122    m_iface: &'static [u8],
123    m_member: &'static [u8],
124    m_dest: &'static [u8],
125    m_sig: Option<&'static [u8]>,
126    m_nfds: u32, // descriptors sent with the next msg_send
127    m_sendfd: i32,
128    token_n: u32,
129    token: Vec<u8>,
130    req_path: Vec<u8>,
131    handle: Vec<u8>,
132    uri: Vec<u8>,
133    filter: Vec<u8>,
134    name: Vec<u8>,
135    folder: Vec<u8>,
136    keep: Vec<Event>,
137    skip_depth: u32,
138    neg_fd: bool, // bus_open negotiates descriptor passing
139    fd_ok: bool,  // ... and the bus agreed
140    of_keep: i32, // connection kept for a pending open_folder request (0 none)
141    prn_open: bool,
142    prn_uniq: Vec<u8>,
143}
144
145struct Global(RefCell<Option<Bus>>);
146// SAFETY: the portal is used from the UI thread only (as the assembly's plain globals);
147// RefCell checks the (non-reentrant) borrows at run time.
148unsafe impl Sync for Global {}
149static BUS: Global = Global(RefCell::new(None));
150
151fn with_bus<R>(f: impl FnOnce(&mut Bus) -> R) -> R {
152    let mut g = BUS.0.borrow_mut();
153    let b = g.get_or_insert_with(Bus::new);
154    f(b)
155}
156
157// ---------------------------------------------------------------- string helpers
158
159/// the bytes of a C string: up to the first NUL
160fn cstr(s: &[u8]) -> &[u8] {
161    s.iter().position(|&c| c == 0).map_or(s, |n| &s[..n])
162}
163
164/// str_copy(dst, s, cap): at most cap - 1 bytes, never splitting a UTF-8 sequence
165fn copy_cap(s: &[u8], cap: usize) -> Vec<u8> {
166    let s = cstr(s);
167    if cap == 0 {
168        return Vec::new();
169    }
170    let mut n = s.len();
171    if n > cap - 1 {
172        n = cap - 1;
173        while n > 0 && s[n] & 0xC0 == 0x80 {
174            n -= 1;
175        }
176    }
177    s[..n].to_vec()
178}
179
180/// str_cat(dst, s, cap)
181fn cat_cap(d: &mut Vec<u8>, s: &[u8], cap: usize) {
182    if d.len() >= cap {
183        return;
184    }
185    let t = copy_cap(s, cap - d.len());
186    d.extend_from_slice(&t);
187}
188
189fn sys_close(fd: i32) {
190    // SAFETY: closing a descriptor we own.
191    unsafe { syscall6(SYS_CLOSE, fd as usize, 0, 0, 0, 0, 0) };
192}
193
194fn env_is_1(name: &str) -> bool {
195    crate::env(name).is_some_and(|v| v.as_bytes().first() == Some(&b'1'))
196}
197
198/// "x11:<XID in hex>" under X11 (the chooser is modal over the app), "" under Wayland
199fn parent() -> Vec<u8> {
200    if crate::disp_backend() == BK_X11 {
201        format!("x11:{:x}", crate::x11s::main_xid()).into_bytes()
202    } else {
203        Vec::new()
204    }
205}
206
207impl Bus {
208    fn new() -> Bus {
209        Bus {
210            fd: -1,
211            rbuf: vec![0; RB_CAP + 16],
212            rlen: 0,
213            mlen: 0,
214            mhdr: 0,
215            rp: 0,
216            body: Vec::with_capacity(MB_CAP),
217            b_over: false,
218            msg: Vec::with_capacity(MM_CAP),
219            m_path: S_BUS_PATH,
220            m_iface: S_BUS_NAME,
221            m_member: S_HELLO,
222            m_dest: S_BUS_NAME,
223            m_sig: None,
224            m_nfds: 0,
225            m_sendfd: -1,
226            token_n: 0,
227            token: Vec::new(),
228            req_path: Vec::new(),
229            handle: Vec::new(),
230            uri: Vec::new(),
231            filter: Vec::new(),
232            name: Vec::new(),
233            folder: Vec::new(),
234            keep: Vec::new(),
235            skip_depth: 0,
236            neg_fd: false,
237            fd_ok: false,
238            of_keep: 0,
239            prn_open: false,
240            prn_uniq: Vec::new(),
241        }
242    }
243
244    // ------------------------------------------------------------ connection
245
246    /// connect and authenticate -> true  [bus_open]
247    fn bus_open(&mut self) -> bool {
248        self.fd = -1;
249        self.rlen = 0;
250        let mut sa = [0u8; 112];
251        sa[..2].copy_from_slice(&AF_UNIX.to_le_bytes());
252        // DBUS_SESSION_BUS_ADDRESS "unix:path=..." / "unix:abstract=...", else /run/user/UID/bus
253        let mut alen = 0usize;
254        if let Some(a) = crate::env("DBUS_SESSION_BUS_ADDRESS").filter(|a| a.starts_with("unix:")) {
255            let b = cstr(a.as_bytes());
256            let mut i = 5;
257            loop {
258                let rest = &b[i.min(b.len())..];
259                let (abstr, skip) = if rest.starts_with(b"path=") {
260                    (false, 5)
261                } else if rest.starts_with(b"abstract=") {
262                    (true, 9)
263                } else {
264                    // skip "key=value," and try the next key
265                    match rest.iter().position(|&c| c == b',') {
266                        Some(k) => {
267                            i += k + 1;
268                            continue;
269                        }
270                        None => break,
271                    }
272                };
273                let mut o = if abstr { 3 } else { 2 };
274                for &c in &rest[skip..] {
275                    if c == b',' {
276                        break;
277                    }
278                    if o >= 108 {
279                        return false;
280                    }
281                    sa[o] = c;
282                    o += 1;
283                }
284                alen = o;
285                break;
286            }
287        }
288        if alen == 0 {
289            // SAFETY: getuid has no arguments.
290            let uid = unsafe { syscall6(SYS_GETUID, 0, 0, 0, 0, 0, 0) } as u32;
291            let p = copy_cap(format!("/run/user/{uid}/bus").as_bytes(), 100);
292            sa[2..2 + p.len()].copy_from_slice(&p);
293            alen = p.len() + 2;
294        }
295        // SAFETY: socket / connect with a sockaddr_un of alen bytes.
296        let fd = unsafe { syscall6(SYS_SOCKET, AF_UNIX as usize, SOCK_STREAM_CLOEXEC, 0, 0, 0, 0) };
297        if fd < 0 {
298            return false;
299        }
300        self.fd = fd as i32;
301        // SAFETY: as above.
302        if unsafe { syscall6(SYS_CONNECT, fd as usize, sa.as_ptr() as usize, alen, 0, 0, 0) } != 0 {
303            self.bus_close();
304            return false;
305        }
306        // SASL: "\0AUTH EXTERNAL <hex of the uid digits>\r\n"
307        // SAFETY: getuid has no arguments.
308        let uid = unsafe { syscall6(SYS_GETUID, 0, 0, 0, 0, 0, 0) } as u32;
309        let digits = copy_cap(format!("{uid}").as_bytes(), UID_CAP);
310        let mut line = b"AUTH EXTERNAL ".to_vec();
311        for &d in &digits {
312            line.push(S_HEXD[(d >> 4) as usize]);
313            line.push(S_HEXD[(d & 15) as usize]);
314        }
315        line.extend_from_slice(b"\r\n");
316        if !self.bus_write(&[0]) || !self.bus_write(&line) || !self.read_line() || &self.rbuf[..2] != b"OK" {
317            self.bus_close();
318            return false;
319        }
320        self.rlen = 0;
321        // descriptor passing for open_folder / Print: NEGOTIATE_UNIX_FD -> AGREE_UNIX_FD
322        self.fd_ok = false;
323        if self.neg_fd {
324            if !self.bus_write(b"NEGOTIATE_UNIX_FD\r\n") || !self.read_line() {
325                self.bus_close();
326                return false;
327            }
328            if &self.rbuf[..4] == b"AGRE" {
329                self.fd_ok = true;
330            }
331            self.rlen = 0;
332        }
333        if !self.bus_write(b"BEGIN\r\n") {
334            self.bus_close();
335            return false;
336        }
337        true
338    }
339
340    fn bus_close(&mut self) {
341        if self.fd >= 0 {
342            sys_close(self.fd);
343            self.fd = -1;
344        }
345    }
346
347    /// write all bytes -> false on an error  [bus_write]
348    fn bus_write(&mut self, mut b: &[u8]) -> bool {
349        while !b.is_empty() {
350            // SAFETY: write from a live slice.
351            let r = unsafe { syscall6(SYS_WRITE, self.fd as usize, b.as_ptr() as usize, b.len(), 0, 0, 0) };
352            if r == EINTR {
353                continue;
354            }
355            if r <= 0 {
356                return false;
357            }
358            b = &b[r as usize..];
359        }
360        true
361    }
362
363    /// write the bytes with descriptor fd attached (SCM_RIGHTS) to the first part  [bus_sendfd]
364    fn bus_sendfd(&mut self, b: &[u8], fd: i32) -> bool {
365        loop {
366            let iov = [b.as_ptr() as usize, b.len()];
367            let mut ctl = [0u8; 24];
368            ctl[..8].copy_from_slice(&20u64.to_le_bytes()); // cmsg_len = CMSG_LEN(4)
369            ctl[8..12].copy_from_slice(&1u32.to_le_bytes()); // SOL_SOCKET
370            ctl[12..16].copy_from_slice(&1u32.to_le_bytes()); // SCM_RIGHTS
371            ctl[16..20].copy_from_slice(&fd.to_le_bytes());
372            // msghdr: name, namelen, iov, iovlen, control, controllen, flags
373            let mh = [0usize, 0, iov.as_ptr() as usize, 1, ctl.as_ptr() as usize, 24, 0];
374            // SAFETY: a msghdr over live buffers of this frame.
375            let r = unsafe { syscall6(SYS_SENDMSG, self.fd as usize, mh.as_ptr() as usize, MSG_NOSIGNAL, 0, 0, 0) };
376            if r == EINTR {
377                continue;
378            }
379            if r <= 0 {
380                return false;
381            }
382            // the rest (if any) without the descriptor
383            return self.bus_write(&b[r as usize..]);
384        }
385    }
386
387    /// wait up to timeout_ms for bytes -> 1 arrived, 0 timeout, -1 error / closed  [bus_fill]
388    fn bus_fill(&mut self, timeout_ms: i32) -> i32 {
389        loop {
390            let mut pfd = [0u8; 8];
391            pfd[..4].copy_from_slice(&self.fd.to_le_bytes());
392            pfd[4..6].copy_from_slice(&POLLIN.to_le_bytes());
393            // SAFETY: one pollfd.
394            let r = unsafe { rt::os::poll(pfd.as_mut_ptr() as usize, 1, timeout_ms) };
395            if r == EINTR {
396                continue;
397            }
398            if r < 0 {
399                return -1;
400            }
401            if r == 0 {
402                return 0;
403            }
404            if self.rlen >= RB_CAP {
405                return -1;
406            }
407            let room = RB_CAP - self.rlen;
408            // SAFETY: read into the free part of rbuf.
409            let n = unsafe {
410                syscall6(SYS_READ, self.fd as usize, self.rbuf.as_mut_ptr().add(self.rlen) as usize, room, 0, 0, 0)
411            };
412            if n == EINTR {
413                continue;
414            }
415            if n <= 0 || n as usize > room {
416                return -1;
417            }
418            self.rlen += n as usize;
419            return 1;
420        }
421    }
422
423    /// rbuf holds a line ending in "\r\n" (SASL)  [read_line]
424    fn read_line(&mut self) -> bool {
425        loop {
426            if self.rlen >= 2 && self.rbuf[..self.rlen].windows(2).any(|w| w == b"\r\n") {
427                return true;
428            }
429            if self.bus_fill(T_HELLO) != 1 {
430                return false;
431            }
432        }
433    }
434
435    // ------------------------------------------------------------ writing messages
436    // The body never grows past MB_CAP: a write that does not fit sets b_over instead, and
437    // msg_send refuses such a message.
438
439    fn body_reset(&mut self) {
440        self.body.clear();
441        self.b_over = false;
442    }
443
444    fn b_align(&mut self, n: usize) {
445        while self.body.len() & (n - 1) != 0 {
446            if self.body.len() >= MB_CAP {
447                self.b_over = true;
448                return;
449            }
450            self.body.push(0);
451        }
452    }
453
454    fn b_u8(&mut self, v: u8) {
455        if self.body.len() >= MB_CAP {
456            self.b_over = true;
457            return;
458        }
459        self.body.push(v);
460    }
461
462    fn b_u32(&mut self, v: u32) {
463        self.b_align(4);
464        if self.body.len() + 4 > MB_CAP {
465            self.b_over = true;
466            return;
467        }
468        self.body.extend_from_slice(&v.to_le_bytes());
469    }
470
471    fn b_f64(&mut self, v: f64) {
472        self.b_align(8);
473        if self.body.len() + 8 > MB_CAP {
474            self.b_over = true;
475            return;
476        }
477        self.body.extend_from_slice(&v.to_le_bytes());
478    }
479
480    /// 's' / 'o': u32 length, bytes, NUL
481    fn b_str(&mut self, s: &[u8]) {
482        let s = cstr(s);
483        self.b_u32(s.len() as u32);
484        self.b_raw0(s);
485    }
486
487    /// 'g': u8 length, bytes, NUL
488    fn b_sig(&mut self, s: &[u8]) {
489        let s = cstr(s);
490        self.b_u8(s.len() as u8);
491        self.b_raw0(s);
492    }
493
494    // the bytes and a NUL (nothing when they do not fit)
495    fn b_raw0(&mut self, s: &[u8]) {
496        if self.body.len() + s.len() + 1 > MB_CAP {
497            self.b_over = true;
498            return;
499        }
500        self.body.extend_from_slice(s);
501        self.body.push(0);
502    }
503
504    fn b_bytes(&mut self, s: &[u8]) {
505        if self.body.len() + s.len() > MB_CAP {
506            self.b_over = true;
507            return;
508        }
509        self.body.extend_from_slice(s);
510    }
511
512    /// -> (length slot, first element offset)
513    fn b_array_begin(&mut self, elem_align: usize) -> (usize, usize) {
514        self.b_u32(0);
515        let slot = self.body.len().wrapping_sub(4);
516        self.b_align(elem_align);
517        (slot, self.body.len())
518    }
519
520    /// patch the byte length (a slot past the body: nothing)
521    fn b_array_end(&mut self, (slot, start): (usize, usize)) {
522        let n = self.body.len().wrapping_sub(start) as u32;
523        if slot <= MB_CAP - 4 && slot + 4 <= self.body.len() {
524            self.body[slot..slot + 4].copy_from_slice(&n.to_le_bytes());
525        }
526    }
527
528    /// start an a{sv} entry: align 8, key, variant signature
529    fn b_dict_key(&mut self, key: &[u8], sig: &[u8]) {
530        self.b_align(8);
531        self.b_str(key);
532        self.b_sig(sig);
533    }
534
535    /// one header field (a field that would end past HDR_CAP marks the message as not
536    /// sendable)  [h_field]
537    fn h_field(&mut self, value: Option<&[u8]>, code: u8, typ: u8) {
538        let Some(v) = value else { return };
539        let v = cstr(v);
540        if self.msg.len() + v.len() + 24 > HDR_CAP {
541            self.b_over = true;
542            return;
543        }
544        while self.msg.len() & 7 != 0 {
545            self.msg.push(0);
546        }
547        self.msg.extend_from_slice(&[code, 1, typ, 0]);
548        if typ == b'g' {
549            self.msg.push(v.len() as u8);
550        } else {
551            while self.msg.len() & 3 != 0 {
552                self.msg.push(0);
553            }
554            self.msg.extend_from_slice(&(v.len() as u32).to_le_bytes());
555        }
556        self.msg.extend_from_slice(v);
557        self.msg.push(0);
558    }
559
560    /// a method call from the m_* fields and the body -> true when written  [msg_send]
561    fn msg_send(&mut self, serial: u32) -> bool {
562        if self.b_over {
563            return false;
564        }
565        let mut m = core::mem::take(&mut self.msg);
566        m.clear();
567        m.extend_from_slice(&[b'l', MSG_CALL, 0, 1]);
568        m.extend_from_slice(&(self.body.len() as u32).to_le_bytes());
569        m.extend_from_slice(&serial.to_le_bytes());
570        m.extend_from_slice(&[0; 4]); // fields length
571        self.msg = m;
572        self.h_field(Some(self.m_path), HF_PATH, b'o');
573        self.h_field(Some(self.m_iface), 2, b's');
574        self.h_field(Some(self.m_member), HF_MEMBER, b's');
575        self.h_field(Some(self.m_dest), 6, b's');
576        self.h_field(self.m_sig, 8, b'g');
577        // UNIX_FDS (9, 'u') when a descriptor goes with the message
578        if self.m_nfds != 0 {
579            while self.msg.len() & 7 != 0 {
580                self.msg.push(0);
581            }
582            self.msg.extend_from_slice(&[9, 1, b'u', 0]);
583            self.msg.extend_from_slice(&self.m_nfds.to_le_bytes());
584        }
585        if self.b_over {
586            return false; // a header field did not fit
587        }
588        let fl = (self.msg.len() - 16) as u32;
589        self.msg[12..16].copy_from_slice(&fl.to_le_bytes());
590        while self.msg.len() & 7 != 0 {
591            self.msg.push(0);
592        }
593        let mut m = core::mem::take(&mut self.msg);
594        m.extend_from_slice(&self.body);
595        let ok = if self.m_nfds != 0 { self.bus_sendfd(&m, self.m_sendfd) } else { self.bus_write(&m) };
596        self.msg = m;
597        ok
598    }
599
600    // ------------------------------------------------------------ reading messages
601
602    fn rd32(&self, o: usize) -> u32 {
603        u32::from_le_bytes([self.rbuf[o], self.rbuf[o + 1], self.rbuf[o + 2], self.rbuf[o + 3]])
604    }
605
606    /// rbuf starts with a complete message (mlen, mhdr set; the lengths come from the peer:
607    /// one that cannot fit reads as RB_CAP + 1 long)  [msg_ready]
608    fn msg_ready(&mut self) -> bool {
609        if self.rlen < 16 {
610            return false;
611        }
612        let hdr = (self.rd32(12) as u64 + 16 + 7) & !7;
613        let len = self.rd32(4) as u64 + hdr;
614        self.mhdr = hdr.min(RB_CAP as u64 + 1) as usize;
615        self.mlen = len.min(RB_CAP as u64 + 1) as usize;
616        self.mlen <= self.rlen
617    }
618
619    /// -> 1 message ready, 0 timeout, -1 error  [msg_next]
620    fn msg_next(&mut self, timeout_ms: i32) -> i32 {
621        loop {
622            if self.msg_ready() {
623                return 1;
624            }
625            if self.mlen > RB_CAP {
626                return -1;
627            }
628            let r = self.bus_fill(timeout_ms);
629            if r != 1 {
630                return r;
631            }
632        }
633    }
634
635    /// drop the current message from the receive buffer  [msg_consume]
636    fn msg_consume(&mut self) {
637        let n = self.mlen.min(self.rlen);
638        self.rbuf.copy_within(n..self.rlen, 0);
639        self.rlen -= n;
640    }
641
642    fn msg_type(&self) -> u8 {
643        self.rbuf[1]
644    }
645
646    /// the value of header field code of the current message  [msg_field]
647    fn msg_field(&self, code: u8) -> Field {
648        let b = &self.rbuf;
649        let mut p: u64 = 16;
650        let end = (self.rd32(12) as u64 + 16).min(self.mhdr as u64);
651        loop {
652            p = (p + 7) & !7 & 0xFFFF_FFFF;
653            if p >= end {
654                return Field::None;
655            }
656            let c = b[p as usize];
657            let slen = b[p as usize + 1] as u64;
658            let typ = b[p as usize + 2];
659            p = (p + slen + 3) & 0xFFFF_FFFF;
660            match typ {
661                b'g' => {
662                    if p >= end {
663                        return Field::None;
664                    }
665                    let n = b[p as usize] as u64;
666                    p += 1;
667                    let e = p + n;
668                    if e >= end || b[e as usize] != 0 {
669                        return Field::None;
670                    }
671                    if c == code {
672                        return Field::Str(p as usize, e as usize);
673                    }
674                    p = e + 1;
675                }
676                b'u' => {
677                    p = (p + 3) & !3;
678                    if p + 4 > end {
679                        return Field::None;
680                    }
681                    let v = self.rd32(p as usize);
682                    p += 4;
683                    if c == code {
684                        return Field::U32(v);
685                    }
686                }
687                _ => {
688                    p = (p + 3) & !3;
689                    if p + 4 > end {
690                        return Field::None;
691                    }
692                    let n = self.rd32(p as usize) as u64;
693                    p += 4;
694                    let e = p + n;
695                    if e >= end || b[e as usize] != 0 {
696                        return Field::None;
697                    }
698                    if c == code {
699                        return Field::Str(p as usize, e as usize);
700                    }
701                    p = (e + 1) & 0xFFFF_FFFF;
702                }
703            }
704        }
705    }
706
707    /// a string field equals s
708    fn field_is(&self, code: u8, s: &[u8]) -> bool {
709        match self.msg_field(code) {
710            Field::Str(a, e) => &self.rbuf[a..e] == cstr(s),
711            _ => false,
712        }
713    }
714
715    /// the reply / error to serial is at rbuf -> true  [wait_reply]
716    fn wait_reply(&mut self, serial: u32, timeout_ms: i32) -> bool {
717        loop {
718            if self.msg_next(timeout_ms) != 1 {
719                return false;
720            }
721            let t = self.msg_type();
722            if (t == MSG_RETURN || t == MSG_ERROR) && matches!(self.msg_field(HF_REPLY), Field::U32(v) if v == serial) {
723                return true;
724            }
725            self.msg_consume();
726        }
727    }
728
729    /// the signal at rbuf is the Response of our request (by its request path or handle)
730    fn is_our_response(&self) -> bool {
731        if self.msg_type() != MSG_SIGNAL || !self.field_is(HF_MEMBER, S_RESPONSE) {
732            return false;
733        }
734        match self.msg_field(HF_PATH) {
735            Field::Str(a, e) => {
736                let p = &self.rbuf[a..e];
737                p == &self.req_path[..] || p == &self.handle[..]
738            }
739            _ => false,
740        }
741    }
742
743    /// take the events wait() queued: window events are kept for later, input events are
744    /// dropped (the dialog is modal for the user)  [keep_drain]
745    fn keep_drain(&mut self) {
746        while let Some(e) = crate::evq_pop() {
747            if matches!(e.typ, E_RESIZE | E_DPI | E_FOCUS_IN | E_FOCUS_OUT | E_CLOSE | E_PAINT) && self.keep.len() < KEEP_MAX {
748                self.keep.push(e);
749            }
750        }
751    }
752
753    /// queue the kept window events again  [keep_restore]
754    fn keep_restore(&mut self) {
755        for e in self.keep.drain(..) {
756            crate::evq_push(e);
757        }
758    }
759
760    /// let the window breathe while waiting (plat_wait(0) + keep_drain)
761    fn breathe(&mut self) {
762        crate::wait(0);
763        self.keep_drain();
764    }
765
766    // ------------------------------------------------------------ body reading
767
768    fn rd_body_begin(&mut self) {
769        self.rp = self.mhdr;
770    }
771
772    fn rd_align(&mut self, n: usize) {
773        self.rp = ((self.rp + n - 1) & !(n - 1)).min(self.mlen);
774    }
775
776    fn rd_skip(&mut self, n: u32) {
777        self.rp = (self.rp as u64 + n as u64).min(self.mlen as u64) as usize;
778    }
779
780    /// -> the value (0 past the end of the message; the reader then stays at the end)
781    fn rd_u32(&mut self) -> u32 {
782        self.rd_align(4);
783        if self.rp + 4 > self.mlen {
784            self.rp = self.mlen;
785            return 0;
786        }
787        let v = self.rd32(self.rp);
788        self.rp += 4;
789        v
790    }
791
792    fn rd_f64(&mut self) -> f64 {
793        self.rd_align(8);
794        if self.rp + 8 > self.mlen {
795            self.rp = self.mlen;
796            return 0.0;
797        }
798        let mut b = [0u8; 8];
799        b.copy_from_slice(&self.rbuf[self.rp..self.rp + 8]);
800        self.rp += 8;
801        f64::from_le_bytes(b)
802    }
803
804    /// a 's' / 'o' value -> its bytes (None when it does not fit into the message or is not
805    /// NUL-terminated)
806    fn rd_str(&mut self) -> Option<Vec<u8>> {
807        let n = self.rd_u32() as u64;
808        let s = self.rp as u64;
809        let e = s + n + 1;
810        if e > self.mlen as u64 || self.rbuf[e as usize - 1] != 0 {
811            return None;
812        }
813        self.rp = e as usize;
814        Some(self.rbuf[s as usize..e as usize - 1].to_vec())
815    }
816
817    /// a 'g' value ("" when it does not fit into the message)
818    fn rd_sig(&mut self) -> Vec<u8> {
819        let p = self.rp;
820        if p >= self.mlen {
821            self.rp = self.mlen;
822            return Vec::new();
823        }
824        let n = self.rbuf[p] as usize;
825        let e = p + 1 + n + 1;
826        if e > self.mlen || self.rbuf[e - 1] != 0 {
827            self.rp = self.mlen;
828            return Vec::new();
829        }
830        self.rp = e;
831        self.rbuf[p + 1..e - 1].to_vec()
832    }
833
834    /// skip one value of the type at sig[i] -> the index after the type  [skip_value]
835    fn skip_value(&mut self, sig: &[u8], i: usize) -> usize {
836        self.skip_depth += 1;
837        let r = self.skip_value_in(sig, i);
838        self.skip_depth -= 1;
839        r
840    }
841
842    fn skip_value_in(&mut self, sig: &[u8], i: usize) -> usize {
843        let at = |k: usize| sig.get(k).copied().unwrap_or(0);
844        if self.skip_depth > SKIP_DEPTH_MAX {
845            // nested deeper than any real portal reply: give up on the rest of the message
846            self.rp = self.mlen;
847            return sig_end(sig, i);
848        }
849        let c = at(i);
850        self.rd_align(type_align(c));
851        match c {
852            b'y' => self.rd_skip(1),
853            b'n' | b'q' => self.rd_skip(2),
854            b'x' | b't' | b'd' => self.rd_skip(8),
855            b's' | b'o' => {
856                self.rd_str();
857            }
858            b'g' => {
859                self.rd_sig();
860            }
861            b'v' => {
862                let s = self.rd_sig();
863                self.skip_value(&s, 0);
864            }
865            b'a' => {
866                let n = self.rd_u32();
867                self.rd_align(type_align(at(i + 1)));
868                self.rd_skip(n);
869                return sig_end(sig, i + 1);
870            }
871            b'(' | b'{' => {
872                let mut k = i + 1;
873                loop {
874                    match at(k) {
875                        0 => return k,
876                        b')' | b'}' => return k + 1,
877                        _ => k = self.skip_value(sig, k),
878                    }
879                }
880            }
881            _ => self.rd_skip(4), // b i u h
882        }
883        i + 1
884    }
885
886    // ------------------------------------------------------------ the portal calls
887
888    fn set_bus_call(&mut self, member: &'static [u8], sig: Option<&'static [u8]>) {
889        self.m_path = S_BUS_PATH;
890        self.m_iface = S_BUS_NAME;
891        self.m_member = member;
892        self.m_dest = S_BUS_NAME;
893        self.m_sig = sig;
894        self.body_reset();
895    }
896
897    /// Hello (serial 1) -> our unique name
898    fn hello(&mut self) -> Option<Vec<u8>> {
899        self.set_bus_call(S_HELLO, None);
900        if !self.msg_send(1) || !self.wait_reply(1, T_HELLO) {
901            return None;
902        }
903        self.rd_body_begin();
904        let name = self.rd_str()?;
905        Some(name)
906    }
907
908    /// req_path = .../request/<sender>/<token>  [make_request_path]
909    fn make_request_path(&mut self, uniq: &[u8]) {
910        self.token_n = self.token_n.wrapping_add(1);
911        self.token = copy_cap(format!("jfgui{}", self.token_n).as_bytes(), 32);
912        let mut p = b"/org/freedesktop/portal/desktop/request/".to_vec();
913        // the unique name comes from the bus: at most up to MP_NAME_END
914        let mut s = cstr(uniq);
915        if s.first() == Some(&b':') {
916            s = &s[1..];
917        }
918        for &c in s {
919            if p.len() >= MP_NAME_END {
920                break;
921            }
922            p.push(if c == b'.' { b'_' } else { c });
923        }
924        p.push(b'/');
925        let tok = self.token.clone();
926        cat_cap(&mut p, &tok, 256);
927        self.req_path = p;
928        self.handle.clear();
929    }
930
931    /// AddMatch for the Response signal of our request (message serial)
932    fn add_match(&mut self, serial: u32) -> bool {
933        let mut t = copy_cap(S_MATCH1, 512);
934        let rp = self.req_path.clone();
935        cat_cap(&mut t, &rp, 512);
936        cat_cap(&mut t, b"'", 512);
937        self.set_bus_call(S_ADDMATCH, Some(b"s"));
938        self.b_str(&t);
939        self.msg_send(serial)
940    }
941
942    /// Hello + request path + AddMatch (serials 1, 2)
943    fn hello_match(&mut self) -> bool {
944        let Some(name) = self.hello() else { return false };
945        self.make_request_path(&name);
946        self.msg_consume();
947        self.add_match(2)
948    }
949
950    /// AddMatch for the portal's end (a dialog waiting for its Response)
951    fn owner_match(&mut self, serial: u32) -> bool {
952        self.set_bus_call(S_ADDMATCH, Some(b"s"));
953        self.b_str(S_MATCH_OWNER);
954        self.msg_send(serial)
955    }
956
957    /// the message is NameOwnerChanged of the portal to nobody (it ended)
958    fn is_portal_gone(&mut self) -> bool {
959        if self.msg_type() != MSG_SIGNAL || !self.field_is(HF_MEMBER, S_NAME_OWNER_CHANGED) {
960            return false;
961        }
962        self.rd_body_begin();
963        let name = self.rd_str();
964        let _old = self.rd_str();
965        let new = self.rd_str();
966        name.as_deref() == Some(S_PORTAL_NAME) && new.is_some_and(|n| cstr(&n).is_empty())
967    }
968
969    // ------------------------------------------------------------ FileChooser
970
971    /// see the file header  [plat_file_dialog]
972    fn file_dialog(&mut self, flags: u32, path: &mut String, exts: &[&[u8]]) -> (i32, u32) {
973        self.keep.clear();
974        if !self.bus_open() {
975            return (-1, 0);
976        }
977        let r = self.file_dialog_call(flags, path, exts);
978        self.bus_close();
979        self.keep_restore();
980        r
981    }
982
983    fn file_dialog_call(&mut self, flags: u32, path: &mut String, exts: &[&[u8]]) -> (i32, u32) {
984        if !self.hello_match() || !self.owner_match(9) {
985            return (-1, 0);
986        }
987        // FileChooser.SaveFile / OpenFile
988        self.build_call(flags, path.as_bytes(), exts);
989        if !self.msg_send(3) || !self.wait_reply(3, T_CALL) || self.msg_type() == MSG_ERROR {
990            return (-1, 0);
991        }
992        self.rd_body_begin();
993        if let Some(h) = self.rd_str() {
994            self.handle = copy_cap(&h, 256);
995        }
996        self.msg_consume();
997        // the user is choosing: wait for Response, keep the window alive
998        let r = self.wait_response();
999        if r <= 0 {
1000            return (r, 0);
1001        }
1002        // a file: path from the URI, extension index from the chosen filter
1003        let Some(p) = self.uri_to_path(PATH_CAP) else { return (0, 0) };
1004        let mut p = p;
1005        let index = if flags & FD_FOLDER != 0 { 0 } else { self.filter_index(exts) };
1006        if flags & (FD_SAVE | FD_FOLDER) == FD_SAVE {
1007            add_ext(&mut p, PATH_CAP, exts, index);
1008        }
1009        *path = String::from_utf8_lossy(&p).into_owned();
1010        (1, index)
1011    }
1012
1013    /// g_m_* and the body of FileChooser.SaveFile / OpenFile  [build_call]
1014    fn build_call(&mut self, flags: u32, path: &[u8], exts: &[&[u8]]) {
1015        let folder = flags & FD_FOLDER != 0;
1016        let save = !folder && flags & FD_SAVE != 0;
1017        self.m_path = S_PORTAL_PATH;
1018        self.m_iface = S_FILECHOOSER;
1019        self.m_member = if save { b"SaveFile" } else { b"OpenFile" };
1020        self.m_dest = S_PORTAL_NAME;
1021        self.m_sig = Some(b"ssa{sv}");
1022        self.body_reset();
1023        self.b_str(&parent());
1024        self.b_str(if save { b"Speichern unter" } else if folder { "Ordner auswählen".as_bytes() } else { "Öffnen".as_bytes() });
1025        // options a{sv}
1026        let arr = self.b_array_begin(8);
1027        self.b_dict_key(b"handle_token", b"s");
1028        let tok = self.token.clone();
1029        self.b_str(&tok);
1030        self.b_dict_key(b"modal", b"b");
1031        self.b_u32(1);
1032        if folder {
1033            // FD_FOLDER: OpenFile with "directory" (portal version 3), starting in path itself
1034            self.b_dict_key(b"directory", b"b");
1035            self.b_u32(1);
1036            if !path.is_empty() {
1037                self.b_dict_key(b"current_folder", b"ay");
1038                let a2 = self.b_array_begin(1);
1039                let mut f = path.to_vec();
1040                f.push(0);
1041                self.b_bytes(&f);
1042                self.b_array_end(a2);
1043            }
1044            self.b_array_end(arr);
1045            return;
1046        }
1047        // suggested folder (both) and name (save)
1048        self.split_path(path);
1049        if save && !self.name.is_empty() {
1050            self.b_dict_key(b"current_name", b"s");
1051            let n = self.name.clone();
1052            self.b_str(&n);
1053        }
1054        if !self.folder.is_empty() {
1055            self.b_dict_key(b"current_folder", b"ay");
1056            let a2 = self.b_array_begin(1);
1057            let mut f = self.folder.clone();
1058            f.push(0); // with the NUL
1059            self.b_bytes(&f);
1060            self.b_array_end(a2);
1061        }
1062        if !exts.is_empty() {
1063            // "filters": a(sa(us))
1064            self.b_dict_key(b"filters", b"a(sa(us))");
1065            let a2 = self.b_array_begin(8);
1066            if !save {
1067                self.b_filter(-1, exts); // open: all supported files first
1068            }
1069            for i in 0..exts.len() {
1070                self.b_filter(i as i32, exts);
1071            }
1072            self.b_array_end(a2);
1073            // "current_filter": (sa(us))
1074            self.b_dict_key(b"current_filter", b"(sa(us))");
1075            let mut cur = -1;
1076            if save {
1077                cur = ((flags >> FD_EXT_SHIFT) & 0xFF) as i32;
1078                if cur as usize >= exts.len() {
1079                    cur = 0;
1080                }
1081            }
1082            self.b_filter(cur, exts);
1083        }
1084        self.b_array_end(arr);
1085    }
1086
1087    /// one (sa(us)) filter: name and glob patterns (i = -1: all entries); every extension of
1088    /// an entry ("a;b=...") becomes a pattern  [b_filter]
1089    fn b_filter(&mut self, i: i32, exts: &[&[u8]]) {
1090        let fname = filter_name(i, exts);
1091        self.b_align(8);
1092        self.b_str(&fname);
1093        let arr = self.b_array_begin(8);
1094        let mut j = if i == -1 { 0 } else { i as usize };
1095        while let Some(&e) = exts.get(j) {
1096            let mut p = 0;
1097            loop {
1098                let ext = ext_copy(&e[p..]);
1099                self.b_align(8);
1100                self.b_u32(0); // 0 = glob pattern
1101                // GTK's file filters (GNOME's portal) match globs case-sensitively: every
1102                // letter both ways ("*.[jJ][pP][gG]"), so IMG_0001.JPG shows too
1103                let mut pat = copy_cap(b"*.", 128);
1104                for &c in cstr(&ext) {
1105                    if pat.len() + 4 >= 512 {
1106                        break;
1107                    }
1108                    if c.is_ascii_alphabetic() {
1109                        pat.extend_from_slice(&[b'[', c.to_ascii_lowercase(), c.to_ascii_uppercase(), b']']);
1110                    } else {
1111                        pat.push(c);
1112                    }
1113                }
1114                self.b_str(&pat);
1115                // the next extension of this entry ("a;b=...")
1116                let mut k = p;
1117                let mut next = None;
1118                while k < e.len() && e[k] != 0 && e[k] != b'=' {
1119                    k += 1;
1120                    if e[k - 1] == b';' {
1121                        next = Some(k);
1122                        break;
1123                    }
1124                }
1125                match next {
1126                    Some(n) => p = n,
1127                    None => break,
1128                }
1129            }
1130            if i != -1 {
1131                break;
1132            }
1133            j += 1;
1134        }
1135        self.b_array_end(arr);
1136    }
1137
1138    /// folder = the existing folder part (absolute), name = the name part  [split_path]
1139    fn split_path(&mut self, path: &[u8]) {
1140        self.folder.clear();
1141        self.name.clear();
1142        let tmp = copy_cap(path, 512);
1143        match tmp.iter().rposition(|&c| c == b'/') {
1144            None => self.name = copy_cap(&tmp, 256),
1145            Some(k) => {
1146                self.name = copy_cap(&tmp[k + 1..], 256);
1147                let dir = &tmp[..k];
1148                if dir.first() == Some(&b'/') && crate::file_exists_raw(&String::from_utf8_lossy(dir)) == 2 {
1149                    self.folder = copy_cap(dir, 512);
1150                }
1151            }
1152        }
1153    }
1154
1155    /// "file:///a%20b" -> "/a b" (at most cap - 1 bytes)  [uri_to_path]
1156    fn uri_to_path(&self, cap: usize) -> Option<Vec<u8>> {
1157        let u = &self.uri;
1158        if !u.starts_with(b"file://") {
1159            return None;
1160        }
1161        let at = |k: usize| u.get(k).copied().unwrap_or(0);
1162        let mut k = 7;
1163        if at(k) != b'/' {
1164            // "file://host/path": drop the host
1165            loop {
1166                if k >= u.len() || at(k) == 0 {
1167                    return None;
1168                }
1169                if at(k) == b'/' {
1170                    break;
1171                }
1172                k += 1;
1173            }
1174        }
1175        let mut out = Vec::new();
1176        while out.len() < cap - 1 && k < u.len() && at(k) != 0 {
1177            let mut c = at(k);
1178            k += 1;
1179            if c == b'%' {
1180                // "%XY": both digits inside the URI, neither the NUL
1181                if k + 2 > URI_CAP || at(k) == 0 || at(k + 1) == 0 {
1182                    break;
1183                }
1184                c = (hexval(at(k)) << 4 | hexval(at(k + 1))) as u8;
1185                k += 2;
1186                if c == 0 {
1187                    break; // "%00": the C string of the assembly ends here
1188                }
1189            }
1190            out.push(c);
1191        }
1192        Some(out)
1193    }
1194
1195    /// the index of the entry whose description is the chosen filter (0 if none)  [filter_index]
1196    fn filter_index(&self, exts: &[&[u8]]) -> u32 {
1197        if self.filter.is_empty() {
1198            return 0;
1199        }
1200        for i in 0..exts.len() {
1201            if filter_name(i as i32, exts) == self.filter {
1202                return i as u32;
1203            }
1204        }
1205        0
1206    }
1207
1208    /// 1 file chosen (uri, filter), 0 cancelled, -1 lost connection; services the display
1209    /// meanwhile  [wait_response]
1210    fn wait_response(&mut self) -> i32 {
1211        loop {
1212            if self.msg_ready() {
1213                if self.is_our_response() {
1214                    let r = self.parse_response();
1215                    self.msg_consume();
1216                    return r;
1217                }
1218                if self.is_portal_gone() {
1219                    self.msg_consume();
1220                    return -1; // no answer will come
1221                }
1222                self.msg_consume();
1223                continue;
1224            }
1225            let r = self.bus_fill(40);
1226            if r < 0 {
1227                return -1;
1228            }
1229            if r > 0 {
1230                continue;
1231            }
1232            self.breathe(); // nothing from the bus: let the window breathe
1233        }
1234    }
1235
1236    /// body "ua{sv}": code, results ("uris" as -> uri, "current_filter" (sa(us)) -> filter)
1237    /// -> 1 file chosen, 0 cancelled  [parse_response]
1238    fn parse_response(&mut self) -> i32 {
1239        self.uri.clear();
1240        self.filter.clear();
1241        self.rd_body_begin();
1242        if self.rd_u32() != 0 {
1243            return 0;
1244        }
1245        let len = self.rd_u32();
1246        self.rd_align(8);
1247        let end = (len as usize + self.rp) & 0xFFFF_FFFF;
1248        loop {
1249            self.rd_align(8);
1250            if self.rp >= end {
1251                break;
1252            }
1253            let Some(key) = self.rd_str() else { break };
1254            let sig = self.rd_sig();
1255            if key == b"uris" {
1256                if sig == b"as" {
1257                    let n = self.rd_u32();
1258                    let start = self.rp;
1259                    if n != 0 {
1260                        if let Some(s) = self.rd_str() {
1261                            self.uri = copy_cap(&s, URI_CAP); // the first URI
1262                        }
1263                    }
1264                    self.rp = start; // back to the array start, then skip it
1265                    self.rd_skip(n);
1266                    continue;
1267                }
1268            } else if key == b"current_filter" && sig == b"(sa(us))" {
1269                self.rd_align(8);
1270                let start = self.rp;
1271                if let Some(s) = self.rd_str() {
1272                    self.filter = copy_cap(&s, 256); // the filter name
1273                }
1274                self.rp = start;
1275            }
1276            self.skip_value(&sig, 0);
1277        }
1278        (!self.uri.is_empty()) as i32
1279    }
1280
1281    // ------------------------------------------------------------ OpenURI
1282
1283    /// close the connection a previous open_folder kept open  [of_release]
1284    fn of_release(&mut self) {
1285        if self.of_keep > 0 {
1286            sys_close(self.of_keep);
1287            self.of_keep = 0;
1288        }
1289    }
1290
1291    /// see the file header  [plat_open_folder]
1292    fn open_folder(&mut self, path: &[u8]) -> i32 {
1293        self.of_release();
1294        let path = cstr(path);
1295        if path.is_empty() {
1296            return -1;
1297        }
1298        // absolute path
1299        let mut of = Vec::new();
1300        if path[0] != b'/' {
1301            of = copy_cap(crate::cwd().as_bytes(), OF_CAP);
1302            cat_cap(&mut of, b"/", OF_CAP);
1303        }
1304        cat_cap(&mut of, path, OF_CAP);
1305        // an O_PATH descriptor (the path must exist)
1306        let mut z = of.clone();
1307        z.push(0);
1308        // SAFETY: openat with a NUL-terminated path.
1309        let fd = unsafe { syscall6(SYS_OPENAT, AT_FDCWD, z.as_ptr() as usize, O_PATH | O_CLOEXEC, 0, 0, 0) };
1310        if fd < 0 {
1311            return -1;
1312        }
1313        let fd = fd as i32;
1314        let mut r = -1;
1315        if of_uri(&mut self.uri, &of) {
1316            if env_is_1("jfgui_OPEN_DRYRUN") {
1317                let line = [
1318                    &b"plat_open_folder: OpenURI.OpenDirectory(fd of "[..],
1319                    &of,
1320                    b"), fallback OpenURI.OpenURI(",
1321                    &self.uri,
1322                    b")\n",
1323                ]
1324                .concat();
1325                crate::write_stdout(&line);
1326                r = 0;
1327            } else {
1328                r = self.of_send(fd);
1329            }
1330        }
1331        sys_close(fd);
1332        r
1333    }
1334
1335    /// see the file header  [plat_open_url]
1336    fn open_url(&mut self, url: &[u8]) -> i32 {
1337        self.of_release();
1338        let url = cstr(url);
1339        if url.is_empty() || url.len() - 1 > URI_CAP - 2 {
1340            return -1; // empty or too long
1341        }
1342        self.uri = copy_cap(url, URI_CAP);
1343        if env_is_1("jfgui_OPEN_DRYRUN") {
1344            let line = [&b"plat_open_url: OpenURI.OpenURI("[..], &self.uri, b")\n"].concat();
1345            crate::write_stdout(&line);
1346            return 0;
1347        }
1348        self.of_send(-1)
1349    }
1350
1351    /// the portal call of open_folder / open_url: fd >= 0 OpenDirectory with that descriptor
1352    /// first, else (or when rejected) OpenURI with uri  [of_send]
1353    fn of_send(&mut self, fd: i32) -> i32 {
1354        self.keep.clear();
1355        self.neg_fd = fd >= 0; // descriptors only when there is one
1356        let ok = self.bus_open();
1357        self.neg_fd = false;
1358        if !ok {
1359            return -1;
1360        }
1361        let r = self.of_send_call(fd);
1362        self.bus_close();
1363        self.keep_restore();
1364        r
1365    }
1366
1367    fn of_send_call(&mut self, fd: i32) -> i32 {
1368        if !self.hello_match() {
1369            return -1;
1370        }
1371        let mut done = false;
1372        if fd >= 0 && self.fd_ok {
1373            // OpenDirectory with the descriptor
1374            self.of_build(true);
1375            self.m_nfds = 1;
1376            self.m_sendfd = fd;
1377            let ok = self.msg_send(3);
1378            self.m_nfds = 0;
1379            if !ok {
1380                return -1;
1381            }
1382            done = self.of_answer(3);
1383        }
1384        if !done {
1385            // rejected (or no descriptor): OpenURI with a file:// URI
1386            self.of_build(false);
1387            if !self.msg_send(4) || !self.of_answer(4) {
1388                return -1;
1389            }
1390        }
1391        // the Response (or keep the request alive)
1392        match self.of_wait(OF_WAIT) {
1393            -2 => {
1394                self.of_keep = self.fd;
1395                self.fd = -1;
1396                0
1397            }
1398            0 => 0,
1399            _ => -1,
1400        }
1401    }
1402
1403    /// the call returned (request handle stored) -> true; false on an error reply / no answer
1404    /// [of_answer]
1405    fn of_answer(&mut self, serial: u32) -> bool {
1406        if !self.wait_reply(serial, T_CALL) {
1407            return false;
1408        }
1409        if self.msg_type() == MSG_ERROR {
1410            self.msg_consume();
1411            return false;
1412        }
1413        self.handle.clear();
1414        self.rd_body_begin();
1415        if let Some(h) = self.rd_str() {
1416            self.handle = copy_cap(&h, 256);
1417        }
1418        self.msg_consume();
1419        true
1420    }
1421
1422    /// g_m_* and the body of OpenURI.OpenDirectory(s parent, h 0, a{sv}) or
1423    /// OpenURI.OpenURI(s parent, s uri, a{sv}); options: handle_token  [of_build]
1424    fn of_build(&mut self, with_fd: bool) {
1425        self.m_path = S_PORTAL_PATH;
1426        self.m_iface = S_OPENURI;
1427        self.m_member = if with_fd { b"OpenDirectory" } else { b"OpenURI" };
1428        self.m_dest = S_PORTAL_NAME;
1429        self.m_sig = Some(if with_fd { b"sha{sv}" } else { b"ssa{sv}" });
1430        self.body_reset();
1431        self.b_str(&parent());
1432        if with_fd {
1433            self.b_u32(0); // 'h': index 0 of the message's descriptors
1434        } else {
1435            let u = self.uri.clone();
1436            self.b_str(&u);
1437        }
1438        let arr = self.b_array_begin(8);
1439        self.b_dict_key(b"handle_token", b"s");
1440        let t = self.token.clone();
1441        self.b_str(&t);
1442        self.b_array_end(arr);
1443    }
1444
1445    /// the Response code of our request (0 ok, 1 cancelled, 2 failed), -2 no answer within
1446    /// ms, -1 connection lost; services the window meanwhile.  parse: a code 0 Response's
1447    /// results go to page (Print)  [of_wait, prn_wait]
1448    fn resp_wait(&mut self, ms: u32, mut page: Option<&mut PPage>) -> i32 {
1449        let deadline = crate::time_us() + ms as u64 * 1000;
1450        loop {
1451            if self.msg_ready() {
1452                if self.is_our_response() {
1453                    self.rd_body_begin();
1454                    let code = self.rd_u32();
1455                    if code == 0 {
1456                        if let Some(p) = page.as_deref_mut() {
1457                            self.prn_results(p);
1458                        }
1459                    }
1460                    self.msg_consume();
1461                    return code as i32;
1462                }
1463                self.msg_consume();
1464                continue;
1465            }
1466            if crate::time_us() >= deadline {
1467                return -2;
1468            }
1469            let r = self.bus_fill(20);
1470            if r < 0 {
1471                return -1;
1472            }
1473            if r > 0 {
1474                continue;
1475            }
1476            if crate::disp_backend() != 0 {
1477                self.breathe(); // a window to keep alive
1478            }
1479        }
1480    }
1481
1482    fn of_wait(&mut self, ms: u32) -> i32 {
1483        self.resp_wait(ms, None)
1484    }
1485
1486    // ------------------------------------------------------------ Print
1487
1488    /// PreparePrint with the job's paper / orientation as suggestion; page gets the dialog's
1489    /// page setup and the token -> 1 ok, 0 cancelled, -1 no bus / no portal / no descriptor
1490    /// passing.  The connection stays open for prn_send.  [portal_print_prepare]
1491    fn prn_prepare(&mut self, job: &PrintJob, page: &mut PPage, title: &[u8]) -> i32 {
1492        self.prn_end();
1493        self.keep.clear();
1494        self.neg_fd = true;
1495        let ok = self.bus_open();
1496        self.neg_fd = false;
1497        self.prn_open = true;
1498        if !ok || !self.fd_ok {
1499            return -1; // Print needs descriptor passing
1500        }
1501        let Some(u) = self.hello() else { return -1 };
1502        self.prn_uniq = copy_cap(&u, 128);
1503        self.msg_consume();
1504        if !self.prn_match(2) {
1505            return -1;
1506        }
1507        self.prn_build_prepare(job, title);
1508        if !self.msg_send(3) || !self.of_answer(3) {
1509            return -1;
1510        }
1511        match self.resp_wait(PRN_T_DIALOG, Some(page)) {
1512            r if r < 0 => -1,
1513            0 => 1,
1514            _ => 0, // 1 cancelled, 2 ended otherwise
1515        }
1516    }
1517
1518    /// Print(parent, title, h fd, {handle_token, modal, token}) -> 0 printed, 1 cancelled,
1519    /// -1 failed  [portal_print_send]
1520    fn prn_send(&mut self, title: &[u8], fd: i32, token: u32) -> i32 {
1521        if !self.prn_open || self.fd < 0 || !self.prn_match(4) {
1522            return -1;
1523        }
1524        self.prn_build_print(title, token);
1525        self.m_nfds = 1;
1526        self.m_sendfd = fd;
1527        let ok = self.msg_send(5);
1528        self.m_nfds = 0;
1529        if !ok || !self.of_answer(5) {
1530            return -1;
1531        }
1532        match self.resp_wait(PRN_T_PRINT, None) {
1533            r @ (0 | 1) => r,
1534            _ => -1, // 2 failed, < 0 lost / no answer
1535        }
1536    }
1537
1538    /// close the connection of prn_prepare  [portal_print_end]
1539    fn prn_end(&mut self) {
1540        if self.prn_open {
1541            self.prn_open = false;
1542            self.bus_close();
1543            self.keep_restore();
1544        }
1545    }
1546
1547    /// a new request path (token) and AddMatch on its Response  [prn_match]
1548    fn prn_match(&mut self, serial: u32) -> bool {
1549        let u = self.prn_uniq.clone();
1550        self.make_request_path(&u);
1551        self.add_match(serial)
1552    }
1553
1554    /// g_m_* for a Print portal call, body = the parent window string  [prn_head]
1555    fn prn_head(&mut self, member: &'static [u8], sig: &'static [u8]) {
1556        self.m_member = member;
1557        self.m_sig = Some(sig);
1558        self.m_path = S_PORTAL_PATH;
1559        self.m_iface = S_PRN_IFACE;
1560        self.m_dest = S_PORTAL_NAME;
1561        self.body_reset();
1562        self.b_str(&parent());
1563    }
1564
1565    fn prn_dict_s(&mut self, key: &[u8], s: &[u8]) {
1566        self.b_dict_key(key, b"s");
1567        self.b_str(s);
1568    }
1569
1570    fn prn_dict_d(&mut self, key: &[u8], v: f64) {
1571        self.b_dict_key(key, b"d");
1572        self.b_f64(v);
1573    }
1574
1575    fn prn_dict_u(&mut self, key: &[u8], sig: &[u8], v: u32) {
1576        self.b_dict_key(key, sig);
1577        self.b_u32(v);
1578    }
1579
1580    /// PreparePrint(s parent, s title, a{sv} settings, a{sv} page_setup, a{sv} options)
1581    /// [prn_build_prepare]
1582    fn prn_build_prepare(&mut self, job: &PrintJob, title: &[u8]) {
1583        self.prn_head(b"PreparePrint", b"ssa{sv}a{sv}a{sv}");
1584        self.b_str(title);
1585        let p = PRN_PAPERS[if job.paper >= PAPER_N { 0 } else { job.paper as usize }];
1586        let orient: &[u8] = if job.flags & PJF_LANDSCAPE != 0 { b"landscape" } else { b"portrait" };
1587        // settings
1588        let a = self.b_array_begin(8);
1589        self.prn_dict_s(b"orientation", orient);
1590        self.b_array_end(a);
1591        // page setup
1592        let a = self.b_array_begin(8);
1593        self.prn_dict_s(b"Name", p.2);
1594        self.prn_dict_s(b"DisplayName", p.3);
1595        self.prn_dict_d(b"Width", p.0);
1596        self.prn_dict_d(b"Height", p.1);
1597        self.prn_dict_s(b"Orientation", orient);
1598        self.b_array_end(a);
1599        // options
1600        let a = self.b_array_begin(8);
1601        let t = self.token.clone();
1602        self.prn_dict_s(b"handle_token", &t);
1603        self.prn_dict_u(b"modal", b"b", 1);
1604        self.b_array_end(a);
1605    }
1606
1607    /// Print(s parent, s title, h 0, a{sv} options)  [prn_build_print]
1608    fn prn_build_print(&mut self, title: &[u8], token: u32) {
1609        self.prn_head(b"Print", b"ssha{sv}");
1610        self.b_str(title);
1611        self.b_u32(0); // 'h': index 0 of the message's descriptors
1612        let a = self.b_array_begin(8);
1613        let t = self.token.clone();
1614        self.prn_dict_s(b"handle_token", &t);
1615        self.prn_dict_u(b"modal", b"b", 1);
1616        self.prn_dict_u(b"token", b"u", token);
1617        self.b_array_end(a);
1618    }
1619
1620    /// the end offset of the a{sv} array starting at the reader (never past the message)
1621    /// [prn_dict_end]
1622    fn prn_dict_end(&mut self) -> usize {
1623        let n = self.rd_u32() as usize;
1624        self.rd_align(8);
1625        let room = self.mlen.saturating_sub(self.rp);
1626        self.rp + n.min(room)
1627    }
1628
1629    /// the results a{sv} of PreparePrint: "token" u, "page-setup" a{sv}  [prn_results]
1630    fn prn_results(&mut self, page: &mut PPage) {
1631        let end = self.prn_dict_end();
1632        loop {
1633            self.rd_align(8);
1634            if self.rp >= end {
1635                return;
1636            }
1637            let Some(key) = self.rd_str() else { return };
1638            let sig = self.rd_sig();
1639            if key == b"token" {
1640                if sig == b"u" {
1641                    page.token = self.rd_u32();
1642                    continue;
1643                }
1644            } else if key == b"page-setup" && sig == b"a{sv}" {
1645                self.prn_setup(page);
1646                continue;
1647            }
1648            self.skip_value(&sig, 0);
1649        }
1650    }
1651
1652    /// page-setup a{sv}: Width / Height / Margin* (d, mm), Orientation (s) -> page (pt)
1653    /// [prn_setup]
1654    fn prn_setup(&mut self, page: &mut PPage) {
1655        let mut mm = [0f64; 6];
1656        let mut land = 0u32;
1657        let end = self.prn_dict_end();
1658        loop {
1659            self.rd_align(8);
1660            if self.rp >= end {
1661                break;
1662            }
1663            let Some(key) = self.rd_str() else { break };
1664            let sig = self.rd_sig();
1665            if sig.first() == Some(&b'd') {
1666                if sig.len() == 1 {
1667                    let v = self.rd_f64();
1668                    if let Some(k) = PRN_DKEYS.iter().position(|&n| n == &key[..]) {
1669                        mm[k] = v;
1670                    }
1671                    continue;
1672                }
1673            } else if key == b"Orientation" && sig == b"s" {
1674                if let Some(s) = self.rd_str() {
1675                    land = s.windows(9).any(|w| w == b"landscape") as u32;
1676                }
1677                continue;
1678            }
1679            self.skip_value(&sig, 0);
1680        }
1681        let (mut w, mut h) = (mm[0], mm[1]);
1682        if !(w > 0.0) || !(h > 0.0) {
1683            return;
1684        }
1685        w *= PRN_MM_PT;
1686        h *= PRN_MM_PT;
1687        page.flags = land;
1688        if land != 0 {
1689            core::mem::swap(&mut w, &mut h);
1690        }
1691        page.w = w;
1692        page.h = h;
1693        page.t = mm[2] * PRN_MM_PT;
1694        page.b = mm[3] * PRN_MM_PT;
1695        page.l = mm[4] * PRN_MM_PT;
1696        page.r = mm[5] * PRN_MM_PT;
1697    }
1698}
1699
1700// ---------------------------------------------------------------- signatures
1701
1702/// the index after one complete type at sig[i]  [sig_end]
1703fn sig_end(sig: &[u8], mut i: usize) -> usize {
1704    let at = |k: usize| sig.get(k).copied().unwrap_or(0);
1705    while at(i) == b'a' {
1706        i += 1;
1707    }
1708    if at(i) != b'(' && at(i) != b'{' {
1709        return i + 1;
1710    }
1711    let mut depth = 0i32;
1712    loop {
1713        let c = at(i);
1714        if c == 0 {
1715            return i;
1716        }
1717        i += 1;
1718        match c {
1719            b'(' | b'{' => depth += 1,
1720            b')' | b'}' => {
1721                depth -= 1;
1722                if depth == 0 {
1723                    return i;
1724                }
1725            }
1726            _ => {}
1727        }
1728    }
1729}
1730
1731/// the alignment of a type code  [type_align]
1732fn type_align(c: u8) -> usize {
1733    match c {
1734        b'n' | b'q' => 2,
1735        b'x' | b't' | b'd' | b'(' | b'{' => 8,
1736        b'b' | b'i' | b'u' | b'h' | b's' | b'o' | b'a' => 4,
1737        _ => 1,
1738    }
1739}
1740
1741/// 0..15 (0 for anything else)  [hexval]
1742fn hexval(c: u8) -> u32 {
1743    let c = c as u32;
1744    let d = c.wrapping_sub(48);
1745    if d <= 9 {
1746        return d;
1747    }
1748    let x = (c | 32).wrapping_sub(87);
1749    if x <= 15 { x } else { 0 }
1750}
1751
1752/// the first extension of "a;b=description" (at most 30 bytes)  [ext_copy]
1753fn ext_copy(e: &[u8]) -> Vec<u8> {
1754    let mut v = Vec::new();
1755    for &c in e {
1756        if c == 0 || c == b'=' || c == b';' || v.len() >= 30 {
1757            break;
1758        }
1759        v.push(c);
1760    }
1761    v
1762}
1763
1764/// the description of entry i ("PNG-Datei" without one); i = -1: "Alle unterstützten
1765/// Dateien"  [filter_name]
1766fn filter_name(i: i32, exts: &[&[u8]]) -> Vec<u8> {
1767    if i == -1 {
1768        return copy_cap(S_ALL_FILES, 128);
1769    }
1770    let e = exts.get(i as usize).copied().unwrap_or(b"");
1771    match e.iter().position(|&c| c == b'=') {
1772        Some(k) => copy_cap(&e[k + 1..], 128),
1773        None => {
1774            let mut x = ext_copy(e);
1775            x.make_ascii_uppercase();
1776            let mut f = copy_cap(&x, 128);
1777            cat_cap(&mut f, b"-Datei", 128);
1778            f
1779        }
1780    }
1781}
1782
1783/// append ".ext" of entry index when the name has none  [add_ext]
1784fn add_ext(path: &mut Vec<u8>, cap: usize, exts: &[&[u8]], index: u32) {
1785    let from = path.iter().rposition(|&c| c == b'/').unwrap_or(0);
1786    if path[from..].contains(&b'.') {
1787        return;
1788    }
1789    let Some(e) = exts.get(index as usize) else { return };
1790    let ext = ext_copy(e);
1791    cat_cap(path, b".", cap);
1792    cat_cap(path, &ext, cap);
1793}
1794
1795/// uri = "file://" + path percent-encoded (A-Z a-z 0-9 - . _ ~ / unchanged) -> false when too
1796/// long  [of_uri]
1797fn of_uri(uri: &mut Vec<u8>, path: &[u8]) -> bool {
1798    uri.clear();
1799    uri.extend_from_slice(b"file://");
1800    for &c in path {
1801        if uri.len() >= URI_CAP - 4 {
1802            return false;
1803        }
1804        if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'.' | b'_' | b'~' | b'/') {
1805            uri.push(c);
1806        } else {
1807            uri.push(b'%');
1808            uri.push(S_HEXU[(c >> 4) as usize]);
1809            uri.push(S_HEXU[(c & 15) as usize]);
1810        }
1811    }
1812    uri.len() < URI_CAP - 4 // (the room is checked before the terminator too)
1813}
1814
1815// ---------------------------------------------------------------- the API
1816
1817/// the portal's FileChooser (see the file header)  [plat_file_dialog]
1818pub fn file_dialog(flags: u32, path: &mut String, exts: &[&str]) -> (i32, u32) {
1819    let list: Vec<&[u8]> = exts.iter().map(|e| cstr(e.as_bytes())).take_while(|e| !e.is_empty()).collect();
1820    with_bus(|b| b.file_dialog(flags, path, &list))
1821}
1822
1823/// show a folder / a file in the file manager (OpenURI.OpenDirectory) -> 0 / -1  [plat_open_folder]
1824pub fn open_folder(path: &str) -> i32 {
1825    if !crate::policy::fs_allowed(path, crate::policy::FS_READ) {
1826        return -1;
1827    }
1828    with_bus(|b| b.open_folder(path.as_bytes()))
1829}
1830
1831/// a web page in the default application (OpenURI.OpenURI) -> 0 / -1  [plat_open_url]
1832pub fn open_url(url: &str) -> i32 {
1833    if !crate::policy::url_allowed(url) {
1834        return -1;
1835    }
1836    with_bus(|b| b.open_url(url.as_bytes()))
1837}
1838
1839/// (crate, linux_print.rs) PreparePrint -> 1 ok, 0 cancelled, -1 unsupported  [portal_print_prepare]
1840pub(crate) fn portal_print_prepare(job: &PrintJob, page: &mut PPage, title: &str) -> i32 {
1841    with_bus(|b| b.prn_prepare(job, page, title.as_bytes()))
1842}
1843
1844/// (crate) Print with the PDF in fd -> 0 printed, 1 cancelled, -1 failed  [portal_print_send]
1845pub(crate) fn portal_print_send(title: &str, fd: i32, token: u32) -> i32 {
1846    with_bus(|b| b.prn_send(title.as_bytes(), fd, token))
1847}
1848
1849/// (crate) close the connection of portal_print_prepare  [portal_print_end]
1850pub(crate) fn portal_print_end() {
1851    with_bus(|b| b.prn_end())
1852}