use std::path::Path;
use serde::Serialize;
pub const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;
pub const MAX_INLINE_SIZE: usize = 50 * 1024 * 1024;
pub fn read_file_bytes(file_path: &str) -> Result<Vec<u8>, ToolErrorInfo> {
let path = Path::new(file_path);
check_file_size(path)?;
std::fs::read(path).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => ToolErrorInfo::new(
"FILE_NOT_FOUND",
format!("File not found: {file_path}"),
"Check the file path and try again.",
),
_ => ToolErrorInfo::new(
"READ_ERROR",
format!("Failed to read file: {e}"),
"Check file permissions.",
),
})
}
pub fn read_file_string(file_path: &str) -> Result<String, ToolErrorInfo> {
let bytes = read_file_bytes(file_path)?;
String::from_utf8(bytes).map_err(|e| {
ToolErrorInfo::new(
"READ_ERROR",
format!("File is not valid UTF-8: {e}"),
"Ensure the file is UTF-8 encoded.",
)
})
}
pub fn write_output_file(output_path: &str, data: &[u8]) -> Result<(), ToolErrorInfo> {
let out = Path::new(output_path);
if let Some(parent) = out.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(|e| {
ToolErrorInfo::new(
"WRITE_ERROR",
format!("Cannot create output directory: {e}"),
"Check write permissions.",
)
})?;
}
}
std::fs::write(out, data).map_err(|e| {
ToolErrorInfo::new(
"WRITE_ERROR",
format!("Failed to write file: {e}"),
"Check disk space and permissions.",
)
})
}
fn check_file_size(path: &Path) -> Result<(), ToolErrorInfo> {
match std::fs::metadata(path) {
Ok(m) if m.len() > MAX_FILE_SIZE => Err(ToolErrorInfo::new(
"INPUT_TOO_LARGE",
format!(
"File '{}' is {} MB, exceeds {} MB limit",
path.display(),
m.len() / 1024 / 1024,
MAX_FILE_SIZE / 1024 / 1024,
),
"Use a smaller file or split the document into sections.",
)),
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(ToolErrorInfo::new(
"FILE_NOT_FOUND",
format!("File not found: '{}'", path.display()),
"Check the file path and try again.",
)),
Err(e) => Err(ToolErrorInfo::new(
"METADATA_ERROR",
format!("Cannot read file metadata for '{}': {e}", path.display()),
"Check file permissions.",
)),
}
}
#[derive(Debug, Serialize)]
pub struct ToolOutput<T: Serialize> {
pub data: T,
pub summary: String,
pub next: Vec<String>,
}
impl<T: Serialize> ToolOutput<T> {
pub fn new(data: T, summary: impl Into<String>, next: Vec<&str>) -> Self {
Self { data, summary: summary.into(), next: next.into_iter().map(String::from).collect() }
}
pub fn to_json_string(&self) -> String {
serde_json::to_string_pretty(self)
.unwrap_or_else(|e| format!(r#"{{"error": "serialization failed: {e}"}}"#))
}
}
#[derive(Debug, Serialize)]
pub struct ToolErrorInfo {
pub code: String,
pub message: String,
pub hint: String,
}
impl ToolErrorInfo {
pub fn new(
code: impl Into<String>,
message: impl Into<String>,
hint: impl Into<String>,
) -> Self {
Self { code: code.into(), message: message.into(), hint: hint.into() }
}
pub fn to_json_string(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| format!("Error: {}", self.message))
}
}
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
pub struct ToolWarningInfo {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
}
impl ToolWarningInfo {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self { code: code.into(), message: message.into(), hint: None }
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_file_bytes_missing_file() {
let err = read_file_bytes("/nonexistent/path.hwpx").unwrap_err();
assert_eq!(err.code, "FILE_NOT_FOUND");
}
#[test]
fn read_file_string_missing_file() {
let err = read_file_string("/nonexistent/path.md").unwrap_err();
assert_eq!(err.code, "FILE_NOT_FOUND");
}
#[test]
fn read_file_string_non_utf8() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("binary.dat");
std::fs::write(&path, [0xFF, 0xFE, 0x00, 0x80]).unwrap();
let err = read_file_string(path.to_str().unwrap()).unwrap_err();
assert_eq!(err.code, "READ_ERROR");
assert!(err.message.contains("UTF-8"));
}
#[test]
fn read_file_bytes_valid_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
std::fs::write(&path, b"hello").unwrap();
let bytes = read_file_bytes(path.to_str().unwrap()).unwrap();
assert_eq!(bytes, b"hello");
}
#[test]
fn read_file_string_valid_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.txt");
std::fs::write(&path, "한글 텍스트").unwrap();
let content = read_file_string(path.to_str().unwrap()).unwrap();
assert_eq!(content, "한글 텍스트");
}
#[test]
fn write_output_file_creates_dirs() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a/b/c/output.hwpx");
write_output_file(path.to_str().unwrap(), b"data").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"data");
}
#[test]
fn write_output_file_overwrites() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.hwpx");
std::fs::write(&path, b"old").unwrap();
write_output_file(path.to_str().unwrap(), b"new").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"new");
}
}