use anyhow::{Result, bail};
use btrfs_uapi::{balance::BalanceArgs, space::BlockGroupFlags};
pub fn parse_filters(filter_str: &str) -> Result<BalanceArgs> {
let mut args = BalanceArgs::new();
if filter_str.is_empty() {
return Ok(args);
}
for part in filter_str.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some((key, value)) = part.split_once('=') {
args = parse_one_filter(args, key.trim(), value.trim())?;
} else if part == "soft" {
args = args.soft();
} else {
bail!("unrecognized balance filter: '{part}'");
}
}
Ok(args)
}
fn parse_one_filter(
args: BalanceArgs,
key: &str,
value: &str,
) -> Result<BalanceArgs> {
if value.is_empty() {
bail!("the {key} filter requires an argument");
}
match key {
"profiles" => {
let bits = parse_profiles(value)?;
Ok(args.profiles(bits))
}
"usage" => {
if let Ok(n) = value.parse::<u64>() {
if n > 100 {
bail!("invalid usage argument: {value} (must be 0-100)");
}
Ok(args.usage(n))
} else if let Some((min, max)) = parse_range_u32(value)? {
if max != u32::MAX && max > 100 {
bail!(
"invalid usage argument: {value} (max must be <= 100)"
);
}
Ok(args.usage_range(min, max))
} else {
bail!("invalid usage argument: {value}")
}
}
"devid" => {
let devid: u64 = value
.parse()
.map_err(|_| anyhow::anyhow!("invalid devid: {value}"))?;
if devid == 0 {
bail!("invalid devid: {value}");
}
Ok(args.devid(devid))
}
"drange" => {
let (start, end) = parse_range_u64(value)?;
Ok(args.drange(start, end))
}
"vrange" => {
let (start, end) = parse_range_u64(value)?;
Ok(args.vrange(start, end))
}
"convert" => {
let profile = parse_one_profile(value)?;
Ok(args.convert(profile))
}
"limit" => {
if let Ok(n) = value.parse::<u64>() {
Ok(args.limit(n))
} else if let Some((min, max)) = parse_range_u32(value)? {
Ok(args.limit_range(min, max))
} else {
bail!("invalid limit argument: {value}")
}
}
"stripes" => {
if let Some((min, max)) = parse_range_u32(value)? {
Ok(args.stripes_range(min, max))
} else {
bail!("invalid stripes argument: {value}")
}
}
_ => bail!("unrecognized balance filter: '{key}'"),
}
}
fn parse_profiles(s: &str) -> Result<u64> {
let mut flags = 0u64;
for name in s.split('|') {
let name = name.trim();
if name.is_empty() {
continue;
}
flags |= parse_one_profile(name)?;
}
Ok(flags)
}
fn parse_one_profile(name: &str) -> Result<u64> {
let bits = match name.to_ascii_lowercase().as_str() {
"single" => BlockGroupFlags::SINGLE.bits(),
"raid0" => BlockGroupFlags::RAID0.bits(),
"raid1" => BlockGroupFlags::RAID1.bits(),
"raid1c3" => BlockGroupFlags::RAID1C3.bits(),
"raid1c4" => BlockGroupFlags::RAID1C4.bits(),
"raid5" => BlockGroupFlags::RAID5.bits(),
"raid6" => BlockGroupFlags::RAID6.bits(),
"raid10" => BlockGroupFlags::RAID10.bits(),
"dup" => BlockGroupFlags::DUP.bits(),
_ => bail!("unknown profile: '{name}'"),
};
Ok(bits)
}
fn parse_range_u32(s: &str) -> Result<Option<(u32, u32)>> {
let Some((left, right)) = s.split_once("..") else {
return Ok(None);
};
let min: u32 = if left.is_empty() {
0
} else {
left.parse()
.map_err(|_| anyhow::anyhow!("invalid range min: {left}"))?
};
let max: u32 = if right.is_empty() {
u32::MAX
} else {
right
.parse()
.map_err(|_| anyhow::anyhow!("invalid range max: {right}"))?
};
Ok(Some((min, max)))
}
fn parse_range_u64(s: &str) -> Result<(u64, u64)> {
let (left, right) = s.split_once("..").ok_or_else(|| {
anyhow::anyhow!("expected range 'start..end', got: {s}")
})?;
let start: u64 = if left.is_empty() {
0
} else {
left.parse()
.map_err(|_| anyhow::anyhow!("invalid range start: {left}"))?
};
let end: u64 = if right.is_empty() {
u64::MAX
} else {
right
.parse()
.map_err(|_| anyhow::anyhow!("invalid range end: {right}"))?
};
Ok((start, end))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_filter() {
let args = parse_filters("").unwrap();
let _ = args; }
#[test]
fn usage_single_value() {
let _args = parse_filters("usage=50").unwrap();
}
#[test]
fn usage_range() {
let _args = parse_filters("usage=10..50").unwrap();
}
#[test]
fn usage_over_100() {
assert!(parse_filters("usage=101").is_err());
}
#[test]
fn usage_range_over_100() {
assert!(parse_filters("usage=0..101").is_err());
}
#[test]
fn devid() {
let _args = parse_filters("devid=1").unwrap();
}
#[test]
fn devid_zero() {
assert!(parse_filters("devid=0").is_err());
}
#[test]
fn profiles_single() {
let _args = parse_filters("profiles=single").unwrap();
}
#[test]
fn profiles_multiple() {
let _args = parse_filters("profiles=raid1|single|dup").unwrap();
}
#[test]
fn profiles_unknown() {
assert!(parse_filters("profiles=raid99").is_err());
}
#[test]
fn convert_profile() {
let _args = parse_filters("convert=raid1").unwrap();
}
#[test]
fn soft_flag() {
let _args = parse_filters("soft").unwrap();
}
#[test]
fn limit_single() {
let _args = parse_filters("limit=10").unwrap();
}
#[test]
fn limit_range() {
let _args = parse_filters("limit=5..20").unwrap();
}
#[test]
fn stripes_range() {
let _args = parse_filters("stripes=1..4").unwrap();
}
#[test]
fn drange() {
let _args = parse_filters("drange=0..1073741824").unwrap();
}
#[test]
fn vrange() {
let _args = parse_filters("vrange=0..1073741824").unwrap();
}
#[test]
fn multiple_filters() {
let _args = parse_filters("usage=50,devid=1,limit=10").unwrap();
}
#[test]
fn convert_with_soft() {
let _args = parse_filters("convert=raid1,soft").unwrap();
}
#[test]
fn unknown_filter() {
assert!(parse_filters("bogus=42").is_err());
}
#[test]
fn missing_value() {
assert!(parse_filters("usage=").is_err());
}
#[test]
fn profiles_case_insensitive() {
let _args = parse_filters("profiles=RAID1|Single").unwrap();
}
#[test]
fn open_ended_ranges() {
let _args = parse_filters("usage=..50").unwrap();
let _args = parse_filters("usage=10..").unwrap();
let _args = parse_filters("limit=..100").unwrap();
}
}