wxtla 0.3.1

Wired eXploring Target Layer Accessor
Documentation
//! Extent and parent-image resolution helpers for VMDK.

use std::sync::Arc;

use super::{
  constants,
  cowd_header::VmdkCowdHeader,
  descriptor::{
    VmdkDescriptor, VmdkDescriptorExtent, VmdkExtentAccessMode, VmdkExtentType, VmdkFileType,
  },
  header::VmdkSparseHeader,
  image::VmdkImage,
  parser::{parse_cowd, parse_sparse_extent},
};
use crate::{
  ByteSource, ByteSourceHandle, Error, RelatedPathBuf, RelatedSourcePurpose, RelatedSourceRequest,
  RelatedSourceResolver, Result, SliceDataSource, SourceHints, SourceIdentity,
};

pub(super) fn is_sparse_extent(source: &dyn ByteSource) -> Result<bool> {
  let mut magic = [0u8; 4];
  Ok(source.read_at(0, &mut magic)? == magic.len() && &magic == constants::SPARSE_HEADER_MAGIC)
}

pub(super) fn is_cowd_extent(source: &dyn ByteSource) -> Result<bool> {
  let mut magic = [0u8; 4];
  Ok(source.read_at(0, &mut magic)? == magic.len() && &magic == constants::COWD_HEADER_MAGIC)
}

pub(super) fn fill_from_parent_or_zero(
  parent_source: Option<&ByteSourceHandle>, offset: u64, buf: &mut [u8],
) -> Result<()> {
  if let Some(parent_source) = parent_source {
    parent_source.read_exact_at(offset, buf)?;
  } else {
    buf.fill(0);
  }

  Ok(())
}

pub(super) fn bounded_cache_capacity(
  entry_size: usize, byte_budget: usize, max_entries: usize,
) -> usize {
  if entry_size == 0 || max_entries == 0 {
    return 1;
  }

  (byte_budget / entry_size).max(1).min(max_entries)
}

pub(super) fn validate_descriptor_file_type(file_type: VmdkFileType) -> Result<()> {
  match file_type {
    VmdkFileType::Unknown => Err(Error::invalid_format(
      "unsupported vmdk descriptor create type".to_string(),
    )),
    VmdkFileType::Custom
    | VmdkFileType::FullDevice
    | VmdkFileType::MonolithicSparse
    | VmdkFileType::MonolithicFlat
    | VmdkFileType::PartitionedDevice
    | VmdkFileType::StreamOptimized
    | VmdkFileType::Flat2GbExtent
    | VmdkFileType::Sparse2GbExtent
    | VmdkFileType::Vmfs
    | VmdkFileType::VmfsSparse
    | VmdkFileType::VmfsThin
    | VmdkFileType::VmfsRaw
    | VmdkFileType::VmfsRdm
    | VmdkFileType::VmfsRdmp => Ok(()),
  }
}

pub(super) fn validate_monolithic_sparse_descriptor(
  header: &VmdkSparseHeader, descriptor: &VmdkDescriptor,
) -> Result<()> {
  if descriptor.file_type != VmdkFileType::MonolithicSparse
    && descriptor.file_type != VmdkFileType::StreamOptimized
  {
    return Err(Error::invalid_format(format!(
      "unsupported embedded vmdk create type: {:?}",
      descriptor.file_type
    )));
  }
  if descriptor.extents.len() != 1 {
    return Err(Error::invalid_format(
      "monolithic sparse vmdk images must declare exactly one extent".to_string(),
    ));
  }
  let extent = &descriptor.extents[0];
  if matches!(
    extent.access_mode,
    VmdkExtentAccessMode::Unknown | VmdkExtentAccessMode::NoAccess
  ) {
    return Err(Error::invalid_format(
      "unsupported vmdk extent access mode".to_string(),
    ));
  }
  if extent.extent_type != VmdkExtentType::Sparse {
    return Err(Error::invalid_format(
      "monolithic sparse vmdk images must use a SPARSE extent".to_string(),
    ));
  }
  if extent.start_sector != 0 {
    return Err(Error::invalid_format(
      "monolithic sparse vmdk extents must start at sector 0".to_string(),
    ));
  }
  if extent.sector_count != header.capacity_sectors {
    return Err(Error::invalid_format(
      "vmdk descriptor extent length does not match the sparse header capacity".to_string(),
    ));
  }

  Ok(())
}

