1use 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
10const AUTO_STATEROOT_PREFIX: &str = "state-";
12
13use crate::utils::async_task_with_spinner;
14
15#[derive(Debug)]
17pub struct SysrootLock {
18 pub sysroot: ostree::Sysroot,
20 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#[allow(unsafe_code)]
43pub fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd<'_> {
44 unsafe { BorrowedFd::borrow_raw(sysroot.fd()) }
45}
46
47#[derive(Debug, PartialEq, Eq)]
49pub enum StaterootKind {
50 Auto((u64, u64)),
52 Manual,
54}
55
56#[derive(Debug, PartialEq, Eq)]
58pub struct Stateroot {
59 pub name: String,
61 pub kind: StaterootKind,
63 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
76fn 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
89pub 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
105fn 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
122pub 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 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 pub fn from_assumed_locked(sysroot: &ostree::Sysroot) -> Self {
168 Self {
169 sysroot: sysroot.clone(),
170 unowned: true,
171 }
172 }
173
174 #[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 ("state-2024-0", Some((2024, 0))),
205 ("state-2024-1", Some((2024, 1))),
206 ("state-2023-123", Some((2023, 123))),
207 (
209 "state-18446744073709551615-18446744073709551615",
210 Some((18446744073709551615, 18446744073709551615)),
211 ),
212 ("state-0-0", Some((0, 0))),
214 ("state-0-123", Some((0, 123))),
215 ("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 "2024-1",
235 "stat-2024-1",
237 "states-2024-1",
238 "prefix-2024-1",
239 "",
241 "state-",
243 "state-20241",
245 "state-2024.1",
247 "state-2024_1",
248 "state-2024:1",
249 "state-2024-1-2",
251 "state--1",
253 "state-2024-",
254 "state-abc-1",
256 "state-2024a-1",
257 "state-2024-abc",
259 "state-2024-1a",
260 "state-abc-def",
262 "state--2024-1",
264 "state-2024--1",
265 "state-2024.5-1",
267 "state-2024-1.5",
268 "state- 2024-1",
270 "state-2024- 1",
271 "state-2024 -1",
272 "state-2024- 1 ",
273 "State-2024-1",
275 "STATE-2024-1",
276 "state-2024-1🦀",
278 "state-2024🦀-1",
279 "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}