1use crate::operations::{show_file_info, FileInfo};
10use crate::{AionError, Result};
11use std::path::Path;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ExportFormat {
16 Json,
18 Yaml,
20 Csv,
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct ExportData {
27 pub export_version: String,
29 pub source_file: String,
31 pub file_info: ExportFileInfo,
33 pub versions: Vec<ExportVersion>,
35 pub signatures: Vec<ExportSignature>,
37}
38
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
41pub struct ExportFileInfo {
42 pub file_id: String,
44 pub version_count: u64,
46 pub current_version: u64,
48}
49
50#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
52pub struct ExportVersion {
53 pub version: u64,
55 pub author_id: u64,
57 pub timestamp: String,
59 pub message: String,
61 pub rules_hash: String,
63 pub parent_hash: Option<String>,
65}
66
67#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
69pub struct ExportSignature {
70 pub version: u64,
72 pub author_id: u64,
74 pub public_key: String,
76 pub verified: bool,
78}
79
80pub fn export_file(
91 path: &Path,
92 format: ExportFormat,
93 registry: &crate::key_registry::KeyRegistry,
94) -> Result<String> {
95 let file_info = show_file_info(path, registry)?;
96
97 match format {
98 ExportFormat::Json => export_json(path, &file_info),
99 ExportFormat::Yaml => export_yaml(path, &file_info),
100 ExportFormat::Csv => export_csv(&file_info),
101 }
102}
103
104fn export_json(path: &Path, file_info: &FileInfo) -> Result<String> {
106 let export_data = build_export_data(path, file_info);
107 serde_json::to_string_pretty(&export_data).map_err(|e| AionError::InvalidFormat {
108 reason: format!("JSON serialization failed: {e}"),
109 })
110}
111
112fn export_yaml(path: &Path, file_info: &FileInfo) -> Result<String> {
114 let export_data = build_export_data(path, file_info);
115 serde_yaml::to_string(&export_data).map_err(|e| AionError::InvalidFormat {
116 reason: format!("YAML serialization failed: {e}"),
117 })
118}
119
120fn export_csv(file_info: &FileInfo) -> Result<String> {
122 let mut output = String::new();
123
124 output.push_str("version,author_id,timestamp,message,rules_hash,parent_hash\n");
126
127 for version in &file_info.versions {
129 let timestamp = format_timestamp_nanos(version.timestamp);
130 let rules_hash = hex::encode(version.rules_hash);
131 let parent_hash = version.parent_hash.map(hex::encode).unwrap_or_default();
132
133 let escaped_message = escape_csv_field(&version.message);
135
136 output.push_str(&format!(
137 "{},{},{},{},{},{}\n",
138 version.version_number,
139 version.author_id,
140 timestamp,
141 escaped_message,
142 rules_hash,
143 parent_hash
144 ));
145 }
146
147 Ok(output)
148}
149
150fn build_export_data(path: &Path, file_info: &FileInfo) -> ExportData {
152 ExportData {
153 export_version: "1.0".to_string(),
154 source_file: path.display().to_string(),
155 file_info: ExportFileInfo {
156 file_id: format!("0x{:016x}", file_info.file_id),
157 version_count: file_info.version_count,
158 current_version: file_info.current_version,
159 },
160 versions: file_info
161 .versions
162 .iter()
163 .map(|v| ExportVersion {
164 version: v.version_number,
165 author_id: v.author_id,
166 timestamp: format_timestamp_nanos(v.timestamp),
167 message: v.message.clone(),
168 rules_hash: hex::encode(v.rules_hash),
169 parent_hash: v.parent_hash.map(hex::encode),
170 })
171 .collect(),
172 signatures: file_info
173 .signatures
174 .iter()
175 .map(|s| ExportSignature {
176 version: s.version_number,
177 author_id: s.author_id,
178 public_key: hex::encode(s.public_key),
179 verified: s.verified,
180 })
181 .collect(),
182 }
183}
184
185fn escape_csv_field(field: &str) -> String {
187 if field.contains(',') || field.contains('"') || field.contains('\n') {
188 format!("\"{}\"", field.replace('"', "\"\""))
189 } else {
190 field.to_string()
191 }
192}
193
194fn format_timestamp_nanos(nanos: u64) -> String {
196 let secs = nanos / 1_000_000_000;
197 let days = secs / 86400;
198 let time_of_day = secs % 86400;
199 let hours = time_of_day / 3600;
200 let minutes = (time_of_day % 3600) / 60;
201 let seconds = time_of_day % 60;
202 let (year, month, day) = days_to_ymd(days);
203 format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
204}
205
206fn days_to_ymd(days: u64) -> (u64, u64, u64) {
208 let mut remaining_days = days as i64;
209 let mut year = 1970u64;
210 loop {
211 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
212 if remaining_days < days_in_year {
213 break;
214 }
215 remaining_days = remaining_days.saturating_sub(days_in_year);
216 year = year.saturating_add(1);
217 }
218 let days_in_months: [i64; 12] = if is_leap_year(year) {
219 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
220 } else {
221 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
222 };
223 let mut month = 1u64;
224 for days_in_month in days_in_months {
225 if remaining_days < days_in_month {
226 break;
227 }
228 remaining_days = remaining_days.saturating_sub(days_in_month);
229 month = month.saturating_add(1);
230 }
231 let day = (remaining_days as u64).saturating_add(1);
232 (year, month, day)
233}
234
235const fn is_leap_year(year: u64) -> bool {
236 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
237}
238
239pub fn import_json(json_data: &str) -> Result<ExportData> {
248 serde_json::from_str(json_data).map_err(|e| AionError::InvalidFormat {
249 reason: format!("JSON parse failed: {e}"),
250 })
251}
252
253pub fn import_yaml(yaml_data: &str) -> Result<ExportData> {
255 serde_yaml::from_str(yaml_data).map_err(|e| AionError::InvalidFormat {
256 reason: format!("YAML parse failed: {e}"),
257 })
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_escape_csv_field_simple() {
266 assert_eq!(escape_csv_field("hello"), "hello");
267 }
268
269 #[test]
270 fn test_escape_csv_field_with_comma() {
271 assert_eq!(escape_csv_field("hello, world"), "\"hello, world\"");
272 }
273
274 #[test]
275 fn test_escape_csv_field_with_quotes() {
276 assert_eq!(escape_csv_field("say \"hi\""), "\"say \"\"hi\"\"\"");
277 }
278
279 #[test]
280 fn test_format_timestamp() {
281 let ts = 1704067200_000_000_000u64;
283 let formatted = format_timestamp_nanos(ts);
284 assert!(formatted.starts_with("2024-01-01"));
285 }
286}