Skip to main content

ljos_cli/
pack_link.rs

1//! The only speaker that may carry this seat's packsetd token.
2//!
3//! The token stays in this module. Each request is one byte stream to the
4//! socket address resolved for the writer's URL, with no proxy and no
5//! redirect. The proof runs before the token is written on that stream. A
6//! stream that closes is not remembered: the next call proves again before
7//! the token goes out. packsetd answers one request and closes, so the two
8//! refusals and the call that carries the token are three sockets in a row;
9//! the proof is not kept for a later call.
10
11use std::collections::HashMap;
12use std::io::{Read, Write};
13use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
14use std::sync::{Arc, Mutex, OnceLock};
15use std::time::{Duration, Instant};
16
17use packset_client::{Error, Hit};
18use serde::de::DeserializeOwned;
19use serde_json::Value;
20use url::Url;
21
22/// The seat's pack writer. The token field is private to this module.
23pub struct PacksetClient {
24    base: String,
25    origin: Url,
26    addr: Option<SocketAddr>,
27    workspace: Option<String>,
28    token: Mutex<Option<String>>,
29}
30
31impl Clone for PacksetClient {
32    fn clone(&self) -> Self {
33        Self {
34            base: self.base.clone(),
35            origin: self.origin.clone(),
36            addr: self.addr,
37            workspace: self.workspace.clone(),
38            token: Mutex::new(self.token.lock().unwrap_or_else(|p| p.into_inner()).clone()),
39        }
40    }
41}
42
43/// One answer. `trusted` means the token was on this answer, after the
44/// proof held.
45#[derive(Debug)]
46pub(crate) struct Answer {
47    pub status: u16,
48    pub body: String,
49    pub trusted: bool,
50}
51
52struct Slot {
53    stream: Option<TcpStream>,
54    /// The proof held on `stream` and the stream is still the one it held on.
55    proven: bool,
56    token_tag: u64,
57    /// The last answer on this socket carried the token.
58    carried: bool,
59}
60
61fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
62    static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
63    SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
64}
65
66fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
67    let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
68    map.entry(addr)
69        .or_insert_with(|| {
70            Arc::new(Mutex::new(Slot {
71                stream: None,
72                proven: false,
73                token_tag: 0,
74                carried: false,
75            }))
76        })
77        .clone()
78}
79
80fn load_token() -> Option<String> {
81    packset_client::token()
82}
83
84fn token_tag(token: Option<&str>) -> u64 {
85    let Some(token) = token else {
86        return 0;
87    };
88    let mut hash = 0xcbf29ce484222325u64;
89    for byte in token.as_bytes() {
90        hash ^= u64::from(*byte);
91        hash = hash.wrapping_mul(0x100000001b3);
92    }
93    hash
94}
95
96fn nonce() -> Option<String> {
97    use std::io::Read as _;
98    let mut raw = [0u8; 32];
99    std::fs::File::open("/dev/urandom")
100        .and_then(|mut file| file.read_exact(&mut raw))
101        .ok()
102        .map(|()| hex::encode(raw))
103}
104
105fn pin(url: &Url) -> Option<SocketAddr> {
106    let host = url.host_str()?;
107    let port = url.port_or_known_default()?;
108    (host, port).to_socket_addrs().ok()?.next()
109}
110
111fn host_header(url: &Url) -> String {
112    match (url.host_str(), url.port()) {
113        (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
114        (Some(host), Some(port)) => format!("{host}:{port}"),
115        (Some(host), None) => host.to_string(),
116        _ => "localhost".to_string(),
117    }
118}
119
120fn budget() -> Duration {
121    std::env::var("PACKSET_TIMEOUT_MS")
122        .ok()
123        .and_then(|v| v.trim().parse::<u64>().ok())
124        .filter(|ms| *ms > 0)
125        .map_or(Duration::from_secs(30), Duration::from_millis)
126}
127
128/// Whether every socket listening on the loopback port of `url` belongs to
129/// another user. Unknown is not foreign.
130fn foreign(url: &str) -> bool {
131    #[cfg(target_os = "linux")]
132    {
133        use std::os::unix::fs::MetadataExt;
134        let Some(port) = loopback_port(url) else {
135            return false;
136        };
137        let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
138            return false;
139        };
140        let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
141            .iter()
142            .filter_map(|table| std::fs::read_to_string(table).ok())
143            .flat_map(|text| listen_uids(&text, port))
144            .collect();
145        !owners.is_empty() && owners.iter().all(|uid| *uid != me)
146    }
147    #[cfg(not(target_os = "linux"))]
148    {
149        let _ = url;
150        false
151    }
152}
153
154fn loopback_port(url: &str) -> Option<u16> {
155    let rest = url
156        .strip_prefix("http://")
157        .or_else(|| url.strip_prefix("https://"))?;
158    let authority = rest.split(['/', '?', '#']).next()?;
159    let (host, port) = match authority.rsplit_once(':') {
160        Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
161        _ => (authority, 80),
162    };
163    matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
164}
165
166fn listen_uids(table: &str, port: u16) -> Vec<u32> {
167    table
168        .lines()
169        .skip(1)
170        .filter_map(|line| {
171            let fields: Vec<&str> = line.split_whitespace().collect();
172            if fields.get(3) != Some(&"0A") {
173                return None;
174            }
175            let hex = fields.get(1)?.rsplit(':').next()?;
176            if u16::from_str_radix(hex, 16).ok()? != port {
177                return None;
178            }
179            fields.get(7)?.parse().ok()
180        })
181        .collect()
182}
183
184fn target(path: &str, query: &[(&str, &str)]) -> String {
185    if query.is_empty() {
186        return path.to_string();
187    }
188    let mut ser = url::form_urlencoded::Serializer::new(String::new());
189    for (key, value) in query {
190        ser.append_pair(key, value);
191    }
192    format!("{path}?{}", ser.finish())
193}
194
195struct Raw {
196    status: u16,
197    body: String,
198    keep: bool,
199}
200
201fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
202    if side_budget_spent() {
203        return Err(budget_spent());
204    }
205    let started = Instant::now();
206    let result = connect_once(addr, timeout);
207    // A connect that returns a socket is the pack accepting. Time spent
208    // there is not a quiet wait. A connect that fails spends the budget.
209    if result.is_err() {
210        super::charge_side_wait(started.elapsed());
211    }
212    result
213}
214
215/// A side event's pack budget is gone. Opening a socket for a one
216/// millisecond timer still blocks, and a prompt makes several of those
217/// reads, so the call returns before the connect.
218fn side_budget_spent() -> bool {
219    super::side_remaining_ms() == Some(0)
220}
221
222fn budget_spent() -> std::io::Error {
223    std::io::Error::new(
224        std::io::ErrorKind::TimedOut,
225        "the side event's pack budget is spent",
226    )
227}
228
229fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
230    let wait = capped_wait(timeout);
231    let stream = TcpStream::connect_timeout(&addr, wait)?;
232    stream.set_read_timeout(Some(wait))?;
233    stream.set_write_timeout(Some(wait))?;
234    let _ = stream.set_nodelay(true);
235    Ok(stream)
236}
237
238/// A side-event bound shrinks this wait. A zero bound is one millisecond,
239/// so a later read fails at once instead of taking the default.
240fn capped_wait(timeout: Duration) -> Duration {
241    let wait = super::cap_pack_wait(timeout);
242    if wait.is_zero() {
243        Duration::from_millis(1)
244    } else {
245        wait
246    }
247}
248
249fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
250    let mut sent = 0;
251    while sent < bytes.len() {
252        match stream.write(&bytes[sent..]) {
253            Ok(0) => {
254                return Err(std::io::Error::new(
255                    std::io::ErrorKind::WriteZero,
256                    "the pack closed the socket",
257                ))
258            }
259            Ok(n) => sent += n,
260            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
261            Err(err) => return Err(err),
262        }
263    }
264    stream.flush()
265}
266
267fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
268    let mut buf = Vec::new();
269    let mut tmp = [0u8; 4096];
270    let head_end = loop {
271        if buf.len() > 64 * 1024 {
272            return Err(std::io::Error::new(
273                std::io::ErrorKind::InvalidData,
274                "the pack's headers are too long",
275            ));
276        }
277        arm_read(stream, deadline)?;
278        let n = read_some(stream, &mut tmp)?;
279        buf.extend_from_slice(&tmp[..n]);
280        if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
281            break at;
282        }
283    };
284    let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
285    let status = header
286        .lines()
287        .next()
288        .and_then(|line| line.split_whitespace().nth(1))
289        .and_then(|code| code.parse().ok())
290        .ok_or_else(|| {
291            std::io::Error::new(
292                std::io::ErrorKind::InvalidData,
293                "the pack's status line is not HTTP",
294            )
295        })?;
296    let mut length = None;
297    let mut close = header.starts_with("HTTP/1.0");
298    for line in header.lines().skip(1) {
299        let Some((name, value)) = line.split_once(':') else {
300            continue;
301        };
302        if name.eq_ignore_ascii_case("content-length") {
303            length = value.trim().parse().ok();
304        } else if name.eq_ignore_ascii_case("connection")
305            && value.trim().eq_ignore_ascii_case("close")
306        {
307            close = true;
308        }
309    }
310    let mut body = buf.split_off(head_end + 4);
311    if let Some(length) = length {
312        while body.len() < length {
313            arm_read(stream, deadline)?;
314            let n = read_some(stream, &mut tmp)?;
315            body.extend_from_slice(&tmp[..n]);
316            if body.len() > length.saturating_add(64 * 1024) {
317                break;
318            }
319        }
320        body.truncate(length);
321    } else {
322        loop {
323            if deadline.saturating_duration_since(Instant::now()).is_zero() {
324                break;
325            }
326            let _ = arm_read(stream, deadline);
327            match stream.read(&mut tmp) {
328                Ok(0) => break,
329                Ok(n) => body.extend_from_slice(&tmp[..n]),
330                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
331                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
332                Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
333                Err(_) => break,
334            }
335        }
336        close = true;
337    }
338    // A redirect is the answer. The token must not follow it.
339    Ok(Raw {
340        status,
341        body: String::from_utf8_lossy(&body).into_owned(),
342        keep: !close && !(300..400).contains(&status),
343    })
344}
345
346fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
347    let remain = deadline.saturating_duration_since(Instant::now());
348    if remain.is_zero() {
349        return Err(std::io::Error::new(
350            std::io::ErrorKind::TimedOut,
351            "the pack did not answer",
352        ));
353    }
354    stream.set_read_timeout(Some(remain))?;
355    Ok(())
356}
357
358fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
359    loop {
360        match stream.read(tmp) {
361            Ok(0) => {
362                return Err(std::io::Error::new(
363                    std::io::ErrorKind::UnexpectedEof,
364                    "the pack closed the socket",
365                ))
366            }
367            Ok(n) => return Ok(n),
368            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
369            Err(err) => return Err(err),
370        }
371    }
372}
373
374fn render(
375    method: &str,
376    host: &str,
377    path: &str,
378    token: Option<&str>,
379    body: Option<&[u8]>,
380) -> Vec<u8> {
381    let mut head =
382        format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\n");
383    if let Some(token) = token {
384        head.push_str("Authorization: Bearer ");
385        head.push_str(token);
386        head.push_str("\r\n");
387    }
388    let mut out = Vec::new();
389    if let Some(body) = body {
390        head.push_str("Content-Type: application/json\r\n");
391        head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
392        out.extend(head.into_bytes());
393        out.extend_from_slice(body);
394    } else {
395        head.push_str("\r\n");
396        out.extend(head.into_bytes());
397    }
398    out
399}
400
401struct Wire<'a> {
402    method: &'a str,
403    path: &'a str,
404    query: &'a [(&'a str, &'a str)],
405    body: Option<&'a [u8]>,
406    token: Option<&'a str>,
407}
408
409enum Gate {
410    /// The proof held. The call may carry the token.
411    Send,
412    /// The listener did not ask for the token.
413    Bare,
414    /// The writer is busy. The call is not sent.
415    Busy(Raw),
416}
417
418impl PacksetClient {
419    pub fn new(base: impl Into<String>) -> Self {
420        let mut base = base.into();
421        while base.ends_with('/') {
422            base.pop();
423        }
424        let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
425        let addr = Url::parse(&base).ok().as_ref().and_then(pin);
426        Self {
427            base,
428            origin,
429            addr,
430            workspace: None,
431            token: Mutex::new(load_token()),
432        }
433    }
434
435    /// The writer the seat talks to. `PACKSET_URL=off` is no pack.
436    pub fn from_env() -> Result<Self, Error> {
437        packset_client::load_seat_env();
438        let url = std::env::var("PACKSET_URL")
439            .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
440            .ok()
441            .filter(|url| !url.is_empty());
442        match url {
443            Some(url) if url == "off" => Err(Error::NoUrl),
444            Some(url) => Ok(Self::new(url)),
445            None => Ok(Self::new(format!(
446                "http://127.0.0.1:{}",
447                packset_client::default_port()
448            ))),
449        }
450    }
451
452    #[must_use]
453    pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
454        let workspace = workspace.into();
455        self.workspace = (!workspace.is_empty()).then_some(workspace);
456        self
457    }
458
459    pub fn base(&self) -> &str {
460        &self.base
461    }
462
463    /// Whether this seat has a token to show. The token itself stays here.
464    #[must_use]
465    pub(crate) fn has_token(&self) -> bool {
466        load_token().is_some()
467    }
468
469    /// Whether a call may carry the token: the seat has one, and another
470    /// user does not hold the loopback port.
471    #[must_use]
472    pub(crate) fn may_carry(&self) -> bool {
473        load_token().is_some() && !foreign(&self.base)
474    }
475
476    /// Whether the last answer on this socket carried the token. A closed
477    /// socket does not keep this for the next call: that call proves again.
478    #[must_use]
479    pub(crate) fn carried(&self) -> bool {
480        let Some(addr) = self.addr else {
481            return false;
482        };
483        let map = slots().lock().unwrap_or_else(|p| p.into_inner());
484        map.get(&addr)
485            .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
486    }
487
488    pub fn workspace(&self) -> String {
489        if let Some(workspace) = &self.workspace {
490            return workspace.clone();
491        }
492        if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
493            if !workspace.is_empty() {
494                return workspace;
495            }
496        }
497        let cwd = std::env::var("GROKOS_WORKSPACE")
498            .ok()
499            .map(|s| s.trim().to_string())
500            .filter(|s| !s.is_empty())
501            .map(std::path::PathBuf::from)
502            .or_else(|| std::env::current_dir().ok())
503            .unwrap_or_else(|| std::path::PathBuf::from("."));
504        let abs = cwd.canonicalize().unwrap_or(cwd);
505        let shown = abs.to_string_lossy().into_owned();
506        if let Ok(answer) = self.roundtrip(
507            "GET",
508            "/v1/identity",
509            &[("cwd", shown.as_str())],
510            None,
511            true,
512            budget(),
513        ) {
514            if answer.status == 200 {
515                if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
516                    if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
517                        if !ws.is_empty() {
518                            return ws.to_string();
519                        }
520                    }
521                }
522            }
523        }
524        format!("dir:{}", abs.display())
525    }
526
527    /// One request. `bearer` asks to carry the token; it is left off when
528    /// the proof does not hold or another user owns the port. A 3xx is
529    /// returned as the answer and not followed. HTTP 503 is one answer:
530    /// the caller decides whether to ask again.
531    pub(crate) fn roundtrip(
532        &self,
533        method: &str,
534        path: &str,
535        query: &[(&str, &str)],
536        body: Option<&[u8]>,
537        bearer: bool,
538        timeout: Duration,
539    ) -> Result<Answer, Error> {
540        let Some(addr) = self.addr else {
541            return Err(Error::Bad(format!(
542                "{} is not a socket this seat can pin",
543                self.base
544            )));
545        };
546        let token = load_token();
547        *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
548        let allow = bearer && token.is_some() && !foreign(&self.base);
549        self.exchange(
550            addr,
551            &Wire {
552                method,
553                path,
554                query,
555                body,
556                token: token.as_deref(),
557            },
558            allow,
559            timeout,
560        )
561    }
562
563    fn exchange(
564        &self,
565        addr: SocketAddr,
566        call: &Wire<'_>,
567        allow: bool,
568        timeout: Duration,
569    ) -> Result<Answer, Error> {
570        let slot = slot_for(addr);
571        let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
572        let tag = token_tag(call.token);
573        if live.token_tag != tag {
574            live.stream = None;
575            live.proven = false;
576            live.token_tag = tag;
577        }
578        let carried = if allow {
579            match self.gate(&mut live, addr, timeout)? {
580                Gate::Bare => None,
581                Gate::Send => call.token,
582                Gate::Busy(raw) => {
583                    live.carried = false;
584                    live.proven = false;
585                    return Ok(Answer {
586                        status: raw.status,
587                        body: raw.body,
588                        trusted: false,
589                    });
590                }
591            }
592        } else {
593            None
594        };
595        if live.stream.is_none() {
596            match connect(addr, timeout) {
597                Ok(stream) => live.stream = Some(stream),
598                Err(err) => {
599                    live.proven = false;
600                    live.carried = false;
601                    return Err(Error::Io(err));
602                }
603            }
604            // The probes ran on sockets the writer closed. This socket is
605            // the one the token rides, the third step of the same proof,
606            // not a later call.
607            live.proven = false;
608        }
609        let raw = match send(
610            &mut live,
611            &self.origin,
612            &Wire {
613                token: carried,
614                ..*call
615            },
616            timeout,
617        ) {
618            Ok(raw) => raw,
619            Err(err) => {
620                live.stream = None;
621                live.proven = false;
622                live.carried = false;
623                return Err(Error::Io(err));
624            }
625        };
626        let trusted = carried.is_some();
627        live.carried = trusted;
628        if trusted && live.stream.is_some() {
629            live.proven = true;
630        } else if live.stream.is_none() {
631            live.proven = false;
632        }
633        Ok(Answer {
634            status: raw.status,
635            body: raw.body,
636            trusted,
637        })
638    }
639
640    /// Two refusals on this socket, then the call may carry the token.
641    /// A busy writer (503) is returned as that answer; the call is not sent.
642    fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
643        if live.proven && live.stream.is_some() {
644            return Ok(Gate::Send);
645        }
646        live.proven = false;
647        let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
648            return Err(Error::Io(std::io::Error::other(
649                "no randomness for the pack proof",
650            )));
651        };
652        let workspace = self.proof_workspace();
653        let path = format!("/v1/atoms/{}", &id[..32]);
654        let query = [("workspace", workspace.as_str())];
655        let mut stage = 0u8;
656        let mut restarts = 0u8;
657        loop {
658            if live.stream.is_none() {
659                live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
660                live.proven = false;
661            }
662            let shown = if stage == 0 {
663                None
664            } else {
665                Some(bogus.as_str())
666            };
667            let raw = match send(
668                live,
669                &self.origin,
670                &Wire {
671                    method: "GET",
672                    path: &path,
673                    query: &query,
674                    body: None,
675                    token: shown,
676                },
677                timeout,
678            ) {
679                Ok(raw) => raw,
680                Err(err) => {
681                    live.stream = None;
682                    live.proven = false;
683                    restarts += 1;
684                    if restarts > 2 {
685                        return Err(Error::Io(err));
686                    }
687                    stage = 0;
688                    continue;
689                }
690            };
691            if raw.status == 503 {
692                return Ok(Gate::Busy(raw));
693            }
694            if raw.status != 401 {
695                return Ok(Gate::Bare);
696            }
697            if stage == 0 {
698                stage = 1;
699                continue;
700            }
701            return Ok(Gate::Send);
702        }
703    }
704
705    fn proof_workspace(&self) -> String {
706        if let Some(workspace) = &self.workspace {
707            return workspace.clone();
708        }
709        std::env::var("PACKSET_WORKSPACE")
710            .ok()
711            .filter(|w| !w.is_empty())
712            .unwrap_or_else(|| "seat".to_string())
713    }
714
715    /// [`Self::roundtrip`] again while the writer answers busy, inside `timeout`.
716    fn tried(
717        &self,
718        method: &str,
719        path: &str,
720        query: &[(&str, &str)],
721        body: Option<&[u8]>,
722        timeout: Duration,
723    ) -> Result<Answer, Error> {
724        let started = Instant::now();
725        let mut wait = Duration::from_millis(50);
726        for n in 0..4 {
727            let answer = self.roundtrip(method, path, query, body, true, timeout)?;
728            if answer.status == 503 && n < 3 && started.elapsed() + wait < timeout {
729                std::thread::sleep(wait);
730                wait *= 3;
731                continue;
732            }
733            return Ok(answer);
734        }
735        Err(Error::Bad(format!(
736            "{}{path}: 503: packsetd is busy",
737            self.base
738        )))
739    }
740
741    fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
742        if !(200..300).contains(&answer.status) {
743            return Err(refused(
744                &format!("{}{path}", self.base),
745                answer.status,
746                &answer.body,
747            ));
748        }
749        serde_json::from_str(&answer.body).map_err(Error::from)
750    }
751
752    pub fn health(&self) -> Result<String, Error> {
753        let answer = self.tried("GET", "/health", &[], None, budget())?;
754        if !(200..300).contains(&answer.status) {
755            return Err(refused(
756                &format!("{}/health", self.base),
757                answer.status,
758                &answer.body,
759            ));
760        }
761        Ok(answer.body)
762    }
763
764    pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
765        let query: Vec<(&str, &str)> = workspace
766            .map(|workspace| vec![("workspace", workspace)])
767            .unwrap_or_default();
768        let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
769        self.json_ok("/v1/status", answer)
770    }
771
772    pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
773        let bytes = serde_json::to_vec(atom)?;
774        let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
775        self.json_ok("/v1/atoms", answer)
776    }
777
778    pub fn delete_atom(
779        &self,
780        workspace: &str,
781        id: &str,
782        why: Option<&str>,
783    ) -> Result<Value, Error> {
784        let mut body = serde_json::json!({"workspace": workspace, "id": id});
785        if let Some(accession) = why {
786            body["why"] = Value::String(accession.to_string());
787        }
788        let bytes = serde_json::to_vec(&body)?;
789        let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
790        if answer.status == 404 {
791            return Err(Error::Bad(format!("no atom {id}")));
792        }
793        self.json_ok("/v1/atoms/delete", answer)
794    }
795
796    pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
797        let answer = self.tried(
798            "GET",
799            "/v1/atoms",
800            &[("workspace", workspace), ("kind", kind)],
801            None,
802            budget(),
803        )?;
804        let body: Value = self.json_ok("/v1/atoms", answer)?;
805        Ok(body
806            .get("atoms")
807            .and_then(Value::as_array)
808            .cloned()
809            .unwrap_or_default())
810    }
811
812    pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
813        let answer = self.tried(
814            "GET",
815            "/v1/pack",
816            &[("workspace", workspace), ("set", set)],
817            None,
818            budget(),
819        )?;
820        let body: Value = self.json_ok("/v1/pack", answer)?;
821        Ok(body
822            .get("atoms")
823            .and_then(Value::as_array)
824            .cloned()
825            .unwrap_or_default())
826    }
827
828    pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
829        let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
830        let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
831        self.json_ok("/v1/sweep", answer)
832    }
833
834    pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
835        let bytes = serde_json::to_vec(&serde_json::json!({
836            "workspace": workspace,
837            "id": id,
838            "recalled": recalled,
839        }))?;
840        let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
841        self.json_ok("/v1/grade", answer)
842    }
843
844    pub fn activate_as(
845        &self,
846        workspace: &str,
847        q: &str,
848        limit: u32,
849        fire: bool,
850        lens: Option<&str>,
851    ) -> Result<Value, Error> {
852        let limit = limit.to_string();
853        let fire = if fire { "1" } else { "0" };
854        let mut query = vec![
855            ("workspace", workspace),
856            ("q", q),
857            ("limit", limit.as_str()),
858            ("fire", fire),
859        ];
860        if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
861            query.push(("as", name));
862        }
863        let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
864        self.json_ok("/v1/activate", answer)
865    }
866
867    pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
868        let limit = limit.to_string();
869        let answer = self.tried(
870            "GET",
871            "/v1/hubs",
872            &[("workspace", workspace), ("limit", limit.as_str())],
873            None,
874            budget(),
875        )?;
876        self.json_ok("/v1/hubs", answer)
877    }
878
879    pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
880        let bytes =
881            serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
882        let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
883        self.json_ok("/v1/consolidate", answer)
884    }
885
886    pub fn search_opts(
887        &self,
888        workspace: &str,
889        q: &str,
890        limit: u32,
891        as_of: Option<&str>,
892        rerank: bool,
893    ) -> Result<Vec<Hit>, Error> {
894        Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
895    }
896
897    pub(crate) fn search_fetched(
898        &self,
899        workspace: &str,
900        q: &str,
901        limit: u32,
902        as_of: Option<&str>,
903        rerank: bool,
904    ) -> Result<(Vec<Hit>, bool), Error> {
905        let limit_s = limit.to_string();
906        let mut query = vec![
907            ("workspace", workspace),
908            ("q", q),
909            ("limit", limit_s.as_str()),
910        ];
911        if let Some(at) = as_of {
912            query.push(("as_of", at));
913        }
914        if rerank {
915            query.push(("rerank", "1"));
916        }
917        let timeout = if rerank {
918            Duration::from_secs(60).max(budget())
919        } else {
920            budget()
921        };
922        let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
923        let trusted = answer.trusted;
924        let body: Value = self.json_ok("/v1/search", answer)?;
925        let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
926        Ok((serde_json::from_value(hits)?, trusted))
927    }
928
929    pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
930        let bytes = serde_json::to_vec(&serde_json::json!({
931            "workspace": workspace,
932            "ids": ids,
933            "as": "",
934        }))?;
935        let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
936        self.json_ok("/v1/fire", answer)
937    }
938}
939
940fn refused(url: &str, code: u16, body: &str) -> Error {
941    let reason = serde_json::from_str::<Value>(body)
942        .ok()
943        .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
944        .unwrap_or_else(|| body.to_string());
945    let reason = reason.trim();
946    if code == 401 {
947        return Error::Bad(format!(
948            "{url}: 401: {reason}; this seat's token does not match the writer's, \
949             so it is another user's writer or PACKSET_HOME names another pack"
950        ));
951    }
952    if reason.is_empty() {
953        Error::Bad(format!("{url}: status code {code}"))
954    } else {
955        Error::Bad(format!("{url}: {code}: {reason}"))
956    }
957}
958
959fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
960    if side_budget_spent() {
961        return Err(budget_spent());
962    }
963    let started = Instant::now();
964    let result = send_once(live, origin, call, timeout);
965    // An answer is not charged. A read that ends without one is a quiet
966    // socket and spends the budget, so the next read does not open another.
967    if result.is_err() {
968        super::charge_side_wait(started.elapsed());
969    }
970    result
971}
972
973fn send_once(
974    live: &mut Slot,
975    origin: &Url,
976    call: &Wire<'_>,
977    timeout: Duration,
978) -> std::io::Result<Raw> {
979    let stream = live
980        .stream
981        .as_mut()
982        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
983    let wait = capped_wait(timeout);
984    let deadline = Instant::now() + wait;
985    stream.set_read_timeout(Some(wait))?;
986    stream.set_write_timeout(Some(wait))?;
987    let bytes = render(
988        call.method,
989        &host_header(origin),
990        &target(call.path, call.query),
991        call.token,
992        call.body,
993    );
994    if let Err(err) = write_all(stream, &bytes) {
995        live.stream = None;
996        live.proven = false;
997        return Err(err);
998    }
999    match read_response(stream, deadline) {
1000        Ok(raw) => {
1001            if !raw.keep {
1002                live.stream = None;
1003                live.proven = false;
1004            }
1005            Ok(raw)
1006        }
1007        Err(err) => {
1008            live.stream = None;
1009            live.proven = false;
1010            Err(err)
1011        }
1012    }
1013}