ostree_ext/
sysroot.rs

1//! Helpers for interacting with sysroots.
2
3use std::{ops::Deref, os::fd::BorrowedFd, time::SystemTime};
4
5use anyhow::Result;
6use chrono::Datelike as _;
7use ocidir::cap_std::fs_utf8::Dir;
8use ostree::gio;
9
10/// We may automatically allocate stateroots, this string is the prefix.
11const AUTO_STATEROOT_PREFIX: &str = "state-";
12
13use crate::utils::async_task_with_spinner;
14
15/// A locked system root.
16#[derive(Debug)]
17pub struct SysrootLock {
18    /// The underlying sysroot value.
19    pub sysroot: ostree::Sysroot,
20    /// True if we didn't actually lock
21    unowned: bool,
22}
23
24impl Drop for SysrootLock {
25    fn drop(&mut self) {
26        if self.unowned {
27            return;
28        }
29        self.sysroot.unlock();
30    }
31}
32
33impl Deref for SysrootLock {
34    type Target = ostree::Sysroot;
35
36    fn deref(&self) -> &Self::Target {
37        &self.sysroot
38    }
39}
40
41/// Access the file descriptor for a sysroot
42#[allow(unsafe_code)]
43pub fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd<'_> {
44    unsafe { BorrowedFd::borrow_raw(sysroot.fd()) }
45}
46
47/// A stateroot can match our auto "state-" prefix, or be manual.
48#[derive(Debug, PartialEq, Eq)]
49pub enum StaterootKind {
50    /// This stateroot has an automatic name
51    Auto((u64, u64)),
52    /// This stateroot is manually named
53    Manual,
54}
55
56/// Metadata about a stateroot.
57#[derive(Debug, PartialEq, Eq)]
58pub struct Stateroot {
59    /// The name
60    pub name: String,
61    /// Kind
62    pub kind: StaterootKind,
63    /// Creation timestamp (from the filesystem)
64    pub creation: SystemTime,
65}
66
67impl StaterootKind {
68    fn new(name: &str) -> Self {
69        if let Some(v) = parse_auto_stateroot_name(name) {
70            return Self::Auto(v);
71        }
72        Self::Manual
73    }
74}
75
76/// Load metadata for a stateroot
77fn read_stateroot(sysroot_dir: &Dir, name: &str) -> Result<Stateroot> {
78    let path = format!("ostree/deploy/{name}");
79    let kind = StaterootKind::new(&name);
80    let creation = sysroot_dir.symlink_metadata(&path)?.created()?.into_std();
81    let r = Stateroot {
82        name: name.to_owned(),
83        kind,
84        creation,
85    };
86    Ok(r)
87}
88
89/// Enumerate stateroots, which are basically the default place for `/var`.
90pub fn list_stateroots(sysroot: &ostree::Sysroot) -> Result<Vec<Stateroot>> {
91    let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
92    let r = sysroot_dir
93        .read_dir("ostree/deploy")?
94        .try_fold(Vec::new(), |mut acc, v| {
95            let v = v?;
96            let name = v.file_name()?;
97            if sysroot_dir.try_exists(format!("ostree/deploy/{name}/deploy"))? {
98                acc.push(read_stateroot(sysroot_dir, &name)?);
99            }
100            anyhow::Ok(acc)
101        })?;
102    Ok(r)
103}
104
105/// Given a string, if it matches the form of an automatic state root, parse it into its <year>.<serial> pair.
106fn parse_auto_stateroot_name(name: &str) -> Option<(u64, u64)> {
107    let Some(statename) = name.strip_prefix(AUTO_STATEROOT_PREFIX) else {
108        return None;
109    };
110    let Some((year, serial)) = statename.split_once("-") else {
111        return None;
112    };
113    let Ok(year) = year.parse::<u64>() else {
114        return None;
115    };
116    let Ok(serial) = serial.parse::<u64>() else {
117        return None;
118    };
119    Some((year, serial))
120}
121
122/// Given a set of stateroots, allocate a new one
123pub fn allocate_new_stateroot(
124    sysroot: &ostree::Sysroot,
125    stateroots: &[Stateroot],
126    now: chrono::DateTime<chrono::Utc>,
127) -> Result<Stateroot> {
128    let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
129
130    let current_year = now.year().try_into().unwrap_or_default();
131    let (year, serial) = stateroots
132        .iter()
133        .filter_map(|v| {
134            if let StaterootKind::Auto(v) = v.kind {
135                Some(v)
136            } else {
137                None
138            }
139        })
140        .max()
141        .map(|(year, serial)| (year, serial + 1))
142        .unwrap_or((current_year, 0));
143
144    let name = format!("state-{year}-{serial}");
145
146    sysroot.init_osname(&name, gio::Cancellable::NONE)?;
147
148    read_stateroot(sysroot_dir, &name)
149}
150
151impl SysrootLock {
152    /// Asynchronously acquire a sysroot lock.  If the lock cannot be acquired
153    /// immediately, a status message will be printed to standard output.
154    /// The lock will be unlocked when this object is dropped.
155    pub async fn new_from_sysroot(sysroot: &ostree::Sysroot) -> Result<Self> {
156        let sysroot_clone = sysroot.clone();
157        let locker = tokio::task::spawn_blocking(move || sysroot_clone.lock());
158        async_task_with_spinner("Waiting for sysroot lock...", locker).await??;
159        Ok(Self {
160            sysroot: sysroot.clone(),
161            unowned: false,
162        })
163    }
164
165    /// This function should only be used when you have locked the sysroot
166    /// externally (e.g. in C/C++ code).  This also does not unlock on drop.
167    pub fn from_assumed_locked(sysroot: &ostree::Sysroot) -> Self {
168        Self {
169            sysroot: sysroot.clone(),
170            unowned: true,
171        }
172    }
173
174    /// Toggle the finalization lock state of a staged deployment.
175    /// If the deployment is currently locked, it will be unlocked, and vice versa.
176    /// The deployment must be a staged deployment.
177    #[allow(unsafe_code)]
178    pub fn change_finalization(&self, deployment: &ostree::Deployment) -> Result<()> {
179        use ostree::glib::translate::*;
180        use std::ptr;
181        unsafe {
182            let mut error = ptr::null_mut();
183            let result = ostree::ffi::ostree_sysroot_change_finalization(
184                self.sysroot.to_glib_none().0,
185                deployment.to_glib_none().0,
186                &mut error,
187            );
188            if result == 0 {
189                return Err(from_glib_full::<_, ostree::glib::Error>(error).into());
190            }
191            Ok(())
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_parse_auto_stateroot_name_valid() {
202        let test_cases = [
203            // Basic valid cases
204            ("state-2024-0", Some((2024, 0))),
205            ("state-2024-1", Some((2024, 1))),
206            ("state-2023-123", Some((2023, 123))),
207            // Large numbers
208            (
209                "state-18446744073709551615-18446744073709551615",
210                Some((18446744073709551615, 18446744073709551615)),
211            ),
212            // Zero values
213            ("state-0-0", Some((0, 0))),
214            ("state-0-123", Some((0, 123))),
215            // Leading zeros (should work - u64::parse handles them)
216            ("state-0002024-001", Some((2024, 1))),
217            ("state-000-000", Some((0, 0))),
218        ];
219
220        for (input, expected) in test_cases {
221            assert_eq!(
222                parse_auto_stateroot_name(input),
223                expected,
224                "Failed for input: {}",
225                input
226            );
227        }
228    }
229
230    #[test]
231    fn test_parse_auto_stateroot_name_invalid() {
232        let test_cases = [
233            // Missing prefix
234            "2024-1",
235            // Wrong prefix
236            "stat-2024-1",
237            "states-2024-1",
238            "prefix-2024-1",
239            // Empty string
240            "",
241            // Only prefix
242            "state-",
243            // Missing separator
244            "state-20241",
245            // Wrong separator
246            "state-2024.1",
247            "state-2024_1",
248            "state-2024:1",
249            // Multiple separators
250            "state-2024-1-2",
251            // Missing year or serial
252            "state--1",
253            "state-2024-",
254            // Non-numeric year
255            "state-abc-1",
256            "state-2024a-1",
257            // Non-numeric serial
258            "state-2024-abc",
259            "state-2024-1a",
260            // Both non-numeric
261            "state-abc-def",
262            // Negative numbers (handled by parse::<u64>() failure)
263            "state--2024-1",
264            "state-2024--1",
265            // Floating point numbers
266            "state-2024.5-1",
267            "state-2024-1.5",
268            // Numbers with whitespace
269            "state- 2024-1",
270            "state-2024- 1",
271            "state-2024 -1",
272            "state-2024- 1 ",
273            // Case sensitivity (should fail - prefix is lowercase)
274            "State-2024-1",
275            "STATE-2024-1",
276            // Unicode characters
277            "state-2024-1🦀",
278            "state-2024🦀-1",
279            // Hex-like strings (should fail - not decimal)
280            "state-0x2024-1",
281            "state-2024-0x1",
282        ];
283
284        for input in test_cases {
285            assert_eq!(
286                parse_auto_stateroot_name(input),
287                None,
288                "Expected None for input: {}",
289                input
290            );
291        }
292    }
293
294    #[test]
295    fn test_stateroot_kind_new() {
296        let test_cases = [
297            ("state-2024-1", StaterootKind::Auto((2024, 1))),
298            ("manual-name", StaterootKind::Manual),
299            ("state-invalid", StaterootKind::Manual),
300            ("", StaterootKind::Manual),
301        ];
302
303        for (input, expected) in test_cases {
304            assert_eq!(
305                StaterootKind::new(input),
306                expected,
307                "Failed for input: {}",
308                input
309            );
310        }
311    }
312}