1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::fs::{self, File};
4use std::path::{Component, Path, PathBuf};
5
6use crate::current_exe::CurrentExecutable;
7use crate::error::{Error, Result};
8use crate::stage::{PreparedTransaction, Stage};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct ProductId(String);
13
14impl ProductId {
15 pub fn new(value: impl Into<String>) -> Result<Self> {
17 Ok(Self(validate_identifier("product", value.into())?))
18 }
19
20 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24}
25
26impl fmt::Display for ProductId {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 formatter.write_str(&self.0)
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub struct ReleaseId(String);
35
36impl ReleaseId {
37 pub fn new(value: impl Into<String>) -> Result<Self> {
39 Ok(Self(validate_identifier("release", value.into())?))
40 }
41
42 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48impl fmt::Display for ReleaseId {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 formatter.write_str(&self.0)
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub struct MemberId(String);
57
58impl MemberId {
59 pub fn new(value: impl Into<String>) -> Result<Self> {
61 Ok(Self(validate_identifier("member", value.into())?))
62 }
63
64 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl fmt::Display for MemberId {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 formatter.write_str(&self.0)
73 }
74}
75
76fn validate_identifier(kind: &str, value: String) -> Result<String> {
77 if value.is_empty() || value.chars().any(char::is_control) {
78 return Err(Error::invalid(format!(
79 "{kind} identifier is empty or contains control characters"
80 )));
81 }
82 Ok(value)
83}
84
85const CURRENT_EXECUTABLE_MEMBER: &str = "executable";
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum FileKind {
92 Regular,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum PermissionsIntent {
104 Preserve,
106 Executable,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum Ownership {
117 Absent,
119 Owned,
121 Foreign,
123 Unknown,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133#[non_exhaustive]
134pub enum IntegrityRequirement {
135 None,
137 Sha256([u8; 32]),
139}
140
141pub trait OwnershipVerifier {
152 fn verify(&self, member: &MemberId, destination: &Path) -> Ownership;
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum AbsentPolicy {
163 AllowCreate,
165 DenyCreate,
167}
168
169#[derive(Clone, Copy)]
172pub struct CommitOwnership<'a> {
173 pub verifier: &'a dyn OwnershipVerifier,
175 pub absent: AbsentPolicy,
177}
178
179impl<'a> CommitOwnership<'a> {
180 pub fn new(verifier: &'a dyn OwnershipVerifier, absent: AbsentPolicy) -> Self {
182 Self { verifier, absent }
183 }
184}
185
186impl std::fmt::Debug for CommitOwnership<'_> {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("CommitOwnership")
189 .field("absent", &self.absent)
190 .finish_non_exhaustive()
191 }
192}
193
194#[derive(Debug, Clone, Copy, Default)]
198pub struct AbsentOnlyVerifier;
199
200impl OwnershipVerifier for AbsentOnlyVerifier {
201 fn verify(&self, _member: &MemberId, destination: &Path) -> Ownership {
202 match std::fs::symlink_metadata(destination) {
203 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ownership::Absent,
204 Ok(_) => Ownership::Foreign,
205 Err(_) => Ownership::Unknown,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, Default)]
217pub struct ExistingAsOwnedVerifier;
218
219impl OwnershipVerifier for ExistingAsOwnedVerifier {
220 fn verify(&self, _member: &MemberId, destination: &Path) -> Ownership {
221 match std::fs::symlink_metadata(destination) {
222 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ownership::Absent,
223 Ok(m) if m.is_file() => Ownership::Owned,
224 Ok(_) => Ownership::Foreign,
225 Err(_) => Ownership::Unknown,
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
236pub struct ExactDigestVerifier {
237 expected: std::collections::HashMap<MemberId, [u8; 32]>,
238}
239
240impl ExactDigestVerifier {
241 pub fn new(expected: Vec<(MemberId, [u8; 32])>) -> Self {
243 Self {
244 expected: expected.into_iter().collect(),
245 }
246 }
247}
248
249impl OwnershipVerifier for ExactDigestVerifier {
250 fn verify(&self, member: &MemberId, destination: &Path) -> Ownership {
251 let expected = match self.expected.get(member) {
252 Some(d) => d,
253 None => return Ownership::Unknown,
254 };
255 match std::fs::symlink_metadata(destination) {
256 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ownership::Absent,
257 Err(_) => Ownership::Unknown,
258 Ok(m) => {
259 if !m.is_file() {
260 return Ownership::Foreign;
261 }
262 #[cfg(unix)]
263 {
264 use std::os::unix::fs::MetadataExt;
265 if m.nlink() != 1 {
266 return Ownership::Foreign;
267 }
268 }
269 match crate::integrity::hash_file(destination) {
270 Ok(d) if &d == expected => Ownership::Owned,
271 Ok(_) => Ownership::Foreign,
272 Err(_) => Ownership::Unknown,
273 }
274 }
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct ArtifactMember {
286 id: MemberId,
287 source: PathBuf,
288 destination: PathBuf,
289 file_kind: FileKind,
290 permissions: PermissionsIntent,
291 integrity: IntegrityRequirement,
292}
293
294impl ArtifactMember {
295 pub fn new(
297 id: MemberId,
298 source: impl Into<PathBuf>,
299 destination: impl Into<PathBuf>,
300 ) -> Result<Self> {
301 let destination = normalize_relative_path(&destination.into())?;
302 Ok(Self {
303 id,
304 source: source.into(),
305 destination,
306 file_kind: FileKind::Regular,
307 permissions: PermissionsIntent::Preserve,
308 integrity: IntegrityRequirement::None,
309 })
310 }
311
312 pub fn with_permissions(mut self, permissions: PermissionsIntent) -> Self {
314 self.permissions = permissions;
315 self
316 }
317
318 pub fn with_integrity(mut self, integrity: IntegrityRequirement) -> Self {
324 self.integrity = integrity;
325 self
326 }
327
328 pub fn id(&self) -> &MemberId {
330 &self.id
331 }
332
333 pub fn source(&self) -> &Path {
335 &self.source
336 }
337
338 pub fn destination(&self) -> &Path {
340 &self.destination
341 }
342
343 pub fn file_kind(&self) -> FileKind {
345 self.file_kind
346 }
347
348 pub fn permissions(&self) -> PermissionsIntent {
350 self.permissions
351 }
352
353 pub fn integrity(&self) -> IntegrityRequirement {
355 self.integrity
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct ArtifactSet(Vec<ArtifactMember>);
362
363impl ArtifactSet {
364 pub fn new(members: Vec<ArtifactMember>) -> Result<Self> {
366 if members.is_empty() {
367 return Err(Error::invalid(
368 "artifact set must contain at least one member",
369 ));
370 }
371 let mut ids = HashSet::new();
372 for member in &members {
373 if !ids.insert(member.id.clone()) {
374 return Err(Error::invalid(format!(
375 "duplicate artifact member: {}",
376 member.id
377 )));
378 }
379 }
380 Ok(Self(members))
381 }
382
383 pub fn single(member: ArtifactMember) -> Result<Self> {
385 Self::new(vec![member])
386 }
387
388 pub fn iter(&self) -> impl ExactSizeIterator<Item = &ArtifactMember> {
390 self.0.iter()
391 }
392
393 pub fn len(&self) -> usize {
395 self.0.len()
396 }
397
398 pub fn is_empty(&self) -> bool {
400 self.0.is_empty()
401 }
402
403 pub(crate) fn as_slice(&self) -> &[ArtifactMember] {
404 &self.0
405 }
406}
407
408#[derive(Debug, Default)]
422pub struct BoundSources {
423 handles: HashMap<MemberId, File>,
424}
425
426impl BoundSources {
427 pub fn new() -> Self {
429 Self {
430 handles: HashMap::new(),
431 }
432 }
433
434 pub fn insert(&mut self, member: MemberId, handle: File) -> Option<File> {
437 self.handles.insert(member, handle)
438 }
439
440 pub(crate) fn is_empty(&self) -> bool {
441 self.handles.is_empty()
442 }
443
444 pub(crate) fn take(&mut self, member: &MemberId) -> Option<File> {
445 self.handles.remove(member)
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454#[non_exhaustive]
455pub enum StagePlacement {
456 SiblingOfInstallationRoot,
460 InsideInstallationRoot,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq)]
470pub struct InstallPlan {
471 product: ProductId,
472 release: ReleaseId,
473 installation_root: PathBuf,
474 artifacts: ArtifactSet,
475 placement: StagePlacement,
476 current_executable: Option<CurrentExecutable>,
477}
478
479impl InstallPlan {
480 pub fn new(
482 product: ProductId,
483 release: ReleaseId,
484 installation_root: impl Into<PathBuf>,
485 artifacts: ArtifactSet,
486 ) -> Result<Self> {
487 Self::build(
488 product,
489 release,
490 installation_root.into(),
491 artifacts,
492 StagePlacement::SiblingOfInstallationRoot,
493 None,
494 )
495 }
496
497 fn build(
498 product: ProductId,
499 release: ReleaseId,
500 installation_root: PathBuf,
501 artifacts: ArtifactSet,
502 placement: StagePlacement,
503 current_executable: Option<CurrentExecutable>,
504 ) -> Result<Self> {
505 validate_installation_root(&installation_root)?;
506 let canonical_root = fs::canonicalize(&installation_root)
507 .map_err(|source| Error::io("canonicalizing installation root", source))?;
508 let mut destinations = HashSet::new();
509 for member in artifacts.as_slice() {
510 if !destinations.insert(member.destination.clone()) {
511 return Err(Error::invalid(format!(
512 "duplicate destination: {}",
513 member.destination.display()
514 )));
515 }
516 validate_source(member, &canonical_root, &installation_root)?;
517 }
518 Ok(Self {
519 product,
520 release,
521 installation_root,
522 artifacts,
523 placement,
524 current_executable,
525 })
526 }
527
528 pub fn for_current_executable(
548 product: ProductId,
549 release: ReleaseId,
550 candidate: impl Into<PathBuf>,
551 current: &CurrentExecutable,
552 integrity: IntegrityRequirement,
553 ) -> Result<Self> {
554 let member = ArtifactMember::new(
555 MemberId::new(CURRENT_EXECUTABLE_MEMBER)?,
556 candidate,
557 current.destination_name(),
558 )?
559 .with_permissions(PermissionsIntent::Executable)
560 .with_integrity(integrity);
561 let artifacts = ArtifactSet::single(member)?;
562 Self::build(
563 product,
564 release,
565 current.installation_root().to_path_buf(),
566 artifacts,
567 StagePlacement::InsideInstallationRoot,
568 Some(current.clone()),
569 )
570 }
571
572 pub fn stage_placement(&self) -> StagePlacement {
574 self.placement
575 }
576
577 pub fn current_executable(&self) -> Option<&CurrentExecutable> {
579 self.current_executable.as_ref()
580 }
581
582 pub(crate) fn revalidate_current_executable(&self) -> Result<()> {
583 match &self.current_executable {
584 Some(current) => current.revalidate(),
585 None => Ok(()),
586 }
587 }
588
589 pub fn product(&self) -> &ProductId {
591 &self.product
592 }
593
594 pub fn release(&self) -> &ReleaseId {
596 &self.release
597 }
598
599 pub fn installation_root(&self) -> &Path {
601 &self.installation_root
602 }
603
604 pub fn artifacts(&self) -> &ArtifactSet {
606 &self.artifacts
607 }
608
609 pub fn destination(&self, member: &MemberId) -> Result<PathBuf> {
611 let artifact = self
612 .artifacts
613 .iter()
614 .find(|candidate| candidate.id == *member)
615 .ok_or_else(|| Error::UnknownMember(member.to_string()))?;
616 Ok(self.installation_root.join(&artifact.destination))
617 }
618
619 pub fn prepare(self) -> Result<PreparedTransaction> {
621 Stage::prepare(self)
622 }
623
624 pub fn prepare_with_bound_sources(self, bound: BoundSources) -> Result<PreparedTransaction> {
638 Stage::prepare_bound(self, bound)
639 }
640
641 #[cfg(test)]
642 pub(crate) fn prepare_with_failure(
643 self,
644 point: crate::test_support::FailurePoint,
645 ) -> Result<PreparedTransaction> {
646 Stage::prepare_with_failure(self, point)
647 }
648}
649
650fn validate_installation_root(root: &Path) -> Result<()> {
651 if !root.is_absolute() {
652 return Err(Error::invalid("installation root must be absolute"));
653 }
654 let metadata = fs::symlink_metadata(root)
655 .map_err(|source| Error::io("reading installation root", source))?;
656 if !metadata.is_dir() {
657 return Err(Error::invalid("installation root must be a directory"));
658 }
659 Ok(())
660}
661
662fn validate_source(
663 member: &ArtifactMember,
664 canonical_root: &Path,
665 installation_root: &Path,
666) -> Result<()> {
667 if !member.source.is_absolute() {
668 return Err(Error::invalid(format!(
669 "source for {} must be absolute",
670 member.id
671 )));
672 }
673 let metadata = fs::symlink_metadata(&member.source)
674 .map_err(|source| Error::io("reading artifact source", source))?;
675 if member.file_kind == FileKind::Regular && !metadata.is_file() {
676 return Err(Error::invalid(format!(
677 "source for {} is not a regular file",
678 member.id
679 )));
680 }
681 let canonical_source = fs::canonicalize(&member.source)
682 .map_err(|source| Error::io("canonicalizing artifact source", source))?;
683 if canonical_source.starts_with(canonical_root) {
684 return Err(Error::invalid(format!(
685 "source for {} is inside the installation root",
686 member.id
687 )));
688 }
689 let destination = installation_root.join(&member.destination);
690 if canonical_source == destination {
691 return Err(Error::invalid(format!(
692 "source for {} aliases its destination",
693 member.id
694 )));
695 }
696 Ok(())
697}
698
699pub(crate) fn normalize_relative_path(path: &Path) -> Result<PathBuf> {
700 if path.as_os_str().is_empty() || path.is_absolute() {
701 return Err(Error::invalid(
702 "destination must be a non-empty relative path",
703 ));
704 }
705 let mut normalized = PathBuf::new();
706 for component in path.components() {
707 match component {
708 Component::Normal(value) => {
709 if value.to_string_lossy().chars().any(char::is_control) {
710 return Err(Error::invalid("destination contains control characters"));
711 }
712 normalized.push(value);
713 }
714 Component::CurDir => {}
715 Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
716 return Err(Error::invalid("destination escapes the installation root"));
717 }
718 }
719 }
720 if normalized.as_os_str().is_empty() {
721 return Err(Error::invalid("destination must name a file"));
722 }
723 Ok(normalized)
724}