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 Path(PathBuf),
17 Symlink(PathBuf),
19 Data(Vec<u8>),
21}
22
23impl AssetSource {
24 #[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() { 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 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 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 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 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 .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 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 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 #[must_use]
392 pub(crate) fn default_debug_target_path(&self, lib_dir_base: &Path) -> PathBuf {
393 let relative = self.target_path.strip_prefix(Path::new("/"))
395 .unwrap_or(self.target_path.as_path());
396
397 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
409fn 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
422pub 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 #[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 #[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 #[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 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 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}