use crate::{Error, Result};
use std::path::{Path, PathBuf};
use std::time::Instant;
pub fn to_json_string<T: serde::Serialize + ?Sized>(data: &T, context: &str) -> Result<String> {
serde_json::to_string_pretty(data)
.map_err(|e| Error::config_error(format!("Failed to serialize {} as JSON: {}", context, e)))
}
pub struct CSVBuilder {
headers: Vec<String>,
rows: Vec<Vec<String>>,
}
impl CSVBuilder {
pub fn new(headers: Vec<&str>) -> Self {
Self {
headers: headers.iter().map(|s| s.to_string()).collect(),
rows: Vec::new(),
}
}
pub fn add_row(mut self, values: Vec<&str>) -> Self {
self.rows
.push(values.iter().map(|s| s.to_string()).collect());
self
}
pub fn add_row_owned(mut self, values: Vec<String>) -> Self {
self.rows.push(values);
self
}
pub fn build(self) -> String {
let mut csv = self.headers.join(",") + "\n";
for row in self.rows {
csv.push_str(&row.join(","));
csv.push('\n');
}
csv
}
}
pub struct PathValidator;
impl PathValidator {
pub fn validate_path_in_vault(vault_root: &Path, path: &Path) -> Result<PathBuf> {
let full_path = vault_root.join(path);
let canonical_vault = vault_root
.canonicalize()
.unwrap_or_else(|_| vault_root.to_path_buf());
if let Ok(canonical_full) = full_path.canonicalize() {
if !canonical_full.starts_with(&canonical_vault) {
return Err(Error::path_traversal(full_path));
}
} else {
use std::path::Component;
let mut normalized = PathBuf::new();
for component in full_path.components() {
match component {
Component::ParentDir => {
normalized.pop();
}
Component::Normal(name) => {
normalized.push(name);
}
Component::RootDir => {
normalized.push(component);
}
Component::CurDir => {
}
Component::Prefix(p) => {
normalized.push(p.as_os_str());
}
}
}
if !normalized.starts_with(vault_root) {
return Err(Error::path_traversal(full_path));
}
}
Ok(full_path)
}
pub fn validate_path_exists(vault_root: &Path, path: &Path) -> Result<PathBuf> {
let full_path = Self::validate_path_in_vault(vault_root, path)?;
if !full_path.exists() {
return Err(Error::file_not_found(&full_path));
}
Ok(full_path)
}
pub fn validate_multiple(vault_root: &Path, paths: &[&str]) -> Result<Vec<PathBuf>> {
paths
.iter()
.map(|p| Self::validate_path_in_vault(vault_root, Path::new(p)))
.collect()
}
}
pub struct TransactionBuilder {
transaction_id: String,
start_time: Instant,
}
impl TransactionBuilder {
pub fn new() -> Self {
Self {
transaction_id: uuid::Uuid::new_v4().to_string(),
start_time: Instant::now(),
}
}
pub fn transaction_id(&self) -> &str {
&self.transaction_id
}
pub fn elapsed_ms(&self) -> u64 {
self.start_time.elapsed().as_millis() as u64
}
}
impl Default for TransactionBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct TestData {
name: String,
value: i32,
}
#[test]
fn test_to_json_string() {
let data = TestData {
name: "test".to_string(),
value: 42,
};
let json = to_json_string(&data, "test_data").unwrap();
assert!(json.contains("test"));
assert!(json.contains("42"));
}
#[test]
fn test_csv_builder() {
let csv = CSVBuilder::new(vec!["name", "age"])
.add_row(vec!["Alice", "30"])
.add_row(vec!["Bob", "25"])
.build();
assert!(csv.contains("name,age"));
assert!(csv.contains("Alice,30"));
assert!(csv.contains("Bob,25"));
}
#[test]
fn test_path_validator_valid() {
let vault_root = PathBuf::from("/vault");
let path = Path::new("notes/file.md");
let result = PathValidator::validate_path_in_vault(&vault_root, path);
assert!(result.is_ok());
}
#[test]
fn test_path_validator_traversal() {
let vault_root = PathBuf::from("/vault");
let path = Path::new("../../../etc/passwd");
let result = PathValidator::validate_path_in_vault(&vault_root, path);
assert!(result.is_err());
}
#[test]
fn test_transaction_builder() {
let builder = TransactionBuilder::new();
assert!(!builder.transaction_id().is_empty());
let elapsed = builder.elapsed_ms();
assert!(elapsed < 1000); }
}