11//! Helpers for interacting with sysroots.
22
3- use std:: ops:: Deref ;
3+ use std:: { ops:: Deref , os :: fd :: BorrowedFd , time :: SystemTime } ;
44
55use anyhow:: Result ;
6+ use chrono:: Datelike as _;
7+ use ocidir:: cap_std:: fs_utf8:: Dir ;
8+ use ostree:: gio;
9+
10+ /// We may automatically allocate stateroots, this string is the prefix.
11+ const AUTO_STATEROOT_PREFIX : & str = "state-" ;
612
713use crate :: utils:: async_task_with_spinner;
814
@@ -32,6 +38,117 @@ impl Deref for SysrootLock {
3238 }
3339}
3440
41+
42+ /// Access the file descriptor for a sysroot
43+ #[ allow( unsafe_code) ]
44+ pub fn sysroot_fd ( sysroot : & ostree:: Sysroot ) -> BorrowedFd {
45+ unsafe { BorrowedFd :: borrow_raw ( sysroot. fd ( ) ) }
46+ }
47+
48+ /// A stateroot can match our auto "state-" prefix, or be manual.
49+ #[ derive( Debug , PartialEq , Eq ) ]
50+ pub enum StaterootKind {
51+ /// This stateroot has an automatic name
52+ Auto ( ( u64 , u64 ) ) ,
53+ /// This stateroot is manually named
54+ Manual ,
55+ }
56+
57+ /// Metadata about a stateroot.
58+ #[ derive( Debug , PartialEq , Eq ) ]
59+ pub struct Stateroot {
60+ /// The name
61+ pub name : String ,
62+ /// Kind
63+ pub kind : StaterootKind ,
64+ /// Creation timestamp (from the filesystem)
65+ pub creation : SystemTime ,
66+ }
67+
68+ impl StaterootKind {
69+ fn new ( name : & str ) -> Self {
70+ if let Some ( v) = parse_auto_stateroot_name ( name) {
71+ return Self :: Auto ( v) ;
72+ }
73+ Self :: Manual
74+ }
75+ }
76+
77+ /// Load metadata for a stateroot
78+ fn read_stateroot ( sysroot_dir : & Dir , name : & str ) -> Result < Stateroot > {
79+ let path = format ! ( "ostree/deploy/{name}" ) ;
80+ let kind = StaterootKind :: new ( & name) ;
81+ let creation = sysroot_dir. symlink_metadata ( & path) ?. created ( ) ?. into_std ( ) ;
82+ let r = Stateroot {
83+ name : name. to_owned ( ) ,
84+ kind,
85+ creation,
86+ } ;
87+ Ok ( r)
88+ }
89+
90+ /// Enumerate stateroots, which are basically the default place for `/var`.
91+ pub fn list_stateroots ( sysroot : & ostree:: Sysroot ) -> Result < Vec < Stateroot > > {
92+ let sysroot_dir = & Dir :: reopen_dir ( & sysroot_fd ( sysroot) ) ?;
93+ let r = sysroot_dir
94+ . read_dir ( "ostree/deploy" ) ?
95+ . try_fold ( Vec :: new ( ) , |mut acc, v| {
96+ let v = v?;
97+ let name = v. file_name ( ) ?;
98+ if sysroot_dir. try_exists ( format ! ( "ostree/deploy/{name}/deploy" ) ) ? {
99+ acc. push ( read_stateroot ( sysroot_dir, & name) ?) ;
100+ }
101+ anyhow:: Ok ( acc)
102+ } ) ?;
103+ Ok ( r)
104+ }
105+
106+ /// Given a string, if it matches the form of an automatic state root, parse it into its <year>.<serial> pair.
107+ fn parse_auto_stateroot_name ( name : & str ) -> Option < ( u64 , u64 ) > {
108+ let Some ( statename) = name. strip_prefix ( AUTO_STATEROOT_PREFIX ) else {
109+ return None ;
110+ } ;
111+ let Some ( ( year, serial) ) = statename. split_once ( "-" ) else {
112+ return None ;
113+ } ;
114+ let Ok ( year) = year. parse :: < u64 > ( ) else {
115+ return None ;
116+ } ;
117+ let Ok ( serial) = serial. parse :: < u64 > ( ) else {
118+ return None ;
119+ } ;
120+ Some ( ( year, serial) )
121+ }
122+
123+ /// Given a set of stateroots, allocate a new one
124+ pub fn allocate_new_stateroot (
125+ sysroot : & ostree:: Sysroot ,
126+ stateroots : & [ Stateroot ] ,
127+ now : chrono:: DateTime < chrono:: Utc > ,
128+ ) -> Result < Stateroot > {
129+ let sysroot_dir = & Dir :: reopen_dir ( & sysroot_fd ( sysroot) ) ?;
130+
131+ let current_year = now. year ( ) . try_into ( ) . unwrap_or_default ( ) ;
132+ let ( year, serial) = stateroots
133+ . iter ( )
134+ . filter_map ( |v| {
135+ if let StaterootKind :: Auto ( v) = v. kind {
136+ Some ( v)
137+ } else {
138+ None
139+ }
140+ } )
141+ . max ( )
142+ . map ( |( year, serial) | ( year, serial + 1 ) )
143+ . unwrap_or ( ( current_year, 0 ) ) ;
144+
145+ let name = format ! ( "state-{year}-{serial}" ) ;
146+
147+ sysroot. init_osname ( & name, gio:: Cancellable :: NONE ) ?;
148+
149+ read_stateroot ( sysroot_dir, & name)
150+ }
151+
35152impl SysrootLock {
36153 /// Asynchronously acquire a sysroot lock. If the lock cannot be acquired
37154 /// immediately, a status message will be printed to standard output.
@@ -55,3 +172,121 @@ impl SysrootLock {
55172 }
56173 }
57174}
175+
176+ #[ cfg( test) ]
177+ mod tests {
178+ use super :: * ;
179+
180+ #[ test]
181+ fn test_parse_auto_stateroot_name_valid ( ) {
182+ let test_cases = [
183+ // Basic valid cases
184+ ( "state-2024-0" , Some ( ( 2024 , 0 ) ) ) ,
185+ ( "state-2024-1" , Some ( ( 2024 , 1 ) ) ) ,
186+ ( "state-2023-123" , Some ( ( 2023 , 123 ) ) ) ,
187+ // Large numbers
188+ (
189+ "state-18446744073709551615-18446744073709551615" ,
190+ Some ( ( 18446744073709551615 , 18446744073709551615 ) ) ,
191+ ) ,
192+ // Zero values
193+ ( "state-0-0" , Some ( ( 0 , 0 ) ) ) ,
194+ ( "state-0-123" , Some ( ( 0 , 123 ) ) ) ,
195+ // Leading zeros (should work - u64::parse handles them)
196+ ( "state-0002024-001" , Some ( ( 2024 , 1 ) ) ) ,
197+ ( "state-000-000" , Some ( ( 0 , 0 ) ) ) ,
198+ ] ;
199+
200+ for ( input, expected) in test_cases {
201+ assert_eq ! (
202+ parse_auto_stateroot_name( input) ,
203+ expected,
204+ "Failed for input: {}" ,
205+ input
206+ ) ;
207+ }
208+ }
209+
210+ #[ test]
211+ fn test_parse_auto_stateroot_name_invalid ( ) {
212+ let test_cases = [
213+ // Missing prefix
214+ "2024-1" ,
215+ // Wrong prefix
216+ "stat-2024-1" ,
217+ "states-2024-1" ,
218+ "prefix-2024-1" ,
219+ // Empty string
220+ "" ,
221+ // Only prefix
222+ "state-" ,
223+ // Missing separator
224+ "state-20241" ,
225+ // Wrong separator
226+ "state-2024.1" ,
227+ "state-2024_1" ,
228+ "state-2024:1" ,
229+ // Multiple separators
230+ "state-2024-1-2" ,
231+ // Missing year or serial
232+ "state--1" ,
233+ "state-2024-" ,
234+ // Non-numeric year
235+ "state-abc-1" ,
236+ "state-2024a-1" ,
237+ // Non-numeric serial
238+ "state-2024-abc" ,
239+ "state-2024-1a" ,
240+ // Both non-numeric
241+ "state-abc-def" ,
242+ // Negative numbers (handled by parse::<u64>() failure)
243+ "state--2024-1" ,
244+ "state-2024--1" ,
245+ // Floating point numbers
246+ "state-2024.5-1" ,
247+ "state-2024-1.5" ,
248+ // Numbers with whitespace
249+ "state- 2024-1" ,
250+ "state-2024- 1" ,
251+ "state-2024 -1" ,
252+ "state-2024- 1 " ,
253+ // Case sensitivity (should fail - prefix is lowercase)
254+ "State-2024-1" ,
255+ "STATE-2024-1" ,
256+ // Unicode characters
257+ "state-2024-1🦀" ,
258+ "state-2024🦀-1" ,
259+ // Hex-like strings (should fail - not decimal)
260+ "state-0x2024-1" ,
261+ "state-2024-0x1" ,
262+ ] ;
263+
264+ for input in test_cases {
265+ assert_eq ! (
266+ parse_auto_stateroot_name( input) ,
267+ None ,
268+ "Expected None for input: {}" ,
269+ input
270+ ) ;
271+ }
272+ }
273+
274+ #[ test]
275+ fn test_stateroot_kind_new ( ) {
276+ let test_cases = [
277+ ( "state-2024-1" , StaterootKind :: Auto ( ( 2024 , 1 ) ) ) ,
278+ ( "manual-name" , StaterootKind :: Manual ) ,
279+ ( "state-invalid" , StaterootKind :: Manual ) ,
280+ ( "" , StaterootKind :: Manual ) ,
281+ ] ;
282+
283+ for ( input, expected) in test_cases {
284+ assert_eq ! (
285+ StaterootKind :: new( input) ,
286+ expected,
287+ "Failed for input: {}" ,
288+ input
289+ ) ;
290+ }
291+ }
292+ }
0 commit comments