// Copyright 2018-2026 the Deno authors. MIT license.

use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::collections::hash_map::Entry;
use std::ffi::OsString;
use std::fmt;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;

use deno_path_util::normalize_path;
use deno_path_util::strip_unc_prefix;
use deno_runtime::colors;
use deno_runtime::deno_core::anyhow::Context;
use deno_runtime::deno_core::anyhow::bail;
use deno_runtime::deno_core::error::AnyError;
use indexmap::IndexSet;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use serde::de;
use serde::de::SeqAccess;
use serde::de::Visitor;

use crate::util::text_encoding::is_valid_utf8;

#[derive(Debug, PartialEq, Eq)]
pub enum WindowsSystemRootablePath {
  /// The root of the system above any drive letters.
  WindowSystemRoot,
  Path(PathBuf),
}

impl WindowsSystemRootablePath {
  pub fn root_for_current_os() -> Self {
    if cfg!(windows) {
      WindowsSystemRootablePath::WindowSystemRoot
    } else {
      WindowsSystemRootablePath::Path(PathBuf::from("/"))
    }
  }

  pub fn join(&self, name_component: &str) -> PathBuf {
    // this method doesn't handle multiple components
    debug_assert!(
      !name_component.contains('\\'),
      "Invalid component: {}",
      name_component
    );
    debug_assert!(
      !name_component.contains('/'),
      "Invalid component: {}",
      name_component
    );

    match self {
      WindowsSystemRootablePath::WindowSystemRoot => {
        // windows drive letter
        PathBuf::from(&format!("{}\\", name_component))
      }
      WindowsSystemRootablePath::Path(path) => path.join(name_component),
    }
  }
}

#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum FileSystemCaseSensitivity {
  #[serde(rename = "s")]
  Sensitive,
  #[serde(rename = "i")]
  Insensitive,
}

impl FileSystemCaseSensitivity {
  pub fn cmp_name(&self, a: &str, b: &str) -> Ordering {
    match self {
      FileSystemCaseSensitivity::Sensitive => a.cmp(b),
      FileSystemCaseSensitivity::Insensitive => a
        .chars()
        .zip(b.chars())
        .map(|(a, b)| a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()))
        .find(|&ord| ord != Ordering::Equal)
        .unwrap_or_else(|| a.len().cmp(&b.len())),
    }
  }

  pub fn eq_name(&self, a: &str, b: &str) -> bool {
    self.cmp_name(a, b) == Ordering::Equal
  }
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct VirtualDirectoryEntries(Vec<VfsEntry>);

impl VirtualDirectoryEntries {
  pub fn new(mut entries: Vec<VfsEntry>) -> Self {
    // needs to be sorted by name
    entries.sort_by(|a, b| a.name().cmp(b.name()));
    Self(entries)
  }

  pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, VfsEntry> {
    self.0.iter_mut()
  }

  pub fn iter(&self) -> std::slice::Iter<'_, VfsEntry> {
    self.0.iter()
  }

  pub fn take_inner(&mut self) -> Vec<VfsEntry> {
    std::mem::take(&mut self.0)
  }

  pub fn is_empty(&self) -> bool {
    self.0.is_empty()
  }

  pub fn len(&self) -> usize {
    self.0.len()
  }

  pub fn get_by_name(
    &self,
    name: &str,
    case_sensitivity: FileSystemCaseSensitivity,
  ) -> Option<&VfsEntry> {
    self
      .binary_search(name, case_sensitivity)
      .ok()
      .map(|index| &self.0[index])
  }

  pub fn get_mut_by_name(
    &mut self,
    name: &str,
    case_sensitivity: FileSystemCaseSensitivity,
  ) -> Option<&mut VfsEntry> {
    self
      .binary_search(name, case_sensitivity)
      .ok()
      .map(|index| &mut self.0[index])
  }

  pub fn get_mut_by_index(&mut self, index: usize) -> Option<&mut VfsEntry> {
    self.0.get_mut(index)
  }

  pub fn get_by_index(&self, index: usize) -> Option<&VfsEntry> {
    self.0.get(index)
  }

  pub fn binary_search(
    &self,
    name: &str,
    case_sensitivity: FileSystemCaseSensitivity,
  ) -> Result<usize, usize> {
    self
      .0
      .binary_search_by(|e| case_sensitivity.cmp_name(e.name(), name))
  }

  pub fn insert(
    &mut self,
    entry: VfsEntry,
    case_sensitivity: FileSystemCaseSensitivity,
  ) -> usize {
    match self.binary_search(entry.name(), case_sensitivity) {
      Ok(index) => {
        self.0[index] = entry;
        index
      }
      Err(insert_index) => {
        self.0.insert(insert_index, entry);
        insert_index
      }
    }
  }

  pub fn insert_or_modify(
    &mut self,
    name: &str,
    case_sensitivity: FileSystemCaseSensitivity,
    on_insert: impl FnOnce() -> VfsEntry,
    on_modify: impl FnOnce(&mut VfsEntry),
  ) -> usize {
    match self.binary_search(name, case_sensitivity) {
      Ok(index) => {
        on_modify(&mut self.0[index]);
        index
      }
      Err(insert_index) => {
        self.0.insert(insert_index, on_insert());
        insert_index
      }
    }
  }

  pub fn remove(&mut self, index: usize) -> VfsEntry {
    self.0.remove(index)
  }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VirtualDirectory {
  #[serde(rename = "n")]
  pub name: String,
  // should be sorted by name
  #[serde(rename = "e")]
  pub entries: VirtualDirectoryEntries,
}

