use anyhow::{Context, Result, ensure};
use sha2::{Digest, Sha256};
const GEOMETRY_OFFSET: u64 = 4096;
const GEOMETRY_SIZE: usize = 64;
const LP_HEADER_MAGIC: u32 = 0x414C5030;
const LP_GEOMETRY_MAGIC: u32 = 0x616C4467;
const LP_GEOMETRY_MAGIC_BYTES: [u8; 4] = *b"gDla";
pub const SECTOR_SIZE: u64 = 512;
const MAX_ENTRIES: usize = 4096;
pub const MAX_IMAGE_BYTES: u64 = 1u64 << 40;
const METADATA_ALIGN: u64 = 4096;
fn le16(b: &[u8], at: usize) -> u16 {
u16::from_le_bytes([b[at], b[at + 1]])
}
fn le32(b: &[u8], at: usize) -> u32 {
u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}
fn le64(b: &[u8], at: usize) -> u64 {
u64::from_le_bytes([
b[at],
b[at + 1],
b[at + 2],
b[at + 3],
b[at + 4],
b[at + 5],
b[at + 6],
b[at + 7],
])
}
type Sha256Arr = [u8; 32];
#[allow(dead_code)]
struct Geometry {
magic: u32,
struct_size: u32,
checksum: Sha256Arr,
metadata_max_size: u32,
metadata_slot_count: u32,
logical_block_size: u32,
}
fn parse_geometry(b: &[u8]) -> Result<Geometry> {
ensure!(!b.is_empty(), "super image is empty");
ensure!(
b.len() >= GEOMETRY_SIZE,
"super image is too short for an LP geometry (need {GEOMETRY_SIZE} bytes, got {})",
b.len()
);
let g = Geometry {
magic: le32(b, 0),
struct_size: le32(b, 4),
checksum: read_sha256(b, 8),
metadata_max_size: le32(b, 40),
metadata_slot_count: le32(b, 44),
logical_block_size: le32(b, 48),
};
ensure!(
g.magic == LP_GEOMETRY_MAGIC,
"super image: bad LP geometry magic {:#010x}",
g.magic
);
ensure!(
g.struct_size as usize >= GEOMETRY_SIZE,
"LP geometry struct_size {} is too small",
g.struct_size
);
ensure!(
g.logical_block_size != 0 && g.logical_block_size.is_multiple_of(512),
"LP geometry: invalid logical block size {}",
g.logical_block_size
);
Ok(g)
}
fn verify_geometry_checksum(g: &Geometry, b: &[u8]) -> Result<()> {
let mut copy = b.to_vec();
let start = 8;
let end = start + 32;
for byte in &mut copy[start..end] {
*byte = 0;
}
let mut h = Sha256::new();
h.update(©);
let actual = h.finalize();
ensure!(
&g.checksum[..] == actual.as_slice(),
"LP geometry: checksum mismatch"
);
Ok(())
}
fn verify_header_checksum(h: &MetadataHeader, hdr_bytes: &[u8]) -> Result<()> {
ensure!(
hdr_bytes.len() >= h.header_size as usize,
"LP metadata: not enough bytes for header_size {} (got {})",
h.header_size,
hdr_bytes.len()
);
let mut copy = hdr_bytes[..h.header_size as usize].to_vec();
for byte in &mut copy[12..44] {
*byte = 0;
}
let mut hash = Sha256::new();
hash.update(©);
let actual = hash.finalize();
ensure!(
&h.header_checksum[..] == actual.as_slice(),
"LP metadata: header checksum mismatch"
);
Ok(())
}
fn verify_tables_checksum(h: &MetadataHeader, tables_bytes: &[u8]) -> Result<()> {
ensure!(
tables_bytes.len() == h.tables_size as usize,
"LP metadata: tables region is {} bytes but header says {}",
tables_bytes.len(),
h.tables_size
);
let mut hash = Sha256::new();
hash.update(tables_bytes);
let actual = hash.finalize();
ensure!(
&h.tables_checksum[..] == actual.as_slice(),
"LP metadata: tables checksum mismatch"
);
Ok(())
}
fn read_sha256(b: &[u8], at: usize) -> [u8; 32] {
let mut s = [0u8; 32];
s.copy_from_slice(&b[at..at + 32]);
s
}
struct TableDescriptor {
offset: u32,
num_entries: u32,
entry_size: u32,
}
#[allow(dead_code)]
fn parse_table(b: &[u8], at: usize) -> TableDescriptor {
TableDescriptor {
offset: le32(b, at),
num_entries: le32(b, at + 4),
entry_size: le32(b, at + 8),
}
}
#[allow(dead_code)]
struct MetadataHeader {
magic: u32,
major: u16,
minor: u16,
header_size: u32,
header_checksum: Sha256Arr,
tables_size: u32,
tables_checksum: Sha256Arr,
partitions: TableDescriptor,
extents: TableDescriptor,
groups: TableDescriptor,
block_devices: TableDescriptor,
}
const HEADER_FIXED_SIZE: usize = 80;
const HEADER_SIZE_MIN: usize = HEADER_FIXED_SIZE + 4 * 12;
#[allow(dead_code)]
fn parse_metadata_header(b: &[u8]) -> Result<MetadataHeader> {
ensure!(
b.len() >= HEADER_SIZE_MIN,
"LP metadata header is too short (need {HEADER_SIZE_MIN} bytes, got {})",
b.len()
);
let magic = le32(b, 0);
ensure!(
magic == LP_HEADER_MAGIC,
"LP metadata: bad header magic {:#010x}",
magic
);
let header = MetadataHeader {
magic,
major: le16(b, 4),
minor: le16(b, 6),
header_size: le32(b, 8),
header_checksum: read_sha256(b, 12),
tables_size: le32(b, 44),
tables_checksum: read_sha256(b, 48),
partitions: parse_table(b, 60),
extents: parse_table(b, 72),
groups: parse_table(b, 84),
block_devices: parse_table(b, 96),
};
ensure!(
header.header_size as usize >= HEADER_SIZE_MIN,
"LP metadata header_size {} is too small",
header.header_size
);
ensure!(
header.major >= 1,
"LP metadata: unsupported major version {}",
header.major
);
Ok(header)
}
#[allow(dead_code)]
const PARTITION_ATTR_SLOT_SUFFIXED: u32 = 0x1;
const EXTENT_LINEAR: u32 = 0;
const EXTENT_ZERO: u32 = 1;
const PARTITION_SIZE: usize = 52;
#[derive(Clone)]
#[allow(dead_code)]
struct Partition {
name: String,
attributes: u32,
first_extent_index: u32,
num_extents: u32,
group_index: u32,
}
fn parse_partition(b: &[u8]) -> Result<Partition> {
ensure!(b.len() >= PARTITION_SIZE, "partition entry is too short");
let name = read_name(&b[0..36])?;
Ok(Partition {
name,
attributes: le32(b, 36),
first_extent_index: le32(b, 40),
num_extents: le32(b, 44),
group_index: le32(b, 48),
})
}
const EXTENT_SIZE: usize = 28;
#[derive(Clone, Copy)]
pub struct Extent {
#[allow(dead_code)]
pub num_sectors: u64,
pub target_type: u32,
pub target_data: u64,
#[allow(dead_code)]
pub target_source: u32,
}
fn parse_extent(b: &[u8]) -> Result<Extent> {
ensure!(b.len() >= EXTENT_SIZE, "extent entry is too short");
Ok(Extent {
num_sectors: le64(b, 0),
target_type: le32(b, 8),
target_data: le64(b, 12),
target_source: le32(b, 20),
})
}
const GROUP_SIZE: usize = 48;
#[derive(Clone)]
#[allow(dead_code)]
struct Group {
name: String,
flags: u32,
maximum_size: u64,
}
fn parse_group(b: &[u8]) -> Result<Group> {
ensure!(b.len() >= GROUP_SIZE, "group entry is too short");
let name = read_name(&b[0..36])?;
Ok(Group {
name,
flags: le32(b, 36),
maximum_size: le64(b, 40),
})
}
const BLOCK_DEVICE_SIZE: usize = 56;
#[derive(Clone)]
#[allow(dead_code)]
struct BlockDevice {
first_logical_sector: u64,
partition_name: String,
flags: u32,
size: u64,
}
fn parse_block_device(b: &[u8]) -> Result<BlockDevice> {
ensure!(
b.len() >= BLOCK_DEVICE_SIZE,
"block_device entry is too short"
);
let partition_name = read_name(&b[8..44])?;
Ok(BlockDevice {
first_logical_sector: le64(b, 0),
partition_name,
flags: le32(b, 44),
size: le64(b, 48),
})
}
fn read_name(b: &[u8]) -> Result<String> {
let nul = b.iter().position(|&c| c == 0).unwrap_or(b.len());
let name = &b[..nul];
String::from_utf8(name.to_vec()).with_context(|| "LP name is not valid UTF-8")
}
type Tables = (Vec<Partition>, Vec<Extent>, Vec<Group>, Vec<BlockDevice>);
fn parse_tables(tables: &[u8], h: &MetadataHeader) -> Result<Tables> {
let parts = parse_entries::<Partition>(tables, &h.partitions, PARTITION_SIZE, parse_partition)?;
let extents = parse_entries::<Extent>(tables, &h.extents, EXTENT_SIZE, parse_extent)?;
let groups = parse_entries::<Group>(tables, &h.groups, GROUP_SIZE, parse_group)?;
let bdevs = parse_entries::<BlockDevice>(
tables,
&h.block_devices,
BLOCK_DEVICE_SIZE,
parse_block_device,
)?;
Ok((parts, extents, groups, bdevs))
}
fn parse_entries<T>(
tables: &[u8],
td: &TableDescriptor,
entry_size: usize,
parse_one: fn(&[u8]) -> Result<T>,
) -> Result<Vec<T>> {
ensure!(td.entry_size as usize == entry_size, "wrong LP entry size");
let total = td.num_entries as usize * entry_size;
let start = td.offset as usize;
ensure!(tables.len() >= start + total, "LP table truncated");
ensure!(
td.num_entries <= MAX_ENTRIES as u32,
"LP table claims too many entries: {n}",
n = td.num_entries
);
let mut out = Vec::with_capacity(td.num_entries as usize);
let mut i = start;
for _ in 0..td.num_entries {
let chunk = tables
.get(i..i + entry_size)
.context("LP table entry is too short")?;
out.push(parse_one(chunk)?);
i += entry_size;
}
Ok(out)
}
pub struct ParsedPart {
name: String,
extents: Vec<Extent>,
_group: String,
}
pub fn parse_metadata(image: &[u8]) -> Result<Vec<ParsedPart>> {
ensure!(!image.is_empty(), "super image is empty");
let gstart = GEOMETRY_OFFSET as usize;
let g = parse_geometry(
image
.get(gstart..gstart + GEOMETRY_SIZE)
.context("super image too short for geometry")?,
)?;
verify_geometry_checksum(
&g,
image
.get(gstart..gstart + GEOMETRY_SIZE)
.context("super image too short for geometry")?,
)?;
let hdr_off = GEOMETRY_OFFSET + METADATA_ALIGN;
let header_bytes = image
.get(hdr_off as usize..)
.context("no LP metadata header region")?;
let h = parse_metadata_header(header_bytes)?;
verify_header_checksum(&h, header_bytes)?;
let tables_start = hdr_off as usize + h.header_size as usize;
let tables = image
.get(tables_start..tables_start + h.tables_size as usize)
.context("LP tables region is truncated")?;
verify_tables_checksum(&h, tables)?;
let (parts, extents, _groups, _bdevs) = parse_tables(tables, &h)?;
let mut out = Vec::new();
for p in parts {
let start = p.first_extent_index as usize;
let end = start + p.num_extents as usize;
let pext = extents
.get(start..end)
.context("partition extents out of range")?
.to_vec();
out.push(ParsedPart {
name: p.name,
extents: pext,
_group: String::new(),
});
}
Ok(out)
}
pub fn split_partitions(
image: &[u8],
parsed: &[ParsedPart],
only: Option<&[String]>,
) -> Result<Vec<(String, Vec<u8>)>> {
let mut out = Vec::new();
'p: for part in parsed {
if let Some(only) = only {
let found = only.iter().any(|o| o == &part.name);
if !found {
continue 'p;
}
}
let total: u64 = part
.extents
.iter()
.map(|e| e.num_sectors * SECTOR_SIZE)
.sum();
ensure!(
total <= MAX_IMAGE_BYTES,
"partition {} is too large",
part.name
);
let mut buf = Vec::with_capacity(total as usize);
for e in &part.extents {
ensure!(
e.target_type == EXTENT_LINEAR || e.target_type == EXTENT_ZERO,
"partition {}: unsupported extent type {}",
part.name,
e.target_type
);
let len = (e.num_sectors * SECTOR_SIZE) as usize;
if e.target_type == EXTENT_ZERO {
buf.extend(std::iter::repeat_n(0u8, len));
} else {
let start = (e.target_data * SECTOR_SIZE) as usize;
let end = start + len;
let chunk = image
.get(start..end)
.context("LP extent runs past end of super image")?;
buf.extend_from_slice(chunk);
}
}
out.push((part.name.clone(), buf));
}
Ok(out)
}
#[allow(dead_code)]
pub fn is_super(path: &std::path::Path) -> Result<bool> {
let mut file = std::fs::File::open(path)?;
use std::io::{Read, Seek, SeekFrom};
file.seek(SeekFrom::Start(4096))?;
let mut magic = [0u8; 4];
let n = file.read(&mut magic)?;
Ok(n == 4 && magic == LP_GEOMETRY_MAGIC_BYTES)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::Scratch;
#[test]
fn is_super_detects_magic() {
let mut img = vec![0u8; 8200];
img[4096..4100].copy_from_slice(b"gDla");
let d = Scratch::new("lp-super");
let p = d.join("super.img");
std::fs::write(&p, &img).unwrap();
assert!(is_super(&p).unwrap());
}
#[test]
fn is_super_rejects_plain_bytes() {
let d = Scratch::new("lp-nosuper");
let p = d.join("not.img");
std::fs::write(&p, [0u8; 8200]).unwrap();
assert!(!is_super(&p).unwrap());
}
}