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