1use std::{path::PathBuf, str::FromStr};
2
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum FsTableError {
7 #[error("Invalid fstab entry: {0}")]
8 InvalidEntry(String),
9
10 #[error("Invalid number conversion: {0}")]
11 InvalidNumberConversion(String),
12
13 #[error("Invalid fsck order: {0}")]
14 InvalidFsckOrder(u8),
15
16 #[error("IO error: {0}")]
17 IoError(#[from] std::io::Error),
18
19 #[error("lsblk error: {0}")]
20 LsblkError(#[from] lsblk::LsblkError),
21}
22
23type Result<T> = std::result::Result<T, FsTableError>;
24
25#[derive(Debug, Clone, Default)]
27#[repr(u8)]
28pub enum FsckOrder {
29 #[default]
31 NoCheck = 0,
32 Boot = 1,
34 PostBoot = 2,
36}
37
38impl TryFrom<&u8> for FsckOrder {
39 type Error = FsTableError;
40
41 fn try_from(value: &u8) -> Result<Self> {
42 match value {
43 0 => Ok(Self::NoCheck),
44 1 => Ok(Self::Boot),
45 2 => Ok(Self::PostBoot),
46 _ => Err(FsTableError::InvalidFsckOrder(*value)),
47 }
48 }
49}
50
51impl TryFrom<u8> for FsckOrder {
52 type Error = FsTableError;
53
54 fn try_from(value: u8) -> Result<Self> {
55 Self::try_from(&value)
57 }
58}
59
60impl TryFrom<&str> for FsckOrder {
61 type Error = FsTableError;
62
63 fn try_from(value: &str) -> Result<Self> {
64 let n = value
65 .parse::<u8>()
66 .map_err(|e| FsTableError::InvalidNumberConversion(e.to_string()))?;
67 Self::try_from(n)
68 }
69}
70
71#[derive(Debug, Clone, Default)]
72pub struct FsEntry {
73 pub device_spec: String,
88 pub mountpoint: Option<String>,
103
104 pub fs_type: String,
113
114 pub options: Vec<String>,
119
120 pub dump_freq: u8,
125
126 pub pass: FsckOrder,
130}
131
132impl FsEntry {
133 pub fn from_line_str(line: &str) -> std::result::Result<Self, FsTableError> {
135 let parts: Vec<&str> = line.split_whitespace().collect();
137
138 if parts.len() < 6 {
139 return Err(FsTableError::InvalidEntry(line.to_string()));
140 }
141
142 let device_spec = parts[0].to_string();
143
144 let mountpoint = if parts[1] == "none" {
145 None
146 } else {
147 Some(parts[1].to_string())
148 };
149
150 let fs_type = parts[2].to_string();
151
152 let options = parts[3].split(',').map(|s| s.to_string()).collect();
153
154 let dump_freq = parts[4]
155 .parse::<u8>()
156 .map_err(|_| FsTableError::InvalidEntry(line.to_string()))?;
157 let pass = FsckOrder::try_from(parts[5])?;
158
159 Ok(Self {
160 device_spec,
161 mountpoint,
162 fs_type,
163 options,
164 dump_freq,
165 pass,
166 })
167 }
168
169 pub fn to_line_str(&self) -> String {
171 let mountpoint = self.mountpoint.as_deref().unwrap_or("none");
172 let options = if self.options.is_empty() {
173 "defaults".to_string()
174 } else {
175 self.options.join(",")
176 };
177 let pass = self.pass.clone() as u8;
178
179 format!(
180 "{device_spec}\t{mountpoint}\t{fs_type}\t{options}\t{dump_freq}\t{pass}",
181 device_spec = self.device_spec,
182 mountpoint = mountpoint,
183 fs_type = self.fs_type,
184 options = options,
185 pass = pass,
186 dump_freq = self.dump_freq,
187 )
188 }
189}
190
191impl TryFrom<&str> for FsEntry {
192 type Error = FsTableError;
193
194 fn try_from(value: &str) -> Result<Self> {
195 Self::from_line_str(value)
196 }
197}
198
199impl std::fmt::Display for FsEntry {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 f.write_str(&self.to_line_str())
202 }
203}
204
205#[derive(Debug)]
206pub struct FsTable {
207 pub entries: Vec<FsEntry>,
208}
209
210impl FromStr for FsTable {
211 type Err = FsTableError;
212
213 fn from_str(table: &str) -> Result<Self> {
214 let entries = table
215 .lines()
216 .map(FsEntry::from_line_str)
217 .collect::<Result<Vec<FsEntry>>>()?;
218
219 Ok(Self { entries })
220 }
221}
222
223impl std::fmt::Display for FsTable {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 self.entries
226 .iter()
227 .map(|entry| entry.to_line_str())
228 .collect::<Vec<_>>()
229 .join("\n")
230 .as_str()
231 .fmt(f)
232 }
233}
234
235impl TryFrom<&str> for FsTable {
242 type Error = FsTableError;
243
244 fn try_from(value: &str) -> Result<Self> {
245 Self::from_str(value)
246 }
247}
248
249pub fn read_mtab() -> Result<FsTable> {
250 let mtab = std::fs::read_to_string("/etc/mtab")
251 .map_err(|e| FsTableError::InvalidEntry(e.to_string()))?;
252 FsTable::from_str(&mtab)
253}
254
255pub fn read_fstab() -> Result<FsTable> {
256 let fstab = std::fs::read_to_string("/etc/fstab")
257 .map_err(|e| FsTableError::InvalidEntry(e.to_string()))?;
258 FsTable::from_str(&fstab)
259}
260
261pub fn generate_fstab(prefix: &str) -> Result<FsTable> {
276 let mtab = read_mtab()?;
277
278 let prefix = prefix.trim_end_matches('/');
283
284 let block_list = lsblk::BlockDevice::list()?;
285
286 let entries = (mtab.entries.into_iter())
288 .filter(|entry| (entry.mountpoint.as_ref()).is_some_and(|mp| mp.starts_with(prefix)))
289 .map(|mut entry| -> Result<FsEntry> {
290 entry.mountpoint = Some(
291 match entry.mountpoint.unwrap().strip_prefix(prefix).unwrap() {
292 "" => "/",
293 path => path,
294 }
295 .to_string(),
296 );
297
298 let device_spec_og = entry.device_spec.clone();
299
300 let uuid = block_list
301 .iter()
302 .find(|dev| dev.fullname == PathBuf::from(&device_spec_og))
303 .and_then(|dev| dev.uuid.as_ref())
304 .ok_or_else(|| {
305 FsTableError::InvalidEntry(format!(
306 "Could not find UUID for device: {}",
307 device_spec_og
308 ))
309 })?;
310 entry.device_spec = format!("UUID={uuid}");
311 Ok(entry)
312 })
313 .collect::<Result<Vec<_>>>()?;
314
315 Ok(FsTable { entries })
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_fstab_parse() {
324 let line = "/dev/sda1\t/\text4\trw,relatime\t0\t1";
325 let entry = FsEntry::from_line_str(line).unwrap();
326
327 assert_eq!(entry.device_spec, "/dev/sda1");
328 assert_eq!(entry.mountpoint, Some("/".to_string()));
329 assert_eq!(entry.fs_type, "ext4");
330 assert_eq!(entry.options, vec!["rw", "relatime"]);
331 assert_eq!(entry.dump_freq, 0);
332 assert_eq!(entry.pass as u8, 1);
333 }
334
335 #[test]
336 fn test_fstab_serialize() {
337 let entry = FsEntry {
338 device_spec: "/dev/sda1".to_string(),
339 mountpoint: Some("/".to_string()),
340 fs_type: "ext4".to_string(),
341 options: vec!["rw".to_string(), "relatime".to_string()],
342 dump_freq: 0,
343 pass: FsckOrder::Boot,
344 };
345
346 assert_eq!(entry.to_line_str(), "/dev/sda1\t/\text4\trw,relatime\t0\t1");
347 }
348
349 #[test]
350 fn test_fsck_order() {
351 assert_eq!(FsckOrder::try_from(&0u8).unwrap() as u8, 0);
352 assert_eq!(FsckOrder::try_from(&1u8).unwrap() as u8, 1);
353 assert_eq!(FsckOrder::try_from(&2u8).unwrap() as u8, 2);
354 assert!(FsckOrder::try_from(&3u8).is_err());
355 }
356
357 #[test]
358 fn test_fstab_table() {
359 let table = "/dev/sda1\t/\text4\trw,relatime\t0\t1\n/dev/sda2\tnone\tswap\tsw\t0\t0";
360 let fstab = FsTable::from_str(table).unwrap();
361
362 assert_eq!(fstab.entries.len(), 2);
363 assert_eq!(fstab.entries[0].device_spec, "/dev/sda1");
364 assert_eq!(fstab.entries[1].device_spec, "/dev/sda2");
365
366 let serialized = fstab.to_string();
367 assert_eq!(serialized, table);
368 }
369
370 #[test]
371 fn test_mtab_parse() {
372 let mtab = std::fs::read_to_string("/etc/mtab").unwrap();
373
374 let table = FsTable::from_str(&mtab).unwrap();
375
376 println!("{:#?}", table.to_string());
377 }
378
379 #[test]
380 fn test_generate_fstab() {
381 let fstab = generate_fstab("/mnt/custom").unwrap();
382
383 println!("{}", fstab.to_string());
384
385 assert!(fstab.to_string().contains('\n'));
387 }
388}