Skip to main content

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
14#[derive(Debug, Clone)]
15pub enum AssetSource {
16    /// Copy file from the path (and strip binary if needed).
17    Path(PathBuf),
18    /// A symlink existing in the file system
19    Symlink(SymlinkKind),
20    /// Write data to destination as-is.
21    Data(Vec<u8>),
22}
23
24#[derive(Debug, Clone)]
25pub enum SymlinkKind {
26    // symlinks copied due to preserve_symlinks or 
27    // because the symlink destination didn't exist
28    Copied {
29        source_path: PathBuf
30    },
31    // symlinks defined in the manifest
32    Created {
33        target_path: PathBuf,
34        link_name: PathBuf,
35    }
36    
37}
38
39impl AssetSource {
40    /// Symlink must exist on disk to be preserved
41    #[must_use]
42    pub fn from_path(path: impl Into<PathBuf>, preserve_symlinks: bool) -> Self {
43        let path = path.into();
44        if preserve_symlinks || !path.exists() { // !exists means a symlink to bogus path
45            if let Ok(md) = fs::symlink_metadata(&path) {
46                if md.is_symlink() {
47                    return Self::Symlink(SymlinkKind::Copied { source_path: path });
48                }
49            }
50        }
51        Self::Path(path)
52    }
53
54    #[must_use]
55    pub fn source_path(&self) -> Option<&Path> {
56        match self {
57            Self::Symlink(SymlinkKind ::Copied { source_path:p }) 
58            | Self::Path(p) => Some(p),
59            Self::Symlink(SymlinkKind::Created { .. })
60            | Self::Data(_) => None,
61        }
62    }
63
64    #[must_use]
65    pub fn into_path(self) -> Option<PathBuf> {
66        match self {
67            Self::Symlink(SymlinkKind ::Copied { source_path:p }) 
68            | Self::Path(p) => Some(p),
69            Self::Symlink(SymlinkKind::Created { .. })
70            | Self::Data(_) => None,
71        }
72    }
73
74    #[must_use]
75    pub fn archive_as_symlink_only(&self) -> bool {
76        matches!(self, Self::Symlink(_))
77    }
78
79    #[must_use]
80    pub fn file_size(&self) -> Option<u64> {
81        match *self {
82            Self::Path(ref p) => fs::metadata(p).ok().map(|m| m.len()),
83            Self::Data(ref d) => Some(d.len() as u64),
84            Self::Symlink(_) => None,
85        }
86    }
87
88    pub fn data(&self) -> CDResult<Cow<'_, [u8]>> {
89        Ok(match self {
90            Self::Path(p) => {
91                let data = read_file_to_bytes(p)
92                    .map_err(|e| CargoDebError::IoFile("Unable to read asset to add to archive", e, p.clone()))?;
93                Cow::Owned(data)
94            },
95            Self::Data(d) => Cow::Borrowed(d),
96            Self::Symlink(SymlinkKind::Copied { source_path:p }) => {
97                let data = read_file_to_bytes(p)
98                    .map_err(|e| CargoDebError::IoFile("Symlink unexpectedly used to read file data", e, p.clone()))?;
99                Cow::Owned(data)
100            },
101            AssetSource::Symlink(SymlinkKind::Created { target_path, link_name:_ }) => {
102                return Err(CargoDebError::CannotReadVirtualSymlink(target_path.to_owned()))
103            },
104        })
105    }
106
107    pub(crate) fn magic_bytes(&self) -> Option<[u8; 4]> {
108        match self {
109            Self::Path(p) | Self::Symlink(SymlinkKind::Copied { source_path:p }) => {
110                let mut buf = [0; 4];
111                use std::io::Read;
112                let mut file = std::fs::File::open(p).ok()?;
113                file.read_exact(&mut buf[..]).ok()?;
114                Some(buf)
115            },
116            Self::Data(d) => {
117                d.get(..4).and_then(|b| b.try_into().ok())
118            },
119            Self::Symlink(SymlinkKind::Created { .. }) => {
120                None
121            }
122        }
123    }
124}
125
126#[derive(Debug, Clone)]
127pub(crate) struct Assets {
128    pub unresolved: Vec<UnresolvedAsset>,
129    pub resolved: Vec<Asset>,
130}
131
132#[derive(Debug, Clone, serde::Deserialize)]
133#[serde(try_from = "CargoDebAssetArrayOrTable")]
134pub(crate) enum RawAssetOrAuto {
135    Auto,
136    RawAsset(RawAsset),
137}
138
139impl RawAssetOrAuto {
140    pub fn asset(self) -> Option<RawAsset> {
141        match self {
142            Self::RawAsset(a) => Some(a),
143            Self::Auto => None,
144        }
145    }
146}
147
148impl From<RawAsset> for RawAssetOrAuto {
149    fn from(r: RawAsset) -> Self {
150        Self::RawAsset(r)
151    }
152}
153
154#[derive(Debug, Clone, serde::Deserialize)]
155#[serde(try_from = "RawAssetOrAuto")]
156pub(crate) enum RawAsset {
157    Asset{
158        source_path: PathBuf,
159        target_path: PathBuf,
160        chmod: Option<u32>,
161        preserve_symlinks: Option<bool>,
162    },
163    Symlink {
164        target_path: PathBuf,
165        link_name: PathBuf
166    }
167}
168
169impl RawAsset {
170    pub(crate) fn source_path(&self) -> Option<&PathBuf> {
171        match self {
172            RawAsset::Asset { source_path, .. } => Some(source_path),
173            RawAsset::Symlink { .. } => None,
174        }
175    }
176
177    pub(crate) fn target_path(&self) -> &PathBuf {
178        match self {
179            RawAsset::Asset { target_path, .. } 
180            | RawAsset::Symlink { target_path,.. } => target_path,
181        }
182    }
183
184    pub(crate) fn chmod(&self) -> Option<u32> {
185        match self {
186            RawAsset::Asset {  chmod, .. } => *chmod,
187            RawAsset::Symlink { .. } => None /* or should this return 0o777 ? */,
188        }
189    }
190
191    #[cfg(test)]
192    pub(crate) fn link_name(&self) -> Option<&PathBuf>{
193        match self {
194            RawAsset::Asset { .. } => None,
195            RawAsset::Symlink { link_name ,..} => Some(link_name),
196        }
197    }
198}
199
200impl TryFrom<RawAssetOrAuto> for RawAsset {
201    type Error = &'static str;
202
203    fn try_from(maybe_auto: RawAssetOrAuto) -> Result<Self, Self::Error> {
204        maybe_auto.asset().ok_or("$auto is not allowed here")
205    }
206}
207
208impl Assets {
209    pub(crate) const fn new(unresolved: Vec<UnresolvedAsset>, resolved: Vec<Asset>) -> Self {
210        Self {
211            unresolved,
212            resolved,
213        }
214    }
215
216    pub(crate) fn iter(&self) -> impl Iterator<Item = &AssetCommon> {
217        self.resolved.iter().map(|u| &u.c).chain(self.unresolved.iter().map(UnresolvedAsset::common))
218    }
219}
220
221#[derive(Debug, Copy, Clone, Eq, PartialEq)]
222pub enum AssetKind {
223    Any,
224    CargoExampleBinary,
225    SeparateDebugSymbols,
226}
227
228#[derive(Debug, Copy, Clone, Eq, PartialEq)]
229pub enum IsBuilt {
230    No,
231    SamePackage,
232    /// needs --workspace to build
233    Workspace,
234}
235
236fn get_file_mode(path: &Path) -> CDResult<u32> {
237    
238    #[cfg(not(unix))]
239    {
240        Err(CargoDebError::ImplicitFileModeFromPathNotSupported(path.to_path_buf()))
241    }
242    
243    #[cfg(unix)]
244    {
245        let metadata = fs::metadata(path)
246            .map_err(|e| CargoDebError::IoFile(
247                "Unable to read file metadata for permissions", 
248                e, 
249                path.to_owned()
250            ))?;
251        use std::os::unix::fs::PermissionsExt;
252        Ok(metadata.permissions().mode() & 0o7777)
253    }
254}
255
256#[derive(Debug, Clone)]
257pub enum UnresolvedAsset {
258    Asset {
259        source_path: PathBuf,
260        preserve_symlinks: bool,
261        c: AssetCommon,
262    },
263    Symlink {
264        link_name: PathBuf,
265        c: AssetCommon,
266    }
267}
268
269impl UnresolvedAsset {
270    pub(crate) fn new_asset(source_path: PathBuf, target_path: PathBuf, chmod: Option<u32>, is_built: IsBuilt, asset_kind: AssetKind, preserve_symlinks: bool) -> Self {
271        Self::Asset {
272            source_path,
273            preserve_symlinks,
274            c: AssetCommon { target_path, chmod, asset_kind, is_built },
275        }
276    }
277    pub(crate) fn new_symlink( target_path: PathBuf, link_name: PathBuf) -> Self {
278        Self::Symlink {
279            link_name,
280            c: AssetCommon { target_path, chmod: None, asset_kind: AssetKind::Any, is_built: IsBuilt::No },
281        }
282    }
283
284    pub fn common(&self) ->&AssetCommon {
285        match self {
286            UnresolvedAsset::Asset {c, .. } 
287            | UnresolvedAsset::Symlink {c, .. } => c,
288        }
289    }
290    
291
292    /// Convert `source_path` (with glob or dir) to actual path
293    pub fn resolve(&self) -> CDResult<Vec<Asset>> {
294        let (source_path, &preserve_symlinks, &AssetCommon { ref target_path, chmod, is_built, asset_kind }) =  match self {
295            UnresolvedAsset::Symlink { link_name, c } => {
296                let target_path = Asset::normalized_target_path(c.target_path.to_owned(), Some(link_name));               
297
298                return Ok(vec![Asset{ 
299                    source: AssetSource::Symlink(SymlinkKind::Created { target_path: target_path.clone(), link_name: link_name.to_owned() }), 
300                    processed_from: Some(ProcessedFrom { original_path: None, action: "symlink" }), c: AssetCommon { target_path, ..c.clone() }
301                }])
302            },
303            UnresolvedAsset::Asset { source_path, preserve_symlinks, c } =>  {
304                (source_path, preserve_symlinks, c)
305            },
306        };
307
308        let source_prefix_len = is_glob_pattern(source_path.as_os_str()).then(|| {
309            let file_name_is_glob = source_path
310                .file_name()
311                .is_some_and(is_glob_pattern);
312
313            if file_name_is_glob {
314                // skip to the component before the glob
315                let glob_component_pos = source_path
316                    .parent()
317                    .and_then(|parent| parent.iter().position(is_glob_pattern));
318                glob_component_pos.unwrap_or_else(|| {
319                    source_path
320                        .iter()
321                        .count()
322                })
323            } else {
324                // extract the only file name component
325                source_path
326                    .iter()
327                    .count()
328                    .saturating_sub(1)
329            }
330        });
331
332        let matched_assets = glob::glob(source_path.to_str().ok_or("utf8 path")?)?
333            // Remove dirs from globs without throwing away errors
334            .map(|entry| {
335                let source_file = entry?;
336                Ok(if source_file.is_dir() { None } else { Some(source_file) })
337            })
338            .filter_map(|res: Result<Option<PathBuf>, glob::GlobError>| {
339                Some(res.transpose()?.map_err(CargoDebError::from).and_then(|source_file| {
340                    let target_file = if let Some(source_prefix_len) = source_prefix_len {
341                        target_path.join(
342                            source_file
343                            .iter()
344                            .skip(source_prefix_len)
345                            .collect::<PathBuf>())
346                    } else {
347                        target_path.clone()
348                    };
349                    // Use provided chmod or read from filesystem
350                    let file_chmod = match chmod {
351                        Some(chmod) => chmod,
352                        None => get_file_mode(&source_file)?,
353                    };
354                    log::debug!("asset {} -> {} {} {:o}", source_file.display(), target_file.display(), if is_built != IsBuilt::No {"copy"} else {"build"}, file_chmod);
355                    
356                    let asset = Asset::new(
357                        AssetSource::from_path(source_file, preserve_symlinks),
358                        target_file,
359                        Some(file_chmod),
360                        is_built,
361                        asset_kind,
362                    );
363                    if source_prefix_len.is_some() {
364                        Ok(asset.processed("glob", None))
365                    } else {
366                        Ok(asset)
367                    }
368                }))
369            })
370            .collect::<CDResult<Vec<_>>>()
371            .map_err(|e| e.context(format_args!("Error while glob searching {}", source_path.display())))?;
372
373        if matched_assets.is_empty() {
374            return Err(CargoDebError::AssetFileNotFound(
375                source_path.clone(),
376                Asset::normalized_target_path(target_path.clone(), Some(source_path)),
377                source_prefix_len.is_some(), is_built != IsBuilt::No));
378        }
379        Ok(matched_assets)
380    }
381    
382    pub(crate) fn source_path(&self) -> Option<&Path> {
383        match self {
384            UnresolvedAsset::Asset { source_path, .. } => Some(source_path),
385            UnresolvedAsset::Symlink { .. } => None,
386        }
387    }
388}
389
390#[derive(Debug, Clone)]
391pub struct AssetCommon {
392    pub target_path: PathBuf,
393    pub chmod: Option<u32>,
394    pub(crate) asset_kind: AssetKind,
395    is_built: IsBuilt,
396}
397
398pub(crate) struct AssetFmt<'a> {
399    c: &'a AssetCommon,
400    cwd: &'a Path,
401    source: Option<&'a Path>,
402    processed_from: Option<&'a ProcessedFrom>,
403}
404
405impl<'a> AssetFmt<'a> {
406    pub fn new(asset: &'a Asset, cwd: &'a Path) -> Self {
407        Self {
408            c: &asset.c,
409            source: asset.source.source_path(),
410            processed_from: asset.processed_from.as_ref(),
411            cwd,
412        }
413    }
414
415    pub fn unresolved(asset: &'a UnresolvedAsset, cwd: &'a Path) -> Self {
416        match asset {
417            UnresolvedAsset::Asset { source_path, preserve_symlinks:_, c } => {
418                Self {
419                    c,
420                    source: Some(source_path),
421                    processed_from: None,
422                    cwd,
423                }
424            },
425            UnresolvedAsset::Symlink { .. } => {
426                unreachable!()
427            },
428        }
429    }
430}
431
432impl fmt::Display for AssetFmt<'_> {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        let mut src = self.source;
435        let action = self.processed_from.map(|proc| {
436            src = proc.original_path.as_deref().or(src);
437            proc.action
438        });
439        if let Some(src) = src {
440            write!(f, "{} ", src.strip_prefix(self.cwd).unwrap_or(src).display())?;
441        }
442        if let Some(action) = action {
443            write!(f, "({action}{}) ", if self.c.is_built() { "; built" } else { "" })?;
444        } else if self.c.is_built() {
445            write!(f, "(built) ")?;
446        }
447        write!(f, "-> {}", self.c.target_path.display())?;
448        Ok(())
449    }
450}
451
452#[derive(Debug, Clone)]
453pub struct Asset {
454    pub source: AssetSource,
455    /// For prettier path display not "/tmp/blah.tmp"
456    pub processed_from: Option<ProcessedFrom>,
457    pub c: AssetCommon,
458}
459
460#[derive(Debug, Clone)]
461pub struct ProcessedFrom {
462    pub original_path: Option<PathBuf>,
463    pub action: &'static str,
464}
465
466impl Asset {
467    #[must_use]
468    pub fn normalized_target_path(mut target_path: PathBuf, source_path: Option<&Path>) -> PathBuf {
469        // is_dir() is only for paths that exist
470        // should this use https://doc.rust-lang.org/std/path/struct.Path.html#method.has_trailing_sep once that is stable and MSRV allows using it?
471        if target_path.to_string_lossy().ends_with('/') {
472            let file_name = source_path.and_then(|p| p.file_name()).expect("source must be a file");
473            target_path = target_path.join(file_name);
474        }
475
476        if target_path.is_absolute() || target_path.has_root() {
477            target_path = target_path.strip_prefix("/").expect("no root dir").to_owned();
478        }
479        target_path
480    }
481
482    #[must_use]
483    pub fn new(source: AssetSource, target_path: PathBuf, chmod: Option<u32>, is_built: IsBuilt, asset_kind: AssetKind) -> Self {
484        let target_path = Self::normalized_target_path(target_path, source.source_path());
485        Self {
486            source,
487            processed_from: None,
488            c: AssetCommon { target_path, chmod, asset_kind, is_built },
489        }
490    }
491
492    #[must_use]
493    pub fn processed(mut self, action: &'static str, original_path: impl Into<Option<PathBuf>>) -> Self {
494        debug_assert!(self.processed_from.is_none());
495        self.processed_from = Some(ProcessedFrom {
496            original_path: original_path.into(),
497            action,
498        });
499        self
500    }
501
502    pub(crate) fn is_binary_executable(&self) -> bool {
503        self.c.is_executable()
504            && self.c.target_path.extension().is_none_or(|ext| ext != "sh")
505            && (self.c.is_built() || self.smells_like_elf())
506    }
507
508    fn smells_like_elf(&self) -> bool {
509        self.source.magic_bytes().is_some_and(|b| b == [0x7F, b'E', b'L', b'F'])
510    }
511}
512
513impl AssetCommon {
514    pub(crate) const fn is_executable(&self) -> bool {
515        if let Some(chmod) = self.chmod {
516            0 != chmod & 0o111
517        } else {
518            false
519        }
520    }
521
522    pub(crate) fn is_dynamic_library(&self) -> bool {
523        is_dynamic_library_filename(&self.target_path)
524    }
525
526    pub(crate) fn is_built(&self) -> bool {
527        self.is_built != IsBuilt::No
528    }
529
530    /// Returns the target path for the debug symbol file, which will be
531    /// /usr/lib/debug/<path-to-executable>.debug
532    #[must_use]
533    pub(crate) fn default_debug_target_path(&self, lib_dir_base: &Path) -> PathBuf {
534        // Turn an absolute path into one relative to "/"
535        let relative = self.target_path.strip_prefix(Path::new("/"))
536            .unwrap_or(self.target_path.as_path());
537
538        // Prepend the debug location and add .debug
539        let mut path = Path::new("/").join(lib_dir_base);
540        path.push("debug");
541        path.push(debug_filename(relative));
542        path
543    }
544
545    pub(crate) fn is_same_package(&self) -> bool {
546        self.is_built == IsBuilt::SamePackage
547    }
548}
549
550/// Adds `.debug` to the end of a path to a filename
551fn debug_filename(path: &Path) -> PathBuf {
552    let mut debug_filename = path.as_os_str().to_os_string();
553    debug_filename.push(".debug");
554    debug_filename.into()
555}
556
557pub(crate) fn is_dynamic_library_filename(path: &Path) -> bool {
558    path.file_name()
559        .and_then(|f| f.to_str())
560        .is_some_and(|f| f.ends_with(DLL_SUFFIX))
561}
562
563/// Compress man pages and other assets per Debian Policy.
564///
565/// # References
566///
567/// <https://www.debian.org/doc/debian-policy/ch-docs.html>
568/// <https://lintian.debian.org/tags/manpage-not-compressed.html>
569pub fn compressed_assets(package_deb: &PackageConfig, listener: &dyn Listener) -> CDResult<Vec<(usize, Asset)>> {
570    fn needs_compression(path: &str) -> bool {
571        !path.ends_with(".gz") &&
572            (path.starts_with("usr/share/man/") ||
573                (path.starts_with("usr/share/doc/") && (path.ends_with("/NEWS") || path.ends_with("/changelog"))) ||
574                (path.starts_with("usr/share/info/") && path.ends_with(".info")))
575    }
576
577    package_deb.assets.resolved.iter().enumerate()
578        .filter(|(_, asset)| {
579            asset.c.target_path.starts_with("usr") && !asset.c.is_built() && needs_compression(&asset.c.target_path.to_string_lossy())
580        })
581        .par_bridge()
582        .map(|(idx, orig_asset)| {
583            let mut file_name = orig_asset.c.target_path.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
584            file_name.push_str(".gz");
585            let new_path = orig_asset.c.target_path.with_file_name(file_name);
586            listener.progress("Compressing", format!("'{}'", new_path.display()));
587            let gzdata = gzipped(&orig_asset.source.data()?)
588                .map_err(|e| CargoDebError::Io(e).context("error while gzipping asset"))?;
589            CDResult::Ok((idx, Asset::new(
590                crate::assets::AssetSource::Data(gzdata),
591                new_path,
592                orig_asset.c.chmod,
593                IsBuilt::No,
594                AssetKind::Any,
595            ).processed("compressed",
596                orig_asset.source.source_path().unwrap_or(&orig_asset.c.target_path).to_path_buf()
597            )))
598        }).collect()
599}
600
601pub fn apply_compressed_assets(package_deb: &mut PackageConfig, new_assets: Vec<(usize, Asset)>) {
602    for (idx, asset) in new_assets {
603        package_deb.assets.resolved[idx] = asset;
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use crate::config::{BuildEnvironment, BuildOptions, DebConfigOverrides, DebugSymbolOptions};
611    use crate::parse::manifest::SystemdUnitsConfig;
612    use crate::util::tests::add_test_fs_paths;
613
614    #[test]
615    fn assets() {
616        let a = Asset::new(
617            AssetSource::Path(PathBuf::from("target/release/bar")),
618            PathBuf::from("baz/"),
619            Some(0o644),
620            IsBuilt::SamePackage,
621            AssetKind::Any,
622        );
623        assert_eq!("baz/bar", a.c.target_path.to_str().unwrap());
624        assert!(a.c.is_built != IsBuilt::No);
625
626        let a = Asset::new(
627            AssetSource::Path(PathBuf::from("foo/bar")),
628            PathBuf::from("/baz/quz"),
629            Some(0o644),
630            IsBuilt::No,
631            AssetKind::Any,
632        );
633        assert_eq!("baz/quz", a.c.target_path.to_str().unwrap());
634        assert!(a.c.is_built == IsBuilt::No);
635    }
636
637    #[test]
638    #[cfg(unix)]
639    fn resolve_without_permissions_reads_from_filesystem() {
640        // When chmod is None, resolve() should read the file's permissions from disk
641        let source_path = PathBuf::from("test-resources/testroot/src/main.rs");
642        assert!(source_path.exists(), "test file must exist");
643
644        let asset = UnresolvedAsset::Asset {
645            source_path: source_path.clone(),
646            preserve_symlinks: false,
647            c: AssetCommon {
648                target_path: PathBuf::from("usr/share/test/"),
649                chmod: None, // no permissions specified
650                asset_kind: AssetKind::Any,
651                is_built: IsBuilt::No,
652            },
653        };
654
655        let resolved = asset.resolve().unwrap();
656        assert_eq!(resolved.len(), 1);
657
658        let resolved_asset = &resolved[0];
659        // The chmod should have been populated from the filesystem
660        assert!(resolved_asset.c.chmod.is_some(), "chmod should be read from filesystem when not specified in manifest");
661
662        // Verify the permission value matches what the filesystem reports
663        use std::os::unix::fs::PermissionsExt;
664        let expected_mode = fs::metadata(&source_path).unwrap().permissions().mode() & 0o7777;
665        assert_eq!(resolved_asset.c.chmod.unwrap(), expected_mode);
666    }
667
668    #[test]
669    fn resolve_with_explicit_permissions_ignores_filesystem() {
670        // When chmod is Some, resolve() should use the specified value, not the filesystem
671        let source_path = PathBuf::from("test-resources/testroot/src/main.rs");
672        assert!(source_path.exists(), "test file must exist");
673
674        let asset = UnresolvedAsset::Asset {
675            source_path: source_path.clone(),
676            preserve_symlinks: false,
677            c: AssetCommon {
678                target_path: PathBuf::from("usr/share/test/"),
679                chmod: Some(0o755), // explicit permissions
680                asset_kind: AssetKind::Any,
681                is_built: IsBuilt::No,
682            },
683        };
684
685        let resolved = asset.resolve().unwrap();
686        assert_eq!(resolved.len(), 1);
687        assert_eq!(resolved[0].c.chmod, Some(0o755), "explicit chmod should be preserved");
688    }
689
690    
691    #[test]
692    fn resolve_created_symlink() {
693        let asset = UnresolvedAsset::Symlink {
694            link_name: PathBuf::from("../some.service"), 
695            c: AssetCommon {
696                target_path: PathBuf::from("usr/lib/systemd/system/multi-user.target.wants/"),
697                chmod: None, 
698                asset_kind: AssetKind::Any,
699                is_built: IsBuilt::No,
700            },
701        };
702
703        let resolved = asset.resolve().unwrap();
704        assert_eq!(resolved.len(), 1);
705        assert_eq!(&resolved[0].c.target_path, "usr/lib/systemd/system/multi-user.target.wants/some.service");
706    }
707
708    #[test]
709    fn assets_globs() {
710        for (glob, paths) in [
711            ("test-resources/testroot/src/*", &["bar/main.rs"][..]),
712            ("test-resources/testroot/*/main.rs", &["bar/main.rs"]),
713            ("test-resources/testroot/*/*", &["bar/src/main.rs", "bar/testchild/Cargo.toml"]),
714            ("test-resources/*/src/*", &["bar/testroot/src/main.rs"]),
715            ("test-resources/*/src/main.rs", &["bar/main.rs"]),
716            ("test-resources/*/*/main.rs", &["bar/main.rs"]),
717            ("test-resources/testroot/**/src/*", &["bar/src/main.rs", "bar/testchild/src/main.rs"]),
718            ("test-resources/testroot/**/*.rs", &["bar/src/main.rs", "bar/testchild/src/main.rs"]),
719        ] {
720            let asset = UnresolvedAsset::Asset {
721                source_path: PathBuf::from(glob),
722                preserve_symlinks: false,
723                c: AssetCommon {
724                    target_path: PathBuf::from("bar/"),
725                    chmod: Some(0o644),
726                    asset_kind: AssetKind::Any,
727                    is_built: IsBuilt::SamePackage,
728                },
729            };
730            let assets = asset
731                .resolve()
732                .unwrap()
733                .into_iter()
734                .map(|asset| asset.c.target_path.to_string_lossy().to_string())
735                .collect::<Vec<_>>();
736            if assets != paths {
737                panic!("Glob: `{glob}`:\n  Expected: {paths:?}\n       Got: {assets:?}");
738            }
739        }
740    }
741
742    /// Tests that getting the debug filename from a path returns the same path
743    /// with ".debug" appended
744    #[test]
745    fn test_debug_filename() {
746        let path = Path::new("/my/test/file");
747        assert_eq!(debug_filename(path), Path::new("/my/test/file.debug"));
748    }
749
750    /// Tests that getting the debug target for an Asset that `is_built` returns
751    /// the path "/usr/lib/debug/<path-to-target>.debug"
752    #[test]
753    fn test_debug_target_ok() {
754        let a = Asset::new(
755            AssetSource::Path(PathBuf::from("target/release/bar")),
756            PathBuf::from("/usr/bin/baz/"),
757            Some(0o644),
758            IsBuilt::SamePackage,
759            AssetKind::Any,
760        );
761        let debug_target = a.c.default_debug_target_path("usr/lib".as_ref());
762        assert_eq!(debug_target, Path::new("/usr/lib/debug/usr/bin/baz/bar.debug"));
763    }
764
765    /// Tests that getting the debug target for an Asset that `is_built` and that
766    /// has a relative path target returns the path "/usr/lib/debug/<path-to-target>.debug"
767    #[test]
768    fn test_debug_target_ok_relative() {
769        let a = Asset::new(
770            AssetSource::Path(PathBuf::from("target/release/bar")),
771            PathBuf::from("baz/"),
772            Some(0o644),
773            IsBuilt::Workspace,
774            AssetKind::Any,
775        );
776        let debug_target = a.c.default_debug_target_path("usr/lib".as_ref());
777        assert_eq!(debug_target, Path::new("/usr/lib/debug/baz/bar.debug"));
778    }
779
780    fn to_canon_static_str(s: &str) -> &'static str {
781        let cwd = std::env::current_dir().unwrap();
782        let abs_path = cwd.join(s);
783        let abs_path_string = abs_path.to_string_lossy().into_owned();
784        Box::leak(abs_path_string.into_boxed_str())
785    }
786
787    #[test]
788    fn add_systemd_assets_with_no_config_does_nothing() {
789        let mut mock_listener = crate::listener::MockListener::new();
790        mock_listener.expect_progress().return_const(());
791
792        // supply a systemd unit file as if it were available on disk
793        let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
794
795        let (_config, mut package_debs) = BuildEnvironment::from_manifest(BuildOptions {
796            manifest_path: Some(Path::new("Cargo.toml")),
797            debug: DebugSymbolOptions {
798                #[cfg(feature = "default_enable_dbgsym")]
799                generate_dbgsym_package: Some(false),
800                #[cfg(feature = "default_enable_separate_debug_symbols")]
801                separate_debug_symbols: Some(false),
802                ..Default::default()
803            },
804            ..Default::default()
805        }, &mock_listener).unwrap();
806        let package_deb = package_debs.pop().unwrap();
807
808        let num_unit_assets = package_deb.assets.resolved.iter()
809            .filter(|a| a.c.target_path.starts_with("usr/lib/systemd/system/"))
810            .count();
811
812        assert_eq!(0, num_unit_assets);
813    }
814
815    #[test]
816    fn add_systemd_assets_with_config_adds_unit_assets() {
817        let mut mock_listener = crate::listener::MockListener::new();
818        mock_listener.expect_progress().return_const(());
819
820        // supply a systemd unit file as if it were available on disk
821        let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
822
823        let (_config, mut package_debs) = BuildEnvironment::from_manifest(BuildOptions {
824            manifest_path: Some(Path::new("Cargo.toml")),
825            debug: DebugSymbolOptions {
826                #[cfg(feature = "default_enable_dbgsym")]
827                generate_dbgsym_package: Some(false),
828                #[cfg(feature = "default_enable_separate_debug_symbols")]
829                separate_debug_symbols: Some(false),
830                ..Default::default()
831            },
832            overrides: DebConfigOverrides {
833                systemd_units: Some(vec![SystemdUnitsConfig::default()]),
834                maintainer_scripts_rel_path: Some(PathBuf::new()),
835                ..Default::default()
836            },
837            ..Default::default()
838        }, &mock_listener).unwrap();
839        let package_deb = package_debs.pop().unwrap();
840
841        let num_unit_assets = package_deb.assets.resolved
842            .iter()
843            .filter(|a| a.c.target_path.starts_with("usr/lib/systemd/system/"))
844            .count();
845
846        assert_eq!(1, num_unit_assets);
847    }
848}