1use std::fmt::Write as _;
4
5use crate::contract::{
6 CmpOp, CmpValue, EvidenceKind, ExtractSource, FieldKind, MatchPredicate, QualifiedMatch,
7};
8use crate::runtime::binary;
9use crate::runtime::bytecode::{BytecodeProgram, Instr};
10use crate::runtime::spec::ProbeKind;
11
12pub fn format_human(bytecode: &BytecodeProgram) -> String {
13 let mut out = String::new();
14
15 writeln!(out, ";; metadata").ok();
16 format_metadata(&mut out, &bytecode.spec.metadata);
17 writeln!(out).ok();
18
19 writeln!(out, ";; probes ({})", bytecode.spec.probes.len()).ok();
20 let mut probe_names: Vec<_> = bytecode.spec.probes.keys().collect();
21 probe_names.sort();
22 for name in probe_names {
23 let kind = &bytecode.spec.probes[name];
24 writeln!(out, ";; probe {name}: {}", format_probe_kind(kind)).ok();
25 }
26 writeln!(out).ok();
27
28 if !bytecode.strings.is_empty() {
29 writeln!(out, ";; strings").ok();
30 for (idx, value) in bytecode.strings.iter().enumerate() {
31 writeln!(out, ";; [{idx}] {value:?}").ok();
32 }
33 writeln!(out).ok();
34 }
35
36 if !bytecode.matchers.is_empty() {
37 writeln!(out, ";; matchers").ok();
38 for (idx, matcher) in bytecode.matchers.iter().enumerate() {
39 writeln!(out, ";; [{idx}] {}", format_matcher(matcher)).ok();
40 }
41 writeln!(out).ok();
42 }
43
44 if !bytecode.extracts.is_empty() {
45 writeln!(out, ";; extracts").ok();
46 for (idx, source) in bytecode.extracts.iter().enumerate() {
47 writeln!(out, ";; [{idx}] {}", format_extract(source)).ok();
48 }
49 writeln!(out).ok();
50 }
51
52 if !bytecode.evidence.is_empty() {
53 writeln!(out, ";; evidence").ok();
54 for (idx, kind) in bytecode.evidence.iter().enumerate() {
55 writeln!(out, ";; [{idx}] {}", format_evidence(kind)).ok();
56 }
57 writeln!(out).ok();
58 }
59
60 writeln!(out, ";; code ({} instructions)", bytecode.code.len()).ok();
61 for (pc, instr) in bytecode.code.iter().enumerate() {
62 writeln!(out, " pc {pc:>3}: {}", format_instr(instr, bytecode)).ok();
63 }
64
65 out
66}
67
68fn format_metadata(out: &mut String, metadata: &crate::runtime::spec::CheckMetadata) {
69 if let Some(name) = &metadata.name {
70 writeln!(out, ";; name: {name:?}").ok();
71 }
72 if let Some(description) = &metadata.description {
73 writeln!(out, ";; description: {description:?}").ok();
74 }
75 if let Some(impact) = &metadata.impact {
76 writeln!(out, ";; impact: {impact:?}").ok();
77 }
78 if let Some(severity) = &metadata.severity {
79 writeln!(out, ";; severity: {}", severity.as_str()).ok();
80 }
81 if let Some(author) = &metadata.author {
82 writeln!(out, ";; author: {author:?}").ok();
83 }
84 if let Some(title) = &metadata.report_title {
85 writeln!(out, ";; report: {title:?}").ok();
86 }
87 for cve in &metadata.cve {
88 writeln!(out, ";; cve: {cve:?}").ok();
89 }
90 for cwe in &metadata.cwe {
91 writeln!(out, ";; cwe: {cwe:?}").ok();
92 }
93 for reference in &metadata.references {
94 writeln!(out, ";; references: {reference:?}").ok();
95 }
96 for cvss in &metadata.cvss {
97 writeln!(out, ";; cvss: {cvss:?}").ok();
98 }
99 for score in &metadata.cvss_score {
100 writeln!(out, ";; cvss_score: {score:?}").ok();
101 }
102 if let Some(mitigation) = &metadata.mitigation {
103 writeln!(out, ";; mitigation: {mitigation:?}").ok();
104 }
105 for tag in &metadata.tags {
106 writeln!(out, ";; tag: {tag:?}").ok();
107 }
108 if let Some(version) = &metadata.version {
109 writeln!(out, ";; version: {version:?}").ok();
110 }
111 if let Some(family) = &metadata.family {
112 writeln!(out, ";; family: {family:?}").ok();
113 }
114}
115
116fn format_probe_kind(kind: &ProbeKind) -> String {
117 match kind {
118 ProbeKind::Http(spec) => {
119 format!("http {} {}", format_http_method(&spec.method), spec.path)
120 }
121 ProbeKind::Dns(spec) => format_socket_probe("dns", spec),
122 ProbeKind::Tcp(spec) => format_socket_probe("tcp", spec),
123 ProbeKind::Udp(spec) => format_socket_probe("udp", spec),
124 }
125}
126
127fn format_socket_probe(label: &str, spec: &crate::runtime::spec::SocketProbeSpec) -> String {
128 let mut line = format!("{label} host={:?}", spec.host);
129 if let Some(port) = spec.port {
130 line.push_str(&format!(" port={port}"));
131 }
132 if let Some(payload) = &spec.payload {
133 let is_text = payload.iter().all(|b| {
134 b.is_ascii_graphic() || *b == b' ' || *b == b'\r' || *b == b'\n' || *b == b'\t'
135 });
136 let shown = if is_text {
137 let text = String::from_utf8_lossy(payload);
140 format!("{text:?}")
141 } else {
142 format!("0x{}", crate::runtime::binary::bytes_to_hex(payload))
143 };
144 line.push_str(&format!(" payload={shown}"));
145 }
146 if label == "dns" && spec.is_dns_resolver_mode() {
147 line.push_str(" (resolver)");
148 }
149 line
150}
151
152fn format_http_method(method: &crate::contract::HttpMethod) -> &'static str {
153 use crate::contract::HttpMethod;
154 match method {
155 HttpMethod::Get => "GET",
156 HttpMethod::Post => "POST",
157 HttpMethod::Put => "PUT",
158 HttpMethod::Patch => "PATCH",
159 HttpMethod::Delete => "DELETE",
160 HttpMethod::Head => "HEAD",
161 HttpMethod::Options => "OPTIONS",
162 }
163}
164
165fn format_matcher(matcher: &QualifiedMatch) -> String {
166 let field = match &matcher.field.kind {
167 FieldKind::Status => "status".to_string(),
168 FieldKind::Body => "body".to_string(),
169 FieldKind::Header(name) => format!("header({name:?})"),
170 FieldKind::ResponseTime => "response_time".to_string(),
171 FieldKind::ResponseSize => "response_size".to_string(),
172 FieldKind::Answer => "answer".to_string(),
173 FieldKind::Banner => "banner".to_string(),
174 FieldKind::Response => "response".to_string(),
175 };
176 format!(
177 "{}.{} {}",
178 matcher.field.target,
179 field,
180 format_predicate(&matcher.predicate)
181 )
182}
183
184fn format_predicate(predicate: &MatchPredicate) -> String {
185 match predicate {
186 MatchPredicate::Compare { op, value } => {
187 format!("{} {}", format_cmp_op(*op), format_cmp_value(value))
188 }
189 MatchPredicate::Contains(text) => format!("contains {text:?}"),
190 MatchPredicate::NotContains(text) => format!("not_contains {text:?}"),
191 MatchPredicate::Regex(pattern) => format!("regex {pattern:?}"),
192 }
193}
194
195fn format_cmp_op(op: CmpOp) -> &'static str {
196 match op {
197 CmpOp::Eq => "==",
198 CmpOp::Ne => "!=",
199 CmpOp::Lt => "<",
200 CmpOp::Gt => ">",
201 CmpOp::Le => "<=",
202 CmpOp::Ge => ">=",
203 }
204}
205
206fn format_cmp_value(value: &CmpValue) -> String {
207 match value {
208 CmpValue::Number(n) => n.to_string(),
209 CmpValue::String(s) => format!("{s:?}"),
210 CmpValue::Duration(d) => d.clone(),
211 }
212}
213
214fn format_extract(source: &ExtractSource) -> String {
215 match source {
216 ExtractSource::Body { target, regex } => match regex {
217 Some(pattern) => format!("body from {target} regex {pattern:?}"),
218 None => format!("body from {target}"),
219 },
220 ExtractSource::Header { target, name } => {
221 format!("header {name:?} from {target}")
222 }
223 }
224}
225
226fn format_evidence(kind: &EvidenceKind) -> String {
227 match kind {
228 EvidenceKind::BodyRef(target) => format!("body {target}"),
229 EvidenceKind::ResponseRef(target) => format!("response {target}"),
230 EvidenceKind::Regex { target, pattern } => format!("regex {target} {pattern:?}"),
231 }
232}
233
234fn format_instr(instr: &Instr, bytecode: &BytecodeProgram) -> String {
235 let str_at = |idx: u32| -> String {
236 bytecode
237 .strings
238 .get(idx as usize)
239 .map(|s| format!("{s:?}"))
240 .unwrap_or_else(|| format!("#{idx}?"))
241 };
242 let string_span = |start: u32, len: u16| -> String {
247 let start = start as usize;
248 let end = start.saturating_add(len as usize);
249 match bytecode.strings.get(start..end) {
250 Some(slice) => slice
251 .iter()
252 .map(|value| format!("{value:?}"))
253 .collect::<Vec<_>>()
254 .join(", "),
255 None => format!("<oob {start}..{end}>"),
256 }
257 };
258
259 match instr {
260 Instr::Set { name, value } => {
261 format!("Set name={} value={}", str_at(*name), str_at(*value))
262 }
263 Instr::SetList { name, start, len } => {
264 format!(
265 "SetList name={} values=[{}]",
266 str_at(*name),
267 string_span(*start, *len)
268 )
269 }
270 Instr::Send { probe, payload } => {
271 if let Some(id) = payload {
272 format!(
273 "Send {} payload=[{}]",
274 str_at(*probe),
275 bytecode
276 .payloads
277 .get(*id as usize)
278 .map(|p| binary::bytes_to_hex(p))
279 .unwrap_or_else(|| format!("#{id}?"))
280 )
281 } else {
282 format!("Send {}", str_at(*probe))
283 }
284 }
285 Instr::Match(matcher) => format!("Match [{}]", matcher),
286 Instr::MatchAll { start, len } => format!("MatchAll [{start}..{}]", start + *len as u32),
287 Instr::MatchAny { start, len } => format!("MatchAny [{start}..{}]", start + *len as u32),
288 Instr::Assert(matcher) => format!("Assert [{}]", matcher),
289 Instr::Extract { name, source } => {
290 format!("Extract name={} source=[{}]", str_at(*name), source)
291 }
292 Instr::IfMatch { matcher, else_pc } => {
293 format!("IfMatch [{}] else_pc={else_pc}", matcher)
294 }
295 Instr::ForList {
296 item,
297 start,
298 len,
299 end_pc,
300 } => format!(
301 "ForList item={} values=[{}] end_pc={end_pc}",
302 str_at(*item),
303 string_span(*start, *len)
304 ),
305 Instr::ForVar { item, list, end_pc } => {
306 format!(
307 "ForVar item={} list={} end_pc={end_pc}",
308 str_at(*item),
309 str_at(*list)
310 )
311 }
312 Instr::LoopBack => "LoopBack".into(),
313 Instr::Break => "Break".into(),
314 Instr::Save { from, to } => {
315 format!("Save {} as {}", str_at(*from), str_at(*to))
316 }
317 Instr::Evidence(kind) => format!("Evidence [{}]", kind),
318 Instr::Retry { probe, count } => {
319 format!("Retry {} count={count}", str_at(*probe))
320 }
321 Instr::RetryDelay(value) => format!("RetryDelay {}", str_at(*value)),
322 Instr::Sleep(value) => format!("Sleep {}", str_at(*value)),
323 Instr::Stop => "Stop".into(),
324 Instr::Fail => "Fail".into(),
325 Instr::Continue => "Continue".into(),
326 Instr::Exit => "Exit".into(),
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::runtime::bytecode::Instr;
334 use crate::runtime::spec::{CheckMetadata, ProgramSpec};
335
336 fn empty_bytecode() -> BytecodeProgram {
337 BytecodeProgram {
338 spec: ProgramSpec {
339 probes: Default::default(),
340 metadata: CheckMetadata::default(),
341 },
342 code: vec![],
343 strings: vec![],
344 payloads: vec![],
345 matchers: vec![],
346 extracts: vec![],
347 evidence: vec![],
348 }
349 }
350
351 #[test]
352 fn out_of_bounds_string_span_does_not_panic() {
353 let bytecode = BytecodeProgram {
357 code: vec![Instr::ForList {
358 item: 99,
359 start: 99,
360 len: 5,
361 end_pc: 0,
362 }],
363 ..empty_bytecode()
364 };
365 let out = format_human(&bytecode);
366 assert!(out.contains("<oob"), "expected oob sentinel, got:\n{out}");
367 }
368}