1use std::borrow::Cow;
4use std::future::Future;
5use std::io::Read as _;
6use std::num::NonZeroU64;
7use std::panic::AssertUnwindSafe;
8use std::path::Path;
9use std::string::FromUtf8Error;
10use std::sync::{Mutex as StdMutex, MutexGuard, PoisonError};
11use std::time::{Duration, Instant};
12
13use futures::FutureExt as _;
14use tokio::task::JoinHandle;
15
16use crate::config::SizeLimit;
17
18#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
39#[error("{size} bytes exceed the limit of {max} bytes")]
40pub struct SizeExceeded {
41 pub(crate) size: u64,
42 pub(crate) max: NonZeroU64,
43}
44
45impl SizeExceeded {
46 #[must_use]
48 pub const fn size(&self) -> u64 {
49 self.size
50 }
51
52 #[must_use]
54 pub const fn max(&self) -> NonZeroU64 {
55 self.max
56 }
57
58 pub const fn check(size: u64, max: NonZeroU64) -> Result<(), Self> {
64 if size > max.get() {
65 return Err(Self { size, max });
66 }
67 Ok(())
68 }
69
70 pub const fn check_limit(size: u64, limit: SizeLimit) -> Result<(), Self> {
76 match limit.get() {
77 Some(max) => Self::check(size, max),
78 None => Ok(()),
79 }
80 }
81}
82
83#[derive(thiserror::Error, Debug)]
85pub enum BoundedUtf8Error {
86 #[error(transparent)]
88 TooLarge(#[from] SizeExceeded),
89 #[error(transparent)]
91 InvalidUtf8(#[from] FromUtf8Error),
92}
93
94pub fn check_bounded_utf8(buf: Vec<u8>, max: SizeLimit) -> Result<String, BoundedUtf8Error> {
107 SizeExceeded::check_limit(buf.len() as u64, max)?;
108 Ok(String::from_utf8(buf)?)
109}
110
111#[derive(thiserror::Error, Debug)]
113pub enum OpenRegularFileError {
114 #[error(transparent)]
116 Io(#[from] std::io::Error),
117 #[error("not a regular file")]
119 NotRegular,
120}
121
122#[derive(thiserror::Error, Debug)]
124pub enum ReadBoundedError {
125 #[error(transparent)]
127 Io(std::io::Error),
128 #[error(transparent)]
131 TooLarge(SizeExceeded),
132}
133
134#[derive(thiserror::Error, Debug)]
136pub enum BoundedFileError {
137 #[error(transparent)]
139 Open(#[from] OpenRegularFileError),
140 #[error(transparent)]
142 Read(#[from] ReadBoundedError),
143}
144
145#[derive(Debug)]
156pub struct RegularFile {
157 file: std::fs::File,
158 metadata: std::fs::Metadata,
159}
160
161impl RegularFile {
162 pub fn open(path: &Path) -> Result<Self, OpenRegularFileError> {
169 let mut options = std::fs::OpenOptions::new();
170 options.read(true);
171 #[cfg(unix)]
172 {
173 use std::os::unix::fs::OpenOptionsExt as _;
174 options.custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY);
175 }
176 let file = options.open(path)?;
177 #[cfg(windows)]
179 if !winapi_util::file::typ(&file).is_ok_and(|file_type| file_type.is_disk()) {
180 return Err(OpenRegularFileError::NotRegular);
181 }
182 let metadata = file.metadata()?;
183 if !metadata.is_file() {
184 return Err(OpenRegularFileError::NotRegular);
185 }
186 Ok(Self { file, metadata })
187 }
188
189 #[must_use]
191 pub fn into_parts(self) -> (std::fs::File, std::fs::Metadata) {
192 (self.file, self.metadata)
193 }
194
195 pub fn read_bounded(self, max: NonZeroU64) -> Result<Vec<u8>, ReadBoundedError> {
205 SizeExceeded::check(self.metadata.len(), max).map_err(ReadBoundedError::TooLarge)?;
206 let mut buf = Vec::new();
207 self.file
208 .take(SizeLimit::read_cap_for(max))
209 .read_to_end(&mut buf)
210 .map_err(ReadBoundedError::Io)?;
211 SizeExceeded::check(buf.len() as u64, max).map_err(ReadBoundedError::TooLarge)?;
212 Ok(buf)
213 }
214}
215
216pub fn read_regular_file_bounded(
224 path: &Path,
225 max: NonZeroU64,
226) -> Result<Vec<u8>, BoundedFileError> {
227 Ok(RegularFile::open(path)?.read_bounded(max)?)
228}
229
230pub const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
244
245pub const TRUNCATION_MARKER: &str = "... (truncated)";
248
249pub const MAX_LOG_STRING_BYTES: usize = 200;
257
258pub fn truncate_str(s: &str, max_bytes: usize) -> String {
273 if s.len() <= max_bytes {
274 return s.to_string();
275 }
276 let cut = s.floor_char_boundary(max_bytes);
277 format!("{}{TRUNCATION_MARKER}", &s[..cut])
278}
279
280pub fn truncate_string(mut s: String, max_bytes: usize) -> String {
291 if s.len() <= max_bytes {
292 return s;
293 }
294 let cut = s.floor_char_boundary(max_bytes);
295 s.truncate(cut);
296 s.push_str(TRUNCATION_MARKER);
297 s
298}
299
300pub fn escape_bounded(text: &str, max_bytes: usize) -> String {
305 let mut bounded = String::with_capacity(text.len().min(max_bytes));
306 let mut buffer = [0; 4];
307 for c in text.chars() {
308 let escaped = escape_control(c.encode_utf8(&mut buffer));
309 if bounded.len().saturating_add(escaped.len()) > max_bytes {
310 bounded.push_str(TRUNCATION_MARKER);
311 break;
312 }
313 bounded.push_str(&escaped);
314 }
315 bounded
316}
317
318pub const fn is_deceptive_format_char(c: char) -> bool {
321 matches!(
322 c,
323 '\u{00AD}'
324 | '\u{061C}'
325 | '\u{180E}'
326 | '\u{200B}'..='\u{200F}'
327 | '\u{2028}'..='\u{202E}'
328 | '\u{2060}'..='\u{2064}'
329 | '\u{2066}'..='\u{206F}'
330 | '\u{FEFF}'
331 | '\u{FFF9}'..='\u{FFFB}'
332 | '\u{E0000}'..='\u{E007F}'
333 )
334}
335
336#[must_use]
346pub const fn needs_control_escape(c: char) -> bool {
347 c.is_control() || is_deceptive_format_char(c)
348}
349
350pub fn escape_control(s: &str) -> Cow<'_, str> {
365 if !s.chars().any(needs_control_escape) {
366 return Cow::Borrowed(s);
367 }
368 let mut escaped = String::with_capacity(s.len());
369 for c in s.chars() {
370 match c {
371 '\n' => escaped.push_str("\\n"),
372 '\r' => escaped.push_str("\\r"),
373 '\t' => escaped.push_str("\\t"),
374 c if needs_control_escape(c) => escaped.extend(c.escape_unicode()),
375 c => escaped.push(c),
376 }
377 }
378 Cow::Owned(escaped)
379}
380
381pub fn escape_control_owned(text: String) -> String {
384 match escape_control(&text) {
385 Cow::Borrowed(_) => text,
386 Cow::Owned(escaped) => escaped,
387 }
388}
389
390pub struct AbortOnDrop<'a, T>(pub(crate) &'a JoinHandle<T>);
395
396impl<T> Drop for AbortOnDrop<'_, T> {
397 fn drop(&mut self) {
398 self.0.abort();
399 }
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
404#[error("task panicked: {message}")]
405pub struct TaskPanicked {
406 message: String,
407}
408
409impl TaskPanicked {
410 #[must_use]
412 pub fn message(&self) -> &str {
413 &self.message
414 }
415
416 fn from_payload(payload: &(dyn std::any::Any + Send)) -> Self {
417 let message = payload
418 .downcast_ref::<&str>()
419 .copied()
420 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
421 .unwrap_or("non-string panic payload");
422 Self {
423 message: message.to_owned(),
424 }
425 }
426}
427
428pub async fn catch_panic<T>(fut: impl Future<Output = T>) -> Result<T, TaskPanicked> {
431 AssertUnwindSafe(fut)
432 .catch_unwind()
433 .await
434 .map_err(|payload| TaskPanicked::from_payload(payload.as_ref()))
435}
436
437#[derive(Debug, Default)]
443pub struct WarnLimiter {
444 last: Option<Instant>,
445}
446
447impl WarnLimiter {
448 pub const DEFAULT_PERIOD: Duration = Duration::from_mins(1);
450
451 #[must_use]
453 pub const fn new() -> Self {
454 Self { last: None }
455 }
456
457 pub fn due(&mut self, now: Instant, every: Duration) -> bool {
460 let due = self
461 .last
462 .is_none_or(|last| now.saturating_duration_since(last) > every);
463 if due {
464 self.last = Some(now);
465 }
466 due
467 }
468}
469
470pub fn lock_std<T>(mutex: &StdMutex<T>) -> MutexGuard<'_, T> {
479 mutex.lock().unwrap_or_else(PoisonError::into_inner)
480}
481
482#[cfg(test)]
483mod tests {
484 use std::assert_matches;
485
486 use super::*;
487
488 #[test]
489 fn warn_limiter_is_due_at_first_and_then_once_per_interval() {
490 let every = Duration::from_secs(60);
491 let start = Instant::now();
492 let mut limiter = WarnLimiter::default();
493 assert!(limiter.due(start, every));
494 assert!(!limiter.due(start + Duration::from_secs(1), every));
495 assert!(!limiter.due(start + every, every));
496 assert!(limiter.due(start + every + Duration::from_millis(1), every));
497 assert!(!limiter.due(start + every + Duration::from_secs(2), every));
498 }
499
500 #[tokio::test]
501 async fn catch_panic_passes_value_through() {
502 assert_eq!(catch_panic(async { 7 }).await, Ok(7));
503 }
504
505 #[tokio::test]
506 async fn catch_panic_reports_str_and_string_payloads() {
507 let from_str = catch_panic(async { panic!("static boom") }).await;
508 assert_eq!(from_str.unwrap_err().message(), "static boom");
509 let detail = 42;
510 let from_string = catch_panic(async { panic!("boom {detail}") }).await;
511 assert_eq!(from_string.unwrap_err().message(), "boom 42");
512 }
513
514 #[test]
515 fn lock_std_recovers_poisoned_mutex() {
516 let mutex = std::sync::Arc::new(StdMutex::new(1));
517 let poisoner = std::sync::Arc::clone(&mutex);
518 let _ = std::thread::spawn(move || {
519 let _guard = poisoner.lock().unwrap();
520 panic!("poison");
521 })
522 .join();
523 assert_eq!(*lock_std(&mutex), 1);
524 }
525
526 #[test]
527 fn read_regular_file_bounded_reads_within_limit() {
528 let dir = tempfile::tempdir().unwrap();
529 let path = dir.path().join("a.json");
530 std::fs::write(&path, b"{}").unwrap();
531 assert_eq!(read_regular_file_bounded(&path, limit(2)).unwrap(), b"{}");
532 let empty = dir.path().join("empty.json");
533 std::fs::write(&empty, b"").unwrap();
534 assert!(
535 read_regular_file_bounded(&empty, limit(2))
536 .unwrap()
537 .is_empty()
538 );
539 }
540
541 fn limit(max: u64) -> NonZeroU64 {
542 NonZeroU64::new(max).unwrap()
543 }
544
545 #[test]
546 fn regular_file_open_into_parts_returns_handle_metadata() {
547 let dir = tempfile::tempdir().unwrap();
548 let path = dir.path().join("a.txt");
549 std::fs::write(&path, b"abc").unwrap();
550 let (_handle, metadata) = RegularFile::open(&path).unwrap().into_parts();
551 assert!(metadata.is_file());
552 assert_eq!(metadata.len(), 3);
553 }
554
555 #[test]
556 fn regular_file_open_missing_path_is_io_not_found() {
557 let dir = tempfile::tempdir().unwrap();
558 assert_matches!(
559 RegularFile::open(&dir.path().join("missing")),
560 Err(OpenRegularFileError::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound
561 );
562 }
563
564 #[cfg(unix)]
565 #[test]
566 fn regular_file_open_rejects_directory_and_fifo_as_not_regular() {
567 let dir = tempfile::tempdir().unwrap();
568 assert_matches!(
569 RegularFile::open(dir.path()),
570 Err(OpenRegularFileError::NotRegular)
571 );
572 let fifo = dir.path().join("fifo");
573 crate::test_lsp::make_fifo(&fifo);
574 assert_matches!(
575 RegularFile::open(&fifo),
576 Err(OpenRegularFileError::NotRegular)
577 );
578 }
579
580 #[cfg(unix)]
581 #[test]
582 fn regular_file_open_follows_symlink_to_regular_file() {
583 let dir = tempfile::tempdir().unwrap();
584 let target = dir.path().join("target.txt");
585 std::fs::write(&target, b"abc").unwrap();
586 let link = dir.path().join("link.txt");
587 std::os::unix::fs::symlink(&target, &link).unwrap();
588 let (_handle, metadata) = RegularFile::open(&link).unwrap().into_parts();
589 assert!(metadata.is_file());
590 assert_eq!(metadata.len(), 3);
591 }
592
593 #[cfg(windows)]
594 #[test]
595 fn regular_file_open_rejects_nul_device_as_not_regular() {
596 assert_matches!(
597 RegularFile::open(Path::new("NUL")),
598 Err(OpenRegularFileError::NotRegular)
599 );
600 }
601
602 #[test]
603 fn read_regular_file_bounded_rejects_oversize() {
604 let dir = tempfile::tempdir().unwrap();
605 let path = dir.path().join("a.json");
606 std::fs::write(&path, b"{ }").unwrap();
607 assert_matches!(
608 read_regular_file_bounded(&path, limit(2)),
609 Err(BoundedFileError::Read(ReadBoundedError::TooLarge(SizeExceeded { size: 3, max }))) if max == limit(2)
610 );
611 }
612
613 #[test]
614 fn read_regular_file_bounded_rejects_directory() {
615 let dir = tempfile::tempdir().unwrap();
616 let result = read_regular_file_bounded(dir.path(), limit(10));
617 #[cfg(unix)]
618 assert_matches!(
619 result,
620 Err(BoundedFileError::Open(OpenRegularFileError::NotRegular))
621 );
622 #[cfg(windows)]
624 assert_matches!(result, Err(BoundedFileError::Open(OpenRegularFileError::Io(ref e))) if e.kind() == std::io::ErrorKind::PermissionDenied);
625 }
626
627 #[cfg(unix)]
628 #[test]
629 fn read_regular_file_bounded_does_not_block_on_fifo_or_symlink_to_it() {
630 let dir = tempfile::tempdir().unwrap();
631 let fifo = dir.path().join("fifo");
632 crate::test_lsp::make_fifo(&fifo);
633 let link = dir.path().join("link");
634 std::os::unix::fs::symlink(&fifo, &link).unwrap();
635
636 let (tx, rx) = std::sync::mpsc::channel();
637 std::thread::spawn(move || {
638 let outcomes = [fifo, link].map(|path| read_regular_file_bounded(&path, limit(10)));
639 tx.send(outcomes).unwrap();
640 });
641 let outcomes = rx
642 .recv_timeout(std::time::Duration::from_secs(5))
643 .unwrap_or_else(|_| panic!("reading a FIFO must not block"));
644 for outcome in outcomes {
645 assert_matches!(
646 outcome,
647 Err(BoundedFileError::Open(OpenRegularFileError::NotRegular))
648 );
649 }
650 }
651
652 #[test]
653 fn escape_control_borrows_clean_text() {
654 assert_matches!(escape_control("plain ✓"), Cow::Borrowed("plain ✓"));
655 }
656
657 #[test]
658 fn escape_control_escapes_newlines_and_escape_sequences() {
659 assert_eq!(
660 escape_control("a\nb\r\tc\x1b[31m"),
661 "a\\nb\\r\\tc\\u{1b}[31m"
662 );
663 }
664
665 #[test]
666 fn escape_control_escapes_line_separators_and_bidi_controls() {
667 assert_eq!(
668 escape_control("a\u{2028}b\u{2029}c\u{202E}d\u{2066}e\u{200F}f\u{061C}g"),
669 "a\\u{2028}b\\u{2029}c\\u{202e}d\\u{2066}e\\u{200f}f\\u{61c}g"
670 );
671 }
672
673 #[test]
674 fn escape_control_escapes_forged_log_line() {
675 assert_eq!(escape_control("ok\nERROR forged"), "ok\\nERROR forged");
676 }
677
678 #[test]
679 fn truncate_with_zero_budget_keeps_only_marker() {
680 assert_eq!(truncate_str("é", 0), TRUNCATION_MARKER);
681 assert_eq!(truncate_string("abc".to_owned(), 0), TRUNCATION_MARKER);
682 }
683
684 fn size(max: u64) -> SizeLimit {
685 SizeLimit::from_static(max)
686 }
687
688 #[test]
689 fn size_exceeded_exposes_both_numbers_through_accessors() {
690 let max = NonZeroU64::new(4).unwrap();
691 let exceeded = SizeExceeded::check(9, max).unwrap_err();
692
693 assert_eq!((exceeded.size(), exceeded.max()), (9, max));
694 }
695
696 #[test]
697 fn size_exceeded_check_admits_up_to_the_limit_and_reports_both_numbers() {
698 let max = NonZeroU64::new(4).unwrap();
699 assert_eq!(SizeExceeded::check(4, max), Ok(()));
700 assert_eq!(
701 SizeExceeded::check(5, max),
702 Err(SizeExceeded { size: 5, max })
703 );
704 assert_eq!(
705 SizeExceeded::check_limit(u64::MAX, SizeLimit::UNLIMITED),
706 Ok(())
707 );
708 assert_eq!(
709 SizeExceeded::check_limit(5, size(4)),
710 Err(SizeExceeded { size: 5, max })
711 );
712 assert_eq!(
713 SizeExceeded { size: 5, max }.to_string(),
714 "5 bytes exceed the limit of 4 bytes"
715 );
716 }
717
718 #[test]
719 fn check_bounded_utf8_within_limit() {
720 assert_eq!(
721 check_bounded_utf8(b"hello".to_vec(), size(10)).unwrap(),
722 "hello"
723 );
724 }
725
726 #[test]
727 fn check_bounded_utf8_too_large() {
728 assert_matches!(
729 check_bounded_utf8(b"hello".to_vec(), size(4)),
730 Err(BoundedUtf8Error::TooLarge(SizeExceeded { size: 5, max })) if max.get() == 4
731 );
732 }
733
734 #[test]
735 fn check_bounded_utf8_unlimited() {
736 let text = check_bounded_utf8(b"a".repeat(1000), SizeLimit::UNLIMITED).unwrap();
737 assert_eq!(text.len(), 1000);
738 }
739
740 #[test]
741 fn check_bounded_utf8_invalid_utf8_within_limit() {
742 assert_matches!(
743 check_bounded_utf8(vec![0xFF, 0xFE], size(10)),
744 Err(BoundedUtf8Error::InvalidUtf8(_))
745 );
746 }
747
748 #[test]
752 fn check_bounded_utf8_reports_oversized_before_invalid_utf8() {
753 let mut buf = "é".repeat(3).into_bytes();
754 buf.truncate(5);
755 assert_matches!(
756 check_bounded_utf8(buf, size(4)),
757 Err(BoundedUtf8Error::TooLarge(SizeExceeded { size: 5, .. }))
758 );
759 }
760
761 #[test]
762 fn no_truncation_at_or_below_limit() {
763 let exact = "a".repeat(10);
764 assert_eq!(truncate_str(&exact, 10), exact);
765 assert_eq!(truncate_str("", 10), "");
766 }
767
768 #[test]
769 fn truncates_just_above_limit() {
770 let message = "a".repeat(11);
771 assert_eq!(
772 truncate_str(&message, 10),
773 format!("{}... (truncated)", "a".repeat(10))
774 );
775 }
776
777 #[test]
778 fn handles_multibyte_char_boundary() {
779 let message = "é".repeat(10);
781 let truncated = truncate_str(&message, 5);
782 assert!(truncated.starts_with(&"é".repeat(2)));
783 assert!(truncated.ends_with("... (truncated)"));
784 }
785
786 #[test]
787 fn truncate_string_no_truncation_at_or_below_limit() {
788 let exact = "a".repeat(10);
789 assert_eq!(truncate_string(exact.clone(), 10), exact);
790 assert_eq!(truncate_string(String::new(), 10), "");
791 }
792
793 #[test]
794 fn truncate_string_truncates_just_above_limit() {
795 let message = "a".repeat(11);
796 assert_eq!(
797 truncate_string(message, 10),
798 format!("{}... (truncated)", "a".repeat(10))
799 );
800 }
801
802 #[test]
803 fn truncate_string_handles_multibyte_char_boundary() {
804 let message = "é".repeat(10);
805 let truncated = truncate_string(message, 5);
806 assert!(truncated.starts_with(&"é".repeat(2)));
807 assert!(truncated.ends_with("... (truncated)"));
808 }
809
810 #[test]
811 fn truncate_str_and_truncate_string_agree() {
812 let message = "x".repeat(500);
813 assert_eq!(truncate_str(&message, 100), truncate_string(message, 100));
814 }
815}