pub(super) fn resolve_descriptor_parent_source(
  descriptor: &VmdkDescriptor, hints: SourceHints<'_>,
) -> Result<Option<ByteSourceHandle>> {
  let has_parent =
    descriptor.parent_content_id.is_some() || descriptor.parent_file_name_hint.is_some();
  if !has_parent {
    return Ok(None);
  }

  let parent_hint = descriptor.parent_file_name_hint.as_deref().ok_or_else(|| {
    Error::invalid_source_reference(
      "parent-backed vmdk descriptors require parentFileNameHint".to_string(),
    )
  })?;
  let resolver = hints.resolver().ok_or_else(|| {
    Error::invalid_source_reference(
      "parent-backed vmdk descriptors require a related-source resolver".to_string(),
    )
  })?;
  let identity = hints.source_identity().ok_or_else(|| {
    Error::invalid_source_reference(
      "parent-backed vmdk descriptors require a source identity hint".to_string(),
    )
  })?;
  let parent_image = resolve_parent_image(
    resolver,
    identity,
    parent_hint,
    descriptor.parent_content_id,
  )?;

  Ok(Some(parent_image as ByteSourceHandle))
}

pub(super) fn resolve_cowd_parent_source(
  header: &VmdkCowdHeader, hints: SourceHints<'_>,
) -> Result<Option<ByteSourceHandle>> {
  if header.parent_generation == 0 && header.parent_path.is_empty() {
    return Ok(None);
  }

  let parent_hint = if header.parent_path.is_empty() {
    return Err(Error::invalid_source_reference(
      "parent-backed vmdk cowd extents require a parent path".to_string(),
    ));
  } else {
    header.parent_path.as_str()
  };
  let resolver = hints.resolver().ok_or_else(|| {
    Error::invalid_source_reference(
      "parent-backed vmdk cowd extents require a related-source resolver".to_string(),
    )
  })?;
  let identity = hints.source_identity().ok_or_else(|| {
    Error::invalid_source_reference(
      "parent-backed vmdk cowd extents require a source identity hint".to_string(),
    )
  })?;
  let expected = if header.parent_generation == 0 {
    None
  } else {
    Some(header.parent_generation)
  };
  let parent_image = resolve_parent_image(resolver, identity, parent_hint, expected)?;

  Ok(Some(parent_image as ByteSourceHandle))
}

pub(super) fn resolve_flat_extent(
  extent: &VmdkDescriptorExtent, resolver: &dyn RelatedSourceResolver, identity: &SourceIdentity,
  extent_size: u64,
) -> Result<super::image::VmdkResolvedExtentKind> {
  let file_name = extent
    .file_name
    .as_deref()
    .ok_or_else(|| Error::invalid_format("vmdk flat extent is missing a file name"))?;
  let (source, path) = resolve_extent_source(resolver, identity, file_name)?
    .ok_or_else(|| Error::not_found("unable to resolve the vmdk extent file"))?;
  if &path == identity.logical_path() {
    return Err(Error::invalid_format(
      "vmdk descriptor extent resolves back to the descriptor file".to_string(),
    ));
  }

  let base_offset = extent
    .start_sector
    .checked_mul(constants::BYTES_PER_SECTOR)
    .ok_or_else(|| Error::invalid_range("vmdk flat extent offset overflow"))?;
  let extent_end = base_offset
    .checked_add(extent_size)
    .ok_or_else(|| Error::invalid_range("vmdk flat extent range overflow"))?;
  if extent_end > source.size()? {
    return Err(Error::invalid_format(
      "vmdk flat extent exceeds the backing file size".to_string(),
    ));
  }

  Ok(super::image::VmdkResolvedExtentKind::Source(
    Arc::new(SliceDataSource::new(source, base_offset, extent_size)) as ByteSourceHandle,
  ))
}

pub(super) fn resolve_sparse_extent(
  extent: &VmdkDescriptorExtent, resolver: &dyn RelatedSourceResolver, identity: &SourceIdentity,
  parent_source: Option<ByteSourceHandle>,
) -> Result<super::image::VmdkResolvedExtentKind> {
  if extent.start_sector != 0 {
    return Err(Error::invalid_format(
      "vmdk sparse extents must start at sector 0".to_string(),
    ));
  }

  let file_name = extent
    .file_name
    .as_deref()
    .ok_or_else(|| Error::invalid_format("vmdk sparse extent is missing a file name"))?;
  let (source, path) = resolve_extent_source(resolver, identity, file_name)?
    .ok_or_else(|| Error::not_found("unable to resolve the vmdk sparse extent"))?;
  if &path == identity.logical_path() {
    return Err(Error::invalid_format(
      "vmdk descriptor extent resolves back to the descriptor file".to_string(),
    ));
  }

  let parsed = parse_sparse_extent(source.clone())?;
  let descriptor = parsed
    .embedded_descriptor
    .unwrap_or_else(|| VmdkDescriptor {
      version: 1,
      content_id: 0,
      parent_content_id: None,
      file_type: VmdkFileType::Sparse2GbExtent,
      extents: vec![VmdkDescriptorExtent {
        access_mode: extent.access_mode,
        sector_count: extent.sector_count,
        extent_type: extent.extent_type,
        file_name: None,
        start_sector: 0,
      }],
      parent_file_name_hint: None,
    });
  validate_sparse_extent_matches_descriptor(&parsed.header, extent)?;
  let image = VmdkImage::from_sparse_parts(
    source,
    parsed.header,
    parsed.grain_directory,
    descriptor,
    parent_source,
  )?;
  Ok(super::image::VmdkResolvedExtentKind::Source(
    Arc::new(image) as ByteSourceHandle,
  ))
}

