use std::path::Path;
pub(crate) fn read_source_file_canonical(canonical_root: &Path, file_path: &str) -> Option<String> {
use std::fs::File;
use std::io::Read;
let relative = file_path.trim_start_matches('/');
let canonical_target = canonical_root.join(relative).canonicalize().ok()?;
if !canonical_target.starts_with(canonical_root) {
return None;
}
let mut file = File::open(&canonical_target).ok()?;
let mut content = String::new();
file.read_to_string(&mut content).ok()?;
Some(content)
}
pub(crate) fn slice_lines(content: &str, start_line: u32, end_line: u32) -> Option<String> {
if start_line == 0 || end_line == 0 || start_line > end_line {
return None;
}
let start = (start_line - 1) as usize;
let count = (end_line - start_line + 1) as usize;
let selected: Vec<&str> = content.lines().skip(start).take(count).collect();
if selected.is_empty() {
return None;
}
Some(selected.join("\n"))
}
pub(crate) fn read_source_slice_canonical(
canonical_root: &Path,
file_path: &str,
start_line: u32,
end_line: u32,
) -> Option<String> {
if start_line == 0 || end_line == 0 {
return None;
}
let content = read_source_file_canonical(canonical_root, file_path)?;
slice_lines(&content, start_line, end_line)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn read_source_file_canonical_rejects_traversal() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("lib.rs"), "safe\n").unwrap();
let root = dir.path().canonicalize().unwrap();
assert!(read_source_file_canonical(&root, "../escape.rs").is_none());
assert!(read_source_file_canonical(&root, "lib.rs").is_some());
}
#[test]
fn slice_lines_returns_expected_range_and_clamps() {
assert_eq!(slice_lines("a\nb\nc\nd", 2, 3).unwrap(), "b\nc");
assert_eq!(slice_lines("a\nb", 1, 99).unwrap(), "a\nb");
assert!(slice_lines("a\nb", 0, 1).is_none());
assert!(slice_lines("a\nb", 5, 6).is_none());
}
}