wxtla 0.3.1

Wired eXploring Target Layer Accessor
Documentation
use crate::{Error, Result};

#[derive(Clone, Copy)]
pub(super) struct AreaDescriptor {
  pub(super) offset: u64,
  pub(super) size: u64,
}

#[derive(Clone, Copy)]
pub(super) struct RawLocation {
  pub(super) offset: u64,
  pub(super) size: u64,
  pub(super) checksum: u32,
}

pub(super) struct PhysicalVolumeLabel {
  pub(super) pv_identifier: String,
  pub(super) data_areas: Vec<AreaDescriptor>,
  pub(super) metadata_areas: Vec<AreaDescriptor>,
}

pub struct LvmParsedImage {
  pub(super) label: PhysicalVolumeLabel,
  pub(super) metadata: ParsedMetadata,
  pub(super) current_pv_name: String,
}

pub(super) struct ParsedMetadata {
  pub(super) vg_name: String,
  pub(super) seqno: u64,
  pub(super) extent_size_bytes: u64,
  pub(super) physical_volumes: Vec<MetadataPhysicalVolume>,
  pub(super) logical_volumes: Vec<MetadataLogicalVolume>,
}

pub(super) struct MetadataPhysicalVolume {
  pub(super) name: String,
  pub(super) id: Option<String>,
  pub(super) pe_start_bytes: Option<u64>,
}

pub(super) struct MetadataLogicalVolume {
  pub(super) name: String,
  pub(super) id: Option<String>,
  pub(super) segments: Vec<MetadataSegment>,
}

pub(super) struct MetadataSegment {
  pub(super) start_extent: u64,
  pub(super) extent_count: u64,
  pub(super) stripe_size_bytes: Option<u64>,
  pub(super) stripes: Vec<MetadataStripe>,
}

pub(super) struct MetadataStripe {
  pub(super) pv_name: String,
  pub(super) start_extent: u64,
}

#[derive(Clone)]
pub struct LvmLogicalVolumeInfo {
  pub name: String,
  pub id: Option<String>,
  pub size: u64,
  pub chunks: Vec<LvmChunk>,
}

#[derive(Clone, Copy)]
pub struct LvmChunk {
  pub logical_offset: u64,
  pub size: u64,
  pub physical_offset: Option<u64>,
}

pub(super) fn build_logical_volume_info(
  label: &PhysicalVolumeLabel, extent_size_bytes: u64, current_pv_name: &str,
  current_pv_pe_start: Option<u64>, logical_volume: &MetadataLogicalVolume,
) -> Result<LvmLogicalVolumeInfo> {
  let lv_name = logical_volume.name.clone();
  let mut chunks = Vec::new();
  let mut size = 0u64;

  for segment in &logical_volume.segments {
    let segment_chunks = build_segment_chunks(
      label,
      extent_size_bytes,
      current_pv_name,
      current_pv_pe_start,
      segment,
    )?;
    for chunk in segment_chunks {
      let chunk_end = chunk
        .logical_offset
        .checked_add(chunk.size)
        .ok_or_else(|| Error::invalid_range("LVM logical volume size overflow"))?;
      chunks.push(chunk);
      size = size.max(chunk_end);
    }
  }

  chunks.sort_by_key(|chunk| chunk.logical_offset);
  Ok(LvmLogicalVolumeInfo {
    name: lv_name,
    id: logical_volume.id.clone(),
    size,
    chunks,
  })
}

pub(super) fn logical_volume_size(
  label: &PhysicalVolumeLabel, extent_size_bytes: u64, current_pv_name: &str,
  current_pv_pe_start: Option<u64>, logical_volume: &MetadataLogicalVolume,
) -> Result<u64> {
  let mut size = 0u64;

  for segment in &logical_volume.segments {
    let segment_chunks = build_segment_chunks(
      label,
      extent_size_bytes,
      current_pv_name,
      current_pv_pe_start,
      segment,
    )?;
    for chunk in segment_chunks {
      let chunk_end = chunk
        .logical_offset
        .checked_add(chunk.size)
        .ok_or_else(|| Error::invalid_range("LVM logical volume size overflow"))?;
      size = size.max(chunk_end);
    }
  }

  Ok(size)
}

pub(super) fn resolve_current_pv_name(
  metadata: &ParsedMetadata, pv_identifier: &str,
) -> Option<String> {
  let normalized_target = normalize_lvm_id(pv_identifier);
  for pv in &metadata.physical_volumes {
    if let Some(id) = &pv.id
      && normalize_lvm_id(id) == normalized_target
    {
      return Some(pv.name.clone());
    }
  }

  if metadata.physical_volumes.len() == 1 {
    return Some(metadata.physical_volumes[0].name.clone());
  }
  None
}

pub(super) fn resolve_pv_pe_start(
  physical_volumes: &[MetadataPhysicalVolume], pv_name: &str,
) -> Option<u64> {
  physical_volumes
    .iter()
    .find(|pv| pv.name == pv_name)
    .and_then(|pv| pv.pe_start_bytes)
}

