cargo_deb/
assets.rs

1use crate::config::{is_glob_pattern, PackageConfig};
2use crate::error::{CDResult, CargoDebError};
3use crate::listener::Listener;
4use crate::parse::manifest::CargoDebAssetArrayOrTable;
5use crate::util::compress::gzipped;
6use crate::util::read_file_to_bytes;
7use rayon::prelude::*;
8use std::borrow::Cow;
9use std::env::consts::DLL_SUFFIX;
10use std::path::{Path, PathBuf};
11use std::{fmt, fs};
12
13#[derive(Debug, Clone)]
14pub enum AssetSource {
15    /// Copy file from the path (and strip binary if needed).
16    Path(PathBuf),
17    /// A symlink existing in the file system
18    Symlink(PathBuf),
19    /// Write data to destination as-is.
20    Data(Vec<u8>),
21}
22
23impl AssetSource {
24    /// Symlink must exist on disk to be preserved
25    #[must_use]
26    pub fn from_path(path: impl Into<PathBuf>, preserve_existing_symlink: bool) -> Self {
27        let path = path.into();
28        if preserve_existing_symlink || !path.exists() { // !exists means a symlink to bogus path
29            if let Ok(md) = fs::symlink_metadata(&path) {
30                if md.is_symlink() {
31                    return Self::Symlink(path);
32                }
33            }
34        }
35        Self::Path(path)
36    }
37
38    #[must_use]
39    pub fn path(&self) -> Option<&Path> {
40        match self {
41            Self::Symlink(ref p) |
42            Self::Path(ref p) => Some(p),
43            Self::Data(_) => None,
44        }
45    }
46
47    #[must_use]
48    pub fn into_path(self) -> Option<PathBuf> {
49        match self {
50            Self::Symlink(p) |
51            Self::Path(p) => Some(p),
52            Self::Data(_) => None,
53        }
54    }
55
56    #[must_use]
57    pub fn archive_as_symlink_only(&self) -> bool {
58        matches!(self, Self::Symlink(_))
59    }
60
61    #[must_use]
62    pub fn file_size(&self) -> Option<u64> {
63        match *self {
64            Self::Path(ref p) => fs::metadata(p).ok().map(|m| m.len()),
65            Self::Data(ref d) => Some(d.len() as u64),
66            Self::Symlink(_) => None,
67        }
68    }
69
70    pub fn data(&self) -> CDResult<Cow<'_, [u8]>> {
71        Ok(match self {
72            Self::Path(p) => {
73                let data = read_file_to_bytes(p)
74                    .map_err(|e| CargoDebError::IoFile("Unable to read asset to add to archive", e, p.clone()))?;
75                Cow::Owned(data)
76            },
77            Self::Data(d) => Cow::Borrowed(d),
78            Self::Symlink(p) => {
79                let data = read_file_to_bytes(p)
80                    .map_err(|e| CargoDebError::IoFile("Symlink unexpectedly used to read file data", e, p.clone()))?;
81                Cow::Owned(data)
82            },
83        })
84    }
85
86    pub(crate) fn magic_bytes(&self) -> Option<[u8; 4]> {
87        match self {
88            Self::Path(p) | Self::Symlink(p) => {
89                let mut buf = [0; 4];
90                use std::io::Read;
91                let mut file = std::fs::File::open(p).ok()?;
92                file.read_exact(&mut buf[..]).ok()?;
93                Some(buf)
94            },
95            Self::Data(d) => {
96                d.get(..4).and_then(|b| b.try_into().ok())
97            },
98        }
99    }
100}
101
102#[derive(Debug, Clone)]
103pub(crate) struct Assets {
104    pub unresolved: Vec<UnresolvedAsset>,
105    pub resolved: Vec<Asset>,
106}
107
108#[derive(Debug, Clone, serde::Deserialize)]
109#[serde(try_from = "CargoDebAssetArrayOrTable")]
110pub(crate) enum RawAssetOrAuto {
111    Auto,
112    RawAsset(RawAsset),
113}
114
115impl RawAssetOrAuto {
116    pub fn asset(self) -> Option<RawAsset> {
117        match self {
118            Self::RawAsset(a) => Some(a),
119            Self::Auto => None,
120        }
121    }
122}
123
124impl From<RawAsset> for RawAssetOrAuto {
125    fn from(r: RawAsset) -> Self {
126        Self::RawAsset(r)
127    }
128}
129
130#[derive(Debug, Clone, serde::Deserialize)]
131#[serde(try_from = "RawAssetOrAuto")]
132pub(crate) struct RawAsset {
133    pub source_path: PathBuf,
134    pub target_path: PathBuf,
135    pub chmod: u32,
136}
137
138impl TryFrom<RawAssetOrAuto> for RawAsset {
139    type Error = &'static str;
140
141    fn try_from(maybe_auto: RawAssetOrAuto) -> Result<Self, Self::Error> {
142        maybe_auto.asset().ok_or("$auto is not allowed here")
143    }
144}
145
146impl Assets {
147    pub(crate) const fn new(unresolved: Vec<UnresolvedAsset>, resolved: Vec<Asset>) -> Self {
148        Self {
149            unresolved,
150            resolved,
151        }
152    }
153
154    pub(crate) fn iter(&self) -> impl Iterator<Item = &AssetCommon> {
155        self.resolved.iter().map(|u| &u.c).chain(self.unresolved.iter().map(|r| &r.c))
156    }
157}
158
159#[derive(Debug, Copy, Clone, Eq, PartialEq)]
160pub enum AssetKind {
161    Any,
162    CargoExampleBinary,
163    SeparateDebugSymbols,
164}
165
166#[derive(Debug, Copy, Clone, Eq, PartialEq)]
167pub enum IsBuilt {
168    No,
169    SamePackage,
170    /// needs --workspace to build
171    Workspace,
172}
173
174#[derive(Debug, Clone)]
175pub struct UnresolvedAsset {
176    pub source_path: PathBuf,
177    pub c: AssetCommon,
178}
179
180impl UnresolvedAsset {
181    pub(crate) fn new(source_path: PathBuf, target_path: PathBuf, chmod: u32, is_built: IsBuilt, asset_kind: AssetKind) -> Self {
182        Self {
183            source_path,
184            c: AssetCommon { target_path, chmod, asset_kind, is_built },
185        }
186    }
187
188    /// Convert `source_path` (with glob or dir) to actual path
189    pub fn resolve(&self, preserve_symlinks: bool) -> CDResult<Vec<Asset>> {
190        let Self { ref source_path, c: AssetCommon { ref target_path, chmod, is_built, asset_kind } } = *self;
191
192        let source_prefix_len = is_glob_pattern(source_path.as_os_str()).then(|| {
193            let file_name_is_glob = source_path
194                .file_name()
195                .is_some_and(is_glob_pattern);
196
197            if file_name_is_glob {
198                // skip to the component before the glob
199                let glob_component_pos = source_path
200                    .parent()
201                    .and_then(|parent| parent.iter().position(is_glob_pattern));
202                glob_component_pos.unwrap_or_else(|| {
203                    source_path
204                        .iter()
205                        .count()
206                })
207            } else {
208                // extract the only file name component
209                source_path
210                    .iter()
211                    .count()
212                    .saturating_sub(1)
213            }
214        });
215
216        let matched_assets = glob::glob(source_path.to_str().ok_or("utf8 path")?)?
217            // Remove dirs from globs without throwing away errors
218            .map(|entry| {
219                let source_file = entry?;
220                Ok(if source_file.is_dir() { None } else { Some(source_file) })
221            })
222            .filter_map(|res| {
223                Some(res.transpose()?.map(|source_file| {
224                    let target_file = if let Some(source_prefix_len) = source_prefix_len {
225                        target_path.join(
226                            source_file
227                            .iter()
228                            .skip(source_prefix_len)
229                            .collect::<PathBuf>())
230                    } else {
231                        target_path.clone()
232                    };
233                    log::debug!("asset {} -> {} {} {:o}", source_file.display(), target_file.display(), if is_built != IsBuilt::No {"copy"} else {"build"}, chmod);
234                    let asset = Asset::new(
235                        AssetSource::from_path(source_file, preserve_symlinks),
236                        target_file,
237                        chmod,
238                        is_built,
239                        asset_kind,
240                    );
241                    if source_prefix_len.is_some() {
242                        asset.processed("glob", None)
243                    } else {
244                        asset
245                    }
246                }))
247            })
248            .collect::<CDResult<Vec<_>>>()
249            .map_err(|e| e.context(format_args!("Error while glob searching {}", source_path.display())))?;
250
251        if matched_assets.is_empty() {
252            return Err(CargoDebError::AssetFileNotFound(
253                source_path.clone(),
254                Asset::normalized_target_path(target_path.clone(), Some(source_path)),
255                source_prefix_len.is_some(), is_built != IsBuilt::No));
256        }
257        Ok(matched_assets)
258    }
259}
260
261#[derive(Debug, Clone)]
262pub struct AssetCommon {
263    pub target_path: PathBuf,
264    pub chmod: u32,
265    pub(crate) asset_kind: AssetKind,
266    is_built: IsBuilt,
267}
268
269pub(crate) struct AssetFmt<'a> {
270    c: &'a AssetCommon,
271    cwd: &'a Path,
272    source: Option<&'a Path>,
273    processed_from: Option<&'a ProcessedFrom>,
274}
275
276impl<'a> AssetFmt<'a> {
277    pub fn new(asset: &'a Asset, cwd: &'a Path) -> Self {
278        Self {
279            c: &asset.c,
280            source: asset.source.path(),
281            processed_from: asset.processed_from.as_ref(),
282            cwd,
283        }
284    }
285
286    pub fn unresolved(asset: &'a UnresolvedAsset, cwd: &'a Path) -> Self {
287        Self {
288            c: &asset.c,
289            source: Some(&asset.source_path),
290            processed_from: None,
291            cwd,
292        }
293    }
294}
295
296impl fmt::Display for AssetFmt<'_> {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        let mut src = self.source;
299        let action = self.processed_from.map(|proc| {
300            src = proc.original_path.as_deref().or(src);
301            proc.action
302        });
303        if let Some(src) = src {
304            write!(f, "{} ", src.strip_prefix(self.cwd).unwrap_or(src).display())?;
305        }
306        if let Some(action) = action {
307            write!(f, "({action}{}) ", if self.c.is_built() { "; built" } else { "" })?;
308        } else if self.c.is_built() {
309            write!(f, "(built) ")?;
310        }
311        write!(f, "-> {}", self.c.target_path.display())?;
312        Ok(())
313    }
314}
315
316#[derive(Debug, Clone)]
317pub struct Asset {
318    pub source: AssetSource,
319    /// For prettier path display not "/tmp/blah.tmp"
320    pub processed_from: Option<ProcessedFrom>,
321    pub c: AssetCommon,
322}
323
324#[derive(Debug, Clone)]
325pub struct ProcessedFrom {
326    pub original_path: Option<PathBuf>,
327    pub action: &'static str,
328}
329
330impl Asset {
331    #[must_use]
332    pub fn normalized_target_path(mut target_path: PathBuf, source_path: Option<&Path>) -> PathBuf {
333        // is_dir() is only for paths that exist
334        if target_path.to_string_lossy().ends_with('/') {
335            let file_name = source_path.and_then(|p| p.file_name()).expect("source must be a file");
336            target_path = target_path.join(file_name);
337        }
338
339        if target_path.is_absolute() || target_path.has_root() {
340            target_path = target_path.strip_prefix("/").expect("no root dir").to_owned();
341        }
342        target_path
343    }
344
345    #[must_use]
346    pub fn new(source: AssetSource, target_path: PathBuf, chmod: u32, is_built: IsBuilt, asset_kind: AssetKind) -> Self {
347        let target_path = Self::normalized_target_path(target_path, source.path());
348        Self {
349            source,
350            processed_from: None,
351            c: AssetCommon { target_path, chmod, asset_kind, is_built },
352        }
353    }
354
355    #[must_use]
356    pub fn processed(mut self, action: &'static str, original_path: impl Into<Option<PathBuf>>) -> Self {
357        debug_assert!(self.processed_from.is_none());
358        self.processed_from = Some(ProcessedFrom {
359            original_path: original_path.into(),
360            action,
361        });
362        self
363    }
364
365    pub(crate) fn is_binary_executable(&self) -> bool {
366        self.c.is_executable()
367            && self.c.target_path.extension().map_or(true, |ext| ext != "sh")
368            && (self.c.is_built() || self.smells_like_elf())
369    }
370
371    fn smells_like_elf(&self) -> bool {
372        self.source.magic_bytes().is_some_and(|b| b == [0x7F, b'E', b'L', b'F'])
373    }
374}
375
376impl AssetCommon {
377    pub(crate) const fn is_executable(&self) -> bool {
378        0 != self.chmod & 0o111
379    }
380
381    pub(crate) fn is_dynamic_library(&self) -> bool {
382        is_dynamic_library_filename(&self.target_path)
383    }
384
385    pub(crate) fn is_built(&self) -> bool {
386        self.is_built != IsBuilt::No
387    }
388
389    /// Returns the target path for the debug symbol file, which will be
390    /// /usr/lib/debug/<path-to-executable>.debug
391    #[must_use]
392    pub(crate) fn default_debug_target_path(&self, lib_dir_base: &Path) -> PathBuf {
393        // Turn an absolute path into one relative to "/"
394        let relative = self.target_path.strip_prefix(Path::new("/"))
395            .unwrap_or(self.target_path.as_path());
396
397        // Prepend the debug location and add .debug
398        let mut path = Path::new("/").join(lib_dir_base);
399        path.push("debug");
400        path.push(debug_filename(relative));
401        path
402    }
403
404    pub(crate) fn is_same_package(&self) -> bool {
405        self.is_built == IsBuilt::SamePackage
406    }
407}
408
409/// Adds `.debug` to the end of a path to a filename
410fn debug_filename(path: &Path) -> PathBuf {
411    let mut debug_filename = path.as_os_str().to_os_string();
412    debug_filename.push(".debug");
413    debug_filename.into()
414}
415
416pub(crate) fn is_dynamic_library_filename(path: &Path) -> bool {
417    path.file_name()
418        .and_then(|f| f.to_str())
419        .is_some_and(|f| f.ends_with(DLL_SUFFIX))
420}
421
422/// Compress man pages and other assets per Debian Policy.
423///
424/// # References
425///
426/// <https://www.debian.org/doc/debian-policy/ch-docs.html>
427/// <https://lintian.debian.org/tags/manpage-not-compressed.html>
428pub fn compressed_assets(package_deb: &PackageConfig, listener: &dyn Listener) -> CDResult<Vec<(usize, Asset)>> {
429    fn needs_compression(path: &str) -> bool {
430        !path.ends_with(".gz") &&
431            (path.starts_with("usr/share/man/") ||
432                (path.starts_with("usr/share/doc/") && (path.ends_with("/NEWS") || path.ends_with("/changelog"))) ||
433                (path.starts_with("usr/share/info/") && path.ends_with(".info")))
434    }
435
436    package_deb.assets.resolved.iter().enumerate()
437        .filter(|(_, asset)| {
438            asset.c.target_path.starts_with("usr") && !asset.c.is_built() && needs_compression(&asset.c.target_path.to_string_lossy())
439        })
440        .par_bridge()
441        .map(|(idx, orig_asset)| {
442            let mut file_name = orig_asset.c.target_path.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
443            file_name.push_str(".gz");
444            let new_path = orig_asset.c.target_path.with_file_name(file_name);
445            listener.progress("Compressing", format!("'{}'", new_path.display()));
446            let gzdata = gzipped(&orig_asset.source.data()?)
447                .map_err(|e| CargoDebError::Io(e).context("error while gzipping asset"))?;
448            CDResult::Ok((idx, Asset::new(
449                crate::assets::AssetSource::Data(gzdata),
450                new_path,
451                orig_asset.c.chmod,
452                IsBuilt::No,
453                AssetKind::Any,
454            ).processed("compressed",
455                orig_asset.source.path().unwrap_or(&orig_asset.c.target_path).to_path_buf()
456            )))
457        }).collect()
458}
459
460pub fn apply_compressed_assets(package_deb: &mut PackageConfig, new_assets: Vec<(usize, Asset)>) {
461    for (idx, asset) in new_assets {
462        package_deb.assets.resolved[idx] = asset;
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use crate::config::{BuildEnvironment, BuildOptions, DebConfigOverrides, DebugSymbolOptions};
470    use crate::parse::manifest::SystemdUnitsConfig;
471    use crate::util::tests::add_test_fs_paths;
472
473    #[test]
474    fn assets() {
475        let a = Asset::new(
476            AssetSource::Path(PathBuf::from("target/release/bar")),
477            PathBuf::from("baz/"),
478            0o644,
479            IsBuilt::SamePackage,
480            AssetKind::Any,
481        );
482        assert_eq!("baz/bar", a.c.target_path.to_str().unwrap());
483        assert!(a.c.is_built != IsBuilt::No);
484
485        let a = Asset::new(
486            AssetSource::Path(PathBuf::from("foo/bar")),
487            PathBuf::from("/baz/quz"),
488            0o644,
489            IsBuilt::No,
490            AssetKind::Any,
491        );
492        assert_eq!("baz/quz", a.c.target_path.to_str().unwrap());
493        assert!(a.c.is_built == IsBuilt::No);
494    }
495
496    #[test]
497    fn assets_globs() {
498        for (glob, paths) in [
499            ("test-resources/testroot/src/*", &["bar/main.rs"][..]),
500            ("test-resources/testroot/*/main.rs", &["bar/main.rs"]),
501            ("test-resources/testroot/*/*", &["bar/src/main.rs", "bar/testchild/Cargo.toml"]),
502            ("test-resources/*/src/*", &["bar/testroot/src/main.rs"]),
503            ("test-resources/*/src/main.rs", &["bar/main.rs"]),
504            ("test-resources/*/*/main.rs", &["bar/main.rs"]),
505            ("test-resources/testroot/**/src/*", &["bar/src/main.rs", "bar/testchild/src/main.rs"]),
506            ("test-resources/testroot/**/*.rs", &["bar/src/main.rs", "bar/testchild/src/main.rs"]),
507        ] {
508            let asset = UnresolvedAsset {
509                source_path: PathBuf::from(glob),
510                c: AssetCommon {
511                    target_path: PathBuf::from("bar/"),
512                    chmod: 0o644,
513                    asset_kind: AssetKind::Any,
514                    is_built: IsBuilt::SamePackage,
515                },
516            };
517            let assets = asset
518                .resolve(false)
519                .unwrap()
520                .into_iter()
521                .map(|asset| asset.c.target_path.to_string_lossy().to_string())
522                .collect::<Vec<_>>();
523            if assets != paths {
524                panic!("Glob: `{glob}`:\n  Expected: {paths:?}\n       Got: {assets:?}");
525            }
526        }
527    }
528
529    /// Tests that getting the debug filename from a path returns the same path
530    /// with ".debug" appended
531    #[test]
532    fn test_debug_filename() {
533        let path = Path::new("/my/test/file");
534        assert_eq!(debug_filename(path), Path::new("/my/test/file.debug"));
535    }
536
537    /// Tests that getting the debug target for an Asset that `is_built` returns
538    /// the path "/usr/lib/debug/<path-to-target>.debug"
539    #[test]
540    fn test_debug_target_ok() {
541        let a = Asset::new(
542            AssetSource::Path(PathBuf::from("target/release/bar")),
543            PathBuf::from("/usr/bin/baz/"),
544            0o644,
545            IsBuilt::SamePackage,
546            AssetKind::Any,
547        );
548        let debug_target = a.c.default_debug_target_path("usr/lib".as_ref());
549        assert_eq!(debug_target, Path::new("/usr/lib/debug/usr/bin/baz/bar.debug"));
550    }
551
552    /// Tests that getting the debug target for an Asset that `is_built` and that
553    /// has a relative path target returns the path "/usr/lib/debug/<path-to-target>.debug"
554    #[test]
555    fn test_debug_target_ok_relative() {
556        let a = Asset::new(
557            AssetSource::Path(PathBuf::from("target/release/bar")),
558            PathBuf::from("baz/"),
559            0o644,
560            IsBuilt::Workspace,
561            AssetKind::Any,
562        );
563        let debug_target = a.c.default_debug_target_path("usr/lib".as_ref());
564        assert_eq!(debug_target, Path::new("/usr/lib/debug/baz/bar.debug"));
565    }
566
567    fn to_canon_static_str(s: &str) -> &'static str {
568        let cwd = std::env::current_dir().unwrap();
569        let abs_path = cwd.join(s);
570        let abs_path_string = abs_path.to_string_lossy().into_owned();
571        Box::leak(abs_path_string.into_boxed_str())
572    }
573
574    #[test]
575    fn add_systemd_assets_with_no_config_does_nothing() {
576        let mut mock_listener = crate::listener::MockListener::new();
577        mock_listener.expect_progress().return_const(());
578
579        // supply a systemd unit file as if it were available on disk
580        let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
581
582        let (_config, mut package_debs) = BuildEnvironment::from_manifest(BuildOptions {
583            manifest_path: Some(Path::new("Cargo.toml")),
584            debug: DebugSymbolOptions {
585                #[cfg(feature = "default_enable_dbgsym")]
586                generate_dbgsym_package: Some(false),
587                #[cfg(feature = "default_enable_separate_debug_symbols")]
588                separate_debug_symbols: Some(false),
589                ..Default::default()
590            },
591            ..Default::default()
592        }, &mock_listener).unwrap();
593        let package_deb = package_debs.pop().unwrap();
594
595        let num_unit_assets = package_deb.assets.resolved.iter()
596            .filter(|a| a.c.target_path.starts_with("usr/lib/systemd/system/"))
597            .count();
598
599        assert_eq!(0, num_unit_assets);
600    }
601
602    #[test]
603    fn add_systemd_assets_with_config_adds_unit_assets() {
604        let mut mock_listener = crate::listener::MockListener::new();
605        mock_listener.expect_progress().return_const(());
606
607        // supply a systemd unit file as if it were available on disk
608        let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
609
610        let (_config, mut package_debs) = BuildEnvironment::from_manifest(BuildOptions {
611            manifest_path: Some(Path::new("Cargo.toml")),
612            debug: DebugSymbolOptions {
613                #[cfg(feature = "default_enable_dbgsym")]
614                generate_dbgsym_package: Some(false),
615                #[cfg(feature = "default_enable_separate_debug_symbols")]
616                separate_debug_symbols: Some(false),
617                ..Default::default()
618            },
619            overrides: DebConfigOverrides {
620                systemd_units: Some(vec![SystemdUnitsConfig::default()]),
621                maintainer_scripts_rel_path: Some(PathBuf::new()),
622                ..Default::default()
623            },
624            ..Default::default()
625        }, &mock_listener).unwrap();
626        let package_deb = package_debs.pop().unwrap();
627
628        let num_unit_assets = package_deb.assets.resolved
629            .iter()
630            .filter(|a| a.c.target_path.starts_with("usr/lib/systemd/system/"))
631            .count();
632
633        assert_eq!(1, num_unit_assets);
634    }
635}