1use std::{ffi::OsStr, fs, io, path::PathBuf};
26use uuid::Uuid;
27
28#[must_use]
31pub fn sysfs_btrfs_path(uuid: &Uuid) -> PathBuf {
32 PathBuf::from(format!("/sys/fs/btrfs/{}", uuid.as_hyphenated()))
33}
34
35#[must_use]
38pub fn sysfs_btrfs_path_file(uuid: &Uuid, name: &str) -> PathBuf {
39 sysfs_btrfs_path(uuid).join(name)
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct CommitStats {
46 pub commits: u64,
48 pub cur_commit_ms: u64,
50 pub last_commit_ms: u64,
52 pub max_commit_ms: u64,
54 pub total_commit_ms: u64,
56}
57
58pub struct SysfsBtrfs {
61 base: PathBuf,
62}
63
64impl SysfsBtrfs {
65 #[must_use]
67 pub fn new(uuid: &Uuid) -> Self {
68 Self {
69 base: sysfs_btrfs_path(uuid),
70 }
71 }
72
73 fn read_file(&self, name: &str) -> io::Result<String> {
74 let s = fs::read_to_string(self.base.join(name))?;
75 Ok(s.trim_end().to_owned())
76 }
77
78 fn read_u64(&self, name: &str) -> io::Result<u64> {
79 let s = self.read_file(name)?;
80 s.parse()
81 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
82 }
83
84 fn read_bool(&self, name: &str) -> io::Result<bool> {
85 Ok(self.read_u64(name)? != 0)
86 }
87
88 pub fn bg_reclaim_threshold(&self) -> io::Result<u64> {
95 self.read_u64("bg_reclaim_threshold")
96 }
97
98 pub fn checksum(&self) -> io::Result<String> {
105 self.read_file("checksum")
106 }
107
108 pub fn clone_alignment(&self) -> io::Result<u64> {
115 self.read_u64("clone_alignment")
116 }
117
118 pub fn commit_stats(&self) -> io::Result<CommitStats> {
125 let contents = self.read_file("commit_stats")?;
126 let mut commits = None;
127 let mut cur_commit_ms = None;
128 let mut last_commit_ms = None;
129 let mut max_commit_ms = None;
130 let mut total_commit_ms = None;
131
132 for line in contents.lines() {
133 let mut parts = line.splitn(2, ' ');
134 let key = parts.next().unwrap_or("").trim();
135 let val: u64 =
136 parts.next().unwrap_or("").trim().parse().map_err(|e| {
137 io::Error::new(io::ErrorKind::InvalidData, e)
138 })?;
139 match key {
140 "commits" => commits = Some(val),
141 "cur_commit_ms" => cur_commit_ms = Some(val),
142 "last_commit_ms" => last_commit_ms = Some(val),
143 "max_commit_ms" => max_commit_ms = Some(val),
144 "total_commit_ms" => total_commit_ms = Some(val),
145 _ => {}
146 }
147 }
148
149 let missing = |name| {
150 io::Error::new(
151 io::ErrorKind::InvalidData,
152 format!("commit_stats: missing field '{name}'"),
153 )
154 };
155
156 Ok(CommitStats {
157 commits: commits.ok_or_else(|| missing("commits"))?,
158 cur_commit_ms: cur_commit_ms
159 .ok_or_else(|| missing("cur_commit_ms"))?,
160 last_commit_ms: last_commit_ms
161 .ok_or_else(|| missing("last_commit_ms"))?,
162 max_commit_ms: max_commit_ms
163 .ok_or_else(|| missing("max_commit_ms"))?,
164 total_commit_ms: total_commit_ms
165 .ok_or_else(|| missing("total_commit_ms"))?,
166 })
167 }
168
169 pub fn reset_commit_stats(&self) -> io::Result<()> {
177 fs::write(self.base.join("commit_stats"), b"0")
178 }
179
180 pub fn exclusive_operation(&self) -> io::Result<String> {
188 self.read_file("exclusive_operation")
189 }
190
191 pub fn wait_for_exclusive_operation(&self) -> io::Result<String> {
202 let mut op = self.exclusive_operation()?;
203 if op == "none" {
204 return Ok(op);
205 }
206 let waited_for = op.clone();
207 while op != "none" {
208 std::thread::sleep(std::time::Duration::from_secs(1));
209 op = self.exclusive_operation()?;
210 }
211 Ok(waited_for)
212 }
213
214 pub fn features(&self) -> io::Result<Vec<String>> {
222 let mut features = Vec::new();
223 for entry in fs::read_dir(self.base.join("features"))? {
224 let entry = entry?;
225 if let Some(name) = entry.file_name().to_str() {
226 features.push(name.to_owned());
227 }
228 }
229 features.sort();
230 Ok(features)
231 }
232
233 pub fn generation(&self) -> io::Result<u64> {
240 self.read_u64("generation")
241 }
242
243 pub fn label(&self) -> io::Result<String> {
250 self.read_file("label")
251 }
252
253 pub fn metadata_uuid(&self) -> io::Result<Uuid> {
261 let s = self.read_file("metadata_uuid")?;
262 Uuid::parse_str(&s)
263 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
264 }
265
266 pub fn nodesize(&self) -> io::Result<u64> {
273 self.read_u64("nodesize")
274 }
275
276 pub fn quota_override(&self) -> io::Result<bool> {
283 self.read_bool("quota_override")
284 }
285
286 pub fn read_policy(&self) -> io::Result<String> {
293 self.read_file("read_policy")
294 }
295
296 pub fn sectorsize(&self) -> io::Result<u64> {
303 self.read_u64("sectorsize")
304 }
305
306 pub fn temp_fsid(&self) -> io::Result<bool> {
313 self.read_bool("temp_fsid")
314 }
315
316 pub fn scrub_speed_max_get(&self, devid: u64) -> io::Result<u64> {
324 let path = format!("devinfo/{devid}/scrub_speed_max");
325 match self.read_u64(&path) {
326 Ok(v) => Ok(v),
327 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(0),
328 Err(e) => Err(e),
329 }
330 }
331
332 pub fn scrub_speed_max_set(
341 &self,
342 devid: u64,
343 limit: u64,
344 ) -> io::Result<()> {
345 let path = self.base.join(format!("devinfo/{devid}/scrub_speed_max"));
346 fs::write(path, format!("{limit}\n"))
347 }
348
349 #[must_use]
355 pub fn send_stream_version(&self) -> u32 {
356 let path =
358 std::path::Path::new("/sys/fs/btrfs/features/send_stream_version");
359 match fs::read_to_string(path) {
360 Ok(s) => s.trim().parse::<u32>().unwrap_or(1),
361 Err(_) => 1,
362 }
363 }
364
365 pub fn quota_status(&self) -> io::Result<QuotaStatus> {
377 let qgroups = self.base.join("qgroups");
378
379 if !qgroups.exists() {
380 return Ok(QuotaStatus {
381 enabled: false,
382 mode: None,
383 inconsistent: None,
384 override_limits: None,
385 drop_subtree_threshold: None,
386 total_count: None,
387 level0_count: None,
388 });
389 }
390
391 let mode = {
392 let s = fs::read_to_string(qgroups.join("mode"))?;
393 s.trim_end().to_owned()
394 };
395 let inconsistent = fs::read_to_string(qgroups.join("inconsistent"))?
396 .trim()
397 .parse::<u64>()
398 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
399 != 0;
400 let override_limits = self.read_bool("quota_override")?;
401 let drop_subtree_threshold =
402 fs::read_to_string(qgroups.join("drop_subtree_threshold"))?
403 .trim()
404 .parse::<u64>()
405 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
406
407 let mut total_count: u64 = 0;
408 let mut level0_count: u64 = 0;
409 for entry in fs::read_dir(&qgroups)? {
410 let entry = entry?;
411 let raw_name = entry.file_name();
412 let name = raw_name.to_string_lossy();
413 if let Some((level, _id)) =
414 parse_qgroup_entry_name(OsStr::new(name.as_ref()))
415 {
416 total_count += 1;
417 if level == 0 {
418 level0_count += 1;
419 }
420 }
421 }
422
423 Ok(QuotaStatus {
424 enabled: true,
425 mode: Some(mode),
426 inconsistent: Some(inconsistent),
427 override_limits: Some(override_limits),
428 drop_subtree_threshold: Some(drop_subtree_threshold),
429 total_count: Some(total_count),
430 level0_count: Some(level0_count),
431 })
432 }
433}
434
435#[cfg(test)]
436impl SysfsBtrfs {
437 fn with_base(base: PathBuf) -> Self {
438 Self { base }
439 }
440}
441
442fn parse_qgroup_entry_name(name: &OsStr) -> Option<(u64, u64)> {
447 let s = name.to_str()?;
448 let (level_str, id_str) = s.split_once('_')?;
449 let level: u64 = level_str.parse().ok()?;
450 let id: u64 = id_str.parse().ok()?;
451 Some((level, id))
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use std::fs;
458 use tempfile::TempDir;
459
460 fn setup() -> (TempDir, SysfsBtrfs) {
461 let dir = TempDir::new().unwrap();
462 let sysfs = SysfsBtrfs::with_base(dir.path().to_path_buf());
463 (dir, sysfs)
464 }
465
466 #[test]
467 fn read_u64_values() {
468 let (dir, sysfs) = setup();
469 fs::write(dir.path().join("nodesize"), "16384\n").unwrap();
470 fs::write(dir.path().join("sectorsize"), "4096\n").unwrap();
471 fs::write(dir.path().join("clone_alignment"), "4096\n").unwrap();
472 fs::write(dir.path().join("generation"), "42\n").unwrap();
473 fs::write(dir.path().join("bg_reclaim_threshold"), "75\n").unwrap();
474
475 assert_eq!(sysfs.nodesize().unwrap(), 16384);
476 assert_eq!(sysfs.sectorsize().unwrap(), 4096);
477 assert_eq!(sysfs.clone_alignment().unwrap(), 4096);
478 assert_eq!(sysfs.generation().unwrap(), 42);
479 assert_eq!(sysfs.bg_reclaim_threshold().unwrap(), 75);
480 }
481
482 #[test]
483 fn read_u64_invalid() {
484 let (dir, sysfs) = setup();
485 fs::write(dir.path().join("nodesize"), "not_a_number\n").unwrap();
486 assert!(sysfs.nodesize().is_err());
487 }
488
489 #[test]
490 fn read_u64_missing_file() {
491 let (_dir, sysfs) = setup();
492 let err = sysfs.nodesize().unwrap_err();
493 assert_eq!(err.kind(), io::ErrorKind::NotFound);
494 }
495
496 #[test]
497 fn read_string_values() {
498 let (dir, sysfs) = setup();
499 fs::write(dir.path().join("label"), "my-filesystem\n").unwrap();
500 fs::write(dir.path().join("checksum"), "crc32c (crc32c-lib)\n")
501 .unwrap();
502 fs::write(dir.path().join("read_policy"), "[pid]\n").unwrap();
503 fs::write(dir.path().join("exclusive_operation"), "none\n").unwrap();
504
505 assert_eq!(sysfs.label().unwrap(), "my-filesystem");
506 assert_eq!(sysfs.checksum().unwrap(), "crc32c (crc32c-lib)");
507 assert_eq!(sysfs.read_policy().unwrap(), "[pid]");
508 assert_eq!(sysfs.exclusive_operation().unwrap(), "none");
509 }
510
511 #[test]
512 fn read_empty_label() {
513 let (dir, sysfs) = setup();
514 fs::write(dir.path().join("label"), "\n").unwrap();
515 assert_eq!(sysfs.label().unwrap(), "");
516 }
517
518 #[test]
519 fn read_bool_values() {
520 let (dir, sysfs) = setup();
521 fs::write(dir.path().join("quota_override"), "0\n").unwrap();
522 assert!(!sysfs.quota_override().unwrap());
523
524 fs::write(dir.path().join("quota_override"), "1\n").unwrap();
525 assert!(sysfs.quota_override().unwrap());
526
527 fs::write(dir.path().join("temp_fsid"), "0\n").unwrap();
528 assert!(!sysfs.temp_fsid().unwrap());
529
530 fs::write(dir.path().join("temp_fsid"), "1\n").unwrap();
531 assert!(sysfs.temp_fsid().unwrap());
532 }
533
534 #[test]
535 fn metadata_uuid() {
536 let (dir, sysfs) = setup();
537 fs::write(
538 dir.path().join("metadata_uuid"),
539 "deadbeef-dead-beef-dead-beefdeadbeef\n",
540 )
541 .unwrap();
542 let uuid = sysfs.metadata_uuid().unwrap();
543 assert_eq!(uuid.to_string(), "deadbeef-dead-beef-dead-beefdeadbeef");
544 }
545
546 #[test]
547 fn metadata_uuid_invalid() {
548 let (dir, sysfs) = setup();
549 fs::write(dir.path().join("metadata_uuid"), "not-a-uuid\n").unwrap();
550 assert!(sysfs.metadata_uuid().is_err());
551 }
552
553 #[test]
554 fn commit_stats_valid() {
555 let (dir, sysfs) = setup();
556 fs::write(
557 dir.path().join("commit_stats"),
558 "commits 100\n\
559 cur_commit_ms 5\n\
560 last_commit_ms 12\n\
561 max_commit_ms 50\n\
562 total_commit_ms 2000\n",
563 )
564 .unwrap();
565
566 let stats = sysfs.commit_stats().unwrap();
567 assert_eq!(
568 stats,
569 CommitStats {
570 commits: 100,
571 cur_commit_ms: 5,
572 last_commit_ms: 12,
573 max_commit_ms: 50,
574 total_commit_ms: 2000,
575 }
576 );
577 }
578
579 #[test]
580 fn commit_stats_missing_field() {
581 let (dir, sysfs) = setup();
582 fs::write(
583 dir.path().join("commit_stats"),
584 "commits 100\ncur_commit_ms 5\n",
585 )
586 .unwrap();
587 let err = sysfs.commit_stats().unwrap_err();
588 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
589 }
590
591 #[test]
592 fn commit_stats_extra_fields_ignored() {
593 let (dir, sysfs) = setup();
594 fs::write(
595 dir.path().join("commit_stats"),
596 "commits 1\n\
597 cur_commit_ms 2\n\
598 last_commit_ms 3\n\
599 max_commit_ms 4\n\
600 total_commit_ms 5\n\
601 unknown_field 99\n",
602 )
603 .unwrap();
604 let stats = sysfs.commit_stats().unwrap();
605 assert_eq!(stats.commits, 1);
606 }
607
608 #[test]
609 fn features_directory() {
610 let (dir, sysfs) = setup();
611 let feat_dir = dir.path().join("features");
612 fs::create_dir(&feat_dir).unwrap();
613 fs::write(feat_dir.join("skinny_metadata"), "").unwrap();
614 fs::write(feat_dir.join("extended_iref"), "").unwrap();
615 fs::write(feat_dir.join("no_holes"), "").unwrap();
616
617 let features = sysfs.features().unwrap();
618 assert_eq!(
620 features,
621 vec!["extended_iref", "no_holes", "skinny_metadata"]
622 );
623 }
624
625 #[test]
626 fn features_empty() {
627 let (dir, sysfs) = setup();
628 fs::create_dir(dir.path().join("features")).unwrap();
629 assert!(sysfs.features().unwrap().is_empty());
630 }
631
632 #[test]
633 fn scrub_speed_max_get() {
634 let (dir, sysfs) = setup();
635 let devinfo = dir.path().join("devinfo/1");
636 fs::create_dir_all(&devinfo).unwrap();
637 fs::write(devinfo.join("scrub_speed_max"), "104857600\n").unwrap();
638
639 assert_eq!(sysfs.scrub_speed_max_get(1).unwrap(), 104_857_600);
640 }
641
642 #[test]
643 fn scrub_speed_max_get_missing_returns_zero() {
644 let (_dir, sysfs) = setup();
645 assert_eq!(sysfs.scrub_speed_max_get(99).unwrap(), 0);
647 }
648
649 #[test]
650 fn scrub_speed_max_set() {
651 let (dir, sysfs) = setup();
652 let devinfo = dir.path().join("devinfo/1");
653 fs::create_dir_all(&devinfo).unwrap();
654
655 sysfs.scrub_speed_max_set(1, 500_000_000).unwrap();
656 let contents =
657 fs::read_to_string(devinfo.join("scrub_speed_max")).unwrap();
658 assert_eq!(contents, "500000000\n");
659 }
660
661 #[test]
662 fn reset_commit_stats() {
663 let (dir, sysfs) = setup();
664 fs::write(dir.path().join("commit_stats"), "old data").unwrap();
665
666 sysfs.reset_commit_stats().unwrap();
667 let contents =
668 fs::read_to_string(dir.path().join("commit_stats")).unwrap();
669 assert_eq!(contents, "0");
670 }
671
672 #[test]
673 fn quota_status_disabled() {
674 let (_dir, sysfs) = setup();
675 let status = sysfs.quota_status().unwrap();
677 assert!(!status.enabled);
678 assert!(status.mode.is_none());
679 }
680
681 #[test]
682 fn quota_status_enabled() {
683 let (dir, sysfs) = setup();
684 let qg = dir.path().join("qgroups");
685 fs::create_dir(&qg).unwrap();
686 fs::write(qg.join("mode"), "qgroup\n").unwrap();
687 fs::write(qg.join("inconsistent"), "0\n").unwrap();
688 fs::write(qg.join("drop_subtree_threshold"), "8\n").unwrap();
689 fs::write(dir.path().join("quota_override"), "0\n").unwrap();
690 fs::write(qg.join("0_5"), "").unwrap();
692 fs::write(qg.join("0_256"), "").unwrap();
693 fs::write(qg.join("1_50"), "").unwrap();
695
696 let status = sysfs.quota_status().unwrap();
697 assert!(status.enabled);
698 assert_eq!(status.mode.as_deref(), Some("qgroup"));
699 assert_eq!(status.inconsistent, Some(false));
700 assert_eq!(status.override_limits, Some(false));
701 assert_eq!(status.drop_subtree_threshold, Some(8));
702 assert_eq!(status.total_count, Some(3));
703 assert_eq!(status.level0_count, Some(2));
704 }
705
706 #[test]
707 fn parse_qgroup_entry_name_valid() {
708 assert_eq!(
709 parse_qgroup_entry_name(OsStr::new("0_256")),
710 Some((0, 256))
711 );
712 assert_eq!(parse_qgroup_entry_name(OsStr::new("1_50")), Some((1, 50)));
713 }
714
715 #[test]
716 fn parse_qgroup_entry_name_invalid() {
717 assert_eq!(parse_qgroup_entry_name(OsStr::new("mode")), None);
718 assert_eq!(parse_qgroup_entry_name(OsStr::new("inconsistent")), None);
719 assert_eq!(parse_qgroup_entry_name(OsStr::new("abc_def")), None);
720 assert_eq!(parse_qgroup_entry_name(OsStr::new("")), None);
721 }
722}
723
724#[derive(Debug, Clone, PartialEq, Eq)]
727pub struct QuotaStatus {
728 pub enabled: bool,
730 pub mode: Option<String>,
733 pub inconsistent: Option<bool>,
736 pub override_limits: Option<bool>,
739 pub drop_subtree_threshold: Option<u64>,
742 pub total_count: Option<u64>,
744 pub level0_count: Option<u64>,
746}