Skip to main content

sheetkit_core/
protection.rs

1//! Workbook protection configuration and legacy password hashing.
2
3/// Configuration for workbook protection.
4#[derive(Debug, Clone, Default)]
5pub struct WorkbookProtectionConfig {
6    /// Optional password to protect the workbook.
7    pub password: Option<String>,
8    /// Lock the workbook structure (prevent adding/removing/renaming sheets).
9    pub lock_structure: bool,
10    /// Lock the workbook window position and size.
11    pub lock_windows: bool,
12    /// Lock revision tracking.
13    pub lock_revision: bool,
14}
15
16/// Legacy password hash used by Excel for workbook protection.
17///
18/// This is NOT cryptographically secure -- it is the same hash algorithm
19/// that Excel uses for the `workbookPassword` attribute. The result is a
20/// 16-bit value that is typically stored as a 4-character uppercase hex string.
21pub fn legacy_password_hash(password: &str) -> u16 {
22    if password.is_empty() {
23        return 0;
24    }
25    let mut hash: u16 = 0;
26    let bytes = password.as_bytes();
27    for (i, &byte) in bytes.iter().enumerate() {
28        let mut intermediate = byte as u16;
29        intermediate = (intermediate << (i + 1)) | (intermediate >> (15 - i));
30        hash ^= intermediate;
31    }
32    hash ^= bytes.len() as u16;
33    hash ^= 0xCE4B;
34    hash
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn test_legacy_password_hash_empty() {
43        assert_eq!(legacy_password_hash(""), 0);
44    }
45
46    #[test]
47    fn test_legacy_password_hash_known_values() {
48        // "password" should produce a deterministic non-zero hash
49        let h = legacy_password_hash("password");
50        assert_ne!(h, 0);
51        // Verify it is stable across calls
52        assert_eq!(h, legacy_password_hash("password"));
53
54        // "test" should produce a different hash than "password"
55        let h2 = legacy_password_hash("test");
56        assert_ne!(h2, 0);
57        assert_ne!(h, h2);
58
59        // Single character
60        let h3 = legacy_password_hash("a");
61        assert_ne!(h3, 0);
62    }
63
64    #[test]
65    fn test_legacy_password_hash_format() {
66        // Verify the hash fits in a 4-char hex string
67        let h = legacy_password_hash("password");
68        let hex = format!("{:04X}", h);
69        assert_eq!(hex.len(), 4);
70    }
71
72    #[test]
73    fn test_workbook_protection_config_default() {
74        let config = WorkbookProtectionConfig::default();
75        assert!(config.password.is_none());
76        assert!(!config.lock_structure);
77        assert!(!config.lock_windows);
78        assert!(!config.lock_revision);
79    }
80}