zar/
builder.rs

1use std::collections::hash_map::Entry::Occupied;
2use std::collections::hash_map::Entry::Vacant;
3use std::collections::HashMap;
4use std::io::Error;
5use std::io::ErrorKind;
6use std::io::Write;
7use std::path::Path;
8use std::time::SystemTime;
9
10use base64ct::Base64;
11use base64ct::Encoding;
12use normalize_path::NormalizePath;
13use serde::Deserialize;
14use serde::Serialize;
15use x509_cert::der::Encode;
16use x509_cert::Certificate;
17
18use crate::xml;
19use crate::ChecksumAlgo;
20use crate::Compression;
21use crate::File;
22use crate::FileType;
23use crate::HardLink;
24use crate::Signer;
25use crate::Walk;
26
27/// Builder options.
28pub struct BuilderOptions {
29    file_checksum_algo: ChecksumAlgo,
30    toc_checksum_algo: ChecksumAlgo,
31}
32
33impl BuilderOptions {
34    /// Create new default options.
35    pub fn new() -> Self {
36        Self {
37            file_checksum_algo: Default::default(),
38            toc_checksum_algo: Default::default(),
39        }
40    }
41
42    /// File hashing algorithm.
43    pub fn file_checksum_algo(mut self, algo: ChecksumAlgo) -> Self {
44        self.file_checksum_algo = algo;
45        self
46    }
47
48    /// Table of contents hashing algorithm.
49    pub fn toc_checksum_algo(mut self, algo: ChecksumAlgo) -> Self {
50        self.toc_checksum_algo = algo;
51        self
52    }
53
54    /// Create new builder using the configured options.
55    pub fn create<W: Write, S: Signer, X>(
56        self,
57        writer: W,
58        signer: Option<S>,
59    ) -> ExtendedBuilder<W, S, X> {
60        ExtendedBuilder::with_options(writer, signer, self)
61    }
62}
63
64impl Default for BuilderOptions {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// Signed XAR archive builder without extra data.
71pub type Builder<W, S> = ExtendedBuilder<W, S, ()>;
72
73/// Unsigned XAR archive builder without extra data.
74pub type UnsignedBuilder<W> = ExtendedBuilder<W, NoSigner, ()>;
75
76/// XAR archive builder with extra data.
77pub struct ExtendedBuilder<W: Write, S: Signer = NoSigner, X = ()> {
78    writer: W,
79    signer: Option<S>,
80    file_checksum_algo: ChecksumAlgo,
81    toc_checksum_algo: ChecksumAlgo,
82    files: Vec<File<X>>,
83    contents: Vec<Vec<u8>>,
84    // (dev, inode) -> file index
85    inodes: HashMap<(u64, u64), usize>,
86    offset: u64,
87}
88
89impl<W: Write, S: Signer, X> ExtendedBuilder<W, S, X> {
90    /// Create new archive builder with non-default options.
91    pub fn with_options(writer: W, signer: Option<S>, options: BuilderOptions) -> Self {
92        let toc_checksum_len = options.toc_checksum_algo.hash_len();
93        let offset = if let Some(ref signer) = signer {
94            toc_checksum_len + signer.signature_len()
95        } else {
96            toc_checksum_len
97        };
98        ExtendedBuilder {
99            writer,
100            signer,
101            offset: offset as u64,
102            file_checksum_algo: options.file_checksum_algo,
103            toc_checksum_algo: options.toc_checksum_algo,
104            files: Default::default(),
105            contents: Default::default(),
106            inodes: Default::default(),
107        }
108    }
109
110    /// Create new archive builder with default options.
111    pub fn new(writer: W, signer: Option<S>) -> Self {
112        Self::with_options(writer, signer, Default::default())
113    }
114
115    /// Create new unsigned archive builder with default options.
116    pub fn new_unsigned(writer: W) -> Self {
117        Self::with_options(writer, None, Default::default())
118    }
119
120    /// Get the files added so far.
121    pub fn files(&self) -> &[File<X>] {
122        &self.files[..]
123    }
124
125    /// Append directory to the archive recursively.
126    pub fn append_dir_all<F, P>(
127        &mut self,
128        path: P,
129        // TODO default compression for each mime type
130        compression: Compression,
131        mut extra: F,
132    ) -> Result<(), Error>
133    where
134        F: FnMut(&File<X>, &Path, &Path) -> Result<Option<X>, Error>,
135        P: AsRef<Path>,
136    {
137        let path = path.as_ref();
138        let mut next_id = self.files.len() as u64 + 1;
139        let mut next_offset = self.offset;
140        let mut tree = HashMap::new();
141        for entry in path.walk()? {
142            let entry = entry?;
143            let archive_path = entry
144                .path()
145                .strip_prefix(path)
146                .map_err(|_| ErrorKind::InvalidData)?
147                .normalize();
148            if archive_path == Path::new("") {
149                continue;
150            }
151            let (file, archived_contents) = File::<X>::new(
152                next_id,
153                path,
154                entry.path(),
155                Path::new(archive_path.file_name().unwrap_or_default()).to_path_buf(),
156                compression,
157                self.file_checksum_algo,
158                next_offset,
159                None,
160            )?;
161            next_id += 1;
162            next_offset += archived_contents.len() as u64;
163            let parent = archive_path
164                .parent()
165                .map(|x| x.to_path_buf())
166                .unwrap_or_default();
167            if parent == Path::new("") {
168                tree.insert(archive_path, (file, archived_contents, entry.path()));
169                continue;
170            }
171            let parent = tree.get_mut(&parent).ok_or(ErrorKind::InvalidData)?;
172            parent.0.children.push(file);
173        }
174        let mut files: Vec<_> = tree.into_iter().collect();
175        files.sort_unstable_by_key(|entry| entry.1 .0.id);
176        for (archive_path, (mut file, archived_contents, real_path)) in files.into_iter() {
177            file.extra = extra(&file, &archive_path, &real_path)?;
178            self.append_raw(file, archived_contents)?;
179        }
180        Ok(())
181    }
182
183    /// Append raw entry to the archive.
184    pub fn append_raw(
185        &mut self,
186        mut file: File<X>,
187        archived_contents: Vec<u8>,
188    ) -> Result<(), Error> {
189        self.handle_hard_links(&mut file);
190        self.offset += archived_contents.len() as u64;
191        self.files.push(file);
192        self.contents.push(archived_contents);
193        Ok(())
194    }
195
196    /// Get mutable reference to the underlying writer.
197    pub fn get_mut(&mut self) -> &mut W {
198        self.writer.by_ref()
199    }
200
201    /// Get immutable reference to the underlying writer.
202    pub fn get(&self) -> &W {
203        &self.writer
204    }
205
206    fn handle_hard_links(&mut self, file: &mut File<X>) {
207        match self.inodes.entry((file.deviceno, file.inode)) {
208            Vacant(v) => {
209                let i = self.files.len();
210                v.insert(i);
211            }
212            Occupied(o) => {
213                let i = *o.get();
214                let original_file = &mut self.files[i];
215                file.kind = FileType::HardLink(HardLink::Id(original_file.id));
216                // Do not overwrite original file type if it is already `HardLink`.
217                if !matches!(original_file.kind, FileType::HardLink(..)) {
218                    original_file.kind = FileType::HardLink(HardLink::Original);
219                }
220            }
221        }
222    }
223}
224
225impl<W: Write, S: Signer, X: Serialize + for<'a> Deserialize<'a> + Default>
226    ExtendedBuilder<W, S, X>
227{
228    /// Write the archive to the underlying writer.
229    pub fn finish(mut self) -> Result<W, Error> {
230        let checksum_len = self.toc_checksum_algo.hash_len() as u64;
231        // http://users.wfu.edu/cottrell/productsign/productsign_linux.html
232        let signature = match self.signer.as_ref() {
233            Some(signer) => Some(xml::Signature {
234                style: signer.signature_style().into(),
235                offset: checksum_len,
236                size: signer.signature_len() as u64,
237                key_info: xml::KeyInfo {
238                    data: xml::X509Data {
239                        certificates: signer
240                            .certs()
241                            .iter()
242                            .map(|cert| -> Result<_, Error> {
243                                let bytes = cert.to_der().map_err(|_| ErrorKind::InvalidData)?;
244                                let string = Base64::encode_string(&bytes);
245                                Ok(xml::X509Certificate { data: string })
246                            })
247                            .collect::<Result<Vec<_>, _>>()?,
248                    },
249                },
250            }),
251            None => None,
252        };
253        let xar = xml::Xar::<X> {
254            toc: xml::Toc::<X> {
255                checksum: xml::TocChecksum {
256                    algo: self.toc_checksum_algo,
257                    offset: 0,
258                    size: checksum_len,
259                },
260                files: self.files,
261                signature,
262                creation_time: xml::Timestamp(SystemTime::now()),
263            },
264        };
265        // write header and toc
266        xar.write(
267            self.writer.by_ref(),
268            self.toc_checksum_algo,
269            self.signer.as_ref(),
270        )?;
271        for content in self.contents.into_iter() {
272            self.writer.write_all(&content)?;
273        }
274        Ok(self.writer)
275    }
276}
277
278/// Archive [`Signer`](crate::Signer) that produces unsigned archives.
279pub struct NoSigner;
280
281impl Signer for NoSigner {
282    fn sign(&self, _data: &[u8]) -> Result<Vec<u8>, Error> {
283        Ok(Vec::new())
284    }
285
286    fn signature_style(&self) -> &str {
287        ""
288    }
289
290    fn signature_len(&self) -> usize {
291        0
292    }
293
294    fn certs(&self) -> &[Certificate] {
295        &[]
296    }
297}
298
299/// Use this function as `extra` argument to [`Builder::append_dir_all`] and [`Builder::append_raw`]
300/// calls to not append any extra data to the archive.
301#[allow(unused)]
302pub fn no_extra_contents(
303    file: &File<()>,
304    archive_path: &Path,
305    file_system_path: &Path,
306) -> Result<Option<()>, Error> {
307    Ok(None)
308}