pub(super) fn resolve_vmfs_sparse_extent(
  extent: &VmdkDescriptorExtent, resolver: &dyn RelatedSourceResolver, identity: &SourceIdentity,
  parent_source: Option<ByteSourceHandle>,
) -> Result<super::image::VmdkResolvedExtentKind> {
  if extent.start_sector != 0 {
    return Err(Error::invalid_format(
      "vmdk vmfssparse extents must start at sector 0".to_string(),
    ));
  }

  let file_name = extent
    .file_name
    .as_deref()
    .ok_or_else(|| Error::invalid_format("vmdk vmfssparse extent is missing a file name"))?;
  let (source, path) = resolve_extent_source(resolver, identity, file_name)?
    .ok_or_else(|| Error::not_found("unable to resolve the vmdk vmfssparse extent"))?;
  if &path == identity.logical_path() {
    return Err(Error::invalid_format(
      "vmdk descriptor extent resolves back to the descriptor file".to_string(),
    ));
  }

  let parsed = parse_cowd(source.clone())?;
  if u64::from(parsed.header.capacity_sectors) != extent.sector_count {
    return Err(Error::invalid_format(
      "vmdk vmfssparse extent length does not match the cowd header capacity".to_string(),
    ));
  }
  let image = VmdkImage::from_cowd_parsed(source, parsed, parent_source)?;
  Ok(super::image::VmdkResolvedExtentKind::Source(
    Arc::new(image) as ByteSourceHandle,
  ))
}

pub(super) fn validate_sparse_extent_matches_descriptor(
  header: &VmdkSparseHeader, extent: &VmdkDescriptorExtent,
) -> Result<()> {
  if extent.start_sector != 0 {
    return Err(Error::invalid_format(
      "vmdk sparse extents must start at sector 0".to_string(),
    ));
  }
  if header.capacity_sectors != extent.sector_count {
    return Err(Error::invalid_format(
      "vmdk sparse extent length does not match the sparse header capacity".to_string(),
    ));
  }

  Ok(())
}

pub(super) fn resolve_parent_image(
  resolver: &dyn RelatedSourceResolver, identity: &SourceIdentity, parent_hint: &str,
  expected_content_id: Option<u32>,
) -> Result<Arc<VmdkImage>> {
  let (parent_source, parent_path) = resolve_named_source(
    resolver,
    identity,
    parent_hint,
    RelatedSourcePurpose::BackingFile,
  )?
  .ok_or_else(|| Error::not_found("unable to resolve the parent vmdk image"))?;
  if &parent_path == identity.logical_path() {
    return Err(Error::invalid_format(
      "vmdk parent hint resolves to the same image".to_string(),
    ));
  }

  let parent_identity = SourceIdentity::new(parent_path);
  let parent_image = Arc::new(VmdkImage::open_with_hints(
    parent_source,
    SourceHints::new()
      .with_resolver(resolver)
      .with_source_identity(&parent_identity),
  )?);
  if let Some(expected_content_id) = expected_content_id
    && parent_image.content_id() != expected_content_id
  {
    return Err(Error::invalid_format(format!(
      "resolved parent content id {} does not match expected {}",
      parent_image.content_id(),
      expected_content_id
    )));
  }

  Ok(parent_image)
}

pub(super) fn resolve_extent_source(
  resolver: &dyn RelatedSourceResolver, identity: &SourceIdentity, extent_name: &str,
) -> Result<Option<(ByteSourceHandle, RelatedPathBuf)>> {
  resolve_named_source(
    resolver,
    identity,
    extent_name,
    RelatedSourcePurpose::Extent,
  )
}

pub(super) fn resolve_named_source(
  resolver: &dyn RelatedSourceResolver, identity: &SourceIdentity, name: &str,
  purpose: RelatedSourcePurpose,
) -> Result<Option<(ByteSourceHandle, RelatedPathBuf)>> {
  if let Ok(relative) = RelatedPathBuf::from_relative_path(name)
    && let Some(parent) = identity.logical_path().parent()
  {
    let joined = parent.join(&relative);
    if let Some(source) = resolver.resolve(&RelatedSourceRequest::new(purpose, joined.clone()))? {
      return Ok(Some((source, joined)));
    }
  }

  let file_name = name.rsplit(['\\', '/']).next().unwrap_or(name);
  let sibling = identity.sibling_path(file_name)?;
  Ok(
    resolver
      .resolve(&RelatedSourceRequest::new(purpose, sibling.clone()))?
      .map(|source| (source, sibling)),
  )
}