#[derive(Debug, Clone, Copy)]
pub struct OffsetWithLength {
  pub offset: u64,
  pub len: u64,
}

// serialize as an array in order to save space
impl Serialize for OffsetWithLength {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    let array = [self.offset, self.len];
    array.serialize(serializer)
  }
}

impl<'de> Deserialize<'de> for OffsetWithLength {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    struct OffsetWithLengthVisitor;

    impl<'de> Visitor<'de> for OffsetWithLengthVisitor {
      type Value = OffsetWithLength;

      fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("an array with two elements: [offset, len]")
      }

      fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
      where
        A: SeqAccess<'de>,
      {
        let offset = seq
          .next_element()?
          .ok_or_else(|| de::Error::invalid_length(0, &self))?;
        let len = seq
          .next_element()?
          .ok_or_else(|| de::Error::invalid_length(1, &self))?;
        Ok(OffsetWithLength { offset, len })
      }
    }

    deserializer.deserialize_seq(OffsetWithLengthVisitor)
  }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirtualFile {
  #[serde(rename = "n")]
  pub name: String,
  #[serde(rename = "o")]
  pub offset: OffsetWithLength,
  #[serde(default, rename = "u", skip_serializing_if = "is_false")]
  pub is_valid_utf8: bool,
  #[serde(rename = "m", skip_serializing_if = "Option::is_none")]
  pub transpiled_offset: Option<OffsetWithLength>,
  #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
  pub cjs_export_analysis_offset: Option<OffsetWithLength>,
  #[serde(rename = "s", skip_serializing_if = "Option::is_none")]
  pub source_map_offset: Option<OffsetWithLength>,
  #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
  pub mtime: Option<u128>, // mtime in milliseconds
  #[serde(default, rename = "x", skip_serializing_if = "is_false")]
  pub executable: bool,
}

fn is_false(value: &bool) -> bool {
  !value
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VirtualSymlinkParts(Vec<String>);

impl VirtualSymlinkParts {
  pub fn from_path(path: &Path) -> Self {
    Self(
      path
        .components()
        .filter(|c| !matches!(c, std::path::Component::RootDir))
        .map(|c| c.as_os_str().to_string_lossy().into_owned())
        .collect(),
    )
  }

  pub fn take_parts(&mut self) -> Vec<String> {
    std::mem::take(&mut self.0)
  }

  pub fn parts(&self) -> &[String] {
    &self.0
  }

  pub fn set_parts(&mut self, parts: Vec<String>) {
    self.0 = parts;
  }

  pub fn display(&self) -> String {
    self.0.join("/")
  }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VirtualSymlink {
  #[serde(rename = "n")]
  pub name: String,
  #[serde(rename = "p")]
  pub dest_parts: VirtualSymlinkParts,
  /// Whether the symlink target is a directory (needed on Windows where
  /// file and directory symlinks are distinct).
  #[serde(rename = "d", default, skip_serializing_if = "std::ops::Not::not")]
  pub dest_is_dir: bool,
}

impl VirtualSymlink {
  pub fn resolve_dest_from_root(&self, root: &Path) -> PathBuf {
    let mut dest = root.to_path_buf();
    for part in &self.dest_parts.0 {
      dest.push(part);
    }
    dest
  }
}

#[derive(Debug, Copy, Clone)]
pub enum VfsEntryRef<'a> {
  Dir(&'a VirtualDirectory),
  File(&'a VirtualFile),
  Symlink(&'a VirtualSymlink),
}

impl VfsEntryRef<'_> {
  pub fn name(&self) -> &str {
    match self {
      Self::Dir(dir) => &dir.name,
      Self::File(file) => &file.name,
      Self::Symlink(symlink) => &symlink.name,
    }
  }
}

// todo(dsherret): we should store this more efficiently in the binary
#[derive(Debug, Serialize, Deserialize)]
pub enum VfsEntry {
  Dir(VirtualDirectory),
  File(VirtualFile),
  Symlink(VirtualSymlink),
}

impl VfsEntry {
  pub fn name(&self) -> &str {
    match self {
      Self::Dir(dir) => &dir.name,
      Self::File(file) => &file.name,
      Self::Symlink(symlink) => &symlink.name,
    }
  }

  pub fn as_ref(&self) -> VfsEntryRef<'_> {
    match self {
      VfsEntry::Dir(dir) => VfsEntryRef::Dir(dir),
      VfsEntry::File(file) => VfsEntryRef::File(file),
      VfsEntry::Symlink(symlink) => VfsEntryRef::Symlink(symlink),
    }
  }
}

pub static DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME: &str =
  ".deno_compile_node_modules";

#[derive(Debug)]
pub struct BuiltVfs {
  pub root_path: WindowsSystemRootablePath,
  pub case_sensitivity: FileSystemCaseSensitivity,
  pub entries: VirtualDirectoryEntries,
  pub files: Vec<Vec<u8>>,
}

#[derive(Debug, Default)]
struct FilesData {
  files: Vec<Vec<u8>>,
  current_offset: u64,
  file_offsets: HashMap<(String, usize), OffsetWithLength>,
}

impl FilesData {
  pub fn file_bytes(&self, offset: OffsetWithLength) -> Option<&[u8]> {
    if offset.len == 0 {
      return Some(&[]);
    }

    // the debug assertions in this method should never happen
    // because it would indicate providing an offset not in the vfs
    let mut count: u64 = 0;
    for file in &self.files {
      // clippy wanted a match
      match count.cmp(&offset.offset) {
        Ordering::Equal => {
          debug_assert_eq!(offset.len, file.len() as u64);
          if offset.len == file.len() as u64 {
            return Some(file);
          } else {
            return None;
          }
        }
        Ordering::Less => {
          count += file.len() as u64;
        }
        Ordering::Greater => {
          debug_assert!(false);
          return None;
        }
      }
    }
    debug_assert!(false);
    None
  }

  pub fn add_data(&mut self, data: Vec<u8>) -> OffsetWithLength {
    if data.is_empty() {
      return OffsetWithLength { offset: 0, len: 0 };
    }
    let checksum = crate::util::checksum::r#gen(&[&data]);
    match self.file_offsets.entry((checksum, data.len())) {
      Entry::Occupied(occupied_entry) => {
        let offset_and_len = *occupied_entry.get();
        debug_assert_eq!(data.len() as u64, offset_and_len.len);
        offset_and_len
      }
      Entry::Vacant(vacant_entry) => {
        let offset_and_len = OffsetWithLength {
          offset: self.current_offset,
          len: data.len() as u64,
        };
        vacant_entry.insert(offset_and_len);
        self.current_offset += offset_and_len.len;
        self.files.push(data);
        offset_and_len
      }
    }
  }
}

struct CanonicalPath<'a>(Cow<'a, Path>);

impl CanonicalPath<'_> {
  pub fn as_ref(&self) -> &Path {
    self.0.as_ref()
  }

  pub fn display(&self) -> std::path::Display<'_> {
    self.0.display()
  }

  pub fn parent(&self) -> CanonicalPath<'_> {
    CanonicalPath(Cow::Borrowed(self.0.parent().unwrap()))
  }

  pub fn join_directory_entry(
    &self,
    name: OsString,
  ) -> CanonicalParentPath<'_> {
    CanonicalParentPath(Cow::Owned(self.0.join(name)))
  }
}

