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
122impl OpenRegularFileError {
123 pub(crate) fn into_error(self, path: &Path) -> crate::Error {
129 match self {
130 Self::Io(source)
131 if cfg!(windows)
132 && source.kind() == std::io::ErrorKind::PermissionDenied
133 && path.is_dir() =>
134 {
135 crate::Error::NotARegularFile(path.to_path_buf())
136 }
137 Self::Io(source) => crate::Error::FileIo {
138 path: path.to_path_buf(),
139 source,
140 },
141 Self::NotRegular => crate::Error::NotARegularFile(path.to_path_buf()),
142 }
143 }
144}
145
146#[derive(thiserror::Error, Debug)]
148pub enum ReadBoundedError {
149 #[error(transparent)]
151 Io(std::io::Error),
152 #[error(transparent)]
155 TooLarge(SizeExceeded),
156}
157
158#[derive(thiserror::Error, Debug)]
160pub enum BoundedFileError {
161 #[error(transparent)]
163 Open(#[from] OpenRegularFileError),
164 #[error(transparent)]
166 Read(#[from] ReadBoundedError),
167}
168
169#[derive(Debug)]
180pub struct RegularFile {
181 file: std::fs::File,
182 metadata: std::fs::Metadata,
183}
184
185impl RegularFile {
186 pub fn open(path: &Path) -> Result<Self, OpenRegularFileError> {
193 let mut options = std::fs::OpenOptions::new();
194 options.read(true);
195 #[cfg(unix)]
196 {
197 use std::os::unix::fs::OpenOptionsExt as _;
198 options.custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY);
199 }
200 Self::from_file(options.open(path)?)
201 }
202
203 pub fn from_file(file: std::fs::File) -> Result<Self, OpenRegularFileError> {
213 #[cfg(windows)]
215 if !winapi_util::file::typ(&file).is_ok_and(|file_type| file_type.is_disk()) {
216 return Err(OpenRegularFileError::NotRegular);
217 }
218 let metadata = file.metadata()?;
219 if !metadata.is_file() {
220 return Err(OpenRegularFileError::NotRegular);
221 }
222 Ok(Self { file, metadata })
223 }
224
225 #[must_use]
227 pub fn into_parts(self) -> (std::fs::File, std::fs::Metadata) {
228 (self.file, self.metadata)
229 }
230
231 #[must_use]
233 pub(crate) const fn file(&self) -> &std::fs::File {
234 &self.file
235 }
236
237 pub fn read_bounded(self, max: NonZeroU64) -> Result<Vec<u8>, ReadBoundedError> {
247 SizeExceeded::check(self.metadata.len(), max).map_err(ReadBoundedError::TooLarge)?;
248 let mut buf = Vec::new();
249 self.file
250 .take(SizeLimit::read_cap_for(max))
251 .read_to_end(&mut buf)
252 .map_err(ReadBoundedError::Io)?;
253 SizeExceeded::check(buf.len() as u64, max).map_err(ReadBoundedError::TooLarge)?;
254 Ok(buf)
255 }
256}
257
258#[cfg(windows)]
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct FileIdentity {
267 volume: u64,
268 index: u64,
269}
270
271#[cfg(windows)]
272impl FileIdentity {
273 pub(crate) fn of_file(file: &std::fs::File) -> std::io::Result<Self> {
275 let info = winapi_util::file::information(file)?;
276 Ok(Self {
277 volume: info.volume_serial_number(),
278 index: info.file_index(),
279 })
280 }
281}
282
283pub fn read_regular_file_bounded(
291 path: &Path,
292 max: NonZeroU64,
293) -> Result<Vec<u8>, BoundedFileError> {
294 Ok(RegularFile::open(path)?.read_bounded(max)?)
295}
296
297pub const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
311
312pub const TRUNCATION_MARKER: &str = "... (truncated)";
315
316pub const MAX_LOG_STRING_BYTES: usize = 200;
324
325pub const MAX_ECHOED_NAME_BYTES: usize = 256;
328
329pub const MAX_ECHOED_PATH_BYTES: usize = 1024;
331
332pub fn truncate_str(s: &str, max_bytes: usize) -> String {
347 if s.len() <= max_bytes {
348 return s.to_string();
349 }
350 let cut = s.floor_char_boundary(max_bytes);
351 format!("{}{TRUNCATION_MARKER}", &s[..cut])
352}
353
354pub fn truncate_string(mut s: String, max_bytes: usize) -> String {
365 if s.len() <= max_bytes {
366 return s;
367 }
368 let cut = s.floor_char_boundary(max_bytes);
369 s.truncate(cut);
370 s.push_str(TRUNCATION_MARKER);
371 s
372}
373
374pub fn escape_bounded(text: &str, max_bytes: usize) -> String {
379 let mut bounded = String::with_capacity(text.len().min(max_bytes));
380 let mut buffer = [0; 4];
381 for c in text.chars() {
382 let escaped = escape_control(c.encode_utf8(&mut buffer));
383 if bounded.len().saturating_add(escaped.len()) > max_bytes {
384 bounded.push_str(TRUNCATION_MARKER);
385 break;
386 }
387 bounded.push_str(&escaped);
388 }
389 bounded
390}
391
392pub const fn is_deceptive_format_char(c: char) -> bool {
395 matches!(
396 c,
397 '\u{00AD}'
398 | '\u{061C}'
399 | '\u{180E}'
400 | '\u{200B}'..='\u{200F}'
401 | '\u{2028}'..='\u{202E}'
402 | '\u{2060}'..='\u{2064}'
403 | '\u{2066}'..='\u{206F}'
404 | '\u{FEFF}'
405 | '\u{FFF9}'..='\u{FFFB}'
406 | '\u{E0000}'..='\u{E007F}'
407 )
408}
409
410#[must_use]
420pub const fn needs_control_escape(c: char) -> bool {
421 c.is_control() || is_deceptive_format_char(c)
422}
423
424pub fn escape_control(s: &str) -> Cow<'_, str> {
439 if !s.chars().any(needs_control_escape) {
440 return Cow::Borrowed(s);
441 }
442 let mut escaped = String::with_capacity(s.len());
443 for c in s.chars() {
444 match c {
445 '\n' => escaped.push_str("\\n"),
446 '\r' => escaped.push_str("\\r"),
447 '\t' => escaped.push_str("\\t"),
448 c if needs_control_escape(c) => escaped.extend(c.escape_unicode()),
449 c => escaped.push(c),
450 }
451 }
452 Cow::Owned(escaped)
453}
454
455pub fn escape_control_owned(text: String) -> String {
458 match escape_control(&text) {
459 Cow::Borrowed(_) => text,
460 Cow::Owned(escaped) => escaped,
461 }
462}
463
464pub struct AbortOnDrop<'a, T>(pub(crate) &'a JoinHandle<T>);
469
470impl<T> Drop for AbortOnDrop<'_, T> {
471 fn drop(&mut self) {
472 self.0.abort();
473 }
474}
475
476#[must_use = "dropping the future detaches the task; the work still runs"]
492pub fn run_detached<T: Send + 'static>(
493 work: impl Future<Output = T> + Send + 'static,
494) -> impl Future<Output = Result<T, tokio::task::JoinError>> + Send {
495 let handle = tokio::spawn(work);
496 async move {
497 match handle.await {
498 Ok(value) => Ok(value),
499 Err(error) => match error.try_into_panic() {
500 Ok(payload) => std::panic::resume_unwind(payload),
501 Err(cancelled) => Err(cancelled),
502 },
503 }
504 }
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
509#[error("task panicked: {message}")]
510pub struct TaskPanicked {
511 message: String,
512}
513
514impl TaskPanicked {
515 #[must_use]
517 pub fn message(&self) -> &str {
518 &self.message
519 }
520
521 fn from_payload(payload: &(dyn std::any::Any + Send)) -> Self {
522 let message = payload
523 .downcast_ref::<&str>()
524 .copied()
525 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
526 .unwrap_or("non-string panic payload");
527 Self {
528 message: message.to_owned(),
529 }
530 }
531}
532
533pub async fn catch_panic<T>(fut: impl Future<Output = T>) -> Result<T, TaskPanicked> {
536 AssertUnwindSafe(fut)
537 .catch_unwind()
538 .await
539 .map_err(|payload| TaskPanicked::from_payload(payload.as_ref()))
540}
541
542#[derive(Debug, Default)]
548pub struct WarnLimiter {
549 last: Option<Instant>,
550}
551
552impl WarnLimiter {
553 pub const DEFAULT_PERIOD: Duration = Duration::from_mins(1);
555
556 #[must_use]
558 pub const fn new() -> Self {
559 Self { last: None }
560 }
561
562 pub fn due(&mut self, now: Instant, every: Duration) -> bool {
565 let due = self
566 .last
567 .is_none_or(|last| now.saturating_duration_since(last) > every);
568 if due {
569 self.last = Some(now);
570 }
571 due
572 }
573}
574
575pub fn lock_std<T>(mutex: &StdMutex<T>) -> MutexGuard<'_, T> {
584 mutex.lock().unwrap_or_else(PoisonError::into_inner)
585}
586
587#[cfg(test)]
588mod tests {
589 use std::assert_matches;
590
591 use super::*;
592
593 #[test]
594 fn warn_limiter_is_due_at_first_and_then_once_per_interval() {
595 let every = Duration::from_secs(60);
596 let start = Instant::now();
597 let mut limiter = WarnLimiter::default();
598 assert!(limiter.due(start, every));
599 assert!(!limiter.due(start + Duration::from_secs(1), every));
600 assert!(!limiter.due(start + every, every));
601 assert!(limiter.due(start + every + Duration::from_millis(1), every));
602 assert!(!limiter.due(start + every + Duration::from_secs(2), every));
603 }
604
605 #[tokio::test]
606 async fn run_detached_spawns_eagerly_and_survives_a_dropped_caller() {
607 let (done_tx, done_rx) = tokio::sync::oneshot::channel();
608 let pending = run_detached(async move {
609 tokio::task::yield_now().await;
610 let _ = done_tx.send(());
611 });
612 drop(pending);
613 assert_eq!(done_rx.await, Ok(()));
614 }
615
616 #[tokio::test]
617 async fn run_detached_returns_the_value() {
618 assert_eq!(run_detached(async { 3 }).await.ok(), Some(3));
619 }
620
621 #[tokio::test]
622 async fn catch_panic_passes_value_through() {
623 assert_eq!(catch_panic(async { 7 }).await, Ok(7));
624 }
625
626 #[tokio::test]
627 async fn catch_panic_reports_str_and_string_payloads() {
628 let from_str = catch_panic(async { panic!("static boom") }).await;
629 assert_eq!(from_str.unwrap_err().message(), "static boom");
630 let detail = 42;
631 let from_string = catch_panic(async { panic!("boom {detail}") }).await;
632 assert_eq!(from_string.unwrap_err().message(), "boom 42");
633 }
634
635 #[test]
636 fn lock_std_recovers_poisoned_mutex() {
637 let mutex = std::sync::Arc::new(StdMutex::new(1));
638 let poisoner = std::sync::Arc::clone(&mutex);
639 let _ = std::thread::spawn(move || {
640 let _guard = poisoner.lock().unwrap();
641 panic!("poison");
642 })
643 .join();
644 assert_eq!(*lock_std(&mutex), 1);
645 }
646
647 #[test]
648 fn read_regular_file_bounded_reads_within_limit() {
649 let dir = tempfile::tempdir().unwrap();
650 let path = dir.path().join("a.json");
651 std::fs::write(&path, b"{}").unwrap();
652 assert_eq!(read_regular_file_bounded(&path, limit(2)).unwrap(), b"{}");
653 let empty = dir.path().join("empty.json");
654 std::fs::write(&empty, b"").unwrap();
655 assert_eq!(
656 read_regular_file_bounded(&empty, limit(2)).unwrap().len(),
657 0
658 );
659 }
660
661 fn limit(max: u64) -> NonZeroU64 {
662 NonZeroU64::new(max).unwrap()
663 }
664
665 #[test]
666 fn regular_file_open_into_parts_returns_handle_metadata() {
667 let dir = tempfile::tempdir().unwrap();
668 let path = dir.path().join("a.txt");
669 std::fs::write(&path, b"abc").unwrap();
670 let (_handle, metadata) = RegularFile::open(&path).unwrap().into_parts();
671 assert!(metadata.is_file());
672 assert_eq!(metadata.len(), 3);
673 }
674
675 #[test]
676 fn regular_file_open_missing_path_is_io_not_found() {
677 let dir = tempfile::tempdir().unwrap();
678 assert_matches!(
679 RegularFile::open(&dir.path().join("missing")),
680 Err(OpenRegularFileError::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound
681 );
682 }
683
684 #[cfg(unix)]
685 #[test]
686 fn regular_file_open_rejects_directory_and_fifo_as_not_regular() {
687 let dir = tempfile::tempdir().unwrap();
688 assert_matches!(
689 RegularFile::open(dir.path()),
690 Err(OpenRegularFileError::NotRegular)
691 );
692 let fifo = dir.path().join("fifo");
693 crate::test_lsp::make_fifo(&fifo);
694 assert_matches!(
695 RegularFile::open(&fifo),
696 Err(OpenRegularFileError::NotRegular)
697 );
698 }
699
700 #[cfg(unix)]
701 #[test]
702 fn regular_file_open_follows_symlink_to_regular_file() {
703 let dir = tempfile::tempdir().unwrap();
704 let target = dir.path().join("target.txt");
705 std::fs::write(&target, b"abc").unwrap();
706 let link = dir.path().join("link.txt");
707 std::os::unix::fs::symlink(&target, &link).unwrap();
708 let (_handle, metadata) = RegularFile::open(&link).unwrap().into_parts();
709 assert!(metadata.is_file());
710 assert_eq!(metadata.len(), 3);
711 }
712
713 #[cfg(windows)]
714 #[test]
715 fn regular_file_open_rejects_nul_device_as_not_regular() {
716 assert_matches!(
717 RegularFile::open(Path::new("NUL")),
718 Err(OpenRegularFileError::NotRegular)
719 );
720 }
721
722 #[test]
723 fn read_regular_file_bounded_rejects_oversize() {
724 let dir = tempfile::tempdir().unwrap();
725 let path = dir.path().join("a.json");
726 std::fs::write(&path, b"{ }").unwrap();
727 assert_matches!(
728 read_regular_file_bounded(&path, limit(2)),
729 Err(BoundedFileError::Read(ReadBoundedError::TooLarge(SizeExceeded { size: 3, max }))) if max == limit(2)
730 );
731 }
732
733 #[test]
734 fn read_regular_file_bounded_rejects_directory() {
735 let dir = tempfile::tempdir().unwrap();
736 let result = read_regular_file_bounded(dir.path(), limit(10));
737 #[cfg(unix)]
738 assert_matches!(
739 result,
740 Err(BoundedFileError::Open(OpenRegularFileError::NotRegular))
741 );
742 #[cfg(windows)]
744 assert_matches!(result, Err(BoundedFileError::Open(OpenRegularFileError::Io(ref e))) if e.kind() == std::io::ErrorKind::PermissionDenied);
745 }
746
747 #[cfg(unix)]
748 #[test]
749 fn read_regular_file_bounded_does_not_block_on_fifo_or_symlink_to_it() {
750 let dir = tempfile::tempdir().unwrap();
751 let fifo = dir.path().join("fifo");
752 crate::test_lsp::make_fifo(&fifo);
753 let link = dir.path().join("link");
754 std::os::unix::fs::symlink(&fifo, &link).unwrap();
755
756 let (tx, rx) = std::sync::mpsc::channel();
757 std::thread::spawn(move || {
758 let outcomes = [fifo, link].map(|path| read_regular_file_bounded(&path, limit(10)));
759 tx.send(outcomes).unwrap();
760 });
761 let outcomes = rx
762 .recv_timeout(std::time::Duration::from_secs(5))
763 .unwrap_or_else(|_| panic!("reading a FIFO must not block"));
764 for outcome in outcomes {
765 assert_matches!(
766 outcome,
767 Err(BoundedFileError::Open(OpenRegularFileError::NotRegular))
768 );
769 }
770 }
771
772 #[test]
773 fn escape_control_borrows_clean_text() {
774 assert_matches!(escape_control("plain ✓"), Cow::Borrowed("plain ✓"));
775 }
776
777 #[test]
778 fn escape_control_escapes_newlines_and_escape_sequences() {
779 assert_eq!(
780 escape_control("a\nb\r\tc\x1b[31m"),
781 "a\\nb\\r\\tc\\u{1b}[31m"
782 );
783 }
784
785 #[test]
786 fn escape_control_escapes_line_separators_and_bidi_controls() {
787 assert_eq!(
788 escape_control("a\u{2028}b\u{2029}c\u{202E}d\u{2066}e\u{200F}f\u{061C}g"),
789 "a\\u{2028}b\\u{2029}c\\u{202e}d\\u{2066}e\\u{200f}f\\u{61c}g"
790 );
791 }
792
793 #[test]
794 fn escape_control_escapes_forged_log_line() {
795 assert_eq!(escape_control("ok\nERROR forged"), "ok\\nERROR forged");
796 }
797
798 #[test]
799 fn truncate_with_zero_budget_keeps_only_marker() {
800 assert_eq!(truncate_str("é", 0), TRUNCATION_MARKER);
801 assert_eq!(truncate_string("abc".to_owned(), 0), TRUNCATION_MARKER);
802 }
803
804 fn size(max: u64) -> SizeLimit {
805 SizeLimit::from_static(max)
806 }
807
808 #[test]
809 fn size_exceeded_exposes_both_numbers_through_accessors() {
810 let max = NonZeroU64::new(4).unwrap();
811 let exceeded = SizeExceeded::check(9, max).unwrap_err();
812
813 assert_eq!((exceeded.size(), exceeded.max()), (9, max));
814 }
815
816 #[test]
817 fn size_exceeded_check_admits_up_to_the_limit_and_reports_both_numbers() {
818 let max = NonZeroU64::new(4).unwrap();
819 assert_eq!(SizeExceeded::check(4, max), Ok(()));
820 assert_eq!(
821 SizeExceeded::check(5, max),
822 Err(SizeExceeded { size: 5, max })
823 );
824 assert_eq!(
825 SizeExceeded::check_limit(u64::MAX, SizeLimit::UNLIMITED),
826 Ok(())
827 );
828 assert_eq!(
829 SizeExceeded::check_limit(5, size(4)),
830 Err(SizeExceeded { size: 5, max })
831 );
832 assert_eq!(
833 SizeExceeded { size: 5, max }.to_string(),
834 "5 bytes exceed the limit of 4 bytes"
835 );
836 }
837
838 #[test]
839 fn check_bounded_utf8_within_limit() {
840 assert_eq!(
841 check_bounded_utf8(b"hello".to_vec(), size(10)).unwrap(),
842 "hello"
843 );
844 }
845
846 #[test]
847 fn check_bounded_utf8_too_large() {
848 assert_matches!(
849 check_bounded_utf8(b"hello".to_vec(), size(4)),
850 Err(BoundedUtf8Error::TooLarge(SizeExceeded { size: 5, max })) if max.get() == 4
851 );
852 }
853
854 #[test]
855 fn check_bounded_utf8_unlimited() {
856 let text = check_bounded_utf8(b"a".repeat(1000), SizeLimit::UNLIMITED).unwrap();
857 assert_eq!(text.len(), 1000);
858 }
859
860 #[test]
861 fn check_bounded_utf8_invalid_utf8_within_limit() {
862 assert_matches!(
863 check_bounded_utf8(vec![0xFF, 0xFE], size(10)),
864 Err(BoundedUtf8Error::InvalidUtf8(_))
865 );
866 }
867
868 #[test]
872 fn check_bounded_utf8_reports_oversized_before_invalid_utf8() {
873 let mut buf = "é".repeat(3).into_bytes();
874 buf.truncate(5);
875 assert_matches!(
876 check_bounded_utf8(buf, size(4)),
877 Err(BoundedUtf8Error::TooLarge(SizeExceeded { size: 5, .. }))
878 );
879 }
880
881 #[test]
882 fn no_truncation_at_or_below_limit() {
883 let exact = "a".repeat(10);
884 assert_eq!(truncate_str(&exact, 10), exact);
885 assert_eq!(truncate_str("", 10), "");
886 }
887
888 #[test]
889 fn truncates_just_above_limit() {
890 let message = "a".repeat(11);
891 assert_eq!(
892 truncate_str(&message, 10),
893 format!("{}... (truncated)", "a".repeat(10))
894 );
895 }
896
897 #[test]
898 fn handles_multibyte_char_boundary() {
899 let message = "é".repeat(10);
901 let truncated = truncate_str(&message, 5);
902 assert!(truncated.starts_with(&"é".repeat(2)));
903 assert!(truncated.ends_with("... (truncated)"));
904 }
905
906 #[test]
907 fn truncate_string_no_truncation_at_or_below_limit() {
908 let exact = "a".repeat(10);
909 assert_eq!(truncate_string(exact.clone(), 10), exact);
910 assert_eq!(truncate_string(String::new(), 10), "");
911 }
912
913 #[test]
914 fn truncate_string_truncates_just_above_limit() {
915 let message = "a".repeat(11);
916 assert_eq!(
917 truncate_string(message, 10),
918 format!("{}... (truncated)", "a".repeat(10))
919 );
920 }
921
922 #[test]
923 fn truncate_string_handles_multibyte_char_boundary() {
924 let message = "é".repeat(10);
925 let truncated = truncate_string(message, 5);
926 assert!(truncated.starts_with(&"é".repeat(2)));
927 assert!(truncated.ends_with("... (truncated)"));
928 }
929
930 #[test]
931 fn truncate_str_and_truncate_string_agree() {
932 let message = "x".repeat(500);
933 assert_eq!(truncate_str(&message, 100), truncate_string(message, 100));
934 }
935}