ostree_ext/
ostree_prepareroot.rs1use std::io::Read;
7use std::str::FromStr;
8
9use anyhow::{Context, Result};
10use camino::Utf8Path;
11use cap_std_ext::dirext::CapStdExtDirExt;
12use fn_error_context::context;
13use ocidir::cap_std::fs::Dir;
14use ostree::glib::object::Cast;
15use ostree::prelude::FileExt;
16use ostree::{gio, glib};
17
18use crate::keyfileext::KeyFileExt;
19use crate::ostree_manual;
20use bootc_utils::ResultExt;
21
22pub const CONF_PATH: &str = "ostree/prepare-root.conf";
24
25pub fn load_config(root: &ostree::RepoFile) -> Result<Option<glib::KeyFile>> {
27 let cancellable = gio::Cancellable::NONE;
28 let kf = glib::KeyFile::new();
29 for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) {
30 let path = &path.join(CONF_PATH);
31 let f = root.resolve_relative_path(path);
32 if !f.query_exists(cancellable) {
33 continue;
34 }
35 let f = f.downcast_ref::<ostree::RepoFile>().unwrap();
36 let contents = ostree_manual::repo_file_read_to_string(f)?;
37 kf.load_from_data(&contents, glib::KeyFileFlags::NONE)
38 .with_context(|| format!("Parsing {path}"))?;
39 tracing::debug!("Loaded {path}");
40 return Ok(Some(kf));
41 }
42 tracing::debug!("No {CONF_PATH} found");
43 Ok(None)
44}
45
46pub fn load_config_from_root(root: &Dir) -> Result<Option<glib::KeyFile>> {
48 for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) {
49 let path = path.join(CONF_PATH);
50 let Some(mut f) = root.open_optional(&path)? else {
51 continue;
52 };
53 let mut contents = String::new();
54 f.read_to_string(&mut contents)?;
55 let kf = glib::KeyFile::new();
56 kf.load_from_data(&contents, glib::KeyFileFlags::NONE)
57 .with_context(|| format!("Parsing {path}"))?;
58 return Ok(Some(kf));
59 }
60 Ok(None)
61}
62
63pub fn require_config_from_root(root: &Dir) -> Result<glib::KeyFile> {
65 load_config_from_root(root)?
66 .ok_or_else(|| anyhow::anyhow!("Failed to find {CONF_PATH} in /usr/lib or /etc"))
67}
68
69pub fn overlayfs_root_enabled(root: &ostree::RepoFile) -> Result<bool> {
72 if let Some(config) = load_config(root)? {
73 overlayfs_enabled_in_config(&config)
74 } else {
75 Ok(false)
76 }
77}
78
79#[derive(Debug, PartialEq, Eq, Clone)]
81pub enum Tristate {
82 Enabled,
84 Disabled,
86 Maybe,
88}
89
90impl FromStr for Tristate {
91 type Err = anyhow::Error;
92
93 fn from_str(s: &str) -> Result<Self> {
94 let r = match s {
95 "yes" | "true" | "1" => Tristate::Enabled,
97 "no" | "false" | "0" => Tristate::Disabled,
98 "maybe" => Tristate::Maybe,
99 o => anyhow::bail!("Invalid tristate value: {o}"),
100 };
101 Ok(r)
102 }
103}
104
105impl Default for Tristate {
106 fn default() -> Self {
107 Self::Disabled
108 }
109}
110
111impl Tristate {
112 pub(crate) fn maybe_enabled(&self) -> bool {
113 match self {
114 Self::Enabled | Self::Maybe => true,
115 Self::Disabled => false,
116 }
117 }
118}
119
120#[derive(Debug, PartialEq, Eq)]
122pub enum ComposefsState {
123 Signed,
125 Verity,
127 Tristate(Tristate),
129}
130
131impl Default for ComposefsState {
132 fn default() -> Self {
133 Self::Tristate(Tristate::default())
134 }
135}
136
137impl FromStr for ComposefsState {
138 type Err = anyhow::Error;
139
140 #[context("Parsing composefs.enabled value {s}")]
141 fn from_str(s: &str) -> Result<Self> {
142 let r = match s {
143 "signed" => Self::Signed,
144 "verity" => Self::Verity,
145 o => Self::Tristate(Tristate::from_str(o)?),
146 };
147 Ok(r)
148 }
149}
150
151impl ComposefsState {
152 pub(crate) fn maybe_enabled(&self) -> bool {
153 match self {
154 ComposefsState::Signed | ComposefsState::Verity => true,
155 ComposefsState::Tristate(t) => t.maybe_enabled(),
156 }
157 }
158
159 pub fn requires_fsverity(&self) -> bool {
161 matches!(self, ComposefsState::Signed | ComposefsState::Verity)
162 }
163}
164
165pub fn overlayfs_enabled_in_config(config: &glib::KeyFile) -> Result<bool> {
167 let root_transient = config
168 .optional_bool("root", "transient")?
169 .unwrap_or_default();
170 let composefs = config
171 .optional_string("composefs", "enabled")?
172 .map(|s| ComposefsState::from_str(s.as_str()))
173 .transpose()
174 .log_err_default()
175 .unwrap_or_default();
176 Ok(root_transient || composefs.maybe_enabled())
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_tristate() {
185 for v in ["yes", "true", "1"] {
186 assert_eq!(Tristate::from_str(v).unwrap(), Tristate::Enabled);
187 }
188 assert_eq!(Tristate::from_str("maybe").unwrap(), Tristate::Maybe);
189 for v in ["no", "false", "0"] {
190 assert_eq!(Tristate::from_str(v).unwrap(), Tristate::Disabled);
191 }
192 for v in ["", "junk", "fal", "tr1"] {
193 assert!(Tristate::from_str(v).is_err());
194 }
195 }
196
197 #[test]
198 fn test_composefs_state() {
199 assert_eq!(
200 ComposefsState::from_str("signed").unwrap(),
201 ComposefsState::Signed
202 );
203 for v in ["yes", "true", "1"] {
204 assert_eq!(
205 ComposefsState::from_str(v).unwrap(),
206 ComposefsState::Tristate(Tristate::Enabled)
207 );
208 }
209 assert_eq!(Tristate::from_str("maybe").unwrap(), Tristate::Maybe);
210 for v in ["no", "false", "0"] {
211 assert_eq!(
212 ComposefsState::from_str(v).unwrap(),
213 ComposefsState::Tristate(Tristate::Disabled)
214 );
215 }
216 }
217
218 #[test]
219 fn test_overlayfs_enabled() {
220 let d0 = indoc::indoc! { r#"
221[foo]
222bar = baz
223[root]
224"# };
225 let d1 = indoc::indoc! { r#"
226[root]
227transient = false
228 "# };
229 let d2 = indoc::indoc! { r#"
230[composefs]
231enabled = false
232 "# };
233 for v in ["", d0, d1, d2] {
234 let kf = glib::KeyFile::new();
235 kf.load_from_data(v, glib::KeyFileFlags::empty()).unwrap();
236 assert!(!overlayfs_enabled_in_config(&kf).unwrap());
237 }
238
239 let e0 = format!("{d0}\n[root]\ntransient = true");
240 let e1 = format!("{d1}\n[composefs]\nenabled = true\n[other]\nsomekey = someval");
241 let e2 = format!("{d1}\n[composefs]\nenabled = yes");
242 let e3 = format!("{d1}\n[composefs]\nenabled = signed");
243 for v in [e0, e1, e2, e3] {
244 let kf = glib::KeyFile::new();
245 kf.load_from_data(&v, glib::KeyFileFlags::empty()).unwrap();
246 assert!(overlayfs_enabled_in_config(&kf).unwrap());
247 }
248 }
249}