struct CanonicalParentPath<'a>(Cow<'a, Path>);

impl CanonicalParentPath<'_> {
  pub fn as_ref(&self) -> &Path {
    self.0.as_ref()
  }

  pub fn display(&self) -> std::path::Display<'_> {
    self.0.display()
  }

  pub fn parent(&self) -> CanonicalPath<'_> {
    CanonicalPath(Cow::Borrowed(self.0.parent().unwrap()))
  }

  pub fn assert_canonical_path(&self) -> CanonicalPath<'_> {
    CanonicalPath(Cow::Borrowed(self.as_ref()))
  }
}

pub struct AddFileDataOptions {
  pub data: Vec<u8>,
  pub mtime: Option<SystemTime>,
  pub maybe_transpiled: Option<Vec<u8>>,
  pub maybe_source_map: Option<Vec<u8>>,
  pub maybe_cjs_export_analysis: Option<Vec<u8>>,
}

#[derive(Debug)]
pub struct VfsBuilder {
  executable_root: VirtualDirectory,
  files: FilesData,
  /// The minimum root directory that should be included in the VFS.
  min_root_dir: Option<WindowsSystemRootablePath>,
  case_sensitivity: FileSystemCaseSensitivity,
  exclude_paths: HashSet<PathBuf>,
  /// Cache of path canonicalization results. Maps original paths to their
  /// canonical forms, used to detect aliased parent directories.
  /// - `None` = path is already canonical (no changes needed)
  /// - `Some(path)` = path was aliased, use this canonical form instead
  canonical_path_cache: HashMap<PathBuf, Option<PathBuf>>,
}

impl Default for VfsBuilder {
  fn default() -> Self {
    Self::new()
  }
}

impl VfsBuilder {
  pub fn new() -> Self {
    Self {
      executable_root: VirtualDirectory {
        name: "/".to_string(),
        entries: Default::default(),
      },
      files: Default::default(),
      min_root_dir: Default::default(),
      // This is not exactly correct because file systems on these OSes
      // may be case-sensitive or not based on the directory, but this
      // is a good enough approximation and limitation. In the future,
      // we may want to store this information per directory instead
      // depending on the feedback we get.
      case_sensitivity: if cfg!(windows) || cfg!(target_os = "macos") {
        FileSystemCaseSensitivity::Insensitive
      } else {
        FileSystemCaseSensitivity::Sensitive
      },
      exclude_paths: Default::default(),
      canonical_path_cache: Default::default(),
    }
  }

