1use crate::level::LogLevel;
2
3use std::fmt;
4
5pub(crate) const UUID_LEN: usize = 36;
7
8pub(crate) const UUID_PREFIX_LEN: usize = UUID_LEN + 1;
11
12#[non_exhaustive]
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LineKind {
21 Full,
23 System,
25 UuidContinuation,
27 BareContinuation,
29 Truncated,
31 Empty,
33}
34
35impl fmt::Display for LineKind {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 LineKind::Full => f.pad("full"),
39 LineKind::System => f.pad("system"),
40 LineKind::UuidContinuation => f.pad("uuid-cont"),
41 LineKind::BareContinuation => f.pad("bare-cont"),
42 LineKind::Truncated => f.pad("truncated"),
43 LineKind::Empty => f.pad("empty"),
44 }
45 }
46}
47
48#[derive(Debug, PartialEq, Eq)]
54pub struct RawLine<'a> {
55 pub uuid: Option<&'a str>,
57 pub timestamp: Option<&'a str>,
59 pub idle_pct: Option<&'a str>,
61 pub level: Option<LogLevel>,
63 pub source: Option<&'a str>,
65 pub message: &'a str,
67 pub kind: LineKind,
69}
70
71pub(crate) fn is_uuid_at(bytes: &[u8], offset: usize) -> bool {
72 if bytes.len() < offset + UUID_PREFIX_LEN {
73 return false;
74 }
75 if bytes[offset + UUID_LEN] != b' ' {
76 return false;
77 }
78 for (i, &b) in bytes[offset..offset + UUID_LEN].iter().enumerate() {
79 match i {
80 8 | 13 | 18 | 23 => {
81 if b != b'-' {
82 return false;
83 }
84 }
85 _ => {
86 if !b.is_ascii_hexdigit() {
87 return false;
88 }
89 }
90 }
91 }
92 true
93}
94
95fn find_uuid_in(bytes: &[u8]) -> Option<usize> {
96 if bytes.len() < UUID_PREFIX_LEN {
97 return None;
98 }
99 let max_start = (bytes.len() - UUID_PREFIX_LEN).min(50);
100 (1..=max_start).find(|&start| is_uuid_at(bytes, start))
101}
102
103pub(crate) fn is_date_at(bytes: &[u8], offset: usize) -> bool {
104 if bytes.len() < offset + 5 {
105 return false;
106 }
107 bytes[offset..offset + 4].iter().all(u8::is_ascii_digit) && bytes[offset + 4] == b'-'
108}
109
110pub(crate) fn is_log_header_at(bytes: &[u8], offset: usize) -> bool {
120 if bytes.len() < offset + 31 {
122 return false;
123 }
124 if !(bytes[offset..offset + 4].iter().all(u8::is_ascii_digit)
126 && bytes[offset + 4] == b'-'
127 && bytes[offset + 5..offset + 7].iter().all(u8::is_ascii_digit)
128 && bytes[offset + 7] == b'-'
129 && bytes[offset + 8..offset + 10]
130 .iter()
131 .all(u8::is_ascii_digit)
132 && bytes[offset + 10] == b' '
133 && bytes[offset + 11..offset + 13]
134 .iter()
135 .all(u8::is_ascii_digit)
136 && bytes[offset + 13] == b':'
137 && bytes[offset + 14..offset + 16]
138 .iter()
139 .all(u8::is_ascii_digit)
140 && bytes[offset + 16] == b':'
141 && bytes[offset + 17..offset + 19]
142 .iter()
143 .all(u8::is_ascii_digit)
144 && bytes[offset + 19] == b'.'
145 && bytes[offset + 20..offset + 26]
146 .iter()
147 .all(u8::is_ascii_digit)
148 && bytes[offset + 26] == b' ')
149 {
150 return false;
151 }
152 let rest = &bytes[offset + 27..];
155 if rest[0] == b'[' {
156 return true;
157 }
158 if !rest[0].is_ascii_digit() {
160 return false;
161 }
162 let Some(pct_pos) = rest[..rest.len().min(7)].iter().position(|&b| b == b'%') else {
163 return false;
164 };
165 rest.len() > pct_pos + 2 && rest[pct_pos + 1] == b' ' && rest[pct_pos + 2] == b'['
166}
167
168fn parse_idle_pct(rest: &str) -> (Option<&str>, &str) {
177 let bytes = rest.as_bytes();
178 if bytes.is_empty() || !bytes[0].is_ascii_digit() {
179 return (None, rest);
180 }
181 let search_len = rest.len().min(7);
182 let pct_pos = match bytes[..search_len].iter().position(|&b| b == b'%') {
183 Some(p) => p,
184 None => return (None, rest),
185 };
186 if !bytes[..pct_pos]
187 .iter()
188 .all(|&b| b.is_ascii_digit() || b == b'.')
189 {
190 return (None, rest);
191 }
192 if rest.len() > pct_pos + 2 && !rest.is_char_boundary(pct_pos + 2) {
195 return (None, rest);
196 }
197 let idle_pct = &rest[0..=pct_pos];
198 let after = if rest.len() > pct_pos + 2 {
199 &rest[pct_pos + 2..]
200 } else {
201 ""
202 };
203 (Some(idle_pct), after)
204}
205
206fn header_boundaries_ok(s: &str) -> bool {
210 s.len() < 27 || (s.is_char_boundary(26) && s.is_char_boundary(27))
211}
212
213fn parse_timestamped_fields(
214 s: &str,
215) -> (
216 Option<&str>,
217 Option<&str>,
218 Option<LogLevel>,
219 Option<&str>,
220 &str,
221) {
222 if s.len() < 27 {
223 return (None, None, None, None, s);
224 }
225 let timestamp = &s[0..26];
226 let rest = &s[27..];
227
228 let (idle_pct, rest) = parse_idle_pct(rest);
229
230 let bracket_end = match rest.find(']') {
231 Some(p) => p,
232 None => return (Some(timestamp), idle_pct, None, None, rest),
233 };
234 let level = LogLevel::from_bracketed(&rest[0..=bracket_end]);
235
236 if rest.len() < bracket_end + 3 || !rest.is_char_boundary(bracket_end + 2) {
237 return (Some(timestamp), idle_pct, level, None, "");
238 }
239 let rest = &rest[bracket_end + 2..];
240
241 let source_end = rest.find(' ').unwrap_or(rest.len());
242 let source = &rest[0..source_end];
243 let message = if source_end < rest.len() {
244 &rest[source_end + 1..]
245 } else {
246 ""
247 };
248
249 (Some(timestamp), idle_pct, level, Some(source), message)
250}
251
252pub fn parse_line(line: &str) -> RawLine<'_> {
258 if line.trim().is_empty() {
259 return RawLine {
260 uuid: None,
261 timestamp: None,
262 idle_pct: None,
263 level: None,
264 source: None,
265 message: line,
266 kind: LineKind::Empty,
267 };
268 }
269
270 let bytes = line.as_bytes();
271
272 if is_uuid_at(bytes, 0) {
273 let uuid = &line[0..UUID_LEN];
274 let after_uuid = &line[UUID_PREFIX_LEN..];
275
276 if is_date_at(bytes, UUID_PREFIX_LEN) && header_boundaries_ok(after_uuid) {
277 let (timestamp, idle_pct, level, source, message) =
278 parse_timestamped_fields(after_uuid);
279 return RawLine {
280 uuid: Some(uuid),
281 timestamp,
282 idle_pct,
283 level,
284 source,
285 message,
286 kind: LineKind::Full,
287 };
288 }
289
290 return RawLine {
291 uuid: Some(uuid),
292 timestamp: None,
293 idle_pct: None,
294 level: None,
295 source: None,
296 message: after_uuid,
297 kind: LineKind::UuidContinuation,
298 };
299 }
300
301 if is_date_at(bytes, 0) && header_boundaries_ok(line) {
302 let (timestamp, idle_pct, level, source, message) = parse_timestamped_fields(line);
303 let (uuid, message) = if is_uuid_at(message.as_bytes(), 0) {
304 (Some(&message[0..UUID_LEN]), &message[UUID_PREFIX_LEN..])
305 } else {
306 (None, message)
307 };
308 return RawLine {
309 uuid,
310 timestamp,
311 idle_pct,
312 level,
313 source,
314 message,
315 kind: LineKind::System,
316 };
317 }
318
319 if let Some(uuid_start) = find_uuid_in(bytes) {
320 let uuid = &line[uuid_start..uuid_start + UUID_LEN];
321 let message = if line.len() > uuid_start + UUID_PREFIX_LEN {
322 &line[uuid_start + UUID_PREFIX_LEN..]
323 } else {
324 ""
325 };
326 return RawLine {
327 uuid: Some(uuid),
328 timestamp: None,
329 idle_pct: None,
330 level: None,
331 source: None,
332 message,
333 kind: LineKind::Truncated,
334 };
335 }
336
337 RawLine {
338 uuid: None,
339 timestamp: None,
340 idle_pct: None,
341 level: None,
342 source: None,
343 message: line,
344 kind: LineKind::BareContinuation,
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 const UUID1: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
353
354 #[test]
357 fn full_line_all_fields() {
358 let line = format!(
359 "{UUID1} 2025-01-15 10:30:45.123456 95.97% [DEBUG] sofia.c:100 Test message here"
360 );
361 let parsed = parse_line(&line);
362 assert_eq!(parsed.kind, LineKind::Full);
363 assert_eq!(parsed.uuid, Some(UUID1));
364 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
365 assert_eq!(parsed.idle_pct, Some("95.97%"));
366 assert_eq!(parsed.level, Some(LogLevel::Debug));
367 assert_eq!(parsed.source, Some("sofia.c:100"));
368 assert_eq!(parsed.message, "Test message here");
369 }
370
371 #[test]
372 fn full_line_each_level() {
373 for (name, expected) in [
374 ("DEBUG", LogLevel::Debug),
375 ("INFO", LogLevel::Info),
376 ("NOTICE", LogLevel::Notice),
377 ("WARNING", LogLevel::Warning),
378 ("ERR", LogLevel::Err),
379 ("CRIT", LogLevel::Crit),
380 ("ALERT", LogLevel::Alert),
381 ("CONSOLE", LogLevel::Console),
382 ] {
383 let line =
384 format!("{UUID1} 2025-01-15 10:30:45.123456 95.97% [{name}] sofia.c:100 Test");
385 let parsed = parse_line(&line);
386 assert_eq!(parsed.kind, LineKind::Full);
387 assert_eq!(parsed.level, Some(expected), "failed for [{name}]");
388 }
389 }
390
391 #[test]
392 fn full_line_high_idle() {
393 let line =
394 format!("{UUID1} 2025-01-15 10:30:45.123456 99.99% [DEBUG] sofia.c:100 High idle");
395 let parsed = parse_line(&line);
396 assert_eq!(parsed.idle_pct, Some("99.99%"));
397 }
398
399 #[test]
400 fn full_line_low_idle() {
401 let line = format!("{UUID1} 2025-01-15 10:30:45.123456 0.00% [DEBUG] sofia.c:100 Low idle");
402 let parsed = parse_line(&line);
403 assert_eq!(parsed.idle_pct, Some("0.00%"));
404 }
405
406 #[test]
407 fn full_line_long_message() {
408 let line = format!(
409 "{UUID1} 2025-01-15 10:30:45.123456 95.97% [DEBUG] sofia.c:100 Channel [sofia/internal] key=val:123 (test) {{braces}}"
410 );
411 let parsed = parse_line(&line);
412 assert_eq!(
413 parsed.message,
414 "Channel [sofia/internal] key=val:123 (test) {braces}"
415 );
416 }
417
418 #[test]
421 fn system_line_no_uuid() {
422 let line =
423 "2025-01-15 10:30:45.123456 95.97% [INFO] mod_event_socket.c:1772 Event Socket command";
424 let parsed = parse_line(line);
425 assert_eq!(parsed.kind, LineKind::System);
426 assert_eq!(parsed.uuid, None);
427 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
428 assert_eq!(parsed.idle_pct, Some("95.97%"));
429 assert_eq!(parsed.level, Some(LogLevel::Info));
430 assert_eq!(parsed.source, Some("mod_event_socket.c:1772"));
431 assert_eq!(parsed.message, "Event Socket command");
432 }
433
434 #[test]
435 fn system_line_with_embedded_uuid() {
436 let line = format!(
437 "2025-01-15 10:30:45.123456 95.97% [DEBUG] switch_cpp.cpp:1466 {UUID1} DAA-LOG WaveManager PSAP 911 originate"
438 );
439 let parsed = parse_line(&line);
440 assert_eq!(parsed.kind, LineKind::System);
441 assert_eq!(parsed.uuid, Some(UUID1));
442 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
443 assert_eq!(parsed.level, Some(LogLevel::Debug));
444 assert_eq!(parsed.source, Some("switch_cpp.cpp:1466"));
445 assert_eq!(parsed.message, "DAA-LOG WaveManager PSAP 911 originate");
446 }
447
448 #[test]
449 fn system_line_with_embedded_uuid_empty_message() {
450 let line = format!("2025-01-15 10:30:45.123456 95.97% [INFO] switch_cpp.cpp:1466 {UUID1} ");
451 let parsed = parse_line(&line);
452 assert_eq!(parsed.kind, LineKind::System);
453 assert_eq!(parsed.uuid, Some(UUID1));
454 assert_eq!(parsed.message, "");
455 }
456
457 #[test]
458 fn system_line_without_embedded_uuid() {
459 let line =
460 "2025-01-15 10:30:45.123456 95.97% [INFO] mod_event_socket.c:1772 Event Socket command";
461 let parsed = parse_line(line);
462 assert_eq!(parsed.kind, LineKind::System);
463 assert_eq!(parsed.uuid, None);
464 assert_eq!(parsed.message, "Event Socket command");
465 }
466
467 #[test]
468 fn system_line_event_socket() {
469 let line = "2025-01-15 10:30:45.123456 95.97% [NOTICE] mod_logfile.c:217 New log started.";
470 let parsed = parse_line(line);
471 assert_eq!(parsed.kind, LineKind::System);
472 assert_eq!(parsed.level, Some(LogLevel::Notice));
473 assert_eq!(parsed.message, "New log started.");
474 }
475
476 #[test]
479 fn uuid_continuation_dialplan() {
480 let line =
481 format!("{UUID1} Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public]");
482 let parsed = parse_line(&line);
483 assert_eq!(parsed.kind, LineKind::UuidContinuation);
484 assert_eq!(parsed.uuid, Some(UUID1));
485 assert_eq!(parsed.timestamp, None);
486 assert_eq!(parsed.level, None);
487 assert_eq!(
488 parsed.message,
489 "Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public]"
490 );
491 }
492
493 #[test]
494 fn uuid_continuation_execute() {
495 let line =
496 format!("{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(foo=bar)");
497 let parsed = parse_line(&line);
498 assert_eq!(parsed.kind, LineKind::UuidContinuation);
499 assert_eq!(parsed.uuid, Some(UUID1));
500 assert_eq!(
501 parsed.message,
502 "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(foo=bar)"
503 );
504 }
505
506 #[test]
507 fn uuid_continuation_channel_var() {
508 let line = format!("{UUID1} Channel-State: [CS_EXECUTE]");
509 let parsed = parse_line(&line);
510 assert_eq!(parsed.kind, LineKind::UuidContinuation);
511 assert_eq!(parsed.uuid, Some(UUID1));
512 assert_eq!(parsed.message, "Channel-State: [CS_EXECUTE]");
513 }
514
515 #[test]
516 fn uuid_continuation_variable() {
517 let line = format!("{UUID1} variable_sip_call_id: [test123@192.0.2.1]");
518 let parsed = parse_line(&line);
519 assert_eq!(parsed.kind, LineKind::UuidContinuation);
520 assert_eq!(parsed.uuid, Some(UUID1));
521 assert_eq!(parsed.message, "variable_sip_call_id: [test123@192.0.2.1]");
522 }
523
524 #[test]
525 fn uuid_continuation_blank() {
526 let line = format!("{UUID1} ");
527 let parsed = parse_line(&line);
528 assert_eq!(parsed.kind, LineKind::UuidContinuation);
529 assert_eq!(parsed.uuid, Some(UUID1));
530 assert_eq!(parsed.message, "");
531 }
532
533 #[test]
536 fn bare_variable() {
537 let line = "variable_foo: [bar]";
538 let parsed = parse_line(line);
539 assert_eq!(parsed.kind, LineKind::BareContinuation);
540 assert_eq!(parsed.uuid, None);
541 assert_eq!(parsed.message, "variable_foo: [bar]");
542 }
543
544 #[test]
545 fn bare_sdp_origin() {
546 let line = "o=- 1234 5678 IN IP4 192.0.2.1";
547 let parsed = parse_line(line);
548 assert_eq!(parsed.kind, LineKind::BareContinuation);
549 assert_eq!(parsed.message, line);
550 }
551
552 #[test]
553 fn bare_sdp_media() {
554 let line = "m=audio 47758 RTP/AVP 0 101";
555 let parsed = parse_line(line);
556 assert_eq!(parsed.kind, LineKind::BareContinuation);
557 assert_eq!(parsed.message, line);
558 }
559
560 #[test]
561 fn bare_sdp_attribute() {
562 let line = "a=rtpmap:0 PCMU/8000";
563 let parsed = parse_line(line);
564 assert_eq!(parsed.kind, LineKind::BareContinuation);
565 assert_eq!(parsed.message, line);
566 }
567
568 #[test]
569 fn bare_closing_bracket() {
570 let line = "]";
571 let parsed = parse_line(line);
572 assert_eq!(parsed.kind, LineKind::BareContinuation);
573 assert_eq!(parsed.message, "]");
574 }
575
576 #[test]
577 fn bare_empty_line() {
578 let parsed = parse_line("");
579 assert_eq!(parsed.kind, LineKind::Empty);
580 assert_eq!(parsed.message, "");
581 }
582
583 #[test]
586 fn truncated_varia_prefix() {
587 let line = format!(
588 "varia{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
589 );
590 let parsed = parse_line(&line);
591 assert_eq!(parsed.kind, LineKind::Truncated);
592 assert_eq!(parsed.uuid, Some(UUID1));
593 assert_eq!(
594 parsed.message,
595 "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
596 );
597 }
598
599 #[test]
600 fn truncated_variab_prefix() {
601 let line = format!(
602 "variab{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
603 );
604 let parsed = parse_line(&line);
605 assert_eq!(parsed.kind, LineKind::Truncated);
606 assert_eq!(parsed.uuid, Some(UUID1));
607 }
608
609 #[test]
610 fn truncated_var_prefix() {
611 let line =
612 format!("var{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)");
613 let parsed = parse_line(&line);
614 assert_eq!(parsed.kind, LineKind::Truncated);
615 assert_eq!(parsed.uuid, Some(UUID1));
616 }
617
618 #[test]
619 fn truncated_variable_prefix() {
620 let line = format!(
621 "variable{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
622 );
623 let parsed = parse_line(&line);
624 assert_eq!(parsed.kind, LineKind::Truncated);
625 assert_eq!(parsed.uuid, Some(UUID1));
626 }
627
628 #[test]
631 fn log_header_with_idle_pct() {
632 let line = "2024-04-02 10:31:28.785679 98.03% [NOTICE] sofia.c:1114 Hangup";
633 assert!(is_log_header_at(line.as_bytes(), 0));
634 }
635
636 #[test]
637 fn log_header_no_idle_pct() {
638 let line = "2024-04-02 10:31:28.785679 [NOTICE] sofia.c:1114 Hangup";
640 assert!(is_log_header_at(line.as_bytes(), 0));
641 }
642
643 #[test]
644 fn log_header_no_idle_pct_at_offset() {
645 let line = "Session does not exist, aborting REFER.2024-04-02 10:31:28.785679 [WARNING] sofia_presence.c:4546 x";
646 let offset = line.find("2024").unwrap();
647 assert!(is_log_header_at(line.as_bytes(), offset));
648 }
649
650 #[test]
651 fn log_header_rejects_non_header() {
652 let line = "2024-04-02 not a real timestamp here";
653 assert!(!is_log_header_at(line.as_bytes(), 0));
654 }
655
656 #[test]
659 fn full_line_no_idle_pct() {
660 let line = format!(
661 "{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] switch_core_session.c:1744 Session 3178948 ended"
662 );
663 let parsed = parse_line(&line);
664 assert_eq!(parsed.kind, LineKind::Full);
665 assert_eq!(parsed.uuid, Some(UUID1));
666 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
667 assert_eq!(parsed.idle_pct, None);
668 assert_eq!(parsed.level, Some(LogLevel::Notice));
669 assert_eq!(parsed.source, Some("switch_core_session.c:1744"));
670 assert_eq!(parsed.message, "Session 3178948 ended");
671 }
672
673 #[test]
674 fn full_line_no_idle_pct_url_encoded_percent() {
675 let line = format!(
676 "{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] switch_core_session.c:1744 Session 3178948 (sofia/psap/gw%2Bsg1vofswb-inbound@198.51.100.5:5060) Ended"
677 );
678 let parsed = parse_line(&line);
679 assert_eq!(parsed.kind, LineKind::Full);
680 assert_eq!(parsed.uuid, Some(UUID1));
681 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
682 assert_eq!(parsed.idle_pct, None);
683 assert_eq!(parsed.level, Some(LogLevel::Notice));
684 assert_eq!(parsed.source, Some("switch_core_session.c:1744"));
685 assert_eq!(
686 parsed.message,
687 "Session 3178948 (sofia/psap/gw%2Bsg1vofswb-inbound@198.51.100.5:5060) Ended"
688 );
689 }
690
691 #[test]
692 fn system_line_no_idle_pct() {
693 let line = "2025-01-15 10:30:45.123456 [INFO] mod_event_socket.c:1772 Event Socket command";
694 let parsed = parse_line(line);
695 assert_eq!(parsed.kind, LineKind::System);
696 assert_eq!(parsed.uuid, None);
697 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
698 assert_eq!(parsed.idle_pct, None);
699 assert_eq!(parsed.level, Some(LogLevel::Info));
700 assert_eq!(parsed.source, Some("mod_event_socket.c:1772"));
701 assert_eq!(parsed.message, "Event Socket command");
702 }
703
704 #[test]
705 fn full_line_no_idle_pct_hangup_url_encoded() {
706 let line = format!(
707 "{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] sofia.c:1089 Hangup sofia/psap/gw%2Bgateway@198.51.100.5:5060 [CS_EXCHANGE_MEDIA] [CALL_AWARDED_DELIVERED]"
708 );
709 let parsed = parse_line(&line);
710 assert_eq!(parsed.kind, LineKind::Full);
711 assert_eq!(parsed.idle_pct, None);
712 assert_eq!(parsed.level, Some(LogLevel::Notice));
713 assert_eq!(parsed.source, Some("sofia.c:1089"));
714 assert_eq!(
715 parsed.message,
716 "Hangup sofia/psap/gw%2Bgateway@198.51.100.5:5060 [CS_EXCHANGE_MEDIA] [CALL_AWARDED_DELIVERED]"
717 );
718 }
719
720 #[test]
723 fn not_uuid_36_chars() {
724 let line = "this-is-not-a-valid-uuid-value-12345 rest of line";
725 let parsed = parse_line(line);
726 assert_eq!(parsed.kind, LineKind::BareContinuation);
727 assert_eq!(parsed.message, line);
728 }
729
730 #[test]
731 fn uuid_in_message_not_prefix() {
732 let line =
733 format!("This is some log message body with extra context then {UUID1} appears here");
734 let parsed = parse_line(&line);
735 assert_eq!(parsed.kind, LineKind::BareContinuation);
736 assert_eq!(parsed.message, line.as_str());
737 }
738
739 #[test]
740 fn whitespace_only_is_empty() {
741 let parsed = parse_line(" \t ");
742 assert_eq!(parsed.kind, LineKind::Empty);
743 }
744
745 #[test]
748 fn multibyte_straddling_timestamp_end_not_system() {
749 let line = "2025-01-15 10:30:45.12345é more content following here";
751 assert!(!line.is_char_boundary(26));
752 let parsed = parse_line(line);
753 assert_eq!(parsed.kind, LineKind::BareContinuation);
754 assert_eq!(parsed.timestamp, None);
755 assert_eq!(parsed.message, line);
756 }
757
758 #[test]
759 fn multibyte_after_timestamp_not_system() {
760 let line = "2025-01-15 10:30:45.123456é more content following here";
762 assert!(!line.is_char_boundary(27));
763 let parsed = parse_line(line);
764 assert_eq!(parsed.kind, LineKind::BareContinuation);
765 assert_eq!(parsed.timestamp, None);
766 assert_eq!(parsed.message, line);
767 }
768
769 #[test]
770 fn multibyte_after_uuid_timestamp_is_continuation() {
771 let line = format!("{UUID1} 2025-01-15 10:30:45.123456é more content");
772 let parsed = parse_line(&line);
773 assert_eq!(parsed.kind, LineKind::UuidContinuation);
774 assert_eq!(parsed.uuid, Some(UUID1));
775 assert_eq!(parsed.timestamp, None);
776 assert_eq!(parsed.message, "2025-01-15 10:30:45.123456é more content");
777 }
778
779 #[test]
780 fn multibyte_after_idle_pct_degrades() {
781 let line = "2025-01-15 10:30:45.123456 9%é[DEBUG] x";
783 let parsed = parse_line(line);
784 assert_eq!(parsed.kind, LineKind::System);
785 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
786 assert_eq!(parsed.idle_pct, None);
787 assert_eq!(parsed.level, None);
788 }
789
790 #[test]
791 fn multibyte_after_level_bracket_degrades() {
792 let line = "2025-01-15 10:30:45.123456 95.97% [DEBUG]éxx";
794 let parsed = parse_line(line);
795 assert_eq!(parsed.kind, LineKind::System);
796 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
797 assert_eq!(parsed.idle_pct, Some("95.97%"));
798 assert_eq!(parsed.level, Some(LogLevel::Debug));
799 assert_eq!(parsed.source, None);
800 assert_eq!(parsed.message, "");
801 }
802}