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> {
322 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
323 args.assign = 1;
324 args.src = src;
325 args.dst = dst;
326 let ret = unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &args) }?;
329 Ok(ret > 0)
330}
331
332pub fn qgroup_remove(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
339 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
340 args.assign = 0;
341 args.src = src;
342 args.dst = dst;
343 let ret = unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &args) }?;
346 Ok(ret > 0)
347}
348
349pub fn qgroup_limit(
355 fd: BorrowedFd,
356 qgroupid: u64,
357 flags: QgroupLimitFlags,
358 max_rfer: u64,
359 max_excl: u64,
360) -> nix::Result<()> {
361 let lim = btrfs_qgroup_limit {
362 flags: flags.bits(),
363 max_referenced: max_rfer,
364 max_exclusive: max_excl,
365 rsv_referenced: 0,
366 rsv_exclusive: 0,
367 };
368 let mut args: btrfs_ioctl_qgroup_limit_args = unsafe { mem::zeroed() };
369 args.qgroupid = qgroupid;
370 args.lim = lim;
371 unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &mut args) }?;
375 Ok(())
376}
377
378pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
384 let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
386 let mut status_flags = QgroupStatusFlags::empty();
387
388 let quota_key = SearchKey {
390 tree_id: BTRFS_QUOTA_TREE_OBJECTID as u64,
391 min_objectid: 0,
392 max_objectid: u64::MAX,
393 min_type: BTRFS_QGROUP_STATUS_KEY,
394 max_type: BTRFS_QGROUP_RELATION_KEY,
395 min_offset: 0,
396 max_offset: u64::MAX,
397 min_transid: 0,
398 max_transid: u64::MAX,
399 };
400
401 let scan_result = tree_search(fd, quota_key, |hdr, data| {
402 match hdr.item_type {
403 t if t == BTRFS_QGROUP_STATUS_KEY => {
404 if let Some(raw) = parse_status_flags(data) {
405 status_flags = QgroupStatusFlags::from_bits_truncate(raw);
406 }
407 }
408 t if t == BTRFS_QGROUP_INFO_KEY => {
409 let entry = builders.entry(hdr.offset).or_default();
411 parse_info(entry, data);
412 }
413 t if t == BTRFS_QGROUP_LIMIT_KEY => {
414 let entry = builders.entry(hdr.offset).or_default();
416 parse_limit(entry, data);
417 }
418 t if t == BTRFS_QGROUP_RELATION_KEY => {
419 if hdr.objectid > hdr.offset {
425 let parent = hdr.objectid;
426 let child = hdr.offset;
427 builders.entry(child).or_default().parents.push(parent);
428 builders.entry(parent).or_default().children.push(child);
429 }
430 }
431 _ => {}
432 }
433 Ok(())
434 });
435
436 match scan_result {
437 Err(Errno::ENOENT) => {
438 return Ok(QgroupList {
440 status_flags: QgroupStatusFlags::empty(),
441 qgroups: Vec::new(),
442 });
443 }
444 Err(e) => return Err(e),
445 Ok(()) => {}
446 }
447
448 let existing_subvol_ids = collect_subvol_ids(fd)?;
450
451 let mut qgroups: Vec<QgroupInfo> = builders
453 .into_iter()
454 .map(|(qgroupid, builder)| {
455 let stale = if qgroupid_level(qgroupid) == 0 {
456 !existing_subvol_ids.contains(&qgroupid_subvolid(qgroupid))
457 } else {
458 false
459 };
460 builder.build(qgroupid, stale)
461 })
462 .collect();
463
464 qgroups.sort_by_key(|q| q.qgroupid);
465
466 Ok(QgroupList {
467 status_flags,
468 qgroups,
469 })
470}
471
472fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
475 let mut ids: HashSet<u64> = HashSet::new();
476
477 let key = SearchKey::for_objectid_range(
480 BTRFS_ROOT_TREE_OBJECTID as u64,
481 BTRFS_ROOT_ITEM_KEY,
482 BTRFS_FIRST_FREE_OBJECTID as u64,
483 BTRFS_LAST_FREE_OBJECTID as u64,
484 );
485
486 tree_search(fd, key, |hdr, _data| {
487 ids.insert(hdr.objectid);
488 Ok(())
489 })?;
490
491 Ok(ids)
492}
493
494pub fn qgroup_clear_stale(fd: BorrowedFd) -> nix::Result<usize> {
503 let list = qgroup_list(fd)?;
504 let simple_mode =
505 list.status_flags.contains(QgroupStatusFlags::SIMPLE_MODE);
506
507 let mut count = 0usize;
508
509 for qg in &list.qgroups {
510 if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
512 continue;
513 }
514
515 if simple_mode && (qg.rfer != 0 || qg.excl != 0) {
517 continue;
518 }
519
520 if qgroup_destroy(fd, qg.qgroupid).is_ok() {
521 count += 1;
522 }
523 }
524
525 Ok(count)
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn qgroupid_level_zero() {
534 assert_eq!(qgroupid_level(5), 0);
535 assert_eq!(qgroupid_level(256), 0);
536 }
537
538 #[test]
539 fn qgroupid_level_nonzero() {
540 let id = (1u64 << 48) | 100;
541 assert_eq!(qgroupid_level(id), 1);
542
543 let id = (3u64 << 48) | 42;
544 assert_eq!(qgroupid_level(id), 3);
545 }
546
547 #[test]
548 fn qgroupid_subvolid_extracts_lower_48_bits() {
549 assert_eq!(qgroupid_subvolid(256), 256);
550 assert_eq!(qgroupid_subvolid((1u64 << 48) | 100), 100);
551 assert_eq!(qgroupid_subvolid((2u64 << 48) | 0), 0);
552 }
553
554 #[test]
555 fn qgroupid_roundtrip() {
556 let level: u64 = 2;
557 let subvolid: u64 = 999;
558 let packed = (level << 48) | subvolid;
559 assert_eq!(qgroupid_level(packed), level as u16);
560 assert_eq!(qgroupid_subvolid(packed), subvolid);
561 }
562}