use clap::{Parser, ValueEnum};
use std::path::PathBuf;
use uuid::Uuid;
const HEADING_LAYOUT: &str = "Block layout";
const HEADING_FEATURES: &str = "Features";
const HEADING_IDENTITY: &str = "Identity";
const HEADING_ROOTDIR: &str = "Rootdir population";
#[derive(Parser, Debug)]
#[command(name = "mkfs.btrfs", version)]
#[allow(clippy::struct_excessive_bools)]
pub struct Arguments {
#[arg(short = 'd', long = "data", value_name = "PROFILE",
help_heading = HEADING_LAYOUT)]
pub data_profile: Option<Profile>,
#[arg(short = 'm', long = "metadata", value_name = "PROFILE",
help_heading = HEADING_LAYOUT)]
pub metadata_profile: Option<Profile>,
#[arg(short = 'M', long, help_heading = HEADING_LAYOUT)]
pub mixed: bool,
#[arg(short = 'n', long, value_name = "SIZE",
help_heading = HEADING_LAYOUT)]
pub nodesize: Option<SizeArg>,
#[arg(short = 's', long, value_name = "SIZE",
help_heading = HEADING_LAYOUT)]
pub sectorsize: Option<SizeArg>,
#[arg(short = 'b', long = "byte-count", value_name = "SIZE",
help_heading = HEADING_LAYOUT)]
pub byte_count: Option<SizeArg>,
#[arg(long = "checksum", alias = "csum", value_name = "TYPE",
help_heading = HEADING_FEATURES)]
pub checksum: Option<ChecksumArg>,
#[arg(
short = 'O',
long = "features",
alias = "runtime-features",
short_alias = 'R',
value_name = "LIST",
value_delimiter = ',',
help_heading = HEADING_FEATURES,
)]
pub features: Vec<FeatureArg>,
#[arg(short = 'L', long = "label", value_name = "LABEL",
help_heading = HEADING_IDENTITY)]
pub label: Option<String>,
#[arg(short = 'U', long = "uuid", value_name = "UUID",
help_heading = HEADING_IDENTITY)]
pub filesystem_uuid: Option<Uuid>,
#[arg(long = "device-uuid", value_name = "UUID",
help_heading = HEADING_IDENTITY)]
pub device_uuid: Option<Uuid>,
#[arg(short = 'r', long = "rootdir", value_name = "DIR",
help_heading = HEADING_ROOTDIR)]
pub rootdir: Option<PathBuf>,
#[arg(short = 'u', long = "subvol", value_name = "TYPE:SUBDIR",
help_heading = HEADING_ROOTDIR)]
pub subvol: Vec<SubvolArg>,
#[arg(long = "inode-flags", value_name = "FLAGS:PATH",
help_heading = HEADING_ROOTDIR)]
pub inode_flags: Vec<InodeFlagsArg>,
#[arg(long = "compress", value_name = "ALGO[:LEVEL]",
help_heading = HEADING_ROOTDIR)]
pub compress: Option<CompressArg>,
#[arg(long, help_heading = HEADING_ROOTDIR)]
pub reflink: bool,
#[arg(long, help_heading = HEADING_ROOTDIR)]
pub shrink: bool,
#[arg(short = 'f', long)]
pub force: bool,
#[arg(short = 'K', long)]
pub nodiscard: bool,
#[arg(short = 'q', long)]
pub quiet: bool,
#[arg(short = 'v', long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(required = true)]
pub devices: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SizeArg(pub u64);
impl std::str::FromStr for SizeArg {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let (num_str, suffix) = match s.find(|c: char| c.is_alphabetic()) {
Some(i) => (&s[..i], &s[i..]),
None => (s, ""),
};
let base: u64 =
num_str.parse().map_err(|e| format!("invalid size: {e}"))?;
let multiplier = match suffix.to_lowercase().as_str() {
"" => 1u64,
"k" | "kib" => 1 << 10,
"m" | "mib" => 1 << 20,
"g" | "gib" => 1 << 30,
"t" | "tib" => 1 << 40,
"p" | "pib" => 1 << 50,
"e" | "eib" => 1 << 60,
_ => return Err(format!("unknown size suffix: {suffix}")),
};
base.checked_mul(multiplier)
.map(SizeArg)
.ok_or_else(|| format!("size overflow: {s}"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "snake_case")]
pub enum Profile {
Single,
Dup,
Raid0,
Raid1,
Raid1c3,
Raid1c4,
Raid5,
Raid6,
Raid10,
}
impl std::fmt::Display for Profile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Profile::Single => write!(f, "single"),
Profile::Dup => write!(f, "DUP"),
Profile::Raid0 => write!(f, "RAID0"),
Profile::Raid1 => write!(f, "RAID1"),
Profile::Raid1c3 => write!(f, "RAID1C3"),
Profile::Raid1c4 => write!(f, "RAID1C4"),
Profile::Raid5 => write!(f, "RAID5"),
Profile::Raid6 => write!(f, "RAID6"),
Profile::Raid10 => write!(f, "RAID10"),
}
}
}
impl Profile {
#[must_use]
pub fn block_group_flag(self) -> u64 {
use btrfs_disk::raw;
match self {
Profile::Single => 0,
Profile::Dup => u64::from(raw::BTRFS_BLOCK_GROUP_DUP),
Profile::Raid0 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID0),
Profile::Raid1 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID1),
Profile::Raid1c3 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID1C3),
Profile::Raid1c4 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID1C4),
Profile::Raid5 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID5),
Profile::Raid6 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID6),
Profile::Raid10 => u64::from(raw::BTRFS_BLOCK_GROUP_RAID10),
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn num_stripes(self, n_devices: usize) -> u16 {
match self {
Profile::Single => 1,
Profile::Dup | Profile::Raid1 => 2,
Profile::Raid1c3 => 3,
Profile::Raid1c4 => 4,
Profile::Raid0 | Profile::Raid5 | Profile::Raid6 => {
n_devices as u16
}
Profile::Raid10 => (n_devices as u16) & !1, }
}
#[must_use]
pub fn sub_stripes(self) -> u16 {
match self {
Profile::Raid10 => 2,
_ => 1,
}
}
#[must_use]
pub fn nparity(self) -> u16 {
match self {
Profile::Raid5 => 1,
Profile::Raid6 => 2,
_ => 0,
}
}
#[must_use]
pub fn data_stripes(self, n_devices: usize) -> u16 {
let ns = self.num_stripes(n_devices);
match self {
Profile::Single
| Profile::Dup
| Profile::Raid1
| Profile::Raid1c3
| Profile::Raid1c4 => 1,
Profile::Raid0 => ns,
Profile::Raid10 => ns / self.sub_stripes(),
Profile::Raid5 | Profile::Raid6 => ns - self.nparity(),
}
}
#[must_use]
pub fn is_mirror(self) -> bool {
matches!(
self,
Profile::Single
| Profile::Dup
| Profile::Raid1
| Profile::Raid1c3
| Profile::Raid1c4
)
}
#[must_use]
pub fn min_devices(self) -> usize {
match self {
Profile::Single | Profile::Dup => 1,
Profile::Raid0
| Profile::Raid1
| Profile::Raid5
| Profile::Raid10 => 2,
Profile::Raid1c3 | Profile::Raid6 => 3,
Profile::Raid1c4 => 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "snake_case")]
pub enum ChecksumArg {
Crc32c,
#[value(alias = "xxhash64")]
Xxhash,
Sha256,
#[value(alias = "blake2b")]
Blake2,
}
impl std::fmt::Display for ChecksumArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChecksumArg::Crc32c => write!(f, "crc32c"),
ChecksumArg::Xxhash => write!(f, "xxhash"),
ChecksumArg::Sha256 => write!(f, "sha256"),
ChecksumArg::Blake2 => write!(f, "blake2"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeatureArg {
pub feature: Feature,
pub enabled: bool,
}
impl std::str::FromStr for FeatureArg {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (enabled, name) = if let Some(rest) = s.strip_prefix('^') {
(false, rest)
} else {
(true, s)
};
let feature = Feature::from_str(name, false)?;
Ok(FeatureArg { feature, enabled })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum Feature {
MixedBg,
Extref,
Raid56,
#[value(alias = "skinny_metadata")]
SkinnyMetadata,
#[value(alias = "no_holes")]
NoHoles,
Zoned,
Quota,
#[value(alias = "fst")]
FreeSpaceTree,
#[value(alias = "bgt")]
BlockGroupTree,
RaidStripeTree,
Squota,
ListAll,
}
impl std::fmt::Display for Feature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Feature::MixedBg => write!(f, "mixed-bg"),
Feature::Extref => write!(f, "extref"),
Feature::Raid56 => write!(f, "raid56"),
Feature::SkinnyMetadata => write!(f, "skinny-metadata"),
Feature::NoHoles => write!(f, "no-holes"),
Feature::Zoned => write!(f, "zoned"),
Feature::Quota => write!(f, "quota"),
Feature::FreeSpaceTree => write!(f, "free-space-tree"),
Feature::BlockGroupTree => write!(f, "block-group-tree"),
Feature::RaidStripeTree => write!(f, "raid-stripe-tree"),
Feature::Squota => write!(f, "squota"),
Feature::ListAll => write!(f, "list-all"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubvolArg {
pub subvol_type: SubvolType,
pub path: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SubvolType {
#[default]
Rw,
Ro,
Default,
DefaultRo,
}
impl std::str::FromStr for SubvolArg {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.starts_with("./") {
return Ok(SubvolArg {
subvol_type: SubvolType::Rw,
path: PathBuf::from(s),
});
}
if let Some((prefix, rest)) = s.split_once(':') {
let subvol_type = match prefix {
"rw" => SubvolType::Rw,
"ro" => SubvolType::Ro,
"default" => SubvolType::Default,
"default-ro" => SubvolType::DefaultRo,
_ => {
return Ok(SubvolArg {
subvol_type: SubvolType::Rw,
path: PathBuf::from(s),
});
}
};
if rest.is_empty() {
return Err("subvolume path cannot be empty".to_string());
}
Ok(SubvolArg {
subvol_type,
path: PathBuf::from(rest),
})
} else {
Ok(SubvolArg {
subvol_type: SubvolType::Rw,
path: PathBuf::from(s),
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InodeFlagsArg {
pub nodatacow: bool,
pub nodatasum: bool,
pub path: PathBuf,
}
impl std::str::FromStr for InodeFlagsArg {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (flags_str, path) = s
.split_once(':')
.ok_or_else(|| "expected FLAGS:PATH format".to_string())?;
if path.is_empty() {
return Err("path cannot be empty".to_string());
}
let mut nodatacow = false;
let mut nodatasum = false;
for flag in flags_str.split(',') {
match flag.trim() {
"nodatacow" => nodatacow = true,
"nodatasum" => nodatasum = true,
other => return Err(format!("unknown inode flag: {other}")),
}
}
Ok(InodeFlagsArg {
nodatacow,
nodatasum,
path: PathBuf::from(path),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompressArg {
pub algorithm: CompressAlgorithm,
pub level: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressAlgorithm {
No,
Zstd,
Lzo,
Zlib,
}
impl std::str::FromStr for CompressArg {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (algo_str, level) = if let Some((a, l)) = s.split_once(':') {
let level: u32 =
l.parse().map_err(|e| format!("invalid level: {e}"))?;
(a, Some(level))
} else {
(s, None)
};
let algorithm = match algo_str.to_lowercase().as_str() {
"no" | "none" => CompressAlgorithm::No,
"zstd" => CompressAlgorithm::Zstd,
"lzo" => CompressAlgorithm::Lzo,
"zlib" => CompressAlgorithm::Zlib,
_ => {
return Err(format!(
"unknown compression algorithm: {algo_str}"
));
}
};
if level.is_some() && algorithm == CompressAlgorithm::No {
return Err(
"compression level not valid with 'no' algorithm".to_string()
);
}
if let Some(l) = level {
match algorithm {
CompressAlgorithm::Zstd if l > 15 => {
return Err(format!("zstd level must be 1..15, got {l}"));
}
CompressAlgorithm::Zlib if l > 9 => {
return Err(format!("zlib level must be 1..9, got {l}"));
}
CompressAlgorithm::Lzo if level.is_some() => {
return Err(
"lzo does not support compression levels".to_string()
);
}
_ => {}
}
}
Ok(CompressArg { algorithm, level })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn size_arg_bare_number() {
assert_eq!("0".parse::<SizeArg>().unwrap(), SizeArg(0));
assert_eq!("42".parse::<SizeArg>().unwrap(), SizeArg(42));
}
#[test]
fn size_arg_all_suffixes() {
assert_eq!("1k".parse::<SizeArg>().unwrap(), SizeArg(1 << 10));
assert_eq!("1kib".parse::<SizeArg>().unwrap(), SizeArg(1 << 10));
assert_eq!("1m".parse::<SizeArg>().unwrap(), SizeArg(1 << 20));
assert_eq!("1mib".parse::<SizeArg>().unwrap(), SizeArg(1 << 20));
assert_eq!("1g".parse::<SizeArg>().unwrap(), SizeArg(1 << 30));
assert_eq!("1gib".parse::<SizeArg>().unwrap(), SizeArg(1 << 30));
assert_eq!("1t".parse::<SizeArg>().unwrap(), SizeArg(1 << 40));
assert_eq!("1p".parse::<SizeArg>().unwrap(), SizeArg(1 << 50));
assert_eq!("1e".parse::<SizeArg>().unwrap(), SizeArg(1 << 60));
}
#[test]
fn size_arg_case_insensitive() {
assert_eq!("16K".parse::<SizeArg>().unwrap(), SizeArg(16 << 10));
assert_eq!("4MiB".parse::<SizeArg>().unwrap(), SizeArg(4 << 20));
assert_eq!("2G".parse::<SizeArg>().unwrap(), SizeArg(2 << 30));
}
#[test]
fn size_arg_bad_number() {
assert!("abc".parse::<SizeArg>().is_err());
assert!("".parse::<SizeArg>().is_err());
}
#[test]
fn size_arg_unknown_suffix() {
assert!("10X".parse::<SizeArg>().is_err());
}
#[test]
fn size_arg_overflow() {
assert!("16385P".parse::<SizeArg>().is_err());
assert!("17E".parse::<SizeArg>().is_err());
}
#[test]
fn profile_all_variants() {
assert_eq!(Profile::from_str("single", true).unwrap(), Profile::Single);
assert_eq!(Profile::from_str("dup", true).unwrap(), Profile::Dup);
assert_eq!(Profile::from_str("raid0", true).unwrap(), Profile::Raid0);
assert_eq!(Profile::from_str("raid1", true).unwrap(), Profile::Raid1);
assert_eq!(
Profile::from_str("raid1c3", true).unwrap(),
Profile::Raid1c3
);
assert_eq!(
Profile::from_str("raid1c4", true).unwrap(),
Profile::Raid1c4
);
assert_eq!(Profile::from_str("raid5", true).unwrap(), Profile::Raid5);
assert_eq!(Profile::from_str("raid6", true).unwrap(), Profile::Raid6);
assert_eq!(Profile::from_str("raid10", true).unwrap(), Profile::Raid10);
}
#[test]
fn profile_case_insensitive() {
assert_eq!(Profile::from_str("SINGLE", true).unwrap(), Profile::Single);
assert_eq!(
Profile::from_str("Raid1C3", true).unwrap(),
Profile::Raid1c3
);
}
#[test]
fn profile_unknown() {
assert!(Profile::from_str("raid99", true).is_err());
}
#[test]
fn profile_display_roundtrip() {
let all = [
Profile::Single,
Profile::Dup,
Profile::Raid0,
Profile::Raid1,
Profile::Raid1c3,
Profile::Raid1c4,
Profile::Raid5,
Profile::Raid6,
Profile::Raid10,
];
for p in all {
let s = p.to_string();
assert_eq!(
Profile::from_str(&s, true).unwrap(),
p,
"round-trip failed for {s}"
);
}
}
#[test]
fn profile_block_group_flag_single() {
assert_eq!(Profile::Single.block_group_flag(), 0);
}
#[test]
fn profile_block_group_flag_dup() {
assert_ne!(Profile::Dup.block_group_flag(), 0);
}
#[test]
fn profile_block_group_flag_raid1() {
assert_ne!(Profile::Raid1.block_group_flag(), 0);
assert_ne!(
Profile::Raid1.block_group_flag(),
Profile::Dup.block_group_flag()
);
}
#[test]
fn profile_num_stripes() {
assert_eq!(Profile::Single.num_stripes(4), 1);
assert_eq!(Profile::Dup.num_stripes(1), 2);
assert_eq!(Profile::Raid1.num_stripes(2), 2);
assert_eq!(Profile::Raid1c3.num_stripes(3), 3);
assert_eq!(Profile::Raid1c4.num_stripes(4), 4);
assert_eq!(Profile::Raid0.num_stripes(5), 5);
assert_eq!(Profile::Raid5.num_stripes(4), 4);
assert_eq!(Profile::Raid6.num_stripes(4), 4);
assert_eq!(Profile::Raid10.num_stripes(4), 4);
assert_eq!(Profile::Raid10.num_stripes(5), 4);
assert_eq!(Profile::Raid10.num_stripes(3), 2);
}
#[test]
fn profile_min_devices() {
assert_eq!(Profile::Single.min_devices(), 1);
assert_eq!(Profile::Dup.min_devices(), 1);
assert_eq!(Profile::Raid0.min_devices(), 2);
assert_eq!(Profile::Raid1.min_devices(), 2);
assert_eq!(Profile::Raid5.min_devices(), 2);
assert_eq!(Profile::Raid1c3.min_devices(), 3);
assert_eq!(Profile::Raid6.min_devices(), 3);
assert_eq!(Profile::Raid1c4.min_devices(), 4);
assert_eq!(Profile::Raid10.min_devices(), 2);
}
#[test]
fn profile_data_stripes() {
assert_eq!(Profile::Single.data_stripes(1), 1);
assert_eq!(Profile::Dup.data_stripes(1), 1);
assert_eq!(Profile::Raid1.data_stripes(2), 1);
assert_eq!(Profile::Raid0.data_stripes(4), 4);
assert_eq!(Profile::Raid10.data_stripes(4), 2);
assert_eq!(Profile::Raid10.data_stripes(6), 3);
assert_eq!(Profile::Raid5.data_stripes(4), 3);
assert_eq!(Profile::Raid6.data_stripes(4), 2);
}
#[test]
fn profile_sub_stripes() {
assert_eq!(Profile::Single.sub_stripes(), 1);
assert_eq!(Profile::Raid0.sub_stripes(), 1);
assert_eq!(Profile::Raid10.sub_stripes(), 2);
}
#[test]
fn profile_nparity() {
assert_eq!(Profile::Single.nparity(), 0);
assert_eq!(Profile::Raid5.nparity(), 1);
assert_eq!(Profile::Raid6.nparity(), 2);
}
#[test]
fn checksum_all_names() {
assert_eq!(
ChecksumArg::from_str("crc32c", true).unwrap(),
ChecksumArg::Crc32c
);
assert_eq!(
ChecksumArg::from_str("xxhash", true).unwrap(),
ChecksumArg::Xxhash
);
assert_eq!(
ChecksumArg::from_str("sha256", true).unwrap(),
ChecksumArg::Sha256
);
assert_eq!(
ChecksumArg::from_str("blake2", true).unwrap(),
ChecksumArg::Blake2
);
}
#[test]
fn checksum_aliases() {
assert_eq!(
ChecksumArg::from_str("xxhash64", true).unwrap(),
ChecksumArg::Xxhash
);
assert_eq!(
ChecksumArg::from_str("blake2b", true).unwrap(),
ChecksumArg::Blake2
);
}
#[test]
fn checksum_unknown() {
assert!(ChecksumArg::from_str("md5", true).is_err());
}
#[test]
fn checksum_display_roundtrip() {
let all = [
ChecksumArg::Crc32c,
ChecksumArg::Xxhash,
ChecksumArg::Sha256,
ChecksumArg::Blake2,
];
for c in all {
let s = c.to_string();
assert_eq!(
ChecksumArg::from_str(&s, true).unwrap(),
c,
"round-trip failed for {s}"
);
}
}
#[test]
fn feature_enable() {
let f: FeatureArg = "no-holes".parse().unwrap();
assert_eq!(f.feature, Feature::NoHoles);
assert!(f.enabled);
}
#[test]
fn feature_disable() {
let f: FeatureArg = "^no-holes".parse().unwrap();
assert_eq!(f.feature, Feature::NoHoles);
assert!(!f.enabled);
}
#[test]
fn feature_aliases() {
assert_eq!(
"fst".parse::<FeatureArg>().unwrap().feature,
Feature::FreeSpaceTree
);
assert_eq!(
"bgt".parse::<FeatureArg>().unwrap().feature,
Feature::BlockGroupTree
);
}
#[test]
fn feature_underscore_normalization() {
let f: FeatureArg = "skinny_metadata".parse().unwrap();
assert_eq!(f.feature, Feature::SkinnyMetadata);
}
#[test]
fn feature_unknown() {
assert!("bogus".parse::<FeatureArg>().is_err());
}
#[test]
fn feature_display_roundtrip() {
let samples = [
Feature::NoHoles,
Feature::SkinnyMetadata,
Feature::FreeSpaceTree,
Feature::BlockGroupTree,
Feature::Extref,
];
for feat in samples {
let s = feat.to_string();
assert_eq!(
Feature::from_str(&s, false).unwrap(),
feat,
"round-trip failed for {s}"
);
}
}
}