  pub fn case_sensitivity(&self) -> FileSystemCaseSensitivity {
    self.case_sensitivity
  }

  pub fn files_len(&self) -> usize {
    self.files.files.len()
  }

  pub fn file_bytes(&self, offset: OffsetWithLength) -> Option<&[u8]> {
    self.files.file_bytes(offset)
  }

  pub fn add_exclude_path(&mut self, path: PathBuf) {
    self.exclude_paths.insert(path);
  }

  /// Add a directory that might be the minimum root directory
  /// of the VFS.
  ///
  /// For example, say the user has a deno.json and specifies an
  /// import map in a parent directory. The import map won't be
  /// included in the VFS, but its base will meaning we need to
  /// tell the VFS builder to include the base of the import map
  /// by calling this method.
  pub fn add_possible_min_root_dir(&mut self, path: &Path) {
    let path = self.ensure_canonical_dir(path);
    self.add_dir_raw(&path);

    match &self.min_root_dir {
      Some(WindowsSystemRootablePath::WindowSystemRoot) => {
        // already the root dir
      }
      Some(WindowsSystemRootablePath::Path(current_path)) => {
        let mut common_components = Vec::new();
        for (a, b) in current_path.components().zip(path.as_ref().components())
        {
          if a != b {
            break;
          }
          common_components.push(a);
        }
        if common_components.is_empty() {
          self.min_root_dir =
            Some(WindowsSystemRootablePath::root_for_current_os());
        } else {
          self.min_root_dir = Some(WindowsSystemRootablePath::Path(
            common_components.iter().collect(),
          ));
        }
      }
      None => {
        self.min_root_dir =
          Some(WindowsSystemRootablePath::Path(path.0.into_owned()));
      }
    }
  }

  pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> {
    if self.exclude_paths.contains(path) {
      return Ok(());
    }
    let canonical = self.ensure_canonical_dir(path);
    self.add_dir_recursive_not_symlink(&canonical)
  }

  fn add_dir_recursive_not_symlink(
    &mut self,
    dir_path: &CanonicalPath,
  ) -> Result<(), AnyError> {
    if self.exclude_paths.contains(dir_path.as_ref()) {
      return Ok(());
    }
    self.add_dir_raw(dir_path);
    #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
    let read_dir = std::fs::read_dir(dir_path.as_ref())
      .with_context(|| format!("Reading {}", dir_path.display()))?;

    let mut dir_entries =
      read_dir.into_iter().collect::<Result<Vec<_>, _>>()?;
    dir_entries.sort_by_cached_key(|entry| entry.file_name()); // determinism

    for entry in dir_entries {
      let file_type = entry.file_type()?;
      let file_name = entry.file_name();
      let file_path = dir_path.join_directory_entry(file_name);
      self.add_path_with_file_type(&file_path, file_type)?;
    }

    Ok(())
  }

  pub fn add_path(&mut self, path: &Path) -> Result<(), AnyError> {
    let path = self.ensure_canonical_parent_path(path);
    #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
    let file_type = path.as_ref().symlink_metadata()?.file_type();
    self.add_path_with_file_type(&path, file_type)
  }

  fn add_path_with_file_type(
    &mut self,
    path: &CanonicalParentPath,
    file_type: std::fs::FileType,
  ) -> Result<(), AnyError> {
    if self.exclude_paths.contains(path.as_ref()) {
      return Ok(());
    }
    if file_type.is_dir() {
      self.add_dir_recursive_not_symlink(&path.assert_canonical_path())
    } else if file_type.is_file() {
      self.add_file_at_path_not_symlink(&path.assert_canonical_path())
    } else if file_type.is_symlink() {
      match self.add_symlink_for_canonical_parent_path(path) {
        Ok(target) => match &target {
          SymlinkTarget::File(_) => {
            self.add_file_at_path_not_symlink(&target.into_canonicalized())
          }
          SymlinkTarget::Dir(_) => {
            self.add_dir_recursive_not_symlink(&target.into_canonicalized())
          }
        },
        Err(err) => {
          log::warn!(
            "{} Failed resolving symlink. Ignoring.\n    Path: {}\n    Message: {:#}",
            colors::yellow("Warning"),
            path.display(),
            err
          );
          Ok(())
        }
      }
    } else {
      // ignore
      Ok(())
    }
  }

  fn add_dir_raw(&mut self, path: &CanonicalPath) -> &mut VirtualDirectory {
    log::debug!("Ensuring directory '{}'", path.display());
    debug_assert!(path.as_ref().is_absolute());
    let mut current_dir = &mut self.executable_root;

    for component in path.as_ref().components() {
      if matches!(component, std::path::Component::RootDir) {
        continue;
      }
      let name = component.as_os_str().to_string_lossy();
      let index = current_dir.entries.insert_or_modify(
        &name,
        self.case_sensitivity,
        || {
          VfsEntry::Dir(VirtualDirectory {
            name: name.to_string(),
            entries: Default::default(),
          })
        },
        |_| {
          // ignore
        },
      );
      match current_dir.entries.get_mut_by_index(index) {
        Some(VfsEntry::Dir(dir)) => {
          current_dir = dir;
        }
        _ => unreachable!(),
      };
    }

    current_dir
  }

