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