use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
const DISPLAY_MAX_CHARS: usize = 1_000;
const MODEL_MAX_CHARS: usize = 30_000;
pub fn detect_project_root(start: &Path) -> PathBuf {
const MARKERS: &[&str] = &[
".git",
"Cargo.toml",
"package.json",
"go.mod",
"pyproject.toml",
];
let mut dir = start.to_path_buf();
loop {
for marker in MARKERS {
if dir.join(marker).exists() {
return dir;
}
}
if !dir.pop() {
return start.to_path_buf();
}
}
}
pub fn validate_path(target: &Path, root: &Path) -> Result<PathBuf> {
let canonical_root = root.canonicalize()
.map_err(|e| anyhow::anyhow!("Failed to resolve project root '{}': {}", root.display(), e))?;
let resolved = if target.exists() {
target.canonicalize()
.map_err(|e| anyhow::anyhow!("Failed to resolve path '{}': {}", target.display(), e))?
} else {
let parent = target.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid path: {}", target.display()))?;
let canonical_parent = if parent.as_os_str().is_empty() {
std::env::current_dir()?
} else if parent.exists() {
parent.canonicalize()
.map_err(|e| anyhow::anyhow!("Failed to resolve parent '{}': {}", parent.display(), e))?
} else {
bail!("Parent directory does not exist: {}", parent.display());
};
let filename = target.file_name()
.ok_or_else(|| anyhow::anyhow!("Invalid path: {}", target.display()))?;
canonical_parent.join(filename)
};
if !resolved.starts_with(&canonical_root) {
bail!(
"Path '{}' is outside the project root '{}'",
resolved.display(),
canonical_root.display()
);
}
Ok(resolved)
}
pub fn clip_for_display(output: &str) -> String {
clip(output, DISPLAY_MAX_CHARS)
}
pub fn clip_for_model(output: &str) -> String {
clip(output, MODEL_MAX_CHARS)
}
fn clip(output: &str, max: usize) -> String {
if output.len() <= max {
output.to_string()
} else {
let truncated = &output[..max];
let remaining = output.len() - max;
format!("{truncated}\n\n... ({remaining} characters truncated)")
}
}
const GROX_MD_MAX_CHARS: usize = 10_000;
pub fn load_grox_md(project_root: &Path) -> Option<String> {
let path = project_root.join("GROX.md");
let content = std::fs::read_to_string(&path).ok()?;
if content.len() <= GROX_MD_MAX_CHARS {
Some(content)
} else {
let truncated = &content[..GROX_MD_MAX_CHARS];
let remaining = content.len() - GROX_MD_MAX_CHARS;
Some(format!("{truncated}\n\n... (GROX.md truncated — {remaining} characters omitted)"))
}
}
pub fn is_binary(data: &[u8]) -> bool {
let check_len = data.len().min(8192);
data[..check_len].contains(&0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn detect_root_git_repo() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join(".git")).unwrap();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap();
let root = detect_project_root(&sub);
assert_eq!(root, dir.path());
}
#[test]
fn detect_root_cargo_project() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let root = detect_project_root(dir.path());
assert_eq!(root, dir.path());
}
#[test]
fn detect_root_no_markers_falls_back() {
let dir = tempdir().unwrap();
let root = detect_project_root(dir.path());
assert_eq!(root, dir.path());
}
#[test]
fn validate_path_within_root() {
let dir = tempdir().unwrap();
let file = dir.path().join("test.txt");
fs::write(&file, "").unwrap();
let result = validate_path(&file, dir.path());
assert!(result.is_ok());
}
#[test]
fn validate_path_outside_root() {
let dir = tempdir().unwrap();
let other = tempdir().unwrap();
let file = other.path().join("escape.txt");
fs::write(&file, "").unwrap();
let result = validate_path(&file, dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("outside the project root"));
}
#[test]
fn validate_path_relative_escape() {
let dir = tempdir().unwrap();
let escape = dir.path().join("../escape.txt");
let result = validate_path(&escape, dir.path());
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn validate_path_symlink_escape() {
let dir = tempdir().unwrap();
let outside = tempdir().unwrap();
let target_file = outside.path().join("secret.txt");
fs::write(&target_file, "secret").unwrap();
let link = dir.path().join("sneaky_link");
std::os::unix::fs::symlink(&target_file, &link).unwrap();
let result = validate_path(&link, dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("outside the project root"));
}
#[test]
fn validate_path_new_file_in_root() {
let dir = tempdir().unwrap();
let new_file = dir.path().join("new.txt");
let result = validate_path(&new_file, dir.path());
assert!(result.is_ok());
}
#[test]
fn clip_under_threshold() {
let short = "hello world";
assert_eq!(clip_for_display(short), short);
assert_eq!(clip_for_model(short), short);
}
#[test]
fn clip_over_display_threshold() {
let long = "x".repeat(DISPLAY_MAX_CHARS + 500);
let clipped = clip_for_display(&long);
assert!(clipped.len() < long.len());
assert!(clipped.contains("truncated"));
assert!(clipped.contains("500"));
}
#[test]
fn clip_over_model_threshold() {
let long = "x".repeat(MODEL_MAX_CHARS + 1000);
let clipped = clip_for_model(&long);
assert!(clipped.len() < long.len());
assert!(clipped.contains("truncated"));
}
#[test]
fn clip_at_exact_boundary() {
let exact = "x".repeat(DISPLAY_MAX_CHARS);
assert_eq!(clip_for_display(&exact), exact);
}
#[test]
fn binary_detection_text() {
assert!(!is_binary(b"Hello, world!\nThis is text."));
}
#[test]
fn binary_detection_binary() {
let mut data = vec![0u8; 100];
data[50] = 0; assert!(is_binary(&data));
}
#[test]
fn binary_detection_empty() {
assert!(!is_binary(b""));
}
#[test]
fn load_grox_md_present() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("GROX.md"), "You are a helpful assistant.").unwrap();
let result = load_grox_md(dir.path());
assert_eq!(result, Some("You are a helpful assistant.".to_string()));
}
#[test]
fn load_grox_md_absent() {
let dir = tempdir().unwrap();
let result = load_grox_md(dir.path());
assert_eq!(result, None);
}
#[test]
fn load_grox_md_truncated_over_10k() {
let dir = tempdir().unwrap();
let long = "x".repeat(12_000);
fs::write(dir.path().join("GROX.md"), &long).unwrap();
let result = load_grox_md(dir.path()).unwrap();
assert!(result.len() < long.len());
assert!(result.contains("truncated"));
}
#[test]
fn load_grox_md_at_exactly_10k() {
let dir = tempdir().unwrap();
let exact = "y".repeat(10_000);
fs::write(dir.path().join("GROX.md"), &exact).unwrap();
let result = load_grox_md(dir.path()).unwrap();
assert_eq!(result, exact);
}
}