cargo_deb/deb/
tar.rs

1use crate::assets::{Asset, AssetSource};
2use crate::error::{CDResult, CargoDebError};
3use crate::listener::Listener;
4use crate::PackageConfig;
5use crate::util::pathbytes::AsUnixPathBytes;
6use std::collections::HashSet;
7use std::io::{Read, Write};
8use std::path::Path;
9use std::{fs, io};
10use tar::{EntryType, Header as TarHeader};
11
12/// Tarball for control and data files
13pub(crate) struct Tarball<W: Write> {
14    added_directories: HashSet<Box<Path>>,
15    time: u64,
16    tar: tar::Builder<W>,
17}
18
19impl<W: Write> Tarball<W> {
20    pub fn new(dest: W, time: u64) -> Self {
21        Self {
22            added_directories: HashSet::new(),
23            time,
24            tar: tar::Builder::new(dest),
25        }
26    }
27
28    /// Copies all the files to be packaged into the tar archive.
29    pub fn archive_files(mut self, package_deb: &PackageConfig, rsyncable: bool, listener: &dyn Listener) -> CDResult<W> {
30        let mut archive_data_added = 0;
31        let mut prev_is_built = false;
32        let log_display_base_dir = std::env::current_dir().unwrap_or_default();
33
34        debug_assert!(package_deb.assets.unresolved.is_empty());
35        for asset in &package_deb.assets.resolved {
36            log_asset(asset, &log_display_base_dir, listener);
37
38            if let AssetSource::Symlink(source_path) = &asset.source {
39                let link_name = fs::read_link(source_path)
40                    .map_err(|e| CargoDebError::IoFile("Symlink asset", e, source_path.clone()))?;
41                self.symlink(&asset.c.target_path, &link_name)?;
42            } else {
43                let out_data = asset.source.data()?;
44                if rsyncable {
45                    if archive_data_added > 1_000_000 || prev_is_built != asset.c.is_built() {
46                        self.flush().map_err(|e| CargoDebError::Io(e).context("error while writing tar archive"))?;
47                        archive_data_added = 0;
48                    }
49                    // puts synchronization point between non-code and code assets
50                    prev_is_built = asset.c.is_built();
51                    archive_data_added += out_data.len();
52                }
53                self.file(&asset.c.target_path, &out_data, asset.c.chmod)?;
54            }
55        }
56
57        self.tar.into_inner().map_err(|e| CargoDebError::Io(e).context("error while finalizing tar archive"))
58    }
59
60    fn directory(&mut self, path: &Path) -> io::Result<()> {
61        let mut header = self.header_for_path(path, true)?;
62        header.set_mtime(self.time);
63        header.set_size(0);
64        header.set_mode(0o755);
65        header.set_entry_type(EntryType::Directory);
66        header.set_cksum();
67        self.tar.append(&header, &mut io::empty())
68    }
69
70    fn add_parent_directories(&mut self, path: &Path) -> CDResult<()> {
71        debug_assert!(path.is_relative());
72
73        let dirs = path.ancestors().skip(1)
74            .take_while(|&d| !self.added_directories.contains(d))
75            .filter(|&d| !d.as_os_str().is_empty())
76            .map(Box::from)
77            .collect::<Vec<_>>();
78
79        for directory in dirs.into_iter().rev() {
80            if let Err(e) = self.directory(&directory) {
81                return Err(CargoDebError::IoFile("Can't add directory to tarball", e, directory.into()));
82            }
83            self.added_directories.insert(directory);
84        }
85        Ok(())
86    }
87
88    pub(crate) fn file<P: AsRef<Path>>(&mut self, path: P, out_data: &[u8], chmod: u32) -> CDResult<()> {
89        self.file_(path.as_ref(), out_data, chmod)
90    }
91
92    fn file_(&mut self, path: &Path, out_data: &[u8], chmod: u32) -> CDResult<()> {
93        debug_assert!(path.is_relative());
94        self.add_parent_directories(path)?;
95
96        let mut header = self.header_for_path(path, false)
97            .map_err(|e| CargoDebError::IoFile("Can't set header path", e, path.into()))?;
98        header.set_mtime(self.time);
99        header.set_mode(chmod);
100        header.set_size(out_data.len() as u64);
101        header.set_cksum();
102        self.tar.append(&header, out_data)
103            .map_err(|e| CargoDebError::IoFile("Can't add file to tarball", e, path.into()))?;
104        Ok(())
105    }
106
107    pub(crate) fn symlink(&mut self, path: &Path, link_name: &Path) -> CDResult<()> {
108        debug_assert!(path.is_relative());
109        self.add_parent_directories(path.as_ref())?;
110
111        let mut header = self.header_for_path(path, false)
112            .map_err(|e| CargoDebError::IoFile("Can't set header path", e, path.into()))?;
113        header.set_mtime(self.time);
114        header.set_entry_type(EntryType::Symlink);
115        header.set_size(0);
116        header.set_mode(0o777);
117        header.set_link_name(link_name)
118            .map_err(|e| CargoDebError::IoFile("Can't set header link name", e, path.into()))?;
119        header.set_cksum();
120        self.tar.append(&header, &mut io::empty())
121            .map_err(|e| CargoDebError::IoFile("Can't add symlink to tarball", e, path.into()))?;
122        Ok(())
123    }
124
125    #[inline]
126    fn header_for_path(&mut self, path: &Path, is_dir: bool) -> io::Result<TarHeader> {
127        debug_assert!(path.is_relative());
128        let path_bytes = path.to_bytes();
129
130        let mut header = if path_bytes.len() < 98 {
131            TarHeader::new_old()
132        } else {
133            TarHeader::new_gnu()
134        };
135        self.set_header_path(&mut header, path_bytes, is_dir)?;
136        Ok(header)
137    }
138
139    #[inline(never)]
140    fn set_header_path(&mut self, header: &mut TarHeader, path_bytes: &[u8], is_dir: bool) -> io::Result<()> {
141        debug_assert!(is_dir || path_bytes.last() != Some(&b'/'));
142        let needs_slash = is_dir && path_bytes.last() != Some(&b'/');
143
144        const PREFIX: &[u8] = b"./";
145        let (prefix, path_slot) = header.as_old_mut().name.split_at_mut(PREFIX.len());
146        prefix.copy_from_slice(PREFIX);
147        let (path_slot, zero) = path_slot.split_at_mut(path_bytes.len().min(path_slot.len()));
148        path_slot.copy_from_slice(&path_bytes[..path_slot.len()]);
149        if cfg!(target_os = "windows") {
150            path_slot.iter_mut().for_each(|b| if *b == b'\\' { *b = b'/' });
151        }
152
153        if let Some((t, rest)) = zero.split_first_mut() {
154            if !needs_slash {
155                *t = 0;
156                return Ok(());
157            }
158            if let Some(t2) = rest.first_mut() {
159                // Lintian insists on dir paths ending with /, which Rust doesn't
160                *t = b'/';
161                *t2 = 0;
162                return Ok(());
163            }
164        }
165
166        // GNU long name extension, copied from
167        // https://github.com/alexcrichton/tar-rs/blob/a1c3036af48fa02437909112239f0632e4cfcfae/src/builder.rs#L731-L744
168        let mut header = TarHeader::new_gnu();
169        const LONG_LINK: &[u8] = b"././@LongLink\0";
170        header.as_gnu_mut().ok_or(io::ErrorKind::Other)?
171            .name[..LONG_LINK.len()].copy_from_slice(LONG_LINK);
172        header.set_mode(0o644);
173        header.set_uid(0);
174        header.set_gid(0);
175        header.set_mtime(0);
176        // include \0 in len to be compliant with GNU tar
177        let suffix = b"/\0";
178        let suffix = if needs_slash { &suffix[..] } else { &suffix[1..] };
179        header.set_size((PREFIX.len() + path_bytes.len() + suffix.len()) as u64);
180        header.set_entry_type(EntryType::new(b'L'));
181        header.set_cksum();
182        self.tar.append(&header, PREFIX.chain(path_bytes).chain(suffix))
183    }
184
185    fn flush(&mut self) -> io::Result<()> {
186        self.tar.get_mut().flush()
187    }
188
189    pub fn into_inner(self) -> io::Result<W> {
190        self.tar.into_inner()
191    }
192}
193
194fn log_asset(asset: &Asset, log_display_base_dir: &Path, listener: &dyn Listener) {
195    let operation = if let AssetSource::Symlink(_) = &asset.source {
196        "Linking"
197    } else {
198        "Adding"
199    };
200    let mut log_line = format!("'{}' {}-> {}",
201        asset.processed_from.as_ref().and_then(|p| p.original_path.as_deref()).or(asset.source.path())
202            .map(|p| p.strip_prefix(log_display_base_dir).unwrap_or(p))
203            .unwrap_or_else(|| Path::new("-")).display(),
204        asset.processed_from.as_ref().map(|p| p.action).unwrap_or_default(),
205        asset.c.target_path.display()
206    );
207    if let Some(len) = asset.source.file_size() {
208        let (size, unit) = human_size(len);
209        use std::fmt::Write;
210        let _ = write!(&mut log_line, " ({size}{unit})");
211    }
212    listener.progress(operation, log_line);
213}
214
215fn human_size(len: u64) -> (u64, &'static str) {
216    if len < 1000 {
217        return (len, "B");
218    }
219    if len < 1_000_000 {
220        return (len.div_ceil(1000), "KB");
221    }
222    (len.div_ceil(1_000_000), "MB")
223}
224
225#[cfg(test)]
226mod tests {
227    use super::Tarball;
228    use std::{io::{Cursor, Read}, path::Path};
229    use tar::{Archive, EntryType};
230
231    struct ExpectedEntry<'a> {
232        path: &'a str,
233        entry_type: EntryType,
234        mode: u32,
235        check: Option<Box<dyn Fn(&mut tar::Entry<Cursor<Vec<u8>>>) + 'a>>,
236    }
237
238    impl<'a> ExpectedEntry<'a> {
239        fn with_check<F>(mut self, check: F) -> Self
240            where F: Fn(&mut tar::Entry<Cursor<Vec<u8>>>) + 'a
241        {
242            self.check = Some(Box::new(check));
243            self
244        }
245    }
246
247    fn expected_entry<'a>(path: &'a str, entry_type: EntryType, mode: u32) -> ExpectedEntry<'a> {
248        ExpectedEntry { path, entry_type, mode, check: None }
249    }
250
251    fn check_tarball_content(tarball: Vec<u8>, expected_entries: &[ExpectedEntry]) {
252        let cursor = Cursor::new(tarball);
253        let mut archive = Archive::new(cursor);
254        let mut entries = archive.entries().unwrap();
255        let mut expected_entries = expected_entries.iter();
256        loop {
257            let (entry_result, expected_entry) = match (entries.next(), expected_entries.next()) {
258                (Some(entry_result), Some(expected_entry)) => (entry_result, expected_entry),
259                (None, None) => break,
260                _ => panic!("mismatched number of entries"),
261            };
262            let mut entry = entry_result.unwrap();
263            let path = entry.path().unwrap().to_string_lossy().to_string();
264            let entry_type = entry.header().entry_type();
265            let mode = entry.header().mode().unwrap();
266            let mtime = entry.header().mtime().unwrap();
267            assert_eq!(path.strip_prefix("./").unwrap(), expected_entry.path);
268            assert_eq!(entry_type, expected_entry.entry_type);
269            assert_eq!(mode, expected_entry.mode);
270            assert_eq!(mtime, 1234567890);
271            if let Some(check) = &expected_entry.check {
272                check(&mut entry);
273            }
274        }
275    }
276
277    #[test]
278    fn basic() {
279        let buffer = Vec::new();
280        let mut tarball = Tarball::new(buffer, 1234567890);
281        let file_content = b"Hello, world!";
282        tarball.file("test/file.txt", file_content, 0o644).unwrap();
283        let script_content = b"#!/bin/bash\necho 'test'";
284        tarball.file("usr/bin/script", script_content, 0o755).unwrap();
285        tarball.symlink(Path::new("usr/bin/link"), Path::new("script")).unwrap();
286
287        let buffer = tarball.into_inner().unwrap();
288        check_tarball_content(buffer, &[
289            expected_entry("test/", EntryType::Directory, 0o755),
290            expected_entry("test/file.txt", EntryType::Regular, 0o644).with_check(|entry| {
291                let mut content = Vec::new();
292                entry.read_to_end(&mut content).unwrap();
293                assert_eq!(content, file_content);
294            }),
295            expected_entry("usr/", EntryType::Directory, 0o755),
296            expected_entry("usr/bin/", EntryType::Directory, 0o755),
297            expected_entry("usr/bin/script", EntryType::Regular, 0o755).with_check(|entry| {
298                let mut content = Vec::new();
299                entry.read_to_end(&mut content).unwrap();
300                assert_eq!(content, script_content);
301            }),
302            expected_entry("usr/bin/link", EntryType::Symlink, 0o777).with_check(|entry| {
303                let link_name = entry.header().link_name().unwrap().unwrap();
304                assert_eq!(link_name.to_string_lossy(), "script");
305            }),
306        ]);
307    }
308
309    #[test]
310    fn long_path() {
311        let buffer = Vec::new();
312        let mut tarball = Tarball::new(buffer, 1234567890);
313
314        tarball.file("a.txt", b"start", 0o644).unwrap();
315        let level = "long/";
316        let deep_path = level.repeat(25) + "file.txt";
317        tarball.file(&deep_path, b"long path", 0o644).unwrap();
318        let long_filename = "very_".repeat(25) + "long_filename.txt";
319        tarball.file(&long_filename, b"long filename", 0o644).unwrap();
320        tarball.file("b.txt", b"end", 0o644).unwrap();
321        let buffer = tarball.into_inner().unwrap();
322
323        let mut expected_entries = vec![expected_entry("a.txt", EntryType::Regular, 0o644)];
324        expected_entries.extend((1..=25).map(|i| expected_entry(&deep_path[..i * level.len()], EntryType::Directory, 0o755)));
325        expected_entries.extend([
326            expected_entry(&deep_path, EntryType::Regular, 0o644),
327            expected_entry(&long_filename, EntryType::Regular, 0o644),
328            expected_entry("b.txt", EntryType::Regular, 0o644),
329        ]);
330        check_tarball_content(buffer, &expected_entries);
331    }
332}