1use crate::serve::protocol::{HelloAgreement, PROTOCOL, RECEIPT_CAPABILITY};
12use crate::serve::receipt_fragments::{Assembler, VerifiedReceipt, minimum_cap};
13use std::io::{self, Read, Write};
14use std::num::{NonZeroU32, NonZeroU64};
15use std::os::unix::net::UnixStream;
16use std::path::{Path, PathBuf};
17use std::process::ExitCode;
18use std::sync::{Arc, mpsc};
19use std::time::Duration;
20
21use serde::{Deserialize, Deserializer, Serialize};
22use serde_json::value::RawValue;
23
24use crate::serve::{cli::Config, frame};
25
26pub mod network;
27pub mod query;
28pub mod write;
29
30pub use network::Address;
31use network::{Hangup, Stream};
32
33const USAGE: &str = "\
34usage: haematite call (--socket <path> | --config <serve.json> | --endpoint <host:port> --server-name <name>) [--pass-file <path>] [--timeout-ms N] <method> [<params> | -]
35<params> is one JSON value; - reads it from standard input; absent, the operation has no params.
36--timeout-ms bounds connecting, writing and reading; the default is 30000.
37Exit 0: answered, the result on standard output. Exit 1: could not ask; nothing was sent.
38Exit 2: refused by the service, its code and message on standard error. Exit 3: the outcome
39is not known to the caller; the service may have acted.";
40
41const DEFAULT_TIMEOUT_MS: NonZeroU64 = NonZeroU64::MIN.saturating_add(29_999);
42const HELLO_BOUND: NonZeroU32 = NonZeroU32::MIN.saturating_add(65_535);
44const ID: &str = "haem-call";
45
46pub const MAY_HAVE_APPLIED: &[&str] = &[
51 "durability_unknown",
55 "committed_unpublished",
58 "reference_missing",
62 "discard_abort_failed",
65 "catalogue_unavailable",
69 "storage_unavailable",
72 "response_limit",
75];
76
77pub use crate::serve::protocol::NOT_APPLIED_REFUSALS as KNOWN_REFUSALS;
79
80#[derive(Debug)]
82pub enum Ended {
83 Usage(String),
85 NotAsked(String),
87 Unknown(String),
90 Received {
95 why: String,
96 receipt: Arc<VerifiedReceipt>,
97 },
98}
99
100impl Ended {
101 #[must_use]
103 pub fn why(&self) -> &str {
104 match self {
105 Self::Usage(why)
106 | Self::NotAsked(why)
107 | Self::Unknown(why)
108 | Self::Received { why, .. } => why,
109 }
110 }
111}
112
113impl std::fmt::Display for Ended {
114 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 let kind = match self {
116 Self::Usage(_) => "usage",
117 Self::NotAsked(_) => "not asked",
118 Self::Unknown(_) => "outcome unknown",
119 Self::Received { .. } => "complete receipt received",
120 };
121 write!(formatter, "{kind}: {}", self.why())
122 }
123}
124
125impl std::error::Error for Ended {}
126
127#[derive(Debug)]
129pub enum Reply {
130 Answered(Box<RawValue>),
132 Refused(Refusal),
133}
134
135#[derive(Debug, Deserialize)]
137pub struct Refusal {
138 pub code: String,
139 pub message: String,
140 #[serde(default)]
141 pub path: Option<String>,
142 #[serde(skip)]
143 pub verified: Option<Arc<VerifiedReceipt>>,
144}
145
146pub struct Client {
148 stream: Stream,
149 hello: Box<RawValue>,
150 max_frame: NonZeroU32,
151 max_page: NonZeroU32,
152}
153
154#[derive(Deserialize)]
155struct Limits {
156 protocol: String,
157 capabilities: Vec<String>,
158 max_frame: NonZeroU32,
159 max_page: NonZeroU32,
160}
161
162impl Client {
163 pub fn connect(socket: &Path, timeout: Duration) -> Result<Self, Ended> {
170 Self::over(Stream::Unix(connect(socket, timeout)?), timeout)
171 }
172
173 pub fn reach(address: &Address, timeout: Duration) -> Result<Self, Ended> {
182 match address {
183 Address::Socket(socket) => Self::connect(socket, timeout),
184 Address::Network(remote) => Self::over(
185 network::open(remote, timeout)
186 .map_err(|refused| Ended::NotAsked(refused.why().to_owned()))?,
187 timeout,
188 ),
189 }
190 }
191
192 fn over(mut stream: Stream, timeout: Duration) -> Result<Self, Ended> {
194 let unset =
195 |error: io::Error| Ended::NotAsked(format!("the timeout could not be set: {error}"));
196 stream.set_read_timeout(Some(timeout)).map_err(unset)?;
197 stream.set_write_timeout(Some(timeout)).map_err(unset)?;
198 let agreement = serde_json::value::to_raw_value(&HelloAgreement::client())
199 .map_err(|error| Ended::NotAsked(format!("receipt_hello_encode_failed: {error}")))?;
200 let hello = match exchange(&mut stream, HELLO_BOUND, "hello", Some(&agreement)) {
201 Ok(Reply::Answered(hello)) => hello,
202 Ok(Reply::Refused(refusal)) => {
203 return Err(Ended::NotAsked(format!(
204 "the service refused hello: {}: {}",
205 refusal.code, refusal.message
206 )));
207 }
208 Err(Ended::Received { why, receipt }) => {
209 return Err(Ended::Received {
210 why: format!("hello failed: {why}"),
211 receipt,
212 });
213 }
214 Err(Ended::Unknown(why) | Ended::NotAsked(why) | Ended::Usage(why)) => {
215 return Err(Ended::NotAsked(format!("hello failed: {why}")));
216 }
217 };
218 let limits: Limits = serde_json::from_str(hello.get()).map_err(|error| {
219 Ended::NotAsked(format!(
220 "the service's hello did not give its limits: {error}"
221 ))
222 })?;
223 if limits.protocol != PROTOCOL {
224 return Err(Ended::NotAsked(format!(
225 "receipt_protocol_mismatch: expected {PROTOCOL}, received {}",
226 limits.protocol
227 )));
228 }
229 if !limits
230 .capabilities
231 .iter()
232 .any(|capability| capability == RECEIPT_CAPABILITY)
233 {
234 return Err(Ended::NotAsked(format!(
235 "typed_receipt_capability_missing: required {RECEIPT_CAPABILITY}"
236 )));
237 }
238 let minimum = minimum_cap().map_err(|error| Ended::NotAsked(error.to_string()))?;
239 if u64::from(limits.max_frame.get())
240 < u64::try_from(minimum).map_err(|error| Ended::NotAsked(error.to_string()))?
241 {
242 return Err(Ended::NotAsked(format!(
243 "receipt_frame_too_small: offered {}, minimum {minimum}",
244 limits.max_frame
245 )));
246 }
247 Ok(Self {
248 stream,
249 hello,
250 max_frame: limits.max_frame,
251 max_page: limits.max_page,
252 })
253 }
254
255 #[must_use]
257 pub fn hello(&self) -> &RawValue {
258 &self.hello
259 }
260
261 #[must_use]
263 pub const fn max_frame(&self) -> NonZeroU32 {
264 self.max_frame
265 }
266
267 #[must_use]
269 pub const fn max_page(&self) -> NonZeroU32 {
270 self.max_page
271 }
272
273 pub fn call(&mut self, method: &str, params: Option<&RawValue>) -> Result<Reply, Ended> {
284 let reply = exchange(&mut self.stream, self.max_frame, method, params)?;
285 let Reply::Refused(refusal) = reply else {
286 return Ok(reply);
287 };
288 if method != "mutation.apply" || KNOWN_REFUSALS.contains(&refusal.code.as_str()) {
290 return Ok(Reply::Refused(refusal));
291 }
292 let why = if MAY_HAVE_APPLIED.contains(&refusal.code.as_str()) {
293 "the change may have been applied"
294 } else {
295 "this client does not know that code, so whether the change was applied is not known"
296 };
297 let why = format!(
298 "the service answered {}: {}; {why}",
299 refusal.code, refusal.message
300 );
301 match refusal.verified {
302 Some(receipt) => Err(Ended::Received { why, receipt }),
303 None => Err(Ended::Unknown(why)),
304 }
305 }
306
307 pub fn next_reply(&mut self) -> Result<Reply, Ended> {
316 receive(&mut self.stream, self.max_frame)
317 }
318
319 pub(crate) fn follow_socket(&self) -> io::Result<Hangup> {
321 self.stream.hangup()
322 }
323
324 pub fn read_within(&self, timeout: Option<Duration>) -> Result<(), Ended> {
330 self.stream
331 .set_read_timeout(timeout)
332 .map_err(|error| Ended::Unknown(format!("the read bound could not be set: {error}")))
333 }
334}
335
336pub enum Reached {
338 Answered,
340 Unanswered(String),
342 Refused(network::Refused),
344}
345
346#[must_use]
349pub fn probe(remote: &network::Remote, timeout: Duration) -> Reached {
350 match network::open(remote, timeout) {
351 Err(refused) => Reached::Refused(refused),
352 Ok(stream) => match Client::over(stream, timeout) {
353 Ok(_) => Reached::Answered,
354 Err(ended) => Reached::Unanswered(ended.why().to_owned()),
355 },
356 }
357}
358
359#[must_use]
361pub fn run(args: impl Iterator<Item = String>) -> ExitCode {
362 finish(call(args))
363}
364
365fn finish(outcome: Result<Reply, Ended>) -> ExitCode {
368 match outcome {
369 Ok(Reply::Answered(result)) => {
370 let mut output = io::stdout().lock();
371 match writeln!(output, "{}", result.get()).and_then(|()| output.flush()) {
372 Ok(()) => ExitCode::SUCCESS,
373 Err(error) => {
374 eprintln!(
376 "haematite: answer received, but the result could not be written to stdout: {error}"
377 );
378 ExitCode::from(3)
379 }
380 }
381 }
382 Ok(Reply::Refused(refusal)) => {
383 if let Err(error) = print_refusal(&refusal) {
384 eprintln!("haematite: receipt_output_failed: {error}");
385 }
386 ExitCode::from(2)
387 }
388 Err(ended) => {
389 if let Err(error) = print_ended(&ended) {
390 eprintln!("haematite: receipt_output_failed: {error}");
391 }
392 ExitCode::from(exit_code(&ended))
393 }
394 }
395}
396
397#[must_use]
399pub const fn exit_code(ended: &Ended) -> u8 {
400 match ended {
401 Ended::Usage(_) | Ended::NotAsked(_) => 1,
402 Ended::Unknown(_) | Ended::Received { .. } => 3,
403 }
404}
405
406#[must_use]
408pub fn refused(refusal: &Refusal) -> String {
409 let at = refusal
410 .path
411 .as_deref()
412 .map_or_else(String::new, |path| format!(" at {path}"));
413 let (code, message) = (&refusal.code, &refusal.message);
414 format!("haematite: refused {code}: {message}{at}")
415}
416
417#[must_use]
419pub fn words(ended: &Ended) -> String {
420 match ended {
421 Ended::Usage(why) => format!("haematite: {why}"),
422 Ended::NotAsked(why) => format!("haematite: could not ask: {why}"),
423 Ended::Unknown(why) => format!("haematite: the outcome is not known: {why}"),
424 Ended::Received { why, .. } => format!("haematite: complete receipt received: {why}"),
425 }
426}
427
428pub(super) fn print_refusal(refusal: &Refusal) -> io::Result<()> {
429 let mut output = io::stderr().lock();
430 writeln!(output, "{}", refused(refusal))?;
431 if let Some(receipt) = &refusal.verified {
432 output.write_all(receipt.bytes())?;
433 output.write_all(b"\n")?;
434 }
435 output.flush()
436}
437pub(super) fn print_ended(ended: &Ended) -> io::Result<()> {
438 let mut output = io::stderr().lock();
439 writeln!(output, "{}", words(ended))?;
440 if let Ended::Received { receipt, .. } = ended {
441 output.write_all(receipt.bytes())?;
442 output.write_all(b"\n")?;
443 }
444 output.flush()
445}
446
447enum Place {
448 Socket(PathBuf),
449 Config(PathBuf),
450 Endpoint {
454 endpoint: String,
455 server_name: String,
456 },
457}
458
459struct Target {
463 place: Place,
464 pass: Option<PathBuf>,
465}
466
467impl Target {
468 fn open(&self, timeout: Duration) -> Result<Client, Ended> {
471 let address = match &self.place {
472 Place::Socket(socket) => network::Address::Socket(socket.clone()),
473 Place::Config(_) => network::Address::Socket(socket_of(&self.place)?),
474 Place::Endpoint {
475 endpoint,
476 server_name,
477 } => {
478 let trust = network::Trust::system().map_err(Ended::NotAsked)?;
479 network::Address::Network(
480 network::Remote::new(endpoint, server_name, &trust).map_err(Ended::NotAsked)?,
481 )
482 }
483 };
484 let mut client = Client::reach(&address, timeout)?;
485 if let Some(file) = &self.pass {
486 let named = file.display();
487 let pass = std::fs::read_to_string(file).map_err(|error| {
488 Ended::NotAsked(format!("--pass-file {named}: {}", error.kind()))
489 })?;
490 let pass = pass.trim();
491 if pass.is_empty() {
492 return Err(Ended::NotAsked(format!("--pass-file {named}: empty")));
493 }
494 let params = serde_json::value::to_raw_value(&serde_json::json!({ "pass": pass }))
495 .map_err(|_| Ended::NotAsked("the pass could not be written".to_owned()))?;
496 if let Reply::Refused(refusal) = client.call("session.pass", Some(¶ms))? {
497 return Err(Ended::NotAsked(format!(
498 "the store refused the pass: {}: {}",
499 refusal.code, refusal.message
500 )));
501 }
502 }
503 Ok(client)
504 }
505}
506
507const ONE_PLACE: &str = "give exactly one of --socket, --config and --endpoint";
508
509#[derive(Default)]
512struct Common {
513 place: Option<Place>,
514 endpoint: Option<String>,
515 server_name: Option<String>,
516 pass: Option<PathBuf>,
517 timeout: Option<NonZeroU64>,
518}
519
520impl Common {
521 fn take(&mut self, arg: &str, args: &mut impl Iterator<Item = String>) -> Result<bool, String> {
523 match arg {
524 "--socket" | "--config" if self.place.is_none() => {
525 let path = PathBuf::from(args.next().ok_or_else(|| format!("{arg} needs a path"))?);
526 self.place = Some(if arg == "--socket" {
527 Place::Socket(path)
528 } else {
529 Place::Config(path)
530 });
531 Ok(true)
532 }
533 "--endpoint" if self.endpoint.is_none() => {
534 self.endpoint = Some(args.next().ok_or("--endpoint needs <host>:<port>")?);
535 Ok(true)
536 }
537 "--server-name" if self.server_name.is_none() => {
538 self.server_name = Some(args.next().ok_or("--server-name needs a name")?);
539 Ok(true)
540 }
541 "--pass-file" if self.pass.is_none() => {
542 self.pass = Some(PathBuf::from(
543 args.next().ok_or("--pass-file needs a path")?,
544 ));
545 Ok(true)
546 }
547 "--socket" | "--config" => Err(ONE_PLACE.to_owned()),
548 "--timeout-ms" if self.timeout.is_none() => {
549 let text = args
550 .next()
551 .ok_or("--timeout-ms needs a number of milliseconds")?;
552 let millis = text
553 .parse::<NonZeroU64>()
554 .map_err(|error| format!("--timeout-ms {text:?}: {error}"))?;
555 self.timeout = Some(millis);
556 Ok(true)
557 }
558 _ => Ok(false),
559 }
560 }
561
562 fn finish(self) -> Result<(Target, Duration), String> {
564 let place = match (self.place, self.endpoint, self.server_name) {
565 (Some(place), None, None) => place,
566 (None, Some(endpoint), Some(server_name)) => {
567 if self.pass.is_none() {
568 return Err(
569 "--endpoint needs --pass-file: the network admits only a pass".to_owned(),
570 );
571 }
572 Place::Endpoint {
573 endpoint,
574 server_name,
575 }
576 }
577 (None, Some(_), None) => return Err("--endpoint needs --server-name".to_owned()),
578 (_, None, Some(_)) => {
579 return Err("--server-name is given only with --endpoint".to_owned());
580 }
581 (None, None, None) | (Some(_), Some(_), _) => return Err(ONE_PLACE.to_owned()),
582 };
583 let millis = self.timeout.unwrap_or(DEFAULT_TIMEOUT_MS);
584 Ok((
585 Target {
586 place,
587 pass: self.pass,
588 },
589 Duration::from_millis(millis.get()),
590 ))
591 }
592}
593
594struct Call {
595 target: Target,
596 timeout: Duration,
597 method: String,
598 params: Option<Text>,
600}
601
602enum Text {
604 Given(String),
605 Stdin,
606}
607
608impl Text {
609 fn of(text: String) -> Self {
610 if text == "-" {
611 Self::Stdin
612 } else {
613 Self::Given(text)
614 }
615 }
616}
617
618fn call(args: impl Iterator<Item = String>) -> Result<Reply, Ended> {
619 let asked = parse(args).map_err(|why| Ended::Usage(format!("{why}\n{USAGE}")))?;
620 let params = asked
621 .params
622 .map(|text| one_value(text, "params"))
623 .transpose()?;
624 let mut client = asked.target.open(asked.timeout)?;
625 if asked.method == "hello" && params.is_none() {
626 return Ok(Reply::Answered(client.hello().to_owned()));
627 }
628 client.call(&asked.method, params.as_deref())
629}
630
631fn parse(mut args: impl Iterator<Item = String>) -> Result<Call, String> {
632 let mut common = Common::default();
633 let mut rest = Vec::new();
634 while let Some(arg) = args.next() {
635 if common.take(&arg, &mut args)? {
636 continue;
637 }
638 if arg.starts_with("--") {
639 return Err(format!("unknown or repeated flag {arg:?}"));
640 }
641 rest.push(arg);
642 }
643 let (target, timeout) = common.finish()?;
644 let mut rest = rest.into_iter();
645 let method = rest.next().ok_or("name the method to call")?;
646 let params = rest.next().map(Text::of);
647 if let Some(extra) = rest.next() {
648 return Err(format!("unexpected argument {extra:?}"));
649 }
650 Ok(Call {
651 target,
652 timeout,
653 method,
654 params,
655 })
656}
657
658fn one_value(given: Text, what: &str) -> Result<Box<RawValue>, Ended> {
660 let text = match given {
661 Text::Given(text) => text,
662 Text::Stdin => {
663 let mut text = String::new();
664 io::stdin().read_to_string(&mut text).map_err(|error| {
665 Ended::NotAsked(format!(
666 "the {what} could not be read from standard input: {error}"
667 ))
668 })?;
669 text
670 }
671 };
672 serde_json::from_str(&text)
673 .map_err(|error| Ended::NotAsked(format!("the {what} is not one JSON value: {error}")))
674}
675
676fn socket_of(place: &Place) -> Result<PathBuf, Ended> {
679 match place {
680 Place::Socket(path) => Ok(path.clone()),
681 Place::Endpoint { endpoint, .. } => Err(Ended::NotAsked(format!(
682 "{endpoint} is a network endpoint, not a socket"
683 ))),
684 Place::Config(path) => {
685 let named = path.display();
686 let bytes = std::fs::read(path).map_err(|error| {
687 Ended::NotAsked(format!(
688 "the service config {named} could not be read: {error}"
689 ))
690 })?;
691 let config: Config = serde_json::from_slice(&bytes)
692 .map_err(|error| Ended::NotAsked(format!("the service config {named}: {error}")))?;
693 config
694 .endpoint(false)
695 .map(|endpoint| endpoint.socket)
696 .map_err(|error| Ended::NotAsked(format!("the service config {named}: {error}")))
697 }
698 }
699}
700
701fn connect(socket: &Path, timeout: Duration) -> Result<UnixStream, Ended> {
704 let (sender, receiver) = mpsc::channel();
705 let path = socket.to_path_buf();
706 std::thread::Builder::new()
707 .name("haem-call-connect".to_owned())
708 .spawn(move || {
709 let _unwaited = sender.send(UnixStream::connect(path));
711 })
712 .map_err(|error| Ended::NotAsked(format!("the connect could not start: {error}")))?;
713 match receiver.recv_timeout(timeout) {
714 Ok(Ok(stream)) => Ok(stream),
715 Ok(Err(error)) => Err(Ended::NotAsked(format!(
716 "could not connect to {}: {error}",
717 socket.display()
718 ))),
719 Err(_) => Err(Ended::NotAsked(format!(
720 "could not connect to {} within {} ms",
721 socket.display(),
722 timeout.as_millis()
723 ))),
724 }
725}
726
727#[derive(Serialize)]
728struct Outgoing<'a> {
729 id: &'a str,
730 operation: Operation<'a>,
731}
732
733#[derive(Serialize)]
734struct Operation<'a> {
735 method: &'a str,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 params: Option<&'a RawValue>,
738}
739
740#[derive(Deserialize)]
741struct Incoming {
742 id: String,
743 #[serde(default, deserialize_with = "present")]
745 result: Option<Box<RawValue>>,
746 #[serde(default)]
747 error: Option<Refusal>,
748}
749
750fn present<'de, D: Deserializer<'de>>(input: D) -> Result<Option<Box<RawValue>>, D::Error> {
751 Box::<RawValue>::deserialize(input).map(Some)
752}
753
754#[derive(Deserialize)]
756struct Probe {
757 #[serde(default)]
758 receipt: Option<Box<RawValue>>,
759}
760
761fn exchange(
762 stream: &mut (impl Read + Write),
763 limit: NonZeroU32,
764 method: &str,
765 params: Option<&RawValue>,
766) -> Result<Reply, Ended> {
767 let operation = Operation { method, params };
768 let request = serde_json::to_vec(&Outgoing { id: ID, operation })
769 .map_err(|error| Ended::NotAsked(format!("the request could not be written: {error}")))?;
770 if !u32::try_from(request.len()).is_ok_and(|length| length <= limit.get()) {
771 return Err(Ended::NotAsked(format!(
772 "request_too_large: the request is {} bytes and the service reads at most {limit}",
773 request.len()
774 )));
775 }
776 frame::write(stream, &request, limit).map_err(|error| lost("sending the request", &error))?;
777 receive(stream, limit)
778}
779
780fn receive(stream: &mut impl Read, limit: NonZeroU32) -> Result<Reply, Ended> {
782 let bytes = match frame::read(stream, limit) {
783 Ok(Some(bytes)) => bytes,
784 Ok(None) => {
785 return Err(Ended::Unknown(
786 "the service closed the connection before answering".to_owned(),
787 ));
788 }
789 Err(error) => return Err(lost("waiting for the answer", &error)),
790 };
791 let probe: Probe = serde_json::from_slice(&bytes)
792 .map_err(|error| Ended::Unknown(format!("receipt_frame_decode_failed: {error}")))?;
793 if probe.receipt.is_none() {
794 return reply(&bytes);
795 }
796 let mut assembler =
797 Assembler::new(ID, limit.get()).map_err(|error| Ended::Unknown(error.to_string()))?;
798 let mut body = bytes;
799 loop {
800 if let Some(complete) = assembler
801 .accept(&body)
802 .map_err(|error| Ended::Unknown(error.to_string()))?
803 {
804 let complete = Arc::new(complete);
805 let refusal = complete.refusal().ok_or_else(|| Ended::Received {
806 why: "receipt_refusal_missing".to_owned(),
807 receipt: Arc::clone(&complete),
808 })?;
809 return Ok(Reply::Refused(Refusal {
810 code: refusal.code.clone(),
811 message: refusal.message.clone(),
812 path: refusal.path.clone(),
813 verified: Some(complete),
814 }));
815 }
816 body = match frame::read(stream, limit) {
817 Ok(Some(body)) => body,
818 Ok(None) => return Err(Ended::Unknown("receipt_connection_closed".to_owned())),
819 Err(error) => {
820 return Err(Ended::Unknown(format!(
821 "receipt_connection_failed: {error}"
822 )));
823 }
824 };
825 }
826}
827
828fn lost(doing: &str, error: &io::Error) -> Ended {
829 match error.kind() {
830 io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
831 Ended::Unknown(format!("timed out {doing}: {error}"))
832 }
833 _ => Ended::Unknown(format!("the connection failed {doing}: {error}")),
834 }
835}
836
837fn reply(bytes: &[u8]) -> Result<Reply, Ended> {
838 let unreadable = |why: String| Ended::Unknown(format!("the answer could not be read: {why}"));
839 let incoming: Incoming =
840 serde_json::from_slice(bytes).map_err(|error| unreadable(error.to_string()))?;
841 if incoming.id != ID {
842 return Err(unreadable(format!(
843 "it answers {:?}, not {ID:?}",
844 incoming.id
845 )));
846 }
847 match (incoming.result, incoming.error) {
848 (Some(result), None) => Ok(Reply::Answered(result)),
849 (None, Some(refusal)) => Ok(Reply::Refused(refusal)),
850 _ => Err(unreadable(
851 "it carries neither or both of result and error".to_owned(),
852 )),
853 }
854}
855
856#[cfg(test)]
857mod nonzero_tests {
858 use super::{Common, DEFAULT_TIMEOUT_MS, NonZeroU64, PathBuf, Place};
859
860 #[test]
861 fn nonzero_timeout_readback() -> Result<(), Box<dyn std::error::Error>> {
862 let cases = [
863 (None, DEFAULT_TIMEOUT_MS.get()),
864 (Some(1), 1),
865 (Some(37), 37),
866 (Some(u64::MAX), u64::MAX),
867 ];
868 for (given, expected) in cases {
869 let timeout = given
870 .map(|value| NonZeroU64::new(value).ok_or("zero timeout"))
871 .transpose()?;
872 let common = Common {
873 place: Some(Place::Socket(PathBuf::from("unused.sock"))),
874 timeout: std::hint::black_box(timeout),
875 ..Common::default()
876 };
877 let (_, actual) = common.finish()?;
878 assert_eq!(actual.as_millis(), u128::from(expected));
879 }
880 Ok(())
881 }
882}
883
884#[cfg(test)]
885mod json_text_refusal_tests {
886 use super::{KNOWN_REFUSALS, Refusal, Reply, finish, refused};
887 use std::process::ExitCode;
888
889 #[test]
890 fn json_text_invalid_is_a_named_client_refusal() {
891 let refusal = Refusal {
892 code: "json_text_invalid".to_owned(),
893 message: "value is not JSON".to_owned(),
894 path: Some("/payload".to_owned()),
895 verified: None,
896 };
897 assert!(KNOWN_REFUSALS.contains(&refusal.code.as_str()));
898 assert_eq!(
899 refused(&refusal),
900 "haematite: refused json_text_invalid: value is not JSON at /payload"
901 );
902 assert_eq!(finish(Ok(Reply::Refused(refusal))), ExitCode::from(2));
903 }
904}
905
906#[cfg(test)]
907mod access_refusal_tests {
908 use super::KNOWN_REFUSALS;
909
910 #[test]
913 fn every_access_refusal_is_known_not_to_have_applied() {
914 for code in [
915 "route_unclassified",
916 "permission_refused",
917 "held_for_approval",
918 "pass_missing",
919 "lys_could_not_be_asked",
920 "signature_refused",
921 "pass_expired",
922 ] {
923 assert!(KNOWN_REFUSALS.contains(&code), "{code} is not known");
924 }
925 }
926}