wxtla 0.3.1

Wired eXploring Target Layer Accessor
Documentation
//! NTFS reparse-point parsing.

use crate::{Error, Result};

const REPARSE_TAG_MOUNT_POINT: u32 = 0xA000_0003;
const REPARSE_TAG_SYMLINK: u32 = 0xA000_000C;
const REPARSE_TAG_COMPRESSED: u32 = 0x8000_0017;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NtfsReparsePointKind {
  MountPoint,
  SymbolicLink,
  WofCompressed,
  Other,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NtfsReparsePointInfo {
  pub kind: NtfsReparsePointKind,
  pub tag: u32,
  pub substitute_name: Option<String>,
  pub print_name: Option<String>,
  pub flags: Option<u32>,
  pub compression_method: Option<u32>,
}

impl NtfsReparsePointInfo {
  pub fn preferred_target(&self) -> Option<String> {
    matches!(
      self.kind,
      NtfsReparsePointKind::MountPoint | NtfsReparsePointKind::SymbolicLink
    )
    .then(|| {
      self
        .print_name
        .clone()
        .filter(|name| !name.is_empty())
        .or_else(|| {
          self
            .substitute_name
            .as_deref()
            .map(normalize_substitute_name)
        })
    })
    .flatten()
  }

  pub fn is_link_like(&self) -> bool {
    matches!(
      self.kind,
      NtfsReparsePointKind::MountPoint | NtfsReparsePointKind::SymbolicLink
    )
  }
}

pub(crate) fn parse_reparse_point(bytes: &[u8]) -> Result<NtfsReparsePointInfo> {
  if bytes.len() < 8 {
    return Err(Error::invalid_format(
      "ntfs reparse point is too small".to_string(),
    ));
  }

  let tag = le_u32(&bytes[0..4]);
  let reparse_data_size = usize::from(le_u16(&bytes[4..6]));
  if reparse_data_size > bytes.len() - 8 {
    return Err(Error::invalid_format(
      "ntfs reparse-point payload exceeds the attribute bounds".to_string(),
    ));
  }
  let reparse_data = &bytes[8..8 + reparse_data_size];

  let mut info = NtfsReparsePointInfo {
    kind: match tag {
      REPARSE_TAG_MOUNT_POINT => NtfsReparsePointKind::MountPoint,
      REPARSE_TAG_SYMLINK => NtfsReparsePointKind::SymbolicLink,
      REPARSE_TAG_COMPRESSED => NtfsReparsePointKind::WofCompressed,
      _ => NtfsReparsePointKind::Other,
    },
    tag,
    substitute_name: None,
    print_name: None,
    flags: None,
    compression_method: None,
  };

  match tag {
    REPARSE_TAG_COMPRESSED => {
      if reparse_data.len() < 16 {
        return Err(Error::invalid_format(
          "compressed ntfs reparse-point payload is truncated".to_string(),
        ));
      }
      info.compression_method = Some(le_u32(&reparse_data[12..16]));
    }
    REPARSE_TAG_MOUNT_POINT | REPARSE_TAG_SYMLINK => {
      let header_size = if tag == REPARSE_TAG_SYMLINK {
        12usize
      } else {
        8usize
      };
      if reparse_data.len() < header_size {
        return Err(Error::invalid_format(
          "ntfs mount-point/symlink reparse payload is truncated".to_string(),
        ));
      }
      let substitute_name_offset = usize::from(le_u16(&reparse_data[0..2])) + header_size;
      let substitute_name_size = usize::from(le_u16(&reparse_data[2..4]));
      let print_name_offset = usize::from(le_u16(&reparse_data[4..6])) + header_size;
      let print_name_size = usize::from(le_u16(&reparse_data[6..8]));
      if tag == REPARSE_TAG_SYMLINK {
        info.flags = Some(le_u32(&reparse_data[8..12]));
      }

      info.substitute_name = read_utf16le_component(
        reparse_data,
        substitute_name_offset,
        substitute_name_size,
        "ntfs reparse substitute name",
      )?;
      info.print_name = read_utf16le_component(
        reparse_data,
        print_name_offset,
        print_name_size,
        "ntfs reparse print name",
      )?;
    }
    _ => {}
  }

  Ok(info)
}

fn read_utf16le_component(
  bytes: &[u8], offset: usize, size: usize, label: &str,
) -> Result<Option<String>> {
  if size == 0 {
    return Ok(None);
  }
  if !size.is_multiple_of(2) {
    return Err(Error::invalid_format(format!(
      "{label} has an odd byte length"
    )));
  }
  let end = offset
    .checked_add(size)
    .ok_or_else(|| Error::invalid_range(format!("{label} end overflow")))?;
  let slice = bytes
    .get(offset..end)
    .ok_or_else(|| Error::invalid_format(format!("{label} exceeds the payload bounds")))?;
  let units = slice
    .chunks_exact(2)
    .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
    .collect::<Vec<_>>();
  Ok(Some(String::from_utf16_lossy(&units)))
}

fn normalize_substitute_name(name: &str) -> String {
  name.strip_prefix(r"\??\").unwrap_or(name).to_string()
}

fn le_u16(bytes: &[u8]) -> u16 {
  let mut raw = [0u8; 2];
  raw.copy_from_slice(bytes);
  u16::from_le_bytes(raw)
}

fn le_u32(bytes: &[u8]) -> u32 {
  let mut raw = [0u8; 4];
  raw.copy_from_slice(bytes);
  u32::from_le_bytes(raw)
}

#[cfg(test)]
mod tests {
  use std::path::Path;

  use super::*;

  fn fixture_path(relative: &str) -> std::path::PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
      .join("formats")
      .join("ntfs")
      .join("libfsntfs")
      .join(relative)
  }

  #[test]
  fn parses_libfsntfs_reparse_fixture() {
    let bytes = std::fs::read(fixture_path("reparse_point_values.1")).unwrap();
    let info = parse_reparse_point(&bytes[24..]).unwrap();

    assert_eq!(info.kind, NtfsReparsePointKind::MountPoint);
    assert_eq!(info.tag, REPARSE_TAG_MOUNT_POINT);
    assert_eq!(info.substitute_name.as_deref(), Some(r"\??\C:\Users"));
    assert_eq!(info.print_name.as_deref(), Some(r"C:\Users"));
    assert_eq!(info.preferred_target().as_deref(), Some(r"C:\Users"));
    assert_eq!(info.flags, None);
  }

  #[test]
  fn parses_wof_reparse_points_without_a_target() {
    let info = parse_reparse_point(&[
      0x17, 0x00, 0x00, 0x80, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
      0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
    ])
    .unwrap();

    assert_eq!(info.kind, NtfsReparsePointKind::WofCompressed);
    assert_eq!(info.compression_method, Some(3));
    assert_eq!(info.preferred_target(), None);
  }

  #[test]
  fn leaves_unknown_reparse_points_as_non_link_targets() {
    let info = parse_reparse_point(&[0x34, 0x12, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00]).unwrap();

    assert_eq!(info.kind, NtfsReparsePointKind::Other);
    assert!(!info.is_link_like());
    assert_eq!(info.preferred_target(), None);
  }
}