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 for cve in &metadata.cve {
85 writeln!(out, ";; cve: {cve:?}").ok();
86 }
87 for cwe in &metadata.cwe {
88 writeln!(out, ";; cwe: {cwe:?}").ok();
89 }
90 for reference in &metadata.references {
91 writeln!(out, ";; references: {reference:?}").ok();
92 }
93 for cvss in &metadata.cvss {
94 writeln!(out, ";; cvss: {cvss:?}").ok();
95 }
96 for score in &metadata.cvss_score {
97 writeln!(out, ";; cvss_score: {score:?}").ok();
98 }
99 if let Some(mitigation) = &metadata.mitigation {
100 writeln!(out, ";; mitigation: {mitigation:?}").ok();
101 }
102 for tag in &metadata.tags {
103 writeln!(out, ";; tag: {tag:?}").ok();
104 }
105 if let Some(version) = &metadata.version {
106 writeln!(out, ";; version: {version:?}").ok();
107 }
108 if let Some(family) = &metadata.family {
109 writeln!(out, ";; family: {family:?}").ok();
110 }
111}
112
113fn format_probe_kind(kind: &ProbeKind) -> String {
114 match kind {
115 ProbeKind::Http(spec) => {
116 format!("http {} {}", format_http_method(&spec.method), spec.path)
117 }
118 ProbeKind::Dns(spec) => format_socket_probe("dns", spec),
119 ProbeKind::Tcp(spec) => format_socket_probe("tcp", spec),
120 ProbeKind::Udp(spec) => format_socket_probe("udp", spec),
121 }
122}
123
124fn format_socket_probe(label: &str, spec: &crate::runtime::spec::SocketProbeSpec) -> String {
125 let mut line = format!("{label} host={:?}", spec.host);
126 if let Some(port) = spec.port {
127 line.push_str(&format!(" port={port}"));
128 }
129 if let Some(payload) = &spec.payload {
130 let is_text = payload.iter().all(|b| {
131 b.is_ascii_graphic() || *b == b' ' || *b == b'\r' || *b == b'\n' || *b == b'\t'
132 });
133 let shown = if is_text {
134 let text = String::from_utf8_lossy(payload);
137 format!("{text:?}")
138 } else {
139 format!("0x{}", crate::runtime::binary::bytes_to_hex(payload))
140 };
141 line.push_str(&format!(" payload={shown}"));
142 }
143 if label == "dns" && spec.is_dns_resolver_mode() {
144 line.push_str(" (resolver)");
145 }
146 line
147}
148
149fn format_http_method(method: &crate::contract::HttpMethod) -> &'static str {
150 use crate::contract::HttpMethod;
151 match method {
152 HttpMethod::Get => "GET",
153 HttpMethod::Post => "POST",
154 HttpMethod::Put => "PUT",
155 HttpMethod::Patch => "PATCH",
156 HttpMethod::Delete => "DELETE",
157 HttpMethod::Head => "HEAD",
158 HttpMethod::Options => "OPTIONS",
159 }
160}
161
162fn format_matcher(matcher: &QualifiedMatch) -> String {
163 let field = match &matcher.field.kind {
164 FieldKind::Status => "status".to_string(),
165 FieldKind::Body => "body".to_string(),
166 FieldKind::Header(name) => format!("header({name:?})"),
167 FieldKind::ResponseTime => "response_time".to_string(),
168 FieldKind::ResponseSize => "response_size".to_string(),
169 FieldKind::Answer => "answer".to_string(),
170 FieldKind::Banner => "banner".to_string(),
171 FieldKind::Response => "response".to_string(),
172 };
173 format!(
174 "{}.{} {}",
175 matcher.field.target,
176 field,
177 format_predicate(&matcher.predicate)
178 )
179}
180
181fn format_predicate(predicate: &MatchPredicate) -> String {
182 match predicate {
183 MatchPredicate::Compare { op, value } => {
184 format!("{} {}", format_cmp_op(*op), format_cmp_value(value))
185 }
186 MatchPredicate::Contains(text) => format!("contains {text:?}"),
187 MatchPredicate::NotContains(text) => format!("not_contains {text:?}"),
188 MatchPredicate::Regex(pattern) => format!("regex {pattern:?}"),
189 }
190}
191
192fn format_cmp_op(op: CmpOp) -> &'static str {
193 match op {
194 CmpOp::Eq => "==",
195 CmpOp::Ne => "!=",
196 CmpOp::Lt => "<",
197 CmpOp::Gt => ">",
198 CmpOp::Le => "<=",
199 CmpOp::Ge => ">=",
200 }
201}
202
203fn format_cmp_value(value: &CmpValue) -> String {
204 match value {
205 CmpValue::Number(n) => n.to_string(),
206 CmpValue::String(s) => format!("{s:?}"),
207 CmpValue::Duration(d) => d.clone(),
208 }
209}
210
211fn format_extract(source: &ExtractSource) -> String {
212 match source {
213 ExtractSource::Body { target, regex } => match regex {
214 Some(pattern) => format!("body from {target} regex {pattern:?}"),
215 None => format!("body from {target}"),
216 },
217 ExtractSource::Header { target, name } => {
218 format!("header {name:?} from {target}")
219 }
220 }
221}
222
223fn format_evidence(kind: &EvidenceKind) -> String {
224 let re = |p: &Option<String>| match p {
225 Some(pat) => format!(" regex {pat:?}"),
226 None => String::new(),
227 };
228 match kind {
229 EvidenceKind::Body { target, pattern } => format!("{target}.body{}", re(pattern)),
230 EvidenceKind::Response { target, pattern } => format!("{target}.response{}", re(pattern)),
231 EvidenceKind::Header {
232 target,
233 name,
234 pattern,
235 } => format!("{target}.header {name:?}{}", re(pattern)),
236 }
237}
238
239fn format_instr(instr: &Instr, bytecode: &BytecodeProgram) -> String {
240 let str_at = |idx: u32| -> String {
241 bytecode
242 .strings
243 .get(idx as usize)
244 .map(|s| format!("{s:?}"))
245 .unwrap_or_else(|| format!("#{idx}?"))
246 };
247 let string_span = |start: u32, len: u16| -> String {
252 let start = start as usize;
253 let end = start.saturating_add(len as usize);
254 match bytecode.strings.get(start..end) {
255 Some(slice) => slice
256 .iter()
257 .map(|value| format!("{value:?}"))
258 .collect::<Vec<_>>()
259 .join(", "),
260 None => format!("<oob {start}..{end}>"),
261 }
262 };
263
264 match instr {
265 Instr::Set { name, value } => {
266 format!("Set name={} value={}", str_at(*name), str_at(*value))
267 }
268 Instr::SetList { name, start, len } => {
269 format!(
270 "SetList name={} values=[{}]",
271 str_at(*name),
272 string_span(*start, *len)
273 )
274 }
275 Instr::Send { probe, payload } => {
276 if let Some(id) = payload {
277 format!(
278 "Send {} payload=[{}]",
279 str_at(*probe),
280 bytecode
281 .payloads
282 .get(*id as usize)
283 .map(|p| binary::bytes_to_hex(p))
284 .unwrap_or_else(|| format!("#{id}?"))
285 )
286 } else {
287 format!("Send {}", str_at(*probe))
288 }
289 }
290 Instr::Match(matcher) => format!("Match [{}]", matcher),
291 Instr::MatchAll { start, len } => format!("MatchAll [{start}..{}]", start + *len as u32),
292 Instr::MatchAny { start, len } => format!("MatchAny [{start}..{}]", start + *len as u32),
293 Instr::Assert(matcher) => format!("Assert [{}]", matcher),
294 Instr::Extract { name, source } => {
295 format!("Extract name={} source=[{}]", str_at(*name), source)
296 }
297 Instr::IfMatch { matcher, else_pc } => {
298 format!("IfMatch [{}] else_pc={else_pc}", matcher)
299 }
300 Instr::ForList {
301 item,
302 start,
303 len,
304 end_pc,
305 } => format!(
306 "ForList item={} values=[{}] end_pc={end_pc}",
307 str_at(*item),
308 string_span(*start, *len)
309 ),
310 Instr::ForVar { item, list, end_pc } => {
311 format!(
312 "ForVar item={} list={} end_pc={end_pc}",
313 str_at(*item),
314 str_at(*list)
315 )
316 }
317 Instr::LoopBack => "LoopBack".into(),
318 Instr::Break => "Break".into(),
319 Instr::Save { from, to } => {
320 format!("Save {} as {}", str_at(*from), str_at(*to))
321 }
322 Instr::Evidence(kind) => format!("Evidence [{}]", kind),
323 Instr::Retry { probe, count } => {
324 format!("Retry {} count={count}", str_at(*probe))
325 }
326 Instr::RetryDelay(value) => format!("RetryDelay {}", str_at(*value)),
327 Instr::Sleep(value) => format!("Sleep {}", str_at(*value)),
328 Instr::Stop => "Stop".into(),
329 Instr::Fail => "Fail".into(),
330 Instr::Continue => "Continue".into(),
331 Instr::Exit => "Exit".into(),
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::runtime::bytecode::Instr;
339 use crate::runtime::spec::{CheckMetadata, ProgramSpec};
340
341 fn empty_bytecode() -> BytecodeProgram {
342 BytecodeProgram {
343 spec: ProgramSpec {
344 probes: Default::default(),
345 metadata: CheckMetadata::default(),
346 },
347 code: vec![],
348 strings: vec![],
349 payloads: vec![],
350 matchers: vec![],
351 extracts: vec![],
352 evidence: vec![],
353 }
354 }
355
356 #[test]
357 fn out_of_bounds_string_span_does_not_panic() {
358 let bytecode = BytecodeProgram {
362 code: vec![Instr::ForList {
363 item: 99,
364 start: 99,
365 len: 5,
366 end_pc: 0,
367 }],
368 ..empty_bytecode()
369 };
370 let out = format_human(&bytecode);
371 assert!(out.contains("<oob"), "expected oob sentinel, got:\n{out}");
372 }
373}