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 ApplicationPlan {
136    pub(crate) executable: PlannedFile,
137    pub(crate) graph: DependencyGraph,
138    /// `PT_INTERP` as declared by this executable.
139    pub(crate) interpreter: Option<PathBuf>,
140    /// Where that interpreter lives after following symlinks.
141    pub(crate) interpreter_resolved: Option<PathBuf>,
142}
143
144impl ApplicationPlan {
145    pub fn executable(&self) -> &PlannedFile {
146        &self.executable
147    }
148
149    pub fn graph(&self) -> &DependencyGraph {
150        &self.graph
151    }
152
153    pub fn interpreter(&self) -> Option<&Path> {
154        self.interpreter.as_deref()
155    }
156
157    pub fn interpreter_resolved(&self) -> Option<&Path> {
158        self.interpreter_resolved.as_deref()
159    }
160}
161
162#[derive(Debug, Clone)]
163pub struct BundlePlan {
164    pub(crate) applications: Vec<ApplicationPlan>,
165    /// All entries, including the executable, sorted by destination.
166    pub(crate) files: Vec<PlannedFile>,
167    pub(crate) architecture: Architecture,
168    /// Preset the policy was derived from, when one was named.
169    pub(crate) preset: Option<Preset>,
170    /// Runtime policy this plan was built with, recorded for the manifest.
171    pub(crate) runtime_policy: RuntimePolicy,
172    pub(crate) dependency_policy: DependencyPolicy,
173    pub(crate) warnings: Vec<Warning>,
174}
175
176impl BundlePlan {
177    /// The first application, retained for singular callers.
178    pub fn executable(&self) -> &PlannedFile {
179        self.applications
180            .first()
181            .expect("a bundle plan always contains an application")
182            .executable()
183    }
184
185    pub fn applications(&self) -> &[ApplicationPlan] {
186        &self.applications
187    }
188
189    pub fn executables(&self) -> impl Iterator<Item = &PlannedFile> {
190        self.applications.iter().map(ApplicationPlan::executable)
191    }
192
193    pub fn files(&self) -> &[PlannedFile] {
194        &self.files
195    }
196
197    /// The first dependency graph, retained for singular callers.
198    pub fn graph(&self) -> &DependencyGraph {
199        self.applications
200            .first()
201            .expect("a bundle plan always contains an application")
202            .graph()
203    }
204
205    pub fn architecture(&self) -> Architecture {
206        self.architecture
207    }
208
209    pub fn preset(&self) -> Option<Preset> {
210        self.preset
211    }
212
213    pub fn runtime_policy(&self) -> &RuntimePolicy {
214        &self.runtime_policy
215    }
216
217    pub fn dependency_policy(&self) -> &DependencyPolicy {
218        &self.dependency_policy
219    }
220
221    /// The first application's interpreter, retained for singular callers.
222    pub fn interpreter(&self) -> Option<&Path> {
223        self.applications
224            .first()
225            .and_then(ApplicationPlan::interpreter)
226    }
227
228    /// The first application's resolved interpreter, retained for singular callers.
229    pub fn interpreter_resolved(&self) -> Option<&Path> {
230        self.applications
231            .first()
232            .and_then(ApplicationPlan::interpreter_resolved)
233    }
234
235    pub fn warnings(&self) -> &[Warning] {
236        &self.warnings
237    }
238
239    pub fn total_size(&self) -> u64 {
240        self.files.iter().map(|file| file.size).sum()
241    }
242
243    pub fn files_of_kind(&self, kind: PlannedFileKind) -> impl Iterator<Item = &PlannedFile> {
244        self.files.iter().filter(move |file| file.kind == kind)
245    }
246}
247
248#[derive(Debug, Clone)]
249pub struct Warning {
250    pub code: &'static str,
251    pub message: String,
252    pub details: Vec<String>,
253}