1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PayloadError {
27 MetricNoPipe {
29 line: usize,
31 },
32 MetricNameValueNoColon {
34 line: usize,
36 },
37 MetricEmptyName {
39 line: usize,
41 },
42 MetricEmptyValue {
44 line: usize,
46 },
47 MetricBadType {
49 line: usize,
51 },
52 MetricUnparseableValue {
54 line: usize,
56 },
57 MetricBadRate {
59 line: usize,
61 },
62 EventNoColon {
64 line: usize,
66 },
67 EventHeaderTooShort {
69 line: usize,
71 },
72 EventNoLengthComma {
74 line: usize,
76 },
77 EventBadTitleLen {
79 line: usize,
81 },
82 EventEmptyTitle {
84 line: usize,
86 },
87 EventBadTextLen {
89 line: usize,
91 },
92 EventLengthOverflow {
94 line: usize,
96 },
97 EventBodyTooShort {
99 line: usize,
101 },
102 ServiceCheckTooFewPipes {
104 line: usize,
106 },
107 ServiceCheckTooShort {
109 line: usize,
111 },
112 ServiceCheckEmptyName {
114 line: usize,
116 },
117 ServiceCheckBadStatus {
119 line: usize,
121 },
122}
123
124pub fn is_malformed(payload: &[u8]) -> Result<(), PayloadError> {
139 for (line, segment) in payload.split(|&b| b == b'\n').enumerate() {
140 let segment = segment.strip_suffix(b"\r").unwrap_or(segment);
141 if segment.is_empty() {
142 continue;
143 }
144 if segment.starts_with(b"_e{") {
145 classify_event(line, segment)?;
146 } else if segment.starts_with(b"_sc") {
147 classify_service_check(line, segment)?;
148 } else {
149 classify_metric(line, segment)?;
150 }
151 }
152 Ok(())
153}
154
155fn classify_metric(line: usize, seg: &[u8]) -> Result<(), PayloadError> {
157 let mut parts = seg.split(|&b| b == b'|');
158 let field0 = parts.next().unwrap_or(&[]);
159 let Some(type_field) = parts.next() else {
161 return Err(PayloadError::MetricNoPipe { line });
162 };
163 let Some(colon) = field0.iter().position(|&b| b == b':') else {
165 return Err(PayloadError::MetricNameValueNoColon { line });
166 };
167 let name = &field0[..colon];
168 let value = &field0[colon + 1..];
169 if name.is_empty() {
171 return Err(PayloadError::MetricEmptyName { line });
172 }
173 if value.is_empty() {
174 return Err(PayloadError::MetricEmptyValue { line });
175 }
176 match type_field {
178 b"s" => {}
179 b"g" | b"c" | b"h" | b"d" | b"ms" => {
180 if value_is_malformed(value) {
181 return Err(PayloadError::MetricUnparseableValue { line });
182 }
183 }
184 _ => return Err(PayloadError::MetricBadType { line }),
185 }
186 for chunk in parts {
192 if let Some((&first, rest)) = chunk.split_first() {
193 if first == b'@' && !go_parse_float_ok(rest) {
194 return Err(PayloadError::MetricBadRate { line });
195 }
196 }
197 }
198 Ok(())
199}
200
201fn classify_event(line: usize, seg: &[u8]) -> Result<(), PayloadError> {
204 let Some(colon) = seg.iter().position(|&b| b == b':') else {
205 return Err(PayloadError::EventNoColon { line });
206 };
207 let header = &seg[..colon];
208 let body = &seg[colon + 1..];
209 if header.len() < 7 {
212 return Err(PayloadError::EventHeaderTooShort { line });
213 }
214 let raw_lengths = &header[3..header.len() - 1];
215 let Some(comma) = raw_lengths.iter().position(|&b| b == b',') else {
216 return Err(PayloadError::EventNoLengthComma { line });
217 };
218 let raw_title_len = &raw_lengths[..comma];
219 let raw_text_len = &raw_lengths[comma + 1..];
220 let Some(title_len) = atoi(raw_title_len).filter(|&len| len >= 0) else {
221 return Err(PayloadError::EventBadTitleLen { line });
222 };
223 if title_len == 0 {
224 return Err(PayloadError::EventEmptyTitle { line });
225 }
226 let Some(text_len) = atoi(raw_text_len).filter(|&len| len >= 0) else {
227 return Err(PayloadError::EventBadTextLen { line });
228 };
229 let (title_len, text_len) = (title_len.unsigned_abs(), text_len.unsigned_abs());
232 let Some(content_len) = title_len.checked_add(1).and_then(|v| v.checked_add(text_len)) else {
234 return Err(PayloadError::EventLengthOverflow { line });
235 };
236 let Ok(body_len) = u64::try_from(body.len()) else {
237 return Ok(());
239 };
240 if body_len < content_len {
241 return Err(PayloadError::EventBodyTooShort { line });
242 }
243 Ok(())
244}
245
246fn atoi(s: &[u8]) -> Option<i64> {
251 let (neg, digits) = match s.first() {
252 Some(b'+') => (false, &s[1..]),
253 Some(b'-') => (true, &s[1..]),
254 _ => (false, s),
255 };
256 if digits.is_empty() {
257 return None;
258 }
259 let mut acc: i128 = 0;
260 for &c in digits {
261 if !c.is_ascii_digit() {
262 return None;
263 }
264 acc = acc * 10 + i128::from(c - b'0');
265 if acc > i128::from(u64::MAX) {
266 return None;
267 }
268 }
269 let value = if neg { -acc } else { acc };
270 i64::try_from(value).ok()
271}
272
273fn classify_service_check(line: usize, seg: &[u8]) -> Result<(), PayloadError> {
275 let two_pipes = seg
277 .iter()
278 .position(|&b| b == b'|')
279 .is_some_and(|i| seg[i + 1..].contains(&b'|'));
280 if !two_pipes {
281 return Err(PayloadError::ServiceCheckTooFewPipes { line });
282 }
283 if seg.len() < 4 {
285 return Err(PayloadError::ServiceCheckTooShort { line });
286 }
287 let mut fields = seg[4..].split(|&b| b == b'|');
288 let name = fields.next().unwrap_or(&[]);
289 if name.is_empty() {
290 return Err(PayloadError::ServiceCheckEmptyName { line });
291 }
292 let status = fields.next().unwrap_or(&[]);
293 if !matches!(status, b"0" | b"1" | b"2" | b"3") {
294 return Err(PayloadError::ServiceCheckBadStatus { line });
295 }
296 Ok(())
297}
298
299fn value_is_malformed(value: &[u8]) -> bool {
304 if value.contains(&b':') {
305 let mut survived = 0usize;
306 for seg in value.split(|&b| b == b':') {
307 if seg.is_empty() {
308 continue;
309 }
310 survived += 1;
311 if !go_parse_float_ok(seg) {
312 return true;
313 }
314 }
315 survived == 0
316 } else {
317 !go_parse_float_ok(value)
318 }
319}
320
321fn go_parse_float_ok(s: &[u8]) -> bool {
328 if s.is_empty() {
329 return false;
330 }
331 if is_special_float(s) {
332 return true;
333 }
334 let Some(scan) = scan_float(s) else {
335 return false;
336 };
337 if scan.hex {
338 !hex_value_overflows(s)
339 } else {
340 decimal_is_finite(s)
341 }
342}
343
344fn is_special_float(s: &[u8]) -> bool {
347 let (rest, signed) = match s.first() {
348 Some(b'+' | b'-') => (&s[1..], true),
349 _ => (s, false),
350 };
351 let eq_ci = |token: &[u8]| rest.len() == token.len() && rest.iter().zip(token).all(|(&a, &b)| (a | 0x20) == b);
352 if eq_ci(b"inf") || eq_ci(b"infinity") {
353 return true;
354 }
355 !signed && eq_ci(b"nan")
356}
357
358#[derive(Clone, Copy, Debug)]
360struct FloatScan {
361 hex: bool,
362}
363
364fn scan_float(s: &[u8]) -> Option<FloatScan> {
368 if !underscore_ok(s) {
369 return None;
370 }
371 let n = s.len();
372 let mut i = 0usize;
373
374 if i < n && (s[i] == b'+' || s[i] == b'-') {
375 i += 1;
376 }
377
378 let mut hex = false;
380 let mut exp_char = b'e';
381 if i + 2 < n && s[i] == b'0' && (s[i + 1] | 0x20) == b'x' {
382 hex = true;
383 exp_char = b'p';
384 i += 2;
385 }
386
387 let mut saw_digits = false;
388 let mut saw_dot = false;
389 while i < n {
390 let c = s[i];
391 if c == b'_' {
392 i += 1;
393 continue;
394 }
395 if c == b'.' {
396 if saw_dot {
397 break;
398 }
399 saw_dot = true;
400 i += 1;
401 continue;
402 }
403 if c.is_ascii_digit() {
404 saw_digits = true;
405 i += 1;
406 continue;
407 }
408 if hex && (b'a'..=b'f').contains(&(c | 0x20)) {
409 saw_digits = true;
410 i += 1;
411 continue;
412 }
413 break;
414 }
415 if !saw_digits {
416 return None;
417 }
418
419 if i < n && (s[i] | 0x20) == exp_char {
420 i += 1;
421 if i < n && (s[i] == b'+' || s[i] == b'-') {
422 i += 1;
423 }
424 if i >= n || !s[i].is_ascii_digit() {
425 return None;
426 }
427 while i < n {
428 let c = s[i];
429 if c == b'_' || c.is_ascii_digit() {
430 i += 1;
431 } else {
432 break;
433 }
434 }
435 } else if hex {
436 return None;
438 }
439
440 if i != n {
441 return None;
442 }
443 Some(FloatScan { hex })
444}
445
446fn underscore_ok(s: &[u8]) -> bool {
449 if !s.contains(&b'_') {
450 return true;
451 }
452 let mut saw = b'^';
454 let n = s.len();
455 let mut i = 0usize;
456
457 if n >= 1 && (s[0] == b'-' || s[0] == b'+') {
458 i = 1;
459 }
460
461 let mut hex = false;
462 if n - i >= 2 && s[i] == b'0' {
463 let lc = s[i + 1] | 0x20;
464 if lc == b'b' || lc == b'o' || lc == b'x' {
465 saw = b'0';
466 hex = lc == b'x';
467 i += 2;
468 }
469 }
470
471 while i < n {
472 let c = s[i];
473 if c.is_ascii_digit() || (hex && (b'a'..=b'f').contains(&(c | 0x20))) {
474 saw = b'0';
475 } else if c == b'_' {
476 if saw != b'0' {
477 return false;
478 }
479 saw = b'_';
480 } else {
481 if saw == b'_' {
482 return false;
483 }
484 saw = b'!';
485 }
486 i += 1;
487 }
488 saw != b'_'
489}
490
491fn decimal_is_finite(s: &[u8]) -> bool {
495 let parsed = if s.contains(&b'_') {
496 let cleaned: Vec<u8> = s.iter().copied().filter(|&b| b != b'_').collect();
497 parse_ascii_f64(&cleaned)
498 } else {
499 parse_ascii_f64(s)
500 };
501 match parsed {
502 Some(v) => v.is_finite(),
503 None => true,
506 }
507}
508
509fn parse_ascii_f64(s: &[u8]) -> Option<f64> {
511 simdutf8::basic::from_utf8(s).ok()?.parse::<f64>().ok()
512}
513
514fn hex_value_overflows(s: &[u8]) -> bool {
523 const MAX_MANT_HEX_DIGITS: i64 = 16;
524
525 let len = s.len();
526 let mut i = 0usize;
527 if i < len && (s[i] == b'+' || s[i] == b'-') {
528 i += 1;
529 }
530 i += 2; let mut mantissa: f64 = 0.0;
533 let mut nd: i64 = 0; let mut nd_mant: i64 = 0; let mut dp: i64 = 0; let mut saw_dot = false;
537 while i < len {
538 let byte = s[i];
539 if byte == b'_' {
540 i += 1;
541 continue;
542 }
543 if byte == b'.' {
544 if saw_dot {
545 break;
546 }
547 saw_dot = true;
548 dp = nd;
549 i += 1;
550 continue;
551 }
552 let digit: u8 = if byte.is_ascii_digit() {
553 byte - b'0'
554 } else {
555 let lower = byte | 0x20;
556 if (b'a'..=b'f').contains(&lower) {
557 lower - b'a' + 10
558 } else {
559 break;
560 }
561 };
562 if digit == 0 && nd == 0 {
564 dp -= 1;
565 i += 1;
566 continue;
567 }
568 nd += 1;
569 if nd_mant < MAX_MANT_HEX_DIGITS {
570 mantissa = mantissa * 16.0 + f64::from(digit);
571 nd_mant += 1;
572 }
573 i += 1;
574 }
575 if nd_mant == 0 {
577 return false;
578 }
579 if !saw_dot {
580 dp = nd;
581 }
582 dp *= 4;
584 nd_mant *= 4;
585
586 i += 1; let mut esign: i64 = 1;
588 if i < len && (s[i] == b'+' || s[i] == b'-') {
589 if s[i] == b'-' {
590 esign = -1;
591 }
592 i += 1;
593 }
594 let mut exp: i64 = 0;
595 while i < len {
596 let byte = s[i];
597 if byte == b'_' {
598 i += 1;
599 } else if byte.is_ascii_digit() {
600 exp = exp.saturating_mul(10).saturating_add(i64::from(byte - b'0'));
601 i += 1;
602 } else {
603 break;
604 }
605 }
606 dp = dp.saturating_add(esign.saturating_mul(exp));
607
608 let total = dp.saturating_sub(nd_mant);
609 if total > 1100 {
610 return true;
611 }
612 if total < -1200 {
613 return false;
614 }
615 let Ok(total_i32) = i32::try_from(total) else {
616 return true;
617 };
618 (mantissa * 2f64.powi(total_i32)).is_infinite()
619}
620
621#[cfg(test)]
622mod tests {
623 use proptest::prelude::*;
624
625 use super::{is_malformed, PayloadError};
626
627 #[test]
630 fn metric_well_formed_forwards() {
631 for line in [
635 &b"m:1|c"[..],
636 b"m:1|g",
637 b"m:1|ms",
638 b"m:1|h",
639 b"m:1|d",
640 b"m:anything goes|s",
641 b"m:1:2:3|d",
642 b"m:nan|g",
643 b"m:inf|g",
644 b"m:-inf|g",
645 b"m:0x1p4|g",
646 b"m:1_000|g",
647 b"m:1.|g",
648 b"m:.5|g",
649 b"m:1|c|@0.5",
650 b"m:1|c|@nan",
651 b"m:1|c|#a:b,c:d",
652 b"m:1|c|c:cid-deadbeef",
653 b"m:1|c|card:high",
654 b"na\xff\x00me:1|c",
655 ] {
656 assert_eq!(is_malformed(line), Ok(()), "expected forward: {line:?}");
657 }
658 }
659
660 #[test]
661 fn metric_drop_rules() {
662 assert_eq!(is_malformed(b"m:1"), Err(PayloadError::MetricNoPipe { line: 0 }));
663 assert_eq!(
664 is_malformed(b"noColon|g"),
665 Err(PayloadError::MetricNameValueNoColon { line: 0 })
666 );
667 assert_eq!(is_malformed(b":1|g"), Err(PayloadError::MetricEmptyName { line: 0 }));
668 assert_eq!(is_malformed(b"m:|g"), Err(PayloadError::MetricEmptyValue { line: 0 }));
669 assert_eq!(is_malformed(b"m:1|gg"), Err(PayloadError::MetricBadType { line: 0 }));
670 assert_eq!(is_malformed(b"m:1|"), Err(PayloadError::MetricBadType { line: 0 }));
671 assert_eq!(
672 is_malformed(b"m:notafloat|g"),
673 Err(PayloadError::MetricUnparseableValue { line: 0 })
674 );
675 assert_eq!(
676 is_malformed(b"m:::|d"),
677 Err(PayloadError::MetricUnparseableValue { line: 0 })
678 );
679 assert_eq!(
680 is_malformed(b"m:1|c|@bad"),
681 Err(PayloadError::MetricBadRate { line: 0 })
682 );
683 }
684
685 #[test]
688 fn event_well_formed_forwards() {
689 for line in [
690 &b"_e{1,0}:a|"[..],
691 b"_e{5,4}:title|text",
692 b"_e{5,0}:title|",
693 b"_e{5,4}:title|text|h:host|k:key|p:normal|t:error|s:src|#a:b",
694 b"_e{5,4}:title|text|p:bogus|t:bogus|d:notanint", b"_e{5,4}:title|text|extra bytes past declared body", b"_e{5,4}:t\xff\x00le|te\xffxt", ] {
698 assert_eq!(is_malformed(line), Ok(()), "expected forward: {line:?}");
699 }
700 }
701
702 #[test]
703 fn event_drop_rules() {
704 assert_eq!(
705 is_malformed(b"_e{5,4}title|text"),
706 Err(PayloadError::EventNoColon { line: 0 })
707 );
708 assert_eq!(
709 is_malformed(b"_e{}:x"),
710 Err(PayloadError::EventHeaderTooShort { line: 0 })
711 );
712 assert_eq!(
713 is_malformed(b"_e{500}:title"),
714 Err(PayloadError::EventNoLengthComma { line: 0 })
715 );
716 assert_eq!(
717 is_malformed(b"_e{x,0}:title"),
718 Err(PayloadError::EventBadTitleLen { line: 0 })
719 );
720 assert_eq!(
721 is_malformed(b"_e{0,0}:"),
722 Err(PayloadError::EventEmptyTitle { line: 0 })
723 );
724 assert_eq!(
725 is_malformed(b"_e{5,x}:title"),
726 Err(PayloadError::EventBadTextLen { line: 0 })
727 );
728 assert_eq!(
729 is_malformed(b"_e{5,4}:t"),
730 Err(PayloadError::EventBodyTooShort { line: 0 })
731 );
732 }
733
734 #[test]
738 fn event_signed_zero_length_matches_atoi() {
739 assert_eq!(is_malformed(b"_e{1,-0}:a|"), Ok(()));
740 assert_eq!(is_malformed(b"_e{1,-00}:a|"), Ok(()));
741 assert_eq!(
743 is_malformed(b"_e{1,-1}:a|"),
744 Err(PayloadError::EventBadTextLen { line: 0 })
745 );
746 assert_eq!(
748 is_malformed(b"_e{-0,0}:"),
749 Err(PayloadError::EventEmptyTitle { line: 0 })
750 );
751 }
752
753 #[test]
756 fn service_check_well_formed_forwards() {
757 for line in [
758 &b"_sc|name|0"[..],
759 b"_sc|name|1",
760 b"_sc|name|2",
761 b"_sc|name|3",
762 b"_sc|name|0|h:host|#a:b|m:message text",
763 b"_sc|na\xffme|0", ] {
765 assert_eq!(is_malformed(line), Ok(()), "expected forward: {line:?}");
766 }
767 }
768
769 #[test]
770 fn service_check_drop_rules() {
771 assert_eq!(
772 is_malformed(b"_sc|nameonly"),
773 Err(PayloadError::ServiceCheckTooFewPipes { line: 0 })
774 );
775 assert_eq!(
776 is_malformed(b"_sc"),
777 Err(PayloadError::ServiceCheckTooFewPipes { line: 0 })
778 );
779 assert_eq!(
780 is_malformed(b"_sc||0"),
781 Err(PayloadError::ServiceCheckEmptyName { line: 0 })
782 );
783 assert_eq!(
784 is_malformed(b"_sc|name|9"),
785 Err(PayloadError::ServiceCheckBadStatus { line: 0 })
786 );
787 assert_eq!(
788 is_malformed(b"_sc|name|"),
789 Err(PayloadError::ServiceCheckBadStatus { line: 0 })
790 );
791 }
792
793 #[test]
796 fn framing_skips_blanks_and_reports_first_offending_line() {
797 let payload = b"m:1|c\n\n\nbad|line\nm:2|c";
799 assert_eq!(
800 is_malformed(payload),
801 Err(PayloadError::MetricNameValueNoColon { line: 3 })
802 );
803 }
804
805 #[test]
806 fn framing_handles_crlf_and_empty_payload() {
807 assert_eq!(is_malformed(b"m:1|c\r\nm:2|g\r\n"), Ok(()));
808 assert_eq!(is_malformed(b""), Ok(()));
809 assert_eq!(is_malformed(b"\n\n"), Ok(()));
810 }
811
812 #[test]
813 fn routing_by_prefix() {
814 assert_eq!(is_malformed(b"_e:1|g"), Ok(()));
816 assert_eq!(is_malformed(b"gibberish"), Err(PayloadError::MetricNoPipe { line: 0 }));
817 }
818
819 #[test]
822 fn go_float_accepts_go_specific_forms() {
823 for value in [
825 &b"0x1p-2"[..],
826 b"1_000",
827 b"nan",
828 b"inf",
829 b"infinity",
830 b"+inf",
831 b"-inf",
832 b"1.",
833 b".5",
834 b"0x1.8p3",
835 ] {
836 let mut line = b"m:".to_vec();
837 line.extend_from_slice(value);
838 line.extend_from_slice(b"|g");
839 assert_eq!(is_malformed(&line), Ok(()), "expected forward for value {value:?}");
840 }
841 }
842
843 #[test]
844 fn go_float_rejects_what_go_rejects() {
845 for value in [
846 &b"+nan"[..],
847 b"-nan",
848 b"0x1",
849 b"_1",
850 b"1_",
851 b"1__0",
852 b"1e",
853 b"1e+",
854 b"abc",
855 ] {
856 let mut line = b"m:".to_vec();
857 line.extend_from_slice(value);
858 line.extend_from_slice(b"|g");
859 assert_eq!(
860 is_malformed(&line),
861 Err(PayloadError::MetricUnparseableValue { line: 0 }),
862 "expected drop for value {value:?}"
863 );
864 }
865 }
866
867 #[test]
868 fn hex_float_overflow_boundary() {
869 assert_eq!(is_malformed(b"m:0x1p1023|g"), Ok(()));
871 assert_eq!(
872 is_malformed(b"m:0x1p1024|g"),
873 Err(PayloadError::MetricUnparseableValue { line: 0 })
874 );
875 }
876
877 proptest! {
878 #[test]
880 fn property_test_never_panics(payload in proptest::collection::vec(any::<u8>(), 0..128)) {
881 let _ = is_malformed(&payload);
882 }
883 }
884}