1use std::collections::HashMap;
19
20use thiserror::Error;
21
22use crate::contract::{
23 BodyValue, CmpOp, CmpValue, EvidenceKind, ExtractSource, FieldKind, HttpMethod, InlinePart,
24 InlinePartBody, MatchPredicate, ObjectBody, QualifiedField, QualifiedMatch, Severity,
25};
26use crate::runtime::bytecode::{BytecodeProgram, Instr};
27use crate::runtime::spec::{CheckMetadata, HttpRequestSpec, ProbeKind, ProgramSpec};
28
29pub const MAGIC: &[u8; 4] = b"RUSO";
30pub const VERSION: u8 = 1;
31
32#[derive(Debug, Error)]
33pub enum BytecodeError {
34 #[error("bytecode too short")]
35 TooShort,
36 #[error("invalid magic")]
37 BadMagic,
38 #[error(
39 "unsupported bytecode version {found} (this build reads version {supported}); \
40 recompile the script or update ruso"
41 )]
42 BadVersion { found: u8, supported: u8 },
43 #[error("corrupt bytecode: {0}")]
44 Corrupt(&'static str),
45 #[error("invalid hex: {0}")]
46 InvalidHex(String),
47}
48
49pub fn encode(program: &BytecodeProgram) -> Vec<u8> {
50 let mut w = Writer::default();
51 w.bytes(MAGIC);
52 w.u8(VERSION);
53 write_metadata(&mut w, &program.spec.metadata);
54 write_probes(&mut w, &program.spec.probes);
55 write_strings(&mut w, &program.strings);
56 write_payloads(&mut w, &program.payloads);
57 write_matchers(&mut w, &program.matchers);
58 write_extracts(&mut w, &program.extracts);
59 write_evidence(&mut w, &program.evidence);
60 write_code(&mut w, &program.code);
61 w.0
62}
63
64pub fn decode(bytes: &[u8]) -> Result<BytecodeProgram, BytecodeError> {
65 let mut r = Reader::new(bytes);
66 r.consume_magic()?;
67 let version = r.u8()?;
68 if version != VERSION {
69 return Err(BytecodeError::BadVersion {
70 found: version,
71 supported: VERSION,
72 });
73 }
74 let metadata = read_metadata(&mut r)?;
75 let probes = read_probes(&mut r)?;
76 let strings = read_strings(&mut r)?;
77 let payloads = read_payloads(&mut r)?;
78 let matchers = read_matchers(&mut r)?;
79 let extracts = read_extracts(&mut r)?;
80 let evidence = read_evidence(&mut r)?;
81 let code = read_code(&mut r)?;
82 if r.remaining() != 0 {
83 return Err(BytecodeError::Corrupt("trailing bytes"));
84 }
85 let program = BytecodeProgram {
86 spec: ProgramSpec { probes, metadata },
87 code,
88 strings,
89 payloads,
90 matchers,
91 extracts,
92 evidence,
93 };
94 validate_program(&program)?;
95 Ok(program)
96}
97
98fn validate_program(p: &BytecodeProgram) -> Result<(), BytecodeError> {
115 let strings = p.strings.len();
116 let payloads = p.payloads.len();
117 let matchers = p.matchers.len();
118 let extracts = p.extracts.len();
119 let evidence = p.evidence.len();
120
121 let one = |idx: u32, bound: usize| -> Result<(), BytecodeError> {
124 if (idx as usize) < bound {
125 Ok(())
126 } else {
127 Err(BytecodeError::Corrupt("operand index out of range"))
128 }
129 };
130 let range = |start: u32, len: u16, bound: usize| -> Result<(), BytecodeError> {
131 let end = (start as usize)
132 .checked_add(len as usize)
133 .ok_or(BytecodeError::Corrupt("operand range overflow"))?;
134 if end <= bound {
135 Ok(())
136 } else {
137 Err(BytecodeError::Corrupt("operand range out of bounds"))
138 }
139 };
140
141 for instr in &p.code {
142 match instr {
143 Instr::Set { name, value } => {
144 one(*name, strings)?;
145 one(*value, strings)?;
146 }
147 Instr::SetList { name, start, len } => {
148 one(*name, strings)?;
149 range(*start, *len, strings)?;
150 }
151 Instr::Send { probe, payload } => {
152 one(*probe, strings)?;
153 if let Some(id) = payload {
154 one(*id, payloads)?;
155 }
156 }
157 Instr::Match(m) | Instr::Assert(m) => one(*m, matchers)?,
158 Instr::MatchAll { start, len } | Instr::MatchAny { start, len } => {
159 range(*start, *len, matchers)?;
160 }
161 Instr::Extract { name, source } => {
162 one(*name, strings)?;
163 one(*source, extracts)?;
164 }
165 Instr::IfMatch { matcher, .. } => one(*matcher, matchers)?,
166 Instr::ForList {
167 item, start, len, ..
168 } => {
169 one(*item, strings)?;
170 range(*start, *len, strings)?;
171 }
172 Instr::ForVar { item, list, .. } => {
173 one(*item, strings)?;
174 one(*list, strings)?;
175 }
176 Instr::Save { from, to } => {
177 one(*from, strings)?;
178 one(*to, strings)?;
179 }
180 Instr::Evidence(k) => one(*k, evidence)?,
181 Instr::Retry { probe, .. } => one(*probe, strings)?,
182 Instr::RetryDelay(v) | Instr::Sleep(v) => one(*v, strings)?,
183 Instr::LoopBack
185 | Instr::Break
186 | Instr::Stop
187 | Instr::Fail
188 | Instr::Continue
189 | Instr::Exit => {}
190 }
191 }
192 Ok(())
193}
194
195pub fn bytes_to_hex(bytes: &[u8]) -> String {
196 use std::fmt::Write as _;
197 let mut out = String::with_capacity(bytes.len() * 2);
198 for b in bytes {
199 let _ = write!(out, "{b:02x}");
200 }
201 out
202}
203
204pub fn bytes_to_hex_dump(bytes: &[u8]) -> String {
205 use std::fmt::Write as _;
206 let mut out = String::new();
207 for (offset, chunk) in bytes.chunks(16).enumerate() {
208 let off = offset * 16;
209 let _ = write!(out, "{off:08x}: ");
210 for b in chunk {
211 let _ = write!(out, "{b:02x} ");
212 }
213 for _ in chunk.len()..16 {
214 out.push_str(" ");
215 }
216 out.push_str(" |");
217 for &b in chunk {
218 if b.is_ascii_graphic() || b == b' ' {
219 out.push(b as char);
220 } else {
221 out.push('.');
222 }
223 }
224 out.push_str("|\n");
225 }
226 out
227}
228
229pub fn hex_to_bytes(input: &str) -> Result<Vec<u8>, BytecodeError> {
230 let mut compact = String::with_capacity(input.len());
231 for c in input.chars() {
232 if c.is_ascii_whitespace() {
233 continue;
234 }
235 if !c.is_ascii_hexdigit() {
236 return Err(BytecodeError::InvalidHex(format!("non-hex char: {c:?}")));
237 }
238 compact.push(c);
239 }
240 if !compact.len().is_multiple_of(2) {
241 return Err(BytecodeError::InvalidHex("odd length".into()));
242 }
243 let bytes = compact.as_bytes();
244 let mut out = Vec::with_capacity(bytes.len() / 2);
245 for chunk in bytes.chunks_exact(2) {
246 let pair = std::str::from_utf8(chunk).expect("ascii hex");
248 let byte = u8::from_str_radix(pair, 16)
249 .map_err(|err| BytecodeError::InvalidHex(err.to_string()))?;
250 out.push(byte);
251 }
252 Ok(out)
253}
254
255pub fn load_bytecode_input(input: &str) -> Result<Vec<u8>, BytecodeError> {
264 hex_to_bytes(input.trim())
265}
266
267#[derive(Default)]
268struct Writer(Vec<u8>);
269
270impl Writer {
271 fn u8(&mut self, v: u8) {
272 self.0.push(v);
273 }
274
275 fn u16(&mut self, v: u16) {
276 self.0.extend_from_slice(&v.to_le_bytes());
277 }
278
279 fn u32(&mut self, v: u32) {
280 self.0.extend_from_slice(&v.to_le_bytes());
281 }
282
283 fn u64(&mut self, v: u64) {
284 self.0.extend_from_slice(&v.to_le_bytes());
285 }
286
287 fn bytes(&mut self, data: &[u8]) {
288 self.0.extend_from_slice(data);
289 }
290
291 fn str(&mut self, s: &str) {
292 let b = s.as_bytes();
293 self.u32(b.len() as u32);
294 self.bytes(b);
295 }
296
297 fn opt_str(&mut self, value: &Option<String>) {
298 match value {
299 Some(s) => {
300 self.u8(1);
301 self.str(s);
302 }
303 None => self.u8(0),
304 }
305 }
306
307 fn opt_bytes(&mut self, value: &Option<Vec<u8>>) {
308 match value {
309 Some(data) => {
310 self.u8(1);
311 self.u32(data.len() as u32);
312 self.bytes(data);
313 }
314 None => self.u8(0),
315 }
316 }
317
318 fn opt_u16(&mut self, value: Option<u16>) {
319 match value {
320 Some(v) => {
321 self.u8(1);
322 self.u16(v);
323 }
324 None => self.u8(0),
325 }
326 }
327}
328
329struct Reader<'a> {
330 data: &'a [u8],
331 pos: usize,
332}
333
334impl<'a> Reader<'a> {
335 fn new(data: &'a [u8]) -> Self {
336 Self { data, pos: 0 }
337 }
338
339 fn remaining(&self) -> usize {
340 self.data.len().saturating_sub(self.pos)
341 }
342
343 fn consume_magic(&mut self) -> Result<(), BytecodeError> {
344 let magic = self.need(4)?;
345 if magic != MAGIC {
346 return Err(BytecodeError::BadMagic);
347 }
348 Ok(())
349 }
350
351 fn need(&mut self, n: usize) -> Result<&'a [u8], BytecodeError> {
352 if self.pos + n > self.data.len() {
353 return Err(BytecodeError::Corrupt("unexpected end"));
354 }
355 let slice = &self.data[self.pos..self.pos + n];
356 self.pos += n;
357 Ok(slice)
358 }
359
360 fn u8(&mut self) -> Result<u8, BytecodeError> {
361 Ok(self.need(1)?[0])
362 }
363
364 fn u16(&mut self) -> Result<u16, BytecodeError> {
365 Ok(u16::from_le_bytes(self.need(2)?.try_into().unwrap()))
366 }
367
368 fn u32(&mut self) -> Result<u32, BytecodeError> {
369 Ok(u32::from_le_bytes(self.need(4)?.try_into().unwrap()))
370 }
371
372 fn u64(&mut self) -> Result<u64, BytecodeError> {
373 Ok(u64::from_le_bytes(self.need(8)?.try_into().unwrap()))
374 }
375
376 fn str(&mut self) -> Result<String, BytecodeError> {
377 let len = self.u32()? as usize;
378 if len > self.remaining() {
380 return Err(BytecodeError::Corrupt("string length exceeds buffer"));
381 }
382 let bytes = self.need(len)?;
383 String::from_utf8(bytes.to_vec()).map_err(|_| BytecodeError::Corrupt("utf8"))
384 }
385
386 fn opt_str(&mut self) -> Result<Option<String>, BytecodeError> {
387 if self.u8()? == 0 {
388 Ok(None)
389 } else {
390 Ok(Some(self.str()?))
391 }
392 }
393
394 fn opt_bytes(&mut self) -> Result<Option<Vec<u8>>, BytecodeError> {
395 if self.u8()? == 0 {
396 Ok(None)
397 } else {
398 let len = self.u32()? as usize;
399 if len > self.remaining() {
400 return Err(BytecodeError::Corrupt("bytes length exceeds buffer"));
401 }
402 Ok(Some(self.need(len)?.to_vec()))
403 }
404 }
405
406 fn opt_u16(&mut self) -> Result<Option<u16>, BytecodeError> {
407 if self.u8()? == 0 {
408 Ok(None)
409 } else {
410 Ok(Some(self.u16()?))
411 }
412 }
413
414 fn bounded_count(&self, raw: u32) -> Result<usize, BytecodeError> {
424 let count = raw as usize;
425 if count > self.remaining() {
426 return Err(BytecodeError::Corrupt(
427 "list length exceeds remaining bytes",
428 ));
429 }
430 Ok(count)
431 }
432}
433
434fn write_metadata(w: &mut Writer, metadata: &CheckMetadata) {
435 w.opt_str(&metadata.name);
436 w.opt_str(&metadata.description);
437 w.opt_str(&metadata.impact);
438 match &metadata.severity {
439 Some(s) => {
440 w.u8(1);
441 w.u8(severity_tag(s));
442 }
443 None => w.u8(0),
444 }
445 w.opt_str(&metadata.author);
446 write_strings(w, &metadata.cve);
447 write_strings(w, &metadata.cwe);
448 write_strings(w, &metadata.references);
449 write_strings(w, &metadata.cvss);
450 write_strings(w, &metadata.cvss_score);
451 w.opt_str(&metadata.mitigation);
452 write_strings(w, &metadata.tags);
453 w.opt_str(&metadata.version);
454 w.opt_str(&metadata.family);
457}
458
459fn read_metadata(r: &mut Reader<'_>) -> Result<CheckMetadata, BytecodeError> {
460 Ok(CheckMetadata {
461 name: r.opt_str()?,
462 description: r.opt_str()?,
463 impact: r.opt_str()?,
464 severity: if r.u8()? == 0 {
465 None
466 } else {
467 Some(read_severity(r)?)
468 },
469 author: r.opt_str()?,
470 cve: read_strings(r)?,
471 cwe: read_strings(r)?,
472 references: read_strings(r)?,
473 cvss: read_strings(r)?,
474 cvss_score: read_strings(r)?,
475 mitigation: r.opt_str()?,
476 tags: read_strings(r)?,
477 version: r.opt_str()?,
478 family: r.opt_str()?,
479 })
480}
481
482fn severity_tag(s: &Severity) -> u8 {
483 match s {
484 Severity::Low => 0,
485 Severity::Medium => 1,
486 Severity::High => 2,
487 Severity::Critical => 3,
488 Severity::Info => 4,
489 }
490}
491
492fn read_severity(r: &mut Reader<'_>) -> Result<Severity, BytecodeError> {
493 Ok(match r.u8()? {
494 0 => Severity::Low,
495 1 => Severity::Medium,
496 2 => Severity::High,
497 3 => Severity::Critical,
498 4 => Severity::Info,
499 _ => return Err(BytecodeError::Corrupt("severity")),
500 })
501}
502
503fn write_probes(w: &mut Writer, probes: &HashMap<String, ProbeKind>) {
504 let mut names: Vec<_> = probes.keys().cloned().collect();
505 names.sort();
506 w.u32(names.len() as u32);
507 for name in names {
508 w.str(&name);
509 write_probe_kind(w, probes.get(&name).expect("sorted key"));
510 }
511}
512
513fn read_probes(r: &mut Reader<'_>) -> Result<HashMap<String, ProbeKind>, BytecodeError> {
514 let raw = r.u32()?;
515 let count = r.bounded_count(raw)?;
516 let mut probes = HashMap::with_capacity(count);
517 for _ in 0..count {
518 let name = r.str()?;
519 let kind = read_probe_kind(r)?;
520 probes.insert(name, kind);
521 }
522 Ok(probes)
523}
524
525fn write_probe_kind(w: &mut Writer, kind: &ProbeKind) {
526 match kind {
527 ProbeKind::Http(spec) => {
528 w.u8(0);
529 write_http_spec(w, spec);
530 }
531 ProbeKind::Dns(spec) => {
532 w.u8(1);
533 write_socket_probe(w, spec);
534 }
535 ProbeKind::Tcp(spec) => {
536 w.u8(2);
537 write_socket_probe(w, spec);
538 }
539 ProbeKind::Udp(spec) => {
540 w.u8(3);
541 write_socket_probe(w, spec);
542 }
543 }
544}
545
546fn write_socket_probe(w: &mut Writer, spec: &crate::runtime::spec::SocketProbeSpec) {
547 w.str(&spec.host);
548 w.opt_u16(spec.port);
549 w.opt_bytes(&spec.payload);
550 w.u8(u8::from(spec.tls));
551 w.u8(u8::from(spec.session));
552 w.u32(spec.read_max);
553 w.u32(spec.read_idle_ms);
554}
555
556fn read_socket_probe(
557 r: &mut Reader<'_>,
558) -> Result<crate::runtime::spec::SocketProbeSpec, BytecodeError> {
559 Ok(crate::runtime::spec::SocketProbeSpec {
560 host: r.str()?,
561 port: r.opt_u16()?,
562 payload: r.opt_bytes()?,
563 tls: r.u8()? != 0,
564 session: r.u8()? != 0,
565 read_max: r.u32()?,
566 read_idle_ms: r.u32()?,
567 })
568}
569
570fn read_probe_kind(r: &mut Reader<'_>) -> Result<ProbeKind, BytecodeError> {
571 Ok(match r.u8()? {
572 0 => ProbeKind::Http(read_http_spec(r)?),
573 1 => ProbeKind::Dns(read_socket_probe(r)?),
574 2 => ProbeKind::Tcp(read_socket_probe(r)?),
575 3 => ProbeKind::Udp(read_socket_probe(r)?),
576 _ => return Err(BytecodeError::Corrupt("probe kind")),
577 })
578}
579
580fn write_http_spec(w: &mut Writer, spec: &HttpRequestSpec) {
581 w.u8(http_method_tag(&spec.method));
582 w.str(&spec.path);
583 w.opt_str(&spec.timeout);
584 write_opt_bool(w, &spec.follow_redirect);
585 write_opt_bool(w, &spec.verify_ssl);
586 w.opt_str(&spec.proxy);
587 w.opt_str(&spec.user_agent);
588 write_header_list(w, &spec.headers);
589 write_header_list(w, &spec.cookies);
590 write_header_list(w, &spec.queries);
591 write_opt_object(w, &spec.data_body);
592 write_opt_object(w, &spec.json_body);
593 w.opt_str(&spec.raw_body);
594 w.opt_str(&spec.body_bytes);
595 write_opt_object(w, &spec.multipart_body);
596}
597
598fn read_http_spec(r: &mut Reader<'_>) -> Result<HttpRequestSpec, BytecodeError> {
599 Ok(HttpRequestSpec {
600 method: read_http_method(r)?,
601 path: r.str()?,
602 timeout: r.opt_str()?,
603 follow_redirect: read_opt_bool(r)?,
604 verify_ssl: read_opt_bool(r)?,
605 proxy: r.opt_str()?,
606 user_agent: r.opt_str()?,
607 headers: read_header_list(r)?,
608 cookies: read_header_list(r)?,
609 queries: read_header_list(r)?,
610 data_body: read_opt_object(r)?,
611 json_body: read_opt_object(r)?,
612 raw_body: r.opt_str()?,
613 body_bytes: r.opt_str()?,
614 multipart_body: read_opt_object(r)?,
615 })
616}
617
618fn http_method_tag(m: &HttpMethod) -> u8 {
619 match m {
620 HttpMethod::Get => 0,
621 HttpMethod::Post => 1,
622 HttpMethod::Put => 2,
623 HttpMethod::Patch => 3,
624 HttpMethod::Delete => 4,
625 HttpMethod::Head => 5,
626 HttpMethod::Options => 6,
627 }
628}
629
630fn read_http_method(r: &mut Reader<'_>) -> Result<HttpMethod, BytecodeError> {
631 Ok(match r.u8()? {
632 0 => HttpMethod::Get,
633 1 => HttpMethod::Post,
634 2 => HttpMethod::Put,
635 3 => HttpMethod::Patch,
636 4 => HttpMethod::Delete,
637 5 => HttpMethod::Head,
638 6 => HttpMethod::Options,
639 _ => return Err(BytecodeError::Corrupt("http method")),
640 })
641}
642
643fn write_opt_bool(w: &mut Writer, value: &Option<bool>) {
644 match value {
645 Some(v) => {
646 w.u8(1);
647 w.u8(u8::from(*v));
648 }
649 None => w.u8(0),
650 }
651}
652
653fn read_opt_bool(r: &mut Reader<'_>) -> Result<Option<bool>, BytecodeError> {
654 if r.u8()? == 0 {
655 Ok(None)
656 } else {
657 Ok(Some(r.u8()? != 0))
658 }
659}
660
661fn write_header_list(w: &mut Writer, pairs: &[(String, String)]) {
662 w.u32(pairs.len() as u32);
663 for (k, v) in pairs {
664 w.str(k);
665 w.str(v);
666 }
667}
668
669fn read_header_list(r: &mut Reader<'_>) -> Result<Vec<(String, String)>, BytecodeError> {
670 let raw = r.u32()?;
671 let count = r.bounded_count(raw)?;
672 let mut pairs = Vec::with_capacity(count);
673 for _ in 0..count {
674 pairs.push((r.str()?, r.str()?));
675 }
676 Ok(pairs)
677}
678
679fn write_opt_object(w: &mut Writer, body: &Option<ObjectBody>) {
680 match body {
681 Some(obj) => {
682 w.u8(1);
683 write_object(w, obj);
684 }
685 None => w.u8(0),
686 }
687}
688
689fn read_opt_object(r: &mut Reader<'_>) -> Result<Option<ObjectBody>, BytecodeError> {
690 if r.u8()? == 0 {
691 Ok(None)
692 } else {
693 Ok(Some(read_object(r)?))
694 }
695}
696
697fn write_object(w: &mut Writer, obj: &ObjectBody) {
698 w.u32(obj.pairs.len() as u32);
699 for (key, value) in &obj.pairs {
700 w.str(key);
701 write_body_value(w, value);
702 }
703}
704
705fn read_object(r: &mut Reader<'_>) -> Result<ObjectBody, BytecodeError> {
706 let raw = r.u32()?;
707 let count = r.bounded_count(raw)?;
708 let mut pairs = Vec::with_capacity(count);
709 for _ in 0..count {
710 pairs.push((r.str()?, read_body_value(r)?));
711 }
712 Ok(ObjectBody { pairs })
713}
714
715fn write_body_value(w: &mut Writer, value: &BodyValue) {
716 match value {
717 BodyValue::String(s) => {
718 w.u8(0);
719 w.str(s);
720 }
721 BodyValue::Interpolation(s) => {
722 w.u8(1);
723 w.str(s);
724 }
725 BodyValue::Object(obj) => {
726 w.u8(2);
727 write_object(w, obj);
728 }
729 BodyValue::Bytes(hex) => {
730 w.u8(3);
731 w.str(hex);
732 }
733 BodyValue::Part(part) => {
734 w.u8(4);
735 w.opt_str(&part.filename);
736 match &part.body {
737 InlinePartBody::Text(t) => {
738 w.u8(0);
739 w.str(t);
740 }
741 InlinePartBody::Bytes(b) => {
742 w.u8(1);
743 w.str(b);
744 }
745 }
746 }
747 }
748}
749
750fn read_body_value(r: &mut Reader<'_>) -> Result<BodyValue, BytecodeError> {
751 Ok(match r.u8()? {
752 0 => BodyValue::String(r.str()?),
753 1 => BodyValue::Interpolation(r.str()?),
754 2 => BodyValue::Object(read_object(r)?),
755 3 => BodyValue::Bytes(r.str()?),
756 4 => BodyValue::Part(InlinePart {
757 filename: r.opt_str()?,
758 body: match r.u8()? {
759 1 => InlinePartBody::Bytes(r.str()?),
760 _ => InlinePartBody::Text(r.str()?),
761 },
762 }),
763 _ => return Err(BytecodeError::Corrupt("body value")),
764 })
765}
766
767fn write_strings(w: &mut Writer, strings: &[String]) {
768 w.u32(strings.len() as u32);
769 for s in strings {
770 w.str(s);
771 }
772}
773
774fn read_strings(r: &mut Reader<'_>) -> Result<Vec<String>, BytecodeError> {
775 let raw = r.u32()?;
776 let count = r.bounded_count(raw)?;
777 let mut strings = Vec::with_capacity(count);
778 for _ in 0..count {
779 strings.push(r.str()?);
780 }
781 Ok(strings)
782}
783
784fn write_payloads(w: &mut Writer, payloads: &[Vec<u8>]) {
785 w.u32(payloads.len() as u32);
786 for data in payloads {
787 w.u32(data.len() as u32);
788 w.bytes(data);
789 }
790}
791
792fn read_payloads(r: &mut Reader<'_>) -> Result<Vec<Vec<u8>>, BytecodeError> {
793 let raw = r.u32()?;
794 let count = r.bounded_count(raw)?;
795 let mut payloads = Vec::with_capacity(count);
796 for _ in 0..count {
797 let len = r.u32()? as usize;
798 if len > r.remaining() {
799 return Err(BytecodeError::Corrupt("payload length exceeds buffer"));
800 }
801 payloads.push(r.need(len)?.to_vec());
802 }
803 Ok(payloads)
804}
805
806fn write_matchers(w: &mut Writer, matchers: &[QualifiedMatch]) {
807 w.u32(matchers.len() as u32);
808 for m in matchers {
809 write_matcher(w, m);
810 }
811}
812
813fn read_matchers(r: &mut Reader<'_>) -> Result<Vec<QualifiedMatch>, BytecodeError> {
814 let raw = r.u32()?;
815 let count = r.bounded_count(raw)?;
816 let mut matchers = Vec::with_capacity(count);
817 for _ in 0..count {
818 matchers.push(read_matcher(r)?);
819 }
820 Ok(matchers)
821}
822
823fn write_matcher(w: &mut Writer, m: &QualifiedMatch) {
824 w.str(&m.field.target);
825 write_field_kind(w, &m.field.kind);
826 write_predicate(w, &m.predicate);
827}
828
829fn read_matcher(r: &mut Reader<'_>) -> Result<QualifiedMatch, BytecodeError> {
830 Ok(QualifiedMatch {
831 field: QualifiedField {
832 target: r.str()?,
833 kind: read_field_kind(r)?,
834 },
835 predicate: read_predicate(r)?,
836 })
837}
838
839fn write_field_kind(w: &mut Writer, kind: &FieldKind) {
840 match kind {
841 FieldKind::Status => w.u8(0),
842 FieldKind::Body => w.u8(1),
843 FieldKind::Header(name) => {
844 w.u8(2);
845 w.str(name);
846 }
847 FieldKind::ResponseTime => w.u8(3),
848 FieldKind::ResponseSize => w.u8(4),
849 FieldKind::Answer => w.u8(5),
850 FieldKind::Banner => w.u8(6),
851 FieldKind::Response => w.u8(7),
852 }
853}
854
855fn read_field_kind(r: &mut Reader<'_>) -> Result<FieldKind, BytecodeError> {
856 Ok(match r.u8()? {
857 0 => FieldKind::Status,
858 1 => FieldKind::Body,
859 2 => FieldKind::Header(r.str()?),
860 3 => FieldKind::ResponseTime,
861 4 => FieldKind::ResponseSize,
862 5 => FieldKind::Answer,
863 6 => FieldKind::Banner,
864 7 => FieldKind::Response,
865 _ => return Err(BytecodeError::Corrupt("field kind")),
866 })
867}
868
869fn write_predicate(w: &mut Writer, p: &MatchPredicate) {
870 match p {
871 MatchPredicate::Compare { op, value } => {
872 w.u8(0);
873 w.u8(cmp_op_tag(*op));
874 write_cmp_value(w, value);
875 }
876 MatchPredicate::Contains(s) => {
877 w.u8(1);
878 w.str(s);
879 }
880 MatchPredicate::NotContains(s) => {
881 w.u8(2);
882 w.str(s);
883 }
884 MatchPredicate::Regex(s) => {
885 w.u8(3);
886 w.str(s);
887 }
888 }
889}
890
891fn read_predicate(r: &mut Reader<'_>) -> Result<MatchPredicate, BytecodeError> {
892 Ok(match r.u8()? {
893 0 => MatchPredicate::Compare {
894 op: read_cmp_op(r)?,
895 value: read_cmp_value(r)?,
896 },
897 1 => MatchPredicate::Contains(r.str()?),
898 2 => MatchPredicate::NotContains(r.str()?),
899 3 => MatchPredicate::Regex(r.str()?),
900 _ => return Err(BytecodeError::Corrupt("predicate")),
901 })
902}
903
904fn cmp_op_tag(op: CmpOp) -> u8 {
905 match op {
906 CmpOp::Eq => 0,
907 CmpOp::Ne => 1,
908 CmpOp::Lt => 2,
909 CmpOp::Gt => 3,
910 CmpOp::Le => 4,
911 CmpOp::Ge => 5,
912 }
913}
914
915fn read_cmp_op(r: &mut Reader<'_>) -> Result<CmpOp, BytecodeError> {
916 Ok(match r.u8()? {
917 0 => CmpOp::Eq,
918 1 => CmpOp::Ne,
919 2 => CmpOp::Lt,
920 3 => CmpOp::Gt,
921 4 => CmpOp::Le,
922 5 => CmpOp::Ge,
923 _ => return Err(BytecodeError::Corrupt("cmp op")),
924 })
925}
926
927fn write_cmp_value(w: &mut Writer, value: &CmpValue) {
928 match value {
929 CmpValue::Number(n) => {
930 w.u8(0);
931 w.u64(*n);
935 }
936 CmpValue::String(s) => {
937 w.u8(1);
938 w.str(s);
939 }
940 CmpValue::Duration(d) => {
941 w.u8(2);
942 w.str(d);
943 }
944 }
945}
946
947fn read_cmp_value(r: &mut Reader<'_>) -> Result<CmpValue, BytecodeError> {
948 Ok(match r.u8()? {
949 0 => CmpValue::Number(r.u64()?),
950 1 => CmpValue::String(r.str()?),
951 2 => CmpValue::Duration(r.str()?),
952 _ => return Err(BytecodeError::Corrupt("cmp value")),
953 })
954}
955
956fn write_extracts(w: &mut Writer, extracts: &[ExtractSource]) {
957 w.u32(extracts.len() as u32);
958 for e in extracts {
959 write_extract(w, e);
960 }
961}
962
963fn read_extracts(r: &mut Reader<'_>) -> Result<Vec<ExtractSource>, BytecodeError> {
964 let raw = r.u32()?;
965 let count = r.bounded_count(raw)?;
966 let mut extracts = Vec::with_capacity(count);
967 for _ in 0..count {
968 extracts.push(read_extract(r)?);
969 }
970 Ok(extracts)
971}
972
973fn write_extract(w: &mut Writer, e: &ExtractSource) {
974 match e {
975 ExtractSource::Body { target, regex } => {
976 w.u8(0);
977 w.str(target);
978 w.opt_str(regex);
979 }
980 ExtractSource::Header { target, name } => {
981 w.u8(1);
982 w.str(target);
983 w.str(name);
984 }
985 }
986}
987
988fn read_extract(r: &mut Reader<'_>) -> Result<ExtractSource, BytecodeError> {
989 Ok(match r.u8()? {
990 0 => ExtractSource::Body {
991 target: r.str()?,
992 regex: r.opt_str()?,
993 },
994 1 => ExtractSource::Header {
995 target: r.str()?,
996 name: r.str()?,
997 },
998 _ => return Err(BytecodeError::Corrupt("extract")),
999 })
1000}
1001
1002fn write_evidence(w: &mut Writer, kinds: &[EvidenceKind]) {
1003 w.u32(kinds.len() as u32);
1004 for k in kinds {
1005 write_evidence_kind(w, k);
1006 }
1007}
1008
1009fn read_evidence(r: &mut Reader<'_>) -> Result<Vec<EvidenceKind>, BytecodeError> {
1010 let raw = r.u32()?;
1011 let count = r.bounded_count(raw)?;
1012 let mut kinds = Vec::with_capacity(count);
1013 for _ in 0..count {
1014 kinds.push(read_evidence_kind(r)?);
1015 }
1016 Ok(kinds)
1017}
1018
1019fn write_evidence_kind(w: &mut Writer, k: &EvidenceKind) {
1020 match k {
1021 EvidenceKind::Body { target, pattern } => {
1022 w.u8(0);
1023 w.str(target);
1024 w.opt_str(pattern);
1025 }
1026 EvidenceKind::Response { target, pattern } => {
1027 w.u8(1);
1028 w.str(target);
1029 w.opt_str(pattern);
1030 }
1031 EvidenceKind::Header {
1032 target,
1033 name,
1034 pattern,
1035 } => {
1036 w.u8(2);
1037 w.str(target);
1038 w.str(name);
1039 w.opt_str(pattern);
1040 }
1041 }
1042}
1043
1044fn read_evidence_kind(r: &mut Reader<'_>) -> Result<EvidenceKind, BytecodeError> {
1045 Ok(match r.u8()? {
1046 0 => EvidenceKind::Body {
1047 target: r.str()?,
1048 pattern: r.opt_str()?,
1049 },
1050 1 => EvidenceKind::Response {
1051 target: r.str()?,
1052 pattern: r.opt_str()?,
1053 },
1054 2 => EvidenceKind::Header {
1055 target: r.str()?,
1056 name: r.str()?,
1057 pattern: r.opt_str()?,
1058 },
1059 _ => return Err(BytecodeError::Corrupt("evidence")),
1060 })
1061}
1062
1063const OP_SET: u8 = 1;
1064const OP_SEND: u8 = 2;
1065const OP_MATCH: u8 = 3;
1066const OP_MATCH_ALL: u8 = 4;
1067const OP_MATCH_ANY: u8 = 5;
1068const OP_ASSERT: u8 = 6;
1069const OP_EXTRACT: u8 = 7;
1070const OP_IF_MATCH: u8 = 8;
1071const OP_SAVE: u8 = 9;
1072const OP_EVIDENCE: u8 = 10;
1073const OP_RETRY: u8 = 11;
1074const OP_RETRY_DELAY: u8 = 12;
1075const OP_SLEEP: u8 = 13;
1076const OP_STOP: u8 = 14;
1077const OP_FAIL: u8 = 15;
1078const OP_CONTINUE: u8 = 16;
1079const OP_EXIT: u8 = 17;
1080const OP_LOOP_BACK: u8 = 19;
1083const OP_BREAK: u8 = 20;
1084const OP_SET_LIST: u8 = 21;
1085const OP_FOR_LIST: u8 = 22;
1086const OP_FOR_VAR: u8 = 23;
1087
1088fn write_code(w: &mut Writer, code: &[Instr]) {
1089 w.u32(code.len() as u32);
1090 for instr in code {
1091 write_instr(w, instr);
1092 }
1093}
1094
1095fn read_code(r: &mut Reader<'_>) -> Result<Vec<Instr>, BytecodeError> {
1096 let raw = r.u32()?;
1097 let count = r.bounded_count(raw)?;
1098 let mut code = Vec::with_capacity(count);
1099 for _ in 0..count {
1100 code.push(read_instr(r)?);
1101 }
1102 Ok(code)
1103}
1104
1105fn write_instr(w: &mut Writer, instr: &Instr) {
1106 match instr {
1107 Instr::Set { name, value } => {
1108 w.u8(OP_SET);
1109 w.u32(*name);
1110 w.u32(*value);
1111 }
1112 Instr::SetList { name, start, len } => {
1113 w.u8(OP_SET_LIST);
1114 w.u32(*name);
1115 w.u32(*start);
1116 w.u16(*len);
1117 }
1118 Instr::Send { probe, payload } => {
1119 w.u8(OP_SEND);
1120 w.u32(*probe);
1121 match payload {
1122 Some(id) => {
1123 w.u8(1);
1124 w.u32(*id);
1125 }
1126 None => w.u8(0),
1127 }
1128 }
1129 Instr::Match(matcher) => {
1130 w.u8(OP_MATCH);
1131 w.u32(*matcher);
1132 }
1133 Instr::MatchAll { start, len } => {
1134 w.u8(OP_MATCH_ALL);
1135 w.u32(*start);
1136 w.u16(*len);
1137 }
1138 Instr::MatchAny { start, len } => {
1139 w.u8(OP_MATCH_ANY);
1140 w.u32(*start);
1141 w.u16(*len);
1142 }
1143 Instr::Assert(matcher) => {
1144 w.u8(OP_ASSERT);
1145 w.u32(*matcher);
1146 }
1147 Instr::Extract { name, source } => {
1148 w.u8(OP_EXTRACT);
1149 w.u32(*name);
1150 w.u32(*source);
1151 }
1152 Instr::IfMatch { matcher, else_pc } => {
1153 w.u8(OP_IF_MATCH);
1154 w.u32(*matcher);
1155 w.u32(*else_pc);
1156 }
1157 Instr::ForList {
1158 item,
1159 start,
1160 len,
1161 end_pc,
1162 } => {
1163 w.u8(OP_FOR_LIST);
1164 w.u32(*item);
1165 w.u32(*start);
1166 w.u16(*len);
1167 w.u32(*end_pc);
1168 }
1169 Instr::ForVar { item, list, end_pc } => {
1170 w.u8(OP_FOR_VAR);
1171 w.u32(*item);
1172 w.u32(*list);
1173 w.u32(*end_pc);
1174 }
1175 Instr::LoopBack => w.u8(OP_LOOP_BACK),
1176 Instr::Break => w.u8(OP_BREAK),
1177 Instr::Save { from, to } => {
1178 w.u8(OP_SAVE);
1179 w.u32(*from);
1180 w.u32(*to);
1181 }
1182 Instr::Evidence(kind) => {
1183 w.u8(OP_EVIDENCE);
1184 w.u32(*kind);
1185 }
1186 Instr::Retry { probe, count } => {
1187 w.u8(OP_RETRY);
1188 w.u32(*probe);
1189 w.u32(*count);
1190 }
1191 Instr::RetryDelay(value) => {
1192 w.u8(OP_RETRY_DELAY);
1193 w.u32(*value);
1194 }
1195 Instr::Sleep(value) => {
1196 w.u8(OP_SLEEP);
1197 w.u32(*value);
1198 }
1199 Instr::Stop => w.u8(OP_STOP),
1200 Instr::Fail => w.u8(OP_FAIL),
1201 Instr::Continue => w.u8(OP_CONTINUE),
1202 Instr::Exit => w.u8(OP_EXIT),
1203 }
1204}
1205
1206fn read_instr(r: &mut Reader<'_>) -> Result<Instr, BytecodeError> {
1207 Ok(match r.u8()? {
1208 OP_SET => Instr::Set {
1209 name: r.u32()?,
1210 value: r.u32()?,
1211 },
1212 OP_SET_LIST => Instr::SetList {
1213 name: r.u32()?,
1214 start: r.u32()?,
1215 len: r.u16()?,
1216 },
1217 OP_SEND => {
1218 let probe = r.u32()?;
1219 let payload = if r.u8()? == 0 { None } else { Some(r.u32()?) };
1220 Instr::Send { probe, payload }
1221 }
1222 OP_MATCH => Instr::Match(r.u32()?),
1223 OP_MATCH_ALL => Instr::MatchAll {
1224 start: r.u32()?,
1225 len: r.u16()?,
1226 },
1227 OP_MATCH_ANY => Instr::MatchAny {
1228 start: r.u32()?,
1229 len: r.u16()?,
1230 },
1231 OP_ASSERT => Instr::Assert(r.u32()?),
1232 OP_EXTRACT => Instr::Extract {
1233 name: r.u32()?,
1234 source: r.u32()?,
1235 },
1236 OP_IF_MATCH => Instr::IfMatch {
1237 matcher: r.u32()?,
1238 else_pc: r.u32()?,
1239 },
1240 OP_FOR_LIST => Instr::ForList {
1241 item: r.u32()?,
1242 start: r.u32()?,
1243 len: r.u16()?,
1244 end_pc: r.u32()?,
1245 },
1246 OP_FOR_VAR => Instr::ForVar {
1247 item: r.u32()?,
1248 list: r.u32()?,
1249 end_pc: r.u32()?,
1250 },
1251 OP_LOOP_BACK => Instr::LoopBack,
1252 OP_BREAK => Instr::Break,
1253 OP_SAVE => Instr::Save {
1254 from: r.u32()?,
1255 to: r.u32()?,
1256 },
1257 OP_EVIDENCE => Instr::Evidence(r.u32()?),
1258 OP_RETRY => Instr::Retry {
1259 probe: r.u32()?,
1260 count: r.u32()?,
1261 },
1262 OP_RETRY_DELAY => Instr::RetryDelay(r.u32()?),
1263 OP_SLEEP => Instr::Sleep(r.u32()?),
1264 OP_STOP => Instr::Stop,
1265 OP_FAIL => Instr::Fail,
1266 OP_CONTINUE => Instr::Continue,
1267 OP_EXIT => Instr::Exit,
1268 _ => return Err(BytecodeError::Corrupt("opcode")),
1269 })
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275
1276 #[test]
1277 fn hex_roundtrip() {
1278 let bytes = vec![0x52, 0x55, 0x53, 0x4f, 0x01, 0xff];
1279 let hex = bytes_to_hex(&bytes);
1280 assert_eq!(hex_to_bytes(&hex).unwrap(), bytes);
1281 }
1282
1283 #[test]
1284 fn hex_rejects_non_hex_chars() {
1285 match hex_to_bytes("zz") {
1286 Err(BytecodeError::InvalidHex(_)) => {}
1287 other => panic!("expected InvalidHex, got {other:?}"),
1288 }
1289 }
1290
1291 #[test]
1292 fn read_severity_rejects_unknown_byte() {
1293 let mut r = Reader::new(&[0x99]);
1294 match read_severity(&mut r) {
1295 Err(BytecodeError::Corrupt("severity")) => {}
1296 other => panic!("expected Corrupt(severity), got {other:?}"),
1297 }
1298 }
1299
1300 #[test]
1301 fn read_severity_accepts_known_bytes() {
1302 for (byte, expected) in [
1303 (0u8, Severity::Low),
1304 (1, Severity::Medium),
1305 (2, Severity::High),
1306 (3, Severity::Critical),
1307 (4, Severity::Info),
1308 ] {
1309 let data = [byte];
1310 let mut r = Reader::new(&data);
1311 assert_eq!(read_severity(&mut r).unwrap(), expected);
1312 }
1313 }
1314
1315 #[test]
1316 fn read_http_method_rejects_unknown_byte() {
1317 let mut r = Reader::new(&[0xff]);
1318 assert!(matches!(
1319 read_http_method(&mut r),
1320 Err(BytecodeError::Corrupt("http method"))
1321 ));
1322 }
1323
1324 #[test]
1325 fn read_http_method_accepts_head_and_options() {
1326 for (byte, expected) in [(5u8, HttpMethod::Head), (6, HttpMethod::Options)] {
1327 let data = [byte];
1328 let mut r = Reader::new(&data);
1329 assert_eq!(read_http_method(&mut r).unwrap(), expected);
1330 }
1331 }
1332
1333 #[test]
1334 fn read_cmp_op_rejects_unknown_byte() {
1335 let mut r = Reader::new(&[0xfe]);
1336 assert!(matches!(
1337 read_cmp_op(&mut r),
1338 Err(BytecodeError::Corrupt("cmp op"))
1339 ));
1340 }
1341
1342 #[test]
1343 fn read_cmp_value_rejects_unknown_byte() {
1344 let mut r = Reader::new(&[0x77]);
1345 assert!(matches!(
1346 read_cmp_value(&mut r),
1347 Err(BytecodeError::Corrupt("cmp value"))
1348 ));
1349 }
1350
1351 #[test]
1352 fn cmp_number_roundtrips_full_u64() {
1353 let mut w = Writer::default();
1357 let value = CmpValue::Number(u64::MAX - 5);
1358 write_cmp_value(&mut w, &value);
1359 let mut r = Reader::new(&w.0);
1360 assert_eq!(read_cmp_value(&mut r).unwrap(), value);
1361 }
1362
1363 #[test]
1364 fn bounded_count_rejects_oversized_count() {
1365 let mut payload = Vec::new();
1368 payload.extend_from_slice(&u32::MAX.to_le_bytes());
1369 let mut r = Reader::new(&payload);
1370 let raw = r.u32().unwrap();
1371 assert!(matches!(
1372 r.bounded_count(raw),
1373 Err(BytecodeError::Corrupt(
1374 "list length exceeds remaining bytes"
1375 ))
1376 ));
1377 }
1378
1379 #[test]
1380 fn read_strings_rejects_oversized_count() {
1381 let mut bad = Vec::new();
1383 bad.extend_from_slice(MAGIC);
1384 bad.push(VERSION);
1385 let mut buf = Vec::new();
1388 buf.extend_from_slice(&u32::MAX.to_le_bytes());
1389 let mut r = Reader::new(&buf);
1390 assert!(matches!(
1391 read_strings(&mut r),
1392 Err(BytecodeError::Corrupt(_))
1393 ));
1394 }
1395
1396 #[test]
1397 fn read_payloads_rejects_oversized_count() {
1398 let mut buf = Vec::new();
1399 buf.extend_from_slice(&u32::MAX.to_le_bytes());
1400 let mut r = Reader::new(&buf);
1401 assert!(matches!(
1402 read_payloads(&mut r),
1403 Err(BytecodeError::Corrupt(_))
1404 ));
1405 }
1406
1407 #[test]
1408 fn read_payloads_rejects_oversized_payload_length() {
1409 let mut buf = Vec::new();
1411 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&u32::MAX.to_le_bytes()); let mut r = Reader::new(&buf);
1414 assert!(matches!(
1415 read_payloads(&mut r),
1416 Err(BytecodeError::Corrupt(_))
1417 ));
1418 }
1419
1420 #[test]
1421 fn read_str_rejects_oversized_length() {
1422 let mut buf = Vec::new();
1423 buf.extend_from_slice(&u32::MAX.to_le_bytes());
1424 let mut r = Reader::new(&buf);
1425 assert!(matches!(r.str(), Err(BytecodeError::Corrupt(_))));
1426 }
1427
1428 #[test]
1429 fn decode_rejects_bad_version() {
1430 let mut buf = Vec::new();
1431 buf.extend_from_slice(MAGIC);
1432 buf.push(99); assert!(matches!(
1434 decode(&buf),
1435 Err(BytecodeError::BadVersion { found: 99, .. })
1436 ));
1437 }
1438
1439 #[test]
1440 fn decode_rejects_bad_magic() {
1441 let buf = [0u8, 0u8, 0u8, 0u8, VERSION];
1442 assert!(matches!(decode(&buf), Err(BytecodeError::BadMagic)));
1443 }
1444
1445 #[test]
1446 fn metadata_roundtrip_preserves_tags_and_lists() {
1447 let metadata = CheckMetadata {
1448 name: Some("Check".into()),
1449 description: None,
1450 impact: None,
1451 severity: Some(Severity::High),
1452 author: None,
1453 cve: vec!["CVE-2024-1".into()],
1454 cwe: vec!["CWE-79".into()],
1455 references: vec!["https://example.com".into()],
1456 cvss: vec![],
1457 cvss_score: vec![],
1458 mitigation: Some("patch".into()),
1459 tags: vec!["auth".into(), "rce".into()],
1460 version: Some("1.2.3".into()),
1461 family: Some("web".into()),
1462 };
1463 let mut w = Writer::default();
1464 write_metadata(&mut w, &metadata);
1465 let mut r = Reader::new(&w.0);
1466 let decoded = read_metadata(&mut r).unwrap();
1467 assert_eq!(decoded.tags, vec!["auth", "rce"]);
1468 assert_eq!(decoded.cve, vec!["CVE-2024-1"]);
1469 assert_eq!(decoded.mitigation.as_deref(), Some("patch"));
1470 assert_eq!(decoded.severity, Some(Severity::High));
1471 assert_eq!(decoded.version.as_deref(), Some("1.2.3"));
1472 assert_eq!(decoded.family.as_deref(), Some("web"));
1473 }
1474
1475 #[test]
1476 fn decode_rejects_out_of_range_string_index() {
1477 let program = BytecodeProgram {
1481 spec: ProgramSpec {
1482 probes: Default::default(),
1483 metadata: CheckMetadata::default(),
1484 },
1485 code: vec![Instr::Set { name: 7, value: 0 }],
1486 strings: vec![],
1487 payloads: vec![],
1488 matchers: vec![],
1489 extracts: vec![],
1490 evidence: vec![],
1491 };
1492 let bytes = encode(&program);
1493 match decode(&bytes) {
1494 Err(BytecodeError::Corrupt("operand index out of range")) => {}
1495 other => panic!("expected operand-index Corrupt, got {other:?}"),
1496 }
1497 }
1498
1499 #[test]
1500 fn decode_rejects_out_of_range_match_slice() {
1501 let program = BytecodeProgram {
1502 spec: ProgramSpec {
1503 probes: Default::default(),
1504 metadata: CheckMetadata::default(),
1505 },
1506 code: vec![Instr::MatchAll { start: 0, len: 3 }],
1508 strings: vec![],
1509 payloads: vec![],
1510 matchers: vec![],
1511 extracts: vec![],
1512 evidence: vec![],
1513 };
1514 let bytes = encode(&program);
1515 assert!(matches!(decode(&bytes), Err(BytecodeError::Corrupt(_))));
1516 }
1517
1518 #[test]
1519 fn decode_accepts_in_range_operands() {
1520 let program = BytecodeProgram {
1521 spec: ProgramSpec {
1522 probes: Default::default(),
1523 metadata: CheckMetadata::default(),
1524 },
1525 code: vec![Instr::Set { name: 0, value: 1 }],
1526 strings: vec!["host".into(), "value".into()],
1527 payloads: vec![],
1528 matchers: vec![],
1529 extracts: vec![],
1530 evidence: vec![],
1531 };
1532 let bytes = encode(&program);
1533 let decoded = decode(&bytes).expect("valid operands round-trip");
1534 assert_eq!(decoded.code.len(), 1);
1535 }
1536
1537 #[test]
1538 fn load_bytecode_input_no_longer_reads_files() {
1539 match load_bytecode_input("@/etc/passwd") {
1543 Err(BytecodeError::InvalidHex(_)) => {}
1544 other => panic!("expected InvalidHex, got {other:?}"),
1545 }
1546 }
1547}