lib3mf_core/utils/
c14n.rs

1use crate::error::Result;
2use quick_xml::events::{BytesStart, Event};
3use std::collections::BTreeMap;
4use std::io::Write;
5
6/// A simplified C14N (Canonical XML) implementation for 3MF signatures.
7///
8/// This handles:
9/// - Attribute sorting
10/// - Namespace handling (basic)
11/// - Empty element expansion
12///
13/// Note: This is NOT a fully compliant W3C C14N implementation, but sufficient for
14/// standard 3MF signature use cases involving `SignedInfo`.
15pub struct Canonicalizer;
16
17impl Canonicalizer {
18    /// Canonicalize a specific subtree (e.g., "SignedInfo")
19    pub fn canonicalize_subtree(input_xml: &str, target_tag: &str) -> Result<Vec<u8>> {
20        let mut reader = quick_xml::Reader::from_str(input_xml);
21        reader.config_mut().trim_text(true);
22        let mut writer = Vec::new();
23        let mut buf = Vec::new();
24        let mut capturing = false;
25        let mut depth = 0;
26
27        loop {
28            match reader.read_event_into(&mut buf) {
29                Ok(Event::Start(ref e)) => {
30                    let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
31                    if name == target_tag {
32                        capturing = true;
33                        depth = 1;
34                        write_start_tag(&mut writer, e)?;
35                    } else if capturing {
36                        depth += 1;
37                        write_start_tag(&mut writer, e)?;
38                    }
39                }
40                Ok(Event::End(ref e)) => {
41                    if capturing {
42                        write!(writer, "</{}>", String::from_utf8_lossy(e.name().as_ref()))
43                            .map_err(crate::error::Lib3mfError::Io)?;
44                        depth -= 1;
45                        if depth == 0 {
46                            break; // Done capturing target
47                        }
48                    }
49                }
50                Ok(Event::Empty(ref e)) => {
51                    let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
52                    if name == target_tag {
53                        // Empty target tag <Target/> -> <Target></Target>
54                        write_start_tag(&mut writer, e)?;
55                        write!(writer, "</{}>", name).map_err(crate::error::Lib3mfError::Io)?;
56                        break;
57                    } else if capturing {
58                        write_start_tag(&mut writer, e)?;
59                        write!(writer, "</{}>", name).map_err(crate::error::Lib3mfError::Io)?;
60                    }
61                }
62                Ok(Event::Text(e)) => {
63                    if capturing {
64                        let content = String::from_utf8_lossy(&e.into_inner()).to_string();
65                        writer
66                            .write_all(content.as_bytes())
67                            .map_err(crate::error::Lib3mfError::Io)?;
68                    }
69                }
70                Ok(Event::Eof) => break,
71                Err(e) => return Err(crate::error::Lib3mfError::InvalidStructure(e.to_string())),
72                _ => {}
73            }
74            buf.clear();
75        }
76
77        if writer.is_empty() {
78            return Err(crate::error::Lib3mfError::Validation(format!(
79                "Tag <{}> not found for C14N",
80                target_tag
81            )));
82        }
83
84        Ok(writer)
85    }
86
87    /// Canonicalize strict whole document
88    pub fn canonicalize(input_xml: &str) -> Result<Vec<u8>> {
89        let mut reader = quick_xml::Reader::from_str(input_xml);
90        reader.config_mut().trim_text(true);
91        let mut writer = Vec::new();
92        let mut buf = Vec::new();
93
94        // Very basic canonicalization:
95        // 1. Parse events
96        // 2. Sort attributes
97        // 3. Write back
98        // Real C14N is complex (namespace propagation, etc.).
99        // 3MF signatures usually just sign the exact bytes of the `SignedInfo` in the package,
100        // but strict construction requires C14N.
101
102        loop {
103            match reader.read_event_into(&mut buf) {
104                Ok(Event::Start(ref e)) => {
105                    write_start_tag(&mut writer, e)?;
106                }
107                Ok(Event::End(ref e)) => {
108                    write!(writer, "</{}>", String::from_utf8_lossy(e.name().as_ref()))
109                        .map_err(crate::error::Lib3mfError::Io)?;
110                }
111                Ok(Event::Empty(ref e)) => {
112                    // C14N expands empty tags: <a/> -> <a></a>
113                    write_start_tag(&mut writer, e)?;
114                    write!(writer, "</{}>", String::from_utf8_lossy(e.name().as_ref()))
115                        .map_err(crate::error::Lib3mfError::Io)?;
116                }
117                Ok(Event::Text(e)) => {
118                    // Escape basic entities
119                    let content = String::from_utf8_lossy(&e.into_inner()).to_string();
120                    // Basic escaping should already be done or preserved?
121                    // Quick-xml text contains unescaped content usually?
122                    // Actually, if we read raw, we get entities.
123                    // For verification, we often operate on the byte stream.
124                    // If we are reconstructing, we need to be careful.
125                    writer
126                        .write_all(content.as_bytes())
127                        .map_err(crate::error::Lib3mfError::Io)?;
128                }
129                Ok(Event::Eof) => break,
130                Err(e) => return Err(crate::error::Lib3mfError::InvalidStructure(e.to_string())),
131                _ => {} // Ignore comments, PIs for simple C14N
132            }
133            buf.clear();
134        }
135
136        Ok(writer)
137    }
138}
139
140fn write_start_tag(writer: &mut Vec<u8>, e: &BytesStart) -> Result<()> {
141    write!(writer, "<{}", String::from_utf8_lossy(e.name().as_ref()))
142        .map_err(crate::error::Lib3mfError::Io)?;
143
144    // Sort attributes by name
145    let mut attrs: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
146    for attr in e.attributes() {
147        let attr = attr.map_err(|e| crate::error::Lib3mfError::InvalidStructure(e.to_string()))?;
148        let val: &[u8] = attr.value.as_ref();
149        attrs.insert(attr.key.as_ref().to_vec(), val.to_vec());
150    }
151
152    for (key, value) in &attrs {
153        write!(
154            writer,
155            " {}=\"{}\"",
156            String::from_utf8_lossy(key),
157            String::from_utf8_lossy(value)
158        )
159        .map_err(crate::error::Lib3mfError::Io)?;
160    }
161
162    write!(writer, ">").map_err(crate::error::Lib3mfError::Io)?;
163    Ok(())
164}