use anyhow::{Context, Result, bail};
use chrono::{DateTime, Local};
use serde::Serialize;
use std::{
fs::{self, File},
os::unix::fs::FileTypeExt,
path::Path,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use uuid::Uuid;
pub fn open_path(path: &Path) -> Result<File> {
File::open(path)
.with_context(|| format!("failed to open '{}'", path.display()))
}
pub fn is_mounted(device: &Path) -> bool {
btrfs_uapi::filesystem::is_mounted(device).unwrap_or(false)
}
pub enum SizeFormat {
Raw,
HumanIec,
HumanSi,
Fixed(u64),
}
pub fn fmt_size(bytes: u64, mode: &SizeFormat) -> String {
match mode {
SizeFormat::Raw => bytes.to_string(),
SizeFormat::HumanIec => human_bytes(bytes),
SizeFormat::HumanSi => human_bytes_si(bytes),
SizeFormat::Fixed(divisor) => format!("{}", bytes / divisor),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn human_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1024.0 && unit + 1 < UNITS.len() {
value /= 1024.0;
unit += 1;
}
format!("{value:.2}{}", UNITS[unit])
}
#[allow(clippy::cast_precision_loss)]
pub fn human_bytes_si(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB", "PB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1000.0 && unit + 1 < UNITS.len() {
value /= 1000.0;
unit += 1;
}
format!("{value:.2}{}", UNITS[unit])
}
pub fn format_time(t: SystemTime) -> String {
if t == UNIX_EPOCH {
return "-".to_string();
}
match DateTime::<Local>::from(t)
.format("%Y-%m-%d %H:%M:%S %z")
.to_string()
{
s if s.is_empty() => "-".to_string(),
s => s,
}
}
pub fn format_time_short(t: &SystemTime) -> String {
DateTime::<Local>::from(*t).format("%e.%b %T").to_string()
}
pub fn format_timespec(sec: u64, nsec: u32) -> String {
#[allow(clippy::cast_possible_wrap)] let sec_i64 = sec as i64;
match DateTime::from_timestamp(sec_i64, nsec) {
Some(utc) => {
let local = utc.with_timezone(&Local);
format!("{}.{} ({})", sec, nsec, local.format("%Y-%m-%d %H:%M:%S"))
}
None => format!("{sec}.{nsec}"),
}
}
pub fn parse_size_with_suffix(s: &str) -> Result<u64> {
let (num_str, suffix) = match s.find(|c: char| c.is_alphabetic()) {
Some(i) => (&s[..i], &s[i..]),
None => (s, ""),
};
let n: u64 = num_str
.parse()
.with_context(|| format!("invalid size number: '{num_str}'"))?;
let multiplier: u64 = match suffix.to_uppercase().as_str() {
"" => 1,
"K" => 1024,
"M" => 1024 * 1024,
"G" => 1024 * 1024 * 1024,
"T" => 1024u64.pow(4),
"P" => 1024u64.pow(5),
"E" => 1024u64.pow(6),
_ => anyhow::bail!("unknown size suffix: '{suffix}'"),
};
n.checked_mul(multiplier)
.ok_or_else(|| anyhow::anyhow!("size overflow: '{s}'"))
}
#[derive(Debug, Clone, Copy)]
pub struct ParsedUuid(Uuid);
impl std::ops::Deref for ParsedUuid {
type Target = Uuid;
fn deref(&self) -> &Uuid {
&self.0
}
}
pub fn parse_qgroupid(s: &str) -> anyhow::Result<u64> {
let (level_str, id_str) = s.split_once('/').ok_or_else(|| {
anyhow::anyhow!("invalid qgroup ID '{s}': expected <level>/<id>")
})?;
let level: u64 = level_str.parse().map_err(|_| {
anyhow::anyhow!("invalid qgroup level '{level_str}' in '{s}'")
})?;
let subvolid: u64 = id_str.parse().map_err(|_| {
anyhow::anyhow!("invalid qgroup subvolid '{id_str}' in '{s}'")
})?;
Ok((level << 48) | subvolid)
}
impl FromStr for ParsedUuid {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"clear" => Ok(Self(Uuid::nil())),
"random" => Ok(Self(Uuid::new_v4())),
"time" => Ok(Self(Uuid::now_v7())),
_ => Uuid::parse_str(s)
.map(Self)
.map_err(|e| format!("invalid UUID: {e}")),
}
}
}
pub fn check_device_for_overwrite(device: &Path, force: bool) -> Result<()> {
let meta = fs::metadata(device).with_context(|| {
format!("cannot access device '{}'", device.display())
})?;
if !meta.file_type().is_block_device() {
bail!("'{}' is not a block device", device.display());
}
if is_device_mounted(device)? {
bail!(
"'{}' is mounted; refusing to use a mounted device",
device.display()
);
}
if !force && has_btrfs_superblock(device) {
bail!(
"'{}' already contains a btrfs filesystem; use -f to force",
device.display()
);
}
Ok(())
}
pub fn is_device_mounted(device: &Path) -> Result<bool> {
btrfs_uapi::filesystem::is_mounted(device).with_context(|| {
format!("cannot check mount status of '{}'", device.display())
})
}
pub fn has_btrfs_superblock(device: &Path) -> bool {
let Ok(mut file) = File::open(device) else {
return false;
};
match btrfs_disk::superblock::read_superblock(&mut file, 0) {
Ok(sb) => sb.magic_is_valid(),
Err(_) => false,
}
}
pub fn print_json(key: &str, data: &impl Serialize) -> Result<()> {
#[derive(Serialize)]
struct Header {
version: &'static str,
}
let mut map = serde_json::Map::new();
map.insert(
"__header".to_string(),
serde_json::to_value(Header { version: "1" })?,
);
map.insert(key.to_string(), serde_json::to_value(data)?);
serde_json::to_writer_pretty(std::io::stdout(), &map)?;
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_bytes_zero() {
assert_eq!(human_bytes(0), "0.00B");
}
#[test]
fn human_bytes_small() {
assert_eq!(human_bytes(1), "1.00B");
assert_eq!(human_bytes(1023), "1023.00B");
}
#[test]
fn human_bytes_exact_powers() {
assert_eq!(human_bytes(1024), "1.00KiB");
assert_eq!(human_bytes(1024 * 1024), "1.00MiB");
assert_eq!(human_bytes(1024 * 1024 * 1024), "1.00GiB");
assert_eq!(human_bytes(1024u64.pow(4)), "1.00TiB");
assert_eq!(human_bytes(1024u64.pow(5)), "1.00PiB");
}
#[test]
fn human_bytes_fractional() {
assert_eq!(
human_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024),
"1.50GiB"
);
}
#[test]
fn human_bytes_u64_max() {
let s = human_bytes(u64::MAX);
assert!(s.ends_with("PiB"), "expected PiB suffix, got: {s}");
}
#[test]
fn parse_size_bare_number() {
assert_eq!(parse_size_with_suffix("0").unwrap(), 0);
assert_eq!(parse_size_with_suffix("42").unwrap(), 42);
}
#[test]
fn parse_size_all_suffixes() {
assert_eq!(parse_size_with_suffix("1K").unwrap(), 1024);
assert_eq!(parse_size_with_suffix("1M").unwrap(), 1024 * 1024);
assert_eq!(parse_size_with_suffix("1G").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_size_with_suffix("1T").unwrap(), 1024u64.pow(4));
assert_eq!(parse_size_with_suffix("1P").unwrap(), 1024u64.pow(5));
assert_eq!(parse_size_with_suffix("1E").unwrap(), 1024u64.pow(6));
}
#[test]
fn parse_size_case_insensitive() {
assert_eq!(parse_size_with_suffix("4k").unwrap(), 4 * 1024);
assert_eq!(
parse_size_with_suffix("2g").unwrap(),
2 * 1024 * 1024 * 1024
);
}
#[test]
fn parse_size_overflow() {
assert!(parse_size_with_suffix("16385P").is_err());
}
#[test]
fn parse_size_bad_number() {
assert!(parse_size_with_suffix("abcM").is_err());
assert!(parse_size_with_suffix("").is_err());
}
#[test]
fn parse_size_unknown_suffix() {
assert!(parse_size_with_suffix("10X").is_err());
}
#[test]
fn parse_qgroupid_level0() {
assert_eq!(parse_qgroupid("0/5").unwrap(), 5);
assert_eq!(parse_qgroupid("0/256").unwrap(), 256);
}
#[test]
fn parse_qgroupid_higher_level() {
assert_eq!(parse_qgroupid("1/256").unwrap(), (1u64 << 48) | 256);
assert_eq!(parse_qgroupid("2/0").unwrap(), 2u64 << 48);
}
#[test]
fn parse_qgroupid_missing_slash() {
assert!(parse_qgroupid("5").is_err());
}
#[test]
fn parse_qgroupid_bad_level() {
assert!(parse_qgroupid("abc/5").is_err());
}
#[test]
fn parse_qgroupid_bad_subvolid() {
assert!(parse_qgroupid("0/abc").is_err());
}
#[test]
fn parsed_uuid_clear() {
let u: ParsedUuid = "clear".parse().unwrap();
assert!(u.is_nil());
}
#[test]
fn parsed_uuid_random() {
let u: ParsedUuid = "random".parse().unwrap();
assert!(!u.is_nil());
}
#[test]
fn parsed_uuid_time() {
let u: ParsedUuid = "time".parse().unwrap();
assert!(!u.is_nil());
}
#[test]
fn parsed_uuid_explicit() {
let u: ParsedUuid =
"550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn parsed_uuid_no_hyphens() {
let u: ParsedUuid = "550e8400e29b41d4a716446655440000".parse().unwrap();
assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn parsed_uuid_invalid() {
assert!("not-a-uuid".parse::<ParsedUuid>().is_err());
assert!("".parse::<ParsedUuid>().is_err());
}
}