debian_packaging/deb/
builder.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Create .deb package files and their components. */
6
7use {
8    crate::{control::ControlFile, deb::DebCompression, error::Result},
9    md5::Digest,
10    os_str_bytes::OsStrBytes,
11    simple_file_manifest::{FileEntry, FileManifest},
12    std::{
13        io::{BufWriter, Cursor, Read, Write},
14        path::Path,
15        time::SystemTime,
16    },
17};
18
19/// A builder for a `.deb` package file.
20pub struct DebBuilder<'control> {
21    control_builder: ControlTarBuilder<'control>,
22
23    compression: DebCompression,
24
25    /// Files to install as part of the package.
26    install_files: FileManifest,
27
28    mtime: Option<SystemTime>,
29}
30
31impl<'control> DebBuilder<'control> {
32    /// Construct a new instance using a control file.
33    pub fn new(control_file: ControlFile<'control>) -> Self {
34        Self {
35            control_builder: ControlTarBuilder::new(control_file),
36            compression: DebCompression::Gzip,
37            install_files: FileManifest::default(),
38            mtime: None,
39        }
40    }
41
42    /// Set the compression format to use.
43    ///
44    /// Not all compression formats are supported by all Linux distributions.
45    #[must_use]
46    pub fn set_compression(mut self, compression: DebCompression) -> Self {
47        self.compression = compression;
48        self
49    }
50
51    fn mtime(&self) -> u64 {
52        self.mtime
53            .unwrap_or_else(std::time::SystemTime::now)
54            .duration_since(std::time::UNIX_EPOCH)
55            .expect("times before UNIX epoch not accepted")
56            .as_secs()
57    }
58
59    /// Set the modified time to use on archive members.
60    ///
61    /// If this is called, all archive members will use the specified time, helping
62    /// to make archive content deterministic.
63    ///
64    /// If not called, the current time will be used.
65    #[must_use]
66    pub fn set_mtime(mut self, time: Option<SystemTime>) -> Self {
67        self.mtime = time;
68        self.control_builder = self.control_builder.set_mtime(time);
69        self
70    }
71
72    /// Add an extra file to the `control.tar` archive.
73    pub fn extra_control_tar_file(
74        mut self,
75        path: impl AsRef<Path>,
76        entry: impl Into<FileEntry>,
77    ) -> Result<Self> {
78        self.control_builder = self.control_builder.add_extra_file(path, entry)?;
79        Ok(self)
80    }
81
82    /// Register a file as to be installed by this package.
83    ///
84    /// Filenames should be relative to the filesystem root. e.g.
85    /// `usr/bin/myapp`.
86    ///
87    /// The file content will be added to the `data.tar` archive and registered with
88    /// the `control.tar` archive so its checksum is computed.
89    pub fn install_file(
90        mut self,
91        path: impl AsRef<Path> + Clone,
92        entry: impl Into<FileEntry> + Clone,
93    ) -> Result<Self> {
94        let entry = entry.into();
95
96        let data = entry.resolve_content()?;
97        let mut cursor = Cursor::new(&data);
98        self.control_builder = self
99            .control_builder
100            .add_data_file(path.clone(), &mut cursor)?;
101
102        self.install_files.add_file_entry(path, entry)?;
103
104        Ok(self)
105    }
106
107    /// Write `.deb` file content to a writer.
108    ///
109    /// This effectively materialized the `.deb` package somewhere.
110    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
111        let mut ar_builder = ar::Builder::new(writer);
112
113        // First entry is a debian-binary file with static content.
114        let data: &[u8] = b"2.0\n";
115        let mut header = ar::Header::new(b"debian-binary".to_vec(), data.len() as _);
116        header.set_mode(0o644);
117        header.set_mtime(self.mtime());
118        header.set_uid(0);
119        header.set_gid(0);
120        ar_builder.append(&header, data)?;
121
122        // Second entry is a control.tar with metadata.
123        let mut control_writer = BufWriter::new(Vec::new());
124        self.control_builder.write(&mut control_writer)?;
125        let control_tar = control_writer.into_inner().map_err(|e| e.into_error())?;
126        let control_tar = self
127            .compression
128            .compress(&mut std::io::Cursor::new(control_tar))?;
129
130        let mut header = ar::Header::new(
131            format!("control.tar{}", self.compression.extension()).into_bytes(),
132            control_tar.len() as _,
133        );
134        header.set_mode(0o644);
135        header.set_mtime(self.mtime());
136        header.set_uid(0);
137        header.set_gid(0);
138        ar_builder.append(&header, &*control_tar)?;
139
140        // Third entry is a data.tar with file content.
141        let mut data_writer = BufWriter::new(Vec::new());
142        write_deb_tar(&mut data_writer, &self.install_files, self.mtime())?;
143        let data_tar = data_writer.into_inner().map_err(|e| e.into_error())?;
144        let data_tar = self
145            .compression
146            .compress(&mut std::io::Cursor::new(data_tar))?;
147
148        let mut header = ar::Header::new(
149            format!("data.tar{}", self.compression.extension()).into_bytes(),
150            data_tar.len() as _,
151        );
152        header.set_mode(0o644);
153        header.set_mtime(self.mtime());
154        header.set_uid(0);
155        header.set_gid(0);
156        ar_builder.append(&header, &*data_tar)?;
157
158        Ok(())
159    }
160}
161
162fn new_tar_header(mtime: u64) -> Result<tar::Header> {
163    let mut header = tar::Header::new_gnu();
164    header.set_uid(0);
165    header.set_gid(0);
166    header.set_username("root")?;
167    header.set_groupname("root")?;
168    header.set_mtime(mtime);
169
170    Ok(header)
171}
172
173fn set_header_path(
174    builder: &mut tar::Builder<impl Write>,
175    header: &mut tar::Header,
176    path: &Path,
177    is_directory: bool,
178) -> Result<()> {
179    // Debian archives in the wild have filenames beginning with `./`. And
180    // paths ending with `/` are directories. However, we cannot call
181    // `header.set_path()` with `./` on anything except the root directory
182    // because it will normalize away the `./` bit. So we set the header field
183    // directly when adding directories and files.
184
185    // We should only be dealing with GNU headers, which simplifies our code a bit.
186    assert!(header.as_ustar().is_none());
187
188    let value = format!(
189        "./{}{}",
190        path.display(),
191        if is_directory { "/" } else { "" }
192    );
193    let value_bytes = value.as_bytes();
194
195    let name_buffer = &mut header.as_old_mut().name;
196
197    // If it fits within the buffer, copy it over.
198    if value_bytes.len() <= name_buffer.len() {
199        name_buffer[0..value_bytes.len()].copy_from_slice(value_bytes);
200    } else {
201        // Else we emit a special entry to extend the filename. Who knew tar
202        // files were this jank.
203        let mut header2 = tar::Header::new_gnu();
204        let name = b"././@LongLink";
205        header2.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]);
206        header2.set_mode(0o644);
207        header2.set_uid(0);
208        header2.set_gid(0);
209        header2.set_mtime(0);
210        header2.set_size(value_bytes.len() as u64 + 1);
211        header2.set_entry_type(tar::EntryType::new(b'L'));
212        header2.set_cksum();
213        let mut data = value_bytes.chain(std::io::repeat(0).take(1));
214        builder.append(&header2, &mut data)?;
215
216        let truncated_bytes = &value_bytes[0..name_buffer.len()];
217        name_buffer[0..truncated_bytes.len()].copy_from_slice(truncated_bytes);
218    }
219
220    Ok(())
221}
222
223/// A builder for a `control.tar` file inside `.deb` packages.
224pub struct ControlTarBuilder<'a> {
225    /// The file that will become the `control` file.
226    control: ControlFile<'a>,
227    /// Extra maintainer scripts to install.
228    extra_files: FileManifest,
229    /// Hashes of files that will be installed.
230    md5sums: Vec<Vec<u8>>,
231    /// Modified time for tar archive entries.
232    mtime: Option<SystemTime>,
233}
234
235impl<'a> ControlTarBuilder<'a> {
236    /// Create a new instance from a control file.
237    pub fn new(control_file: ControlFile<'a>) -> Self {
238        Self {
239            control: control_file,
240            extra_files: FileManifest::default(),
241            md5sums: vec![],
242            mtime: None,
243        }
244    }
245
246    /// Add an extra file to the control archive.
247    ///
248    /// This is usually used to add maintainer scripts. Maintainer scripts
249    /// are special scripts like `preinst` and `postrm` that are executed
250    /// during certain activities.
251    pub fn add_extra_file(
252        mut self,
253        path: impl AsRef<Path>,
254        entry: impl Into<FileEntry>,
255    ) -> Result<Self> {
256        self.extra_files.add_file_entry(path, entry)?;
257
258        Ok(self)
259    }
260
261    /// Add a data file to be indexed.
262    ///
263    /// This should be called for every file in the corresponding `data.tar`
264    /// archive in the `.deb` archive.
265    ///
266    /// `path` is the relative path the file will be installed to.
267    /// `reader` is a reader to obtain the file content.
268    ///
269    /// This method has the side-effect of computing the checksum for the path
270    /// so a checksums entry can be written.
271    pub fn add_data_file<P: AsRef<Path>, R: Read>(
272        mut self,
273        path: P,
274        reader: &mut R,
275    ) -> Result<Self> {
276        let mut context = md5::Md5::new();
277
278        let mut buffer = [0; 32768];
279
280        loop {
281            let read = reader.read(&mut buffer)?;
282            if read == 0 {
283                break;
284            }
285
286            context.update(&buffer[0..read]);
287        }
288
289        let digest = context.finalize();
290
291        let mut entry = Vec::new();
292        entry.write_all(&digest.to_ascii_lowercase())?;
293        entry.write_all(b"  ")?;
294        entry.write_all(path.as_ref().to_raw_bytes().as_ref())?;
295        entry.write_all(b"\n")?;
296
297        self.md5sums.push(entry);
298
299        Ok(self)
300    }
301
302    fn mtime(&self) -> u64 {
303        self.mtime
304            .unwrap_or_else(std::time::SystemTime::now)
305            .duration_since(std::time::UNIX_EPOCH)
306            .expect("times before UNIX epoch not accepted")
307            .as_secs()
308    }
309
310    #[must_use]
311    pub fn set_mtime(mut self, time: Option<SystemTime>) -> Self {
312        self.mtime = time;
313        self
314    }
315
316    /// Write the `control.tar` file to a writer.
317    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
318        let mut control_buffer = BufWriter::new(Vec::new());
319        self.control.write(&mut control_buffer)?;
320        let control_data = control_buffer.into_inner().map_err(|e| e.into_error())?;
321
322        let mut manifest = self.extra_files.clone();
323        manifest.add_file_entry("control", control_data)?;
324        manifest.add_file_entry("md5sums", self.md5sums.concat::<u8>())?;
325
326        write_deb_tar(writer, &manifest, self.mtime())
327    }
328}
329
330/// Write a tar archive suitable for inclusion in a `.deb` archive.
331pub fn write_deb_tar<W: Write>(writer: W, files: &FileManifest, mtime: u64) -> Result<()> {
332    let mut builder = tar::Builder::new(writer);
333
334    // Add root directory entry.
335    let mut header = new_tar_header(mtime)?;
336    header.set_path(Path::new("./"))?;
337    header.set_mode(0o755);
338    header.set_size(0);
339    header.set_entry_type(tar::EntryType::Directory);
340    header.set_cksum();
341    builder.append(&header, &*vec![])?;
342
343    // And entries for each directory in the tree.
344    for directory in files.relative_directories() {
345        let mut header = new_tar_header(mtime)?;
346        set_header_path(&mut builder, &mut header, &directory, true)?;
347        header.set_mode(0o755);
348        header.set_size(0);
349        header.set_entry_type(tar::EntryType::Directory);
350        header.set_cksum();
351        builder.append(&header, &*vec![])?;
352    }
353
354    // FileManifest is backed by a BTreeMap, so iteration is deterministic.
355    for (rel_path, content) in files.iter_entries() {
356        let data = content.resolve_content()?;
357
358        let mut header = new_tar_header(mtime)?;
359        set_header_path(&mut builder, &mut header, rel_path, false)?;
360        header.set_mode(if content.is_executable() {
361            0o755
362        } else {
363            0o644
364        });
365        header.set_size(data.len() as _);
366        header.set_cksum();
367        builder.append(&header, &*data)?;
368    }
369
370    builder.finish()?;
371
372    Ok(())
373}
374
375#[cfg(test)]
376mod tests {
377    use {super::*, crate::control::ControlParagraph, std::path::PathBuf};
378
379    #[test]
380    fn test_write_control_tar_simple() -> Result<()> {
381        let mut control_para = ControlParagraph::default();
382        control_para.set_field_from_string("Package".into(), "mypackage".into());
383        control_para.set_field_from_string("Architecture".into(), "amd64".into());
384
385        let mut control = ControlFile::default();
386        control.add_paragraph(control_para);
387
388        let builder = ControlTarBuilder::new(control)
389            .set_mtime(Some(SystemTime::UNIX_EPOCH))
390            .add_extra_file("prerm", FileEntry::new_from_data(vec![42], true))?
391            .add_data_file("usr/bin/myapp", &mut std::io::Cursor::new("data"))?;
392
393        let mut buffer = vec![];
394        builder.write(&mut buffer)?;
395
396        let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
397
398        for (i, entry) in archive.entries()?.enumerate() {
399            let entry = entry?;
400
401            let path = match i {
402                0 => Path::new("./"),
403                1 => Path::new("./control"),
404                2 => Path::new("./md5sums"),
405                3 => Path::new("./prerm"),
406                _ => panic!("unexpected archive entry"),
407            };
408
409            assert_eq!(entry.path()?, path, "entry {} path matches", i);
410        }
411
412        Ok(())
413    }
414
415    #[test]
416    fn test_write_data_tar_one_file() -> Result<()> {
417        let mut manifest = FileManifest::default();
418        manifest.add_file_entry("foo/bar.txt", FileEntry::new_from_data(vec![42], true))?;
419
420        let mut buffer = vec![];
421        write_deb_tar(&mut buffer, &manifest, 2)?;
422
423        let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
424
425        for (i, entry) in archive.entries()?.enumerate() {
426            let entry = entry?;
427
428            let path = match i {
429                0 => Path::new("./"),
430                1 => Path::new("./foo/"),
431                2 => Path::new("./foo/bar.txt"),
432                _ => panic!("unexpected archive entry"),
433            };
434
435            assert_eq!(entry.path()?, path, "entry {} path matches", i);
436        }
437
438        Ok(())
439    }
440
441    #[test]
442    fn test_write_data_tar_long_path() -> Result<()> {
443        let long_path = PathBuf::from(format!("f{}.txt", "u".repeat(200)));
444
445        let mut manifest = FileManifest::default();
446
447        manifest.add_file_entry(&long_path, vec![42])?;
448
449        let mut buffer = vec![];
450        write_deb_tar(&mut buffer, &manifest, 2)?;
451
452        let mut archive = tar::Archive::new(std::io::Cursor::new(buffer));
453
454        for (i, entry) in archive.entries()?.enumerate() {
455            let entry = entry?;
456
457            if i != 1 {
458                continue;
459            }
460
461            assert_eq!(
462                entry.path()?,
463                Path::new(&format!("./f{}.txt", "u".repeat(200)))
464            );
465        }
466
467        Ok(())
468    }
469
470    #[test]
471    fn test_write_deb() -> Result<()> {
472        let mut control_para = ControlParagraph::default();
473        control_para.set_field_from_string("Package".into(), "mypackage".into());
474        control_para.set_field_from_string("Architecture".into(), "amd64".into());
475
476        let mut control = ControlFile::default();
477        control.add_paragraph(control_para);
478
479        let builder = DebBuilder::new(control)
480            .set_compression(DebCompression::Zstandard(3))
481            .install_file("usr/bin/myapp", FileEntry::new_from_data(vec![42], true))?;
482
483        let mut buffer = vec![];
484        builder.write(&mut buffer)?;
485
486        let mut archive = ar::Archive::new(std::io::Cursor::new(buffer));
487        {
488            let entry = archive.next_entry().unwrap().unwrap();
489            assert_eq!(entry.header().identifier(), b"debian-binary");
490        }
491        {
492            let entry = archive.next_entry().unwrap().unwrap();
493            assert_eq!(entry.header().identifier(), b"control.tar.zst");
494        }
495        {
496            let entry = archive.next_entry().unwrap().unwrap();
497            assert_eq!(entry.header().identifier(), b"data.tar.zst");
498        }
499
500        assert!(archive.next_entry().is_none());
501
502        Ok(())
503    }
504}