Skip to main content

elfpak_core/plan/
model.rs

1//! Immutable output-plan data exposed to callers.
2
3use crate::{
4    elf::Architecture,
5    graph::{DependencyGraph, Digest},
6    policy::{DependencyPolicy, Preset, RuntimeFeature, RuntimePolicy},
7};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum PlannedFileKind {
12    Directory,
13    Symlink,
14    Executable,
15    Interpreter,
16    SharedObject,
17    CertificateBundle,
18    RuntimeConfig,
19    ApplicationData,
20}
21
22impl PlannedFileKind {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            PlannedFileKind::Directory => "directory",
26            PlannedFileKind::Symlink => "symlink",
27            PlannedFileKind::Executable => "executable",
28            PlannedFileKind::Interpreter => "interpreter",
29            PlannedFileKind::SharedObject => "shared-object",
30            PlannedFileKind::CertificateBundle => "certificate-bundle",
31            PlannedFileKind::RuntimeConfig => "runtime-config",
32            PlannedFileKind::ApplicationData => "application-data",
33        }
34    }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum InclusionReason {
39    Application,
40    Interpreter,
41    NeededBy { binary: PathBuf, soname: String },
42    RuntimePolicy { feature: RuntimeFeature },
43    ExplicitInclude,
44}
45
46/// One entry of the output rootfs. Nothing is written that is not planned here.
47#[derive(Debug, Clone)]
48pub struct PlannedFile {
49    /// Host path to copy from, if the content comes from the source root.
50    pub(crate) source: Option<PathBuf>,
51    /// Absolute path inside the generated rootfs.
52    pub(crate) destination: PathBuf,
53    pub(crate) kind: PlannedFileKind,
54    pub(crate) reason: InclusionReason,
55    pub(crate) mode: u32,
56    pub(crate) size: u64,
57    pub(crate) sha256: Option<Digest>,
58    /// Verbatim link target for [`PlannedFileKind::Symlink`].
59    pub(crate) link_target: Option<PathBuf>,
60    /// Generated content (passwd, nsswitch.conf, ...).
61    pub(crate) content: Option<Vec<u8>>,
62}
63
64impl PlannedFile {
65    pub fn source(&self) -> Option<&Path> {
66        self.source.as_deref()
67    }
68
69    pub fn destination(&self) -> &Path {
70        &self.destination
71    }
72
73    pub fn kind(&self) -> PlannedFileKind {
74        self.kind
75    }
76
77    pub fn reason(&self) -> &InclusionReason {
78        &self.reason
79    }
80
81    pub fn mode(&self) -> u32 {
82        self.mode
83    }
84
85    pub fn size(&self) -> u64 {
86        self.size
87    }
88
89    pub fn sha256(&self) -> Option<&Digest> {
90        self.sha256.as_ref()
91    }
92
93    pub fn link_target(&self) -> Option<&Path> {
94        self.link_target.as_deref()
95    }
96
97    pub fn content(&self) -> Option<&[u8]> {
98        self.content.as_deref()
99    }
100
101    /// Invariants every entry holds, checked when it enters a plan and again
102    /// before it is written.
103    pub(crate) fn assert_well_formed(&self) {
104        assert!(self.destination.is_absolute());
105        assert!(self.mode <= 0o7777);
106
107        match self.kind {
108            PlannedFileKind::Directory => {
109                assert!(self.source.is_none());
110                assert!(self.link_target.is_none());
111                assert!(self.content.is_none());
112                assert!(self.sha256.is_none());
113                assert_eq!(self.size, 0);
114            }
115            PlannedFileKind::Symlink => {
116                assert!(self.source.is_none());
117                assert!(self.link_target.is_some());
118                assert!(self.content.is_none());
119                assert!(self.sha256.is_none());
120                assert_eq!(self.size, 0);
121            }
122            _ => {
123                assert!(self.link_target.is_none());
124                assert_ne!(self.source.is_some(), self.content.is_some());
125                assert!(self.sha256.as_ref().is_some_and(Digest::is_well_formed));
126                if let Some(content) = &self.content {
127                    assert_eq!(self.size, content.len() as u64);
128                }
129            }
130        }
131    }
132}
133
134#[derive(Debug, Clone)]
135pub struct BundlePlan {
136    pub(crate) executable: PlannedFile,
137    /// All entries, including the executable, sorted by destination.
138    pub(crate) files: Vec<PlannedFile>,
139    pub(crate) graph: DependencyGraph,
140    pub(crate) architecture: Architecture,
141    /// Preset the policy was derived from, when one was named.
142    pub(crate) preset: Option<Preset>,
143    /// Runtime policy this plan was built with, recorded for the manifest.
144    pub(crate) runtime_policy: RuntimePolicy,
145    pub(crate) dependency_policy: DependencyPolicy,
146    /// `PT_INTERP` as declared by the executable.
147    pub(crate) interpreter: Option<PathBuf>,
148    /// Where that interpreter actually lives after following symlinks.
149    pub(crate) interpreter_resolved: Option<PathBuf>,
150    pub(crate) warnings: Vec<Warning>,
151}
152
153impl BundlePlan {
154    pub fn executable(&self) -> &PlannedFile {
155        &self.executable
156    }
157
158    pub fn files(&self) -> &[PlannedFile] {
159        &self.files
160    }
161
162    pub fn graph(&self) -> &DependencyGraph {
163        &self.graph
164    }
165
166    pub fn architecture(&self) -> Architecture {
167        self.architecture
168    }
169
170    pub fn preset(&self) -> Option<Preset> {
171        self.preset
172    }
173
174    pub fn runtime_policy(&self) -> &RuntimePolicy {
175        &self.runtime_policy
176    }
177
178    pub fn dependency_policy(&self) -> &DependencyPolicy {
179        &self.dependency_policy
180    }
181
182    pub fn interpreter(&self) -> Option<&Path> {
183        self.interpreter.as_deref()
184    }
185
186    pub fn interpreter_resolved(&self) -> Option<&Path> {
187        self.interpreter_resolved.as_deref()
188    }
189
190    pub fn warnings(&self) -> &[Warning] {
191        &self.warnings
192    }
193
194    pub fn total_size(&self) -> u64 {
195        self.files.iter().map(|file| file.size).sum()
196    }
197
198    pub fn files_of_kind(&self, kind: PlannedFileKind) -> impl Iterator<Item = &PlannedFile> {
199        self.files.iter().filter(move |file| file.kind == kind)
200    }
201}
202
203#[derive(Debug, Clone)]
204pub struct Warning {
205    pub code: &'static str,
206    pub message: String,
207    pub details: Vec<String>,
208}