  pub fn get_system_root_dir_mut(&mut self) -> &mut VirtualDirectory {
    &mut self.executable_root
  }

  pub fn get_dir_mut(&mut self, path: &Path) -> Option<&mut VirtualDirectory> {
    debug_assert!(path.is_absolute());
    let mut current_dir = &mut self.executable_root;

    for component in path.components() {
      if matches!(component, std::path::Component::RootDir) {
        continue;
      }
      let name = component.as_os_str().to_string_lossy();
      let entry = current_dir
        .entries
        .get_mut_by_name(&name, self.case_sensitivity)?;
      match entry {
        VfsEntry::Dir(dir) => {
          current_dir = dir;
        }
        _ => unreachable!("{}", path.display()),
      };
    }

    Some(current_dir)
  }

  fn add_file_at_path_not_symlink(
    &mut self,
    path: &CanonicalPath,
  ) -> Result<(), AnyError> {
    if self.exclude_paths.contains(path.as_ref()) {
      return Ok(());
    }
    let (file_bytes, mtime) = self.read_file_bytes_and_mtime(path)?;
    self.add_canonical_file_with_data_raw(path, file_bytes, mtime)
  }

  fn read_file_bytes_and_mtime(
    &self,
    path: &CanonicalPath,
  ) -> Result<(Vec<u8>, Option<SystemTime>), AnyError> {
    #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
    {
      let mut file = std::fs::OpenOptions::new()
        .read(true)
        .open(path.as_ref())
        .with_context(|| format!("Opening {}", path.display()))?;
      let mtime = file.metadata().ok().and_then(|m| m.modified().ok());
      let mut file_bytes = Vec::new();
      file
        .read_to_end(&mut file_bytes)
        .with_context(|| format!("Reading {}", path.display()))?;
      Ok((file_bytes, mtime))
    }
  }

