| use std::cmp; |
| use std::path::Path; |
|
|
| use anyhow::{Context, Result}; |
| use bstr::ByteSlice; |
| use forge_domain::FileInfo; |
|
|
| use crate::error::Error; |
|
|
| impl crate::ForgeFS { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn read_range_utf8<T: AsRef<Path>>( |
| path: T, |
| start_line: u64, |
| end_line: u64, |
| ) -> Result<(String, FileInfo)> { |
| let path_ref = path.as_ref(); |
|
|
| |
| if start_line > end_line { |
| return Err(Error::StartGreaterThanEnd { start: start_line, end: end_line }.into()); |
| } |
|
|
| |
| let mut file = tokio::fs::File::open(path_ref) |
| .await |
| .with_context(|| format!("Failed to open file {}", path_ref.display()))?; |
|
|
| if start_line == 0 || end_line == 0 { |
| return Err(Error::IndexStartingWithZero { start: start_line, end: end_line }.into()); |
| } |
|
|
| let (is_text, file_type) = Self::is_binary(&mut file).await?; |
| if !is_text { |
| return Err(Error::BinaryFileNotSupported(file_type).into()); |
| } |
|
|
| |
| let content = tokio::fs::read(path_ref) |
| .await |
| .with_context(|| format!("Failed to read file content from {}", path_ref.display()))?; |
| let content = content.to_str_lossy(); |
|
|
| |
| |
| let content_hash = crate::ForgeFS::compute_hash(content.as_ref()); |
|
|
| if start_line < 2 && content.is_empty() { |
| |
| return Ok(( |
| String::new(), |
| FileInfo::new(start_line, end_line, 0, content_hash), |
| )); |
| } |
| |
| let lines: Vec<&str> = content.lines().collect(); |
| let total_lines = lines.len() as u64; |
|
|
| |
| let start_pos = start_line.saturating_sub(1); |
| let mut end_pos = end_line.saturating_sub(1); |
|
|
| |
| if start_pos >= total_lines { |
| return Err( |
| Error::StartBeyondFileSize { start: start_line, total: total_lines }.into(), |
| ); |
| } |
|
|
| |
| end_pos = cmp::min(end_pos, total_lines - 1); |
|
|
| |
| let actual_end_line = cmp::min(end_line, total_lines); |
|
|
| let info = FileInfo::new(start_line, actual_end_line, total_lines, content_hash); |
|
|
| |
| let result_content = if start_pos == 0 && end_pos == total_lines - 1 { |
| content.to_string() |
| } else { |
| lines |
| .get(start_pos as usize..=end_pos as usize) |
| .map(|slice| slice.join("\n")) |
| .unwrap_or_default() |
| }; |
|
|
| Ok((result_content, info)) |
| } |
| } |
|
|
| #[cfg(test)] |
| mod test { |
| use anyhow::Result; |
| use pretty_assertions::assert_eq; |
| use tokio::fs; |
|
|
| |
| async fn create_test_file(content: &str) -> Result<tempfile::NamedTempFile> { |
| let file = tempfile::NamedTempFile::new()?; |
| fs::write(file.path(), content).await?; |
| Ok(file) |
| } |
|
|
| #[tokio::test] |
| async fn test_read_range_utf8() -> Result<()> { |
| let content = |
| "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"; |
| let file = create_test_file(content).await?; |
| let full_hash = crate::ForgeFS::compute_hash(content); |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 2, 5).await?; |
| assert_eq!(result, "Line 2\nLine 3\nLine 4\nLine 5"); |
| assert_eq!(info.start_line, 2); |
| assert_eq!(info.end_line, 5); |
| assert_eq!(info.total_lines, 10); |
| assert_eq!(info.content_hash, full_hash); |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 1, 3).await?; |
| assert_eq!(result, "Line 1\nLine 2\nLine 3"); |
| assert_eq!(info.start_line, 1); |
| assert_eq!(info.end_line, 3); |
| assert_eq!(info.content_hash, full_hash); |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 8, 10).await?; |
| assert_eq!(result, "Line 8\nLine 9\nLine 10"); |
| assert_eq!(info.start_line, 8); |
| assert_eq!(info.end_line, 10); |
| assert_eq!(info.content_hash, full_hash); |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 1, 10).await?; |
| assert_eq!(result, content); |
| assert_eq!(info.start_line, 1); |
| assert_eq!(info.end_line, 10); |
| assert_eq!(info.content_hash, full_hash); |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 5, 5).await?; |
| assert_eq!(result, "Line 5"); |
| assert_eq!(info.start_line, 5); |
| assert_eq!(info.end_line, 5); |
| assert_eq!(info.content_hash, full_hash); |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 1, 1).await?; |
| assert_eq!(result, "Line 1"); |
| assert_eq!(info.start_line, 1); |
| assert_eq!(info.end_line, 1); |
| assert_eq!(info.total_lines, 10); |
| assert_eq!(info.content_hash, full_hash); |
|
|
| |
| assert!( |
| crate::ForgeFS::read_range_utf8(file.path(), 8, 5) |
| .await |
| .is_err() |
| ); |
| assert!( |
| crate::ForgeFS::read_range_utf8(file.path(), 15, 10) |
| .await |
| .is_err() |
| ); |
| assert!( |
| crate::ForgeFS::read_range_utf8(file.path(), 0, 5) |
| .await |
| .is_err() |
| ); |
|
|
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_utf8_multi_line_handling() -> Result<()> { |
| let content = "Hello world!\nこんにちは 世界!\nПривет мир!\nBonjour le monde!"; |
| let file = create_test_file(content).await?; |
|
|
| |
| let (result, info) = crate::ForgeFS::read_range_utf8(file.path(), 2, 3).await?; |
| assert_eq!(result, "こんにちは 世界!\nПривет мир!"); |
| assert_eq!(info.start_line, 2); |
| assert_eq!(info.end_line, 3); |
|
|
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_invalid_utf8_handling() -> Result<()> { |
| let content = b"Hello world!\nValid line\n\xFF\xFE\xFD Invalid UTF-8\nAnother valid line"; |
| let file = tempfile::NamedTempFile::new()?; |
| fs::write(file.path(), content).await?; |
|
|
| |
| let result = crate::ForgeFS::read_range_utf8(&file.path(), 1, 4).await; |
| assert!(result.is_ok()); |
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_end_line_capping() -> Result<()> { |
| let content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; |
| let file = create_test_file(content).await?; |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 1, 5).await?; |
| assert_eq!(actual_content, content); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (1, 5, 5) |
| ); |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 1, 6).await?; |
| assert_eq!(actual_content, content); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (1, 5, 5) |
| ); |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 1, 10000).await?; |
| assert_eq!(actual_content, content); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (1, 5, 5) |
| ); |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 3, 100).await?; |
| assert_eq!(actual_content, "Line 3\nLine 4\nLine 5"); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (3, 5, 5) |
| ); |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 5, 100).await?; |
| assert_eq!(actual_content, "Line 5"); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (5, 5, 5) |
| ); |
|
|
| Ok(()) |
| } |
|
|
| #[tokio::test] |
| async fn test_large_file_ranges() -> Result<()> { |
| |
| let lines: Vec<String> = (1..=5000).map(|i| format!("Line {}", i)).collect(); |
| let content = lines.join("\n"); |
| let file = create_test_file(&content).await?; |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 1, 50).await?; |
| assert_eq!(actual_content.lines().count(), 50); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (1, 50, 5000) |
| ); |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 2500, 3500).await?; |
| assert_eq!(actual_content.lines().count(), 1001); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (2500, 3500, 5000) |
| ); |
|
|
| |
| let (actual_content, actual_info) = |
| crate::ForgeFS::read_range_utf8(file.path(), 4990, 6000).await?; |
| assert_eq!(actual_content.lines().count(), 11); |
| assert_eq!( |
| ( |
| actual_info.start_line, |
| actual_info.end_line, |
| actual_info.total_lines |
| ), |
| (4990, 5000, 5000) |
| ); |
|
|
| Ok(()) |
| } |
| } |
|
|