Skip to main content

asic_e/
container.rs

1//! In-memory representation of an ASiC-E container.
2
3use std::io::{Cursor, Read, Seek, Write};
4
5use zip::write::SimpleFileOptions;
6use zip::{CompressionMethod, ZipArchive, ZipWriter};
7
8use crate::{manifest, LibError, Result, MIMETYPE};
9
10/// A data file stored in the container.
11#[derive(Debug, Clone)]
12pub struct DataFile {
13    /// File name inside the archive.
14    pub name: String,
15    /// Media type recorded in the manifest.
16    pub mime_type: String,
17    /// Raw file content.
18    pub content: Vec<u8>,
19}
20
21/// A signature document (`META-INF/*signatures*.xml`).
22#[derive(Debug, Clone)]
23pub struct SignatureFile {
24    /// Document name inside the archive.
25    pub name: String,
26    /// The signature document as opaque XML.
27    pub xml: String,
28}
29
30/// Policy for `Container::open_with`.
31#[derive(Debug, Clone)]
32pub struct OpenOptions {
33    /// Require the `mimetype` entry to be present.
34    ///
35    /// Optional in EN 319 162-1 but additional profiles (e.g. BDOC) may require it.
36    /// Defaults to `false`.
37    pub require_mimetype: bool,
38}
39
40impl Default for OpenOptions {
41    fn default() -> Self {
42        Self {
43            require_mimetype: false,
44        }
45    }
46}
47
48/// An ASiC-E container held in memory.
49#[derive(Debug, Default)]
50pub struct Container {
51    data_files: Vec<DataFile>,
52    signatures: Vec<SignatureFile>,
53    /// Potential structural anomalies found while opening a container.
54    warnings: Vec<String>,
55}
56
57impl Container {
58    /// Create an empty container.
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Data files in the container.
64    pub fn data_files(&self) -> &[DataFile] {
65        &self.data_files
66    }
67
68    /// Signature documents in the container.
69    pub fn signatures(&self) -> &[SignatureFile] {
70        &self.signatures
71    }
72
73    /// Structural warnings collected when opening the container.
74    pub fn warnings(&self) -> &[String] {
75        &self.warnings
76    }
77
78    /// Add a data file.
79    ///
80    /// # Errors
81    /// If the name is not relative, a duplicate, or reserved.
82    pub fn add_file(
83        &mut self,
84        name: impl Into<String>,
85        mime_type: impl Into<String>,
86        content: Vec<u8>,
87    ) -> Result<()> {
88        let name = name.into();
89        validate_entry_name(&name)?;
90        if self.data_files.iter().any(|f| f.name == name) {
91            return Err(LibError::Container(format!("duplicate file name: {name}")));
92        }
93        self.data_files.push(DataFile {
94            name,
95            mime_type: mime_type.into(),
96            content,
97        });
98        Ok(())
99    }
100
101    /// Append a signature document.
102    ///
103    /// The entry name is assigned automatically as `META-INF/signaturesN.xml`,
104    /// using the first free index `N`.
105    pub fn add_signature_xml(&mut self, xml: String) -> &SignatureFile {
106        let mut n = self.signatures.len();
107        let name = loop {
108            let candidate = format!("META-INF/signatures{n}.xml");
109            if !self.signatures.iter().any(|s| s.name == candidate) {
110                break candidate;
111            }
112            n += 1;
113        };
114        self.signatures.push(SignatureFile { name, xml });
115        self.signatures.last().expect("a signature should exist")
116    }
117
118    /// Open a container from any seekable reader, using default `OpenOptions`.
119    pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
120        Self::open_with(reader, &OpenOptions::default())
121    }
122
123    /// Open a container from any seekable reader under an explicit policy.
124    pub fn open_with<R: Read + Seek>(reader: R, opts: &OpenOptions) -> Result<Self> {
125        let mut zip = ZipArchive::new(reader)?;
126        let mut container = Container::default();
127
128        let mut names: Vec<String> = Vec::with_capacity(zip.len());
129        for i in 0..zip.len() {
130            names.push(zip.by_index(i)?.name().to_owned());
131        }
132
133        // EN 319 162-1 v1.1.1, A.1. If present, the mimetype shall
134        // - be the first file in the ASiC container
135        // - not be compressed
136        // BDOC mandates the presence of the mimetype file.
137        let mimetype_present = names.iter().any(|n| n == "mimetype");
138        if let Some(n) = names.first() {
139            if n == "mimetype" {
140                let entry = zip.by_index(0)?;
141                if entry.compression() != CompressionMethod::Stored {
142                    container.warnings.push("'mimetype' is compressed".into());
143                }
144            } else if mimetype_present {
145                container
146                    .warnings
147                    .push("'mimetype' is not the first zip entry".into());
148            }
149        }
150        if mimetype_present {
151            let mimetype = read_entry(&mut zip, "mimetype")?;
152            let mimetype = String::from_utf8_lossy(&mimetype);
153            if mimetype.trim() != MIMETYPE {
154                return Err(LibError::Container(format!(
155                    "unexpected container mime type: {}",
156                    mimetype.trim()
157                )));
158            }
159        } else if opts.require_mimetype {
160            return Err(LibError::Container("missing mimetype entry".into()));
161        } else {
162            container
163                .warnings
164                .push("container has no mimetype entry".into());
165        }
166
167        // EN 319 162-1 V1.1.1 ยง 5.3.3 (Table 5)
168        // 'META-INF/manifest.xml shall be present' for ASiC-E with XAdES signatures
169        let manifest_types = match read_entry(&mut zip, "META-INF/manifest.xml") {
170            Ok(bytes) => manifest::parse(&bytes)?,
171            Err(_) => {
172                return Err(LibError::Container(
173                    "container has no META-INF/manifest.xml".into(),
174                ));
175            }
176        };
177
178        for name in &names {
179            if name == "mimetype" || name.ends_with('/') {
180                continue;
181            }
182            let bytes = read_entry(&mut zip, name)?;
183            if let Some(rest) = name.strip_prefix("META-INF/") {
184                if is_signature_entry(rest) {
185                    container.signatures.push(SignatureFile {
186                        name: name.clone(),
187                        xml: String::from_utf8(bytes)
188                            .map_err(|_| LibError::Xml(format!("{name} is not valid UTF-8")))?,
189                    });
190                }
191                // '4.4.3.2 Note 5' of EN 319 162-1 v1.1.1:
192                // Other file objects in META-INF/ need not be parsed and interpreted for the
193                // purpose of the ASiC container validation, provided that they do not contain the
194                // string "signature" or "timestamp" or "manifest" or "container.xml".
195                // TODO: implement this check, and the processing of the other allowed files.
196                continue;
197            }
198            let mime_type = manifest_types
199                .iter()
200                .find(|(n, _)| n == name)
201                .map(|(_, m)| m.clone())
202                .ok_or_else(|| {
203                    LibError::Container(format!("missing MIME type in manifest for {name}"))
204                })?;
205            container.data_files.push(DataFile {
206                name: name.clone(),
207                mime_type,
208                content: bytes,
209            });
210        }
211
212        if container.data_files.is_empty() {
213            return Err(LibError::Container("container has no data files".into()));
214        }
215        Ok(container)
216    }
217
218    /// Open a container from a byte buffer.
219    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
220        Self::open(Cursor::new(bytes))
221    }
222
223    /// Open a container from a byte buffer under an explicit policy.
224    pub fn from_bytes_with(bytes: &[u8], opts: &OpenOptions) -> Result<Self> {
225        Self::open_with(Cursor::new(bytes), opts)
226    }
227
228    /// Open a container file from disk.
229    pub fn open_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
230        Self::open(std::fs::File::open(path)?)
231    }
232
233    /// Open a container file from disk under an explicit policy.
234    pub fn open_file_with(path: impl AsRef<std::path::Path>, opts: &OpenOptions) -> Result<Self> {
235        Self::open_with(std::fs::File::open(path)?, opts)
236    }
237
238    fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
239        if self.data_files.is_empty() {
240            return Err(LibError::Container("container has no data files".into()));
241        }
242        let mut zip = ZipWriter::new(writer);
243
244        zip.start_file(
245            "mimetype",
246            SimpleFileOptions::default().compression_method(CompressionMethod::Stored),
247        )?;
248        zip.write_all(MIMETYPE.as_bytes())?;
249
250        let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
251        for file in &self.data_files {
252            zip.start_file(&file.name, deflated)?;
253            zip.write_all(&file.content)?;
254        }
255
256        zip.start_file("META-INF/manifest.xml", deflated)?;
257        zip.write_all(&manifest::build(&self.data_files))?;
258
259        for sig in &self.signatures {
260            zip.start_file(&sig.name, deflated)?;
261            zip.write_all(sig.xml.as_bytes())?;
262        }
263
264        zip.finish()?;
265        Ok(())
266    }
267
268    /// Serialize the container to a byte buffer.
269    pub fn to_bytes(&self) -> Result<Vec<u8>> {
270        let mut buf = Cursor::new(Vec::new());
271        self.write_to(&mut buf)?;
272        Ok(buf.into_inner())
273    }
274
275    /// Write the container to a file on disk.
276    pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
277        self.write_to(std::fs::File::create(path)?)
278    }
279}
280
281fn read_entry<R: Read + Seek>(zip: &mut ZipArchive<R>, name: &str) -> Result<Vec<u8>> {
282    let mut entry = zip
283        .by_name(name)
284        .map_err(|_| LibError::Container(format!("missing zip entry: {name}")))?;
285    let mut buf = Vec::with_capacity(entry.size() as usize);
286    entry.read_to_end(&mut buf)?;
287    Ok(buf)
288}
289
290fn validate_entry_name(name: &str) -> Result<()> {
291    if name.is_empty() {
292        return Err(LibError::Container("file name must not be empty".into()));
293    }
294    if name == "mimetype" || name.starts_with("META-INF/") {
295        return Err(LibError::Container(format!("reserved entry name: {name}")));
296    }
297    if name.starts_with('/') || name.split('/').any(|part| part == ".." || part.is_empty()) {
298        return Err(LibError::Container(format!(
299            "file name must be a clean relative path: {name}"
300        )));
301    }
302    Ok(())
303}
304
305fn is_signature_entry(meta_inf_rest: &str) -> bool {
306    // See '4.4.3.2 2)' of EN 319 162-1 v1.1.1
307    meta_inf_rest.contains("signatures") && meta_inf_rest.ends_with(".xml")
308}