Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/hand/src/codec.rs

147 KiB, 1 run

created by r2519314175:911, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Turning the wire's messages into bytes, and hostile bytes back into messages.
2//!
3//! Two concerns live here and they are kept apart on purpose:
4//!
5//! * **The JSON.** Every [`wire::Req`] and [`wire::Resp`] is a tagged object
6//! carrying a `"t"` discriminator, built and read through `Dat` and
7//! `fe2o3_jdat`'s JSON encoder rather than a parser written here. The
8//! messages are transport-neutral: the same text is what a native messaging
9//! host writes to stdout and what a WebSocket carries as one text payload.
10//! * **The framing.** [`Frame`] says how one message is marked off from the
11//! next, and it is an enum rather than a trait object because there are two
12//! answers and there will not be many more.
13//!
14//! Everything that reads is written for input the hand did not create. A short
15//! read, a truncated body, a length prefix that promises a gigabyte, bytes that
16//! are not UTF-8, JSON of the wrong shape and a `"t"` from a future build each
17//! produce a named [`Fault`], and none of them panics.
18//!
19//! # The terminal half
20//!
21//! The pty messages carry their payload as base64 rather than as text, because a
22//! terminal is a byte stream and a lossy conversion corrupts exactly the case a
23//! terminal exists for. Two consequences land here:
24//!
25//! * The base64 is [`oxedyne_fe2o3_text::base64`], which is strict on decode --
26//! non-alphabet characters, misplaced padding, a length that is not a whole
27//! quantum and non-zero trailing bits are all refused. `data` arrives from a
28//! page, so a decoder that guesses is a decoder that can be steered.
29//! * Sizing a [`Resp::Output`] is [`output_frames`], not arithmetic. Base64
30//! inflates by four thirds *before* the JSON envelope is paid for, and the
31//! envelope holds a caller-supplied identifier, so a byte budget subtracted
32//! from [`FRAME_MAX`] is wrong in both directions.
33
34use crate::wire::{
35 Breaks,
36 Capture,
37 FenceSpec,
38 FileOp,
39 PtySize,
40 Req,
41 Resp,
42 Run,
43 RunState,
44 Sig,
45 Stream,
46 CHUNK_MAX,
47 FRAME_MAX,
48 RUNS_MAX,
49 RUN_WHAT_MAX,
50 SAFE_INT_MAX,
51};
52
53use oxedyne_fe2o3_core::prelude::*;
54use oxedyne_fe2o3_text::base64;
55use oxedyne_fe2o3_jdat::prelude::*;
56use oxedyne_fe2o3_jdat::{
57 bdat::limits::DecodeLimits,
58 string::dec::DecoderConfig,
59 usr::{
60 UsrKind,
61 UsrKindCode,
62 UsrKindId,
63 },
64};
65
66use std::{
67 collections::BTreeMap,
68 io::{
69 Read,
70 Write,
71 },
72};
73
74// ┌───────────────────────────────────────────────────────────────┐
75// │ Limits │
76// └───────────────────────────────────────────────────────────────┘
77
78/// The width of the native messaging length prefix, in bytes.
79///
80/// Four, native-endian, because that is what Chrome writes and reads. It is
81/// not a choice and it is not negotiable.
82pub const LEN_PREFIX: usize = 4;
83
84/// The largest inbound frame the hand will accept.
85///
86/// Chrome allows an extension→host message of up to 4 GB, which is an offer the
87/// hand declines: nothing the page has to say is larger than what the hand is
88/// allowed to say back, so the same ceiling applies in both directions and a
89/// hostile prefix is refused before a byte of body is read.
90pub const INBOUND_MAX: usize = FRAME_MAX;
91
92/// Greatest nesting the decoder will descend to.
93///
94/// The wire's deepest shape is an [`Req::Exec`] envelope holding a `fence`
95/// object holding a list holding a string, so four levels carry the protocol
96/// and the rest is slack for a decoder that counts brackets rather than values.
97const MAX_DEPTH: usize = 16;
98
99/// The largest run of raw terminal bytes a single [`Resp::Output`] carries.
100///
101/// Derived from [`CHUNK_MAX`] rather than chosen: base64 turns three bytes into
102/// four characters, so this is the byte count whose encoding is the same size as
103/// the text an exec chunk may carry, and one pty frame therefore costs the wire
104/// no more than one exec frame.
105pub const OUTPUT_MAX: usize = CHUNK_MAX / 4 * 3;
106
107// ┌───────────────────────────────────────────────────────────────┐
108// │ Faults │
109// └───────────────────────────────────────────────────────────────┘
110
111/// The named ways a frame fails to become a message, or a message a frame.
112///
113/// Named rather than merely described, because a caller deciding whether to
114/// close the connection or answer with a [`Resp::Error`] needs to know *which*
115/// thing went wrong, and a substring match on prose is not a decision procedure.
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum Fault {
118 /// The stream ended part-way through a length prefix.
119 ShortRead,
120 /// The stream ended before the declared number of payload bytes arrived.
121 Truncated,
122 /// The length prefix declared more than [`INBOUND_MAX`] bytes.
123 LengthTooBig,
124 /// The payload was not UTF-8.
125 NotUtf8,
126 /// The payload was not JSON that could be read at all.
127 NotJson,
128 /// The payload was readable JSON of the wrong shape for what it claims to be.
129 WrongShape,
130 /// The payload carried a `"t"` this build does not know.
131 UnknownTag,
132 /// The message would encode to more than [`FRAME_MAX`] bytes.
133 FrameTooBig,
134 /// The stream could not be read or written at all.
135 Io,
136}
137
138impl Fault {
139
140 /// Every fault, so a search over them is exhaustive by construction.
141 pub const ALL: [Self; 9] = [
142 Self::ShortRead,
143 Self::Truncated,
144 Self::LengthTooBig,
145 Self::NotUtf8,
146 Self::NotJson,
147 Self::WrongShape,
148 Self::UnknownTag,
149 Self::FrameTooBig,
150 Self::Io,
151 ];
152
153 /// The stable identifier that begins every message this fault raises.
154 pub fn name(&self) -> &'static str {
155 match self {
156 Self::ShortRead => "codec.short-read",
157 Self::Truncated => "codec.truncated",
158 Self::LengthTooBig => "codec.length-too-big",
159 Self::NotUtf8 => "codec.not-utf8",
160 Self::NotJson => "codec.not-json",
161 Self::WrongShape => "codec.wrong-shape",
162 Self::UnknownTag => "codec.unknown-tag",
163 Self::FrameTooBig => "codec.frame-too-big",
164 Self::Io => "codec.io",
165 }
166 }
167
168 /// Builds the error this fault raises, with the detail that identifies the case.
169 ///
170 /// # Arguments
171 /// * `detail` - What was wrong, in a sentence a developer reading a journal can use.
172 pub fn raise(&self, detail: &str) -> Error<ErrTag> {
173 match self {
174 Self::ShortRead => err!("{}: {}", self.name(), detail; Input, Decode, Missing),
175 Self::Truncated => err!("{}: {}", self.name(), detail; Input, Decode, Missing),
176 Self::LengthTooBig => err!("{}: {}", self.name(), detail; Input, Decode, TooBig),
177 Self::NotUtf8 => err!("{}: {}", self.name(), detail; Input, Decode, UTF8),
178 Self::NotJson => err!("{}: {}", self.name(), detail; Input, Decode, Invalid),
179 Self::WrongShape => err!("{}: {}", self.name(), detail; Input, Decode, Mismatch),
180 Self::UnknownTag => err!("{}: {}", self.name(), detail; Input, Decode, Unknown),
181 Self::FrameTooBig => err!("{}: {}", self.name(), detail; Output, Encode, TooBig),
182 Self::Io => err!("{}: {}", self.name(), detail; IO, Wire),
183 }
184 }
185
186 /// The fault an error carries, where it carries one.
187 ///
188 /// # Arguments
189 /// * `e` - The error to inspect.
190 pub fn of(e: &Error<ErrTag>) -> Option<Self> {
191 for msg in e.msgs() {
192 for f in Self::ALL {
193 if msg.starts_with(f.name()) {
194 return Some(f);
195 }
196 }
197 }
198 None
199 }
200}
201
202// ┌───────────────────────────────────────────────────────────────┐
203// │ Enum vocabulary │
204// └───────────────────────────────────────────────────────────────┘
205
206/// The wire spelling of a [`Capture`].
207///
208/// # Arguments
209/// * `c` - The capture mode.
210pub fn capture_name(c: Capture) -> &'static str {
211 match c {
212 Capture::Both => "both",
213 Capture::Out => "out",
214 Capture::Err => "err",
215 Capture::None => "none",
216 }
217}
218
219/// The [`Capture`] a wire spelling names.
220///
221/// # Arguments
222/// * `s` - The word from the `capture` field.
223pub fn capture_of(s: &str) -> Outcome<Capture> {
224 match s {
225 "both" => Ok(Capture::Both),
226 "out" => Ok(Capture::Out),
227 "err" => Ok(Capture::Err),
228 "none" => Ok(Capture::None),
229 other => Err(Fault::WrongShape.raise(&fmt!(
230 "The capture mode {:?} is not one of both, out, err or none.", other))),
231 }
232}
233
234/// The wire spelling of a [`Sig`].
235///
236/// # Arguments
237/// * `s` - The signal.
238pub fn sig_name(s: Sig) -> &'static str {
239 match s {
240 Sig::Term => "term",
241 Sig::Kill => "kill",
242 Sig::Int => "int",
243 }
244}
245
246/// The [`Sig`] a wire spelling names.
247///
248/// # Arguments
249/// * `s` - The word from the `sig` field.
250pub fn sig_of(s: &str) -> Outcome<Sig> {
251 match s {
252 "term" => Ok(Sig::Term),
253 "kill" => Ok(Sig::Kill),
254 "int" => Ok(Sig::Int),
255 other => Err(Fault::WrongShape.raise(&fmt!(
256 "The signal {:?} is not one of term, kill or int.", other))),
257 }
258}
259
260/// The wire spelling of a [`Stream`].
261///
262/// # Arguments
263/// * `s` - The stream.
264pub fn stream_name(s: Stream) -> &'static str {
265 match s {
266 Stream::Out => "out",
267 Stream::Err => "err",
268 }
269}
270
271/// The [`Stream`] a wire spelling names.
272///
273/// # Arguments
274/// * `s` - The word from the `stream` field.
275pub fn stream_of(s: &str) -> Outcome<Stream> {
276 match s {
277 "out" => Ok(Stream::Out),
278 "err" => Ok(Stream::Err),
279 other => Err(Fault::WrongShape.raise(&fmt!(
280 "The stream {:?} is neither out nor err.", other))),
281 }
282}
283
284// ┌───────────────────────────────────────────────────────────────┐
285// │ Bytes on the wire │
286// └───────────────────────────────────────────────────────────────┘
287
288/// The wire spelling of a run of raw bytes.
289///
290/// Standard, padded RFC 4648 §4 base64, which is what a browser's `atob` reads
291/// and what the `data` field of [`Req::Input`] and [`Resp::Output`] holds.
292///
293/// # Arguments
294/// * `bytes` - The raw bytes.
295pub fn data_encode(bytes: &[u8]) -> String {
296 base64::encode(bytes)
297}
298
299/// The bytes a `data` field carries, refusing anything that is not base64.
300///
301/// Strict, because this is attacker-reachable input: a character outside the
302/// alphabet, whitespace, the URL-safe alphabet, padding in the wrong place, a
303/// length that is not a whole four-character quantum and a final quantum whose
304/// unused bits are set are each refused rather than guessed at. Two decoders
305/// that disagree about the same string are how a page and a hand end up holding
306/// different bytes and both believing they agree.
307///
308/// # Arguments
309/// * `data` - The base64 text.
310pub fn data_decode(data: &str) -> Outcome<Vec<u8>> {
311 match base64::decode(data) {
312 Ok(v) => Ok(v),
313 Err(e) => Err(Fault::WrongShape.raise(&fmt!(
314 "The \"data\" field is not standard base64: {}.", e.msgs().join("; ")))),
315 }
316}
317
318/// Refuses a `data` field that is not base64, without keeping the bytes.
319///
320/// # Arguments
321/// * `data` - The base64 text.
322fn data_check(data: &str) -> Outcome<()> {
323 res!(data_decode(data));
324 Ok(())
325}
326
327/// The `Dat` object a terminal size encodes to.
328///
329/// # Arguments
330/// * `size` - The size.
331fn size_dat(size: &PtySize) -> Dat {
332 omapdat!{
333 "cols" => size.cols,
334 "rows" => size.rows,
335 }
336}
337
338// ┌───────────────────────────────────────────────────────────────┐
339// │ Reading fields out of a decoded object │
340// └───────────────────────────────────────────────────────────────┘
341
342/// The decoder configuration every inbound payload is read through.
343///
344/// JSON rather than JDAT, and bounded in both length and depth, because the
345/// text arrives from somewhere the hand does not control.
346fn dec_cfg() -> DecoderConfig<BTreeMap<UsrKindCode, UsrKind>, BTreeMap<String, UsrKindId>> {
347 DecoderConfig::json(None)
348 .with_limits(DecodeLimits::new(MAX_DEPTH, INBOUND_MAX))
349}
350
351/// The value at a key, or a fault naming the message and the key that is missing.
352///
353/// # Arguments
354/// * `obj` - The decoded object.
355/// * `what` - The message being read, for the error.
356/// * `key` - The field wanted.
357fn field<'a>(obj: &'a Dat, what: &str, key: &str) -> Outcome<&'a Dat> {
358 match res!(obj.map_get(&Dat::Str(key.to_string()))) {
359 Some(v) => Ok(v),
360 None => Err(Fault::WrongShape.raise(&fmt!(
361 "A {:?} message has no {:?} field.", what, key))),
362 }
363}
364
365/// A string field.
366///
367/// # Arguments
368/// * `obj` - The decoded object.
369/// * `what` - The message being read, for the error.
370/// * `key` - The field wanted.
371fn str_field(obj: &Dat, what: &str, key: &str) -> Outcome<String> {
372 let v = res!(field(obj, what, key));
373 match v.get_string() {
374 Some(s) => Ok(s),
375 None => Err(Fault::WrongShape.raise(&fmt!(
376 "The {:?} field of a {:?} message is not a string.", key, what))),
377 }
378}
379
380/// A boolean field.
381///
382/// # Arguments
383/// * `obj` - The decoded object.
384/// * `what` - The message being read, for the error.
385/// * `key` - The field wanted.
386fn bool_field(obj: &Dat, what: &str, key: &str) -> Outcome<bool> {
387 let v = res!(field(obj, what, key));
388 match v.get_bool() {
389 Some(b) => Ok(b),
390 None => Err(Fault::WrongShape.raise(&fmt!(
391 "The {:?} field of a {:?} message is not a boolean.", key, what))),
392 }
393}
394
395/// A `u32` field.
396///
397/// # Arguments
398/// * `obj` - The decoded object.
399/// * `what` - The message being read, for the error.
400/// * `key` - The field wanted.
401fn u32_field(obj: &Dat, what: &str, key: &str) -> Outcome<u32> {
402 let v = res!(field(obj, what, key));
403 match v.get_u32() {
404 Some(n) => Ok(n),
405 None => Err(Fault::WrongShape.raise(&fmt!(
406 "The {:?} field of a {:?} message is not a number in 0..=u32::MAX.", key, what))),
407 }
408}
409
410/// A `u64` field.
411///
412/// # Arguments
413/// * `obj` - The decoded object.
414/// * `what` - The message being read, for the error.
415/// * `key` - The field wanted.
416fn u64_field(obj: &Dat, what: &str, key: &str) -> Outcome<u64> {
417 let v = res!(field(obj, what, key));
418 match v.get_u64() {
419 Some(n) => Ok(n),
420 None => Err(Fault::WrongShape.raise(&fmt!(
421 "The {:?} field of a {:?} message is not a number in 0..=u64::MAX.", key, what))),
422 }
423}
424
425/// An `i32` field, which is how an exit status arrives.
426///
427/// # Arguments
428/// * `obj` - The decoded object.
429/// * `what` - The message being read, for the error.
430/// * `key` - The field wanted.
431fn i32_field(obj: &Dat, what: &str, key: &str) -> Outcome<i32> {
432 let v = res!(field(obj, what, key));
433 match v.get_i32() {
434 Some(n) => Ok(n),
435 None => Err(Fault::WrongShape.raise(&fmt!(
436 "The {:?} field of a {:?} message is not a number in i32::MIN..=i32::MAX.",
437 key, what))),
438 }
439}
440
441/// A `u16` field, which is how a terminal dimension arrives.
442///
443/// One check catches four hostile shapes at once: a negative number decodes to a
444/// signed daticle, a fractional one to a float, and anything above 65535 to a
445/// wider unsigned one, none of which [`Dat::get_u16`] answers.
446///
447/// # Arguments
448/// * `obj` - The decoded object.
449/// * `what` - The message being read, for the error.
450/// * `key` - The field wanted.
451fn u16_field(obj: &Dat, what: &str, key: &str) -> Outcome<u16> {
452 let v = res!(field(obj, what, key));
453 match v.get_u16() {
454 Some(n) => Ok(n),
455 None => Err(Fault::WrongShape.raise(&fmt!(
456 "The {:?} field of a {:?} message is not a whole number in 0..=65535.",
457 key, what))),
458 }
459}
460
461/// A whole-number field, held to [`SAFE_INT_MAX`] (`REVIEW.md` §3.11).
462///
463/// Every `u64` on the wire comes through here, so there is one answer to "what
464/// happens past 2^53" rather than one per field, and a `u64` added to the wire
465/// later inherits it by being read the same way.
466///
467/// # Why the number is refused rather than clamped
468///
469/// Clamping was considered and rejected, because the worst defect of the
470/// reviewed session was exactly that shape: `extract_json_number` in
471/// `src/tools.rs` parsed an exit status as `u64`, so `-1` failed to parse and
472/// `.unwrap_or(0)` made it zero -- and a **killed** `cargo test` was presented
473/// to the model as `[exit code: 0]`, a crashed build read as a green one
474/// (`REVIEW.md` §1.11). Nothing about it was loud.
475///
476/// A clamped number here would be the same trade. `Resp::Ended`'s byte counts
477/// exist so the page can notice that output went missing (§1.17), and a count
478/// lowered to fit would say that none did; a clamped `seq` breaks the gap
479/// detection it exists for, because two frames would compare equal; a clamped
480/// `timeout_ms` is a limit the caller did not ask for. String encoding was the
481/// other candidate and was rejected too: it moves the same ceiling into every
482/// reader's `BigInt` handling and changes the contract for four fields to fix a
483/// case none of them can reach.
484///
485/// So the rule is the one with no silent arm. A number that would not survive
486/// the crossing is a named [`Fault`] on decode and a refusal to write on encode;
487/// both ends fail at the boundary, and neither invents a value the far end would
488/// believe. Nothing legitimate is refused -- the ceiling is 800 exabytes of
489/// terminal output, nine petabytes down one pipe, or a wall-clock limit of
490/// 285,000 years -- so a number that reaches it is a counter that went wrong or a
491/// sender that is not the page, and both are worth hearing about.
492///
493/// # Arguments
494/// * `obj` - The decoded object.
495/// * `what` - The message being read, for the error.
496/// * `key` - The field wanted.
497fn safe_int_field(obj: &Dat, what: &str, key: &str) -> Outcome<u64> {
498 let n = res!(u64_field(obj, what, key));
499 if n > SAFE_INT_MAX {
500 return Err(Fault::WrongShape.raise(&fmt!(
501 "The {:?} field of a {:?} message is {}, and the largest whole \
502 number both ends can carry unchanged is {}. A larger one does not \
503 survive the page's JSON.parse, so whichever end read it would be \
504 acting on a rounded number without being told.", key, what, n,
505 SAFE_INT_MAX)));
506 }
507 Ok(n)
508}
509
510/// A base64 field, kept as its wire spelling once it is known to be base64.
511///
512/// # Arguments
513/// * `obj` - The decoded object.
514/// * `what` - The message being read, for the error.
515/// * `key` - The field wanted.
516fn b64_field(obj: &Dat, what: &str, key: &str) -> Outcome<String> {
517 let s = res!(str_field(obj, what, key));
518 match base64::decode(&s) {
519 Ok(_) => Ok(s),
520 Err(e) => Err(Fault::WrongShape.raise(&fmt!(
521 "The {:?} field of a {:?} message is not standard base64: {}.",
522 key, what, e.msgs().join("; ")))),
523 }
524}
525
526/// The terminal size field.
527///
528/// # Arguments
529/// * `obj` - The decoded object.
530/// * `what` - The message being read, for the error.
531/// * `key` - The field wanted.
532fn size_field(obj: &Dat, what: &str, key: &str) -> Outcome<PtySize> {
533 let v = res!(field(obj, what, key));
534 res!(want_object(v, "terminal size"));
535 Ok(PtySize {
536 cols: res!(u16_field(v, "size", "cols")),
537 rows: res!(u16_field(v, "size", "rows")),
538 })
539}
540
541/// A list-of-strings field.
542///
543/// # Arguments
544/// * `obj` - The decoded object.
545/// * `what` - The message being read, for the error.
546/// * `key` - The field wanted.
547fn strs_field(obj: &Dat, what: &str, key: &str) -> Outcome<Vec<String>> {
548 let v = res!(field(obj, what, key));
549 let items = match v.get_list() {
550 Some(l) => l,
551 None => return Err(Fault::WrongShape.raise(&fmt!(
552 "The {:?} field of a {:?} message is not a list.", key, what))),
553 };
554 let mut out = Vec::with_capacity(items.len());
555 for (i, item) in items.iter().enumerate() {
556 match item.get_string() {
557 Some(s) => out.push(s),
558 None => return Err(Fault::WrongShape.raise(&fmt!(
559 "Entry {} of the {:?} field of a {:?} message is not a string.",
560 i, key, what))),
561 }
562 }
563 Ok(out)
564}
565
566/// A list-of-strings field that may be absent, reading as the empty list.
567///
568/// Absence is a real answer here and it is the SAFE one: the field says which
569/// toolchains the user granted, and a request that does not mention any has
570/// granted none. So an older page, a hand-written request, and a request that
571/// says `"toolkits":[]` all mean the same thing and all fail closed. A field
572/// that is present and not a list is still an error, because that is a caller
573/// saying something this end cannot read rather than saying nothing.
574///
575/// # Arguments
576/// * `obj` - The decoded object.
577/// * `what` - The message being read, for the error.
578/// * `key` - The field wanted.
579fn opt_strs_field(obj: &Dat, what: &str, key: &str) -> Outcome<Vec<String>> {
580 match res!(obj.map_get(&Dat::Str(key.to_string()))) {
581 Some(_) => strs_field(obj, what, key),
582 None => Ok(Vec::new()),
583 }
584}
585
586/// A field that is either a string or JSON `null`.
587///
588/// An absent key reads as absent rather than as an error, since "there is no
589/// standard input" and "the field saying so was left out" are the same claim.
590///
591/// # Arguments
592/// * `obj` - The decoded object.
593/// * `what` - The message being read, for the error.
594/// * `key` - The field wanted.
595fn opt_str_field(obj: &Dat, what: &str, key: &str) -> Outcome<Option<String>> {
596 let v = match res!(obj.map_get(&Dat::Str(key.to_string()))) {
597 Some(v) => v,
598 None => return Ok(None),
599 };
600 match v {
601 Dat::Str(s) => Ok(Some(s.clone())),
602 // `null` decodes to an absent option, which is exactly what it means.
603 Dat::Opt(b) => match b.as_ref() {
604 None => Ok(None),
605 Some(Dat::Str(s)) => Ok(Some(s.clone())),
606 Some(_) => Err(Fault::WrongShape.raise(&fmt!(
607 "The {:?} field of a {:?} message is neither a string nor null.", key, what))),
608 },
609 _ => Err(Fault::WrongShape.raise(&fmt!(
610 "The {:?} field of a {:?} message is neither a string nor null.", key, what))),
611 }
612}
613
614/// The environment field, a list of two-element `[name, value]` lists.
615///
616/// # Arguments
617/// * `obj` - The decoded object.
618/// * `what` - The message being read, for the error.
619/// * `key` - The field wanted.
620fn env_field(obj: &Dat, what: &str, key: &str) -> Outcome<Vec<(String, String)>> {
621 let v = res!(field(obj, what, key));
622 let pairs = match v.get_list() {
623 Some(l) => l,
624 None => return Err(Fault::WrongShape.raise(&fmt!(
625 "The {:?} field of a {:?} message is not a list.", key, what))),
626 };
627 let mut out = Vec::with_capacity(pairs.len());
628 for (i, pair) in pairs.iter().enumerate() {
629 let kv = match pair.get_list() {
630 Some(l) => l,
631 None => return Err(Fault::WrongShape.raise(&fmt!(
632 "Entry {} of the {:?} field of a {:?} message is not a list.", i, key, what))),
633 };
634 if kv.len() != 2 {
635 return Err(Fault::WrongShape.raise(&fmt!(
636 "Entry {} of the {:?} field of a {:?} message has {} items, not the \
637 two an environment pair has.", i, key, what, kv.len())));
638 }
639 match (kv[0].get_string(), kv[1].get_string()) {
640 (Some(k), Some(val)) => out.push((k, val)),
641 _ => return Err(Fault::WrongShape.raise(&fmt!(
642 "Entry {} of the {:?} field of a {:?} message is not a pair of strings.",
643 i, key, what))),
644 }
645 }
646 Ok(out)
647}
648
649/// The `runs` field of a [`Resp::Runs`].
650///
651/// # Arguments
652/// * `obj` - The decoded object.
653fn runs_field(obj: &Dat) -> Outcome<Vec<Run>> {
654 let v = res!(field(obj, "runs", "runs"));
655 let items = match v.get_list() {
656 Some(l) => l,
657 None => return Err(Fault::WrongShape.raise(&fmt!(
658 "The \"runs\" field of a \"runs\" message is not a list."))),
659 };
660 if items.len() > RUNS_MAX {
661 return Err(Fault::WrongShape.raise(&fmt!(
662 "A \"runs\" message listed {} runs and {} is the most one carries. What did not \
663 fit is counted in \"more\" rather than sent.", items.len(), RUNS_MAX)));
664 }
665 let mut out = Vec::with_capacity(items.len());
666 for (i, it) in items.iter().enumerate() {
667 res!(want_object(it, "run"));
668 let what = res!(str_field(it, "run", "what"));
669 if what.len() > RUN_WHAT_MAX {
670 return Err(Fault::WrongShape.raise(&fmt!(
671 "Entry {} of a \"runs\" message carries a command line of {} bytes and {} is \
672 the most one carries.", i, what.len(), RUN_WHAT_MAX)));
673 }
674 out.push(Run {
675 id: res!(str_field(it, "run", "id")),
676 pid: res!(u32_field(it, "run", "pid")),
677 what,
678 state: res!(run_state_of(&res!(str_field(it, "run", "state")))),
679 secs: res!(u32_field(it, "run", "secs")),
680 });
681 }
682 Ok(out)
683}
684
685/// The word a [`RunState`] travels under, read back.
686///
687/// # Arguments
688/// * `s` - The word.
689fn run_state_of(s: &str) -> Outcome<RunState> {
690 match s {
691 "running" => Ok(RunState::Running),
692 "standing" => Ok(RunState::Standing),
693 other => Err(Fault::WrongShape.raise(&fmt!(
694 "A listed run says its state is {:?}, and the only two are \"running\" -- the \
695 command itself has not finished -- and \"standing\" -- it has, and the process \
696 group it led has not emptied.", other))),
697 }
698}
699
700/// The fence field.
701///
702/// # Arguments
703/// * `obj` - The decoded object.
704/// * `what` - The message being read, for the error.
705/// * `key` - The field wanted.
706fn fence_field(obj: &Dat, what: &str, key: &str) -> Outcome<FenceSpec> {
707 let v = res!(field(obj, what, key));
708 res!(want_object(v, "fence"));
709 Ok(FenceSpec {
710 rw: res!(strs_field(v, "fence", "rw")),
711 ro: res!(strs_field(v, "fence", "ro")),
712 deny: res!(strs_field(v, "fence", "deny")),
713 net: res!(bool_field(v, "fence", "net")),
714 })
715}
716
717/// Refuses anything that is not a JSON object.
718///
719/// The JDAT decoder is happy to read a bare string, a bare number or nothing at
720/// all, none of which is a message, so the top of every payload is checked
721/// before a field is looked for in it.
722///
723/// # Arguments
724/// * `d` - The decoded value.
725/// * `what` - What was expected, for the error.
726/// Which breaks a `verify` request asks for.
727///
728/// Two fields rather than one, because `all` and `none` are decisions and a
729/// break name is a value, and a single string field would make `all` a name
730/// somebody could give a break. `one` without a `break` is refused rather than
731/// read as `none`: a request that asked to prove something must not quietly
732/// become a request that proves nothing.
733///
734/// # Arguments
735/// * `obj` - The decoded object.
736fn breaks_of(obj: &Dat) -> Outcome<Breaks> {
737 let word = res!(str_field(obj, "verify", "breaks"));
738 match word.as_str() {
739 "all" => Ok(Breaks::All),
740 "none" => Ok(Breaks::None),
741 "one" => match res!(opt_str_field(obj, "verify", "break")) {
742 Some(b) if !b.is_empty() => Ok(Breaks::One(b)),
743 _ => Err(Fault::WrongShape.raise(
744 "A verify asking for one break did not say which. Send \"breaks\":\"one\" \
745 with a \"break\" naming one the verifier declares, or \"breaks\":\"all\".")),
746 },
747 other => Err(Fault::WrongShape.raise(&fmt!(
748 "A verify's \"breaks\" is {:?}, which is none of \"all\", \"one\" or \"none\".",
749 other))),
750 }
751}
752
753fn want_object(d: &Dat, what: &str) -> Outcome<()> {
754 match d {
755 Dat::Map(_) | Dat::OrdMap(_) => Ok(()),
756 other => Err(Fault::WrongShape.raise(&fmt!(
757 "A {} is a JSON object; this is {}.", what, other.kind()))),
758 }
759}
760
761/// The `"t"` discriminator, which says which message this is.
762///
763/// # Arguments
764/// * `obj` - The decoded object.
765fn tag_of(obj: &Dat) -> Outcome<String> {
766 match res!(obj.map_get(&Dat::Str("t".to_string()))) {
767 Some(Dat::Str(s)) => Ok(s.clone()),
768 Some(_) => Err(Fault::WrongShape.raise(
769 "The \"t\" field, which says which message this is, is not a string.")),
770 None => Err(Fault::WrongShape.raise(
771 "There is no \"t\" field, so there is no saying which message this is.")),
772 }
773}
774
775/// A list of strings as a `Dat`, without the `Vek` that `From<Vec<String>>` gives.
776///
777/// # Arguments
778/// * `v` - The strings.
779/// The start directories of a walk, which must be at least one.
780///
781/// A walk with no start would walk nothing and answer as though it had looked, which is the one
782/// shape of answer that lies. An absent list falls back to the single `path` every message
783/// carries, so a caller that sends one start need not send it twice.
784///
785/// # Arguments
786/// * `obj` - The decoded `file` request.
787/// * `path` - The message's own `path`, used where no list came.
788fn walk_paths(obj: &Dat, path: &str) -> Outcome<Vec<String>> {
789 let listed = res!(opt_strs_field(obj, "file", "paths"));
790 let out = match listed.is_empty() {
791 false => listed,
792 true => vec![path.to_string()],
793 };
794 if out.iter().all(|p| p.is_empty()) {
795 return Err(Fault::WrongShape.raise(
796 "A walk was asked for with nowhere to start. It would look at nothing and answer \
797 as though it had looked everywhere."));
798 }
799 Ok(out)
800}
801
802/// The [`FileOp`] an object carries, refused rather than guessed at.
803///
804/// **A word chosen from a closed set, never a free string.** The op decides which of the
805/// object's fields are read at all, so an unknown word cannot reach a branch that would
806/// interpret `path` or `text` as anything -- it is refused by name, in the same shape
807/// [`breaks_of`] refuses a break the verifier does not declare.
808///
809/// # Arguments
810/// * `obj` - The decoded `file` request.
811fn fileop_of(obj: &Dat) -> Outcome<FileOp> {
812 let word = res!(str_field(obj, "file", "op"));
813 let path = res!(str_field(obj, "file", "path"));
814 let text = |k: &str| -> Outcome<String> {
815 match res!(opt_str_field(obj, "file", k)) {
816 Some(t) => Ok(t),
817 None => Err(Fault::WrongShape.raise(&fmt!(
818 "A file request to {} needs a {:?} and carried none.", word, k))),
819 }
820 };
821 match word.as_str() {
822 "read" => Ok(FileOp::Read {
823 path,
824 offset: res!(u32_field(obj, "file", "offset")),
825 limit: res!(u32_field(obj, "file", "limit")),
826 }),
827 "write" => Ok(FileOp::Write { path, content: res!(text("text")) }),
828 "edit" => Ok(FileOp::Edit {
829 path,
830 old: res!(text("text")),
831 new: res!(text("text2")),
832 }),
833 "move" => Ok(FileOp::Move { path, to: res!(text("to")) }),
834 "list" => Ok(FileOp::List { path }),
835 "mkdir" => Ok(FileOp::MkDir { path }),
836 "search" => Ok(FileOp::Search {
837 paths: res!(walk_paths(obj, &path)),
838 query: res!(str_field(obj, "file", "query")),
839 ci: res!(bool_field(obj, "file", "ci")),
840 glob: res!(opt_str_field(obj, "file", "glob")).unwrap_or_default(),
841 base: res!(opt_str_field(obj, "file", "base")).unwrap_or_default(),
842 skip: res!(opt_strs_field(obj, "file", "skip")),
843 budget: res!(u32_field(obj, "file", "budget")),
844 cap: res!(u32_field(obj, "file", "cap")),
845 }),
846 "glob" => Ok(FileOp::Glob {
847 paths: res!(walk_paths(obj, &path)),
848 pattern: res!(str_field(obj, "file", "query")),
849 base: res!(opt_str_field(obj, "file", "base")).unwrap_or_default(),
850 skip: res!(opt_strs_field(obj, "file", "skip")),
851 budget: res!(u32_field(obj, "file", "budget")),
852 }),
853 other => Err(Fault::UnknownTag.raise(&fmt!(
854 "There is no file operation called {:?}. The hand does read, write, edit, \
855 move, list, mkdir, search and glob, and nothing else.", other))),
856 }
857}
858
859fn strs(v: &[String]) -> Dat {
860 Dat::List(v.iter().map(|s| Dat::Str(s.clone())).collect())
861}
862
863// ┌───────────────────────────────────────────────────────────────┐
864// │ Strict JSON, before the decoder is asked anything │
865// └───────────────────────────────────────────────────────────────┘
866
867/// Refuses a payload that is not exactly one RFC 8259 JSON value.
868///
869/// Two findings live here and they are the same finding twice.
870///
871/// * `REVIEW.md` §3.12: the JDAT decoder is a *superset* of JSON, so
872/// `{'t':'bye'}`, `{t:"bye"}`, a trailing comma and a `#` comment are all
873/// accepted here and all rejected by `JSON.parse`.
874/// * `REVIEW.md` §3.2: it also stops at the end of the first value, so a payload
875/// holding two objects runs the first and discards the second in silence.
876///
877/// Either way the hand and any second reader of the same bytes -- the page, a
878/// journal, a policy layer, a reviewer with `jq` -- disagree about what the
879/// frame said, and a protocol whose meaning depends on which parser you use has
880/// no meaning. So the text is scanned once, strictly, before the decoder is
881/// asked anything, and what the decoder then sees is JSON both ends agree on.
882///
883/// # Arguments
884/// * `txt` - The payload.
885fn want_strict_json(txt: &str) -> Outcome<()> {
886 let b = txt.as_bytes();
887 let start = skip_ws(b, 0);
888 let end = res!(scan_value(b, start, 0));
889 let tail = skip_ws(b, end);
890 if tail != b.len() {
891 return Err(Fault::NotJson.raise(&fmt!(
892 "The payload carries {} more bytes after the message ends at offset \
893 {}. One frame is one message; a reader that stopped at the first \
894 value would run it and never see the rest.",
895 b.len() - tail, end)));
896 }
897 Ok(())
898}
899
900/// The first index at or after `i` that is not JSON whitespace.
901///
902/// The four bytes RFC 8259 allows and no others: a vertical tab or a form feed
903/// is not whitespace to `JSON.parse` and is not whitespace here.
904///
905/// # Arguments
906/// * `b` - The payload.
907/// * `i` - Where to start.
908fn skip_ws(b: &[u8], mut i: usize) -> usize {
909 while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\n' | b'\r') {
910 i += 1;
911 }
912 i
913}
914
915/// Scans one value, returning the index just past it.
916///
917/// Recursive, and bounded by [`MAX_DEPTH`] rather than by the stack: a payload
918/// of a hundred thousand open brackets is refused at the sixteenth.
919///
920/// # Arguments
921/// * `b` - The payload.
922/// * `i` - Where the value starts.
923/// * `depth` - How deep this value sits.
924fn scan_value(b: &[u8], i: usize, depth: usize) -> Outcome<usize> {
925 if depth > MAX_DEPTH {
926 return Err(Fault::NotJson.raise(&fmt!(
927 "The payload nests more than {} levels deep at offset {}.", MAX_DEPTH, i)));
928 }
929 if i >= b.len() {
930 return Err(Fault::NotJson.raise(
931 "The payload ends where a value was expected."));
932 }
933 match b[i] {
934 b'{' => scan_object(b, i, depth),
935 b'[' => scan_array(b, i, depth),
936 b'"' => scan_string(b, i),
937 b't' => scan_word(b, i, "true"),
938 b'f' => scan_word(b, i, "false"),
939 b'n' => scan_word(b, i, "null"),
940 b'-' | b'0'..=b'9' => scan_number(b, i),
941 other => Err(Fault::NotJson.raise(&fmt!(
942 "A JSON value cannot begin with {:?}, at offset {}.",
943 char::from(other), i))),
944 }
945}
946
947/// Scans one object, returning the index just past its closing brace.
948///
949/// # Arguments
950/// * `b` - The payload.
951/// * `i` - The opening brace.
952/// * `depth` - How deep the object sits.
953fn scan_object(b: &[u8], i: usize, depth: usize) -> Outcome<usize> {
954 let mut i = skip_ws(b, i + 1);
955 if i < b.len() && b[i] == b'}' {
956 return Ok(i + 1);
957 }
958 loop {
959 i = skip_ws(b, i);
960 if i >= b.len() || b[i] != b'"' {
961 // Also where a trailing comma lands, since what follows one must be
962 // a key and a `}` is not one.
963 return Err(Fault::NotJson.raise(&fmt!(
964 "A JSON object member needs a double-quoted key, and offset {} \
965 is not one.", i)));
966 }
967 i = res!(scan_string(b, i));
968 i = skip_ws(b, i);
969 if i >= b.len() || b[i] != b':' {
970 return Err(Fault::NotJson.raise(&fmt!(
971 "A JSON object member needs a colon after its key, at offset {}.", i)));
972 }
973 i = skip_ws(b, i + 1);
974 i = res!(scan_value(b, i, depth + 1));
975 i = skip_ws(b, i);
976 if i >= b.len() {
977 return Err(Fault::NotJson.raise("The payload ends inside an object."));
978 }
979 match b[i] {
980 b',' => i += 1,
981 b'}' => return Ok(i + 1),
982 other => return Err(Fault::NotJson.raise(&fmt!(
983 "A JSON object member is followed by a comma or a brace, not \
984 {:?}, at offset {}.", char::from(other), i))),
985 }
986 }
987}
988
989/// Scans one array, returning the index just past its closing bracket.
990///
991/// # Arguments
992/// * `b` - The payload.
993/// * `i` - The opening bracket.
994/// * `depth` - How deep the array sits.
995fn scan_array(b: &[u8], i: usize, depth: usize) -> Outcome<usize> {
996 let mut i = skip_ws(b, i + 1);
997 if i < b.len() && b[i] == b']' {
998 return Ok(i + 1);
999 }
1000 loop {
1001 i = skip_ws(b, i);
1002 // A trailing comma lands here, where `]` is not the start of a value.
1003 i = res!(scan_value(b, i, depth + 1));
1004 i = skip_ws(b, i);
1005 if i >= b.len() {
1006 return Err(Fault::NotJson.raise("The payload ends inside an array."));
1007 }
1008 match b[i] {
1009 b',' => i += 1,
1010 b']' => return Ok(i + 1),
1011 other => return Err(Fault::NotJson.raise(&fmt!(
1012 "A JSON array element is followed by a comma or a bracket, not \
1013 {:?}, at offset {}.", char::from(other), i))),
1014 }
1015 }
1016}
1017
1018/// Scans one string, returning the index just past its closing quote.
1019///
1020/// # Arguments
1021/// * `b` - The payload.
1022/// * `i` - The opening quote.
1023fn scan_string(b: &[u8], i: usize) -> Outcome<usize> {
1024 let mut i = i + 1;
1025 while i < b.len() {
1026 match b[i] {
1027 b'"' => return Ok(i + 1),
1028 b'\\' => {
1029 i += 1;
1030 if i >= b.len() {
1031 return Err(Fault::NotJson.raise(
1032 "The payload ends inside a string escape."));
1033 }
1034 match b[i] {
1035 b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't' => i += 1,
1036 b'u' => {
1037 if i + 4 >= b.len() {
1038 return Err(Fault::NotJson.raise(
1039 "The payload ends inside a \\u escape."));
1040 }
1041 for k in 1..=4 {
1042 if !b[i + k].is_ascii_hexdigit() {
1043 return Err(Fault::NotJson.raise(&fmt!(
1044 "A \\u escape needs four hexadecimal digits, \
1045 at offset {}.", i)));
1046 }
1047 }
1048 i += 5;
1049 },
1050 other => return Err(Fault::NotJson.raise(&fmt!(
1051 "{:?} is not a JSON string escape, at offset {}.",
1052 char::from(other), i))),
1053 }
1054 },
1055 // A raw control byte in a string is what a terminal's output would
1056 // put there, and JSON requires it escaped.
1057 c if c < 0x20 => return Err(Fault::NotJson.raise(&fmt!(
1058 "A JSON string cannot hold the unescaped control byte {:#04x}, \
1059 at offset {}.", c, i))),
1060 _ => i += 1,
1061 }
1062 }
1063 Err(Fault::NotJson.raise("The payload ends inside a string."))
1064}
1065
1066/// Scans one number, returning the index just past it.
1067///
1068/// # Arguments
1069/// * `b` - The payload.
1070/// * `i` - The first character of the number.
1071fn scan_number(b: &[u8], i: usize) -> Outcome<usize> {
1072 let mut i = i;
1073 let bad = |at: usize| Fault::NotJson.raise(&fmt!(
1074 "A JSON number is malformed at offset {}.", at));
1075 if b[i] == b'-' {
1076 i += 1;
1077 }
1078 if i >= b.len() {
1079 return Err(bad(i));
1080 }
1081 if b[i] == b'0' {
1082 // A leading zero admits no further digits, so `01` is not a number.
1083 i += 1;
1084 } else if b[i].is_ascii_digit() {
1085 while i < b.len() && b[i].is_ascii_digit() {
1086 i += 1;
1087 }
1088 } else {
1089 return Err(bad(i));
1090 }
1091 if i < b.len() && b[i] == b'.' {
1092 i += 1;
1093 if i >= b.len() || !b[i].is_ascii_digit() {
1094 return Err(bad(i));
1095 }
1096 while i < b.len() && b[i].is_ascii_digit() {
1097 i += 1;
1098 }
1099 }
1100 if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
1101 i += 1;
1102 if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
1103 i += 1;
1104 }
1105 if i >= b.len() || !b[i].is_ascii_digit() {
1106 return Err(bad(i));
1107 }
1108 while i < b.len() && b[i].is_ascii_digit() {
1109 i += 1;
1110 }
1111 }
1112 Ok(i)
1113}
1114
1115/// Scans one of the three bare words, returning the index just past it.
1116///
1117/// # Arguments
1118/// * `b` - The payload.
1119/// * `i` - Where the word starts.
1120/// * `word` - The word expected.
1121fn scan_word(b: &[u8], i: usize, word: &str) -> Outcome<usize> {
1122 if b.len() - i >= word.len() && &b[i..i + word.len()] == word.as_bytes() {
1123 Ok(i + word.len())
1124 } else {
1125 Err(Fault::NotJson.raise(&fmt!(
1126 "A JSON value beginning here can only be {:?}, at offset {}.", word, i)))
1127 }
1128}
1129
1130// ┌───────────────────────────────────────────────────────────────┐
1131// │ JSON: requests │
1132// └───────────────────────────────────────────────────────────────┘
1133
1134/// The `Dat` object a request encodes to.
1135///
1136/// Order-preserving, so the text on the wire reads in the order the contract
1137/// states rather than in whatever order a sorted key set produces.
1138///
1139/// # Arguments
1140/// * `req` - The request.
1141fn req_dat(req: &Req) -> Dat {
1142 match req {
1143 Req::Hello { proto, client } => omapdat!{
1144 "t" => "hello",
1145 "proto" => *proto,
1146 "client" => Dat::Str(client.clone()),
1147 },
1148 Req::Exec { id, argv, cwd, env, stdin, timeout_ms, capture, fence, toolkits } => {
1149 let pairs = env.iter()
1150 .map(|(k, v)| Dat::List(vec![Dat::Str(k.clone()), Dat::Str(v.clone())]))
1151 .collect::<Vec<_>>();
1152 omapdat!{
1153 "t" => "exec",
1154 "id" => Dat::Str(id.clone()),
1155 "argv" => strs(argv),
1156 "cwd" => Dat::Str(cwd.clone()),
1157 "env" => Dat::List(pairs),
1158 "stdin" => match stdin {
1159 Some(s) => Dat::Str(s.clone()),
1160 None => Dat::Opt(Box::new(None)),
1161 },
1162 "timeout_ms" => *timeout_ms,
1163 "capture" => capture_name(*capture),
1164 "fence" => omapdat!{
1165 "rw" => strs(&fence.rw),
1166 "ro" => strs(&fence.ro),
1167 "deny" => strs(&fence.deny),
1168 "net" => fence.net,
1169 },
1170 "toolkits" => strs(toolkits),
1171 }
1172 },
1173 Req::Verify { id, name, breaks, timeout_ms } => omapdat!{
1174 "t" => "verify",
1175 "id" => Dat::Str(id.clone()),
1176 "name" => Dat::Str(name.clone()),
1177 "breaks" => Dat::Str(breaks.word().to_string()),
1178 "break" => match breaks {
1179 Breaks::One(b) => Dat::Str(b.clone()),
1180 _ => Dat::Opt(Box::new(None)),
1181 },
1182 "timeout_ms" => *timeout_ms,
1183 },
1184 Req::File { id, op, cwd, fence, toolkits } => omapdat!{
1185 "t" => "file",
1186 "id" => Dat::Str(id.clone()),
1187 "op" => Dat::Str(op.word().to_string()),
1188 "path" => Dat::Str(op.path().to_string()),
1189 // A walk names several starts; every other op names one, and writes it here as a
1190 // list of one so that the field means the same thing in every message.
1191 "paths" => strs(&op.paths().iter().map(|p| p.to_string()).collect::<Vec<_>>()),
1192 "query" => match op {
1193 FileOp::Search { query, .. } => Dat::Str(query.clone()),
1194 FileOp::Glob { pattern, .. } => Dat::Str(pattern.clone()),
1195 _ => Dat::Opt(Box::new(None)),
1196 },
1197 "ci" => match op {
1198 FileOp::Search { ci, .. } => *ci,
1199 _ => false,
1200 },
1201 "glob" => match op {
1202 FileOp::Search { glob, .. } => Dat::Str(glob.clone()),
1203 _ => Dat::Opt(Box::new(None)),
1204 },
1205 "base" => match op {
1206 FileOp::Search { base, .. } | FileOp::Glob { base, .. } => Dat::Str(base.clone()),
1207 _ => Dat::Opt(Box::new(None)),
1208 },
1209 "skip" => match op {
1210 FileOp::Search { skip, .. } | FileOp::Glob { skip, .. } => strs(skip),
1211 _ => strs(&[]),
1212 },
1213 "budget" => match op {
1214 FileOp::Search { budget, .. } | FileOp::Glob { budget, .. } => *budget,
1215 _ => 0u32,
1216 },
1217 "cap" => match op {
1218 FileOp::Search { cap, .. } => *cap,
1219 _ => 0u32,
1220 },
1221 "to" => match op {
1222 FileOp::Move { to, .. } => Dat::Str(to.clone()),
1223 _ => Dat::Opt(Box::new(None)),
1224 },
1225 "text" => match op {
1226 FileOp::Write { content, .. } => Dat::Str(content.clone()),
1227 FileOp::Edit { old, .. } => Dat::Str(old.clone()),
1228 _ => Dat::Opt(Box::new(None)),
1229 },
1230 "text2" => match op {
1231 FileOp::Edit { new, .. } => Dat::Str(new.clone()),
1232 _ => Dat::Opt(Box::new(None)),
1233 },
1234 "offset" => match op {
1235 FileOp::Read { offset, .. } => *offset,
1236 _ => 0u32,
1237 },
1238 "limit" => match op {
1239 FileOp::Read { limit, .. } => *limit,
1240 _ => 0u32,
1241 },
1242 "cwd" => Dat::Str(cwd.clone()),
1243 "fence" => omapdat!{
1244 "rw" => strs(&fence.rw),
1245 "ro" => strs(&fence.ro),
1246 "deny" => strs(&fence.deny),
1247 "net" => fence.net,
1248 },
1249 "toolkits" => strs(toolkits),
1250 },
1251 Req::Signal { id, sig } => omapdat!{
1252 "t" => "signal",
1253 "id" => Dat::Str(id.clone()),
1254 "sig" => sig_name(*sig),
1255 },
1256 Req::Open { id, argv, cwd, env, size, fence, toolkits } => {
1257 let pairs = env.iter()
1258 .map(|(k, v)| Dat::List(vec![Dat::Str(k.clone()), Dat::Str(v.clone())]))
1259 .collect::<Vec<_>>();
1260 omapdat!{
1261 "t" => "open",
1262 "id" => Dat::Str(id.clone()),
1263 "argv" => strs(argv),
1264 "cwd" => Dat::Str(cwd.clone()),
1265 "env" => Dat::List(pairs),
1266 "size" => size_dat(size),
1267 "fence" => omapdat!{
1268 "rw" => strs(&fence.rw),
1269 "ro" => strs(&fence.ro),
1270 "deny" => strs(&fence.deny),
1271 "net" => fence.net,
1272 },
1273 "toolkits" => strs(toolkits),
1274 }
1275 },
1276 Req::Input { id, data } => omapdat!{
1277 "t" => "input",
1278 "id" => Dat::Str(id.clone()),
1279 "data" => Dat::Str(data.clone()),
1280 },
1281 Req::Resize { id, size } => omapdat!{
1282 "t" => "resize",
1283 "id" => Dat::Str(id.clone()),
1284 "size" => size_dat(size),
1285 },
1286 Req::Runs => omapdat!{
1287 "t" => "runs",
1288 },
1289 Req::Dirs { path } => omapdat!{
1290 "t" => "dirs",
1291 "path" => Dat::Str(path.clone()),
1292 },
1293 Req::Grant { path } => omapdat!{
1294 "t" => "grant",
1295 "path" => Dat::Str(path.clone()),
1296 },
1297 Req::Bye => omapdat!{
1298 "t" => "bye",
1299 },
1300 }
1301}
1302
1303/// Refuses a request the hand should never put on the wire.
1304///
1305/// The decode side already refuses these, and encoding is where a bug in the
1306/// hand's own code would otherwise become a page's problem: a `data` that is not
1307/// base64 is a mistake somewhere upstream of here, and it is caught before it is
1308/// written rather than after the far end fails to decode it. A `timeout_ms`
1309/// past [`SAFE_INT_MAX`] is the same case in the other direction -- the hand
1310/// composes requests in the Cloud tier's tests and in `dev/`, and a limit the
1311/// far end would read as a different limit is not a limit.
1312///
1313/// # Arguments
1314/// * `req` - The request.
1315fn check_req(req: &Req) -> Outcome<()> {
1316 match req {
1317 Req::Input { data, .. } => res!(data_check(data)),
1318 Req::Exec { id, timeout_ms, .. } =>
1319 res!(safe_int(*timeout_ms, id, "the wall-clock limit")),
1320 Req::Verify { id, timeout_ms, .. } =>
1321 res!(safe_int(*timeout_ms, id, "the whole sequence's budget")),
1322 _ => (),
1323 }
1324 Ok(())
1325}
1326
1327/// The JSON text of a request.
1328///
1329/// # Arguments
1330/// * `req` - The request.
1331pub fn req_json(req: &Req) -> Outcome<String> {
1332 res!(check_req(req));
1333 Ok(res!(req_dat(req).json()))
1334}
1335
1336/// The request a JSON text carries.
1337///
1338/// # Arguments
1339/// * `txt` - The payload, without any framing.
1340pub fn req_of_json(txt: &str) -> Outcome<Req> {
1341 res!(want_strict_json(txt));
1342 let obj = match Dat::decode_string_with_config(txt, &dec_cfg()) {
1343 Ok(d) => d,
1344 Err(e) => return Err(Fault::NotJson.raise(&fmt!(
1345 "The payload is not JSON that can be read: {}.",
1346 e.msgs().join("; ")))),
1347 };
1348 res!(want_object(&obj, "request"));
1349 let t = res!(tag_of(&obj));
1350 match t.as_str() {
1351 "hello" => Ok(Req::Hello {
1352 proto: res!(u32_field(&obj, "hello", "proto")),
1353 client: res!(str_field(&obj, "hello", "client")),
1354 }),
1355 "exec" => Ok(Req::Exec {
1356 id: res!(str_field(&obj, "exec", "id")),
1357 argv: res!(strs_field(&obj, "exec", "argv")),
1358 cwd: res!(str_field(&obj, "exec", "cwd")),
1359 env: res!(env_field(&obj, "exec", "env")),
1360 stdin: res!(opt_str_field(&obj, "exec", "stdin")),
1361 timeout_ms: res!(safe_int_field(&obj, "exec", "timeout_ms")),
1362 capture: res!(capture_of(&res!(str_field(&obj, "exec", "capture")))),
1363 fence: res!(fence_field(&obj, "exec", "fence")),
1364 toolkits: res!(opt_strs_field(&obj, "exec", "toolkits")),
1365 }),
1366 "verify" => Ok(Req::Verify {
1367 id: res!(str_field(&obj, "verify", "id")),
1368 name: res!(str_field(&obj, "verify", "name")),
1369 breaks: res!(breaks_of(&obj)),
1370 timeout_ms: res!(safe_int_field(&obj, "verify", "timeout_ms")),
1371 }),
1372 "file" => Ok(Req::File {
1373 id: res!(str_field(&obj, "file", "id")),
1374 op: res!(fileop_of(&obj)),
1375 cwd: res!(str_field(&obj, "file", "cwd")),
1376 fence: res!(fence_field(&obj, "file", "fence")),
1377 toolkits: res!(opt_strs_field(&obj, "file", "toolkits")),
1378 }),
1379 "signal" => Ok(Req::Signal {
1380 id: res!(str_field(&obj, "signal", "id")),
1381 sig: res!(sig_of(&res!(str_field(&obj, "signal", "sig")))),
1382 }),
1383 "open" => Ok(Req::Open {
1384 id: res!(str_field(&obj, "open", "id")),
1385 argv: res!(strs_field(&obj, "open", "argv")),
1386 cwd: res!(str_field(&obj, "open", "cwd")),
1387 env: res!(env_field(&obj, "open", "env")),
1388 size: res!(size_field(&obj, "open", "size")),
1389 fence: res!(fence_field(&obj, "open", "fence")),
1390 toolkits: res!(opt_strs_field(&obj, "open", "toolkits")),
1391 }),
1392 "input" => Ok(Req::Input {
1393 id: res!(str_field(&obj, "input", "id")),
1394 data: res!(b64_field(&obj, "input", "data")),
1395 }),
1396 "resize" => Ok(Req::Resize {
1397 id: res!(str_field(&obj, "resize", "id")),
1398 size: res!(size_field(&obj, "resize", "size")),
1399 }),
1400 "runs" => Ok(Req::Runs),
1401 "dirs" => Ok(Req::Dirs {
1402 path: res!(str_field(&obj, "dirs", "path")),
1403 }),
1404 "grant" => Ok(Req::Grant {
1405 path: res!(str_field(&obj, "grant", "path")),
1406 }),
1407 "bye" => Ok(Req::Bye),
1408 other => Err(Fault::UnknownTag.raise(&fmt!(
1409 "There is no request called {:?}. This page is asking for something \
1410 this hand has never heard of; one of the two is newer than the other.",
1411 other))),
1412 }
1413}
1414
1415// ┌───────────────────────────────────────────────────────────────┐
1416// │ JSON: responses │
1417// └───────────────────────────────────────────────────────────────┘
1418
1419/// The `Dat` object a response encodes to.
1420///
1421/// # Arguments
1422/// * `resp` - The response.
1423fn resp_dat(resp: &Resp) -> Dat {
1424 match resp {
1425 Resp::Hello { proto, host, version, os, caps } => omapdat!{
1426 "t" => "hello",
1427 "proto" => *proto,
1428 "host" => Dat::Str(host.clone()),
1429 "version" => Dat::Str(version.clone()),
1430 "os" => Dat::Str(os.clone()),
1431 "caps" => strs(caps),
1432 },
1433 Resp::Started { id, pid } => omapdat!{
1434 "t" => "started",
1435 "id" => Dat::Str(id.clone()),
1436 "pid" => *pid,
1437 },
1438 Resp::Chunk { id, stream, seq, data } => omapdat!{
1439 "t" => "chunk",
1440 "id" => Dat::Str(id.clone()),
1441 "stream" => stream_name(*stream),
1442 "seq" => *seq,
1443 "data" => Dat::Str(data.clone()),
1444 },
1445 Resp::Ended { id, exit, timed_out, killed, out_bytes, err_bytes } => omapdat!{
1446 "t" => "ended",
1447 "id" => Dat::Str(id.clone()),
1448 "exit" => *exit,
1449 "timed_out" => *timed_out,
1450 "killed" => *killed,
1451 "out_bytes" => *out_bytes,
1452 "err_bytes" => *err_bytes,
1453 },
1454 Resp::Refused { id, reason } => omapdat!{
1455 "t" => "refused",
1456 "id" => Dat::Str(id.clone()),
1457 "reason" => Dat::Str(reason.clone()),
1458 },
1459 Resp::Filed { id, ok, text } => omapdat!{
1460 "t" => "filed",
1461 "id" => Dat::Str(id.clone()),
1462 "ok" => *ok,
1463 "text" => Dat::Str(text.clone()),
1464 },
1465 Resp::Opened { id, pid } => omapdat!{
1466 "t" => "opened",
1467 "id" => Dat::Str(id.clone()),
1468 "pid" => *pid,
1469 },
1470 Resp::Output { id, seq, data } => omapdat!{
1471 "t" => "output",
1472 "id" => Dat::Str(id.clone()),
1473 "seq" => *seq,
1474 "data" => Dat::Str(data.clone()),
1475 },
1476 Resp::Granted { path, note } => omapdat!{
1477 "t" => "granted",
1478 "path" => Dat::Str(path.clone()),
1479 "note" => Dat::Str(note.clone()),
1480 },
1481 Resp::Dirs { path, up, dirs, roots } => omapdat!{
1482 "t" => "dirs",
1483 "path" => Dat::Str(path.clone()),
1484 "up" => Dat::Str(up.clone()),
1485 "dirs" => Dat::List(dirs.iter().map(|d| Dat::Str(d.clone())).collect()),
1486 "roots" => Dat::List(roots.iter().map(|d| Dat::Str(d.clone())).collect()),
1487 },
1488 Resp::Runs { runs, more } => omapdat!{
1489 "t" => "runs",
1490 "runs" => Dat::List(runs.iter().map(|r| omapdat!{
1491 "id" => Dat::Str(r.id.clone()),
1492 "pid" => r.pid,
1493 "what" => Dat::Str(r.what.clone()),
1494 "state" => r.state.word(),
1495 "secs" => r.secs,
1496 }).collect()),
1497 "more" => *more,
1498 },
1499 Resp::Closed { id, exit, killed } => omapdat!{
1500 "t" => "closed",
1501 "id" => Dat::Str(id.clone()),
1502 "exit" => *exit,
1503 "killed" => *killed,
1504 },
1505 Resp::Error { id, message } => omapdat!{
1506 "t" => "error",
1507 "id" => match id {
1508 Some(s) => Dat::Str(s.clone()),
1509 None => Dat::Opt(Box::new(None)),
1510 },
1511 "message" => Dat::Str(message.clone()),
1512 },
1513 Resp::Fault { reason } => omapdat!{
1514 "t" => "fault",
1515 "reason" => Dat::Str(reason.clone()),
1516 },
1517 }
1518}
1519
1520/// Refuses a response the hand should never put on the wire.
1521///
1522/// Every one of these is a case where writing the frame would be worse than
1523/// failing to: a `data` that is not base64 arrives at the page as bytes it
1524/// cannot recover, and any number above [`SAFE_INT_MAX`] arrives rounded, which
1525/// silently breaks whatever the number was for instead of loudly breaking the
1526/// send. The failure is at the hand's own boundary, before a byte is written,
1527/// so a counter that has gone wrong is heard about here rather than believed
1528/// there.
1529///
1530/// # Arguments
1531/// * `resp` - The response.
1532fn check_resp(resp: &Resp) -> Outcome<()> {
1533 match resp {
1534 Resp::Output { id, seq, data } => {
1535 res!(data_check(data));
1536 res!(safe_int(*seq, id, "the sequence number of an output frame"));
1537 },
1538 Resp::Chunk { id, seq, .. } => {
1539 res!(safe_int(*seq, id, "the sequence number of an output chunk"));
1540 },
1541 Resp::Ended { id, out_bytes, err_bytes, .. } => {
1542 res!(safe_int(*out_bytes, id, "the standard output byte count"));
1543 res!(safe_int(*err_bytes, id, "the standard error byte count"));
1544 },
1545 _ => (),
1546 }
1547 Ok(())
1548}
1549
1550/// Refuses a number the far end would read as a different number.
1551///
1552/// # Arguments
1553/// * `n` - The value.
1554/// * `id` - The run or session it belongs to, for the sentence.
1555/// * `what` - What the number is, for the sentence.
1556fn safe_int(n: u64, id: &str, what: &str) -> Outcome<()> {
1557 if n > SAFE_INT_MAX {
1558 return Err(Fault::WrongShape.raise(&fmt!(
1559 "For {:?}, {} is {}, and the largest whole number the page can read \
1560 unchanged is {}. The frame was not written, because the page would \
1561 read a rounded number and believe it.", id, what, n, SAFE_INT_MAX)));
1562 }
1563 Ok(())
1564}
1565
1566/// The JSON text of a response.
1567///
1568/// # Arguments
1569/// * `resp` - The response.
1570pub fn resp_json(resp: &Resp) -> Outcome<String> {
1571 res!(check_resp(resp));
1572 Ok(res!(resp_dat(resp).json()))
1573}
1574
1575/// The response a JSON text carries.
1576///
1577/// # Arguments
1578/// * `txt` - The payload, without any framing.
1579pub fn resp_of_json(txt: &str) -> Outcome<Resp> {
1580 res!(want_strict_json(txt));
1581 let obj = match Dat::decode_string_with_config(txt, &dec_cfg()) {
1582 Ok(d) => d,
1583 Err(e) => return Err(Fault::NotJson.raise(&fmt!(
1584 "The payload is not JSON that can be read: {}.",
1585 e.msgs().join("; ")))),
1586 };
1587 res!(want_object(&obj, "response"));
1588 let t = res!(tag_of(&obj));
1589 match t.as_str() {
1590 "hello" => Ok(Resp::Hello {
1591 proto: res!(u32_field(&obj, "hello", "proto")),
1592 host: res!(str_field(&obj, "hello", "host")),
1593 version: res!(str_field(&obj, "hello", "version")),
1594 os: res!(str_field(&obj, "hello", "os")),
1595 caps: res!(strs_field(&obj, "hello", "caps")),
1596 }),
1597 "started" => Ok(Resp::Started {
1598 id: res!(str_field(&obj, "started", "id")),
1599 pid: res!(u32_field(&obj, "started", "pid")),
1600 }),
1601 "chunk" => Ok(Resp::Chunk {
1602 id: res!(str_field(&obj, "chunk", "id")),
1603 stream: res!(stream_of(&res!(str_field(&obj, "chunk", "stream")))),
1604 seq: res!(safe_int_field(&obj, "chunk", "seq")),
1605 data: res!(str_field(&obj, "chunk", "data")),
1606 }),
1607 "ended" => Ok(Resp::Ended {
1608 id: res!(str_field(&obj, "ended", "id")),
1609 exit: res!(i32_field(&obj, "ended", "exit")),
1610 timed_out: res!(bool_field(&obj, "ended", "timed_out")),
1611 killed: res!(bool_field(&obj, "ended", "killed")),
1612 out_bytes: res!(safe_int_field(&obj, "ended", "out_bytes")),
1613 err_bytes: res!(safe_int_field(&obj, "ended", "err_bytes")),
1614 }),
1615 "refused" => Ok(Resp::Refused {
1616 id: res!(str_field(&obj, "refused", "id")),
1617 reason: res!(str_field(&obj, "refused", "reason")),
1618 }),
1619 "filed" => Ok(Resp::Filed {
1620 id: res!(str_field(&obj, "filed", "id")),
1621 ok: res!(bool_field(&obj, "filed", "ok")),
1622 text: res!(str_field(&obj, "filed", "text")),
1623 }),
1624 "opened" => Ok(Resp::Opened {
1625 id: res!(str_field(&obj, "opened", "id")),
1626 pid: res!(u32_field(&obj, "opened", "pid")),
1627 }),
1628 "output" => Ok(Resp::Output {
1629 id: res!(str_field(&obj, "output", "id")),
1630 seq: res!(safe_int_field(&obj, "output", "seq")),
1631 data: res!(b64_field(&obj, "output", "data")),
1632 }),
1633 "closed" => Ok(Resp::Closed {
1634 id: res!(str_field(&obj, "closed", "id")),
1635 exit: res!(i32_field(&obj, "closed", "exit")),
1636 killed: res!(bool_field(&obj, "closed", "killed")),
1637 }),
1638 "granted" => Ok(Resp::Granted {
1639 path: res!(str_field(&obj, "granted", "path")),
1640 note: res!(str_field(&obj, "granted", "note")),
1641 }),
1642 "dirs" => Ok(Resp::Dirs {
1643 path: res!(str_field(&obj, "dirs", "path")),
1644 up: res!(str_field(&obj, "dirs", "up")),
1645 dirs: res!(strs_field(&obj, "dirs", "dirs")),
1646 roots: res!(strs_field(&obj, "dirs", "roots")),
1647 }),
1648 "runs" => Ok(Resp::Runs {
1649 runs: res!(runs_field(&obj)),
1650 more: res!(u32_field(&obj, "runs", "more")),
1651 }),
1652 "error" => Ok(Resp::Error {
1653 id: res!(opt_str_field(&obj, "error", "id")),
1654 message: res!(str_field(&obj, "error", "message")),
1655 }),
1656 "fault" => Ok(Resp::Fault {
1657 reason: res!(str_field(&obj, "fault", "reason")),
1658 }),
1659 other => Err(Fault::UnknownTag.raise(&fmt!(
1660 "There is no response called {:?}. This hand is answering with \
1661 something the page has never heard of; one of the two is newer than \
1662 the other.", other))),
1663 }
1664}
1665
1666// ┌───────────────────────────────────────────────────────────────┐
1667// │ Framing │
1668// └───────────────────────────────────────────────────────────────┘
1669
1670/// How one message is marked off from the next.
1671///
1672/// An enum and not a trait object: there are two answers, the second exists so
1673/// that the protocol is transport-neutral now rather than after a rewrite, and
1674/// neither needs a vtable to be reached.
1675#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1676pub enum Frame {
1677 /// Chrome's native messaging format: a 4-byte native-endian length prefix
1678 /// followed by the UTF-8 JSON.
1679 ///
1680 /// Native-endian, not network-endian, which is a wart in Chrome's design
1681 /// and not one either end gets to correct.
1682 NativeMessaging,
1683 /// The identical JSON as one WebSocket text payload, with no prefix.
1684 ///
1685 /// The transport marks off its own messages, so the framing here is the
1686 /// absence of framing.
1687 WebSocket,
1688}
1689
1690impl Frame {
1691
1692 /// The bytes one payload occupies on this framing.
1693 ///
1694 /// # Arguments
1695 /// * `txt` - The JSON text.
1696 ///
1697 /// # Returns
1698 /// The whole frame, or a [`Fault::FrameTooBig`] where it would exceed
1699 /// [`FRAME_MAX`]. The refusal matters: Chrome drops the connection without
1700 /// ceremony when a host writes more than it allows, so a frame that would
1701 /// be over the cap must never reach the pipe.
1702 pub fn wrap(&self, txt: &str) -> Outcome<Vec<u8>> {
1703 let body = txt.as_bytes();
1704 let total = match self {
1705 Self::NativeMessaging => LEN_PREFIX + body.len(),
1706 Self::WebSocket => body.len(),
1707 };
1708 if total > FRAME_MAX {
1709 return Err(Fault::FrameTooBig.raise(&fmt!(
1710 "The frame is {} bytes and the cap is {}. It was not written, \
1711 because writing it would end the connection rather than \
1712 deliver it.", total, FRAME_MAX)));
1713 }
1714 match self {
1715 Self::NativeMessaging => {
1716 // The cast is safe: `total` is at or below FRAME_MAX, which is
1717 // far inside u32.
1718 let n = body.len() as u32;
1719 let mut out = Vec::with_capacity(total);
1720 out.extend_from_slice(&n.to_ne_bytes());
1721 out.extend_from_slice(body);
1722 Ok(out)
1723 },
1724 Self::WebSocket => Ok(body.to_vec()),
1725 }
1726 }
1727
1728 /// The payload a whole frame carries.
1729 ///
1730 /// # Arguments
1731 /// * `buf` - Exactly one frame, with its prefix where the framing has one.
1732 pub fn unwrap(&self, buf: &[u8]) -> Outcome<String> {
1733 let body = match self {
1734 Self::NativeMessaging => {
1735 if buf.len() < LEN_PREFIX {
1736 return Err(Fault::ShortRead.raise(&fmt!(
1737 "A frame needs a {}-byte length prefix and only {} bytes \
1738 arrived.", LEN_PREFIX, buf.len())));
1739 }
1740 let mut pre = [0u8; LEN_PREFIX];
1741 pre.copy_from_slice(&buf[..LEN_PREFIX]);
1742 let n = u32::from_ne_bytes(pre) as usize;
1743 if n > INBOUND_MAX {
1744 return Err(Fault::LengthTooBig.raise(&fmt!(
1745 "The prefix declares {} bytes and the cap is {}. Nothing \
1746 was read, because reading it is what the sender wanted.",
1747 n, INBOUND_MAX)));
1748 }
1749 if buf.len() < LEN_PREFIX + n {
1750 return Err(Fault::Truncated.raise(&fmt!(
1751 "The prefix declares {} bytes and {} arrived after it.",
1752 n, buf.len() - LEN_PREFIX)));
1753 }
1754 &buf[LEN_PREFIX..LEN_PREFIX + n]
1755 },
1756 Self::WebSocket => {
1757 if buf.len() > INBOUND_MAX {
1758 return Err(Fault::LengthTooBig.raise(&fmt!(
1759 "The payload is {} bytes and the cap is {}.",
1760 buf.len(), INBOUND_MAX)));
1761 }
1762 buf
1763 },
1764 };
1765 match std::str::from_utf8(body) {
1766 Ok(s) => Ok(s.to_string()),
1767 Err(e) => Err(Fault::NotUtf8.raise(&fmt!(
1768 "The payload is not UTF-8: {}.", e))),
1769 }
1770 }
1771
1772 /// Writes one request.
1773 ///
1774 /// # Arguments
1775 /// * `w` - Where the frame goes.
1776 /// * `req` - The request.
1777 pub fn write_req<W: Write>(&self, w: &mut W, req: &Req) -> Outcome<()> {
1778 let txt = res!(req_json(req));
1779 res!(self.write_bytes(w, &res!(self.wrap(&txt))));
1780 Ok(())
1781 }
1782
1783 /// Writes one response.
1784 ///
1785 /// # Arguments
1786 /// * `w` - Where the frame goes.
1787 /// * `resp` - The response.
1788 pub fn write_resp<W: Write>(&self, w: &mut W, resp: &Resp) -> Outcome<()> {
1789 let txt = res!(resp_json(resp));
1790 res!(self.write_bytes(w, &res!(self.wrap(&txt))));
1791 Ok(())
1792 }
1793
1794 /// Reads one request, or nothing where the stream has ended cleanly.
1795 ///
1796 /// # Arguments
1797 /// * `r` - Where the frame comes from.
1798 pub fn read_req<R: Read>(&self, r: &mut R) -> Outcome<Option<Req>> {
1799 match res!(self.read_payload(r)) {
1800 Some(txt) => Ok(Some(res!(req_of_json(&txt)))),
1801 None => Ok(None),
1802 }
1803 }
1804
1805 /// Reads one response, or nothing where the stream has ended cleanly.
1806 ///
1807 /// # Arguments
1808 /// * `r` - Where the frame comes from.
1809 pub fn read_resp<R: Read>(&self, r: &mut R) -> Outcome<Option<Resp>> {
1810 match res!(self.read_payload(r)) {
1811 Some(txt) => Ok(Some(res!(resp_of_json(&txt)))),
1812 None => Ok(None),
1813 }
1814 }
1815
1816 /// Reads one payload, or nothing where the stream has ended cleanly.
1817 ///
1818 /// The WebSocket arm reads to the end of the stream, because a WebSocket
1819 /// payload is delivered whole by the transport and one payload is one
1820 /// message; a byte stream carrying several of them needs the length prefix,
1821 /// which is what the other arm is.
1822 ///
1823 /// # Arguments
1824 /// * `r` - Where the frame comes from.
1825 pub fn read_payload<R: Read>(&self, r: &mut R) -> Outcome<Option<String>> {
1826 match self {
1827 Self::NativeMessaging => {
1828 let mut pre = [0u8; LEN_PREFIX];
1829 let got = res!(read_upto(r, &mut pre));
1830 if got == 0 {
1831 // A clean end between frames, which is how Chrome says goodbye.
1832 return Ok(None);
1833 }
1834 if got < LEN_PREFIX {
1835 return Err(Fault::ShortRead.raise(&fmt!(
1836 "The stream ended {} bytes into a {}-byte length prefix.",
1837 got, LEN_PREFIX)));
1838 }
1839 let n = u32::from_ne_bytes(pre) as usize;
1840 if n > INBOUND_MAX {
1841 return Err(Fault::LengthTooBig.raise(&fmt!(
1842 "The prefix declares {} bytes and the cap is {}. Nothing \
1843 was read, because reading it is what the sender wanted.",
1844 n, INBOUND_MAX)));
1845 }
1846 let mut body = vec![0u8; n];
1847 let got = res!(read_upto(r, &mut body));
1848 if got < n {
1849 return Err(Fault::Truncated.raise(&fmt!(
1850 "The prefix declares {} bytes and the stream ended after {}.",
1851 n, got)));
1852 }
1853 match String::from_utf8(body) {
1854 Ok(s) => Ok(Some(s)),
1855 Err(e) => Err(Fault::NotUtf8.raise(&fmt!(
1856 "The payload is not UTF-8: {}.", e))),
1857 }
1858 },
1859 Self::WebSocket => {
1860 let mut body = Vec::new();
1861 // One more than the cap, so a payload sitting exactly on the
1862 // cap is read and one byte over it is caught rather than
1863 // silently truncated.
1864 let mut lim = r.take((INBOUND_MAX + 1) as u64);
1865 match lim.read_to_end(&mut body) {
1866 Ok(_) => (),
1867 Err(e) => return Err(Fault::Io.raise(&fmt!(
1868 "The payload could not be read: {}.", e))),
1869 }
1870 if body.is_empty() {
1871 return Ok(None);
1872 }
1873 Ok(Some(res!(self.unwrap(&body))))
1874 },
1875 }
1876 }
1877
1878 /// Writes whole bytes, turning an I/O failure into a named fault.
1879 ///
1880 /// # Arguments
1881 /// * `w` - Where the bytes go.
1882 /// * `buf` - The bytes.
1883 fn write_bytes<W: Write>(&self, w: &mut W, buf: &[u8]) -> Outcome<()> {
1884 match w.write_all(buf) {
1885 Ok(()) => (),
1886 Err(e) => return Err(Fault::Io.raise(&fmt!(
1887 "The frame could not be written: {}.", e))),
1888 }
1889 match w.flush() {
1890 Ok(()) => Ok(()),
1891 Err(e) => Err(Fault::Io.raise(&fmt!(
1892 "The frame could not be flushed: {}.", e))),
1893 }
1894 }
1895}
1896
1897/// Fills a buffer, returning how much of it arrived before the stream ended.
1898///
1899/// `Read::read_exact` cannot tell a clean end from a short one, and the
1900/// difference is the whole of what a reader needs to know: nothing at all means
1901/// the page has gone, and three bytes of a four-byte prefix means something is
1902/// wrong.
1903///
1904/// # Arguments
1905/// * `r` - The stream.
1906/// * `buf` - The buffer to fill.
1907fn read_upto<R: Read>(r: &mut R, buf: &mut [u8]) -> Outcome<usize> {
1908 let mut n = 0;
1909 while n < buf.len() {
1910 match r.read(&mut buf[n..]) {
1911 Ok(0) => break,
1912 Ok(k) => n += k,
1913 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1914 Err(e) => return Err(Fault::Io.raise(&fmt!(
1915 "The stream could not be read: {}.", e))),
1916 }
1917 }
1918 Ok(n)
1919}
1920
1921// ┌───────────────────────────────────────────────────────────────┐
1922// │ Fitting output into a frame │
1923// └───────────────────────────────────────────────────────────────┘
1924
1925/// Whether a response fits in one frame.
1926///
1927/// # Arguments
1928/// * `frame` - The framing in use.
1929/// * `resp` - The response.
1930pub fn resp_fits(frame: Frame, resp: &Resp) -> Outcome<bool> {
1931 Ok(res!(resp_frame_len(frame, resp)) <= FRAME_MAX)
1932}
1933
1934/// The number of bytes a response occupies once framed.
1935///
1936/// # Arguments
1937/// * `frame` - The framing in use.
1938/// * `resp` - The response.
1939pub fn resp_frame_len(frame: Frame, resp: &Resp) -> Outcome<usize> {
1940 let txt = res!(resp_json(resp));
1941 Ok(match frame {
1942 Frame::NativeMessaging => LEN_PREFIX + txt.len(),
1943 Frame::WebSocket => txt.len(),
1944 })
1945}
1946
1947/// The framed length of a chunk carrying the given text.
1948///
1949/// # Arguments
1950/// * `frame` - The framing in use.
1951/// * `id` - The run's identifier.
1952/// * `stream` - Which stream the text came from.
1953/// * `seq` - The sequence number the chunk would carry.
1954/// * `text` - The text the chunk would carry.
1955fn chunk_frame_len(
1956 frame: Frame,
1957 id: &str,
1958 stream: Stream,
1959 seq: u64,
1960 text: &str,
1961)
1962 -> Outcome<usize>
1963{
1964 resp_frame_len(frame, &Resp::Chunk {
1965 id: id.to_string(),
1966 stream,
1967 seq,
1968 data: text.to_string(),
1969 })
1970}
1971
1972/// How many bytes of `text` a single chunk can carry.
1973///
1974/// **Measured, not calculated.** JSON escaping inflates: a quote costs two
1975/// bytes, a control byte costs six, so the answer is not `FRAME_MAX` less a
1976/// fixed envelope and a caller that assumes it is will write a frame Chrome
1977/// drops. The size is therefore found by encoding candidate prefixes and
1978/// asking how long they came out, which cannot drift from what the encoder
1979/// actually does because it *is* what the encoder actually does.
1980///
1981/// The answer is also held at or below [`CHUNK_MAX`], and always lands on a
1982/// character boundary so that a chunk never splits a code point.
1983///
1984/// # Arguments
1985/// * `frame` - The framing in use.
1986/// * `id` - The run's identifier, which is part of the envelope and so part of the cost.
1987/// * `stream` - Which stream the text came from.
1988/// * `seq` - The sequence number the chunk would carry.
1989/// * `text` - The text waiting to be sent.
1990///
1991/// # Returns
1992/// A byte count in `0..=text.len()` on a character boundary, or an error where
1993/// the envelope alone is too large for a frame, in which case no split helps.
1994pub fn chunk_fit(
1995 frame: Frame,
1996 id: &str,
1997 stream: Stream,
1998 seq: u64,
1999 text: &str,
2000)
2001 -> Outcome<usize>
2002{
2003 // The most the two limits and the text itself allow, on a boundary.
2004 let mut ceil = text.len().min(CHUNK_MAX);
2005 while ceil > 0 && !text.is_char_boundary(ceil) {
2006 ceil -= 1;
2007 }
2008 if res!(chunk_frame_len(frame, id, stream, seq, &text[..ceil])) <= FRAME_MAX {
2009 return Ok(ceil);
2010 }
2011 // An envelope that does not fit empty will not fit with anything in it.
2012 if res!(chunk_frame_len(frame, id, stream, seq, "")) > FRAME_MAX {
2013 return Err(Fault::FrameTooBig.raise(&fmt!(
2014 "A chunk for run {:?} does not fit in a {}-byte frame even with no \
2015 data in it, so no way of splitting the output will help.",
2016 id, FRAME_MAX)));
2017 }
2018 // Bisect, keeping `lo` a boundary that fits and `hi` one that does not.
2019 let mut lo = 0usize;
2020 let mut hi = ceil;
2021 while hi - lo > 1 {
2022 let mid = lo + (hi - lo) / 2;
2023 // Snap to a boundary strictly inside the interval, downwards first and
2024 // upwards where downwards would land on `lo`.
2025 let mut m = mid;
2026 while m > lo && !text.is_char_boundary(m) {
2027 m -= 1;
2028 }
2029 if m == lo {
2030 m = mid;
2031 while m < hi && !text.is_char_boundary(m) {
2032 m += 1;
2033 }
2034 if m >= hi {
2035 break;
2036 }
2037 }
2038 if res!(chunk_frame_len(frame, id, stream, seq, &text[..m])) <= FRAME_MAX {
2039 lo = m;
2040 } else {
2041 hi = m;
2042 }
2043 }
2044 Ok(lo)
2045}
2046
2047// ┌───────────────────────────────────────────────────────────────┐
2048// │ Fitting terminal output into frames │
2049// └───────────────────────────────────────────────────────────────┘
2050
2051/// The framed length of an output frame carrying the given base64.
2052///
2053/// # Arguments
2054/// * `frame` - The framing in use.
2055/// * `id` - The session's identifier.
2056/// * `seq` - The sequence number the frame would carry.
2057/// * `data` - The base64 the frame would carry.
2058fn output_frame_len(
2059 frame: Frame,
2060 id: &str,
2061 seq: u64,
2062 data: &str,
2063)
2064 -> Outcome<usize>
2065{
2066 resp_frame_len(frame, &Resp::Output {
2067 id: id.to_string(),
2068 seq,
2069 data: data.to_string(),
2070 })
2071}
2072
2073/// One output frame carrying exactly these bytes.
2074///
2075/// Does not check that they fit: [`output_frames`] is what a caller streaming a
2076/// terminal wants, and this is what it builds each frame with.
2077///
2078/// # Arguments
2079/// * `id` - The session's identifier.
2080/// * `seq` - The sequence number.
2081/// * `bytes` - The raw bytes from the terminal.
2082pub fn output_msg(id: &str, seq: u64, bytes: &[u8]) -> Resp {
2083 Resp::Output {
2084 id: id.to_string(),
2085 seq,
2086 data: data_encode(bytes),
2087 }
2088}
2089
2090/// How many raw bytes a single output frame can carry.
2091///
2092/// **Measured, not calculated.** A first estimate is arithmetic -- base64 turns
2093/// three bytes into four characters, and those characters are the one alphabet
2094/// JSON never escapes, so the encoding costs exactly its own length -- but the
2095/// *envelope* is not arithmetic at all: `id` is caller-supplied, is echoed on
2096/// every frame, and may hold quotes and control bytes that cost two and six
2097/// characters each. So the envelope is priced by encoding one, and the answer
2098/// is then confirmed by encoding the candidate rather than trusted.
2099///
2100/// The answer is also held at or below [`OUTPUT_MAX`], and always lands on a
2101/// whole base64 quantum where it is a cut rather than the whole buffer, so the
2102/// far end never has to stitch two frames to decode one quantum.
2103///
2104/// # Arguments
2105/// * `frame` - The framing in use.
2106/// * `id` - The session's identifier, which is part of the envelope and so part of the cost.
2107/// * `seq` - The sequence number the frame would carry.
2108/// * `bytes` - The bytes waiting to be sent.
2109///
2110/// # Returns
2111/// A byte count in `0..=bytes.len()`, or a [`Fault::FrameTooBig`] where the
2112/// envelope alone is too large for a frame, in which case no split helps.
2113pub fn output_fit(
2114 frame: Frame,
2115 id: &str,
2116 seq: u64,
2117 bytes: &[u8],
2118)
2119 -> Outcome<usize>
2120{
2121 // What the envelope costs before a byte of payload is paid for.
2122 let envelope = res!(output_frame_len(frame, id, seq, ""));
2123 if envelope > FRAME_MAX {
2124 return Err(Fault::FrameTooBig.raise(&fmt!(
2125 "An output frame for session {:?} does not fit in a {}-byte frame \
2126 even with no data in it, so no way of splitting the output will \
2127 help.", id, FRAME_MAX)));
2128 }
2129 if bytes.is_empty() {
2130 return Ok(0);
2131 }
2132 // The room left for base64, and the whole quanta that fit in it.
2133 let room = FRAME_MAX - envelope;
2134 let mut n = bytes.len()
2135 .min(OUTPUT_MAX)
2136 .min(room / 4 * 3);
2137 // Confirm by encoding. The estimate is exact for every encoder that escapes
2138 // JSON the way JSON is defined, and this loop is what makes the answer true
2139 // of the encoder actually in use rather than of the one described here.
2140 while n > 0 && res!(output_frame_len(frame, id, seq, &data_encode(&bytes[..n]))) > FRAME_MAX {
2141 n = n.saturating_sub(3);
2142 }
2143 Ok(n)
2144}
2145
2146/// Every frame a run of terminal bytes becomes, each one sendable.
2147///
2148/// **This is what a caller streaming a pty should reach for.** `REVIEW.md` §3.1
2149/// records the shape of the alternative: `exec.rs` split its output at a fixed
2150/// size, a caller-supplied identifier made the envelope larger than the split
2151/// assumed, and the resulting frame was refused at the pipe and the output
2152/// silently lost. A helper that has to be remembered is a helper that is
2153/// forgotten, so the whole buffer goes in and frames that fit come out.
2154///
2155/// The result is one frame per [`OUTPUT_MAX`] bytes and the base64 of each is
2156/// held until the caller has them, so this wants one read's worth of terminal
2157/// output rather than a whole session's: the caller decides how much memory the
2158/// call costs by deciding how much it hands over.
2159///
2160/// # Arguments
2161/// * `frame` - The framing in use.
2162/// * `id` - The session's identifier.
2163/// * `seq` - The sequence number of the first frame; later ones follow it.
2164/// * `bytes` - The bytes read from the terminal.
2165///
2166/// # Returns
2167/// One [`Resp::Output`] per frame, in order, each of which
2168/// [`resp_fits`] accepts. Empty input yields no frames, since a frame carrying
2169/// nothing says nothing. Fails where the envelope alone is too large for a
2170/// frame, or where the run would need a sequence number above [`SAFE_INT_MAX`].
2171pub fn output_frames(
2172 frame: Frame,
2173 id: &str,
2174 seq: u64,
2175 bytes: &[u8],
2176)
2177 -> Outcome<Vec<Resp>>
2178{
2179 if seq > SAFE_INT_MAX {
2180 return Err(Fault::WrongShape.raise(&fmt!(
2181 "An output frame for session {:?} would start at sequence {}, and \
2182 the largest one the page can compare is {}.", id, seq, SAFE_INT_MAX)));
2183 }
2184 let mut out = Vec::new();
2185 let mut pos = 0usize;
2186 let mut n = seq;
2187 while pos < bytes.len() {
2188 let take = res!(output_fit(frame, id, n, &bytes[pos..]));
2189 if take == 0 {
2190 // `output_fit` refuses an impossible envelope, so a zero here can
2191 // only mean the room shrank to nothing between the two calls, which
2192 // it cannot; refusing loudly beats looping.
2193 return Err(Fault::FrameTooBig.raise(&fmt!(
2194 "No output frame for session {:?} at sequence {} can carry even \
2195 one byte.", id, n)));
2196 }
2197 out.push(output_msg(id, n, &bytes[pos..pos + take]));
2198 pos += take;
2199 if pos < bytes.len() {
2200 if n == SAFE_INT_MAX {
2201 return Err(Fault::WrongShape.raise(&fmt!(
2202 "Session {:?} has reached sequence {}, the largest the page \
2203 can compare, with {} bytes still to send.",
2204 id, SAFE_INT_MAX, bytes.len() - pos)));
2205 }
2206 n += 1;
2207 }
2208 }
2209 Ok(out)
2210}
2211
2212// ┌───────────────────────────────────────────────────────────────┐
2213// │ Tests │
2214// └───────────────────────────────────────────────────────────────┘
2215
2216#[cfg(test)]
2217mod tests {
2218 use super::*;
2219
2220 use std::io::Cursor;
2221
2222 /// One of each request, for the round trips.
2223 fn reqs() -> Vec<Req> {
2224 vec![
2225 Req::Hello {
2226 proto: 1,
2227 client: fmt!("daimond/60"),
2228 },
2229 // A folder walk, and the ask that means "where do you start?" -- an empty path is a
2230 // question and not a malformed one, so both shapes round trip.
2231 Req::Dirs { path: fmt!("/home/u/usr") },
2232 Req::Dirs { path: fmt!("") },
2233 Req::Grant { path: fmt!("/home/u/work") },
2234 Req::Exec {
2235 id: fmt!("run-1"),
2236 argv: vec![fmt!("cargo"), fmt!("test")],
2237 cwd: fmt!("/abs"),
2238 env: vec![(fmt!("K"), fmt!("V")), (fmt!("PATH"), fmt!("/usr/bin"))],
2239 stdin: None,
2240 timeout_ms: 120_000,
2241 capture: Capture::Both,
2242 fence: FenceSpec {
2243 rw: vec![fmt!("/a")],
2244 ro: vec![],
2245 deny: vec![],
2246 net: false,
2247 },
2248 toolkits: Vec::new(),
2249 },
2250 Req::Exec {
2251 id: fmt!("run-2"),
2252 argv: vec![fmt!("sh")],
2253 cwd: fmt!("/tmp/x"),
2254 env: vec![],
2255 stdin: Some(fmt!("a \"quoted\" line\nand a tab\there")),
2256 timeout_ms: 0,
2257 capture: Capture::None,
2258 fence: FenceSpec {
2259 rw: vec![fmt!("/a"), fmt!("/b")],
2260 ro: vec![fmt!("/usr")],
2261 deny: vec![fmt!("/a/.daimond")],
2262 net: true,
2263 },
2264 toolkits: Vec::new(),
2265 },
2266 Req::Verify {
2267 id: fmt!("v-1"),
2268 name: fmt!("graph"),
2269 breaks: Breaks::All,
2270 timeout_ms: 1_200_000,
2271 },
2272 Req::Verify {
2273 id: fmt!("v-2"),
2274 name: fmt!("a11y_aria"),
2275 breaks: Breaks::One(fmt!("nolinks")),
2276 timeout_ms: 60_000,
2277 },
2278 Req::Verify {
2279 id: fmt!("v-3"),
2280 name: fmt!("graph"),
2281 breaks: Breaks::None,
2282 timeout_ms: SAFE_INT_MAX,
2283 },
2284 Req::Signal { id: fmt!("run-1"), sig: Sig::Term },
2285 Req::Signal { id: fmt!("run-1"), sig: Sig::Kill },
2286 Req::Signal { id: fmt!("run-1"), sig: Sig::Int },
2287 Req::Open {
2288 id: fmt!("pty-1"),
2289 argv: vec![fmt!("bash"), fmt!("-l")],
2290 cwd: fmt!("/abs"),
2291 env: vec![(fmt!("K"), fmt!("V"))],
2292 size: PtySize { cols: 80, rows: 24 },
2293 fence: FenceSpec {
2294 rw: vec![fmt!("/a")],
2295 ro: vec![fmt!("/usr")],
2296 deny: vec![fmt!("/a/.daimond")],
2297 net: false,
2298 },
2299 toolkits: Vec::new(),
2300 },
2301 Req::Open {
2302 id: fmt!("pty-2"),
2303 argv: vec![fmt!("sh")],
2304 cwd: fmt!("/"),
2305 env: vec![],
2306 // The extremes of the field's own type, which is where a decoder
2307 // that widened or narrowed it would show.
2308 size: PtySize { cols: 0, rows: u16::MAX },
2309 fence: FenceSpec::default(),
2310 toolkits: Vec::new(),
2311 },
2312 // A carriage return, an escape, a NUL and a byte no UTF-8 decoder
2313 // accepts: the four things a terminal sends that text would lose.
2314 Req::Input { id: fmt!("pty-1"), data: data_encode(&[0x0d]) },
2315 Req::Input { id: fmt!("pty-1"), data: data_encode(&[0x1b, b'[', b'A']) },
2316 Req::Input { id: fmt!("pty-1"), data: data_encode(&[0x00, 0xff, 0xfe]) },
2317 Req::Input { id: fmt!("pty-1"), data: data_encode(&[]) },
2318 Req::Resize { id: fmt!("pty-1"), size: PtySize { cols: 120, rows: 40 } },
2319 Req::Runs,
2320 Req::Bye,
2321 ]
2322 }
2323
2324 /// One of each response, for the round trips.
2325 fn resps() -> Vec<Resp> {
2326 vec![
2327 Resp::Hello {
2328 proto: 1,
2329 host: fmt!("daimond-hand"),
2330 version: fmt!("0.1.0"),
2331 os: fmt!("linux"),
2332 caps: vec![fmt!("exec")],
2333 },
2334 Resp::Started { id: fmt!("run-1"), pid: 1234 },
2335 // The folder browser. An EMPTY listing and a full one are different shapes on the
2336 // wire -- an absent list and a list of nothing -- and a decoder that muddled them
2337 // would show a folder with no folders in it as a folder that could not be read.
2338 Resp::Dirs {
2339 path: fmt!("/home/u"),
2340 up: fmt!(""),
2341 dirs: vec![fmt!("work"), fmt!(".config")],
2342 roots: vec![fmt!("/home/u"), fmt!("/home/u/usr")],
2343 },
2344 Resp::Granted {
2345 path: fmt!("/home/u/work"),
2346 note: fmt!("written; it applies when the hand next starts"),
2347 },
2348 Resp::Dirs {
2349 path: fmt!("/home/u/empty"),
2350 up: fmt!("/home/u"),
2351 dirs: Vec::new(),
2352 roots: Vec::new(),
2353 },
2354 Resp::Chunk {
2355 id: fmt!("run-1"),
2356 stream: Stream::Out,
2357 seq: 0,
2358 data: fmt!("hello — 日本\u{1}\n"),
2359 },
2360 Resp::Chunk {
2361 id: fmt!("run-1"),
2362 stream: Stream::Err,
2363 seq: SAFE_INT_MAX,
2364 data: fmt!(""),
2365 },
2366 Resp::Ended {
2367 id: fmt!("run-1"),
2368 exit: 0,
2369 timed_out: false,
2370 killed: false,
2371 out_bytes: 12,
2372 err_bytes: 0,
2373 },
2374 Resp::Ended {
2375 id: fmt!("run-2"),
2376 exit: -1,
2377 timed_out: true,
2378 killed: true,
2379 out_bytes: 0,
2380 err_bytes: 9,
2381 },
2382 Resp::Refused { id: fmt!("run-3"), reason: fmt!("Outside the fence.") },
2383 Resp::Opened { id: fmt!("pty-1"), pid: 4321 },
2384 Resp::Output { id: fmt!("pty-1"), seq: 0, data: data_encode(b"$ ") },
2385 Resp::Output {
2386 id: fmt!("pty-1"),
2387 seq: SAFE_INT_MAX,
2388 data: data_encode(&[0x1b, b'[', b'2', b'J', 0x00, 0xc3, 0x28]),
2389 },
2390 Resp::Output { id: fmt!("pty-1"), seq: 7, data: data_encode(&[]) },
2391 Resp::Closed { id: fmt!("pty-1"), exit: 0, killed: false },
2392 Resp::Closed { id: fmt!("pty-2"), exit: -1, killed: true },
2393 Resp::Error { id: Some(fmt!("run-3")), message: fmt!("Broke.") },
2394 Resp::Error { id: None, message: fmt!("Broke before there was a run.") },
2395 Resp::Runs { runs: Vec::new(), more: 0 },
2396 Resp::Runs {
2397 runs: vec![
2398 Run {
2399 id: fmt!("run-bash-3"),
2400 pid: 4242,
2401 what: fmt!("bash dev/world.sh 3 --up"),
2402 state: RunState::Standing,
2403 secs: 91,
2404 },
2405 // The extremes of the fields' own types, which is where a
2406 // decoder that widened or narrowed one would show.
2407 Run {
2408 id: fmt!("run-cargo-1"),
2409 pid: u32::MAX,
2410 what: fmt!(""),
2411 state: RunState::Running,
2412 secs: u32::MAX,
2413 },
2414 ],
2415 more: 7,
2416 },
2417 ]
2418 }
2419
2420 /// Every capture mode is spelled and read back.
2421 #[test]
2422 fn capture_vocabulary() -> Outcome<()> {
2423 for c in [Capture::Both, Capture::Out, Capture::Err, Capture::None] {
2424 assert_eq!(c, res!(capture_of(capture_name(c))));
2425 }
2426 assert!(capture_of("BOTH").is_err());
2427 assert!(capture_of("").is_err());
2428 Ok(())
2429 }
2430
2431 /// Every run state is spelled and read back, and nothing else is accepted.
2432 #[test]
2433 fn run_state_vocabulary() -> Outcome<()> {
2434 for st in [RunState::Running, RunState::Standing] {
2435 assert_eq!(st, res!(run_state_of(st.word())));
2436 }
2437 assert!(run_state_of("Running").is_err());
2438 assert!(run_state_of("stopped").is_err());
2439 assert!(run_state_of("").is_err());
2440 Ok(())
2441 }
2442
2443 /// A listing that overruns either of its two ceilings is refused rather than
2444 /// read.
2445 ///
2446 /// Both are the wire's own limits and both matter for the same reason: the
2447 /// listing is the ONE message that has to be believed about what is still
2448 /// running, so a frame that could not have been produced by this hand is
2449 /// better refused by name than silently half-read.
2450 #[test]
2451 fn a_listing_past_its_ceilings_is_refused() -> Outcome<()> {
2452 let one = |what: &str| -> String {
2453 fmt!(r#"{{"t":"runs","more":0,"runs":[{{"id":"a","pid":1,"what":"{}", "state":"standing","secs":0}}]}}"#, what)
2454 };
2455 let ok = res!(resp_of_json(&one(&"x".repeat(RUN_WHAT_MAX))));
2456 match ok {
2457 Resp::Runs { runs, .. } => assert_eq!(runs[0].what.len(), RUN_WHAT_MAX),
2458 other => return Err(err!("Expected a listing, got {:?}.", other; Test, Mismatch)),
2459 }
2460 assert!(resp_of_json(&one(&"x".repeat(RUN_WHAT_MAX + 1))).is_err(),
2461 "a command line past RUN_WHAT_MAX was accepted");
2462
2463 let entry = r#"{"id":"a","pid":1,"what":"x","state":"running","secs":0}"#;
2464 let many = |n: usize| -> String {
2465 fmt!(r#"{{"t":"runs","more":0,"runs":[{}]}}"#,
2466 vec![entry; n].join(","))
2467 };
2468 assert!(resp_of_json(&many(RUNS_MAX)).is_ok(), "a listing of exactly RUNS_MAX was refused");
2469 assert!(resp_of_json(&many(RUNS_MAX + 1)).is_err(),
2470 "a listing past RUNS_MAX was accepted");
2471 Ok(())
2472 }
2473
2474 /// Every signal and stream is spelled and read back.
2475 #[test]
2476 fn sig_and_stream_vocabulary() -> Outcome<()> {
2477 for s in [Sig::Term, Sig::Kill, Sig::Int] {
2478 assert_eq!(s, res!(sig_of(sig_name(s))));
2479 }
2480 for s in [Stream::Out, Stream::Err] {
2481 assert_eq!(s, res!(stream_of(stream_name(s))));
2482 }
2483 assert!(sig_of("hup").is_err());
2484 assert!(stream_of("both").is_err());
2485 Ok(())
2486 }
2487
2488 /// Every request survives the JSON.
2489 #[test]
2490 fn req_round_trip() -> Outcome<()> {
2491 for req in reqs() {
2492 let txt = res!(req_json(&req));
2493 let back = res!(req_of_json(&txt));
2494 assert_eq!(req, back, "{}", txt);
2495 }
2496 Ok(())
2497 }
2498
2499 /// A verify asking for one break and naming none is refused, not read as
2500 /// asking for none.
2501 ///
2502 /// The two are opposite requests: one asks to prove something and the other
2503 /// proves nothing, so a decoder that quietly turned the first into the
2504 /// second would hand back a pass with nothing behind it.
2505 #[test]
2506 fn a_verify_asking_for_one_break_must_name_it() -> Outcome<()> {
2507 let bad = r#"{"t":"verify","id":"v","name":"graph","breaks":"one","break":null,"timeout_ms":1000}"#;
2508 assert!(req_of_json(bad).is_err(), "'one' with no break was accepted");
2509 let worse = r#"{"t":"verify","id":"v","name":"graph","breaks":"most","break":null,"timeout_ms":1000}"#;
2510 assert!(req_of_json(worse).is_err(), "an invented breaks word was accepted");
2511 let good = r#"{"t":"verify","id":"v","name":"graph","breaks":"one","break":"x","timeout_ms":1000}"#;
2512 assert_eq!(Req::Verify {
2513 id: fmt!("v"),
2514 name: fmt!("graph"),
2515 breaks: Breaks::One(fmt!("x")),
2516 timeout_ms: 1000,
2517 }, res!(req_of_json(good)));
2518 Ok(())
2519 }
2520
2521 /// Every response survives the JSON.
2522 #[test]
2523 fn resp_round_trip() -> Outcome<()> {
2524 for resp in resps() {
2525 let txt = res!(resp_json(&resp));
2526 let back = res!(resp_of_json(&txt));
2527 assert_eq!(resp, back, "{}", txt);
2528 }
2529 Ok(())
2530 }
2531
2532 /// Every message survives both framings.
2533 #[test]
2534 fn frame_round_trip() -> Outcome<()> {
2535 for frame in [Frame::NativeMessaging, Frame::WebSocket] {
2536 for req in reqs() {
2537 let buf = res!(frame.wrap(&res!(req_json(&req))));
2538 let back = res!(req_of_json(&res!(frame.unwrap(&buf))));
2539 assert_eq!(req, back);
2540 }
2541 for resp in resps() {
2542 let buf = res!(frame.wrap(&res!(resp_json(&resp))));
2543 let back = res!(resp_of_json(&res!(frame.unwrap(&buf))));
2544 assert_eq!(resp, back);
2545 }
2546 }
2547 Ok(())
2548 }
2549
2550 /// The tagged shapes are the ones the contract states.
2551 #[test]
2552 fn tagged_shapes() -> Outcome<()> {
2553 let txt = res!(req_json(&Req::Signal { id: fmt!("x"), sig: Sig::Term }));
2554 assert!(txt.contains("\"t\""), "{}", txt);
2555 assert!(txt.contains("\"signal\""), "{}", txt);
2556 assert!(txt.contains("\"term\""), "{}", txt);
2557
2558 let txt = res!(req_json(&Req::Exec {
2559 id: fmt!("x"),
2560 argv: vec![fmt!("cargo"), fmt!("test")],
2561 cwd: fmt!("/abs"),
2562 env: vec![(fmt!("K"), fmt!("V"))],
2563 stdin: None,
2564 timeout_ms: 120_000,
2565 capture: Capture::Both,
2566 fence: FenceSpec { rw: vec![fmt!("/a")], ..Default::default() },
2567 toolkits: Vec::new(),
2568 }));
2569 // The absent standard input is `null`, not the word "none": in
2570 // JavaScript the latter is a truthy string and would read as present.
2571 assert!(txt.contains("null"), "{}", txt);
2572 assert!(txt.contains("[ \"K\", \"V\"]") || txt.contains("[\"K\",\"V\"]"), "{}", txt);
2573 assert!(txt.contains("\"both\""), "{}", txt);
2574 assert!(txt.contains("\"net\""), "{}", txt);
2575
2576 let txt = res!(resp_json(&Resp::Error { id: None, message: fmt!("m") }));
2577 assert!(txt.contains("null"), "{}", txt);
2578 Ok(())
2579 }
2580
2581 /// A native messaging frame is a 4-byte native-endian prefix and the JSON.
2582 #[test]
2583 fn framing_is_byte_for_byte() -> Outcome<()> {
2584 let buf = res!(Frame::NativeMessaging.wrap(&res!(req_json(&Req::Bye))));
2585
2586 // The whole frame, hand written.
2587 let body: &[u8] = b"{ \"t\": \"bye\"}";
2588 assert_eq!(13, body.len());
2589 let prefix: [u8; 4] = if cfg!(target_endian = "little") {
2590 [13, 0, 0, 0]
2591 } else {
2592 [0, 0, 0, 13]
2593 };
2594 let mut want = Vec::new();
2595 want.extend_from_slice(&prefix);
2596 want.extend_from_slice(body);
2597 assert_eq!(want, buf);
2598
2599 // The WebSocket arm is the same bytes with nothing in front.
2600 let ws = res!(Frame::WebSocket.wrap(&res!(req_json(&Req::Bye))));
2601 assert_eq!(body.to_vec(), ws);
2602 Ok(())
2603 }
2604
2605 /// A frame written to a stream is the frame read back from it.
2606 #[test]
2607 fn write_then_read() -> Outcome<()> {
2608 let mut buf = Vec::new();
2609 for req in reqs() {
2610 res!(Frame::NativeMessaging.write_req(&mut buf, &req));
2611 }
2612 let mut cur = Cursor::new(buf);
2613 for req in reqs() {
2614 match res!(Frame::NativeMessaging.read_req(&mut cur)) {
2615 Some(back) => assert_eq!(req, back),
2616 None => return Err(err!("The stream ended early."; Test, Missing)),
2617 }
2618 }
2619 // And then a clean end, not an error.
2620 assert!(res!(Frame::NativeMessaging.read_req(&mut cur)).is_none());
2621 Ok(())
2622 }
2623
2624 /// Responses go the other way just as well.
2625 #[test]
2626 fn write_then_read_resps() -> Outcome<()> {
2627 let mut buf = Vec::new();
2628 for resp in resps() {
2629 res!(Frame::NativeMessaging.write_resp(&mut buf, &resp));
2630 }
2631 let mut cur = Cursor::new(buf);
2632 for resp in resps() {
2633 match res!(Frame::NativeMessaging.read_resp(&mut cur)) {
2634 Some(back) => assert_eq!(resp, back),
2635 None => return Err(err!("The stream ended early."; Test, Missing)),
2636 }
2637 }
2638 Ok(())
2639 }
2640
2641 /// A stream that stops inside the length prefix is a short read.
2642 #[test]
2643 fn hostile_short_read() -> Outcome<()> {
2644 let mut cur = Cursor::new(vec![1u8, 0, 0]);
2645 match Frame::NativeMessaging.read_req(&mut cur) {
2646 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2647 Err(e) => assert_eq!(Some(Fault::ShortRead), Fault::of(&e), "{}", e),
2648 }
2649 // A prefix that never began is a clean end, not a fault.
2650 let mut cur = Cursor::new(Vec::new());
2651 assert!(res!(Frame::NativeMessaging.read_req(&mut cur)).is_none());
2652 Ok(())
2653 }
2654
2655 /// A stream that stops inside the body is a truncated frame.
2656 #[test]
2657 fn hostile_truncated() -> Outcome<()> {
2658 let mut buf = Vec::new();
2659 res!(Frame::NativeMessaging.write_req(&mut buf, &Req::Bye));
2660 buf.truncate(buf.len() - 3);
2661 let mut cur = Cursor::new(buf.clone());
2662 match Frame::NativeMessaging.read_req(&mut cur) {
2663 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2664 Err(e) => assert_eq!(Some(Fault::Truncated), Fault::of(&e), "{}", e),
2665 }
2666 // The slice form says the same.
2667 match Frame::NativeMessaging.unwrap(&buf) {
2668 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2669 Err(e) => assert_eq!(Some(Fault::Truncated), Fault::of(&e), "{}", e),
2670 }
2671 Ok(())
2672 }
2673
2674 /// A prefix promising more than the cap is refused before a byte is read.
2675 #[test]
2676 fn hostile_length_too_big() -> Outcome<()> {
2677 let n = (INBOUND_MAX + 1) as u32;
2678 let mut buf = n.to_ne_bytes().to_vec();
2679 buf.extend_from_slice(b"{}");
2680 let mut cur = Cursor::new(buf.clone());
2681 match Frame::NativeMessaging.read_req(&mut cur) {
2682 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2683 Err(e) => assert_eq!(Some(Fault::LengthTooBig), Fault::of(&e), "{}", e),
2684 }
2685 // And 4 GB, which is what a hostile sender actually writes.
2686 let mut buf = u32::MAX.to_ne_bytes().to_vec();
2687 buf.extend_from_slice(b"{}");
2688 match Frame::NativeMessaging.unwrap(&buf) {
2689 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2690 Err(e) => assert_eq!(Some(Fault::LengthTooBig), Fault::of(&e), "{}", e),
2691 }
2692 Ok(())
2693 }
2694
2695 /// Bytes that are not UTF-8 are refused rather than replaced.
2696 #[test]
2697 fn hostile_not_utf8() -> Outcome<()> {
2698 let body = vec![0xffu8, 0xfe, 0xfd];
2699 let mut buf = (body.len() as u32).to_ne_bytes().to_vec();
2700 buf.extend_from_slice(&body);
2701 let mut cur = Cursor::new(buf.clone());
2702 match Frame::NativeMessaging.read_req(&mut cur) {
2703 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2704 Err(e) => assert_eq!(Some(Fault::NotUtf8), Fault::of(&e), "{}", e),
2705 }
2706 match Frame::WebSocket.unwrap(&body) {
2707 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2708 Err(e) => assert_eq!(Some(Fault::NotUtf8), Fault::of(&e), "{}", e),
2709 }
2710 Ok(())
2711 }
2712
2713 /// Text that is not JSON at all, and text that is JSON but not an object.
2714 #[test]
2715 fn hostile_not_json_or_not_an_object() -> Outcome<()> {
2716 // Unclosed braces do not parse, and nor does nothing at all.
2717 for bad in ["{", "{\"t\":", "{\"t\": \"bye\"", "[[[[", ""] {
2718 match req_of_json(bad) {
2719 Ok(v) => return Err(err!(
2720 "Expected a refusal for {:?}, got {:?}.", bad, v; Test, Invalid)),
2721 Err(e) => assert_eq!(Some(Fault::NotJson), Fault::of(&e), "{}: {}", bad, e),
2722 }
2723 }
2724 // These parse, and are not messages.
2725 for bad in ["5", "\"bye\"", "[1, 2]"] {
2726 match req_of_json(bad) {
2727 Ok(v) => return Err(err!(
2728 "Expected a refusal for {:?}, got {:?}.", bad, v; Test, Invalid)),
2729 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", bad, e),
2730 }
2731 }
2732 Ok(())
2733 }
2734
2735 /// JSON of the right kind and the wrong shape.
2736 #[test]
2737 fn hostile_wrong_shape() -> Outcome<()> {
2738 let cases = [
2739 // No discriminator.
2740 "{\"proto\": 1}",
2741 // A discriminator that is not a string.
2742 "{\"t\": 5}",
2743 // Missing field.
2744 "{\"t\": \"hello\", \"proto\": 1}",
2745 // Wrong type for a field.
2746 "{\"t\": \"hello\", \"proto\": \"one\", \"client\": \"c\"}",
2747 "{\"t\": \"hello\", \"proto\": 1, \"client\": 5}",
2748 // A signal nobody offers.
2749 "{\"t\": \"signal\", \"id\": \"a\", \"sig\": \"hup\"}",
2750 // An environment entry that is not a pair.
2751 "{\"t\": \"exec\", \"id\": \"a\", \"argv\": [\"x\"], \"cwd\": \"/\", \
2752 \"env\": [[\"K\"]], \"stdin\": null, \"timeout_ms\": 1, \
2753 \"capture\": \"both\", \"fence\": {\"rw\": [], \"ro\": [], \
2754 \"deny\": [], \"net\": false}}",
2755 // An argv holding something that is not a string.
2756 "{\"t\": \"exec\", \"id\": \"a\", \"argv\": [5], \"cwd\": \"/\", \
2757 \"env\": [], \"stdin\": null, \"timeout_ms\": 1, \
2758 \"capture\": \"both\", \"fence\": {\"rw\": [], \"ro\": [], \
2759 \"deny\": [], \"net\": false}}",
2760 // A fence that is not an object.
2761 "{\"t\": \"exec\", \"id\": \"a\", \"argv\": [\"x\"], \"cwd\": \"/\", \
2762 \"env\": [], \"stdin\": null, \"timeout_ms\": 1, \
2763 \"capture\": \"both\", \"fence\": []}",
2764 ];
2765 for bad in cases {
2766 match req_of_json(bad) {
2767 Ok(v) => return Err(err!(
2768 "Expected a refusal for {:?}, got {:?}.", bad, v; Test, Invalid)),
2769 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", bad, e),
2770 }
2771 }
2772 // An exit status too large for an i32 is a shape fault, not a wrap.
2773 let bad = "{\"t\": \"ended\", \"id\": \"a\", \"exit\": 5000000000, \
2774 \"timed_out\": false, \"killed\": false, \"out_bytes\": 0, \"err_bytes\": 0}";
2775 match resp_of_json(bad) {
2776 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2777 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
2778 }
2779 Ok(())
2780 }
2781
2782 /// A `"t"` from a build that does not exist yet.
2783 #[test]
2784 fn hostile_unknown_tag() -> Outcome<()> {
2785 match req_of_json("{\"t\": \"detonate\"}") {
2786 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2787 Err(e) => assert_eq!(Some(Fault::UnknownTag), Fault::of(&e), "{}", e),
2788 }
2789 match resp_of_json("{\"t\": \"gloat\"}") {
2790 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
2791 Err(e) => assert_eq!(Some(Fault::UnknownTag), Fault::of(&e), "{}", e),
2792 }
2793 Ok(())
2794 }
2795
2796 /// An oversized frame is refused rather than written.
2797 #[test]
2798 fn oversize_is_refused() -> Outcome<()> {
2799 let big = Resp::Chunk {
2800 id: fmt!("run-1"),
2801 stream: Stream::Out,
2802 seq: 0,
2803 data: "x".repeat(2 * FRAME_MAX),
2804 };
2805 assert!(!res!(resp_fits(Frame::NativeMessaging, &big)));
2806 let mut sink = Vec::new();
2807 match Frame::NativeMessaging.write_resp(&mut sink, &big) {
2808 Ok(()) => return Err(err!("The oversized frame was written."; Test, Invalid)),
2809 Err(e) => assert_eq!(Some(Fault::FrameTooBig), Fault::of(&e), "{}", e),
2810 }
2811 // Nothing reached the pipe: a partial frame is worse than none.
2812 assert!(sink.is_empty());
2813 Ok(())
2814 }
2815
2816 /// The check measures the *encoded* size, not the raw one.
2817 ///
2818 /// Half a megabyte of `0x01` is well inside the cap as bytes and six times
2819 /// over it as JSON, so a check that subtracted a fixed envelope from
2820 /// [`FRAME_MAX`] would pass this and Chrome would drop the connection.
2821 #[test]
2822 fn oversize_is_measured_after_escaping() -> Outcome<()> {
2823 let raw = "\u{1}".repeat(500_000);
2824 assert!(raw.len() < FRAME_MAX, "the raw text must be inside the cap for this to prove anything");
2825 let resp = Resp::Chunk {
2826 id: fmt!("run-1"),
2827 stream: Stream::Out,
2828 seq: 0,
2829 data: raw,
2830 };
2831 assert!(res!(resp_frame_len(Frame::NativeMessaging, &resp)) > FRAME_MAX);
2832 assert!(!res!(resp_fits(Frame::NativeMessaging, &resp)));
2833 match Frame::NativeMessaging.wrap(&res!(resp_json(&resp))) {
2834 Ok(_) => return Err(err!("The oversized frame was wrapped."; Test, Invalid)),
2835 Err(e) => assert_eq!(Some(Fault::FrameTooBig), Fault::of(&e), "{}", e),
2836 }
2837 Ok(())
2838 }
2839
2840 /// The next character boundary strictly above `n`, or the length of the text.
2841 fn next_boundary(text: &str, n: usize) -> usize {
2842 let mut m = n + 1;
2843 while m < text.len() && !text.is_char_boundary(m) {
2844 m += 1;
2845 }
2846 m
2847 }
2848
2849 /// The fit helper returns a size that fits, and one character more that does not.
2850 ///
2851 /// The envelope is made large by a long identifier so that [`FRAME_MAX`]
2852 /// rather than [`CHUNK_MAX`] is the binding limit, which is the branch that
2853 /// has to measure the escaping.
2854 #[test]
2855 fn chunk_fit_is_tight() -> Outcome<()> {
2856 // A long run identifier is caller-supplied and therefore part of what
2857 // the envelope costs.
2858 let id = "i".repeat(900_000);
2859 // Control bytes cost six each in JSON and one byte here.
2860 let text = "\u{1}".repeat(200_000);
2861 let n = res!(chunk_fit(Frame::NativeMessaging, &id, Stream::Out, 0, &text));
2862 assert!(n > 0);
2863 assert!(n < CHUNK_MAX, "the frame cap must be what binds, not the chunk cap");
2864 assert!(res!(chunk_frame_len(
2865 Frame::NativeMessaging, &id, Stream::Out, 0, &text[..n])) <= FRAME_MAX);
2866 // One character more does not fit, which is what makes the answer tight
2867 // rather than merely safe.
2868 let more = next_boundary(&text, n);
2869 assert!(res!(chunk_frame_len(
2870 Frame::NativeMessaging, &id, Stream::Out, 0, &text[..more])) > FRAME_MAX);
2871 Ok(())
2872 }
2873
2874 /// The fit helper never splits a code point.
2875 #[test]
2876 fn chunk_fit_lands_on_a_boundary() -> Outcome<()> {
2877 let id = "i".repeat(900_000);
2878 // Three bytes each, so two byte offsets in three are not boundaries.
2879 let text = "日".repeat(100_000);
2880 let n = res!(chunk_fit(Frame::NativeMessaging, &id, Stream::Out, 0, &text));
2881 assert!(text.is_char_boundary(n), "{} is not a boundary", n);
2882 assert_eq!(0, n % 3);
2883 assert!(n > 0 && n < text.len());
2884 assert!(res!(chunk_frame_len(
2885 Frame::NativeMessaging, &id, Stream::Out, 0, &text[..n])) <= FRAME_MAX);
2886 let more = next_boundary(&text, n);
2887 assert!(res!(chunk_frame_len(
2888 Frame::NativeMessaging, &id, Stream::Out, 0, &text[..more])) > FRAME_MAX);
2889 Ok(())
2890 }
2891
2892 /// An envelope too large for a frame is an error, since no split helps.
2893 #[test]
2894 fn chunk_fit_refuses_an_impossible_envelope() -> Outcome<()> {
2895 let id = "i".repeat(FRAME_MAX + 1);
2896 match chunk_fit(Frame::NativeMessaging, &id, Stream::Out, 0, "x") {
2897 Ok(v) => return Err(err!("Expected a refusal, got {}.", v; Test, Invalid)),
2898 Err(e) => assert_eq!(Some(Fault::FrameTooBig), Fault::of(&e), "{}", e),
2899 }
2900 Ok(())
2901 }
2902
2903 /// Text that already fits is not split, and the chunk cap still holds.
2904 #[test]
2905 fn chunk_fit_holds_both_caps() -> Outcome<()> {
2906 let small = fmt!("hello");
2907 assert_eq!(
2908 small.len(),
2909 res!(chunk_fit(Frame::NativeMessaging, "run-1", Stream::Out, 0, &small)));
2910
2911 // Plain bytes escape to themselves, so CHUNK_MAX is the binding limit.
2912 let plain = "x".repeat(CHUNK_MAX * 2);
2913 assert_eq!(
2914 CHUNK_MAX,
2915 res!(chunk_fit(Frame::NativeMessaging, "run-1", Stream::Out, 0, &plain)));
2916
2917 // Nothing at all is nothing at all, not an error.
2918 assert_eq!(0, res!(chunk_fit(Frame::NativeMessaging, "run-1", Stream::Out, 0, "")));
2919 Ok(())
2920 }
2921
2922 /// The WebSocket arm carries the same payload with no prefix.
2923 #[test]
2924 fn websocket_carries_the_same_payload() -> Outcome<()> {
2925 let resp = Resp::Started { id: fmt!("run-1"), pid: 1234 };
2926 let txt = res!(resp_json(&resp));
2927 let ws = res!(Frame::WebSocket.wrap(&txt));
2928 let nm = res!(Frame::NativeMessaging.wrap(&txt));
2929 assert_eq!(ws.as_slice(), &nm[LEN_PREFIX..]);
2930 assert_eq!(txt, res!(Frame::WebSocket.unwrap(&ws)));
2931
2932 let mut cur = Cursor::new(ws);
2933 match res!(Frame::WebSocket.read_resp(&mut cur)) {
2934 Some(back) => assert_eq!(resp, back),
2935 None => return Err(err!("The payload read as nothing."; Test, Missing)),
2936 }
2937 Ok(())
2938 }
2939
2940 /// Every fault has its own name, so `Fault::of` cannot confuse two of them.
2941 #[test]
2942 fn fault_names_are_distinct() -> Outcome<()> {
2943 for (i, a) in Fault::ALL.iter().enumerate() {
2944 for b in Fault::ALL.iter().skip(i + 1) {
2945 assert_ne!(a.name(), b.name());
2946 // Nor may one name be a prefix of another, since `of` matches
2947 // on the start of the message.
2948 assert!(!b.name().starts_with(a.name()));
2949 assert!(!a.name().starts_with(b.name()));
2950 }
2951 assert_eq!(Some(*a), Fault::of(&a.raise("because")));
2952 }
2953 Ok(())
2954 }
2955
2956 // ── The terminal half ───────────────────────────────────────────
2957
2958 /// The six pty messages have the tags and the field order the contract states.
2959 #[test]
2960 fn pty_tagged_shapes() -> Outcome<()> {
2961 let txt = res!(req_json(&Req::Open {
2962 id: fmt!("p"),
2963 argv: vec![fmt!("bash")],
2964 cwd: fmt!("/abs"),
2965 env: vec![(fmt!("K"), fmt!("V"))],
2966 size: PtySize { cols: 80, rows: 24 },
2967 fence: FenceSpec { rw: vec![fmt!("/a")], ..Default::default() },
2968 toolkits: Vec::new(),
2969 }));
2970 assert!(txt.contains("\"open\""), "{}", txt);
2971 assert!(txt.contains("\"cols\""), "{}", txt);
2972 assert!(txt.contains("\"rows\""), "{}", txt);
2973 // The order the contract states, not the sorted one `mapdat!` gives.
2974 let order = ["\"t\"", "\"id\"", "\"argv\"", "\"cwd\"", "\"env\"", "\"size\"", "\"fence\""];
2975 let mut at = 0;
2976 for key in order {
2977 match txt[at..].find(key) {
2978 Some(k) => at += k + key.len(),
2979 None => return Err(err!(
2980 "{:?} is missing or out of order in {}", key, txt; Test, Invalid)),
2981 }
2982 }
2983
2984 let txt = res!(req_json(&Req::Input { id: fmt!("p"), data: data_encode(b"hi") }));
2985 assert!(txt.contains("\"input\""), "{}", txt);
2986 assert!(txt.contains("\"aGk=\""), "{}", txt);
2987
2988 let txt = res!(req_json(&Req::Resize {
2989 id: fmt!("p"),
2990 size: PtySize { cols: 120, rows: 40 },
2991 }));
2992 assert!(txt.contains("\"resize\""), "{}", txt);
2993 assert!(txt.contains("120") && txt.contains("40"), "{}", txt);
2994
2995 let txt = res!(resp_json(&Resp::Opened { id: fmt!("p"), pid: 1234 }));
2996 assert!(txt.contains("\"opened\"") && txt.contains("1234"), "{}", txt);
2997
2998 let txt = res!(resp_json(&output_msg("p", 3, b"hi")));
2999 assert!(txt.contains("\"output\""), "{}", txt);
3000 assert!(txt.contains("\"aGk=\""), "{}", txt);
3001
3002 let txt = res!(resp_json(&Resp::Closed { id: fmt!("p"), exit: -1, killed: true }));
3003 assert!(txt.contains("\"closed\"") && txt.contains("-1") && txt.contains("true"),
3004 "{}", txt);
3005 Ok(())
3006 }
3007
3008 /// The terminal's bytes arrive as the terminal's bytes, whatever they are.
3009 ///
3010 /// Every one of the 256 values, invalid UTF-8 and embedded NUL included: the
3011 /// reason the pty half carries base64 rather than text is that a lossy
3012 /// conversion corrupts exactly this, and a test that only sends ASCII would
3013 /// pass against a codec that had thrown the guarantee away.
3014 #[test]
3015 fn pty_data_is_byte_exact() -> Outcome<()> {
3016 let all: Vec<u8> = (0u16..=255).map(|b| b as u8).collect();
3017 let cases: Vec<Vec<u8>> = vec![
3018 Vec::new(),
3019 vec![0x00],
3020 vec![0x00, 0x00, 0x00],
3021 // Lone continuation bytes, a truncated three-byte sequence, and an
3022 // overlong encoding: `from_utf8_lossy` turns each into U+FFFD.
3023 vec![0x80, 0xbf, 0xc3, 0x28, 0xe2, 0x82],
3024 vec![0xff, 0xfe, 0xfd, 0xfc],
3025 // A half-written character at the edge of a read.
3026 fmt!("日本").into_bytes()[..4].to_vec(),
3027 b"\x1b[31mred\x1b[0m\r\n".to_vec(),
3028 all.clone(),
3029 all.repeat(7),
3030 ];
3031 for raw in cases {
3032 // Out: the hand to the page.
3033 let msg = output_msg("pty-1", 1, &raw);
3034 let back = res!(resp_of_json(&res!(resp_json(&msg))));
3035 match back {
3036 Resp::Output { data, .. } => assert_eq!(raw, res!(data_decode(&data))),
3037 other => return Err(err!("Read back {:?}.", other; Test, Invalid)),
3038 }
3039 // In: the page to the hand.
3040 let msg = Req::Input { id: fmt!("pty-1"), data: data_encode(&raw) };
3041 let back = res!(req_of_json(&res!(req_json(&msg))));
3042 match back {
3043 Req::Input { data, .. } => assert_eq!(raw, res!(data_decode(&data))),
3044 other => return Err(err!("Read back {:?}.", other; Test, Invalid)),
3045 }
3046 }
3047 Ok(())
3048 }
3049
3050 /// Anything that is not standard base64 is refused, in both directions.
3051 #[test]
3052 fn hostile_bad_base64() -> Outcome<()> {
3053 let bad = [
3054 "Zm9", // Not a whole quantum.
3055 "Zm9vY", // Nor this.
3056 "Zm9v Zg==", // Whitespace is not in the alphabet.
3057 // Nor a newline, which a folded header would have. Spelled as the
3058 // JSON escape, so that the strict-JSON gate passes it through to the
3059 // base64 check rather than refusing the raw control byte first.
3060 "Zm9v\\nZg==",
3061 "!!!!", // Nothing in the alphabet at all.
3062 "Zm-_", // The URL-safe alphabet is a different alphabet.
3063 "Zm=9", // Padding in the middle.
3064 "====", // Padding and nothing else.
3065 "Zm9v====", // Three pad characters is Base2x, not RFC 4648.
3066 "Zm9=", // A final quantum whose unused bits are set.
3067 "\u{65e5}m9v", // Not even ASCII.
3068 ];
3069 for s in bad {
3070 let txt = fmt!("{{\"t\": \"input\", \"id\": \"p\", \"data\": \"{}\"}}", s);
3071 match req_of_json(&txt) {
3072 Ok(v) => return Err(err!(
3073 "Expected a refusal for {:?}, got {:?}.", s, v; Test, Invalid)),
3074 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", s, e),
3075 }
3076 let txt = fmt!(
3077 "{{\"t\": \"output\", \"id\": \"p\", \"seq\": 0, \"data\": \"{}\"}}", s);
3078 match resp_of_json(&txt) {
3079 Ok(v) => return Err(err!(
3080 "Expected a refusal for {:?}, got {:?}.", s, v; Test, Invalid)),
3081 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", s, e),
3082 }
3083 // And the hand refuses to write one it built wrongly itself.
3084 let out = Resp::Output { id: fmt!("p"), seq: 0, data: s.to_string() };
3085 match resp_json(&out) {
3086 Ok(v) => return Err(err!(
3087 "Expected a refusal for {:?}, got {:?}.", s, v; Test, Invalid)),
3088 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", s, e),
3089 }
3090 }
3091 // A `data` that is not a string at all is caught before base64 is asked.
3092 match req_of_json("{\"t\": \"input\", \"id\": \"p\", \"data\": 5}") {
3093 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3094 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3095 }
3096 // And a missing one.
3097 match req_of_json("{\"t\": \"input\", \"id\": \"p\"}") {
3098 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3099 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3100 }
3101 Ok(())
3102 }
3103
3104 /// A terminal size that is not one.
3105 #[test]
3106 fn hostile_bad_size() -> Outcome<()> {
3107 let cases = [
3108 // Missing outright.
3109 "{\"t\": \"resize\", \"id\": \"p\"}",
3110 // Not an object.
3111 "{\"t\": \"resize\", \"id\": \"p\", \"size\": 80}",
3112 "{\"t\": \"resize\", \"id\": \"p\", \"size\": [80, 24]}",
3113 "{\"t\": \"resize\", \"id\": \"p\", \"size\": null}",
3114 // Half a size.
3115 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": 80}}",
3116 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"rows\": 24}}",
3117 // Negative.
3118 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": -1, \"rows\": 24}}",
3119 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": 80, \"rows\": -24}}",
3120 // Enormous: one past the field, and far past it.
3121 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": 65536, \"rows\": 24}}",
3122 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": 80, \"rows\": 4294967296}}",
3123 // Not a whole number.
3124 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": 80.5, \"rows\": 24}}",
3125 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": \"80\", \"rows\": 24}}",
3126 "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": true, \"rows\": 24}}",
3127 ];
3128 for bad in cases {
3129 match req_of_json(bad) {
3130 Ok(v) => return Err(err!(
3131 "Expected a refusal for {:?}, got {:?}.", bad, v; Test, Invalid)),
3132 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", bad, e),
3133 }
3134 }
3135 // The same field on `open`, so the check is not attached to one message.
3136 let bad = "{\"t\": \"open\", \"id\": \"p\", \"argv\": [\"sh\"], \"cwd\": \"/\", \
3137 \"env\": [], \"size\": {\"cols\": 100000, \"rows\": 24}, \
3138 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}";
3139 match req_of_json(bad) {
3140 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3141 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3142 }
3143 // The two extremes of the field itself are accepted, since they are what
3144 // the contract says a size is.
3145 let good = "{\"t\": \"resize\", \"id\": \"p\", \"size\": {\"cols\": 0, \"rows\": 65535}}";
3146 assert_eq!(
3147 Req::Resize { id: fmt!("p"), size: PtySize { cols: 0, rows: 65535 } },
3148 res!(req_of_json(good)));
3149 Ok(())
3150 }
3151
3152 /// A sequence number the page could not compare is refused, not rounded.
3153 ///
3154 /// `REVIEW.md` §3.11. `JSON.parse` reads 2^53 as 9007199254740992 and 2^53+1
3155 /// as the same number, so the page's `msg.seq !== want` would be comparing
3156 /// two different frames' sequence numbers and finding them equal.
3157 #[test]
3158 fn hostile_seq_beyond_javascript() -> Outcome<()> {
3159 // The largest that works, which is the boundary the check sits on.
3160 let txt = fmt!(
3161 "{{\"t\": \"output\", \"id\": \"p\", \"seq\": {}, \"data\": \"\"}}", SAFE_INT_MAX);
3162 match res!(resp_of_json(&txt)) {
3163 Resp::Output { seq, .. } => assert_eq!(SAFE_INT_MAX, seq),
3164 other => return Err(err!("Read back {:?}.", other; Test, Invalid)),
3165 }
3166 // One past it, and the value the review actually observed.
3167 for n in [SAFE_INT_MAX + 1, u64::MAX] {
3168 let txt = fmt!(
3169 "{{\"t\": \"output\", \"id\": \"p\", \"seq\": {}, \"data\": \"\"}}", n);
3170 match resp_of_json(&txt) {
3171 Ok(v) => return Err(err!(
3172 "Expected a refusal for {}, got {:?}.", n, v; Test, Invalid)),
3173 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3174 }
3175 // The hand will not write one either.
3176 match resp_json(&Resp::Output { id: fmt!("p"), seq: n, data: fmt!("") }) {
3177 Ok(v) => return Err(err!(
3178 "Expected a refusal for {}, got {:?}.", n, v; Test, Invalid)),
3179 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3180 }
3181 }
3182 // A negative sequence is not a sequence.
3183 match resp_of_json("{\"t\": \"output\", \"id\": \"p\", \"seq\": -1, \"data\": \"\"}") {
3184 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3185 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3186 }
3187 Ok(())
3188 }
3189
3190 /// Every `u64` the contract names is refused past 2^53, in both directions.
3191 ///
3192 /// `REVIEW.md` §3.11 names four: two sequence numbers, two byte counts and a
3193 /// wall-clock limit. The pty's `seq` was fixed when the message was added
3194 /// and the other four were left, which is the interesting half of the
3195 /// finding: the fix has to be a rule about numbers rather than a patch on
3196 /// one message, or the next `u64` added to the wire arrives unguarded.
3197 ///
3198 /// The refusal is deliberate and is checked in both directions. Clamping
3199 /// would put back the shape of §1.11 -- a value quietly replaced by a
3200 /// plausible one, which is how a killed `cargo test` was read as a green
3201 /// build -- and there is no third answer that is not a lie to one end.
3202 #[test]
3203 fn every_wire_number_is_refused_past_what_javascript_can_hold() -> Outcome<()> {
3204 // Responses, as text with the number spliced in.
3205 let resps: [(&str, &str); 3] = [
3206 ("chunk seq", "{\"t\": \"chunk\", \"id\": \"r\", \"stream\": \"out\", \
3207 \"seq\": N, \"data\": \"\"}"),
3208 ("ended out_bytes", "{\"t\": \"ended\", \"id\": \"r\", \"exit\": 0, \
3209 \"timed_out\": false, \"killed\": false, \"out_bytes\": N, \
3210 \"err_bytes\": 0}"),
3211 ("ended err_bytes", "{\"t\": \"ended\", \"id\": \"r\", \"exit\": 0, \
3212 \"timed_out\": false, \"killed\": false, \"out_bytes\": 0, \
3213 \"err_bytes\": N}"),
3214 ];
3215 for (what, shape) in resps {
3216 // The boundary itself is carried, since it is the largest number the
3217 // page reads back unchanged.
3218 let ok = shape.replace("N", &fmt!("{}", SAFE_INT_MAX));
3219 res!(resp_of_json(&ok));
3220 for n in [SAFE_INT_MAX + 1, u64::MAX] {
3221 let txt = shape.replace("N", &fmt!("{}", n));
3222 match resp_of_json(&txt) {
3223 Ok(v) => return Err(err!(
3224 "{} accepted {}, reading back {:?}.", what, n, v;
3225 Test, Invalid)),
3226 Err(e) => assert_eq!(
3227 Some(Fault::WrongShape), Fault::of(&e), "{}: {}", what, e),
3228 }
3229 }
3230 }
3231
3232 // The same field on the way out. Nothing legitimate produces these, which
3233 // is why the check is here: it fires for a counter that went wrong.
3234 let out: [(&str, Resp); 3] = [
3235 ("chunk seq", Resp::Chunk {
3236 id: fmt!("r"),
3237 stream: Stream::Out,
3238 seq: SAFE_INT_MAX + 1,
3239 data: fmt!(""),
3240 }),
3241 ("ended out_bytes", Resp::Ended {
3242 id: fmt!("r"),
3243 exit: 0,
3244 timed_out: false,
3245 killed: false,
3246 out_bytes: u64::MAX,
3247 err_bytes: 0,
3248 }),
3249 ("ended err_bytes", Resp::Ended {
3250 id: fmt!("r"),
3251 exit: 0,
3252 timed_out: false,
3253 killed: false,
3254 out_bytes: 0,
3255 err_bytes: SAFE_INT_MAX + 1,
3256 }),
3257 ];
3258 for (what, resp) in out {
3259 match resp_json(&resp) {
3260 Ok(v) => return Err(err!(
3261 "{} was written as {}.", what, v; Test, Invalid)),
3262 Err(e) => assert_eq!(
3263 Some(Fault::WrongShape), Fault::of(&e), "{}: {}", what, e),
3264 }
3265 }
3266
3267 // And the one number that travels the other way.
3268 let exec = "{\"t\": \"exec\", \"id\": \"r\", \"argv\": [\"x\"], \"cwd\": \"/\", \
3269 \"env\": [], \"stdin\": null, \"timeout_ms\": N, \"capture\": \"both\", \
3270 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}";
3271 res!(req_of_json(&exec.replace("N", &fmt!("{}", SAFE_INT_MAX))));
3272 for n in [SAFE_INT_MAX + 1, u64::MAX] {
3273 let txt = exec.replace("N", &fmt!("{}", n));
3274 match req_of_json(&txt) {
3275 Ok(v) => return Err(err!(
3276 "timeout_ms accepted {}, reading back {:?}.", n, v; Test, Invalid)),
3277 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3278 }
3279 match req_json(&Req::Exec {
3280 id: fmt!("r"),
3281 argv: vec![fmt!("x")],
3282 cwd: fmt!("/"),
3283 env: Vec::new(),
3284 stdin: None,
3285 timeout_ms: n,
3286 capture: Capture::Both,
3287 fence: FenceSpec::default(),
3288 toolkits: Vec::new(),
3289 }) {
3290 Ok(v) => return Err(err!(
3291 "timeout_ms {} was written as {}.", n, v; Test, Invalid)),
3292 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3293 }
3294 }
3295
3296 // A negative number is not a count, a sequence or a limit, and the
3297 // unsigned read must refuse it rather than default it away: `-1` parsed
3298 // as `u64` and defaulted to zero is how a killed command reported success
3299 // (`REVIEW.md` §1.11).
3300 let negatives = [
3301 "{\"t\": \"chunk\", \"id\": \"r\", \"stream\": \"out\", \"seq\": -1, \"data\": \"\"}",
3302 "{\"t\": \"ended\", \"id\": \"r\", \"exit\": 0, \"timed_out\": false, \
3303 \"killed\": false, \"out_bytes\": -1, \"err_bytes\": 0}",
3304 ];
3305 for txt in negatives {
3306 match resp_of_json(txt) {
3307 Ok(v) => return Err(err!(
3308 "A negative number was read as {:?}.", v; Test, Invalid)),
3309 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3310 }
3311 }
3312 match req_of_json(&exec.replace("N", "-1")) {
3313 Ok(v) => return Err(err!(
3314 "A negative timeout was read as {:?}.", v; Test, Invalid)),
3315 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3316 }
3317 Ok(())
3318 }
3319
3320 /// A pty message with a field left out is refused, one field at a time.
3321 #[test]
3322 fn hostile_pty_missing_fields() -> Outcome<()> {
3323 let cases = [
3324 "{\"t\": \"open\", \"argv\": [\"sh\"], \"cwd\": \"/\", \"env\": [], \
3325 \"size\": {\"cols\": 80, \"rows\": 24}, \
3326 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}",
3327 "{\"t\": \"open\", \"id\": \"p\", \"cwd\": \"/\", \"env\": [], \
3328 \"size\": {\"cols\": 80, \"rows\": 24}, \
3329 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}",
3330 "{\"t\": \"open\", \"id\": \"p\", \"argv\": [\"sh\"], \"env\": [], \
3331 \"size\": {\"cols\": 80, \"rows\": 24}, \
3332 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}",
3333 "{\"t\": \"open\", \"id\": \"p\", \"argv\": [\"sh\"], \"cwd\": \"/\", \
3334 \"size\": {\"cols\": 80, \"rows\": 24}, \
3335 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}",
3336 "{\"t\": \"open\", \"id\": \"p\", \"argv\": [\"sh\"], \"cwd\": \"/\", \
3337 \"env\": [], \"size\": {\"cols\": 80, \"rows\": 24}}",
3338 "{\"t\": \"input\", \"data\": \"aGk=\"}",
3339 "{\"t\": \"resize\", \"size\": {\"cols\": 80, \"rows\": 24}}",
3340 ];
3341 for bad in cases {
3342 match req_of_json(bad) {
3343 Ok(v) => return Err(err!(
3344 "Expected a refusal for {:?}, got {:?}.", bad, v; Test, Invalid)),
3345 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", bad, e),
3346 }
3347 }
3348 let cases = [
3349 "{\"t\": \"opened\", \"id\": \"p\"}",
3350 "{\"t\": \"opened\", \"pid\": 1}",
3351 "{\"t\": \"output\", \"id\": \"p\", \"data\": \"\"}",
3352 "{\"t\": \"output\", \"id\": \"p\", \"seq\": 0}",
3353 "{\"t\": \"closed\", \"id\": \"p\", \"killed\": false}",
3354 "{\"t\": \"closed\", \"id\": \"p\", \"exit\": 0}",
3355 ];
3356 for bad in cases {
3357 match resp_of_json(bad) {
3358 Ok(v) => return Err(err!(
3359 "Expected a refusal for {:?}, got {:?}.", bad, v; Test, Invalid)),
3360 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}: {}", bad, e),
3361 }
3362 }
3363 Ok(())
3364 }
3365
3366 // ── Sizing an output frame ──────────────────────────────────────
3367
3368 /// A single output frame that would exceed the cap is refused, and splitting works.
3369 ///
3370 /// The payload is chosen so that a naive byte budget passes it: as raw bytes
3371 /// it is comfortably inside [`FRAME_MAX`], and it is the four-thirds of
3372 /// base64 that takes it over.
3373 #[test]
3374 fn output_over_the_cap_is_refused_then_split() -> Outcome<()> {
3375 let raw = vec![0x41u8; 900_000];
3376 assert!(raw.len() < FRAME_MAX,
3377 "the raw bytes must be inside the cap for this to prove anything");
3378 let one = output_msg("pty-1", 0, &raw);
3379 assert!(res!(resp_frame_len(Frame::NativeMessaging, &one)) > FRAME_MAX);
3380 assert!(!res!(resp_fits(Frame::NativeMessaging, &one)));
3381 let mut sink = Vec::new();
3382 match Frame::NativeMessaging.write_resp(&mut sink, &one) {
3383 Ok(()) => return Err(err!("The oversized frame was written."; Test, Invalid)),
3384 Err(e) => assert_eq!(Some(Fault::FrameTooBig), Fault::of(&e), "{}", e),
3385 }
3386 assert!(sink.is_empty());
3387
3388 // And the same bytes, split, all of which are sendable and which carry
3389 // the identical stream back.
3390 let frames = res!(output_frames(Frame::NativeMessaging, "pty-1", 0, &raw));
3391 assert!(frames.len() > 1);
3392 let mut got = Vec::new();
3393 for (i, f) in frames.iter().enumerate() {
3394 assert!(res!(resp_fits(Frame::NativeMessaging, f)), "frame {} does not fit", i);
3395 let mut buf = Vec::new();
3396 res!(Frame::NativeMessaging.write_resp(&mut buf, f));
3397 assert!(buf.len() <= FRAME_MAX);
3398 match f {
3399 Resp::Output { id, seq, data } => {
3400 assert_eq!("pty-1", id);
3401 assert_eq!(i as u64, *seq);
3402 got.extend_from_slice(&res!(data_decode(data)));
3403 },
3404 other => return Err(err!("Not an output frame: {:?}.", other; Test, Invalid)),
3405 }
3406 }
3407 assert_eq!(raw, got);
3408 Ok(())
3409 }
3410
3411 /// The fit is tight: one more byte would not fit in the frame it was cut for.
3412 ///
3413 /// A long session identifier is caller-supplied and part of the envelope, so
3414 /// this is the branch where [`FRAME_MAX`] binds rather than [`OUTPUT_MAX`] --
3415 /// which is the branch `REVIEW.md` §3.1 says the shipping path got wrong.
3416 #[test]
3417 fn output_fit_is_tight() -> Outcome<()> {
3418 let id = "i".repeat(900_000);
3419 let raw = vec![0x41u8; 200_000];
3420 let n = res!(output_fit(Frame::NativeMessaging, &id, 0, &raw));
3421 assert!(n > 0);
3422 assert!(n < OUTPUT_MAX, "the frame cap must be what binds, not the output cap");
3423 assert!(n < raw.len());
3424 assert!(res!(output_frame_len(
3425 Frame::NativeMessaging, &id, 0, &data_encode(&raw[..n]))) <= FRAME_MAX);
3426 // One byte more crosses a base64 quantum and does not fit.
3427 assert!(res!(output_frame_len(
3428 Frame::NativeMessaging, &id, 0, &data_encode(&raw[..n + 1]))) > FRAME_MAX);
3429 // And the cut lands on a whole quantum, so no frame ends mid-quantum.
3430 assert_eq!(0, n % 3);
3431 Ok(())
3432 }
3433
3434 /// A long, caller-supplied identifier does not produce an unsendable frame.
3435 ///
3436 /// This is `REVIEW.md` §3.1 in the pty's own terms: `id` comes from the page,
3437 /// is echoed on every frame, and a splitter that assumed a fixed envelope
3438 /// emitted frames the pipe refused and the output vanished. Every frame
3439 /// here is written for real, which is the only check that cannot be fooled
3440 /// by agreeing with the splitter's own arithmetic.
3441 #[test]
3442 fn output_frames_survive_a_long_identifier() -> Outcome<()> {
3443 let id = "i".repeat(900_000);
3444 let raw: Vec<u8> = (0..300_000u32).map(|i| (i % 251) as u8).collect();
3445 let frames = res!(output_frames(Frame::NativeMessaging, &id, 0, &raw));
3446 assert!(frames.len() > 3, "only {} frames", frames.len());
3447 let mut got = Vec::new();
3448 for (i, f) in frames.iter().enumerate() {
3449 let mut buf = Vec::new();
3450 res!(Frame::NativeMessaging.write_resp(&mut buf, f));
3451 assert!(buf.len() <= FRAME_MAX, "frame {} is {} bytes", i, buf.len());
3452 match f {
3453 Resp::Output { seq, data, .. } => {
3454 assert_eq!(i as u64, *seq);
3455 got.extend_from_slice(&res!(data_decode(data)));
3456 },
3457 other => return Err(err!("Not an output frame: {:?}.", other; Test, Invalid)),
3458 }
3459 }
3460 assert_eq!(raw, got);
3461 Ok(())
3462 }
3463
3464 /// An identifier so long that no output frame can carry anything is an error.
3465 #[test]
3466 fn output_fit_refuses_an_impossible_envelope() -> Outcome<()> {
3467 let id = "i".repeat(FRAME_MAX + 1);
3468 match output_fit(Frame::NativeMessaging, &id, 0, b"x") {
3469 Ok(v) => return Err(err!("Expected a refusal, got {}.", v; Test, Invalid)),
3470 Err(e) => assert_eq!(Some(Fault::FrameTooBig), Fault::of(&e), "{}", e),
3471 }
3472 match output_frames(Frame::NativeMessaging, &id, 0, b"x") {
3473 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3474 Err(e) => assert_eq!(Some(Fault::FrameTooBig), Fault::of(&e), "{}", e),
3475 }
3476 Ok(())
3477 }
3478
3479 /// Both caps hold, and nothing at all yields no frames rather than an empty one.
3480 #[test]
3481 fn output_frames_hold_both_caps() -> Outcome<()> {
3482 // A short identifier leaves room to spare, so OUTPUT_MAX binds.
3483 let raw = vec![0x41u8; OUTPUT_MAX * 2 + 5];
3484 let frames = res!(output_frames(Frame::NativeMessaging, "p", 0, &raw));
3485 assert_eq!(3, frames.len());
3486 let mut got = Vec::new();
3487 for (i, f) in frames.iter().enumerate() {
3488 match f {
3489 Resp::Output { seq, data, .. } => {
3490 assert_eq!(i as u64, *seq);
3491 let bytes = res!(data_decode(data));
3492 assert!(bytes.len() <= OUTPUT_MAX);
3493 got.extend_from_slice(&bytes);
3494 },
3495 other => return Err(err!("Not an output frame: {:?}.", other; Test, Invalid)),
3496 }
3497 }
3498 assert_eq!(raw, got);
3499
3500 // Nothing at all.
3501 assert!(res!(output_frames(Frame::NativeMessaging, "p", 0, &[])).is_empty());
3502 assert_eq!(0, res!(output_fit(Frame::NativeMessaging, "p", 0, &[])));
3503
3504 // A short run is one frame, unsplit, starting where it was told to.
3505 let frames = res!(output_frames(Frame::WebSocket, "p", 41, b"hello"));
3506 assert_eq!(vec![output_msg("p", 41, b"hello")], frames);
3507 Ok(())
3508 }
3509
3510 /// A run that would need a sequence past what the page can compare is refused.
3511 #[test]
3512 fn output_frames_refuse_a_sequence_overflow() -> Outcome<()> {
3513 // Starting past the ceiling.
3514 match output_frames(Frame::NativeMessaging, "p", SAFE_INT_MAX + 1, b"hi") {
3515 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3516 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3517 }
3518 // Reaching it part-way through: two frames' worth of bytes, one frame of
3519 // room left.
3520 let raw = vec![0x41u8; OUTPUT_MAX + 1];
3521 match output_frames(Frame::NativeMessaging, "p", SAFE_INT_MAX, &raw) {
3522 Ok(v) => return Err(err!(
3523 "Expected a refusal, got {} frames.", v.len(); Test, Invalid)),
3524 Err(e) => assert_eq!(Some(Fault::WrongShape), Fault::of(&e), "{}", e),
3525 }
3526 // One frame's worth at the ceiling is fine, since it needs no successor.
3527 let raw = vec![0x41u8; OUTPUT_MAX];
3528 assert_eq!(1, res!(output_frames(Frame::NativeMessaging, "p", SAFE_INT_MAX, &raw)).len());
3529 Ok(())
3530 }
3531
3532 // ── Strict JSON ─────────────────────────────────────────────────
3533
3534 /// The decoder accepts what `JSON.parse` accepts and nothing else.
3535 ///
3536 /// `REVIEW.md` §3.12 and §3.2. Each of these was accepted before, and each
3537 /// is rejected by the page, by `jq` and by any second reader of the frame.
3538 #[test]
3539 fn strict_json_refuses_what_the_page_would() -> Outcome<()> {
3540 let bad = [
3541 // JDAT's spellings, none of which is JSON.
3542 "{'t':'bye'}",
3543 "{t:\"bye\"}",
3544 "{\"t\": \"bye\",}",
3545 "#a comment#{\"t\": \"bye\"}",
3546 "{\"t\": \"bye\"} #trailing#",
3547 "(u8|1)",
3548 "{\"t\": \"hello\", \"proto\": (u32|1), \"client\": \"c\"}",
3549 // §3.2: a second message in the same frame, which was run silently.
3550 "{\"t\": \"bye\"}{\"t\": \"bye\"}",
3551 "{\"t\": \"bye\"} {\"t\": \"exec\"}",
3552 "{\"t\": \"bye\"}]",
3553 "{\"t\": \"bye\"}\u{0}",
3554 // Numbers JSON does not have.
3555 "{\"t\": \"hello\", \"proto\": 01, \"client\": \"c\"}",
3556 "{\"t\": \"hello\", \"proto\": +1, \"client\": \"c\"}",
3557 "{\"t\": \"hello\", \"proto\": 1., \"client\": \"c\"}",
3558 "{\"t\": \"hello\", \"proto\": .1, \"client\": \"c\"}",
3559 "{\"t\": \"hello\", \"proto\": 1e, \"client\": \"c\"}",
3560 // Strings JSON does not have.
3561 "{\"t\": \"by\\ze\"}",
3562 "{\"t\": \"by\\u00ze\"}",
3563 "{\"t\": \"by\te\"}",
3564 // Structure.
3565 "{\"t\" \"bye\"}",
3566 "{\"t\": }",
3567 "[1,]",
3568 "{,}",
3569 "truex",
3570 "tru",
3571 ];
3572 for txt in bad {
3573 match req_of_json(txt) {
3574 Ok(v) => return Err(err!(
3575 "Expected a refusal for {:?}, got {:?}.", txt, v; Test, Invalid)),
3576 Err(e) => {
3577 let f = Fault::of(&e);
3578 assert!(
3579 f == Some(Fault::NotJson) || f == Some(Fault::WrongShape),
3580 "{:?} gave {:?}: {}", txt, f, e);
3581 },
3582 }
3583 }
3584 // The tab is only illegal *inside* a string; between values it is
3585 // whitespace, and refusing it would refuse pretty-printed JSON.
3586 assert_eq!(Req::Bye, res!(req_of_json("\t\r\n {\n\t\"t\": \"bye\"\r\n}\n ")));
3587 // An escaped control byte is how a terminal's output legally travels.
3588 match res!(req_of_json("{\"t\": \"exec\", \"id\": \"a\", \"argv\": [\"x\"], \
3589 \"cwd\": \"/\", \"env\": [], \"stdin\": \"a\\u0001b\\tc\", \
3590 \"timeout_ms\": 1, \"capture\": \"both\", \
3591 \"fence\": {\"rw\": [], \"ro\": [], \"deny\": [], \"net\": false}}"))
3592 {
3593 Req::Exec { stdin, .. } => assert_eq!(Some(fmt!("a\u{1}b\tc")), stdin),
3594 other => return Err(err!("Read back {:?}.", other; Test, Invalid)),
3595 }
3596 Ok(())
3597 }
3598
3599 /// Deep nesting is refused by count rather than by running out of stack.
3600 #[test]
3601 fn strict_json_is_depth_bounded() -> Outcome<()> {
3602 let txt = fmt!("{}{}", "[".repeat(100_000), "]".repeat(100_000));
3603 match req_of_json(&txt) {
3604 Ok(v) => return Err(err!("Expected a refusal, got {:?}.", v; Test, Invalid)),
3605 Err(e) => assert_eq!(Some(Fault::NotJson), Fault::of(&e), "{}", e),
3606 }
3607 Ok(())
3608 }
3609}