use std::io::Read;
use crate::bytes::{le_u64, u8_at};
use crate::error::BtrfsError;
use crate::fstree::{read_by_path, INODE_ITEM_KEY};
use crate::node::{read_node, ChunkMap, Node};
use crate::superblock::Superblock;
pub const EXTENT_DATA_KEY: u8 = 108;
const EXTENT_HEADER_LEN: usize = 21;
mod ext_off {
pub const RAM_BYTES: usize = 8;
pub const COMPRESSION: usize = 16;
pub const TYPE: usize = 20;
pub const DISK_BYTENR: usize = 21;
pub const OFFSET: usize = 37;
pub const NUM_BYTES: usize = 45;
}
const EXTENT_TYPE_INLINE: u8 = 0;
const EXTENT_TYPE_REG: u8 = 1;
const EXTENT_TYPE_PREALLOC: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Compression {
None,
Zlib,
Lzo,
Zstd,
Other(u8),
}
impl Compression {
#[must_use]
pub fn from_byte(b: u8) -> Self {
match b {
0 => Compression::None,
1 => Compression::Zlib,
2 => Compression::Lzo,
3 => Compression::Zstd,
other => Compression::Other(other),
}
}
}
const LOGICAL_ALLOC_FLOOR: u64 = 64 * 1024 * 1024;
pub fn read_file_from_leaf(
leaf: &Node,
image: &[u8],
map: &ChunkMap,
sectorsize: u32,
ino: u64,
) -> Result<Vec<u8>, BtrfsError> {
let size = leaf
.leaf_items()
.find(|(k, _)| k.objectid == ino && k.key_type == INODE_ITEM_KEY)
.map(|(_, data)| le_u64(data, crate::fstree::INODE_SIZE_OFFSET))
.ok_or(BtrfsError::Truncated {
structure: "INODE_ITEM for the requested objectid (read_file)",
need: ino as usize,
have: 0,
})?;
let mut extents: Vec<(u64, &[u8])> = leaf
.leaf_items()
.filter(|(k, _)| k.objectid == ino && k.key_type == EXTENT_DATA_KEY)
.map(|(k, data)| (k.offset, data))
.collect();
extents.sort_by_key(|(file_off, _)| *file_off);
let image_len = image.len() as u64;
let logical_bound = image_len.max(LOGICAL_ALLOC_FLOOR);
guard_alloc("inode size", size, logical_bound)?;
let mut out: Vec<u8> = Vec::new();
for (_file_off, data) in extents {
let etype = u8_at(data, ext_off::TYPE);
let ram_bytes = le_u64(data, ext_off::RAM_BYTES);
let compression = Compression::from_byte(u8_at(data, ext_off::COMPRESSION));
match etype {
EXTENT_TYPE_INLINE => {
let payload = data.get(EXTENT_HEADER_LEN..).unwrap_or(&[]);
let bytes = decompress_extent(compression, payload, ram_bytes, sectorsize)?;
out.extend_from_slice(&bytes);
}
EXTENT_TYPE_REG | EXTENT_TYPE_PREALLOC => {
let disk_bytenr = le_u64(data, ext_off::DISK_BYTENR);
let ext_offset = le_u64(data, ext_off::OFFSET);
let num_bytes = le_u64(data, ext_off::NUM_BYTES);
if disk_bytenr == 0 {
guard_alloc("hole num_bytes", num_bytes, logical_bound)?;
out.resize(out.len() + num_bytes as usize, 0);
continue;
}
let bytes = read_regular_extent(
image,
map,
sectorsize,
compression,
disk_bytenr,
ext_offset,
num_bytes,
ram_bytes,
)?;
out.extend_from_slice(&bytes);
}
_ => {
return Err(BtrfsError::Truncated {
structure: "btrfs_file_extent_item type (unknown extent type)",
need: etype as usize,
have: 0,
});
}
}
}
let size = size as usize;
if out.len() > size {
out.truncate(size);
} else if out.len() < size {
out.resize(size, 0);
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn read_regular_extent(
image: &[u8],
map: &ChunkMap,
sectorsize: u32,
compression: Compression,
disk_bytenr: u64,
ext_offset: u64,
num_bytes: u64,
ram_bytes: u64,
) -> Result<Vec<u8>, BtrfsError> {
let image_len = image.len() as u64;
guard_alloc("extent num_bytes", num_bytes, image_len)?;
let (_devid, phys) = map
.logical_to_physical(disk_bytenr)
.ok_or(BtrfsError::Truncated {
structure: "extent disk_bytenr (no chunk mapping)",
need: disk_bytenr as usize,
have: 0,
})?;
if compression == Compression::None {
let start = phys.saturating_add(ext_offset) as usize;
let end = start.saturating_add(num_bytes as usize);
let slice = image.get(start..end).ok_or(BtrfsError::Truncated {
structure: "regular extent data (out of image)",
need: end,
have: image.len(),
})?;
return Ok(slice.to_vec());
}
let start = phys as usize;
let src = image.get(start..).ok_or(BtrfsError::Truncated {
structure: "compressed extent source (out of image)",
need: start,
have: image.len(),
})?;
let whole = decompress_extent(compression, src, ram_bytes, sectorsize)?;
let win_start = ext_offset as usize;
let win_end = win_start.saturating_add(num_bytes as usize);
let window = whole
.get(win_start..win_end.min(whole.len()))
.unwrap_or(&[]);
Ok(window.to_vec())
}
pub fn decompress_extent(
algo: Compression,
src: &[u8],
ram_bytes: u64,
sectorsize: u32,
) -> Result<Vec<u8>, BtrfsError> {
if algo == Compression::None {
let take = (ram_bytes as usize).min(src.len());
return Ok(src.get(..take).unwrap_or(src).to_vec());
}
let bound = (src.len() as u64)
.saturating_mul(1024)
.saturating_add(1 << 20);
guard_alloc("compressed ram_bytes", ram_bytes, bound)?;
let cap = ram_bytes as usize;
match algo {
Compression::Zlib => decompress_zlib(src, cap),
Compression::Zstd => decompress_zstd(src, cap),
Compression::Lzo => decompress_lzo_btrfs(src, cap, sectorsize),
other => Err(BtrfsError::Truncated {
structure: "extent compression algorithm (unsupported codec)",
need: usize::from(unsupported_codec_byte(other)),
have: 0,
}),
}
}
fn unsupported_codec_byte(algo: Compression) -> u8 {
match algo {
Compression::Other(b) => b,
_ => 0, }
}
fn decompress_zlib(src: &[u8], cap: usize) -> Result<Vec<u8>, BtrfsError> {
let dec = flate2::read::ZlibDecoder::new(src);
let mut out = Vec::with_capacity(cap);
match dec.take(cap as u64).read_to_end(&mut out) {
Ok(_) => Ok(out),
Err(_) => Err(BtrfsError::Truncated {
structure: "zlib extent stream (decompression failed)",
need: cap,
have: out.len(),
}),
}
}
fn decompress_zstd(src: &[u8], cap: usize) -> Result<Vec<u8>, BtrfsError> {
let dec = ruzstd::decoding::StreamingDecoder::new(src).map_err(|_| BtrfsError::Truncated {
structure: "zstd extent frame (bad frame header)",
need: cap,
have: 0,
})?;
let mut out = Vec::with_capacity(cap);
match dec.take(cap as u64).read_to_end(&mut out) {
Ok(_) => Ok(out),
Err(_) => Err(BtrfsError::Truncated {
structure: "zstd extent frame (decompression failed)",
need: cap,
have: out.len(),
}),
}
}
fn decompress_lzo_btrfs(src: &[u8], cap: usize, sectorsize: u32) -> Result<Vec<u8>, BtrfsError> {
const LZO_LEN: usize = 4;
let sectorsize = (sectorsize as usize).max(1);
let total = le_u64(&le32_padded(src, 0), 0) as usize; let total = total.min(src.len()).max(LZO_LEN);
let mut cur = LZO_LEN;
let mut out: Vec<u8> = Vec::with_capacity(cap);
let seg_out_cap = sectorsize
.saturating_add(sectorsize / 16)
.saturating_add(64);
while cur < total && out.len() < cap {
if cur % sectorsize + LZO_LEN > sectorsize {
let next = cur
.checked_add(sectorsize - cur % sectorsize)
.unwrap_or(total);
cur = next;
if cur >= total {
break; }
}
let Some(seg_len_bytes) = src.get(cur..cur + LZO_LEN) else {
break; };
let seg_len = u32::from_le_bytes([
seg_len_bytes[0],
seg_len_bytes[1],
seg_len_bytes[2],
seg_len_bytes[3],
]) as usize;
cur += LZO_LEN;
if seg_len == 0 {
break; }
let Some(seg) = src.get(cur..cur.saturating_add(seg_len).min(src.len())) else {
break; };
cur = cur.saturating_add(seg_len);
let mut dst = vec![0u8; seg_out_cap];
match lzo::decompress_into(seg, &mut dst) {
Ok(n) => {
let want = (cap - out.len()).min(n);
out.extend_from_slice(dst.get(..want).unwrap_or(&dst[..n]));
}
Err(_) => {
return Err(BtrfsError::Truncated {
structure: "LZO extent segment (decompression failed)",
need: seg_len,
have: out.len(),
});
}
}
}
out.truncate(cap);
Ok(out)
}
fn le32_padded(src: &[u8], off: usize) -> [u8; 8] {
let mut b = [0u8; 8];
if let Some(s) = src.get(off..off + 4) {
b[..4].copy_from_slice(s);
}
b
}
fn guard_alloc(field: &'static str, claimed: u64, bound: u64) -> Result<(), BtrfsError> {
if claimed > bound {
return Err(BtrfsError::AllocationBomb {
field,
claimed,
bound,
});
}
Ok(())
}
pub fn read_file(
image: &[u8],
sb: &Superblock,
map: &ChunkMap,
ino: u64,
) -> Result<Vec<u8>, BtrfsError> {
let leaf = fs_tree_leaf(image, sb, map)?;
read_file_from_leaf(&leaf, image, map, sb.sectorsize, ino)
}
pub fn read_by_path_content(
image: &[u8],
sb: &Superblock,
map: &ChunkMap,
path: &str,
) -> Result<Vec<u8>, BtrfsError> {
let leaf = fs_tree_leaf(image, sb, map)?;
let (ino, _inode) = read_by_path(&leaf, path).ok_or(BtrfsError::Truncated {
structure: "path (unresolved in FS_TREE)",
need: path.len(),
have: 0,
})?;
read_file_from_leaf(&leaf, image, map, sb.sectorsize, ino)
}
fn fs_tree_leaf(image: &[u8], sb: &Superblock, map: &ChunkMap) -> Result<Node, BtrfsError> {
let root = crate::fstree::fs_tree_root(image, sb, map)?;
read_node(image, sb, map, root.bytenr)
}