Files
cargo-cxcloud-bot-0.1.0/src/tools/file_ops.rs
T
2026-04-26 16:48:19 +00:00

74 lines
2.1 KiB
Rust

use anyhow::{bail, Result};
use std::path::{Path, PathBuf};
use tracing::info;
const DATA_DIR: &str = "/data";
const MAX_READ_SIZE: u64 = 10 * 1024 * 1024; // 10 MB
/// Read a file from the sandboxed /data directory.
pub async fn read(params: &serde_json::Value) -> Result<serde_json::Value> {
let path_str = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("path parameter is required"))?;
let path = sanitize_path(path_str)?;
info!(path = %path.display(), "Reading file");
let metadata = tokio::fs::metadata(&path).await?;
if metadata.len() > MAX_READ_SIZE {
bail!("File too large: {} bytes (max {})", metadata.len(), MAX_READ_SIZE);
}
let content = tokio::fs::read_to_string(&path).await?;
Ok(serde_json::json!({
"path": path_str,
"size": metadata.len(),
"content": content,
}))
}
/// Write a file to the sandboxed /data directory.
pub async fn write(params: &serde_json::Value) -> Result<serde_json::Value> {
let path_str = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("path parameter is required"))?;
let content = params
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("content parameter is required"))?;
let path = sanitize_path(path_str)?;
info!(path = %path.display(), size = content.len(), "Writing file");
// Create parent directories if needed
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&path, content).await?;
Ok(serde_json::json!({
"path": path_str,
"size": content.len(),
"written": true,
}))
}
/// Sanitize and validate that the path stays within /data.
fn sanitize_path(input: &str) -> Result<PathBuf> {
let base = Path::new(DATA_DIR);
let full = base.join(input.trim_start_matches('/'));
let canonical_base = base.to_path_buf();
// Prevent path traversal
if !full.starts_with(&canonical_base) {
bail!("Path traversal detected: {input}");
}
Ok(full)
}