Skip to main content

lamlvm/
lib.rs

1//! lamlvm — Read-only LVM2 Logical Volume reader for no_std environments.
2//!
3//! Vendored + modernized fork of [`main--/rust-lvm2`](https://github.com/main--/rust-lvm2)
4//! v0.0.3 (MIT). See `PROVENANCE.md` for the origin story and the list of
5//! changes from upstream. Notable change: `acid_io` (unmaintained since
6//! 2022) replaced with `embedded-io` (actively maintained by the
7//! embedded-rust working group).
8//!
9//! Format reference:
10//! <https://github.com/libyal/libvslvm/blob/main/documentation/Logical%20Volume%20Manager%20(LVM)%20format.asciidoc>
11//!
12//! Vocabulary: in this crate we use the term "sheet" to describe a block of
13//! exactly 512 bytes (to avoid confusion around the word "sector").
14//!
15//! # Coverage
16//!
17//! Linear logical volumes on a single physical volume only. Striped,
18//! mirrored, thin pool, snapshot, and cache LVs return [`OpenLvError::Unsupported`]
19//! or analogous errors. This covers the canonical layout used by Proxmox VE
20//! and most default single-disk LVM installs. See `PROVENANCE.md` for the
21//! full coverage matrix and the rationale for the narrow scope.
22
23#![no_std]
24#![forbid(unsafe_code)]
25#![cfg_attr(docsrs, feature(doc_cfg))]
26#![deny(rustdoc::broken_intra_doc_links)]
27
28extern crate alloc;
29
30use alloc::string::{String, ToString};
31use alloc::vec::Vec;
32
33use embedded_io::{ErrorKind, Read, Seek, SeekFrom};
34use serde::Deserialize;
35use snafu::{OptionExt, Snafu, ensure};
36
37use crate::header::{MetadataAreaHeader, PhysicalVolumeHeader, PhysicalVolumeLabelHeader};
38use crate::metadata::{MetadataRoot, deserialize::MetadataElements};
39
40mod force_de_typed_map;
41mod header;
42mod lv;
43pub mod metadata;
44mod owned;
45
46pub use lv::*;
47pub use owned::OwnedLvReader;
48
49/// Internal parser entry points re-exported for the fuzz harness only.
50/// **Not stable API.** Behind the `__fuzzing` feature so non-fuzz
51/// consumers can't accidentally reach in.
52#[cfg(feature = "__fuzzing")]
53pub mod __fuzzing {
54    pub use crate::force_de_typed_map::ForceDeTypedMap;
55    pub use crate::header::{MetadataAreaHeader, PhysicalVolumeHeader, PhysicalVolumeLabelHeader};
56    pub use crate::metadata::MetadataRoot;
57    pub use crate::metadata::deserialize::MetadataElements;
58}
59
60/// Top-level error type for the lamlvm crate.
61///
62/// I/O errors are reduced to `embedded_io::ErrorKind` rather than carrying
63/// the generic `T::Error` from each call site — keeps the enum concrete and
64/// consumer code simple. Specific I/O failures are still recognizable via
65/// the `ErrorKind` value; the original I/O error is converted via
66/// `embedded_io::Error::kind()`.
67#[derive(Debug, Snafu)]
68pub enum Error {
69    /// I/O failure from the underlying reader.
70    #[snafu(display("I/O error: {kind:?}"))]
71    Io { kind: ErrorKind },
72
73    /// Underlying reader ended before a `read_exact` could complete.
74    /// `embedded_io::ReadExactError::UnexpectedEof` is mapped here since
75    /// it has no equivalent in `embedded_io::ErrorKind`.
76    #[snafu(display("unexpected end of PV input during read_exact"))]
77    UnexpectedEof,
78
79    /// PV label header at sheet 1 did not have the `LABELONE` magic.
80    #[snafu(display("PV label header has wrong magic"))]
81    WrongMagic,
82
83    /// nom parser rejected the input bytes (header or metadata-area-header).
84    #[snafu(display("parse error: {reason}"))]
85    Parse { reason: String },
86
87    /// VG metadata text declared more than one volume group. lamlvm
88    /// currently supports the canonical single-VG-per-PV layout only.
89    #[snafu(display("metadata declares multiple VGs (single-VG-per-PV only)"))]
90    MultipleVGs,
91
92    /// VG metadata text did not list the PV we opened — broken or
93    /// mismatched metadata.
94    #[snafu(display("PV metadata does not reference this PV's own UUID"))]
95    PVDoesntContainItself,
96
97    /// serde rejected the parsed metadata token stream.
98    #[snafu(display("metadata deserialize error: {reason}"))]
99    Serde { reason: String },
100
101    /// PV header has no metadata area descriptor (impossible on a valid PV).
102    #[snafu(display("PV header is missing a metadata area descriptor"))]
103    MissingMetadata,
104
105    /// VG metadata bytes were not valid UTF-8.
106    #[snafu(display("metadata text was not valid UTF-8"))]
107    MetadataNotUtf8,
108}
109
110/// Map any `embedded_io::Error` into our flat `Io` variant.
111#[expect(
112    clippy::needless_pass_by_value,
113    reason = "used as a `map_err` function argument, which must take E by value"
114)]
115fn io_err<E: embedded_io::Error>(e: E) -> Error {
116    Error::Io { kind: e.kind() }
117}
118
119/// Map a `ReadExactError<E>` into either `Io` or `UnexpectedEof`.
120fn read_exact_err<E: embedded_io::Error>(e: embedded_io::ReadExactError<E>) -> Error {
121    match e {
122        embedded_io::ReadExactError::UnexpectedEof => Error::UnexpectedEof,
123        embedded_io::ReadExactError::Other(inner) => Error::Io { kind: inner.kind() },
124    }
125}
126
127/// A parsed LVM2 Physical Volume — Volume Group metadata loaded, ready to
128/// open Logical Volumes.
129pub struct Lvm2 {
130    pvh: PhysicalVolumeHeader,
131    pv_name: String,
132    vg_name: String,
133    vg_config: MetadataRoot,
134}
135
136impl Lvm2 {
137    /// Parse the PV label + header + VG metadata from `reader`.
138    ///
139    /// `reader` must be positioned over the start of the PV (typically the
140    /// first byte of a partition that contains LVM2 metadata).
141    pub fn open<T: Read + Seek>(mut reader: T) -> Result<Self, Error> {
142        // Sheet 0 is zero-padding; PV label header lives at sheet 1.
143        reader.seek(SeekFrom::Start(512)).map_err(io_err)?;
144
145        let mut buf = [0u8; 512];
146        reader.read_exact(&mut buf).map_err(read_exact_err)?;
147        tracing::trace!(?buf);
148
149        let (_, vhl) = PhysicalVolumeLabelHeader::parse(&buf).map_err(|e| Error::Parse {
150            reason: e.to_string(),
151        })?;
152        tracing::trace!(?vhl);
153        let (_, pvh) =
154            PhysicalVolumeHeader::parse(&buf[(vhl.data_offset as usize)..]).map_err(|e| {
155                Error::Parse {
156                    reason: e.to_string(),
157                }
158            })?;
159        tracing::trace!(?pvh);
160
161        let metadata_descriptor = pvh
162            .metadata_descriptors
163            .first()
164            .context(MissingMetadataSnafu)?;
165
166        reader
167            .seek(SeekFrom::Start(metadata_descriptor.offset))
168            .map_err(io_err)?;
169        reader.read_exact(&mut buf).map_err(read_exact_err)?;
170        let (_, mah) = MetadataAreaHeader::parse(&buf).map_err(|e| Error::Parse {
171            reason: e.to_string(),
172        })?;
173        tracing::trace!(?mah);
174
175        // Read the VG metadata text. Upstream used `read_to_string` which
176        // doesn't exist on embedded-io. Equivalent: allocate per
177        // location descriptor, read_exact, then UTF-8-validate at the end.
178        let mut metadata_bytes: Vec<u8> = Vec::new();
179        for locdesc in &mah.location_descriptors {
180            reader
181                .seek(SeekFrom::Start(
182                    metadata_descriptor.offset + locdesc.data_area_offset,
183                ))
184                .map_err(io_err)?;
185            let len = usize::try_from(locdesc.data_area_size).map_err(|_| Error::Parse {
186                reason: "metadata area size overflows usize".to_string(),
187            })?;
188            let start = metadata_bytes.len();
189            metadata_bytes.resize(start + len, 0);
190            reader
191                .read_exact(&mut metadata_bytes[start..])
192                .map_err(read_exact_err)?;
193        }
194        let metadata = core::str::from_utf8(&metadata_bytes).map_err(|_| Error::MetadataNotUtf8)?;
195        tracing::debug!(%metadata);
196
197        let (trailing_garbage, metadata) =
198            MetadataElements::parse(metadata).map_err(|e| Error::Parse {
199                reason: e.to_string(),
200            })?;
201        tracing::debug!(?trailing_garbage, ?metadata);
202
203        let meta_root =
204            force_de_typed_map::ForceDeTypedMap::<String, MetadataRoot>::deserialize(&metadata)
205                .map_err(|e| Error::Serde {
206                    reason: e.to_string(),
207                })?;
208        tracing::debug!(?meta_root);
209
210        ensure!(meta_root.0.len() == 1, MultipleVGsSnafu);
211        #[expect(
212            clippy::unwrap_used,
213            reason = "the ensure! above guarantees exactly one entry"
214        )]
215        let (vg_name, vg_config) = meta_root.0.into_iter().next().unwrap();
216
217        let pv_name = vg_config
218            .physical_volumes
219            .iter()
220            .find(|(_, v)| v.id.replace('-', "") == pvh.pv_ident)
221            .context(PVDoesntContainItselfSnafu)?
222            .0
223            .clone();
224
225        Ok(Self {
226            pvh,
227            pv_name,
228            vg_name,
229            vg_config,
230        })
231    }
232
233    pub fn pv_id(&self) -> &str {
234        &self.vg_config.physical_volumes[&self.pv_name].id
235    }
236
237    pub fn pv_name(&self) -> &str {
238        &self.pv_name
239    }
240
241    pub fn vg_name(&self) -> &str {
242        &self.vg_name
243    }
244
245    pub fn vg_id(&self) -> &str {
246        &self.vg_config.id
247    }
248
249    pub fn lvs(&self) -> impl Iterator<Item = LV<'_>> {
250        self.vg_config
251            .logical_volumes
252            .iter()
253            .map(|(name, desc)| LV { name, desc })
254    }
255
256    pub fn open_lv_by_name<'a, T: Read + Seek>(
257        &'a self,
258        name: &str,
259        reader: T,
260    ) -> Option<OpenLV<'a, T>> {
261        self.vg_config
262            .logical_volumes
263            .get_key_value(name)
264            .map(move |(name, desc)| self.open_lv(LV { name, desc }, reader))
265    }
266
267    pub fn open_lv_by_id<'a, T: Read + Seek>(
268        &'a self,
269        id: &str,
270        reader: T,
271    ) -> Option<OpenLV<'a, T>> {
272        self.lvs()
273            .find(|lv| lv.id() == id)
274            .map(move |lv| self.open_lv(lv, reader))
275    }
276
277    pub fn open_lv<'a, T: Read + Seek>(&'a self, lv: LV<'a>, reader: T) -> OpenLV<'a, T> {
278        OpenLV {
279            lv,
280            lvm: self,
281            reader,
282            position: 0,
283            current_segment_end: 0,
284        }
285    }
286
287    pub fn extent_size(&self) -> u64 {
288        self.vg_config.extent_size * 512
289    }
290
291    /// Access to the parsed PV header (used by `lv.rs` for segment→PV offset
292    /// resolution; exposed pub(crate) here to keep the resolver in one place).
293    pub(crate) fn pv_header(&self) -> &PhysicalVolumeHeader {
294        &self.pvh
295    }
296
297    /// Borrowed lookup helper shared by both the borrowing and owning
298    /// LV-open paths.
299    pub(crate) fn vg_config_lookup(
300        &self,
301        name: &str,
302    ) -> Option<(&String, &crate::metadata::LVDesc)> {
303        self.vg_config.logical_volumes.get_key_value(name)
304    }
305}