deps_lsp/document/
loader.rs1use deps_core::error::{DepsError, Result};
33use tower_lsp_server::ls_types::Uri;
34
35pub(crate) const MAX_FILE_SIZE: u64 = 10_000_000; const LARGE_FILE_THRESHOLD: u64 = 1_000_000; pub async fn load_document_from_disk(uri: &Uri) -> Result<String> {
84 let path = match uri.to_file_path() {
87 Some(p) => p.into_owned(),
88 None => {
89 tracing::debug!("Cannot load non-file URI: {:?}", uri);
90 return Err(DepsError::InvalidUri(format!("{uri:?}")));
91 }
92 };
93
94 tracing::debug!("Loading document from disk: {:?}", path);
95
96 match tokio::fs::metadata(&path).await {
98 Ok(metadata) => {
99 if !metadata.is_file() {
106 tracing::warn!("Rejecting non-regular-file document: {:?}", path);
107 return Err(DepsError::CacheError(format!(
108 "not a regular file: {}",
109 path.display()
110 )));
111 }
112
113 let size = metadata.len();
114
115 if size > MAX_FILE_SIZE {
117 tracing::error!(
118 "Document exceeds maximum size: {} bytes (limit: {} bytes)",
119 size,
120 MAX_FILE_SIZE
121 );
122 return Err(DepsError::CacheError(format!(
123 "file too large: {size} bytes (max: {MAX_FILE_SIZE} bytes)"
124 )));
125 }
126
127 if size > LARGE_FILE_THRESHOLD {
129 tracing::warn!(
130 "Document is large: {} bytes for {:?}. Typical manifests are <100KB.",
131 size,
132 path
133 );
134 }
135
136 tracing::trace!("File size: {} bytes", size);
137 }
138 Err(e) => {
139 match e.kind() {
141 std::io::ErrorKind::NotFound => {
142 tracing::debug!("File not found: {:?}", path);
143 }
144 std::io::ErrorKind::PermissionDenied => {
145 tracing::warn!("Permission denied: {:?}", path);
146 }
147 _ => {
148 tracing::error!("IO error reading metadata for {:?}: {}", path, e);
149 }
150 }
151 return Err(DepsError::Io(e));
152 }
153 }
154
155 let read_path = path.clone();
161 let capped = tokio::task::spawn_blocking(move || {
162 deps_core::fs_probe::read_to_string_capped(&read_path, MAX_FILE_SIZE)
163 })
164 .await
165 .map_err(|e| {
166 tracing::error!("document read task for {:?} panicked: {}", path, e);
167 DepsError::CacheError(format!("document read task failed: {e}"))
168 })?
169 .map_err(|e| {
170 match e.kind() {
172 std::io::ErrorKind::NotFound => {
173 tracing::debug!("File not found during read: {:?}", path);
174 }
175 std::io::ErrorKind::PermissionDenied => {
176 tracing::warn!("Permission denied reading file: {:?}", path);
177 }
178 _ => {
179 tracing::error!("IO error reading file {:?}: {}", path, e);
180 }
181 }
182 DepsError::Io(e)
183 })?;
184
185 let content = match capped {
186 Some(content) => content,
187 None => {
188 tracing::error!(
195 "Document exceeds maximum size during read (limit: {} bytes): {:?}",
196 MAX_FILE_SIZE,
197 path
198 );
199 return Err(DepsError::CacheError(format!(
200 "file too large (max: {MAX_FILE_SIZE} bytes)"
201 )));
202 }
203 };
204
205 tracing::debug!(
206 "Successfully loaded document: {:?} ({} bytes)",
207 path,
208 content.len()
209 );
210
211 Ok(content)
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use std::io::Write;
218 use tempfile::NamedTempFile;
219 use tower_lsp_server::ls_types::Uri;
220
221 #[tokio::test]
222 async fn test_load_existing_file() {
223 let mut temp_file = NamedTempFile::new().unwrap();
224 let content = "test content";
225 temp_file.write_all(content.as_bytes()).unwrap();
226 temp_file.flush().unwrap();
227
228 let uri = Uri::from_file_path(temp_file.path()).unwrap();
229 let loaded = load_document_from_disk(&uri).await.unwrap();
230
231 assert_eq!(loaded, content);
232 }
233
234 #[tokio::test]
235 async fn test_load_nonexistent_file() {
236 let uri = deps_core::test_util::test_uri("/nonexistent/file/path.toml");
237 let result = load_document_from_disk(&uri).await;
238
239 assert!(result.is_err());
240 match result {
241 Err(DepsError::Io(_)) => {}
242 _ => panic!("Expected Io error"),
243 }
244 }
245
246 #[tokio::test]
247 async fn test_load_empty_file() {
248 let temp_file = NamedTempFile::new().unwrap();
249 let uri = Uri::from_file_path(temp_file.path()).unwrap();
252 let loaded = load_document_from_disk(&uri).await.unwrap();
253
254 assert_eq!(loaded, "");
255 }
256
257 #[tokio::test]
262 async fn test_load_utf8_file() {
263 let mut temp_file = NamedTempFile::new().unwrap();
264 let content = "Hello δΈη π ΠΡΠΈΠ²Π΅Ρ";
265 temp_file.write_all(content.as_bytes()).unwrap();
266 temp_file.flush().unwrap();
267
268 let uri = Uri::from_file_path(temp_file.path()).unwrap();
269 let loaded = load_document_from_disk(&uri).await.unwrap();
270
271 assert_eq!(loaded, content);
272 }
273
274 #[tokio::test]
275 async fn test_load_non_utf8_file() {
276 let mut temp_file = NamedTempFile::new().unwrap();
277 temp_file.write_all(&[0xFF, 0xFE, 0xFD]).unwrap();
279 temp_file.flush().unwrap();
280
281 let uri = Uri::from_file_path(temp_file.path()).unwrap();
282 let result = load_document_from_disk(&uri).await;
283
284 assert!(result.is_err());
285 match result {
286 Err(DepsError::Io(_)) => {}
287 _ => panic!("Expected Io error for non-UTF8 content"),
288 }
289 }
290
291 #[cfg(unix)]
292 #[tokio::test]
293 async fn test_load_permission_denied() {
294 use std::fs;
295 use std::os::unix::fs::PermissionsExt;
296
297 let mut temp_file = NamedTempFile::new().unwrap();
298 temp_file.write_all(b"test").unwrap();
299 temp_file.flush().unwrap();
300
301 let mut perms = fs::metadata(temp_file.path()).unwrap().permissions();
303 perms.set_mode(0o000);
304 fs::set_permissions(temp_file.path(), perms.clone()).unwrap();
305
306 let uri = Uri::from_file_path(temp_file.path()).unwrap();
307 let result = load_document_from_disk(&uri).await;
308
309 perms.set_mode(0o644);
311 let _ = fs::set_permissions(temp_file.path(), perms);
312
313 assert!(result.is_err());
314 match result {
315 Err(DepsError::Io(_)) => {}
316 _ => panic!("Expected Io error for permission denied"),
317 }
318 }
319
320 #[tokio::test]
321 async fn test_load_large_file_warning() {
322 let mut temp_file = NamedTempFile::new().unwrap();
326 let content = "a".repeat(1000); temp_file.write_all(content.as_bytes()).unwrap();
328 temp_file.flush().unwrap();
329
330 let uri = Uri::from_file_path(temp_file.path()).unwrap();
331 let loaded = load_document_from_disk(&uri).await.unwrap();
332
333 assert_eq!(loaded.len(), 1000);
334 }
335
336 #[tokio::test]
337 async fn test_load_cargo_toml() {
338 let mut temp_file = NamedTempFile::new().unwrap();
339 let content = r#"[package]
340name = "test"
341version = "0.1.0"
342
343[dependencies]
344serde = "1.0"
345"#;
346 temp_file.write_all(content.as_bytes()).unwrap();
347 temp_file.flush().unwrap();
348
349 let uri = Uri::from_file_path(temp_file.path()).unwrap();
350 let loaded = load_document_from_disk(&uri).await.unwrap();
351
352 assert_eq!(loaded, content);
353 assert!(loaded.contains("[dependencies]"));
354 }
355
356 #[tokio::test]
357 async fn test_file_size_limit_constant() {
358 assert_eq!(MAX_FILE_SIZE, 10_000_000);
360 assert_eq!(LARGE_FILE_THRESHOLD, 1_000_000);
361 }
362
363 #[cfg(unix)]
364 #[tokio::test]
365 async fn test_load_symlink_to_valid_file() {
366 use std::os::unix::fs::symlink;
367 use tempfile::TempDir;
368
369 let temp_dir = TempDir::new().unwrap();
370 let target = temp_dir.path().join("target.toml");
371 let link = temp_dir.path().join("link.toml");
372
373 std::fs::write(&target, "[dependencies]").unwrap();
374 symlink(&target, &link).unwrap();
375
376 let uri = Uri::from_file_path(&link).unwrap();
377 let content = load_document_from_disk(&uri).await.unwrap();
378 assert_eq!(content, "[dependencies]");
379 }
380
381 #[cfg(unix)]
382 #[tokio::test]
383 async fn test_load_circular_symlink() {
384 use std::os::unix::fs::symlink;
385 use tempfile::TempDir;
386
387 let temp_dir = TempDir::new().unwrap();
388 let link1 = temp_dir.path().join("link1.toml");
389 let link2 = temp_dir.path().join("link2.toml");
390
391 symlink(&link2, &link1).unwrap();
392 symlink(&link1, &link2).unwrap();
393
394 let uri = Uri::from_file_path(&link1).unwrap();
395 let result = load_document_from_disk(&uri).await;
396 assert!(result.is_err(), "Circular symlink should fail");
397 }
398
399 #[cfg(unix)]
404 #[tokio::test]
405 async fn test_load_fifo_does_not_hang() {
406 use tempfile::TempDir;
407
408 let temp_dir = TempDir::new().unwrap();
409 let fifo_path = temp_dir.path().join("Cargo.toml");
410 let status = std::process::Command::new("mkfifo")
411 .arg(&fifo_path)
412 .status()
413 .unwrap();
414 assert!(
415 status.success(),
416 "mkfifo must succeed for this test to be meaningful"
417 );
418
419 let uri = Uri::from_file_path(&fifo_path).unwrap();
420 let result = tokio::time::timeout(
421 std::time::Duration::from_secs(5),
422 load_document_from_disk(&uri),
423 )
424 .await
425 .expect("a FIFO must be rejected by the is_file gate, not block trying to open it");
426
427 assert!(
428 result.is_err(),
429 "a FIFO must never be treated as a loadable document"
430 );
431 }
432
433 #[tokio::test]
434 async fn test_load_file_exceeding_max_size() {
435 use std::io::Write;
436
437 let mut temp_file = NamedTempFile::new().unwrap();
441
442 let content = "test content";
445 temp_file.write_all(content.as_bytes()).unwrap();
446 temp_file.flush().unwrap();
447
448 assert_eq!(MAX_FILE_SIZE, 10_000_000, "MAX_FILE_SIZE constant changed");
450
451 #[cfg(unix)]
453 {
454 use std::os::unix::fs::FileExt;
455 use tempfile::TempDir;
456
457 let temp_dir = TempDir::new().unwrap();
458 let large_file = temp_dir.path().join("large.toml");
459
460 let file = std::fs::File::create(&large_file).unwrap();
463 let beyond_limit = MAX_FILE_SIZE + 1;
464 file.write_at(b"x", beyond_limit).unwrap();
465
466 let uri = Uri::from_file_path(&large_file).unwrap();
467 let result = load_document_from_disk(&uri).await;
468
469 assert!(result.is_err(), "Should reject files > MAX_FILE_SIZE");
470 match result {
471 Err(DepsError::CacheError(msg)) => {
472 assert!(
473 msg.contains("file too large"),
474 "Error message should indicate file size issue: {msg}"
475 );
476 assert!(
477 msg.contains(&beyond_limit.to_string())
478 || msg.contains(&(beyond_limit + 1).to_string()),
479 "Error should mention actual file size: {msg}"
480 );
481 }
482 _ => panic!("Expected CacheError for oversized file"),
483 }
484 }
485 }
486
487 #[tokio::test]
492 async fn test_load_file_exactly_at_max_size() {
493 let temp_dir = tempfile::TempDir::new().unwrap();
494 let path = temp_dir.path().join("exact.toml");
495 let content = "a".repeat(MAX_FILE_SIZE as usize);
496 std::fs::write(&path, &content).unwrap();
497
498 let uri = Uri::from_file_path(&path).unwrap();
499 let loaded = load_document_from_disk(&uri).await.unwrap();
500
501 assert_eq!(loaded.len(), MAX_FILE_SIZE as usize);
502 }
503
504 #[tokio::test]
509 async fn test_load_routes_through_capped_read() {
510 let mut temp_file = NamedTempFile::new().unwrap();
511 temp_file.write_all(b"test content").unwrap();
512 temp_file.flush().unwrap();
513
514 let uri = Uri::from_file_path(temp_file.path()).unwrap();
515 let (_, reads_before) = deps_core::fs_probe::snapshot();
516 load_document_from_disk(&uri).await.unwrap();
517 let (_, reads_after) = deps_core::fs_probe::snapshot();
518
519 assert_eq!(
520 reads_after - reads_before,
521 1,
522 "load_document_from_disk must read exactly once through the counted fs_probe path"
523 );
524 }
525}