  /// Canonicalizes the parent directory of the given path and, if it
  /// differs from the original, adds VFS symlink entries for any aliased
  /// directory components (e.g. Windows 8.3 short names like
  /// RUNNER~1 -> runneradmin). Only the parent is canonicalized so that
  /// symlink files themselves are not followed.
  /// Returns the path with canonical parent, or the original path if
  /// canonicalization fails or produces the same result.
  fn ensure_canonical_parent_path<'a>(
    &mut self,
    path: &'a Path,
  ) -> CanonicalParentPath<'a> {
    if let Some(cached) = self.canonical_path_cache.get(path) {
      return match cached {
        Some(canonical) => CanonicalParentPath(Cow::Owned(canonical.clone())),
        None => CanonicalParentPath(Cow::Borrowed(path)),
      };
    }

    let Some(parent) = path.parent() else {
      return CanonicalParentPath(Cow::Borrowed(path));
    };
    let Some(file_name) = path.file_name() else {
      return CanonicalParentPath(Cow::Borrowed(path));
    };

    let canonical_parent =
      self.ensure_canonical_dir_inner(parent, &mut HashSet::new());
    if canonical_parent.as_ref() == parent {
      self.canonical_path_cache.insert(path.to_path_buf(), None);
      return CanonicalParentPath(Cow::Borrowed(path));
    }
    let result = canonical_parent.as_ref().join(file_name);
    self
      .canonical_path_cache
      .insert(path.to_path_buf(), Some(result.clone()));
    CanonicalParentPath(Cow::Owned(result))
  }

  /// Resolves a directory path by walking component by component, detecting
  /// symlinks (via `read_link`) and name aliases (e.g. Windows 8.3 short
  /// names). For each symlink or alias found, a VFS symlink entry is
  /// created so the VFS mirrors the real filesystem structure.
  /// Caches results (including negative) to avoid repeated filesystem calls.
  fn ensure_canonical_dir<'a>(
    &mut self,
    dir_path: &'a Path,
  ) -> CanonicalPath<'a> {
    self.ensure_canonical_dir_inner(dir_path, &mut HashSet::new())
  }

  fn ensure_canonical_dir_inner<'a>(
    &mut self,
    dir_path: &'a Path,
    visited: &mut HashSet<PathBuf>,
  ) -> CanonicalPath<'a> {
    if let Some(cached) = self.canonical_path_cache.get(dir_path) {
      return match cached {
        Some(canonical) => CanonicalPath(Cow::Owned(canonical.clone())),
        None => CanonicalPath(Cow::Borrowed(dir_path)),
      };
    }

    if !visited.insert(dir_path.to_path_buf()) {
      log::warn!(
        "Circular symlink detected while canonicalizing directory: {}",
        dir_path.display()
      );
      return CanonicalPath(Cow::Borrowed(dir_path));
    }

    let mut current_real = PathBuf::new();
    let mut changed = false;

    for component in dir_path.components() {
      if matches!(
        component,
        std::path::Component::RootDir | std::path::Component::Prefix(_)
      ) {
        current_real.push(component);
        continue;
      }

      let candidate = current_real.join(component.as_os_str());

      #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
      let is_symlink = std::fs::symlink_metadata(&candidate)
        .map(|m| m.is_symlink())
        .unwrap_or(false);

      if is_symlink {
        #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
        if let Ok(link_target) = std::fs::read_link(&candidate) {
          let resolved = normalize_path(Cow::Owned(
            current_real.join(strip_unc_prefix(link_target)),
          ));

          // recursively ensure the symlink target is canonical
          let resolved = self
            .ensure_canonical_dir_inner(&resolved, visited)
            .0
            .into_owned();

          // determine if the final target is a directory
          #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
          let dest_is_dir = std::fs::metadata(&candidate)
            .map(|m| m.is_dir())
            .unwrap_or(true);

          let name = component.as_os_str().to_string_lossy();
          let case_sensitivity = self.case_sensitivity;
          let dir =
            self.add_dir_raw(&CanonicalPath(Cow::Borrowed(&current_real)));
          dir.entries.insert_or_modify(
            &name,
            case_sensitivity,
            || {
              VfsEntry::Symlink(VirtualSymlink {
                name: name.to_string(),
                dest_parts: VirtualSymlinkParts::from_path(&resolved),
                dest_is_dir,
              })
            },
            |_| {
              // already exists
            },
          );

          log::debug!(
            "Resolved symlink component: {} -> {}",
            candidate.display(),
            resolved.display()
          );

          current_real = resolved;
          changed = true;
          continue;
        }
      }

      // not a symlink — on Windows, check for 8.3 short name aliases
      // by canonicalizing just this path and comparing the last component.
      // This is Windows-specific; on other platforms non-symlink components
      // won't have name aliases, and running this could create spurious VFS
      // symlinks for case differences on case-insensitive filesystems.
      #[cfg(windows)]
      {
        #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
        if let Ok(canonical) = std::fs::canonicalize(&candidate) {
          let canonical = strip_unc_prefix(canonical);
          if let Some(canon_name) = canonical.file_name() {
            let orig_name = component.as_os_str();
            if !self.case_sensitivity.eq_name(
              &orig_name.to_string_lossy(),
              &canon_name.to_string_lossy(),
            ) {
              let real_dir = current_real.join(canon_name);
              let orig_name_str = orig_name.to_string_lossy();
              let case_sensitivity = self.case_sensitivity;
              let dir =
                self.add_dir_raw(&CanonicalPath(Cow::Borrowed(&current_real)));
              dir.entries.insert_or_modify(
                &orig_name_str,
                case_sensitivity,
                || {
                  VfsEntry::Symlink(VirtualSymlink {
                    name: orig_name_str.to_string(),
                    dest_parts: VirtualSymlinkParts::from_path(&real_dir),
                    dest_is_dir: true,
                  })
                },
                |_| {
                  // already exists
                },
              );

              log::debug!(
                "Resolved 8.3 name alias: {} -> {}",
                candidate.display(),
                real_dir.display()
              );

              current_real = real_dir;
              changed = true;
              continue;
            }
          }
        }
      }

      current_real.push(component);
    }

    if changed {
      self
        .canonical_path_cache
        .insert(dir_path.to_path_buf(), Some(current_real.clone()));
      CanonicalPath(Cow::Owned(current_real))
    } else {
      self
        .canonical_path_cache
        .insert(dir_path.to_path_buf(), None);
      CanonicalPath(Cow::Borrowed(dir_path))
    }
  }

  pub fn add_file_with_data(
    &mut self,
    path: &Path,
    options: AddFileDataOptions,
  ) -> Result<(), AnyError> {
    // canonicalize parent to resolve aliased directory components
    // (e.g. Windows 8.3 short names), adding VFS symlinks as needed
    let path = self.ensure_canonical_parent_path(path);
    #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
    let metadata =
      std::fs::symlink_metadata(path.as_ref()).with_context(|| {
        format!("Resolving target path for '{}'", path.display())
      })?;
    if metadata.is_symlink() {
      let target = self
        .add_symlink_for_canonical_parent_path(&path)?
        .into_canonicalized();
      self.add_file_with_data_raw_options(&target, options)
    } else {
      self
        .add_file_with_data_raw_options(&path.assert_canonical_path(), options)
    }
  }

  pub fn add_file_with_data_raw_for_testing(
    &mut self,
    path: &Path,
    data: Vec<u8>,
    mtime: Option<SystemTime>,
  ) -> Result<(), AnyError> {
    let path = self.ensure_canonical_parent_path(path);
    self.add_canonical_file_with_data_raw(
      // not exactly correct, but only used for testing
      &path.assert_canonical_path(),
      data,
      mtime,
    )
  }

  fn add_canonical_file_with_data_raw(
    &mut self,
    path: &CanonicalPath,
    data: Vec<u8>,
    mtime: Option<SystemTime>,
  ) -> Result<(), AnyError> {
    self.add_file_with_data_raw_options(
      path,
      AddFileDataOptions {
        data,
        mtime,
        maybe_transpiled: None,
        maybe_cjs_export_analysis: None,
        maybe_source_map: None,
      },
    )
  }

  fn add_file_with_data_raw_options(
    &mut self,
    path: &CanonicalPath,
    options: AddFileDataOptions,
  ) -> Result<(), AnyError> {
    log::debug!("Adding file '{}'", path.display());
    let case_sensitivity = self.case_sensitivity;

    let is_valid_utf8 = is_valid_utf8(&options.data);
    let offset_and_len = self.files.add_data(options.data);
    let transpiled_offset = options
      .maybe_transpiled
      .map(|data| self.files.add_data(data));
    let source_map_offset = options
      .maybe_source_map
      .map(|data| self.files.add_data(data));
    let cjs_export_analysis_offset = options
      .maybe_cjs_export_analysis
      .map(|data| self.files.add_data(data));
    let dir = self.add_dir_raw(&path.parent());
    let name = path.as_ref().file_name().unwrap().to_string_lossy();

    let mtime = options
      .mtime
      .and_then(|mtime| mtime.duration_since(std::time::UNIX_EPOCH).ok())
      .map(|m| m.as_millis());

    let executable = {
      #[cfg(unix)]
      {
        use std::os::unix::fs::PermissionsExt;
        #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
        std::fs::metadata(path.as_ref())
          .map(|m| m.permissions().mode() & 0o111 != 0)
          .unwrap_or(false)
      }
      #[cfg(not(unix))]
      {
        false
      }
    };

    dir.entries.insert_or_modify(
      &name,
      case_sensitivity,
      || {
        VfsEntry::File(VirtualFile {
          name: name.to_string(),
          is_valid_utf8,
          offset: offset_and_len,
          transpiled_offset,
          cjs_export_analysis_offset,
          source_map_offset,
          mtime,
          executable,
        })
      },
      |entry| match entry {
        VfsEntry::File(virtual_file) => {
          virtual_file.offset = offset_and_len;
          // doesn't overwrite to None
          if transpiled_offset.is_some() {
            virtual_file.transpiled_offset = transpiled_offset;
          }
          if source_map_offset.is_some() {
            virtual_file.source_map_offset = source_map_offset;
          }
          if cjs_export_analysis_offset.is_some() {
            virtual_file.cjs_export_analysis_offset =
              cjs_export_analysis_offset;
          }
          virtual_file.mtime = mtime;
        }
        VfsEntry::Dir(_) | VfsEntry::Symlink(_) => unreachable!(),
      },
    );

    Ok(())
  }

  pub fn add_symlink(
    &mut self,
    path: &Path,
  ) -> Result<SymlinkTarget, AnyError> {
    let path = self.ensure_canonical_parent_path(path);
    self.add_symlink_for_canonical_parent_path(&path)
  }

  fn add_symlink_for_canonical_parent_path(
    &mut self,
    path: &CanonicalParentPath,
  ) -> Result<SymlinkTarget, AnyError> {
    self.add_symlink_inner(path, &mut IndexSet::new())
  }

  fn add_symlink_inner(
    &mut self,
    path: &CanonicalParentPath,
    visited: &mut IndexSet<PathBuf>,
  ) -> Result<SymlinkTarget, AnyError> {
    log::debug!("Adding symlink '{}'", path.display());
    let target = strip_unc_prefix(
      #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
      std::fs::read_link(path.as_ref())
        .with_context(|| format!("Reading symlink '{}'", path.display()))?,
    );
    let case_sensitivity = self.case_sensitivity;
    let target =
      normalize_path(Cow::Owned(path.as_ref().parent().unwrap().join(&target)));
    let target = self.ensure_canonical_parent_path(&target);
    // use metadata (follows symlinks) to determine final target type
    #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
    let dest_is_dir = std::fs::metadata(target.as_ref())
      .map(|m| m.is_dir())
      .unwrap_or(false);
    let dir = self.add_dir_raw(&path.parent());
    let name = path.as_ref().file_name().unwrap().to_string_lossy();
    dir.entries.insert_or_modify(
      &name,
      case_sensitivity,
      || {
        VfsEntry::Symlink(VirtualSymlink {
          name: name.to_string(),
          dest_parts: VirtualSymlinkParts::from_path(target.as_ref()),
          dest_is_dir,
        })
      },
      |_| {
        // ignore previously inserted
      },
    );
    #[allow(clippy::disallowed_methods, reason = "ok, creating vfs")]
    let target_metadata = std::fs::symlink_metadata(target.as_ref())
      .with_context(|| {
        format!("Reading symlink target '{}'", target.display())
      })?;
    if target_metadata.is_symlink() {
      if !visited.insert(target.as_ref().to_path_buf()) {
        // todo: probably don't error in this scenario
        bail!(
          "Circular symlink detected: {} -> {}",
          visited
            .iter()
            .map(|p| p.display().to_string())
            .collect::<Vec<_>>()
            .join(" -> "),
          target.display()
        );
      }
      self.add_symlink_inner(&target, visited)
    } else if target_metadata.is_dir() {
      Ok(SymlinkTarget::Dir(target.0.into_owned()))
    } else {
      Ok(SymlinkTarget::File(target.0.into_owned()))
    }
  }

  /// Adds the CJS export analysis to the provided file.
  ///
  /// Warning: This will panic if the file wasn't properly
  /// setup before calling this.
  pub fn add_cjs_export_analysis(&mut self, path: &Path, data: Vec<u8>) {
    self.add_data_for_file_or_panic(path, data, |file, offset_with_length| {
      file.cjs_export_analysis_offset = Some(offset_with_length);
    })
  }

  fn add_data_for_file_or_panic(
    &mut self,
    path: &Path,
    data: Vec<u8>,
    update_file: impl FnOnce(&mut VirtualFile, OffsetWithLength),
  ) {
    debug_assert_eq!(self.ensure_canonical_parent_path(path).as_ref(), path);
    let offset_with_length = self.files.add_data(data);
    let case_sensitivity = self.case_sensitivity;
    let parent = path.parent().unwrap();
    let dir = self.get_dir_mut(parent).unwrap();
    let name = path.file_name().unwrap().to_string_lossy();
    let file = dir
      .entries
      .get_mut_by_name(&name, case_sensitivity)
      .unwrap();
    match file {
      VfsEntry::File(virtual_file) => {
        update_file(virtual_file, offset_with_length);
      }
      VfsEntry::Dir(_) | VfsEntry::Symlink(_) => {
        unreachable!()
      }
    }
  }

  /// Iterates through all the files in the virtual file system.
  pub fn iter_files(
    &self,
  ) -> impl Iterator<Item = (PathBuf, &VirtualFile)> + '_ {
    FileIterator {
      pending_dirs: VecDeque::from([(
        WindowsSystemRootablePath::root_for_current_os(),
        &self.executable_root,
      )]),
      current_dir_index: 0,
    }
  }

  pub fn build(self) -> BuiltVfs {
    fn strip_prefix_from_symlinks(
      dir: &mut VirtualDirectory,
      parts: &[String],
    ) {
      for entry in dir.entries.iter_mut() {
        match entry {
          VfsEntry::Dir(dir) => {
            strip_prefix_from_symlinks(dir, parts);
          }
          VfsEntry::File(_) => {}
          VfsEntry::Symlink(symlink) => {
            let parts = symlink
              .dest_parts
              .take_parts()
              .into_iter()
              .skip(parts.len())
              .collect();
            symlink.dest_parts.set_parts(parts);
          }
        }
      }
    }

    let mut current_dir = self.executable_root;
    let mut current_path = WindowsSystemRootablePath::root_for_current_os();
    loop {
      if current_dir.entries.len() != 1 {
        break;
      }
      if self.min_root_dir.as_ref() == Some(&current_path) {
        break;
      }
      match current_dir.entries.iter().next().unwrap() {
        VfsEntry::Dir(dir) => {
          if dir.name == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME {
            // special directory we want to maintain
            break;
          }
          match current_dir.entries.remove(0) {
            VfsEntry::Dir(dir) => {
              current_path =
                WindowsSystemRootablePath::Path(current_path.join(&dir.name));
              current_dir = dir;
            }
            _ => unreachable!(),
          };
        }
        VfsEntry::File(_) | VfsEntry::Symlink(_) => break,
      }
    }
    if let WindowsSystemRootablePath::Path(path) = &current_path {
      strip_prefix_from_symlinks(
        &mut current_dir,
        VirtualSymlinkParts::from_path(path).parts(),
      );
    }
    BuiltVfs {
      root_path: current_path,
      case_sensitivity: self.case_sensitivity,
      entries: current_dir.entries,
      files: self.files.files,
    }
  }
}

