1use crate::{
18 raw::{
19 BTRFS_FIRST_FREE_OBJECTID, BTRFS_LAST_FREE_OBJECTID,
20 BTRFS_QGROUP_INFO_KEY, BTRFS_QGROUP_LIMIT_EXCL_CMPR,
21 BTRFS_QGROUP_LIMIT_KEY, BTRFS_QGROUP_LIMIT_MAX_EXCL,
22 BTRFS_QGROUP_LIMIT_MAX_RFER, BTRFS_QGROUP_LIMIT_RFER_CMPR,
23 BTRFS_QGROUP_RELATION_KEY, BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT,
24 BTRFS_QGROUP_STATUS_FLAG_ON, BTRFS_QGROUP_STATUS_FLAG_RESCAN,
25 BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE, BTRFS_QGROUP_STATUS_KEY,
26 BTRFS_QUOTA_CTL_DISABLE, BTRFS_QUOTA_CTL_ENABLE,
27 BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA, BTRFS_QUOTA_TREE_OBJECTID,
28 BTRFS_ROOT_ITEM_KEY, BTRFS_ROOT_TREE_OBJECTID, btrfs_ioc_qgroup_assign,
29 btrfs_ioc_qgroup_create, btrfs_ioc_qgroup_limit, btrfs_ioc_quota_ctl,
30 btrfs_ioc_quota_rescan, btrfs_ioc_quota_rescan_status,
31 btrfs_ioc_quota_rescan_wait, btrfs_ioctl_qgroup_assign_args,
32 btrfs_ioctl_qgroup_create_args, btrfs_ioctl_qgroup_limit_args,
33 btrfs_ioctl_quota_ctl_args, btrfs_ioctl_quota_rescan_args,
34 btrfs_qgroup_limit,
35 },
36 tree_search::{Key, SearchFilter, tree_search},
37};
38use bitflags::bitflags;
39use nix::errno::Errno;
40use std::{
41 collections::{HashMap, HashSet},
42 mem,
43 os::{fd::AsRawFd, unix::io::BorrowedFd},
44};
45
46pub fn quota_enable(fd: BorrowedFd, simple: bool) -> nix::Result<()> {
56 let cmd = if simple {
57 u64::from(BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA)
58 } else {
59 u64::from(BTRFS_QUOTA_CTL_ENABLE)
60 };
61 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
62 args.cmd = cmd;
63 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
64 Ok(())
65}
66
67pub fn quota_disable(fd: BorrowedFd) -> nix::Result<()> {
73 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
74 args.cmd = u64::from(BTRFS_QUOTA_CTL_DISABLE);
75 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
76 Ok(())
77}
78
79pub fn quota_rescan(fd: BorrowedFd) -> nix::Result<()> {
91 let args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
92 unsafe { btrfs_ioc_quota_rescan(fd.as_raw_fd(), &raw const args) }?;
93 Ok(())
94}
95
96pub fn quota_rescan_wait(fd: BorrowedFd) -> nix::Result<()> {
103 unsafe { btrfs_ioc_quota_rescan_wait(fd.as_raw_fd()) }?;
104 Ok(())
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct QuotaRescanStatus {
110 pub running: bool,
112 pub progress: u64,
115}
116
117pub fn quota_rescan_status(fd: BorrowedFd) -> nix::Result<QuotaRescanStatus> {
123 let mut args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
124 unsafe { btrfs_ioc_quota_rescan_status(fd.as_raw_fd(), &raw mut args) }?;
125 Ok(QuotaRescanStatus {
126 running: args.flags != 0,
127 progress: args.progress,
128 })
129}
130
131#[inline]
136#[must_use]
137pub fn qgroupid_level(qgroupid: u64) -> u16 {
138 (qgroupid >> 48) as u16
139}
140
141#[inline]
145#[must_use]
146pub fn qgroupid_subvolid(qgroupid: u64) -> u64 {
147 qgroupid & 0x0000_FFFF_FFFF_FFFF
148}
149
150bitflags! {
151 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
153 pub struct QgroupStatusFlags: u64 {
154 const ON = BTRFS_QGROUP_STATUS_FLAG_ON as u64;
156 const RESCAN = BTRFS_QGROUP_STATUS_FLAG_RESCAN as u64;
158 const INCONSISTENT = BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT as u64;
160 const SIMPLE_MODE = BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE as u64;
162 }
163}
164
165bitflags! {
166 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
168 pub struct QgroupLimitFlags: u64 {
169 const MAX_RFER = BTRFS_QGROUP_LIMIT_MAX_RFER as u64;
171 const MAX_EXCL = BTRFS_QGROUP_LIMIT_MAX_EXCL as u64;
173 const RFER_CMPR = BTRFS_QGROUP_LIMIT_RFER_CMPR as u64;
175 const EXCL_CMPR = BTRFS_QGROUP_LIMIT_EXCL_CMPR as u64;
177 }
178}
179
180#[derive(Debug, Clone)]
182pub struct QgroupInfo {
183 pub qgroupid: u64,
185 pub rfer: u64,
187 pub rfer_cmpr: u64,
189 pub excl: u64,
191 pub excl_cmpr: u64,
193 pub limit_flags: QgroupLimitFlags,
195 pub max_rfer: u64,
197 pub max_excl: u64,
199 pub parents: Vec<u64>,
201 pub children: Vec<u64>,
203 pub stale: bool,
206}
207
208#[derive(Debug, Clone)]
210pub struct QgroupList {
211 pub status_flags: QgroupStatusFlags,
213 pub qgroups: Vec<QgroupInfo>,
215}
216
217#[derive(Default)]
218struct QgroupEntryBuilder {
219 has_info: bool,
221 rfer: u64,
222 rfer_cmpr: u64,
223 excl: u64,
224 excl_cmpr: u64,
225 has_limit: bool,
227 limit_flags: u64,
228 max_rfer: u64,
229 max_excl: u64,
230 parents: Vec<u64>,
232 children: Vec<u64>,
233}
234
235impl QgroupEntryBuilder {
236 fn build(self, qgroupid: u64, stale: bool) -> QgroupInfo {
237 QgroupInfo {
238 qgroupid,
239 rfer: self.rfer,
240 rfer_cmpr: self.rfer_cmpr,
241 excl: self.excl,
242 excl_cmpr: self.excl_cmpr,
243 limit_flags: QgroupLimitFlags::from_bits_truncate(self.limit_flags),
244 max_rfer: if self.limit_flags
245 & u64::from(BTRFS_QGROUP_LIMIT_MAX_RFER)
246 != 0
247 {
248 self.max_rfer
249 } else {
250 u64::MAX
251 },
252 max_excl: if self.limit_flags
253 & u64::from(BTRFS_QGROUP_LIMIT_MAX_EXCL)
254 != 0
255 {
256 self.max_excl
257 } else {
258 u64::MAX
259 },
260 parents: self.parents,
261 children: self.children,
262 stale,
263 }
264 }
265}
266
267fn parse_status_flags(data: &[u8]) -> Option<u64> {
268 btrfs_disk::items::QgroupStatus::parse(data).map(|qs| qs.flags)
269}
270
271fn parse_info(builder: &mut QgroupEntryBuilder, data: &[u8]) {
272 let Some(qi) = btrfs_disk::items::QgroupInfo::parse(data) else {
273 return;
274 };
275 builder.has_info = true;
276 builder.rfer = qi.referenced;
277 builder.rfer_cmpr = qi.referenced_compressed;
278 builder.excl = qi.exclusive;
279 builder.excl_cmpr = qi.exclusive_compressed;
280}
281
282fn parse_limit(builder: &mut QgroupEntryBuilder, data: &[u8]) {
283 let Some(ql) = btrfs_disk::items::QgroupLimit::parse(data) else {
284 return;
285 };
286 builder.has_limit = true;
287 builder.limit_flags = ql.flags;
288 builder.max_rfer = ql.max_referenced;
289 builder.max_excl = ql.max_exclusive;
290}
291
292pub fn qgroup_create(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
301 let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
302 args.create = 1;
303 args.qgroupid = qgroupid;
304 unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
307 Ok(())
308}
309
310pub fn qgroup_destroy(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
317 let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
318 args.create = 0;
319 args.qgroupid = qgroupid;
320 unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
323 Ok(())
324}
325
326pub fn qgroup_assign(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
339 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
340 args.assign = 1;
341 args.src = src;
342 args.dst = dst;
343 let ret =
346 unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
347 Ok(ret > 0)
348}
349
350pub fn qgroup_remove(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
361 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
362 args.assign = 0;
363 args.src = src;
364 args.dst = dst;
365 let ret =
368 unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
369 Ok(ret > 0)
370}
371
372pub fn qgroup_limit(
382 fd: BorrowedFd,
383 qgroupid: u64,
384 flags: QgroupLimitFlags,
385 max_rfer: u64,
386 max_excl: u64,
387) -> nix::Result<()> {
388 let lim = btrfs_qgroup_limit {
389 flags: flags.bits(),
390 max_referenced: max_rfer,
391 max_exclusive: max_excl,
392 rsv_referenced: 0,
393 rsv_exclusive: 0,
394 };
395 let mut args: btrfs_ioctl_qgroup_limit_args = unsafe { mem::zeroed() };
396 args.qgroupid = qgroupid;
397 args.lim = lim;
398 unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &raw mut args) }?;
402 Ok(())
403}
404
405pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
415 let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
417 let mut status_flags = QgroupStatusFlags::empty();
418
419 let quota_key = SearchFilter {
421 tree_id: u64::from(BTRFS_QUOTA_TREE_OBJECTID),
422 start: Key {
423 objectid: 0,
424 item_type: BTRFS_QGROUP_STATUS_KEY,
425 offset: 0,
426 },
427 end: Key {
428 objectid: u64::MAX,
429 item_type: BTRFS_QGROUP_RELATION_KEY,
430 offset: u64::MAX,
431 },
432 min_transid: 0,
433 max_transid: u64::MAX,
434 };
435
436 let scan_result = tree_search(fd, quota_key, |hdr, data| {
437 match hdr.item_type {
438 t if t == BTRFS_QGROUP_STATUS_KEY => {
439 if let Some(raw) = parse_status_flags(data) {
440 status_flags = QgroupStatusFlags::from_bits_truncate(raw);
441 }
442 }
443 t if t == BTRFS_QGROUP_INFO_KEY => {
444 let entry = builders.entry(hdr.offset).or_default();
446 parse_info(entry, data);
447 }
448 t if t == BTRFS_QGROUP_LIMIT_KEY => {
449 let entry = builders.entry(hdr.offset).or_default();
451 parse_limit(entry, data);
452 }
453 t if t == BTRFS_QGROUP_RELATION_KEY
454 && hdr.objectid > hdr.offset =>
460 {
461 let parent = hdr.objectid;
462 let child = hdr.offset;
463 builders.entry(child).or_default().parents.push(parent);
464 builders.entry(parent).or_default().children.push(child);
465 }
466 _ => {}
467 }
468 Ok(())
469 });
470
471 match scan_result {
472 Err(Errno::ENOENT) => {
473 return Ok(QgroupList {
475 status_flags: QgroupStatusFlags::empty(),
476 qgroups: Vec::new(),
477 });
478 }
479 Err(e) => return Err(e),
480 Ok(()) => {}
481 }
482
483 let existing_subvol_ids = collect_subvol_ids(fd)?;
485
486 let mut qgroups: Vec<QgroupInfo> = builders
488 .into_iter()
489 .map(|(qgroupid, builder)| {
490 let stale = if qgroupid_level(qgroupid) == 0 {
491 !existing_subvol_ids.contains(&qgroupid_subvolid(qgroupid))
492 } else {
493 false
494 };
495 builder.build(qgroupid, stale)
496 })
497 .collect();
498
499 qgroups.sort_by_key(|q| q.qgroupid);
500
501 Ok(QgroupList {
502 status_flags,
503 qgroups,
504 })
505}
506
507fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
510 let mut ids: HashSet<u64> = HashSet::new();
511
512 #[allow(clippy::cast_sign_loss)]
515 let key = SearchFilter::for_objectid_range(
516 u64::from(BTRFS_ROOT_TREE_OBJECTID),
517 BTRFS_ROOT_ITEM_KEY,
518 u64::from(BTRFS_FIRST_FREE_OBJECTID),
519 BTRFS_LAST_FREE_OBJECTID as u64,
520 );
521
522 tree_search(fd, key, |hdr, _data| {
523 ids.insert(hdr.objectid);
524 Ok(())
525 })?;
526
527 Ok(ids)
528}
529
530pub fn qgroup_clear_stale(fd: BorrowedFd) -> nix::Result<usize> {
543 let list = qgroup_list(fd)?;
544 let simple_mode =
545 list.status_flags.contains(QgroupStatusFlags::SIMPLE_MODE);
546
547 let mut count = 0usize;
548
549 for qg in &list.qgroups {
550 if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
552 continue;
553 }
554
555 if simple_mode && (qg.rfer != 0 || qg.excl != 0) {
557 continue;
558 }
559
560 if qgroup_destroy(fd, qg.qgroupid).is_ok() {
561 count += 1;
562 }
563 }
564
565 Ok(count)
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn qgroupid_level_zero() {
574 assert_eq!(qgroupid_level(5), 0);
575 assert_eq!(qgroupid_level(256), 0);
576 }
577
578 #[test]
579 fn qgroupid_level_nonzero() {
580 let id = (1u64 << 48) | 100;
581 assert_eq!(qgroupid_level(id), 1);
582
583 let id = (3u64 << 48) | 42;
584 assert_eq!(qgroupid_level(id), 3);
585 }
586
587 #[test]
588 fn qgroupid_subvolid_extracts_lower_48_bits() {
589 assert_eq!(qgroupid_subvolid(256), 256);
590 assert_eq!(qgroupid_subvolid((1u64 << 48) | 100), 100);
591 assert_eq!(qgroupid_subvolid((2u64 << 48) | 0), 0);
592 }
593
594 #[test]
595 fn qgroupid_roundtrip() {
596 let level: u64 = 2;
597 let subvolid: u64 = 999;
598 let packed = (level << 48) | subvolid;
599 assert_eq!(qgroupid_level(packed), level as u16);
600 assert_eq!(qgroupid_subvolid(packed), subvolid);
601 }
602}