Skip to main content

xls_rs/excel/
feature_detector.rs

1//! Excel feature detection and structured errors
2//!
3//! This module provides utilities for detecting Excel features that may not be fully supported
4//! and returns structured error messages with actionable guidance.
5
6use anyhow::{anyhow, Result};
7
8/// Unsupported Excel feature with structured error information
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum UnsupportedFeature {
11    /// Merged cells (partial support - cells are read but merge status is lost)
12    MergedCells {
13        sheet: String,
14        range: String,
15    },
16    /// Pivot tables (not supported - data may be read but pivot structure is lost)
17    PivotTable {
18        sheet: String,
19    },
20    /// Data validation (not supported - data is readable but validation rules are lost)
21    DataValidation {
22        sheet: String,
23        range: String,
24    },
25    /// Conditional formatting (read-only - formats are visible but not editable)
26    ConditionalFormatting {
27        sheet: String,
28    },
29    /// Array formulas (limited support - formulas may be read as static values)
30    ArrayFormulas {
31        sheet: String,
32    },
33    /// Protected sheets (read-only - content readable but cannot be modified)
34    ProtectedSheet {
35        sheet: String,
36        password_protected: bool,
37    },
38    /// External references/links (not supported - references may be broken)
39    ExternalReferences {
40        sheet: String,
41    },
42    /// Charts (read-only - data visible but chart configuration is lost)
43    Charts {
44        sheet: String,
45        count: usize,
46    },
47    /// Images/Objects (not supported - visual elements are lost)
48    EmbeddedObjects {
49        sheet: String,
50        object_type: String,
51    },
52}
53
54impl UnsupportedFeature {
55    /// Get a user-friendly description of the unsupported feature
56    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    /// Get the severity level of the unsupported feature
123    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    /// Get actionable guidance for working around the limitation
139    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    /// Convert to an anyhow::Error with full context
161    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/// Severity level of unsupported features
177#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
178pub enum FeatureSeverity {
179    /// Informational - feature is read-only but functional
180    Info,
181    /// Warning - feature has limited support but data is preserved
182    Warning,
183    /// Limitation - feature is partially supported with some data loss
184    Limitation,
185    /// Error - feature prevents the operation from completing
186    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
200/// Excel file feature detector
201///
202/// This struct provides methods to detect potentially unsupported features
203/// in Excel files before attempting operations that may fail or lose data.
204pub struct FeatureDetector;
205
206impl FeatureDetector {
207    /// Detect features that may affect read/write operations
208    ///
209    /// This is a placeholder for future implementation. Currently, calamine
210    /// doesn't expose detailed feature information. This would be enhanced
211    /// when using a more advanced Excel library or adding custom parsing.
212    pub fn detect_potential_issues(_path: &str) -> Result<Vec<UnsupportedFeature>> {
213        // TODO: Implement actual feature detection
214        // This would require:
215        // 1. Parsing Excel XML structure directly
216        // 2. Detecting merged cells, pivot tables, etc.
217        // 3. Or using a library that exposes this information
218
219        // For now, return empty vector
220        Ok(Vec::new())
221    }
222
223    /// Check if a file is likely to contain unsupported features
224    ///
225    /// This is a heuristic check based on file extension and size.
226    /// Complex Excel files (large, multiple sheets) are more likely
227    /// to have unsupported features.
228    pub fn heuristic_check(path: &str) -> Vec<UnsupportedFeature> {
229        let mut issues = Vec::new();
230
231        let path_lower = path.to_lowercase();
232
233        // Check for very large files (more likely to have complex features)
234        if let Ok(metadata) = std::fs::metadata(path) {
235            if metadata.len() > 10 * 1024 * 1024 {
236                // File > 10MB
237                issues.push(UnsupportedFeature::PivotTable {
238                    sheet: "unknown".to_string(),
239                });
240            }
241        }
242
243        // ODS files have different feature set
244        if path_lower.ends_with(".ods") {
245            // ODS may have different limitations
246        }
247
248        issues
249    }
250
251    /// Validate that a file doesn't contain features that would prevent write operations
252    pub fn validate_for_write(path: &str) -> Result<()> {
253        // Check for potential issues
254        let issues = Self::detect_potential_issues(path)?;
255
256        // Filter to only error-level issues for write validation
257        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}