struct FileIterator<'a> {
  pending_dirs: VecDeque<(WindowsSystemRootablePath, &'a VirtualDirectory)>,
  current_dir_index: usize,
}

impl<'a> Iterator for FileIterator<'a> {
  type Item = (PathBuf, &'a VirtualFile);

  fn next(&mut self) -> Option<Self::Item> {
    while !self.pending_dirs.is_empty() {
      let (dir_path, current_dir) = self.pending_dirs.front()?;
      if let Some(entry) =
        current_dir.entries.get_by_index(self.current_dir_index)
      {
        self.current_dir_index += 1;
        match entry {
          VfsEntry::Dir(virtual_directory) => {
            self.pending_dirs.push_back((
              WindowsSystemRootablePath::Path(
                dir_path.join(&virtual_directory.name),
              ),
              virtual_directory,
            ));
          }
          VfsEntry::File(virtual_file) => {
            return Some((dir_path.join(&virtual_file.name), virtual_file));
          }
          VfsEntry::Symlink(_) => {
            // ignore
          }
        }
      } else {
        self.pending_dirs.pop_front();
        self.current_dir_index = 0;
      }
    }
    None
  }
}

#[derive(Debug)]
pub enum SymlinkTarget {
  File(PathBuf),
  Dir(PathBuf),
}

impl SymlinkTarget {
  pub fn into_path_buf(self) -> PathBuf {
    match self {
      Self::File(path) => path,
      Self::Dir(path) => path,
    }
  }

  fn into_canonicalized(self) -> CanonicalPath<'static> {
    // the target will always be canonicalized
    CanonicalPath(Cow::Owned(self.into_path_buf()))
  }
}
