use anyhow::{Context, Result, anyhow};
use infer::Infer;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
pub struct ViewOptions {
pub max_size: Option<usize>,
pub line_from: Option<usize>,
pub line_to: Option<usize>,
}
impl Default for ViewOptions {
fn default() -> Self {
Self {
max_size: Some(10 * 1024 * 1024), line_from: None,
line_to: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum FileContents {
#[serde(rename = "text")]
Text {
content: TextContent,
metadata: TextMetadata,
},
#[serde(rename = "binary")]
Binary {
message: String,
metadata: BinaryMetadata,
},
#[serde(rename = "image")]
Image {
message: String,
metadata: ImageMetadata,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TextContent {
pub line_contents: Vec<LineContent>,
}
impl TextContent {
pub fn contains(&self, s: &str) -> bool {
self.line_contents.iter().any(|line| line.line.contains(s))
}
pub fn is_empty(&self) -> bool {
self.line_contents.is_empty()
}
pub fn to_lowercase(&self) -> String {
self.line_contents
.iter()
.map(|line| line.line.to_lowercase())
.collect::<Vec<_>>()
.join("\n")
}
pub fn to_string(&self) -> String {
self.line_contents
.iter()
.map(|line| line.line.clone())
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LineContent {
pub line_number: usize,
pub line: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TextMetadata {
pub line_count: usize,
pub char_count: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BinaryMetadata {
pub binary: bool,
pub size_bytes: u64,
pub mime_type: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImageMetadata {
pub binary: bool,
pub size_bytes: u64,
pub media_type: String,
}
#[derive(Serialize, Debug)]
pub struct FileView {
pub file_path: PathBuf,
pub file_type: String,
pub contents: FileContents,
pub total_line_num: Option<usize>,
}
pub fn view_file(path: &Path, options: &ViewOptions) -> Result<FileView> {
if !path.exists() {
return Err(anyhow!("File not found: {}", path.display()));
}
if !path.is_file() {
return Err(anyhow!("Not a file: {}", path.display()));
}
let metadata = path
.metadata()
.with_context(|| format!("Failed to read file metadata for {}", path.display()))?;
let using_line_filters = options.line_from.is_some() || options.line_to.is_some();
if let Some(max_size) = options.max_size {
if !using_line_filters && metadata.len() > max_size as u64 {
return Err(anyhow!(
"File is too large: {} (size: {}, limit: {})",
path.display(),
metadata.len(),
max_size
));
}
}
let infer = Infer::new();
let extension_type = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| match ext.to_lowercase().as_str() {
"txt" | "md" | "rs" | "toml" | "yml" | "yaml" | "json" => Some("text/plain"),
"py" => Some("text/x-python"),
"js" => Some("text/javascript"),
"html" => Some("text/html"),
"css" => Some("text/css"),
_ => None,
})
.unwrap_or(None);
let file_type = match infer.get_from_path(path) {
Ok(Some(kind)) => kind.mime_type().to_string(),
Ok(None) => {
if let Some(ext_type) = extension_type {
ext_type.to_string()
} else {
match std::fs::read(path) {
Ok(bytes) if bytes.len() <= 1024 => {
let text_likelihood = bytes
.iter()
.filter(|b| {
**b >= 32 && **b <= 126
|| **b == b'\n'
|| **b == b'\r'
|| **b == b'\t'
})
.count() as f64
/ bytes.len() as f64;
if text_likelihood > 0.8 {
"text/plain".to_string()
} else {
"application/octet-stream".to_string()
}
}
_ => "application/octet-stream".to_string(), }
}
}
Err(e) => return Err(anyhow!("Failed to determine file type: {}", e)),
};
let mut file =
File::open(path).with_context(|| format!("Failed to open file {}", path.display()))?;
let mut content = Vec::new();
file.read_to_end(&mut content)
.with_context(|| format!("Failed to read file {}", path.display()))?;
let contents = if file_type.starts_with("text/") {
match String::from_utf8(content.clone()) {
Ok(text) => {
let all_lines: Vec<&str> = text.lines().collect();
let line_count = all_lines.len();
let char_count = text.chars().count();
let from_line = options.line_from.unwrap_or(1).max(1);
let to_line = options.line_to.unwrap_or(line_count).min(line_count);
let (effective_from, effective_to) =
if from_line > line_count || from_line > to_line {
(1, 0) } else {
(from_line, to_line)
};
let line_contents = all_lines
.iter()
.enumerate()
.filter(|(idx, _)| {
let line_num = idx + 1; line_num >= effective_from && line_num <= effective_to
})
.map(|(idx, line)| LineContent {
line_number: idx + 1, line: line.to_string().trim_end_matches('\n').to_string(),
})
.collect();
let content = TextContent { line_contents };
if using_line_filters && options.max_size.is_some() {
let max_size = options.max_size.unwrap();
let filtered_size = content
.line_contents
.iter()
.map(|line| line.line.len() + 1) .sum::<usize>();
if filtered_size > max_size {
return Err(anyhow!(
"Filtered content is too large: {} (filtered size: {}, limit: {})",
path.display(),
filtered_size,
max_size
));
}
}
FileContents::Text {
content,
metadata: TextMetadata {
line_count,
char_count,
},
}
}
Err(_) => {
FileContents::Binary {
message: format!("Binary file detected, size: {} bytes", metadata.len()),
metadata: BinaryMetadata {
binary: true,
size_bytes: metadata.len(),
mime_type: None,
},
}
}
}
} else if file_type.starts_with("image/") {
if using_line_filters && options.max_size.is_some() {
let max_size = options.max_size.unwrap();
if metadata.len() > max_size as u64 {
return Err(anyhow!(
"Image file is too large when using line filters: {} (size: {}, limit: {})",
path.display(),
metadata.len(),
max_size
));
}
}
FileContents::Image {
message: format!("Image file detected: {}", file_type),
metadata: ImageMetadata {
binary: true,
size_bytes: metadata.len(),
media_type: "image".to_string(),
},
}
} else {
if using_line_filters && options.max_size.is_some() {
let max_size = options.max_size.unwrap();
if metadata.len() > max_size as u64 {
return Err(anyhow!(
"Binary file is too large when using line filters: {} (size: {}, limit: {})",
path.display(),
metadata.len(),
max_size
));
}
}
FileContents::Binary {
message: format!(
"Binary file detected, size: {} bytes, type: {}",
metadata.len(),
file_type
),
metadata: BinaryMetadata {
binary: true,
size_bytes: metadata.len(),
mime_type: Some(file_type.clone()),
},
}
};
let total_line_num = match &contents {
FileContents::Text { metadata, .. } => Some(metadata.line_count),
_ => None,
};
let result = FileView {
file_path: path.to_path_buf(),
file_type,
contents,
total_line_num,
};
Ok(result)
}