use crate::sandbox::paths;
use std::path::Path;
pub const MAX_INLINE_BYTES: u64 = 12 * 1024;
pub fn language_for(path: &str) -> String {
let extension = Path::new(path)
.extension()
.and_then(|extension| extension.to_str())
.map(str::to_ascii_lowercase);
match extension.as_deref() {
Some("ts") => "ts",
Some("tsx") => "tsx",
Some("js") => "js",
Some("jsx") => "jsx",
Some("json") => "json",
Some("md") => "md",
Some("sh" | "bash") => "bash",
Some("fish") => "fish",
Some("py") => "python",
Some("rs") => "rust",
Some("go") => "go",
Some("c" | "h") => "c",
Some("cpp") => "cpp",
Some("toml") => "toml",
Some("yaml" | "yml") => "yaml",
Some("sql") => "sql",
Some("html") => "html",
Some("css") => "css",
_ => "",
}
.to_owned()
}
pub fn looks_binary(bytes: &[u8]) -> bool {
bytes[..bytes.len().min(8_000)].contains(&0)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Entry {
pub name: String,
pub path: String,
pub directory: bool,
pub size: u64,
}
pub fn read_directory(root: &str, relative: &str) -> std::io::Result<Vec<Entry>> {
let directory = paths::open_beneath(root, relative, &paths::OpenOptions::read())?;
let mut entries = Vec::new();
for found in std::fs::read_dir(paths::pinned_path(&directory))? {
let found = found?;
let file_type = found.file_type()?;
let mut size = 0;
if file_type.is_file() {
size = found.path().symlink_metadata().map_or(0, |meta| meta.len());
}
let name = found.file_name().to_string_lossy().into_owned();
let path = if relative.is_empty() {
name.clone()
} else {
format!("{relative}/{name}")
};
entries.push(Entry {
name,
path,
directory: file_type.is_dir(),
size,
});
}
entries.sort_by(|left, right| match (left.directory, right.directory) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => left.name.cmp(&right.name),
});
Ok(entries)
}
#[derive(Debug, Clone, PartialEq)]
pub struct FileContents {
pub path: String,
pub size: u64,
pub binary: bool,
pub truncated: bool,
pub text: String,
pub language: String,
}
#[derive(Debug, thiserror::Error)]
#[error("{0} is not a file")]
pub struct NotAFileError(pub String);
pub fn read_file_for_display(
root: &str,
relative: &str,
limit: u64,
) -> Result<FileContents, NotAFileError> {
let file = paths::open_beneath(root, relative, &paths::OpenOptions::read())
.map_err(|_| NotAFileError(relative.to_owned()))?;
let meta = file
.metadata()
.map_err(|_| NotAFileError(relative.to_owned()))?;
if !meta.is_file() {
return Err(NotAFileError(relative.to_owned()));
}
let raw = read_head(file, limit);
let truncated = meta.len() > limit;
if looks_binary(&raw) {
return Ok(FileContents {
path: relative.to_owned(),
size: meta.len(),
binary: true,
truncated,
text: String::new(),
language: String::new(),
});
}
Ok(FileContents {
path: relative.to_owned(),
size: meta.len(),
binary: false,
truncated,
text: decode_whole(&raw, truncated),
language: language_for(relative),
})
}
fn read_head(mut file: std::fs::File, limit: u64) -> Vec<u8> {
use std::io::Read;
#[expect(clippy::cast_possible_truncation)]
let mut buffer = vec![0_u8; limit.min(usize::MAX as u64) as usize];
let mut read = 0;
while read < buffer.len() {
match file.read(&mut buffer[read..]) {
Ok(0) | Err(_) => break,
Ok(count) => read += count,
}
}
buffer.truncate(read);
buffer
}
fn decode_whole(bytes: &[u8], truncated: bool) -> String {
let text = String::from_utf8_lossy(bytes).into_owned();
if truncated && text.ends_with('\u{FFFD}') {
text[..text.len() - '\u{FFFD}'.len_utf8()].to_owned()
} else {
text
}
}
#[cfg(test)]
mod tests;