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 util::read_le_u64,
40};
41use bitflags::bitflags;
42use nix::errno::Errno;
43use std::{
44 collections::{HashMap, HashSet},
45 mem::{self, offset_of, size_of},
46 os::{fd::AsRawFd, unix::io::BorrowedFd},
47};
48
49pub fn quota_enable(fd: BorrowedFd, simple: bool) -> nix::Result<()> {
55 let cmd = if simple {
56 u64::from(BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA)
57 } else {
58 u64::from(BTRFS_QUOTA_CTL_ENABLE)
59 };
60 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
61 args.cmd = cmd;
62 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
63 Ok(())
64}
65
66pub fn quota_disable(fd: BorrowedFd) -> nix::Result<()> {
68 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
69 args.cmd = u64::from(BTRFS_QUOTA_CTL_DISABLE);
70 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
71 Ok(())
72}
73
74pub fn quota_rescan(fd: BorrowedFd) -> nix::Result<()> {
81 let args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
82 unsafe { btrfs_ioc_quota_rescan(fd.as_raw_fd(), &raw const args) }?;
83 Ok(())
84}
85
86pub fn quota_rescan_wait(fd: BorrowedFd) -> nix::Result<()> {
89 unsafe { btrfs_ioc_quota_rescan_wait(fd.as_raw_fd()) }?;
90 Ok(())
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct QuotaRescanStatus {
96 pub running: bool,
98 pub progress: u64,
101}
102
103pub fn quota_rescan_status(fd: BorrowedFd) -> nix::Result<QuotaRescanStatus> {
105 let mut args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
106 unsafe { btrfs_ioc_quota_rescan_status(fd.as_raw_fd(), &raw mut args) }?;
107 Ok(QuotaRescanStatus {
108 running: args.flags != 0,
109 progress: args.progress,
110 })
111}
112
113#[inline]
118#[must_use]
119pub fn qgroupid_level(qgroupid: u64) -> u16 {
120 (qgroupid >> 48) as u16
121}
122
123#[inline]
127#[must_use]
128pub fn qgroupid_subvolid(qgroupid: u64) -> u64 {
129 qgroupid & 0x0000_FFFF_FFFF_FFFF
130}
131
132bitflags! {
133 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
135 pub struct QgroupStatusFlags: u64 {
136 const ON = BTRFS_QGROUP_STATUS_FLAG_ON as u64;
138 const RESCAN = BTRFS_QGROUP_STATUS_FLAG_RESCAN as u64;
140 const INCONSISTENT = BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT as u64;
142 const SIMPLE_MODE = BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE as u64;
144 }
145}
146
147bitflags! {
148 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
150 pub struct QgroupLimitFlags: u64 {
151 const MAX_RFER = BTRFS_QGROUP_LIMIT_MAX_RFER as u64;
153 const MAX_EXCL = BTRFS_QGROUP_LIMIT_MAX_EXCL as u64;
155 const RFER_CMPR = BTRFS_QGROUP_LIMIT_RFER_CMPR as u64;
157 const EXCL_CMPR = BTRFS_QGROUP_LIMIT_EXCL_CMPR as u64;
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct QgroupInfo {
165 pub qgroupid: u64,
167 pub rfer: u64,
169 pub rfer_cmpr: u64,
171 pub excl: u64,
173 pub excl_cmpr: u64,
175 pub limit_flags: QgroupLimitFlags,
177 pub max_rfer: u64,
179 pub max_excl: u64,
181 pub parents: Vec<u64>,
183 pub children: Vec<u64>,
185 pub stale: bool,
188}
189
190#[derive(Debug, Clone)]
192pub struct QgroupList {
193 pub status_flags: QgroupStatusFlags,
195 pub qgroups: Vec<QgroupInfo>,
197}
198
199#[derive(Default)]
200struct QgroupEntryBuilder {
201 has_info: bool,
203 rfer: u64,
204 rfer_cmpr: u64,
205 excl: u64,
206 excl_cmpr: u64,
207 has_limit: bool,
209 limit_flags: u64,
210 max_rfer: u64,
211 max_excl: u64,
212 parents: Vec<u64>,
214 children: Vec<u64>,
215}
216
217impl QgroupEntryBuilder {
218 fn build(self, qgroupid: u64, stale: bool) -> QgroupInfo {
219 QgroupInfo {
220 qgroupid,
221 rfer: self.rfer,
222 rfer_cmpr: self.rfer_cmpr,
223 excl: self.excl,
224 excl_cmpr: self.excl_cmpr,
225 limit_flags: QgroupLimitFlags::from_bits_truncate(self.limit_flags),
226 max_rfer: if self.limit_flags
227 & u64::from(BTRFS_QGROUP_LIMIT_MAX_RFER)
228 != 0
229 {
230 self.max_rfer
231 } else {
232 u64::MAX
233 },
234 max_excl: if self.limit_flags
235 & u64::from(BTRFS_QGROUP_LIMIT_MAX_EXCL)
236 != 0
237 {
238 self.max_excl
239 } else {
240 u64::MAX
241 },
242 parents: self.parents,
243 children: self.children,
244 stale,
245 }
246 }
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(read_le_u64(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 = read_le_u64(data, offset_of!(btrfs_qgroup_info_item, rfer));
264 builder.rfer_cmpr =
265 read_le_u64(data, offset_of!(btrfs_qgroup_info_item, rfer_cmpr));
266 builder.excl = read_le_u64(data, offset_of!(btrfs_qgroup_info_item, excl));
267 builder.excl_cmpr =
268 read_le_u64(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 read_le_u64(data, offset_of!(btrfs_qgroup_limit_item, flags));
281 builder.max_rfer =
282 read_le_u64(data, offset_of!(btrfs_qgroup_limit_item, max_rfer));
283 builder.max_excl =
284 read_le_u64(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(), &raw const 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(), &raw const 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 =
329 unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
330 Ok(ret > 0)
331}
332
333pub fn qgroup_remove(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
340 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
341 args.assign = 0;
342 args.src = src;
343 args.dst = dst;
344 let ret =
347 unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
348 Ok(ret > 0)
349}
350
351pub fn qgroup_limit(
357 fd: BorrowedFd,
358 qgroupid: u64,
359 flags: QgroupLimitFlags,
360 max_rfer: u64,
361 max_excl: u64,
362) -> nix::Result<()> {
363 let lim = btrfs_qgroup_limit {
364 flags: flags.bits(),
365 max_referenced: max_rfer,
366 max_exclusive: max_excl,
367 rsv_referenced: 0,
368 rsv_exclusive: 0,
369 };
370 let mut args: btrfs_ioctl_qgroup_limit_args = unsafe { mem::zeroed() };
371 args.qgroupid = qgroupid;
372 args.lim = lim;
373 unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &raw mut args) }?;
377 Ok(())
378}
379
380pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
386 let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
388 let mut status_flags = QgroupStatusFlags::empty();
389
390 let quota_key = SearchKey {
392 tree_id: u64::from(BTRFS_QUOTA_TREE_OBJECTID),
393 min_objectid: 0,
394 max_objectid: u64::MAX,
395 min_type: BTRFS_QGROUP_STATUS_KEY,
396 max_type: BTRFS_QGROUP_RELATION_KEY,
397 min_offset: 0,
398 max_offset: u64::MAX,
399 min_transid: 0,
400 max_transid: u64::MAX,
401 };
402
403 let scan_result = tree_search(fd, quota_key, |hdr, data| {
404 match hdr.item_type {
405 t if t == BTRFS_QGROUP_STATUS_KEY => {
406 if let Some(raw) = parse_status_flags(data) {
407 status_flags = QgroupStatusFlags::from_bits_truncate(raw);
408 }
409 }
410 t if t == BTRFS_QGROUP_INFO_KEY => {
411 let entry = builders.entry(hdr.offset).or_default();
413 parse_info(entry, data);
414 }
415 t if t == BTRFS_QGROUP_LIMIT_KEY => {
416 let entry = builders.entry(hdr.offset).or_default();
418 parse_limit(entry, data);
419 }
420 t if t == BTRFS_QGROUP_RELATION_KEY => {
421 if hdr.objectid > hdr.offset {
427 let parent = hdr.objectid;
428 let child = hdr.offset;
429 builders.entry(child).or_default().parents.push(parent);
430 builders.entry(parent).or_default().children.push(child);
431 }
432 }
433 _ => {}
434 }
435 Ok(())
436 });
437
438 match scan_result {
439 Err(Errno::ENOENT) => {
440 return Ok(QgroupList {
442 status_flags: QgroupStatusFlags::empty(),
443 qgroups: Vec::new(),
444 });
445 }
446 Err(e) => return Err(e),
447 Ok(()) => {}
448 }
449
450 let existing_subvol_ids = collect_subvol_ids(fd)?;
452
453 let mut qgroups: Vec<QgroupInfo> = builders
455 .into_iter()
456 .map(|(qgroupid, builder)| {
457 let stale = if qgroupid_level(qgroupid) == 0 {
458 !existing_subvol_ids.contains(&qgroupid_subvolid(qgroupid))
459 } else {
460 false
461 };
462 builder.build(qgroupid, stale)
463 })
464 .collect();
465
466 qgroups.sort_by_key(|q| q.qgroupid);
467
468 Ok(QgroupList {
469 status_flags,
470 qgroups,
471 })
472}
473
474fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
477 let mut ids: HashSet<u64> = HashSet::new();
478
479 let key = SearchKey::for_objectid_range(
482 u64::from(BTRFS_ROOT_TREE_OBJECTID),
483 BTRFS_ROOT_ITEM_KEY,
484 u64::from(BTRFS_FIRST_FREE_OBJECTID),
485 BTRFS_LAST_FREE_OBJECTID as u64,
486 );
487
488 tree_search(fd, key, |hdr, _data| {
489 ids.insert(hdr.objectid);
490 Ok(())
491 })?;
492
493 Ok(ids)
494}
495
496pub fn qgroup_clear_stale(fd: BorrowedFd) -> nix::Result<usize> {
505 let list = qgroup_list(fd)?;
506 let simple_mode =
507 list.status_flags.contains(QgroupStatusFlags::SIMPLE_MODE);
508
509 let mut count = 0usize;
510
511 for qg in &list.qgroups {
512 if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
514 continue;
515 }
516
517 if simple_mode && (qg.rfer != 0 || qg.excl != 0) {
519 continue;
520 }
521
522 if qgroup_destroy(fd, qg.qgroupid).is_ok() {
523 count += 1;
524 }
525 }
526
527 Ok(count)
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn qgroupid_level_zero() {
536 assert_eq!(qgroupid_level(5), 0);
537 assert_eq!(qgroupid_level(256), 0);
538 }
539
540 #[test]
541 fn qgroupid_level_nonzero() {
542 let id = (1u64 << 48) | 100;
543 assert_eq!(qgroupid_level(id), 1);
544
545 let id = (3u64 << 48) | 42;
546 assert_eq!(qgroupid_level(id), 3);
547 }
548
549 #[test]
550 fn qgroupid_subvolid_extracts_lower_48_bits() {
551 assert_eq!(qgroupid_subvolid(256), 256);
552 assert_eq!(qgroupid_subvolid((1u64 << 48) | 100), 100);
553 assert_eq!(qgroupid_subvolid((2u64 << 48) | 0), 0);
554 }
555
556 #[test]
557 fn qgroupid_roundtrip() {
558 let level: u64 = 2;
559 let subvolid: u64 = 999;
560 let packed = (level << 48) | subvolid;
561 assert_eq!(qgroupid_level(packed), level as u16);
562 assert_eq!(qgroupid_subvolid(packed), subvolid);
563 }
564}