fn resolve_data_area_offset(data_areas: &[AreaDescriptor], mut offset: u64) -> Option<u64> {
  for area in data_areas {
    if area.size == 0 {
      return area.offset.checked_add(offset);
    }
    if offset < area.size {
      return area.offset.checked_add(offset);
    }
    offset -= area.size;
  }
  None
}

fn build_segment_chunks(
  label: &PhysicalVolumeLabel, extent_size_bytes: u64, current_pv_name: &str,
  current_pv_pe_start: Option<u64>, segment: &MetadataSegment,
) -> Result<Vec<LvmChunk>> {
  let logical_offset = segment
    .start_extent
    .checked_mul(extent_size_bytes)
    .ok_or_else(|| Error::invalid_range("LVM logical offset overflow"))?;
  let segment_size = segment
    .extent_count
    .checked_mul(extent_size_bytes)
    .ok_or_else(|| Error::invalid_range("LVM segment size overflow"))?;

  if segment.stripes.len() == 1 {
    let stripe = &segment.stripes[0];
    let physical_offset = if stripe.pv_name == current_pv_name {
      let stripe_rel = stripe
        .start_extent
        .checked_mul(extent_size_bytes)
        .ok_or_else(|| Error::invalid_range("LVM stripe offset overflow"))?;
      match resolve_data_area_offset(&label.data_areas, stripe_rel) {
        Some(offset) => {
          if let Some(pe_start) = current_pv_pe_start {
            if offset < pe_start {
              Some(
                pe_start
                  .checked_add(stripe_rel)
                  .ok_or_else(|| Error::invalid_range("LVM physical offset overflow"))?,
              )
            } else {
              Some(offset)
            }
          } else {
            Some(offset)
          }
        }
        None => {
          if let Some(pe_start) = current_pv_pe_start {
            Some(
              pe_start
                .checked_add(stripe_rel)
                .ok_or_else(|| Error::invalid_range("LVM physical offset overflow"))?,
            )
          } else {
            Some(stripe_rel)
          }
        }
      }
    } else {
      None
    };

    return Ok(vec![LvmChunk {
      logical_offset,
      size: segment_size,
      physical_offset,
    }]);
  }

  let stripe_size = segment
    .stripe_size_bytes
    .ok_or_else(|| Error::invalid_format("LVM striped segments require an explicit stripe size"))?;
  if stripe_size == 0 {
    return Err(Error::invalid_format(
      "LVM striped segments require a non-zero stripe size".to_string(),
    ));
  }

  let stripe_count = u64::try_from(segment.stripes.len())
    .map_err(|_| Error::invalid_range("LVM stripe count is too large"))?;
  let chunk_count = segment_size.div_ceil(stripe_size);
  let mut chunks = Vec::with_capacity(usize::try_from(chunk_count).unwrap_or(0));
  for chunk_index in 0..chunk_count {
    let stripe_index = usize::try_from(chunk_index % stripe_count)
      .map_err(|_| Error::invalid_range("LVM stripe index is too large"))?;
    let row = chunk_index / stripe_count;
    let chunk_logical_offset = logical_offset
      .checked_add(
        chunk_index
          .checked_mul(stripe_size)
          .ok_or_else(|| Error::invalid_range("LVM stripe logical offset overflow"))?,
      )
      .ok_or_else(|| Error::invalid_range("LVM stripe logical offset overflow"))?;
    let remaining = segment_size - chunk_index * stripe_size;
    let chunk_size = remaining.min(stripe_size);
    let stripe = &segment.stripes[stripe_index];
    let physical_offset = if stripe.pv_name == current_pv_name {
      let stripe_rel = stripe
        .start_extent
        .checked_mul(extent_size_bytes)
        .and_then(|offset| offset.checked_add(row * stripe_size))
        .ok_or_else(|| Error::invalid_range("LVM stripe offset overflow"))?;
      match resolve_data_area_offset(&label.data_areas, stripe_rel) {
        Some(offset) => {
          if let Some(pe_start) = current_pv_pe_start {
            if offset < pe_start {
              Some(
                pe_start
                  .checked_add(stripe_rel)
                  .ok_or_else(|| Error::invalid_range("LVM physical offset overflow"))?,
              )
            } else {
              Some(offset)
            }
          } else {
            Some(offset)
          }
        }
        None => {
          if let Some(pe_start) = current_pv_pe_start {
            Some(
              pe_start
                .checked_add(stripe_rel)
                .ok_or_else(|| Error::invalid_range("LVM physical offset overflow"))?,
            )
          } else {
            Some(stripe_rel)
          }
        }
      }
    } else {
      None
    };
    chunks.push(LvmChunk {
      logical_offset: chunk_logical_offset,
      size: chunk_size,
      physical_offset,
    });
  }

  Ok(chunks)
}

fn normalize_lvm_id(value: &str) -> String {
  value
    .chars()
    .filter(|character| character.is_ascii_alphanumeric())
    .map(|character| character.to_ascii_lowercase())
    .collect()
}