1
1
//! Helpers for interacting with sysroots.
2
2
3
- use std:: ops:: Deref ;
3
+ use std:: { ops:: Deref , os :: fd :: BorrowedFd , time :: SystemTime } ;
4
4
5
5
use 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-" ;
6
12
7
13
use crate :: utils:: async_task_with_spinner;
8
14
@@ -32,6 +38,117 @@ impl Deref for SysrootLock {
32
38
}
33
39
}
34
40
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
+
35
152
impl SysrootLock {
36
153
/// Asynchronously acquire a sysroot lock. If the lock cannot be acquired
37
154
/// immediately, a status message will be printed to standard output.
@@ -55,3 +172,121 @@ impl SysrootLock {
55
172
}
56
173
}
57
174
}
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