use std::borrow::Cow;
use std::future::Future;
use std::io::Read as _;
use std::num::NonZeroU64;
use std::panic::AssertUnwindSafe;
use std::path::Path;
use std::string::FromUtf8Error;
use std::sync::{Mutex as StdMutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant};
use futures::FutureExt as _;
use tokio::task::JoinHandle;
use crate::config::SizeLimit;
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("{size} bytes exceed the limit of {max} bytes")]
pub struct SizeExceeded {
pub(crate) size: u64,
pub(crate) max: NonZeroU64,
}
impl SizeExceeded {
#[must_use]
pub const fn size(&self) -> u64 {
self.size
}
#[must_use]
pub const fn max(&self) -> NonZeroU64 {
self.max
}
pub const fn check(size: u64, max: NonZeroU64) -> Result<(), Self> {
if size > max.get() {
return Err(Self { size, max });
}
Ok(())
}
pub const fn check_limit(size: u64, limit: SizeLimit) -> Result<(), Self> {
match limit.get() {
Some(max) => Self::check(size, max),
None => Ok(()),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum BoundedUtf8Error {
#[error(transparent)]
TooLarge(#[from] SizeExceeded),
#[error(transparent)]
InvalidUtf8(#[from] FromUtf8Error),
}
pub fn check_bounded_utf8(buf: Vec<u8>, max: SizeLimit) -> Result<String, BoundedUtf8Error> {
SizeExceeded::check_limit(buf.len() as u64, max)?;
Ok(String::from_utf8(buf)?)
}
#[derive(thiserror::Error, Debug)]
pub enum OpenRegularFileError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("not a regular file")]
NotRegular,
}
impl OpenRegularFileError {
pub(crate) fn into_error(self, path: &Path) -> crate::Error {
match self {
Self::Io(source)
if cfg!(windows)
&& source.kind() == std::io::ErrorKind::PermissionDenied
&& path.is_dir() =>
{
crate::Error::NotARegularFile(path.to_path_buf())
}
Self::Io(source) => crate::Error::FileIo {
path: path.to_path_buf(),
source,
},
Self::NotRegular => crate::Error::NotARegularFile(path.to_path_buf()),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ReadBoundedError {
#[error(transparent)]
Io(std::io::Error),
#[error(transparent)]
TooLarge(SizeExceeded),
}
#[derive(thiserror::Error, Debug)]
pub enum BoundedFileError {
#[error(transparent)]
Open(#[from] OpenRegularFileError),
#[error(transparent)]
Read(#[from] ReadBoundedError),
}
#[derive(Debug)]
pub struct RegularFile {
file: std::fs::File,
metadata: std::fs::Metadata,
}
impl RegularFile {
pub fn open(path: &Path) -> Result<Self, OpenRegularFileError> {
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY);
}
Self::from_file(options.open(path)?)
}
pub fn from_file(file: std::fs::File) -> Result<Self, OpenRegularFileError> {
#[cfg(windows)]
if !winapi_util::file::typ(&file).is_ok_and(|file_type| file_type.is_disk()) {
return Err(OpenRegularFileError::NotRegular);
}
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(OpenRegularFileError::NotRegular);
}
Ok(Self { file, metadata })
}
#[must_use]
pub fn into_parts(self) -> (std::fs::File, std::fs::Metadata) {
(self.file, self.metadata)
}
#[must_use]
pub(crate) const fn file(&self) -> &std::fs::File {
&self.file
}
pub fn read_bounded(self, max: NonZeroU64) -> Result<Vec<u8>, ReadBoundedError> {
SizeExceeded::check(self.metadata.len(), max).map_err(ReadBoundedError::TooLarge)?;
let mut buf = Vec::new();
self.file
.take(SizeLimit::read_cap_for(max))
.read_to_end(&mut buf)
.map_err(ReadBoundedError::Io)?;
SizeExceeded::check(buf.len() as u64, max).map_err(ReadBoundedError::TooLarge)?;
Ok(buf)
}
}
#[cfg(windows)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileIdentity {
volume: u64,
index: u64,
}
#[cfg(windows)]
impl FileIdentity {
pub(crate) fn of_file(file: &std::fs::File) -> std::io::Result<Self> {
let info = winapi_util::file::information(file)?;
Ok(Self {
volume: info.volume_serial_number(),
index: info.file_index(),
})
}
}
pub fn read_regular_file_bounded(
path: &Path,
max: NonZeroU64,
) -> Result<Vec<u8>, BoundedFileError> {
Ok(RegularFile::open(path)?.read_bounded(max)?)
}
pub const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
pub const TRUNCATION_MARKER: &str = "... (truncated)";
pub const MAX_LOG_STRING_BYTES: usize = 200;
pub const MAX_ECHOED_NAME_BYTES: usize = 256;
pub const MAX_ECHOED_PATH_BYTES: usize = 1024;
pub fn truncate_str(s: &str, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s.to_string();
}
let cut = s.floor_char_boundary(max_bytes);
format!("{}{TRUNCATION_MARKER}", &s[..cut])
}
pub fn truncate_string(mut s: String, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s;
}
let cut = s.floor_char_boundary(max_bytes);
s.truncate(cut);
s.push_str(TRUNCATION_MARKER);
s
}
pub fn escape_bounded(text: &str, max_bytes: usize) -> String {
let mut bounded = String::with_capacity(text.len().min(max_bytes));
let mut buffer = [0; 4];
for c in text.chars() {
let escaped = escape_control(c.encode_utf8(&mut buffer));
if bounded.len().saturating_add(escaped.len()) > max_bytes {
bounded.push_str(TRUNCATION_MARKER);
break;
}
bounded.push_str(&escaped);
}
bounded
}
pub const fn is_deceptive_format_char(c: char) -> bool {
matches!(
c,
'\u{00AD}'
| '\u{061C}'
| '\u{180E}'
| '\u{200B}'..='\u{200F}'
| '\u{2028}'..='\u{202E}'
| '\u{2060}'..='\u{2064}'
| '\u{2066}'..='\u{206F}'
| '\u{FEFF}'
| '\u{FFF9}'..='\u{FFFB}'
| '\u{E0000}'..='\u{E007F}'
)
}
#[must_use]
pub const fn needs_control_escape(c: char) -> bool {
c.is_control() || is_deceptive_format_char(c)
}
pub fn escape_control(s: &str) -> Cow<'_, str> {
if !s.chars().any(needs_control_escape) {
return Cow::Borrowed(s);
}
let mut escaped = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c if needs_control_escape(c) => escaped.extend(c.escape_unicode()),
c => escaped.push(c),
}
}
Cow::Owned(escaped)
}
pub fn escape_control_owned(text: String) -> String {
match escape_control(&text) {
Cow::Borrowed(_) => text,
Cow::Owned(escaped) => escaped,
}
}
pub struct AbortOnDrop<'a, T>(pub(crate) &'a JoinHandle<T>);
impl<T> Drop for AbortOnDrop<'_, T> {
fn drop(&mut self) {
self.0.abort();
}
}
#[must_use = "dropping the future detaches the task; the work still runs"]
pub fn run_detached<T: Send + 'static>(
work: impl Future<Output = T> + Send + 'static,
) -> impl Future<Output = Result<T, tokio::task::JoinError>> + Send {
let handle = tokio::spawn(work);
async move {
match handle.await {
Ok(value) => Ok(value),
Err(error) => match error.try_into_panic() {
Ok(payload) => std::panic::resume_unwind(payload),
Err(cancelled) => Err(cancelled),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("task panicked: {message}")]
pub struct TaskPanicked {
message: String,
}
impl TaskPanicked {
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
fn from_payload(payload: &(dyn std::any::Any + Send)) -> Self {
let message = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("non-string panic payload");
Self {
message: message.to_owned(),
}
}
}
pub async fn catch_panic<T>(fut: impl Future<Output = T>) -> Result<T, TaskPanicked> {
AssertUnwindSafe(fut)
.catch_unwind()
.await
.map_err(|payload| TaskPanicked::from_payload(payload.as_ref()))
}
#[derive(Debug, Default)]
pub struct WarnLimiter {
last: Option<Instant>,
}
impl WarnLimiter {
pub const DEFAULT_PERIOD: Duration = Duration::from_mins(1);
#[must_use]
pub const fn new() -> Self {
Self { last: None }
}
pub fn due(&mut self, now: Instant, every: Duration) -> bool {
let due = self
.last
.is_none_or(|last| now.saturating_duration_since(last) > every);
if due {
self.last = Some(now);
}
due
}
}
pub fn lock_std<T>(mutex: &StdMutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::*;
#[test]
fn warn_limiter_is_due_at_first_and_then_once_per_interval() {
let every = Duration::from_secs(60);
let start = Instant::now();
let mut limiter = WarnLimiter::default();
assert!(limiter.due(start, every));
assert!(!limiter.due(start + Duration::from_secs(1), every));
assert!(!limiter.due(start + every, every));
assert!(limiter.due(start + every + Duration::from_millis(1), every));
assert!(!limiter.due(start + every + Duration::from_secs(2), every));
}
#[tokio::test]
async fn run_detached_spawns_eagerly_and_survives_a_dropped_caller() {
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
let pending = run_detached(async move {
tokio::task::yield_now().await;
let _ = done_tx.send(());
});
drop(pending);
assert_eq!(done_rx.await, Ok(()));
}
#[tokio::test]
async fn run_detached_returns_the_value() {
assert_eq!(run_detached(async { 3 }).await.ok(), Some(3));
}
#[tokio::test]
async fn catch_panic_passes_value_through() {
assert_eq!(catch_panic(async { 7 }).await, Ok(7));
}
#[tokio::test]
async fn catch_panic_reports_str_and_string_payloads() {
let from_str = catch_panic(async { panic!("static boom") }).await;
assert_eq!(from_str.unwrap_err().message(), "static boom");
let detail = 42;
let from_string = catch_panic(async { panic!("boom {detail}") }).await;
assert_eq!(from_string.unwrap_err().message(), "boom 42");
}
#[test]
fn lock_std_recovers_poisoned_mutex() {
let mutex = std::sync::Arc::new(StdMutex::new(1));
let poisoner = std::sync::Arc::clone(&mutex);
let _ = std::thread::spawn(move || {
let _guard = poisoner.lock().unwrap();
panic!("poison");
})
.join();
assert_eq!(*lock_std(&mutex), 1);
}
#[test]
fn read_regular_file_bounded_reads_within_limit() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.json");
std::fs::write(&path, b"{}").unwrap();
assert_eq!(read_regular_file_bounded(&path, limit(2)).unwrap(), b"{}");
let empty = dir.path().join("empty.json");
std::fs::write(&empty, b"").unwrap();
assert_eq!(
read_regular_file_bounded(&empty, limit(2)).unwrap().len(),
0
);
}
fn limit(max: u64) -> NonZeroU64 {
NonZeroU64::new(max).unwrap()
}
#[test]
fn regular_file_open_into_parts_returns_handle_metadata() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.txt");
std::fs::write(&path, b"abc").unwrap();
let (_handle, metadata) = RegularFile::open(&path).unwrap().into_parts();
assert!(metadata.is_file());
assert_eq!(metadata.len(), 3);
}
#[test]
fn regular_file_open_missing_path_is_io_not_found() {
let dir = tempfile::tempdir().unwrap();
assert_matches!(
RegularFile::open(&dir.path().join("missing")),
Err(OpenRegularFileError::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound
);
}
#[cfg(unix)]
#[test]
fn regular_file_open_rejects_directory_and_fifo_as_not_regular() {
let dir = tempfile::tempdir().unwrap();
assert_matches!(
RegularFile::open(dir.path()),
Err(OpenRegularFileError::NotRegular)
);
let fifo = dir.path().join("fifo");
crate::test_lsp::make_fifo(&fifo);
assert_matches!(
RegularFile::open(&fifo),
Err(OpenRegularFileError::NotRegular)
);
}
#[cfg(unix)]
#[test]
fn regular_file_open_follows_symlink_to_regular_file() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.txt");
std::fs::write(&target, b"abc").unwrap();
let link = dir.path().join("link.txt");
std::os::unix::fs::symlink(&target, &link).unwrap();
let (_handle, metadata) = RegularFile::open(&link).unwrap().into_parts();
assert!(metadata.is_file());
assert_eq!(metadata.len(), 3);
}
#[cfg(windows)]
#[test]
fn regular_file_open_rejects_nul_device_as_not_regular() {
assert_matches!(
RegularFile::open(Path::new("NUL")),
Err(OpenRegularFileError::NotRegular)
);
}
#[test]
fn read_regular_file_bounded_rejects_oversize() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.json");
std::fs::write(&path, b"{ }").unwrap();
assert_matches!(
read_regular_file_bounded(&path, limit(2)),
Err(BoundedFileError::Read(ReadBoundedError::TooLarge(SizeExceeded { size: 3, max }))) if max == limit(2)
);
}
#[test]
fn read_regular_file_bounded_rejects_directory() {
let dir = tempfile::tempdir().unwrap();
let result = read_regular_file_bounded(dir.path(), limit(10));
#[cfg(unix)]
assert_matches!(
result,
Err(BoundedFileError::Open(OpenRegularFileError::NotRegular))
);
#[cfg(windows)]
assert_matches!(result, Err(BoundedFileError::Open(OpenRegularFileError::Io(ref e))) if e.kind() == std::io::ErrorKind::PermissionDenied);
}
#[cfg(unix)]
#[test]
fn read_regular_file_bounded_does_not_block_on_fifo_or_symlink_to_it() {
let dir = tempfile::tempdir().unwrap();
let fifo = dir.path().join("fifo");
crate::test_lsp::make_fifo(&fifo);
let link = dir.path().join("link");
std::os::unix::fs::symlink(&fifo, &link).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let outcomes = [fifo, link].map(|path| read_regular_file_bounded(&path, limit(10)));
tx.send(outcomes).unwrap();
});
let outcomes = rx
.recv_timeout(std::time::Duration::from_secs(5))
.unwrap_or_else(|_| panic!("reading a FIFO must not block"));
for outcome in outcomes {
assert_matches!(
outcome,
Err(BoundedFileError::Open(OpenRegularFileError::NotRegular))
);
}
}
#[test]
fn escape_control_borrows_clean_text() {
assert_matches!(escape_control("plain ✓"), Cow::Borrowed("plain ✓"));
}
#[test]
fn escape_control_escapes_newlines_and_escape_sequences() {
assert_eq!(
escape_control("a\nb\r\tc\x1b[31m"),
"a\\nb\\r\\tc\\u{1b}[31m"
);
}
#[test]
fn escape_control_escapes_line_separators_and_bidi_controls() {
assert_eq!(
escape_control("a\u{2028}b\u{2029}c\u{202E}d\u{2066}e\u{200F}f\u{061C}g"),
"a\\u{2028}b\\u{2029}c\\u{202e}d\\u{2066}e\\u{200f}f\\u{61c}g"
);
}
#[test]
fn escape_control_escapes_forged_log_line() {
assert_eq!(escape_control("ok\nERROR forged"), "ok\\nERROR forged");
}
#[test]
fn truncate_with_zero_budget_keeps_only_marker() {
assert_eq!(truncate_str("é", 0), TRUNCATION_MARKER);
assert_eq!(truncate_string("abc".to_owned(), 0), TRUNCATION_MARKER);
}
fn size(max: u64) -> SizeLimit {
SizeLimit::from_static(max)
}
#[test]
fn size_exceeded_exposes_both_numbers_through_accessors() {
let max = NonZeroU64::new(4).unwrap();
let exceeded = SizeExceeded::check(9, max).unwrap_err();
assert_eq!((exceeded.size(), exceeded.max()), (9, max));
}
#[test]
fn size_exceeded_check_admits_up_to_the_limit_and_reports_both_numbers() {
let max = NonZeroU64::new(4).unwrap();
assert_eq!(SizeExceeded::check(4, max), Ok(()));
assert_eq!(
SizeExceeded::check(5, max),
Err(SizeExceeded { size: 5, max })
);
assert_eq!(
SizeExceeded::check_limit(u64::MAX, SizeLimit::UNLIMITED),
Ok(())
);
assert_eq!(
SizeExceeded::check_limit(5, size(4)),
Err(SizeExceeded { size: 5, max })
);
assert_eq!(
SizeExceeded { size: 5, max }.to_string(),
"5 bytes exceed the limit of 4 bytes"
);
}
#[test]
fn check_bounded_utf8_within_limit() {
assert_eq!(
check_bounded_utf8(b"hello".to_vec(), size(10)).unwrap(),
"hello"
);
}
#[test]
fn check_bounded_utf8_too_large() {
assert_matches!(
check_bounded_utf8(b"hello".to_vec(), size(4)),
Err(BoundedUtf8Error::TooLarge(SizeExceeded { size: 5, max })) if max.get() == 4
);
}
#[test]
fn check_bounded_utf8_unlimited() {
let text = check_bounded_utf8(b"a".repeat(1000), SizeLimit::UNLIMITED).unwrap();
assert_eq!(text.len(), 1000);
}
#[test]
fn check_bounded_utf8_invalid_utf8_within_limit() {
assert_matches!(
check_bounded_utf8(vec![0xFF, 0xFE], size(10)),
Err(BoundedUtf8Error::InvalidUtf8(_))
);
}
#[test]
fn check_bounded_utf8_reports_oversized_before_invalid_utf8() {
let mut buf = "é".repeat(3).into_bytes();
buf.truncate(5);
assert_matches!(
check_bounded_utf8(buf, size(4)),
Err(BoundedUtf8Error::TooLarge(SizeExceeded { size: 5, .. }))
);
}
#[test]
fn no_truncation_at_or_below_limit() {
let exact = "a".repeat(10);
assert_eq!(truncate_str(&exact, 10), exact);
assert_eq!(truncate_str("", 10), "");
}
#[test]
fn truncates_just_above_limit() {
let message = "a".repeat(11);
assert_eq!(
truncate_str(&message, 10),
format!("{}... (truncated)", "a".repeat(10))
);
}
#[test]
fn handles_multibyte_char_boundary() {
let message = "é".repeat(10);
let truncated = truncate_str(&message, 5);
assert!(truncated.starts_with(&"é".repeat(2)));
assert!(truncated.ends_with("... (truncated)"));
}
#[test]
fn truncate_string_no_truncation_at_or_below_limit() {
let exact = "a".repeat(10);
assert_eq!(truncate_string(exact.clone(), 10), exact);
assert_eq!(truncate_string(String::new(), 10), "");
}
#[test]
fn truncate_string_truncates_just_above_limit() {
let message = "a".repeat(11);
assert_eq!(
truncate_string(message, 10),
format!("{}... (truncated)", "a".repeat(10))
);
}
#[test]
fn truncate_string_handles_multibyte_char_boundary() {
let message = "é".repeat(10);
let truncated = truncate_string(message, 5);
assert!(truncated.starts_with(&"é".repeat(2)));
assert!(truncated.ends_with("... (truncated)"));
}
#[test]
fn truncate_str_and_truncate_string_agree() {
let message = "x".repeat(500);
assert_eq!(truncate_str(&message, 100), truncate_string(message, 100));
}
}