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