1use crate::{
18 field_size,
19 raw::{
20 BTRFS_FIRST_FREE_OBJECTID, BTRFS_LAST_FREE_OBJECTID,
21 BTRFS_QGROUP_INFO_KEY, BTRFS_QGROUP_LIMIT_EXCL_CMPR,
22 BTRFS_QGROUP_LIMIT_KEY, BTRFS_QGROUP_LIMIT_MAX_EXCL,
23 BTRFS_QGROUP_LIMIT_MAX_RFER, BTRFS_QGROUP_LIMIT_RFER_CMPR,
24 BTRFS_QGROUP_RELATION_KEY, BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT,
25 BTRFS_QGROUP_STATUS_FLAG_ON, BTRFS_QGROUP_STATUS_FLAG_RESCAN,
26 BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE, BTRFS_QGROUP_STATUS_KEY,
27 BTRFS_QUOTA_CTL_DISABLE, BTRFS_QUOTA_CTL_ENABLE,
28 BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA, BTRFS_QUOTA_TREE_OBJECTID,
29 BTRFS_ROOT_ITEM_KEY, BTRFS_ROOT_TREE_OBJECTID, btrfs_ioc_qgroup_assign,
30 btrfs_ioc_qgroup_create, btrfs_ioc_qgroup_limit, btrfs_ioc_quota_ctl,
31 btrfs_ioc_quota_rescan, btrfs_ioc_quota_rescan_status,
32 btrfs_ioc_quota_rescan_wait, btrfs_ioctl_qgroup_assign_args,
33 btrfs_ioctl_qgroup_create_args, btrfs_ioctl_qgroup_limit_args,
34 btrfs_ioctl_quota_ctl_args, btrfs_ioctl_quota_rescan_args,
35 btrfs_qgroup_info_item, btrfs_qgroup_limit, btrfs_qgroup_limit_item,
36 btrfs_qgroup_status_item,
37 },
38 tree_search::{SearchKey, tree_search},
39};
40use bitflags::bitflags;
41use nix::errno::Errno;
42use std::{
43 collections::{HashMap, HashSet},
44 mem::{self, offset_of, size_of},
45 os::{fd::AsRawFd, unix::io::BorrowedFd},
46};
47
48pub fn quota_enable(fd: BorrowedFd, simple: bool) -> nix::Result<()> {
54 let cmd = if simple {
55 BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA as u64
56 } else {
57 BTRFS_QUOTA_CTL_ENABLE as u64
58 };
59 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
60 args.cmd = cmd;
61 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &mut args) }?;
62 Ok(())
63}
64
65pub fn quota_disable(fd: BorrowedFd) -> nix::Result<()> {
67 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
68 args.cmd = BTRFS_QUOTA_CTL_DISABLE as u64;
69 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &mut args) }?;
70 Ok(())
71}
72
73pub fn quota_rescan(fd: BorrowedFd) -> nix::Result<()> {
80 let args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
81 unsafe { btrfs_ioc_quota_rescan(fd.as_raw_fd(), &args) }?;
82 Ok(())
83}
84
85pub fn quota_rescan_wait(fd: BorrowedFd) -> nix::Result<()> {
88 unsafe { btrfs_ioc_quota_rescan_wait(fd.as_raw_fd()) }?;
89 Ok(())
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct QuotaRescanStatus {
95 pub running: bool,
97 pub progress: u64,
100}
101
102pub fn quota_rescan_status(fd: BorrowedFd) -> nix::Result<QuotaRescanStatus> {
104 let mut args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
105 unsafe { btrfs_ioc_quota_rescan_status(fd.as_raw_fd(), &mut args) }?;
106 Ok(QuotaRescanStatus {
107 running: args.flags != 0,
108 progress: args.progress,
109 })
110}
111
112#[inline]
117pub fn qgroupid_level(qgroupid: u64) -> u16 {
118 (qgroupid >> 48) as u16
119}
120
121#[inline]
125pub fn qgroupid_subvolid(qgroupid: u64) -> u64 {
126 qgroupid & 0x0000_FFFF_FFFF_FFFF
127}
128
129bitflags! {
130 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
132 pub struct QgroupStatusFlags: u64 {
133 const ON = BTRFS_QGROUP_STATUS_FLAG_ON as u64;
135 const RESCAN = BTRFS_QGROUP_STATUS_FLAG_RESCAN as u64;
137 const INCONSISTENT = BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT as u64;
139 const SIMPLE_MODE = BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE as u64;
141 }
142}
143
144bitflags! {
145 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
147 pub struct QgroupLimitFlags: u64 {
148 const MAX_RFER = BTRFS_QGROUP_LIMIT_MAX_RFER as u64;
150 const MAX_EXCL = BTRFS_QGROUP_LIMIT_MAX_EXCL as u64;
152 const RFER_CMPR = BTRFS_QGROUP_LIMIT_RFER_CMPR as u64;
154 const EXCL_CMPR = BTRFS_QGROUP_LIMIT_EXCL_CMPR as u64;
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct QgroupInfo {
162 pub qgroupid: u64,
164 pub rfer: u64,
166 pub rfer_cmpr: u64,
168 pub excl: u64,
170 pub excl_cmpr: u64,
172 pub limit_flags: QgroupLimitFlags,
174 pub max_rfer: u64,
176 pub max_excl: u64,
178 pub parents: Vec<u64>,
180 pub children: Vec<u64>,
182 pub stale: bool,
185}
186
187#[derive(Debug, Clone)]
189pub struct QgroupList {
190 pub status_flags: QgroupStatusFlags,
192 pub qgroups: Vec<QgroupInfo>,
194}
195
196#[derive(Default)]
197struct QgroupEntryBuilder {
198 has_info: bool,
200 rfer: u64,
201 rfer_cmpr: u64,
202 excl: u64,
203 excl_cmpr: u64,
204 has_limit: bool,
206 limit_flags: u64,
207 max_rfer: u64,
208 max_excl: u64,
209 parents: Vec<u64>,
211 children: Vec<u64>,
212}
213
214impl QgroupEntryBuilder {
215 fn build(self, qgroupid: u64, stale: bool) -> QgroupInfo {
216 QgroupInfo {
217 qgroupid,
218 rfer: self.rfer,
219 rfer_cmpr: self.rfer_cmpr,
220 excl: self.excl,
221 excl_cmpr: self.excl_cmpr,
222 limit_flags: QgroupLimitFlags::from_bits_truncate(self.limit_flags),
223 max_rfer: if self.limit_flags & BTRFS_QGROUP_LIMIT_MAX_RFER as u64
224 != 0
225 {
226 self.max_rfer
227 } else {
228 u64::MAX
229 },
230 max_excl: if self.limit_flags & BTRFS_QGROUP_LIMIT_MAX_EXCL as u64
231 != 0
232 {
233 self.max_excl
234 } else {
235 u64::MAX
236 },
237 parents: self.parents,
238 children: self.children,
239 stale,
240 }
241 }
242}
243
244#[inline]
245fn rle64(buf: &[u8], off: usize) -> u64 {
246 u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
247}
248
249fn parse_status_flags(data: &[u8]) -> Option<u64> {
250 let off = offset_of!(btrfs_qgroup_status_item, flags);
251 if data.len() < off + field_size!(btrfs_qgroup_status_item, flags) {
252 return None;
253 }
254 Some(rle64(data, off))
255}
256
257fn parse_info(builder: &mut QgroupEntryBuilder, data: &[u8]) {
258 if data.len() < size_of::<btrfs_qgroup_info_item>() {
259 return;
260 }
261
262 builder.has_info = true;
263 builder.rfer = rle64(data, offset_of!(btrfs_qgroup_info_item, rfer));
264 builder.rfer_cmpr =
265 rle64(data, offset_of!(btrfs_qgroup_info_item, rfer_cmpr));
266 builder.excl = rle64(data, offset_of!(btrfs_qgroup_info_item, excl));
267 builder.excl_cmpr =
268 rle64(data, offset_of!(btrfs_qgroup_info_item, excl_cmpr));
269}
270
271fn parse_limit(builder: &mut QgroupEntryBuilder, data: &[u8]) {
272 let end = offset_of!(btrfs_qgroup_limit_item, max_excl)
273 + field_size!(btrfs_qgroup_limit_item, max_excl);
274 if data.len() < end {
275 return;
276 }
277
278 builder.has_limit = true;
279 builder.limit_flags =
280 rle64(data, offset_of!(btrfs_qgroup_limit_item, flags));
281 builder.max_rfer =
282 rle64(data, offset_of!(btrfs_qgroup_limit_item, max_rfer));
283 builder.max_excl =
284 rle64(data, offset_of!(btrfs_qgroup_limit_item, max_excl));
285}
286
287pub fn qgroup_create(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
292 let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
293 args.create = 1;
294 args.qgroupid = qgroupid;
295 unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &args) }?;
298 Ok(())
299}
300
301pub fn qgroup_destroy(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
304 let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
305 args.create = 0;
306 args.qgroupid = qgroupid;
307 unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &args) }?;
310 Ok(())
311}
312
313pub fn qgroup_assign(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
319 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
320 args.assign = 1;
321 args.src = src;
322 args.dst = dst;
323 let ret = unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &args) }?;
326 Ok(ret > 0)
327}
328
329pub fn qgroup_remove(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
333 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
334 args.assign = 0;
335 args.src = src;
336 args.dst = dst;
337 let ret = unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &args) }?;
340 Ok(ret > 0)
341}
342
343pub fn qgroup_limit(
349 fd: BorrowedFd,
350 qgroupid: u64,
351 flags: QgroupLimitFlags,
352 max_rfer: u64,
353 max_excl: u64,
354) -> nix::Result<()> {
355 let lim = btrfs_qgroup_limit {
356 flags: flags.bits(),
357 max_referenced: max_rfer,
358 max_exclusive: max_excl,
359 rsv_referenced: 0,
360 rsv_exclusive: 0,
361 };
362 let mut args: btrfs_ioctl_qgroup_limit_args = unsafe { mem::zeroed() };
363 args.qgroupid = qgroupid;
364 args.lim = lim;
365 unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &mut args) }?;
369 Ok(())
370}
371
372pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
378 let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
380 let mut status_flags = QgroupStatusFlags::empty();
381
382 let quota_key = SearchKey {
384 tree_id: BTRFS_QUOTA_TREE_OBJECTID as u64,
385 min_objectid: 0,
386 max_objectid: u64::MAX,
387 min_type: BTRFS_QGROUP_STATUS_KEY as u32,
388 max_type: BTRFS_QGROUP_RELATION_KEY as u32,
389 min_offset: 0,
390 max_offset: u64::MAX,
391 min_transid: 0,
392 max_transid: u64::MAX,
393 };
394
395 let scan_result = tree_search(fd, quota_key, |hdr, data| {
396 match hdr.item_type as u32 {
397 t if t == BTRFS_QGROUP_STATUS_KEY as u32 => {
398 if let Some(raw) = parse_status_flags(data) {
399 status_flags = QgroupStatusFlags::from_bits_truncate(raw);
400 }
401 }
402 t if t == BTRFS_QGROUP_INFO_KEY as u32 => {
403 let entry = builders.entry(hdr.offset).or_default();
405 parse_info(entry, data);
406 }
407 t if t == BTRFS_QGROUP_LIMIT_KEY as u32 => {
408 let entry = builders.entry(hdr.offset).or_default();
410 parse_limit(entry, data);
411 }
412 t if t == BTRFS_QGROUP_RELATION_KEY as u32 => {
413 if hdr.objectid > hdr.offset {
419 let parent = hdr.objectid;
420 let child = hdr.offset;
421 builders.entry(child).or_default().parents.push(parent);
422 builders.entry(parent).or_default().children.push(child);
423 }
424 }
425 _ => {}
426 }
427 Ok(())
428 });
429
430 match scan_result {
431 Err(Errno::ENOENT) => {
432 return Ok(QgroupList {
434 status_flags: QgroupStatusFlags::empty(),
435 qgroups: Vec::new(),
436 });
437 }
438 Err(e) => return Err(e),
439 Ok(()) => {}
440 }
441
442 let existing_subvol_ids = collect_subvol_ids(fd)?;
444
445 let mut qgroups: Vec<QgroupInfo> = builders
447 .into_iter()
448 .map(|(qgroupid, builder)| {
449 let stale = if qgroupid_level(qgroupid) == 0 {
450 !existing_subvol_ids.contains(&qgroupid_subvolid(qgroupid))
451 } else {
452 false
453 };
454 builder.build(qgroupid, stale)
455 })
456 .collect();
457
458 qgroups.sort_by_key(|q| q.qgroupid);
459
460 Ok(QgroupList {
461 status_flags,
462 qgroups,
463 })
464}
465
466fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
469 let mut ids: HashSet<u64> = HashSet::new();
470
471 let key = SearchKey::for_objectid_range(
474 BTRFS_ROOT_TREE_OBJECTID as u64,
475 BTRFS_ROOT_ITEM_KEY as u32,
476 BTRFS_FIRST_FREE_OBJECTID as u64,
477 BTRFS_LAST_FREE_OBJECTID as u64,
478 );
479
480 tree_search(fd, key, |hdr, _data| {
481 ids.insert(hdr.objectid);
482 Ok(())
483 })?;
484
485 Ok(ids)
486}
487
488pub fn qgroup_clear_stale(fd: BorrowedFd) -> nix::Result<usize> {
497 let list = qgroup_list(fd)?;
498 let simple_mode =
499 list.status_flags.contains(QgroupStatusFlags::SIMPLE_MODE);
500
501 let mut count = 0usize;
502
503 for qg in &list.qgroups {
504 if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
506 continue;
507 }
508
509 if simple_mode && (qg.rfer != 0 || qg.excl != 0) {
511 continue;
512 }
513
514 if qgroup_destroy(fd, qg.qgroupid).is_ok() {
515 count += 1;
516 }
517 }
518
519 Ok(count)
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 #[test]
527 fn qgroupid_level_zero() {
528 assert_eq!(qgroupid_level(5), 0);
529 assert_eq!(qgroupid_level(256), 0);
530 }
531
532 #[test]
533 fn qgroupid_level_nonzero() {
534 let id = (1u64 << 48) | 100;
535 assert_eq!(qgroupid_level(id), 1);
536
537 let id = (3u64 << 48) | 42;
538 assert_eq!(qgroupid_level(id), 3);
539 }
540
541 #[test]
542 fn qgroupid_subvolid_extracts_lower_48_bits() {
543 assert_eq!(qgroupid_subvolid(256), 256);
544 assert_eq!(qgroupid_subvolid((1u64 << 48) | 100), 100);
545 assert_eq!(qgroupid_subvolid((2u64 << 48) | 0), 0);
546 }
547
548 #[test]
549 fn qgroupid_roundtrip() {
550 let level: u64 = 2;
551 let subvolid: u64 = 999;
552 let packed = (level << 48) | subvolid;
553 assert_eq!(qgroupid_level(packed), level as u16);
554 assert_eq!(qgroupid_subvolid(packed), subvolid);
555 }
556}