1use crate::{Error, Result};
10use std::path::{Path, PathBuf};
11use std::time::Instant;
12
13pub fn to_json_string<T: serde::Serialize + ?Sized>(data: &T, context: &str) -> Result<String> {
16 serde_json::to_string_pretty(data).map_err(|e| {
17 Error::config_error(format!("Failed to serialize {} as JSON: {}", context, e))
18 })
19}
20
21pub struct CSVBuilder {
24 headers: Vec<String>,
25 rows: Vec<Vec<String>>,
26}
27
28impl CSVBuilder {
29 pub fn new(headers: Vec<&str>) -> Self {
31 Self {
32 headers: headers.iter().map(|s| s.to_string()).collect(),
33 rows: Vec::new(),
34 }
35 }
36
37 pub fn add_row(mut self, values: Vec<&str>) -> Self {
39 self.rows.push(values.iter().map(|s| s.to_string()).collect());
40 self
41 }
42
43 pub fn add_row_owned(mut self, values: Vec<String>) -> Self {
45 self.rows.push(values);
46 self
47 }
48
49 pub fn build(self) -> String {
51 let mut csv = self.headers.join(",") + "\n";
52 for row in self.rows {
53 csv.push_str(&row.join(","));
54 csv.push('\n');
55 }
56 csv
57 }
58}
59
60pub struct PathValidator;
62
63impl PathValidator {
64 pub fn validate_path_in_vault(vault_root: &Path, path: &Path) -> Result<PathBuf> {
66 let full_path = vault_root.join(path);
67
68 let canonical_vault = vault_root.canonicalize()
71 .unwrap_or_else(|_| vault_root.to_path_buf());
72
73 if let Ok(canonical_full) = full_path.canonicalize() {
76 if !canonical_full.starts_with(&canonical_vault) {
77 return Err(Error::path_traversal(full_path));
78 }
79 } else {
80 use std::path::Component;
82 let mut normalized = PathBuf::new();
83 for component in full_path.components() {
84 match component {
85 Component::ParentDir => {
86 normalized.pop();
87 }
88 Component::Normal(name) => {
89 normalized.push(name);
90 }
91 Component::RootDir => {
92 normalized.push(component);
93 }
94 Component::CurDir => {
95 }
97 Component::Prefix(p) => {
98 normalized.push(p.as_os_str());
99 }
100 }
101 }
102
103 if !normalized.starts_with(vault_root) {
104 return Err(Error::path_traversal(full_path));
105 }
106 }
107
108 Ok(full_path)
109 }
110
111 pub fn validate_path_exists(vault_root: &Path, path: &Path) -> Result<PathBuf> {
113 let full_path = Self::validate_path_in_vault(vault_root, path)?;
114 if !full_path.exists() {
115 return Err(Error::file_not_found(&full_path));
116 }
117 Ok(full_path)
118 }
119
120 pub fn validate_multiple(vault_root: &Path, paths: &[&str]) -> Result<Vec<PathBuf>> {
122 paths
123 .iter()
124 .map(|p| Self::validate_path_in_vault(vault_root, Path::new(p)))
125 .collect()
126 }
127}
128
129pub struct TransactionBuilder {
131 transaction_id: String,
132 start_time: Instant,
133}
134
135impl TransactionBuilder {
136 pub fn new() -> Self {
138 Self {
139 transaction_id: uuid::Uuid::new_v4().to_string(),
140 start_time: Instant::now(),
141 }
142 }
143
144 pub fn transaction_id(&self) -> &str {
146 &self.transaction_id
147 }
148
149 pub fn elapsed_ms(&self) -> u64 {
151 self.start_time.elapsed().as_millis() as u64
152 }
153}
154
155impl Default for TransactionBuilder {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use serde::{Deserialize, Serialize};
165
166 #[derive(Serialize, Deserialize)]
167 struct TestData {
168 name: String,
169 value: i32,
170 }
171
172 #[test]
173 fn test_to_json_string() {
174 let data = TestData {
175 name: "test".to_string(),
176 value: 42,
177 };
178 let json = to_json_string(&data, "test_data").unwrap();
179 assert!(json.contains("test"));
180 assert!(json.contains("42"));
181 }
182
183 #[test]
184 fn test_csv_builder() {
185 let csv = CSVBuilder::new(vec!["name", "age"])
186 .add_row(vec!["Alice", "30"])
187 .add_row(vec!["Bob", "25"])
188 .build();
189
190 assert!(csv.contains("name,age"));
191 assert!(csv.contains("Alice,30"));
192 assert!(csv.contains("Bob,25"));
193 }
194
195 #[test]
196 fn test_path_validator_valid() {
197 let vault_root = PathBuf::from("/vault");
198 let path = Path::new("notes/file.md");
199 let result = PathValidator::validate_path_in_vault(&vault_root, path);
200 assert!(result.is_ok());
201 }
202
203 #[test]
204 fn test_path_validator_traversal() {
205 let vault_root = PathBuf::from("/vault");
206 let path = Path::new("../../../etc/passwd");
207 let result = PathValidator::validate_path_in_vault(&vault_root, path);
208 assert!(result.is_err());
209 }
210
211 #[test]
212 fn test_transaction_builder() {
213 let builder = TransactionBuilder::new();
214 assert!(!builder.transaction_id().is_empty());
215 let elapsed = builder.elapsed_ms();
216 assert!(elapsed < 1000); }
218}