Skip to main content

btrfs_uapi/
quota.rs

1//! # Quota and qgroup management: enabling quotas and tracking disk usage
2//!
3//! Quota accounting tracks disk usage per subvolume via qgroups (quota groups).
4//! It must be explicitly enabled before any qgroup limits or usage data are
5//! available.  Once enabled, usage numbers are maintained incrementally by the
6//! kernel; a rescan rebuilds them from scratch if they become inconsistent.
7//!
8//! Every subvolume automatically gets a level-0 qgroup whose ID matches the
9//! subvolume ID.  Higher-level qgroups can be created and linked into a
10//! parent-child hierarchy so that space usage rolls up through the tree.
11//!
12//! Quota status (whether quotas are on, which mode, inconsistency flag) is
13//! read from sysfs via [`crate::sysfs::SysfsBtrfs::quota_status`].
14//!
15//! Most operations require `CAP_SYS_ADMIN`.
16
17use 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
49/// Enable quota accounting on the filesystem referred to by `fd`.
50///
51/// When `simple` is `true`, uses `BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA`, which
52/// accounts for extent ownership by lifetime rather than backref walks. This is
53/// faster but less precise than full qgroup accounting.
54pub 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
66/// Disable quota accounting on the filesystem referred to by `fd`.
67pub 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
74/// Start a quota rescan on the filesystem referred to by `fd`.
75///
76/// Returns immediately after kicking off the background scan. Use
77/// [`quota_rescan_wait`] to block until it finishes. If a rescan is already
78/// in progress the kernel returns `EINPROGRESS`; callers that are about to
79/// wait anyway can treat that as a non-error.
80pub 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
86/// Block until the quota rescan currently running on the filesystem referred
87/// to by `fd` completes. Returns immediately if no rescan is in progress.
88pub fn quota_rescan_wait(fd: BorrowedFd) -> nix::Result<()> {
89    unsafe { btrfs_ioc_quota_rescan_wait(fd.as_raw_fd()) }?;
90    Ok(())
91}
92
93/// Status of an in-progress (or absent) quota rescan.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct QuotaRescanStatus {
96    /// Whether a rescan is currently running.
97    pub running: bool,
98    /// Object ID of the most recently scanned tree item. Only meaningful
99    /// when `running` is `true`.
100    pub progress: u64,
101}
102
103/// Query the status of the quota rescan on the filesystem referred to by `fd`.
104pub 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/// Extract the hierarchy level from a packed qgroup ID.
114///
115/// `qgroupid = (level << 48) | subvolid`.  Level 0 qgroups correspond
116/// directly to subvolumes.
117#[inline]
118#[must_use]
119pub fn qgroupid_level(qgroupid: u64) -> u16 {
120    (qgroupid >> 48) as u16
121}
122
123/// Extract the subvolume ID component from a packed qgroup ID.
124///
125/// Only meaningful for level-0 qgroups.
126#[inline]
127#[must_use]
128pub fn qgroupid_subvolid(qgroupid: u64) -> u64 {
129    qgroupid & 0x0000_FFFF_FFFF_FFFF
130}
131
132bitflags! {
133    /// Status flags for the quota tree as a whole (`BTRFS_QGROUP_STATUS_KEY`).
134    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
135    pub struct QgroupStatusFlags: u64 {
136        /// Quota accounting is enabled.
137        const ON           = BTRFS_QGROUP_STATUS_FLAG_ON as u64;
138        /// A rescan is currently in progress.
139        const RESCAN       = BTRFS_QGROUP_STATUS_FLAG_RESCAN as u64;
140        /// Accounting is inconsistent and a rescan is needed.
141        const INCONSISTENT = BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT as u64;
142        /// Simple quota mode (squota) is active.
143        const SIMPLE_MODE  = BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE as u64;
144    }
145}
146
147bitflags! {
148    /// Which limit fields are actively enforced on a qgroup.
149    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
150    pub struct QgroupLimitFlags: u64 {
151        /// `max_rfer` (maximum referenced bytes) is enforced.
152        const MAX_RFER  = BTRFS_QGROUP_LIMIT_MAX_RFER as u64;
153        /// `max_excl` (maximum exclusive bytes) is enforced.
154        const MAX_EXCL  = BTRFS_QGROUP_LIMIT_MAX_EXCL as u64;
155        /// Referenced bytes are compressed before comparison.
156        const RFER_CMPR = BTRFS_QGROUP_LIMIT_RFER_CMPR as u64;
157        /// Exclusive bytes are compressed before comparison.
158        const EXCL_CMPR = BTRFS_QGROUP_LIMIT_EXCL_CMPR as u64;
159    }
160}
161
162/// Usage and limit information for a single qgroup.
163#[derive(Debug, Clone)]
164pub struct QgroupInfo {
165    /// Packed qgroup ID: `(level << 48) | subvolid`.
166    pub qgroupid: u64,
167    /// Total referenced bytes (includes shared data).
168    pub rfer: u64,
169    /// Referenced bytes after compression.
170    pub rfer_cmpr: u64,
171    /// Exclusively-owned bytes (not shared with any other subvolume).
172    pub excl: u64,
173    /// Exclusively-owned bytes after compression.
174    pub excl_cmpr: u64,
175    /// Limit flags — which of the limit fields below are enforced.
176    pub limit_flags: QgroupLimitFlags,
177    /// Maximum referenced bytes.  `u64::MAX` when no limit is set.
178    pub max_rfer: u64,
179    /// Maximum exclusive bytes.  `u64::MAX` when no limit is set.
180    pub max_excl: u64,
181    /// IDs of qgroups that are parents of this one in the hierarchy.
182    pub parents: Vec<u64>,
183    /// IDs of qgroups that are children of this one in the hierarchy.
184    pub children: Vec<u64>,
185    /// Level-0 only: `true` when the corresponding subvolume no longer
186    /// exists (this is a "stale" qgroup left behind after deletion).
187    pub stale: bool,
188}
189
190/// Result of [`qgroup_list`]: overall quota status and per-qgroup details.
191#[derive(Debug, Clone)]
192pub struct QgroupList {
193    /// Flags from the `BTRFS_QGROUP_STATUS_KEY` item.
194    pub status_flags: QgroupStatusFlags,
195    /// All qgroups found in the quota tree, sorted by `qgroupid`.
196    pub qgroups: Vec<QgroupInfo>,
197}
198
199#[derive(Default)]
200struct QgroupEntryBuilder {
201    // From INFO item
202    has_info: bool,
203    rfer: u64,
204    rfer_cmpr: u64,
205    excl: u64,
206    excl_cmpr: u64,
207    // From LIMIT item
208    has_limit: bool,
209    limit_flags: u64,
210    max_rfer: u64,
211    max_excl: u64,
212    // From RELATION items
213    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
287/// Create a new qgroup with the given `qgroupid` on the filesystem referred
288/// to by `fd`.
289///
290/// `qgroupid` is the packed form: `(level << 48) | subvolid`.
291pub 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    // SAFETY: args is fully initialised above and lives for the duration of
296    // the ioctl call.
297    unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
298    Ok(())
299}
300
301/// Destroy the qgroup with the given `qgroupid` on the filesystem referred
302/// to by `fd`.
303pub 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    // SAFETY: args is fully initialised above and lives for the duration of
308    // the ioctl call.
309    unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
310    Ok(())
311}
312
313/// Assign qgroup `src` as a member of qgroup `dst` (i.e. `src` becomes a
314/// child of `dst`).
315///
316/// Returns `true` if the kernel indicates that a quota rescan is now needed
317/// (the ioctl returned a positive value).
318///
319/// Errors: ENOENT if either qgroup does not exist.  EEXIST if the
320/// relationship already exists.
321pub 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    // SAFETY: args is fully initialised above and lives for the duration of
327    // the ioctl call.
328    let ret =
329        unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
330    Ok(ret > 0)
331}
332
333/// Remove the child-parent relationship between qgroups `src` and `dst`.
334///
335/// Returns `true` if the kernel indicates that a quota rescan is now needed.
336///
337/// Errors: ENOENT if either qgroup does not exist or the relationship
338/// is not present.
339pub 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    // SAFETY: args is fully initialised above and lives for the duration of
345    // the ioctl call.
346    let ret =
347        unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
348    Ok(ret > 0)
349}
350
351/// Set usage limits on a qgroup.
352///
353/// Pass `QgroupLimitFlags::MAX_RFER` in `flags` to enforce `max_rfer`, and/or
354/// `QgroupLimitFlags::MAX_EXCL` to enforce `max_excl`.  Clear a limit by
355/// omitting the corresponding flag.
356pub 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    // SAFETY: args is fully initialised above and lives for the duration of
374    // the ioctl call.  The ioctl number is #43 (_IOR direction in the kernel
375    // header), which reads args from userspace.
376    unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &raw mut args) }?;
377    Ok(())
378}
379
380/// List all qgroups and overall quota status for the filesystem referred to
381/// by `fd`.
382///
383/// Returns `Ok(QgroupList { status_flags: empty, qgroups: [] })` when quota
384/// accounting is not enabled (`ENOENT` from the kernel).
385pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
386    // Build a map of qgroupid → builder as we walk the quota tree.
387    let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
388    let mut status_flags = QgroupStatusFlags::empty();
389
390    // Scan the quota tree for STATUS / INFO / LIMIT / RELATION items in one pass.
391    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                // offset = qgroupid
412                let entry = builders.entry(hdr.offset).or_default();
413                parse_info(entry, data);
414            }
415            t if t == BTRFS_QGROUP_LIMIT_KEY => {
416                // offset = qgroupid
417                let entry = builders.entry(hdr.offset).or_default();
418                parse_limit(entry, data);
419            }
420            t if t == BTRFS_QGROUP_RELATION_KEY => {
421                // The kernel stores two entries per relation:
422                //   (child, RELATION_KEY, parent)
423                //   (parent, RELATION_KEY, child)
424                // Only process the canonical form where objectid > offset,
425                // i.e. parent > child.
426                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            // Quota tree does not exist — quotas are disabled.
441            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    // Collect existing subvolume IDs so we can mark stale level-0 qgroups.
451    let existing_subvol_ids = collect_subvol_ids(fd)?;
452
453    // Convert builders to QgroupInfo, computing stale flag for level-0 groups.
454    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
474/// Collect the set of all existing subvolume IDs by scanning
475/// `ROOT_ITEM_KEY` entries in the root tree.
476fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
477    let mut ids: HashSet<u64> = HashSet::new();
478
479    // BTRFS_LAST_FREE_OBJECTID binds as i32 = -256; cast to u64 gives
480    // 0xFFFFFFFF_FFFFFF00 as expected.
481    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
496/// Destroy all "stale" level-0 qgroups — those whose corresponding subvolume
497/// no longer exists.
498///
499/// In simple-quota mode (`SIMPLE_MODE` flag set), stale qgroups with non-zero
500/// `rfer` or `excl` are retained because they hold accounting information for
501/// dropped subvolumes.
502///
503/// Returns the number of qgroups successfully destroyed.
504pub 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        // Only process level-0 stale qgroups.
513        if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
514            continue;
515        }
516
517        // In simple-quota mode, keep stale qgroups that still have usage data.
518        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}