1use anyhow::{anyhow, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum UnsupportedFeature {
11 MergedCells {
13 sheet: String,
14 range: String,
15 },
16 PivotTable {
18 sheet: String,
19 },
20 DataValidation {
22 sheet: String,
23 range: String,
24 },
25 ConditionalFormatting {
27 sheet: String,
28 },
29 ArrayFormulas {
31 sheet: String,
32 },
33 ProtectedSheet {
35 sheet: String,
36 password_protected: bool,
37 },
38 ExternalReferences {
40 sheet: String,
41 },
42 Charts {
44 sheet: String,
45 count: usize,
46 },
47 EmbeddedObjects {
49 sheet: String,
50 object_type: String,
51 },
52}
53
54impl UnsupportedFeature {
55 pub fn description(&self) -> String {
57 match self {
58 Self::MergedCells { sheet, range } => {
59 format!(
60 "Merged cells detected in sheet '{}' range '{}'. Merged cells will be read as individual cells. Merge structure will be lost on write.",
61 sheet, range
62 )
63 }
64 Self::PivotTable { sheet } => {
65 format!(
66 "Pivot table detected in sheet '{}'. Pivot tables are not fully supported - data will be read as static values. Pivot structure, filters, and calculations will be lost.",
67 sheet
68 )
69 }
70 Self::DataValidation { sheet, range } => {
71 format!(
72 "Data validation detected in sheet '{}' range '{}'. Validation rules will be lost when modifying or writing this file.",
73 sheet, range
74 )
75 }
76 Self::ConditionalFormatting { sheet } => {
77 format!(
78 "Conditional formatting detected in sheet '{}'. Formatting rules will be preserved on read but may not be editable through xls-rs.",
79 sheet
80 )
81 }
82 Self::ArrayFormulas { sheet } => {
83 format!(
84 "Array formulas detected in sheet '{}'. Array formulas may be read as static values. Dynamic calculation behavior may be lost.",
85 sheet
86 )
87 }
88 Self::ProtectedSheet { sheet, password_protected } => {
89 if *password_protected {
90 format!(
91 "Sheet '{}' is password protected. Content is readable but cannot be modified. Remove protection to enable editing.",
92 sheet
93 )
94 } else {
95 format!(
96 "Sheet '{}' is protected. Content is readable but editing may be limited.",
97 sheet
98 )
99 }
100 }
101 Self::ExternalReferences { sheet } => {
102 format!(
103 "External references detected in sheet '{}'. External links may be broken or not accessible. Consider consolidating data.",
104 sheet
105 )
106 }
107 Self::Charts { sheet, count } => {
108 format!(
109 "Charts detected in sheet '{}' ({} chart(s)). Charts are read-only through xls-rs - data is visible but chart configuration cannot be modified.",
110 sheet, count
111 )
112 }
113 Self::EmbeddedObjects { sheet, object_type } => {
114 format!(
115 "Embedded {} detected in sheet '{}'. Visual elements like images and shapes are not fully supported - they may be lost on read/write.",
116 object_type, sheet
117 )
118 }
119 }
120 }
121
122 pub fn severity(&self) -> FeatureSeverity {
124 match self {
125 Self::MergedCells { .. } => FeatureSeverity::Warning,
126 Self::PivotTable { .. } => FeatureSeverity::Limitation,
127 Self::DataValidation { .. } => FeatureSeverity::Warning,
128 Self::ConditionalFormatting { .. } => FeatureSeverity::Warning,
129 Self::ArrayFormulas { .. } => FeatureSeverity::Limitation,
130 Self::ProtectedSheet { password_protected: true, .. } => FeatureSeverity::Error,
131 Self::ProtectedSheet { password_protected: false, .. } => FeatureSeverity::Warning,
132 Self::ExternalReferences { .. } => FeatureSeverity::Warning,
133 Self::Charts { .. } => FeatureSeverity::Warning,
134 Self::EmbeddedObjects { .. } => FeatureSeverity::Limitation,
135 }
136 }
137
138 pub fn guidance(&self) -> Option<String> {
140 match self {
141 Self::MergedCells { .. } => Some(
142 "To preserve merged cells, consider using Excel directly or exporting to a format that maintains merge structure.".to_string()
143 ),
144 Self::PivotTable { .. } => Some(
145 "For full pivot table support, use Excel directly. To work with pivot data, consider flattening the pivot table to static values first.".to_string()
146 ),
147 Self::DataValidation { .. } => Some(
148 "Data validation rules can be re-applied after modification using the conditional-format command.".to_string()
149 ),
150 Self::ProtectedSheet { password_protected: true, .. } => Some(
151 "Unprotect the sheet in Excel with the password to enable full editing capabilities.".to_string()
152 ),
153 Self::ExternalReferences { .. } => Some(
154 "Replace external references with static values or consolidate external data into the workbook.".to_string()
155 ),
156 _ => None,
157 }
158 }
159
160 pub fn to_error(&self) -> anyhow::Error {
162 let desc = self.description();
163 let severity = self.severity();
164 let guidance = self.guidance();
165
166 let msg = if let Some(g) = guidance {
167 format!("[{}] {}. Guidance: {}", severity, desc, g)
168 } else {
169 format!("[{}] {}", severity, desc)
170 };
171
172 anyhow!(msg)
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
178pub enum FeatureSeverity {
179 Info,
181 Warning,
183 Limitation,
185 Error,
187}
188
189impl std::fmt::Display for FeatureSeverity {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 match self {
192 FeatureSeverity::Info => write!(f, "INFO"),
193 FeatureSeverity::Warning => write!(f, "WARNING"),
194 FeatureSeverity::Limitation => write!(f, "LIMITATION"),
195 FeatureSeverity::Error => write!(f, "ERROR"),
196 }
197 }
198}
199
200pub struct FeatureDetector;
205
206impl FeatureDetector {
207 pub fn detect_potential_issues(_path: &str) -> Result<Vec<UnsupportedFeature>> {
213 Ok(Vec::new())
221 }
222
223 pub fn heuristic_check(path: &str) -> Vec<UnsupportedFeature> {
229 let mut issues = Vec::new();
230
231 let path_lower = path.to_lowercase();
232
233 if let Ok(metadata) = std::fs::metadata(path) {
235 if metadata.len() > 10 * 1024 * 1024 {
236 issues.push(UnsupportedFeature::PivotTable {
238 sheet: "unknown".to_string(),
239 });
240 }
241 }
242
243 if path_lower.ends_with(".ods") {
245 }
247
248 issues
249 }
250
251 pub fn validate_for_write(path: &str) -> Result<()> {
253 let issues = Self::detect_potential_issues(path)?;
255
256 let errors: Vec<_> = issues
258 .into_iter()
259 .filter(|f| f.severity() == FeatureSeverity::Error)
260 .collect();
261
262 if !errors.is_empty() {
263 let error_messages: Vec<String> = errors
264 .iter()
265 .map(|f| f.description())
266 .collect();
267 return Err(anyhow!(
268 "File contains features that prevent write operations:\n{}",
269 error_messages.join("\n")
270 ));
271 }
272
273 Ok(())
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_unsupported_feature_description() {
283 let feature = UnsupportedFeature::MergedCells {
284 sheet: "Sheet1".to_string(),
285 range: "A1:B2".to_string(),
286 };
287 let desc = feature.description();
288 assert!(desc.contains("Merged cells"));
289 assert!(desc.contains("Sheet1"));
290 assert!(desc.contains("A1:B2"));
291 }
292
293 #[test]
294 fn test_feature_severity() {
295 assert_eq!(
296 UnsupportedFeature::MergedCells {
297 sheet: "S".to_string(),
298 range: "A1".to_string(),
299 }
300 .severity(),
301 FeatureSeverity::Warning
302 );
303
304 assert_eq!(
305 UnsupportedFeature::ProtectedSheet {
306 sheet: "S".to_string(),
307 password_protected: true,
308 }
309 .severity(),
310 FeatureSeverity::Error
311 );
312 }
313
314 #[test]
315 fn test_to_error() {
316 let feature = UnsupportedFeature::PivotTable {
317 sheet: "Sheet1".to_string(),
318 };
319 let error = feature.to_error();
320 assert!(error.to_string().contains("Pivot table"));
321 }
322}