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