#![no_std]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(rustdoc::broken_intra_doc_links)]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use embedded_io::{ErrorKind, Read, Seek, SeekFrom};
use serde::Deserialize;
use snafu::{OptionExt, Snafu, ensure};
use crate::header::{MetadataAreaHeader, PhysicalVolumeHeader, PhysicalVolumeLabelHeader};
use crate::metadata::{MetadataRoot, deserialize::MetadataElements};
mod force_de_typed_map;
mod header;
mod lv;
pub mod metadata;
mod owned;
pub use lv::*;
pub use owned::OwnedLvReader;
#[cfg(feature = "__fuzzing")]
pub mod __fuzzing {
pub use crate::force_de_typed_map::ForceDeTypedMap;
pub use crate::header::{MetadataAreaHeader, PhysicalVolumeHeader, PhysicalVolumeLabelHeader};
pub use crate::metadata::MetadataRoot;
pub use crate::metadata::deserialize::MetadataElements;
}
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("I/O error: {kind:?}"))]
Io { kind: ErrorKind },
#[snafu(display("unexpected end of PV input during read_exact"))]
UnexpectedEof,
#[snafu(display("PV label header has wrong magic"))]
WrongMagic,
#[snafu(display("parse error: {reason}"))]
Parse { reason: String },
#[snafu(display("metadata declares multiple VGs (single-VG-per-PV only)"))]
MultipleVGs,
#[snafu(display("PV metadata does not reference this PV's own UUID"))]
PVDoesntContainItself,
#[snafu(display("metadata deserialize error: {reason}"))]
Serde { reason: String },
#[snafu(display("PV header is missing a metadata area descriptor"))]
MissingMetadata,
#[snafu(display("metadata text was not valid UTF-8"))]
MetadataNotUtf8,
}
#[expect(
clippy::needless_pass_by_value,
reason = "used as a `map_err` function argument, which must take E by value"
)]
fn io_err<E: embedded_io::Error>(e: E) -> Error {
Error::Io { kind: e.kind() }
}
fn read_exact_err<E: embedded_io::Error>(e: embedded_io::ReadExactError<E>) -> Error {
match e {
embedded_io::ReadExactError::UnexpectedEof => Error::UnexpectedEof,
embedded_io::ReadExactError::Other(inner) => Error::Io { kind: inner.kind() },
}
}
pub struct Lvm2 {
pvh: PhysicalVolumeHeader,
pv_name: String,
vg_name: String,
vg_config: MetadataRoot,
}
impl Lvm2 {
pub fn open<T: Read + Seek>(mut reader: T) -> Result<Self, Error> {
reader.seek(SeekFrom::Start(512)).map_err(io_err)?;
let mut buf = [0u8; 512];
reader.read_exact(&mut buf).map_err(read_exact_err)?;
tracing::trace!(?buf);
let (_, vhl) = PhysicalVolumeLabelHeader::parse(&buf).map_err(|e| Error::Parse {
reason: e.to_string(),
})?;
tracing::trace!(?vhl);
let (_, pvh) =
PhysicalVolumeHeader::parse(&buf[(vhl.data_offset as usize)..]).map_err(|e| {
Error::Parse {
reason: e.to_string(),
}
})?;
tracing::trace!(?pvh);
let metadata_descriptor = pvh
.metadata_descriptors
.first()
.context(MissingMetadataSnafu)?;
reader
.seek(SeekFrom::Start(metadata_descriptor.offset))
.map_err(io_err)?;
reader.read_exact(&mut buf).map_err(read_exact_err)?;
let (_, mah) = MetadataAreaHeader::parse(&buf).map_err(|e| Error::Parse {
reason: e.to_string(),
})?;
tracing::trace!(?mah);
let mut metadata_bytes: Vec<u8> = Vec::new();
for locdesc in &mah.location_descriptors {
reader
.seek(SeekFrom::Start(
metadata_descriptor.offset + locdesc.data_area_offset,
))
.map_err(io_err)?;
let len = usize::try_from(locdesc.data_area_size).map_err(|_| Error::Parse {
reason: "metadata area size overflows usize".to_string(),
})?;
let start = metadata_bytes.len();
metadata_bytes.resize(start + len, 0);
reader
.read_exact(&mut metadata_bytes[start..])
.map_err(read_exact_err)?;
}
let metadata = core::str::from_utf8(&metadata_bytes).map_err(|_| Error::MetadataNotUtf8)?;
tracing::debug!(%metadata);
let (trailing_garbage, metadata) =
MetadataElements::parse(metadata).map_err(|e| Error::Parse {
reason: e.to_string(),
})?;
tracing::debug!(?trailing_garbage, ?metadata);
let meta_root =
force_de_typed_map::ForceDeTypedMap::<String, MetadataRoot>::deserialize(&metadata)
.map_err(|e| Error::Serde {
reason: e.to_string(),
})?;
tracing::debug!(?meta_root);
ensure!(meta_root.0.len() == 1, MultipleVGsSnafu);
#[expect(
clippy::unwrap_used,
reason = "the ensure! above guarantees exactly one entry"
)]
let (vg_name, vg_config) = meta_root.0.into_iter().next().unwrap();
let pv_name = vg_config
.physical_volumes
.iter()
.find(|(_, v)| v.id.replace('-', "") == pvh.pv_ident)
.context(PVDoesntContainItselfSnafu)?
.0
.clone();
Ok(Self {
pvh,
pv_name,
vg_name,
vg_config,
})
}
pub fn pv_id(&self) -> &str {
&self.vg_config.physical_volumes[&self.pv_name].id
}
pub fn pv_name(&self) -> &str {
&self.pv_name
}
pub fn vg_name(&self) -> &str {
&self.vg_name
}
pub fn vg_id(&self) -> &str {
&self.vg_config.id
}
pub fn lvs(&self) -> impl Iterator<Item = LV<'_>> {
self.vg_config
.logical_volumes
.iter()
.map(|(name, desc)| LV { name, desc })
}
pub fn open_lv_by_name<'a, T: Read + Seek>(
&'a self,
name: &str,
reader: T,
) -> Option<OpenLV<'a, T>> {
self.vg_config
.logical_volumes
.get_key_value(name)
.map(move |(name, desc)| self.open_lv(LV { name, desc }, reader))
}
pub fn open_lv_by_id<'a, T: Read + Seek>(
&'a self,
id: &str,
reader: T,
) -> Option<OpenLV<'a, T>> {
self.lvs()
.find(|lv| lv.id() == id)
.map(move |lv| self.open_lv(lv, reader))
}
pub fn open_lv<'a, T: Read + Seek>(&'a self, lv: LV<'a>, reader: T) -> OpenLV<'a, T> {
OpenLV {
lv,
lvm: self,
reader,
position: 0,
current_segment_end: 0,
}
}
pub fn extent_size(&self) -> u64 {
self.vg_config.extent_size * 512
}
pub(crate) fn pv_header(&self) -> &PhysicalVolumeHeader {
&self.pvh
}
pub(crate) fn vg_config_lookup(
&self,
name: &str,
) -> Option<(&String, &crate::metadata::LVDesc)> {
self.vg_config.logical_volumes.get_key_value(name)
}
}