use crate::error::{Error, Result};
pub const BTRFS_MAGIC: [u8; 8] = *b"_BHRfS_M";
pub const BTRFS_MAGIC_LE64: u64 = 0x4D5F_5366_5248_425F;
pub const SUPER_INFO_SIZE: usize = 4096;
pub const SUPER_INFO_OFFSET: u64 = 0x1_0000;
pub const SUPER_OFFSETS: [u64; 3] = [SUPER_INFO_OFFSET, 0x400_0000, 0x40_0000_0000];
pub const CSUM_SIZE: usize = 32;
pub const UUID_SIZE: usize = 16;
pub const LABEL_SIZE: usize = 256;
pub const SYS_CHUNK_ARRAY_SIZE: usize = 2048;
pub const DEV_ITEM_SIZE: usize = 0x62;
pub const MAX_LEVEL: u8 = 8;
pub const MAX_METADATA_BLOCKSIZE: u32 = 65536;
pub const MIN_SECTORSIZE: u32 = 4096;
pub mod offsets {
pub const CSUM: usize = 0x00;
pub const FSID: usize = 0x20;
pub const BYTENR: usize = 0x30;
pub const FLAGS: usize = 0x38;
pub const MAGIC: usize = 0x40;
pub const GENERATION: usize = 0x48;
pub const ROOT: usize = 0x50;
pub const CHUNK_ROOT: usize = 0x58;
pub const LOG_ROOT: usize = 0x60;
pub const LOG_ROOT_TRANSID: usize = 0x68;
pub const TOTAL_BYTES: usize = 0x70;
pub const BYTES_USED: usize = 0x78;
pub const ROOT_DIR_OBJECTID: usize = 0x80;
pub const NUM_DEVICES: usize = 0x88;
pub const SECTORSIZE: usize = 0x90;
pub const NODESIZE: usize = 0x94;
pub const LEAFSIZE: usize = 0x98;
pub const STRIPESIZE: usize = 0x9c;
pub const SYS_CHUNK_ARRAY_SIZE: usize = 0xa0;
pub const CHUNK_ROOT_GENERATION: usize = 0xa4;
pub const COMPAT_FLAGS: usize = 0xac;
pub const COMPAT_RO_FLAGS: usize = 0xb4;
pub const INCOMPAT_FLAGS: usize = 0xbc;
pub const CSUM_TYPE: usize = 0xc4;
pub const ROOT_LEVEL: usize = 0xc6;
pub const CHUNK_ROOT_LEVEL: usize = 0xc7;
pub const LOG_ROOT_LEVEL: usize = 0xc8;
pub const DEV_ITEM: usize = 0xc9;
pub const LABEL: usize = 0x12b;
pub const CACHE_GENERATION: usize = 0x22b;
pub const UUID_TREE_GENERATION: usize = 0x233;
pub const METADATA_UUID: usize = 0x23b;
pub const NR_GLOBAL_ROOTS: usize = 0x24b;
pub const SYS_CHUNK_ARRAY: usize = 0x32b;
pub const SUPER_ROOTS: usize = 0xb2b;
}
pub mod dev_item_offsets {
pub const DEVID: usize = 0x00;
pub const TOTAL_BYTES: usize = 0x08;
pub const BYTES_USED: usize = 0x10;
pub const IO_ALIGN: usize = 0x18;
pub const IO_WIDTH: usize = 0x1c;
pub const SECTOR_SIZE: usize = 0x20;
pub const TYPE: usize = 0x24;
pub const GENERATION: usize = 0x2c;
pub const START_OFFSET: usize = 0x34;
pub const DEV_GROUP: usize = 0x3c;
pub const SEEK_SPEED: usize = 0x40;
pub const BANDWIDTH: usize = 0x41;
pub const UUID: usize = 0x42;
pub const FSID: usize = 0x52;
}
pub mod super_flags {
pub const ERROR: u64 = 1 << 2;
pub const SEEDING: u64 = 1 << 32;
pub const METADUMP: u64 = 1 << 33;
pub const METADUMP_V2: u64 = 1 << 34;
pub const CHANGING_FSID: u64 = 1 << 35;
pub const CHANGING_FSID_V2: u64 = 1 << 36;
}
pub mod incompat {
pub const MIXED_BACKREF: u64 = 1 << 0;
pub const DEFAULT_SUBVOL: u64 = 1 << 1;
pub const MIXED_GROUPS: u64 = 1 << 2;
pub const COMPRESS_LZO: u64 = 1 << 3;
pub const COMPRESS_ZSTD: u64 = 1 << 4;
pub const BIG_METADATA: u64 = 1 << 5;
pub const EXTENDED_IREF: u64 = 1 << 6;
pub const RAID56: u64 = 1 << 7;
pub const SKINNY_METADATA: u64 = 1 << 8;
pub const NO_HOLES: u64 = 1 << 9;
pub const METADATA_UUID: u64 = 1 << 10;
pub const RAID1C34: u64 = 1 << 11;
pub const ZONED: u64 = 1 << 12;
pub const EXTENT_TREE_V2: u64 = 1 << 13;
pub const RAID_STRIPE_TREE: u64 = 1 << 14;
pub const SIMPLE_QUOTA: u64 = 1 << 16;
pub const REMAP_TREE: u64 = 1 << 17;
pub const SUPPORTED: u64 = MIXED_BACKREF
| DEFAULT_SUBVOL
| MIXED_GROUPS
| COMPRESS_LZO
| COMPRESS_ZSTD
| BIG_METADATA
| EXTENDED_IREF
| SKINNY_METADATA
| NO_HOLES
| METADATA_UUID
| RAID1C34;
}
pub mod compat_ro {
pub const FREE_SPACE_TREE: u64 = 1 << 0;
pub const FREE_SPACE_TREE_VALID: u64 = 1 << 1;
pub const VERITY: u64 = 1 << 2;
pub const BLOCK_GROUP_TREE: u64 = 1 << 3;
pub const SUPPORTED: u64 = FREE_SPACE_TREE | FREE_SPACE_TREE_VALID;
}
pub fn refuse_unmaintained_compat_ro(compat_ro_flags: u64) -> Result<()> {
let mut unmaintained = compat_ro_flags & !compat_ro::SUPPORTED;
if unmaintained == 0 {
return Ok(());
}
let mut named = Vec::new();
for (bit, name) in [
(compat_ro::VERITY, "fs-verity"),
(compat_ro::BLOCK_GROUP_TREE, "the block group tree"),
] {
if unmaintained & bit != 0 {
named.push(name.to_string());
unmaintained &= !bit;
}
}
if unmaintained != 0 {
named.push(format!("unknown bits {unmaintained:#018x}"));
}
Err(Error::UnsupportedFeature(format!(
"this volume uses read-only-compatible features this driver does not maintain \
({}), so it can be read but not written",
named.join(", ")
)))
}
#[cfg(test)]
mod compat_ro_tests {
use super::*;
#[test]
fn the_compat_ro_bits_are_upstreams() {
assert_eq!(compat_ro::FREE_SPACE_TREE, 1 << 0);
assert_eq!(compat_ro::FREE_SPACE_TREE_VALID, 1 << 1);
assert_eq!(compat_ro::VERITY, 1 << 2);
assert_eq!(compat_ro::BLOCK_GROUP_TREE, 1 << 3);
}
#[test]
fn a_volume_with_only_maintained_features_may_be_written() {
for flags in [
0,
compat_ro::FREE_SPACE_TREE,
compat_ro::FREE_SPACE_TREE | compat_ro::FREE_SPACE_TREE_VALID,
] {
refuse_unmaintained_compat_ro(flags)
.unwrap_or_else(|e| panic!("{flags:#x} must be writable: {e:?}"));
}
}
#[test]
fn a_volume_with_an_unmaintained_feature_is_refused_by_name() {
for (flags, needle) in [
(compat_ro::VERITY, "fs-verity"),
(compat_ro::BLOCK_GROUP_TREE, "block group tree"),
(1 << 40, "0x0000010000000000"),
(
compat_ro::FREE_SPACE_TREE | compat_ro::BLOCK_GROUP_TREE | (1 << 7),
"0x0000000000000080",
),
] {
match refuse_unmaintained_compat_ro(flags) {
Err(Error::UnsupportedFeature(m)) => {
assert!(m.contains(needle), "{flags:#x}: {m}");
assert!(m.contains("can be read but not written"), "{flags:#x}: {m}");
}
other => panic!("{flags:#x} gave {other:?}"),
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChecksumType {
Crc32c,
XxHash64,
Sha256,
Blake2b256,
}
impl ChecksumType {
pub fn from_raw(raw: u16) -> Result<Self> {
match raw {
0 => Ok(ChecksumType::Crc32c),
1 => Ok(ChecksumType::XxHash64),
2 => Ok(ChecksumType::Sha256),
3 => Ok(ChecksumType::Blake2b256),
other => Err(Error::UnsupportedChecksum(other)),
}
}
pub fn to_raw(self) -> u16 {
match self {
ChecksumType::Crc32c => 0,
ChecksumType::XxHash64 => 1,
ChecksumType::Sha256 => 2,
ChecksumType::Blake2b256 => 3,
}
}
pub fn digest_len(self) -> usize {
match self {
ChecksumType::Crc32c => 4,
ChecksumType::XxHash64 => 8,
ChecksumType::Sha256 | ChecksumType::Blake2b256 => 32,
}
}
pub fn digest(self, data: &[u8]) -> [u8; CSUM_SIZE] {
let mut out = [0u8; CSUM_SIZE];
match self {
ChecksumType::Crc32c => {
out[..4].copy_from_slice(&crc32c::crc32c(data).to_le_bytes());
}
ChecksumType::XxHash64 => {
let h = twox_hash::XxHash64::oneshot(0, data);
out[..8].copy_from_slice(&h.to_le_bytes());
}
ChecksumType::Sha256 => {
use sha2::Digest;
let h = sha2::Sha256::digest(data);
out.copy_from_slice(&h);
}
ChecksumType::Blake2b256 => {
let h = blake2b_simd::Params::new().hash_length(32).hash(data);
out.copy_from_slice(h.as_bytes());
}
}
out
}
pub fn verify(self, data: &[u8], stored: &[u8]) -> bool {
let n = self.digest_len();
if stored.len() < n {
return false;
}
self.digest(data)[..n] == stored[..n]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DevItem {
pub devid: u64,
pub total_bytes: u64,
pub bytes_used: u64,
pub io_align: u32,
pub io_width: u32,
pub sector_size: u32,
pub dev_type: u64,
pub generation: u64,
pub start_offset: u64,
pub dev_group: u32,
pub seek_speed: u8,
pub bandwidth: u8,
pub uuid: [u8; UUID_SIZE],
pub fsid: [u8; UUID_SIZE],
}
impl DevItem {
pub fn parse(buf: &[u8]) -> Result<Self> {
use dev_item_offsets as o;
if buf.len() < DEV_ITEM_SIZE {
return Err(Error::BadSuperblock(format!(
"dev_item needs {DEV_ITEM_SIZE} bytes, got {}",
buf.len()
)));
}
Ok(DevItem {
devid: le64(buf, o::DEVID),
total_bytes: le64(buf, o::TOTAL_BYTES),
bytes_used: le64(buf, o::BYTES_USED),
io_align: le32(buf, o::IO_ALIGN),
io_width: le32(buf, o::IO_WIDTH),
sector_size: le32(buf, o::SECTOR_SIZE),
dev_type: le64(buf, o::TYPE),
generation: le64(buf, o::GENERATION),
start_offset: le64(buf, o::START_OFFSET),
dev_group: le32(buf, o::DEV_GROUP),
seek_speed: buf[o::SEEK_SPEED],
bandwidth: buf[o::BANDWIDTH],
uuid: uuid_at(buf, o::UUID),
fsid: uuid_at(buf, o::FSID),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Superblock {
pub csum: [u8; CSUM_SIZE],
pub fsid: [u8; UUID_SIZE],
pub bytenr: u64,
pub flags: u64,
pub generation: u64,
pub root: u64,
pub chunk_root: u64,
pub log_root: u64,
pub log_root_transid: u64,
pub total_bytes: u64,
pub bytes_used: u64,
pub root_dir_objectid: u64,
pub num_devices: u64,
pub sectorsize: u32,
pub nodesize: u32,
pub leafsize: u32,
pub stripesize: u32,
pub sys_chunk_array_size: u32,
pub chunk_root_generation: u64,
pub compat_flags: u64,
pub compat_ro_flags: u64,
pub incompat_flags: u64,
pub csum_type: ChecksumType,
pub root_level: u8,
pub chunk_root_level: u8,
pub log_root_level: u8,
pub dev_item: DevItem,
pub label: String,
pub cache_generation: u64,
pub uuid_tree_generation: u64,
pub metadata_uuid: [u8; UUID_SIZE],
pub nr_global_roots: u64,
pub sys_chunk_array: Vec<u8>,
}
#[inline]
pub(crate) fn le16(b: &[u8], off: usize) -> u16 {
u16::from_le_bytes(b[off..off + 2].try_into().expect("2 bytes"))
}
#[inline]
pub(crate) fn le32(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes(b[off..off + 4].try_into().expect("4 bytes"))
}
#[inline]
pub(crate) fn le64(b: &[u8], off: usize) -> u64 {
u64::from_le_bytes(b[off..off + 8].try_into().expect("8 bytes"))
}
#[inline]
pub(crate) fn uuid_at(b: &[u8], off: usize) -> [u8; UUID_SIZE] {
b[off..off + UUID_SIZE].try_into().expect("16 bytes")
}
impl Superblock {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < SUPER_INFO_SIZE {
return Err(Error::BadSuperblock(format!(
"need {SUPER_INFO_SIZE} bytes, got {}",
buf.len()
)));
}
let magic: [u8; 8] = buf[offsets::MAGIC..offsets::MAGIC + 8]
.try_into()
.expect("8 bytes");
if magic != BTRFS_MAGIC {
return Err(Error::NotBtrfs { magic });
}
let csum_type = ChecksumType::from_raw(le16(buf, offsets::CSUM_TYPE))?;
let csum: [u8; CSUM_SIZE] = buf[offsets::CSUM..offsets::CSUM + CSUM_SIZE]
.try_into()
.expect("32 bytes");
let covered = &buf[CSUM_SIZE..SUPER_INFO_SIZE];
if !csum_type.verify(covered, &csum) {
return Err(Error::ChecksumMismatch {
what: "superblock",
offset: le64(buf, offsets::BYTENR),
});
}
let incompat_flags = le64(buf, offsets::INCOMPAT_FLAGS);
let unknown = incompat_flags & !incompat::SUPPORTED;
if unknown != 0 {
return Err(Error::UnsupportedFeature(format!(
"incompatible feature bits {unknown:#018x} not implemented"
)));
}
let sys_len = le32(buf, offsets::SYS_CHUNK_ARRAY_SIZE) as usize;
if sys_len > SYS_CHUNK_ARRAY_SIZE {
return Err(Error::BadSuperblock(format!(
"sys_chunk_array_size {sys_len} exceeds the {SYS_CHUNK_ARRAY_SIZE}-byte array"
)));
}
let sys_chunk_array =
buf[offsets::SYS_CHUNK_ARRAY..offsets::SYS_CHUNK_ARRAY + sys_len].to_vec();
let label_raw = &buf[offsets::LABEL..offsets::LABEL + LABEL_SIZE];
let label_end = label_raw.iter().position(|&b| b == 0).unwrap_or(LABEL_SIZE);
let label = String::from_utf8_lossy(&label_raw[..label_end]).into_owned();
let fsid = uuid_at(buf, offsets::FSID);
let sb = Superblock {
csum,
fsid,
bytenr: le64(buf, offsets::BYTENR),
flags: le64(buf, offsets::FLAGS),
generation: le64(buf, offsets::GENERATION),
root: le64(buf, offsets::ROOT),
chunk_root: le64(buf, offsets::CHUNK_ROOT),
log_root: le64(buf, offsets::LOG_ROOT),
log_root_transid: le64(buf, offsets::LOG_ROOT_TRANSID),
total_bytes: le64(buf, offsets::TOTAL_BYTES),
bytes_used: le64(buf, offsets::BYTES_USED),
root_dir_objectid: le64(buf, offsets::ROOT_DIR_OBJECTID),
num_devices: le64(buf, offsets::NUM_DEVICES),
sectorsize: le32(buf, offsets::SECTORSIZE),
nodesize: le32(buf, offsets::NODESIZE),
leafsize: le32(buf, offsets::LEAFSIZE),
stripesize: le32(buf, offsets::STRIPESIZE),
sys_chunk_array_size: sys_len as u32,
chunk_root_generation: le64(buf, offsets::CHUNK_ROOT_GENERATION),
compat_flags: le64(buf, offsets::COMPAT_FLAGS),
compat_ro_flags: le64(buf, offsets::COMPAT_RO_FLAGS),
incompat_flags,
csum_type,
root_level: buf[offsets::ROOT_LEVEL],
chunk_root_level: buf[offsets::CHUNK_ROOT_LEVEL],
log_root_level: buf[offsets::LOG_ROOT_LEVEL],
dev_item: DevItem::parse(&buf[offsets::DEV_ITEM..offsets::DEV_ITEM + DEV_ITEM_SIZE])?,
label,
cache_generation: le64(buf, offsets::CACHE_GENERATION),
uuid_tree_generation: le64(buf, offsets::UUID_TREE_GENERATION),
metadata_uuid: if le64(buf, offsets::INCOMPAT_FLAGS) & incompat::METADATA_UUID != 0 {
uuid_at(buf, offsets::METADATA_UUID)
} else {
fsid
},
nr_global_roots: le64(buf, offsets::NR_GLOBAL_ROOTS),
sys_chunk_array,
};
sb.validate()?;
Ok(sb)
}
pub fn parse_at(buf: &[u8], offset: u64) -> Result<Self> {
let sb = Self::parse(buf)?;
if sb.bytenr != offset {
return Err(Error::BlockIdentityMismatch {
what: "superblock",
expected: offset,
found: sb.bytenr,
});
}
Ok(sb)
}
fn validate(&self) -> Result<()> {
self.validate_geometry()?;
self.validate_tree_roots()?;
self.validate_sys_chunk_array()?;
self.validate_device_identity()?;
Ok(())
}
fn validate_geometry(&self) -> Result<()> {
let bad = |m: String| Err(Error::BadSuperblock(m));
if !SUPER_OFFSETS.contains(&self.bytenr) {
return bad(format!(
"bytenr {:#x} is not one of the superblock mirror offsets {SUPER_OFFSETS:#x?}",
self.bytenr
));
}
if !self.sectorsize.is_power_of_two()
|| !(MIN_SECTORSIZE..=MAX_METADATA_BLOCKSIZE).contains(&self.sectorsize)
{
return bad(format!(
"sectorsize {} is not a power of two in [{MIN_SECTORSIZE}, {MAX_METADATA_BLOCKSIZE}]",
self.sectorsize
));
}
if !self.nodesize.is_power_of_two()
|| self.nodesize < self.sectorsize
|| self.nodesize > MAX_METADATA_BLOCKSIZE
{
return bad(format!(
"nodesize {} is not a power of two in [sectorsize {}, {MAX_METADATA_BLOCKSIZE}]",
self.nodesize, self.sectorsize
));
}
if self.stripesize != self.sectorsize {
return bad(format!(
"stripesize {} disagrees with sectorsize {}",
self.stripesize, self.sectorsize
));
}
if self.num_devices == 0 {
return bad("num_devices is zero".into());
}
if self.total_bytes == 0 {
return bad("total_bytes is zero".into());
}
if !self.total_bytes.is_multiple_of(u64::from(self.sectorsize)) {
return bad(format!(
"total_bytes {} is not a multiple of sectorsize {}",
self.total_bytes, self.sectorsize
));
}
if self.bytes_used > self.total_bytes {
return bad(format!(
"bytes_used {} exceeds total_bytes {}",
self.bytes_used, self.total_bytes
));
}
Ok(())
}
fn validate_tree_roots(&self) -> Result<()> {
let bad = |m: String| Err(Error::BadSuperblock(m));
for (name, level) in [
("root_level", self.root_level),
("chunk_root_level", self.chunk_root_level),
("log_root_level", self.log_root_level),
] {
if level >= MAX_LEVEL {
return bad(format!(
"{name} {level} is at or above the maximum tree height {MAX_LEVEL}"
));
}
}
for (name, addr) in [
("root", self.root),
("chunk_root", self.chunk_root),
("log_root", self.log_root),
] {
if !addr.is_multiple_of(u64::from(self.sectorsize)) {
return bad(format!(
"{name} {addr:#x} is not sectorsize-aligned ({})",
self.sectorsize
));
}
}
if self.chunk_root == 0 {
return bad("chunk_root is zero — nothing can be mapped without it".into());
}
if self.root == 0 {
return bad("root tree address is zero".into());
}
Ok(())
}
fn validate_sys_chunk_array(&self) -> Result<()> {
let bad = |m: String| Err(Error::BadSuperblock(m));
const MIN_SYS_CHUNK_ARRAY: u32 = 17 + 48 + 32;
if self.sys_chunk_array_size < MIN_SYS_CHUNK_ARRAY {
return bad(format!(
"sys_chunk_array_size {} cannot hold a single chunk mapping (need {MIN_SYS_CHUNK_ARRAY})",
self.sys_chunk_array_size
));
}
Ok(())
}
fn validate_device_identity(&self) -> Result<()> {
let bad = |m: String| Err(Error::BadSuperblock(m));
let identity_in_flux = self.flags
& (super_flags::CHANGING_FSID | super_flags::CHANGING_FSID_V2 | super_flags::SEEDING)
!= 0;
if !identity_in_flux && self.dev_item.fsid != self.node_uuid() {
return bad("dev_item.fsid does not match the volume's own UUID".into());
}
Ok(())
}
pub fn node_uuid(&self) -> [u8; UUID_SIZE] {
if self.incompat_flags & incompat::METADATA_UUID != 0 {
self.metadata_uuid
} else {
self.fsid
}
}
pub fn has_dirty_log(&self) -> bool {
self.log_root != 0
}
pub fn is_seeding(&self) -> bool {
self.flags & super_flags::SEEDING != 0
}
pub fn has_error_flag(&self) -> bool {
self.flags & super_flags::ERROR != 0
}
pub fn is_metadump(&self) -> bool {
self.flags & (super_flags::METADUMP | super_flags::METADUMP_V2) != 0
}
pub fn has_no_holes(&self) -> bool {
self.incompat_flags & incompat::NO_HOLES != 0
}
pub fn has_skinny_metadata(&self) -> bool {
self.incompat_flags & incompat::SKINNY_METADATA != 0
}
pub fn has_mixed_groups(&self) -> bool {
self.incompat_flags & incompat::MIXED_GROUPS != 0
}
pub fn has_free_space_tree(&self) -> bool {
self.compat_ro_flags & (compat_ro::FREE_SPACE_TREE | compat_ro::FREE_SPACE_TREE_VALID)
== (compat_ro::FREE_SPACE_TREE | compat_ro::FREE_SPACE_TREE_VALID)
}
pub fn has_block_group_tree(&self) -> bool {
self.compat_ro_flags & compat_ro::BLOCK_GROUP_TREE != 0
}
}
pub fn read_superblock(dev: &dyn fs_core::BlockRead) -> Result<(Superblock, usize)> {
let size = dev.size_bytes();
let mut best: Option<(Superblock, usize)> = None;
let mut primary_error = None;
for (copy, &offset) in SUPER_OFFSETS.iter().enumerate() {
if offset.saturating_add(SUPER_INFO_SIZE as u64) > size {
continue;
}
let mut buf = vec![0u8; SUPER_INFO_SIZE];
let parsed = dev
.read_at(offset, &mut buf)
.map_err(Error::from)
.and_then(|()| Superblock::parse_at(&buf, offset));
match parsed {
Ok(sb) => {
if best
.as_ref()
.is_none_or(|(b, _)| sb.generation > b.generation)
{
best = Some((sb, copy));
}
}
Err(e) if copy == 0 => primary_error = Some(e),
Err(_) => {}
}
}
best.ok_or_else(|| {
primary_error.unwrap_or_else(|| {
Error::BadSuperblock("the device is too small to hold a superblock".to_string())
})
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) const TEST_FSID: [u8; UUID_SIZE] = [0xA1; UUID_SIZE];
const TEST_DEV_UUID: [u8; UUID_SIZE] = [0xB2; UUID_SIZE];
pub(crate) fn sb_bytes() -> Vec<u8> {
let mut b = vec![0u8; SUPER_INFO_SIZE];
b[offsets::MAGIC..offsets::MAGIC + 8].copy_from_slice(&BTRFS_MAGIC);
b[offsets::FSID..offsets::FSID + UUID_SIZE].copy_from_slice(&TEST_FSID);
put64(&mut b, offsets::BYTENR, SUPER_INFO_OFFSET);
put64(&mut b, offsets::GENERATION, 7);
put64(&mut b, offsets::ROOT, 0x2000_0000);
put64(&mut b, offsets::CHUNK_ROOT, 0x100_0000);
put64(&mut b, offsets::TOTAL_BYTES, 1 << 30);
put64(&mut b, offsets::BYTES_USED, 1 << 20);
put64(&mut b, offsets::ROOT_DIR_OBJECTID, 6);
put64(&mut b, offsets::NUM_DEVICES, 1);
put32(&mut b, offsets::SECTORSIZE, 4096);
put32(&mut b, offsets::NODESIZE, 16384);
put32(&mut b, offsets::LEAFSIZE, 16384);
put32(&mut b, offsets::STRIPESIZE, 4096);
put32(&mut b, offsets::SYS_CHUNK_ARRAY_SIZE, 97);
put64(&mut b, offsets::CHUNK_ROOT_GENERATION, 7);
put64(
&mut b,
offsets::INCOMPAT_FLAGS,
incompat::MIXED_BACKREF | incompat::BIG_METADATA | incompat::SKINNY_METADATA,
);
put16(&mut b, offsets::CSUM_TYPE, 0);
b[offsets::ROOT_LEVEL] = 1;
b[offsets::CHUNK_ROOT_LEVEL] = 0;
b[offsets::LOG_ROOT_LEVEL] = 0;
let d = offsets::DEV_ITEM;
put64(&mut b, d + dev_item_offsets::DEVID, 1);
put64(&mut b, d + dev_item_offsets::TOTAL_BYTES, 1 << 30);
put64(&mut b, d + dev_item_offsets::BYTES_USED, 1 << 20);
put32(&mut b, d + dev_item_offsets::IO_ALIGN, 4096);
put32(&mut b, d + dev_item_offsets::IO_WIDTH, 4096);
put32(&mut b, d + dev_item_offsets::SECTOR_SIZE, 4096);
b[d + dev_item_offsets::UUID..d + dev_item_offsets::UUID + UUID_SIZE]
.copy_from_slice(&TEST_DEV_UUID);
b[d + dev_item_offsets::FSID..d + dev_item_offsets::FSID + UUID_SIZE]
.copy_from_slice(&TEST_FSID);
b[offsets::LABEL..offsets::LABEL + 5].copy_from_slice(b"disks");
reseal(&mut b);
b
}
pub(crate) fn reseal(b: &mut [u8]) {
let t = ChecksumType::from_raw(le16(b, offsets::CSUM_TYPE)).unwrap_or(ChecksumType::Crc32c);
let digest = t.digest(&b[CSUM_SIZE..SUPER_INFO_SIZE]);
b[..CSUM_SIZE].copy_from_slice(&digest);
}
fn put16(b: &mut [u8], off: usize, v: u16) {
b[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
fn put32(b: &mut [u8], off: usize, v: u32) {
b[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
pub(crate) fn put64(b: &mut [u8], off: usize, v: u64) {
b[off..off + 8].copy_from_slice(&v.to_le_bytes());
}
#[test]
fn magic_bytes_and_u64_form_agree() {
assert_eq!(u64::from_le_bytes(BTRFS_MAGIC), BTRFS_MAGIC_LE64);
assert_eq!(&BTRFS_MAGIC, b"_BHRfS_M");
}
#[test]
fn offsets_are_contiguous_and_end_where_the_format_says() {
assert_eq!(offsets::CSUM + CSUM_SIZE, offsets::FSID);
assert_eq!(offsets::FSID + UUID_SIZE, offsets::BYTENR);
assert_eq!(offsets::BYTENR + 8, offsets::FLAGS);
assert_eq!(offsets::FLAGS + 8, offsets::MAGIC);
assert_eq!(offsets::MAGIC + 8, offsets::GENERATION);
assert_eq!(offsets::GENERATION + 8, offsets::ROOT);
assert_eq!(offsets::ROOT + 8, offsets::CHUNK_ROOT);
assert_eq!(offsets::CHUNK_ROOT + 8, offsets::LOG_ROOT);
assert_eq!(offsets::LOG_ROOT + 8, offsets::LOG_ROOT_TRANSID);
assert_eq!(offsets::LOG_ROOT_TRANSID + 8, offsets::TOTAL_BYTES);
assert_eq!(offsets::TOTAL_BYTES + 8, offsets::BYTES_USED);
assert_eq!(offsets::BYTES_USED + 8, offsets::ROOT_DIR_OBJECTID);
assert_eq!(offsets::ROOT_DIR_OBJECTID + 8, offsets::NUM_DEVICES);
assert_eq!(offsets::NUM_DEVICES + 8, offsets::SECTORSIZE);
assert_eq!(offsets::SECTORSIZE + 4, offsets::NODESIZE);
assert_eq!(offsets::NODESIZE + 4, offsets::LEAFSIZE);
assert_eq!(offsets::LEAFSIZE + 4, offsets::STRIPESIZE);
assert_eq!(offsets::STRIPESIZE + 4, offsets::SYS_CHUNK_ARRAY_SIZE);
assert_eq!(
offsets::SYS_CHUNK_ARRAY_SIZE + 4,
offsets::CHUNK_ROOT_GENERATION
);
assert_eq!(offsets::CHUNK_ROOT_GENERATION + 8, offsets::COMPAT_FLAGS);
assert_eq!(offsets::COMPAT_FLAGS + 8, offsets::COMPAT_RO_FLAGS);
assert_eq!(offsets::COMPAT_RO_FLAGS + 8, offsets::INCOMPAT_FLAGS);
assert_eq!(offsets::INCOMPAT_FLAGS + 8, offsets::CSUM_TYPE);
assert_eq!(offsets::CSUM_TYPE + 2, offsets::ROOT_LEVEL);
assert_eq!(offsets::ROOT_LEVEL + 1, offsets::CHUNK_ROOT_LEVEL);
assert_eq!(offsets::CHUNK_ROOT_LEVEL + 1, offsets::LOG_ROOT_LEVEL);
assert_eq!(offsets::LOG_ROOT_LEVEL + 1, offsets::DEV_ITEM);
assert_eq!(offsets::DEV_ITEM + DEV_ITEM_SIZE, offsets::LABEL);
assert_eq!(offsets::LABEL + LABEL_SIZE, offsets::CACHE_GENERATION);
assert_eq!(offsets::CACHE_GENERATION + 8, offsets::UUID_TREE_GENERATION);
assert_eq!(offsets::UUID_TREE_GENERATION + 8, offsets::METADATA_UUID);
assert_eq!(offsets::METADATA_UUID + UUID_SIZE, offsets::NR_GLOBAL_ROOTS);
assert_eq!(offsets::NR_GLOBAL_ROOTS + 8 + 216, offsets::SYS_CHUNK_ARRAY);
assert_eq!(
offsets::SYS_CHUNK_ARRAY + SYS_CHUNK_ARRAY_SIZE,
offsets::SUPER_ROOTS
);
assert_eq!(offsets::SUPER_ROOTS + 4 * 168 + 565, SUPER_INFO_SIZE);
}
#[test]
fn dev_item_offsets_close_on_its_size() {
assert_eq!(dev_item_offsets::FSID + UUID_SIZE, DEV_ITEM_SIZE);
}
#[test]
fn parses_a_valid_superblock() {
let sb = Superblock::parse(&sb_bytes()).unwrap();
assert_eq!(sb.bytenr, SUPER_INFO_OFFSET);
assert_eq!(sb.sectorsize, 4096);
assert_eq!(sb.nodesize, 16384);
assert_eq!(sb.num_devices, 1);
assert_eq!(sb.generation, 7);
assert_eq!(sb.root, 0x2000_0000);
assert_eq!(sb.chunk_root, 0x100_0000);
assert_eq!(sb.total_bytes, 1 << 30);
assert_eq!(sb.csum_type, ChecksumType::Crc32c);
assert_eq!(sb.label, "disks");
assert_eq!(sb.fsid, TEST_FSID);
assert_eq!(sb.dev_item.devid, 1);
assert_eq!(sb.dev_item.uuid, TEST_DEV_UUID);
assert!(!sb.has_dirty_log());
}
#[test]
fn parse_at_accepts_the_matching_offset() {
let sb = Superblock::parse_at(&sb_bytes(), SUPER_INFO_OFFSET).unwrap();
assert_eq!(sb.bytenr, SUPER_INFO_OFFSET);
}
#[test]
fn parse_at_rejects_a_misdirected_read() {
assert!(matches!(
Superblock::parse_at(&sb_bytes(), SUPER_OFFSETS[1]),
Err(Error::BlockIdentityMismatch { .. })
));
}
#[test]
fn rejects_wrong_magic() {
let mut b = sb_bytes();
b[offsets::MAGIC..offsets::MAGIC + 8].copy_from_slice(b"NOTBTRFS");
reseal(&mut b);
assert!(matches!(Superblock::parse(&b), Err(Error::NotBtrfs { .. })));
}
#[test]
fn rejects_byte_reversed_magic() {
let mut b = sb_bytes();
let mut rev = BTRFS_MAGIC;
rev.reverse();
b[offsets::MAGIC..offsets::MAGIC + 8].copy_from_slice(&rev);
reseal(&mut b);
assert!(matches!(Superblock::parse(&b), Err(Error::NotBtrfs { .. })));
}
#[test]
fn rejects_short_buffer() {
assert!(matches!(
Superblock::parse(&[0u8; 512]),
Err(Error::BadSuperblock(_))
));
assert!(matches!(
Superblock::parse(&vec![0u8; SUPER_INFO_SIZE - 1]),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_a_corrupt_checksum() {
let mut b = sb_bytes();
b[offsets::GENERATION] ^= 0xFF;
assert!(matches!(
Superblock::parse(&b),
Err(Error::ChecksumMismatch { .. })
));
}
#[test]
fn checksum_covers_the_whole_structure_not_just_parsed_fields() {
let mut b = sb_bytes();
b[SUPER_INFO_SIZE - 1] ^= 0xFF;
assert!(matches!(
Superblock::parse(&b),
Err(Error::ChecksumMismatch { .. })
));
}
#[test]
fn crc32c_ignores_the_unused_tail_of_the_csum_field() {
let mut b = sb_bytes();
b[8] = 0xEE;
b[31] = 0xEE;
assert!(Superblock::parse(&b).is_ok());
}
#[test]
fn parses_every_supported_checksum_type() {
for (raw, want) in [
(0u16, ChecksumType::Crc32c),
(1, ChecksumType::XxHash64),
(2, ChecksumType::Sha256),
(3, ChecksumType::Blake2b256),
] {
let mut b = sb_bytes();
put16(&mut b, offsets::CSUM_TYPE, raw);
reseal(&mut b);
let sb = Superblock::parse(&b).unwrap_or_else(|e| panic!("csum_type {raw}: {e}"));
assert_eq!(sb.csum_type, want);
assert_eq!(sb.csum_type.to_raw(), raw);
}
}
#[test]
fn a_checksum_from_the_wrong_algorithm_is_rejected() {
let mut b = sb_bytes();
put16(&mut b, offsets::CSUM_TYPE, 2);
assert!(matches!(
Superblock::parse(&b),
Err(Error::ChecksumMismatch { .. })
));
}
#[test]
fn rejects_unknown_checksum_type() {
let mut b = sb_bytes();
put16(&mut b, offsets::CSUM_TYPE, 9);
assert!(matches!(
Superblock::parse(&b),
Err(Error::UnsupportedChecksum(9))
));
}
#[test]
fn rejects_unknown_incompat_bit() {
let mut b = sb_bytes();
put64(&mut b, offsets::INCOMPAT_FLAGS, 1 << 40);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::UnsupportedFeature(_))
));
}
#[test]
fn rejects_known_but_unimplemented_incompat_features() {
for bit in [
incompat::RAID56,
incompat::ZONED,
incompat::EXTENT_TREE_V2,
incompat::RAID_STRIPE_TREE,
incompat::SIMPLE_QUOTA,
incompat::REMAP_TREE,
] {
let mut b = sb_bytes();
put64(
&mut b,
offsets::INCOMPAT_FLAGS,
incompat::MIXED_BACKREF | bit,
);
reseal(&mut b);
assert!(
matches!(Superblock::parse(&b), Err(Error::UnsupportedFeature(_))),
"incompat bit {bit:#x} should have been refused"
);
}
}
#[test]
fn accepts_every_supported_incompat_bit_together() {
let mut b = sb_bytes();
put64(&mut b, offsets::INCOMPAT_FLAGS, incompat::SUPPORTED);
b[offsets::METADATA_UUID..offsets::METADATA_UUID + UUID_SIZE].copy_from_slice(&TEST_FSID);
reseal(&mut b);
assert!(Superblock::parse(&b).is_ok());
}
#[test]
fn unknown_compat_ro_bits_are_tolerated() {
let mut b = sb_bytes();
put64(&mut b, offsets::COMPAT_RO_FLAGS, 1 << 60);
reseal(&mut b);
assert!(Superblock::parse(&b).is_ok());
}
#[test]
fn reports_free_space_tree_and_block_group_tree() {
let mut b = sb_bytes();
put64(
&mut b,
offsets::COMPAT_RO_FLAGS,
compat_ro::FREE_SPACE_TREE
| compat_ro::FREE_SPACE_TREE_VALID
| compat_ro::BLOCK_GROUP_TREE,
);
reseal(&mut b);
let sb = Superblock::parse(&b).unwrap();
assert!(sb.has_free_space_tree());
assert!(sb.has_block_group_tree());
}
#[test]
fn rejects_bytenr_that_is_not_a_mirror_offset() {
let mut b = sb_bytes();
put64(&mut b, offsets::BYTENR, 0x1234);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn accepts_each_mirror_offset() {
for off in SUPER_OFFSETS {
let mut b = sb_bytes();
put64(&mut b, offsets::BYTENR, off);
reseal(&mut b);
assert_eq!(Superblock::parse_at(&b, off).unwrap().bytenr, off);
}
}
#[test]
fn rejects_bad_sectorsize() {
for bad in [0u32, 512, 3000, 1 << 17] {
let mut b = sb_bytes();
put32(&mut b, offsets::SECTORSIZE, bad);
put32(&mut b, offsets::STRIPESIZE, bad);
reseal(&mut b);
assert!(
matches!(Superblock::parse(&b), Err(Error::BadSuperblock(_))),
"sectorsize {bad} should have been refused"
);
}
}
#[test]
fn rejects_nodesize_below_sectorsize() {
let mut b = sb_bytes();
put32(&mut b, offsets::NODESIZE, 2048);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_nodesize_above_the_metadata_maximum() {
let mut b = sb_bytes();
put32(&mut b, offsets::NODESIZE, 1 << 17);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_stripesize_disagreeing_with_sectorsize() {
let mut b = sb_bytes();
put32(&mut b, offsets::STRIPESIZE, 8192);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_zero_devices() {
let mut b = sb_bytes();
put64(&mut b, offsets::NUM_DEVICES, 0);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_zero_or_misaligned_total_bytes() {
for bad in [0u64, (1 << 30) + 1] {
let mut b = sb_bytes();
put64(&mut b, offsets::TOTAL_BYTES, bad);
reseal(&mut b);
assert!(
matches!(Superblock::parse(&b), Err(Error::BadSuperblock(_))),
"total_bytes {bad} should have been refused"
);
}
}
#[test]
fn rejects_bytes_used_exceeding_total() {
let mut b = sb_bytes();
put64(&mut b, offsets::BYTES_USED, (1u64 << 30) + 4096);
put64(&mut b, offsets::TOTAL_BYTES, 1 << 30);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_impossible_tree_heights() {
for off in [
offsets::ROOT_LEVEL,
offsets::CHUNK_ROOT_LEVEL,
offsets::LOG_ROOT_LEVEL,
] {
let mut b = sb_bytes();
b[off] = MAX_LEVEL;
reseal(&mut b);
assert!(
matches!(Superblock::parse(&b), Err(Error::BadSuperblock(_))),
"level field at {off:#x} should have been refused"
);
}
}
#[test]
fn rejects_unaligned_tree_roots() {
for off in [offsets::ROOT, offsets::CHUNK_ROOT, offsets::LOG_ROOT] {
let mut b = sb_bytes();
put64(&mut b, off, 0x2000_0001);
reseal(&mut b);
assert!(
matches!(Superblock::parse(&b), Err(Error::BadSuperblock(_))),
"unaligned address at {off:#x} should have been refused"
);
}
}
#[test]
fn rejects_zero_chunk_root() {
let mut b = sb_bytes();
put64(&mut b, offsets::CHUNK_ROOT, 0);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_sys_chunk_array_size_out_of_range() {
for bad in [0u32, 96, (SYS_CHUNK_ARRAY_SIZE + 1) as u32] {
let mut b = sb_bytes();
put32(&mut b, offsets::SYS_CHUNK_ARRAY_SIZE, bad);
reseal(&mut b);
assert!(
matches!(Superblock::parse(&b), Err(Error::BadSuperblock(_))),
"sys_chunk_array_size {bad} should have been refused"
);
}
}
#[test]
fn copies_out_exactly_the_valid_prefix_of_the_sys_chunk_array() {
let mut b = sb_bytes();
put32(&mut b, offsets::SYS_CHUNK_ARRAY_SIZE, 200);
for (i, slot) in b[offsets::SYS_CHUNK_ARRAY..offsets::SYS_CHUNK_ARRAY + 300]
.iter_mut()
.enumerate()
{
*slot = (i % 251) as u8;
}
reseal(&mut b);
let sb = Superblock::parse(&b).unwrap();
assert_eq!(sb.sys_chunk_array.len(), 200);
assert_eq!(sb.sys_chunk_array[0], 0);
assert_eq!(sb.sys_chunk_array[199], 199);
}
#[test]
fn rejects_dev_item_fsid_mismatch() {
let mut b = sb_bytes();
let d = offsets::DEV_ITEM + dev_item_offsets::FSID;
b[d..d + UUID_SIZE].copy_from_slice(&[0xCC; UUID_SIZE]);
reseal(&mut b);
assert!(matches!(
Superblock::parse(&b),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn tolerates_dev_item_fsid_mismatch_while_identity_is_in_flux() {
for flag in [
super_flags::SEEDING,
super_flags::CHANGING_FSID,
super_flags::CHANGING_FSID_V2,
] {
let mut b = sb_bytes();
let d = offsets::DEV_ITEM + dev_item_offsets::FSID;
b[d..d + UUID_SIZE].copy_from_slice(&[0xCC; UUID_SIZE]);
put64(&mut b, offsets::FLAGS, flag);
reseal(&mut b);
assert!(
Superblock::parse(&b).is_ok(),
"flag {flag:#x} should have suspended the fsid identity check"
);
}
}
#[test]
fn node_uuid_follows_the_metadata_uuid_feature() {
let plain = Superblock::parse(&sb_bytes()).unwrap();
assert_eq!(plain.node_uuid(), plain.fsid);
let mut b = sb_bytes();
put64(
&mut b,
offsets::INCOMPAT_FLAGS,
incompat::MIXED_BACKREF | incompat::METADATA_UUID,
);
let meta = [0x5A; UUID_SIZE];
b[offsets::METADATA_UUID..offsets::METADATA_UUID + UUID_SIZE].copy_from_slice(&meta);
let d = offsets::DEV_ITEM + dev_item_offsets::FSID;
b[d..d + UUID_SIZE].copy_from_slice(&meta);
reseal(&mut b);
let sb = Superblock::parse(&b).unwrap();
assert_eq!(sb.node_uuid(), meta);
assert_ne!(sb.node_uuid(), sb.fsid);
}
#[test]
fn reports_a_dirty_log() {
let mut b = sb_bytes();
put64(&mut b, offsets::LOG_ROOT, 0x3000_0000);
reseal(&mut b);
let sb = Superblock::parse(&b).unwrap();
assert!(sb.has_dirty_log());
}
#[test]
fn reports_flag_bits() {
let mut b = sb_bytes();
put64(
&mut b,
offsets::FLAGS,
super_flags::ERROR | super_flags::METADUMP_V2,
);
reseal(&mut b);
let sb = Superblock::parse(&b).unwrap();
assert!(sb.has_error_flag());
assert!(sb.is_metadump());
assert!(!sb.is_seeding());
}
#[test]
fn label_is_trimmed_at_the_first_nul_and_a_full_label_survives() {
let sb = Superblock::parse(&sb_bytes()).unwrap();
assert_eq!(sb.label, "disks");
let mut b = sb_bytes();
b[offsets::LABEL..offsets::LABEL + LABEL_SIZE].copy_from_slice(&[b'x'; LABEL_SIZE]);
reseal(&mut b);
assert_eq!(Superblock::parse(&b).unwrap().label.len(), LABEL_SIZE);
}
#[test]
fn checksum_digest_lengths_match_the_algorithms() {
assert_eq!(ChecksumType::Crc32c.digest_len(), 4);
assert_eq!(ChecksumType::XxHash64.digest_len(), 8);
assert_eq!(ChecksumType::Sha256.digest_len(), 32);
assert_eq!(ChecksumType::Blake2b256.digest_len(), 32);
for t in [
ChecksumType::Crc32c,
ChecksumType::XxHash64,
ChecksumType::Sha256,
ChecksumType::Blake2b256,
] {
let d = t.digest(b"btrfs");
assert_eq!(d.len(), CSUM_SIZE);
assert!(d[t.digest_len()..].iter().all(|&b| b == 0));
assert!(t.verify(b"btrfs", &d));
assert!(!t.verify(b"btrfz", &d));
assert!(!t.verify(b"btrfs", &d[..t.digest_len() - 1]));
}
}
#[test]
fn crc32c_digest_is_stored_little_endian() {
let raw = crc32c::crc32c(b"btrfs");
let d = ChecksumType::Crc32c.digest(b"btrfs");
assert_eq!(&d[..4], &raw.to_le_bytes());
assert_ne!(&d[..4], &raw.to_be_bytes());
}
}
#[cfg(test)]
mod endian_tests {
use super::{le16, le32, le64};
#[test]
fn the_low_byte_comes_first() {
let b = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
assert_eq!(le16(&b, 0), 0x0201);
assert_eq!(le32(&b, 0), 0x0403_0201);
assert_eq!(le64(&b, 0), 0x0807_0605_0403_0201);
}
#[test]
fn the_offset_counts_bytes() {
let b = [0x00u8, 0xAA, 0xBB, 0x00];
assert_eq!(le16(&b, 1), 0xBBAA);
}
#[test]
fn the_widest_values_survive() {
let b = [0xFFu8; 8];
assert_eq!(le16(&b, 0), u16::MAX);
assert_eq!(le32(&b, 0), u32::MAX);
assert_eq!(le64(&b, 0), u64::MAX);
}
}