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

use std::borrow::Cow;
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Formatter;
use std::io;
use std::io::SeekFrom;
use std::path::Path;
use std::path::PathBuf;
use std::path::StripPrefixError;
use std::rc::Rc;

use boxed_error::Boxed;
use deno_core::CancelFuture;
use deno_core::CancelHandle;
use deno_core::FastString;
use deno_core::FromV8;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::ToV8;
use deno_core::convert::Uint8Array;
use deno_core::error::ResourceError;
use deno_core::op2;
use deno_error::JsErrorBox;
use deno_io::fs::FileResource;
use deno_io::fs::FsError;
use deno_io::fs::FsStat;
use deno_permissions::CheckedPath;
use deno_permissions::CheckedPathBuf;
use deno_permissions::OpenAccessKind;
use deno_permissions::PathWithRequested;
use deno_permissions::PermissionCheckError;
use rand::Rng;
use rand::rngs::ThreadRng;
use rand::thread_rng;

use crate::OpenOptions;
use crate::interface::FileSystemRc;
use crate::interface::FsDirEntry;
use crate::interface::FsFileType;
use crate::interface::FsReadDirRc;

#[derive(Debug, Boxed, deno_error::JsError)]
pub struct FsOpsError(pub Box<FsOpsErrorKind>);

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum FsOpsErrorKind {
  #[class(inherit)]
  #[error("{0}")]
  Io(#[source] std::io::Error),
  #[class(inherit)]
  #[error("{0}")]
  OperationError(#[source] OperationError),
  #[class(inherit)]
  #[error(transparent)]
  Permission(#[from] PermissionCheckError),
  #[class(inherit)]
  #[error(transparent)]
  Resource(#[from] ResourceError),
  #[class("InvalidData")]
  #[error("File name or path {0:?} is not valid UTF-8")]
  InvalidUtf8(std::ffi::OsString),
  #[class(generic)]
  #[error("{0}")]
  StripPrefix(#[from] StripPrefixError),
  #[class(inherit)]
  #[error("{0}")]
  Canceled(#[from] deno_core::Canceled),
  #[class(type)]
  #[error("Invalid seek mode: {0}")]
  InvalidSeekMode(i32),
  #[class(generic)]
  #[error("Invalid control character in prefix or suffix: {0:?}")]
  InvalidControlCharacter(String),
  #[class(generic)]
  #[error("Invalid character in prefix or suffix: {0:?}")]
  InvalidCharacter(String),
  #[cfg(windows)]
  #[class(generic)]
  #[error("Invalid trailing character in suffix")]
  InvalidTrailingCharacter,
  #[class(inherit)]
  #[error(transparent)]
  Other(JsErrorBox),
}

impl From<FsError> for FsOpsError {
  fn from(err: FsError) -> Self {
    match err {
      FsError::Io(err) => FsOpsErrorKind::Io(err),
      FsError::FileBusy => FsOpsErrorKind::Resource(ResourceError::Unavailable),
      FsError::NotSupported => {
        FsOpsErrorKind::Other(JsErrorBox::not_supported())
      }
      FsError::PermissionCheck(err) => FsOpsErrorKind::Permission(err),
      FsError::JoinError(err) => {
        FsOpsErrorKind::Other(JsErrorBox::from_err(err))
      }
    }
    .into_box()
  }
}

#[derive(Debug)]
struct ReadDirResource(FsReadDirRc);

impl Resource for ReadDirResource {
  fn name(&self) -> Cow<'_, str> {
    "readDir".into()
  }
}

fn open_options_to_access_kind(open_options: &OpenOptions) -> OpenAccessKind {
  let read = open_options.read;
  let write = open_options.write || open_options.append;
  match (read, write) {
    (true, true) => OpenAccessKind::ReadWrite,
    (false, true) => OpenAccessKind::Write,
    (true, false) | (false, false) => OpenAccessKind::Read,
  }
}

#[op2]
#[string]
pub fn op_fs_cwd(state: &mut OpState) -> Result<String, FsOpsError> {
  let fs = state.borrow::<FileSystemRc>();
  let path = fs.cwd()?;
  let path_str = path_into_string(path.into_os_string())?;
  Ok(path_str)
}

#[op2(fast, stack_trace)]
pub fn op_fs_chdir(
  state: &mut OpState,
  #[string] directory: &str,
) -> Result<(), FsOpsError> {
  let d = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(directory)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.chdir()"),
    )?;
  state
    .borrow::<FileSystemRc>()
    .chdir(&d)
    .context_path("chdir", &d)
}

#[op2]
pub fn op_fs_umask(
  state: &mut OpState,
  mask: Option<u32>,
) -> Result<u32, FsOpsError>
where
{
  state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_sys("umask", "Deno.umask()")?;
  state.borrow::<FileSystemRc>().umask(mask).context("umask")
}

#[derive(FromV8, Default, Debug, Clone, Copy)]
struct FsOpenOptions {
  #[from_v8(default)]
  read: bool,
  #[from_v8(default)]
  write: bool,
  #[from_v8(default)]
  create: bool,
  #[from_v8(default)]
  truncate: bool,
  #[from_v8(default)]
  append: bool,
  #[from_v8(default)]
  create_new: bool,
  #[from_v8(default)]
  mode: Option<u32>,
}

impl From<FsOpenOptions> for OpenOptions {
  fn from(options: FsOpenOptions) -> Self {
    OpenOptions {
      read: options.read,
      write: options.write,
      create: options.create,
      truncate: options.truncate,
      append: options.append,
      create_new: options.create_new,
      custom_flags: None,
      mode: options.mode,
    }
  }
}

#[op2(stack_trace)]
#[smi]
pub fn op_fs_open_sync(
  state: &mut OpState,
  #[string] path: &str,
  #[scoped] options: Option<FsOpenOptions>,
) -> Result<ResourceId, FsOpsError> {
  let options = match options {
    Some(options) => OpenOptions::from(options),
    None => OpenOptions::read(),
  };
  let path = Path::new(path);

  let fs = state.borrow::<FileSystemRc>().clone();
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(path),
      open_options_to_access_kind(&options),
      Some("Deno.openSync()"),
    )?;
  let file = fs.open_sync(&path, options).context_path("open", &path)?;
  let rid = state
    .resource_table
    .add(FileResource::new(file, "fsFile".to_string()));
  Ok(rid)
}

#[op2(stack_trace)]
#[smi]
pub async fn op_fs_open_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  #[scoped] options: Option<FsOpenOptions>,
) -> Result<ResourceId, FsOpsError> {
  let options = match options {
    Some(options) => OpenOptions::from(options),
    None => OpenOptions::read(),
  };
  let path = PathBuf::from(path);

  let (fs, path) = {
    let mut state = state.borrow_mut();
    (
      state.borrow::<FileSystemRc>().clone(),
      state
        .borrow_mut::<deno_permissions::PermissionsContainer>()
        .check_open(
          Cow::Owned(path),
          open_options_to_access_kind(&options),
          Some("Deno.open()"),
        )?,
    )
  };
  let file = fs
    .open_async(path.as_owned(), options)
    .await
    .context_path("open", &path)?;

  let rid = state
    .borrow_mut()
    .resource_table
    .add(FileResource::new(file, "fsFile".to_string()));
  Ok(rid)
}

#[op2(stack_trace)]
pub fn op_fs_mkdir_sync(
  state: &mut OpState,
  #[string] path: &str,
  recursive: bool,
  mode: Option<u32>,
) -> Result<(), FsOpsError> {
  let mode = mode.unwrap_or(0o777) & 0o777;

  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.mkdirSync()"),
    )?;

  let fs = state.borrow::<FileSystemRc>();
  fs.mkdir_sync(&path, recursive, Some(mode))
    .context_path("mkdir", &path)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_mkdir_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  recursive: bool,
  mode: Option<u32>,
) -> Result<(), FsOpsError> {
  let mode = mode.unwrap_or(0o777) & 0o777;

  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.mkdir()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };

  fs.mkdir_async(path.as_owned(), recursive, Some(mode))
    .await
    .context_path("mkdir", &path)?;

  Ok(())
}

#[cfg(unix)]
#[op2(fast, stack_trace)]
pub fn op_fs_chmod_sync(
  state: &mut OpState,
  #[string] path: &str,
  mode: u32,
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.chmodSync()"),
    )?;
  let fs = state.borrow::<FileSystemRc>();
  fs.chmod_sync(&path, mode).context_path("chmod", &path)?;
  Ok(())
}

#[cfg(not(unix))]
#[op2(fast, stack_trace)]
pub fn op_fs_chmod_sync(
  state: &mut OpState,
  #[string] path: &str,
  mode: i32,
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.chmodSync()"),
    )?;
  let fs = state.borrow::<FileSystemRc>();
  fs.chmod_sync(&path, mode).context_path("chmod", &path)?;
  Ok(())
}

#[cfg(unix)]
#[op2(stack_trace)]
pub async fn op_fs_chmod_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  mode: u32,
) -> Result<(), FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.chmod()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };
  fs.chmod_async(path.as_owned(), mode)
    .await
    .context_path("chmod", &path)?;
  Ok(())
}

#[cfg(not(unix))]
#[op2(stack_trace)]
pub async fn op_fs_chmod_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  mode: i32,
) -> Result<(), FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.chmod()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };
  fs.chmod_async(path.as_owned(), mode)
    .await
    .context_path("chmod", &path)?;
  Ok(())
}

#[op2(stack_trace)]
pub fn op_fs_chown_sync(
  state: &mut OpState,
  #[string] path: &str,
  uid: Option<u32>,
  gid: Option<u32>,
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.chownSync()"),
    )?;
  let fs = state.borrow::<FileSystemRc>();
  fs.chown_sync(&path, uid, gid)
    .context_path("chown", &path)?;
  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_chown_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  uid: Option<u32>,
  gid: Option<u32>,
) -> Result<(), FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.chown()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };
  fs.chown_async(path.as_owned(), uid, gid)
    .await
    .context_path("chown", &path)?;
  Ok(())
}

#[op2(fast, stack_trace)]
pub fn op_fs_fchmod_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  mode: u32,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.chmod_sync(mode)?;
  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_fchmod_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  mode: u32,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.chmod_async(mode).await?;
  Ok(())
}

#[op2(stack_trace)]
pub fn op_fs_fchown_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  uid: Option<u32>,
  gid: Option<u32>,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.chown_sync(uid, gid)?;
  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_fchown_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  uid: Option<u32>,
  gid: Option<u32>,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.chown_async(uid, gid).await?;
  Ok(())
}

#[op2(fast, stack_trace)]
pub fn op_fs_remove_sync(
  state: &mut OpState,
  #[string] path: &str,
  recursive: bool,
) -> Result<(), FsOpsError> {
  let path = Cow::Borrowed(Path::new(path));
  let path = if recursive {
    // Recursive remove descends into the whole tree, so it must use the strict
    // write check: a deny scope *inside* the requested path has to block the
    // operation. `check_open` uses partial-deny semantics and would let a
    // recursive delete bypass a denied descendant.
    state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_write(path, "Deno.removeSync()")?
  } else {
    state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_write_partial(path, "Deno.removeSync()")?
  };

  let fs = state.borrow::<FileSystemRc>();
  fs.remove_sync(&path, recursive)
    .context_path("remove", &path)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_remove_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  recursive: bool,
) -> Result<(), FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = Cow::Owned(PathBuf::from(path));
    let path = if recursive {
      // Recursive remove descends into the whole tree, so it must use the
      // strict write check: a deny scope *inside* the requested path has to
      // block the operation. `check_open` uses partial-deny semantics and would
      // let a recursive delete bypass a denied descendant.
      state
        .borrow_mut::<deno_permissions::PermissionsContainer>()
        .check_write(path, "Deno.remove()")?
    } else {
      state
        .borrow_mut::<deno_permissions::PermissionsContainer>()
        .check_write_partial(path, "Deno.remove()")?
    };

    (state.borrow::<FileSystemRc>().clone(), path)
  };

  fs.remove_async(path.as_owned(), recursive)
    .await
    .context_path("remove", &path)?;

  Ok(())
}

#[op2(fast, stack_trace)]
pub fn op_fs_copy_file_sync(
  state: &mut OpState,
  #[string] from: &str,
  #[string] to: &str,
) -> Result<(), FsOpsError> {
  let permissions =
    state.borrow_mut::<deno_permissions::PermissionsContainer>();
  let from = permissions.check_open(
    Cow::Borrowed(Path::new(from)),
    OpenAccessKind::Read,
    Some("Deno.copyFileSync()"),
  )?;
  let to = permissions.check_open(
    Cow::Borrowed(Path::new(to)),
    OpenAccessKind::WriteNoFollow,
    Some("Deno.copyFileSync()"),
  )?;

  let fs = state.borrow::<FileSystemRc>();
  fs.copy_file_sync(&from, &to)
    .context_two_path("copy", &from, &to)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_copy_file_async(
  state: Rc<RefCell<OpState>>,
  #[string] from: String,
  #[string] to: String,
) -> Result<(), FsOpsError> {
  let (fs, from, to) = {
    let mut state = state.borrow_mut();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    let from = permissions.check_open(
      Cow::Owned(PathBuf::from(from)),
      OpenAccessKind::Read,
      Some("Deno.copyFile()"),
    )?;
    let to = permissions.check_open(
      Cow::Owned(PathBuf::from(to)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.copyFile()"),
    )?;
    (state.borrow::<FileSystemRc>().clone(), from, to)
  };
  fs.copy_file_async(from.as_owned(), to.as_owned())
    .await
    .context_two_path("copy", &from, &to)?;

  Ok(())
}

#[op2(fast, stack_trace)]
pub fn op_fs_stat_sync(
  state: &mut OpState,
  #[string] path: &str,
  #[buffer] stat_out_buf: &mut [u32],
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.statSync()"),
    )?;
  let fs = state.borrow::<FileSystemRc>();
  let stat = fs.stat_sync(&path).context_path("stat", &path)?;
  let serializable_stat = SerializableStat::from(stat);
  serializable_stat.write(stat_out_buf);
  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_stat_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
) -> Result<SerializableStat, FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    let path = permissions.check_open(
      Cow::Owned(PathBuf::from(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.stat()"),
    )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };
  let stat = fs
    .stat_async(path.as_owned())
    .await
    .context_path("stat", &path)?;
  Ok(SerializableStat::from(stat))
}

#[op2(fast, stack_trace)]
pub fn op_fs_lstat_sync(
  state: &mut OpState,
  #[string] path: &str,
  #[buffer] stat_out_buf: &mut [u32],
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.lstatSync()"),
    )?;
  let fs = state.borrow::<FileSystemRc>();
  let stat = fs.lstat_sync(&path).context_path("lstat", &path)?;
  let serializable_stat = SerializableStat::from(stat);
  serializable_stat.write(stat_out_buf);
  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_lstat_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
) -> Result<SerializableStat, FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    let path = permissions.check_open(
      Cow::Owned(PathBuf::from(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.lstat()"),
    )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };
  let stat = fs
    .lstat_async(path.as_owned())
    .await
    .context_path("lstat", &path)?;
  Ok(SerializableStat::from(stat))
}

#[op2(stack_trace)]
#[string]
pub fn op_fs_realpath_sync(
  state: &mut OpState,
  #[string] path: &str,
) -> Result<String, FsOpsError> {
  let fs = state.borrow::<FileSystemRc>().clone();
  let permissions =
    state.borrow_mut::<deno_permissions::PermissionsContainer>();
  let path = permissions.check_open(
    Cow::Borrowed(Path::new(path)),
    OpenAccessKind::ReadNoFollow,
    Some("Deno.realPathSync()"),
  )?;
  let resolved_path =
    fs.realpath_sync(&path).context_path("realpath", &path)?;

  let path_string = path_into_string(resolved_path.into_os_string())?;
  Ok(path_string)
}

#[op2(stack_trace)]
#[string]
pub async fn op_fs_realpath_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
) -> Result<String, FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let fs = state.borrow::<FileSystemRc>().clone();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    let path = permissions.check_open(
      Cow::Owned(PathBuf::from(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.realPath()"),
    )?;
    (fs, path)
  };
  let resolved_path = fs
    .realpath_async(path.as_owned())
    .await
    .context_path("realpath", &path)?;

  let path_string = path_into_string(resolved_path.into_os_string())?;
  Ok(path_string)
}

#[op2(stack_trace)]
pub fn op_fs_read_dir_sync(
  state: &mut OpState,
  #[string] path: &str,
) -> Result<Vec<FsDirEntry>, FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.readDirSync()"),
    )?;

  let fs = state.borrow::<FileSystemRc>();
  let entries = fs.read_dir_sync(&path).context_path("readdir", &path)?;

  Ok(entries)
}

#[op2(stack_trace)]
pub async fn op_fs_read_dir_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
) -> Result<ResourceId, FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::ReadNoFollow,
        Some("Deno.readDir()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };

  let read_dir = fs
    .read_dir_async(path.as_owned())
    .await
    .context_path("readdir", &path)?;

  Ok(
    state
      .borrow_mut()
      .resource_table
      .add(ReadDirResource(read_dir)),
  )
}

#[op2(stack_trace)]
pub async fn op_fs_read_dir_async_next(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
) -> Result<Option<FsDirEntry>, FsOpsError> {
  let resource = {
    let state = state.borrow();
    state.resource_table.get::<ReadDirResource>(rid)?
  };

  resource.0.next().await.map_err(Into::into)
}

#[op2(fast, stack_trace)]
pub fn op_fs_rename_sync(
  state: &mut OpState,
  #[string] oldpath: &str,
  #[string] newpath: &str,
) -> Result<(), FsOpsError> {
  let permissions =
    state.borrow_mut::<deno_permissions::PermissionsContainer>();
  let oldpath = permissions.check_open(
    Cow::Borrowed(Path::new(oldpath)),
    OpenAccessKind::ReadWriteNoFollow,
    Some("Deno.renameSync()"),
  )?;
  let newpath = permissions.check_open(
    Cow::Borrowed(Path::new(newpath)),
    OpenAccessKind::WriteNoFollow,
    Some("Deno.renameSync()"),
  )?;

  let fs = state.borrow::<FileSystemRc>();
  fs.rename_sync(&oldpath, &newpath)
    .context_two_path("rename", &oldpath, &newpath)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_rename_async(
  state: Rc<RefCell<OpState>>,
  #[string] oldpath: String,
  #[string] newpath: String,
) -> Result<(), FsOpsError> {
  let (fs, oldpath, newpath) = {
    let mut state = state.borrow_mut();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    let oldpath = permissions.check_open(
      Cow::Owned(PathBuf::from(oldpath)),
      OpenAccessKind::ReadWriteNoFollow,
      Some("Deno.rename()"),
    )?;
    let newpath = permissions.check_open(
      Cow::Owned(PathBuf::from(newpath)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.rename()"),
    )?;
    (state.borrow::<FileSystemRc>().clone(), oldpath, newpath)
  };

  fs.rename_async(oldpath.as_owned(), newpath.as_owned())
    .await
    .context_two_path("rename", &oldpath, &newpath)?;

  Ok(())
}

#[op2(fast, stack_trace)]
pub fn op_fs_link_sync(
  state: &mut OpState,
  #[string] oldpath: &str,
  #[string] newpath: &str,
) -> Result<(), FsOpsError> {
  let permissions =
    state.borrow_mut::<deno_permissions::PermissionsContainer>();
  let oldpath = permissions.check_open(
    Cow::Borrowed(Path::new(oldpath)),
    OpenAccessKind::ReadWriteNoFollow,
    Some("Deno.linkSync()"),
  )?;
  let newpath = permissions.check_open(
    Cow::Borrowed(Path::new(newpath)),
    OpenAccessKind::WriteNoFollow,
    Some("Deno.linkSync()"),
  )?;

  let fs = state.borrow::<FileSystemRc>();
  fs.link_sync(&oldpath, &newpath)
    .context_two_path("link", &oldpath, &newpath)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_link_async(
  state: Rc<RefCell<OpState>>,
  #[string] oldpath: String,
  #[string] newpath: String,
) -> Result<(), FsOpsError> {
  let (fs, oldpath, newpath) = {
    let mut state = state.borrow_mut();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    let oldpath = permissions.check_open(
      Cow::Owned(PathBuf::from(oldpath)),
      OpenAccessKind::ReadWriteNoFollow,
      Some("Deno.link()"),
    )?;
    let newpath = permissions.check_open(
      Cow::Owned(PathBuf::from(newpath)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.link()"),
    )?;
    (state.borrow::<FileSystemRc>().clone(), oldpath, newpath)
  };

  fs.link_async(oldpath.as_owned(), newpath.as_owned())
    .await
    .context_two_path("link", &oldpath, &newpath)?;

  Ok(())
}

// Symlink permissions cannot be path-scoped. A symlink's target (`oldpath`) is
// just a string stored in the link: it may be relative, absolute, or not yet
// exist, and it is only resolved when the link is later traversed. There is no
// concrete path to validate against an allow-list at creation time, so we
// require unscoped read+write rather than give a false sense of containment.
// The generic "Requires write access" message does not convey that scoping is
// the problem, so we replace it with a clearer one.
fn check_symlink_permissions(
  permissions: &deno_permissions::PermissionsContainer,
  api_name: &str,
) -> Result<(), FsOpsError> {
  permissions
    .check_write_all(api_name)
    .and_then(|()| permissions.check_read_all(api_name))
    .map_err(|err| {
      let err = match err {
        PermissionCheckError::PermissionDenied(mut denied)
          if denied.custom_message.is_none() =>
        {
          denied.custom_message = Some(format!(
            "{api_name} requires unscoped --allow-read and --allow-write permissions; path-scoped grants (e.g. --allow-write=<path>) are not supported because a symlink's target is only resolved when the link is traversed"
          ));
          PermissionCheckError::PermissionDenied(denied)
        }
        other => other,
      };
      FsOpsErrorKind::Permission(err).into_box()
    })
}

#[op2(stack_trace)]
pub fn op_fs_symlink_sync(
  state: &mut OpState,
  #[string] oldpath: &str,
  #[string] newpath: &str,
  #[serde] file_type: Option<FsFileType>,
) -> Result<(), FsOpsError> {
  let permissions =
    state.borrow_mut::<deno_permissions::PermissionsContainer>();
  check_symlink_permissions(permissions, "Deno.symlinkSync()")?;

  // PERMISSIONS: ok because we verified --allow-write and --allow-read above
  let oldpath = CheckedPath::unsafe_new(Cow::Borrowed(Path::new(oldpath)));
  let newpath = CheckedPath::unsafe_new(Cow::Borrowed(Path::new(newpath)));

  let fs = state.borrow::<FileSystemRc>();
  fs.symlink_sync(&oldpath, &newpath, file_type)
    .context_two_path("symlink", &oldpath, &newpath)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_symlink_async(
  state: Rc<RefCell<OpState>>,
  #[string] oldpath: String,
  #[string] newpath: String,
  #[serde] file_type: Option<FsFileType>,
) -> Result<(), FsOpsError> {
  let fs = {
    let mut state = state.borrow_mut();
    let permissions =
      state.borrow_mut::<deno_permissions::PermissionsContainer>();
    check_symlink_permissions(permissions, "Deno.symlink()")?;
    state.borrow::<FileSystemRc>().clone()
  };

  // PERMISSIONS: ok because we verified --allow-write and --allow-read above
  let oldpath = CheckedPathBuf::unsafe_new(PathBuf::from(&oldpath));
  let newpath = CheckedPathBuf::unsafe_new(PathBuf::from(&newpath));

  fs.symlink_async(oldpath.clone(), newpath.clone(), file_type)
    .await
    .context_two_path(
      "symlink",
      oldpath.as_checked_path(),
      newpath.as_checked_path(),
    )?;

  Ok(())
}

#[op2(stack_trace)]
#[string]
pub fn op_fs_read_link_sync(
  state: &mut OpState,
  #[string] path: &str,
) -> Result<String, FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::ReadNoFollow,
      Some("Deno.readLink()"),
    )?;

  let fs = state.borrow::<FileSystemRc>();

  let target = fs.read_link_sync(&path).context_path("readlink", &path)?;
  let target_string = path_into_string(target.into_os_string())?;
  Ok(target_string)
}

#[op2(stack_trace)]
#[string]
pub async fn op_fs_read_link_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
) -> Result<String, FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::ReadNoFollow,
        Some("Deno.readLink()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };

  let target = fs
    .read_link_async(path.as_owned())
    .await
    .context_path("readlink", &path)?;
  let target_string = path_into_string(target.into_os_string())?;
  Ok(target_string)
}

#[op2(fast, stack_trace)]
pub fn op_fs_truncate_sync(
  state: &mut OpState,
  #[string] path: &str,
  #[number] len: u64,
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.truncateSync()"),
    )?;

  let fs = state.borrow::<FileSystemRc>();
  fs.truncate_sync(&path, len)
    .context_path("truncate", &path)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_truncate_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  #[number] len: u64,
) -> Result<(), FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.truncate()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };

  fs.truncate_async(path.as_owned(), len)
    .await
    .context_path("truncate", &path)?;

  Ok(())
}

#[op2(fast, stack_trace)]
pub fn op_fs_utime_sync(
  state: &mut OpState,
  #[string] path: &str,
  #[number] atime_secs: i64,
  #[smi] atime_nanos: u32,
  #[number] mtime_secs: i64,
  #[smi] mtime_nanos: u32,
) -> Result<(), FsOpsError> {
  let path = state
    .borrow_mut::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(Path::new(path)),
      OpenAccessKind::WriteNoFollow,
      Some("Deno.utime()"),
    )?;

  let fs = state.borrow::<FileSystemRc>();
  fs.utime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
    .context_path("utime", &path)?;

  Ok(())
}

#[op2(stack_trace)]
pub async fn op_fs_utime_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  #[number] atime_secs: i64,
  #[smi] atime_nanos: u32,
  #[number] mtime_secs: i64,
  #[smi] mtime_nanos: u32,
) -> Result<(), FsOpsError> {
  let (fs, path) = {
    let mut state = state.borrow_mut();
    let path = state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(PathBuf::from(path)),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.utime()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), path)
  };

  fs.utime_async(
    path.as_owned(),
    atime_secs,
    atime_nanos,
    mtime_secs,
    mtime_nanos,
  )
  .await
  .context_path("utime", &path)?;

  Ok(())
}

#[op2(stack_trace)]
#[string]
pub fn op_fs_make_temp_dir_sync(
  state: &mut OpState,
  #[string] dir_arg: Option<String>,
  #[string] prefix: Option<String>,
  #[string] suffix: Option<String>,
) -> Result<String, FsOpsError> {
  let (dir, fs) =
    make_temp_check_sync(state, dir_arg.as_deref(), "Deno.makeTempDirSync()")?;

  let mut rng = thread_rng();

  const MAX_TRIES: u32 = 10;
  for _ in 0..MAX_TRIES {
    let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
    // PERMISSIONS: this is ok because we verified the directory above
    let path = CheckedPath::unsafe_new(Cow::Owned(path));
    match fs.mkdir_sync(&path, false, Some(0o700)) {
      Ok(_) => {
        // PERMISSIONS: ensure the absolute path is not leaked
        let path =
          strip_dir_prefix(&dir, dir_arg.as_deref(), path.into_owned_path())?;
        return path_into_string(path.into_os_string());
      }
      Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
        continue;
      }
      Err(e) => return Err(e).context("tmpdir"),
    }
  }

  Err(FsError::Io(io::Error::new(
    io::ErrorKind::AlreadyExists,
    "too many temp dirs exist",
  )))
  .context("tmpdir")
}

#[op2(stack_trace)]
#[string]
pub async fn op_fs_make_temp_dir_async(
  state: Rc<RefCell<OpState>>,
  #[string] dir_arg: Option<String>,
  #[string] prefix: Option<String>,
  #[string] suffix: Option<String>,
) -> Result<String, FsOpsError> {
  let (dir, fs) =
    make_temp_check_async(state, dir_arg.as_deref(), "Deno.makeTempDir()")?;

  let mut rng = thread_rng();

  const MAX_TRIES: u32 = 10;
  for _ in 0..MAX_TRIES {
    let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
    // PERMISSIONS: ok because we verified the directory above
    let path = CheckedPathBuf::unsafe_new(path);
    match fs
      .clone()
      .mkdir_async(path.clone(), false, Some(0o700))
      .await
    {
      Ok(_) => {
        // PERMISSIONS: ensure the absolute path is not leaked
        let path =
          strip_dir_prefix(&dir, dir_arg.as_deref(), path.into_path_buf())?;
        return path_into_string(path.into_os_string());
      }
      Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
        continue;
      }
      Err(e) => return Err(e).context("tmpdir"),
    }
  }

  Err(FsError::Io(io::Error::new(
    io::ErrorKind::AlreadyExists,
    "too many temp dirs exist",
  )))
  .context("tmpdir")
}

#[op2(stack_trace)]
#[string]
pub fn op_fs_make_temp_file_sync(
  state: &mut OpState,
  #[string] dir_arg: Option<String>,
  #[string] prefix: Option<String>,
  #[string] suffix: Option<String>,
) -> Result<String, FsOpsError> {
  let (dir, fs) =
    make_temp_check_sync(state, dir_arg.as_deref(), "Deno.makeTempFileSync()")?;

  let open_opts = OpenOptions {
    write: true,
    create_new: true,
    mode: Some(0o600),
    ..Default::default()
  };

  let mut rng = thread_rng();
  const MAX_TRIES: u32 = 10;
  for _ in 0..MAX_TRIES {
    let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
    // PERMISSIONS: this is fine because the dir was checked
    let path = CheckedPath::unsafe_new(Cow::Owned(path));
    match fs.open_sync(&path, open_opts) {
      Ok(_) => {
        // PERMISSIONS: ensure the absolute path is not leaked
        let path =
          strip_dir_prefix(&dir, dir_arg.as_deref(), path.into_owned_path())?;
        return path_into_string(path.into_os_string());
      }
      Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
        continue;
      }
      Err(e) => return Err(e).context("tmpfile"),
    }
  }

  Err(FsError::Io(io::Error::new(
    io::ErrorKind::AlreadyExists,
    "too many temp files exist",
  )))
  .context("tmpfile")
}

#[op2(stack_trace)]
#[string]
pub async fn op_fs_make_temp_file_async(
  state: Rc<RefCell<OpState>>,
  #[string] dir_arg: Option<String>,
  #[string] prefix: Option<String>,
  #[string] suffix: Option<String>,
) -> Result<String, FsOpsError> {
  let (dir, fs) =
    make_temp_check_async(state, dir_arg.as_deref(), "Deno.makeTempFile()")?;

  let open_opts = OpenOptions {
    write: true,
    create_new: true,
    mode: Some(0o600),
    ..Default::default()
  };

  let mut rng = thread_rng();

  const MAX_TRIES: u32 = 10;
  for _ in 0..MAX_TRIES {
    let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?;
    // PERMISSIONS: this is fine because the dir was checked
    let path = CheckedPathBuf::unsafe_new(path);
    match fs.clone().open_async(path.clone(), open_opts).await {
      Ok(_) => {
        // PERMISSIONS: ensure the absolute path is not leaked
        let path =
          strip_dir_prefix(&dir, dir_arg.as_deref(), path.into_path_buf())?;
        return path_into_string(path.into_os_string());
      }
      Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => {
        continue;
      }
      Err(e) => return Err(e).context("tmpfile"),
    }
  }
  Err(FsError::Io(io::Error::new(
    io::ErrorKind::AlreadyExists,
    "too many temp files exist",
  )))
  .context("tmpfile")
}

fn strip_dir_prefix(
  resolved_dir: &Path,
  dir_arg: Option<&str>,
  result_path: PathBuf,
) -> Result<PathBuf, StripPrefixError> {
  if resolved_dir.is_absolute() {
    match &dir_arg {
      Some(dir_arg) => {
        Ok(Path::new(dir_arg).join(result_path.strip_prefix(resolved_dir)?))
      }
      None => Ok(result_path),
    }
  } else {
    Ok(result_path)
  }
}

fn make_temp_check_sync<'a>(
  state: &mut OpState,
  dir: Option<&'a str>,
  api_name: &str,
) -> Result<(CheckedPath<'a>, FileSystemRc), FsOpsError> {
  let fs = state.borrow::<FileSystemRc>().clone();
  let dir = match dir {
    Some(dir) => state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Borrowed(Path::new(dir)),
        OpenAccessKind::WriteNoFollow,
        Some(api_name),
      )?,
    None => {
      let dir = fs.tmp_dir().context("tmpdir")?;
      state
        .borrow_mut::<deno_permissions::PermissionsContainer>()
        .check_open_blind(
          Cow::Owned(dir),
          OpenAccessKind::WriteNoFollow,
          "TMP",
          Some(api_name),
        )?
    }
  };
  Ok((dir, fs))
}

fn make_temp_check_async<'a>(
  state: Rc<RefCell<OpState>>,
  dir: Option<&'a str>,
  api_name: &str,
) -> Result<(CheckedPath<'a>, FileSystemRc), FsOpsError> {
  let mut state = state.borrow_mut();
  let fs = state.borrow::<FileSystemRc>().clone();
  let dir = match dir {
    Some(dir) => state
      .borrow_mut::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Borrowed(Path::new(dir)),
        OpenAccessKind::WriteNoFollow,
        Some(api_name),
      )?,
    None => {
      let dir = fs.tmp_dir().context("tmpdir")?;
      state
        .borrow_mut::<deno_permissions::PermissionsContainer>()
        .check_open_blind(
          Cow::Owned(dir),
          OpenAccessKind::WriteNoFollow,
          "TMP",
          Some(api_name),
        )?
    }
  };
  Ok((dir, fs))
}

/// Identify illegal filename characters before attempting to use them in a filesystem
/// operation. We're a bit stricter with tempfile and tempdir names than with regular
/// files.
fn validate_temporary_filename_component(
  component: &str,
  _suffix: bool,
) -> Result<(), FsOpsError> {
  // Ban ASCII and Unicode control characters: these will often fail
  if let Some(c) = component.matches(|c: char| c.is_control()).next() {
    return Err(
      FsOpsErrorKind::InvalidControlCharacter(c.to_string()).into_box(),
    );
  }
  // Windows has the most restrictive filenames. As temp files aren't normal files, we just
  // use this set of banned characters for all platforms because wildcard-like files can also
  // be problematic in unix-like shells.

  // The list of banned characters in Windows:
  // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions

  // You might ask why <, >, and " are included in the Windows list? It's because they are
  // special wildcard implemented in the filesystem driver!
  // https://learn.microsoft.com/en-ca/archive/blogs/jeremykuhne/wildcards-in-windows
  if let Some(c) = component
    .matches(|c: char| "<>:\"/\\|?*".contains(c))
    .next()
  {
    return Err(FsOpsErrorKind::InvalidCharacter(c.to_string()).into_box());
  }

  // This check is only for Windows
  #[cfg(windows)]
  if _suffix && component.ends_with(|c: char| ". ".contains(c)) {
    return Err(FsOpsErrorKind::InvalidTrailingCharacter.into_box());
  }

  Ok(())
}

fn tmp_name(
  rng: &mut ThreadRng,
  dir: &Path,
  prefix: Option<&str>,
  suffix: Option<&str>,
) -> Result<PathBuf, FsOpsError> {
  let prefix = prefix.unwrap_or("");
  validate_temporary_filename_component(prefix, false)?;
  let suffix = suffix.unwrap_or("");
  validate_temporary_filename_component(suffix, true)?;

  // If we use a 32-bit number, we only need ~70k temp files before we have a 50%
  // chance of collision. By bumping this up to 64-bits, we require ~5 billion
  // before hitting a 50% chance.
  let unique = rng.r#gen::<u64>();
  let path = dir.join(format!("{prefix}{unique:08x}{suffix}"));

  Ok(path)
}

#[op2(stack_trace)]
pub fn op_fs_write_file_sync(
  state: &mut OpState,
  #[string] path: &str,
  mode: Option<u32>,
  append: bool,
  create: bool,
  create_new: bool,
  data: Uint8Array,
) -> Result<(), FsOpsError> {
  let path = Path::new(path);

  let options = OpenOptions::write(create, append, create_new, mode);
  let fs = state.borrow::<FileSystemRc>().clone();
  let path = state
    .borrow::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(path),
      OpenAccessKind::Write,
      Some("Deno.writeFileSync()"),
    )?;

  fs.write_file_sync(&path, options, &data)
    .context_path("writefile", &path)?;

  Ok(())
}

#[op2(stack_trace)]
#[allow(clippy::too_many_arguments, reason = "op")]
pub async fn op_fs_write_file_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  #[smi] mode: Option<u32>,
  append: bool,
  create: bool,
  create_new: bool,
  data: Uint8Array,
  #[smi] cancel_rid: Option<ResourceId>,
) -> Result<(), FsOpsError> {
  let path = PathBuf::from(path);

  let options = OpenOptions::write(create, append, create_new, mode);

  let (fs, cancel_handle, path) = {
    let state = state.borrow_mut();
    let cancel_handle = cancel_rid
      .and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
    let path = state
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(path),
        OpenAccessKind::Write,
        Some("Deno.writeFile()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), cancel_handle, path)
  };

  let fut = fs.write_file_async(path.as_owned(), options, data.0);

  if let Some(cancel_handle) = cancel_handle {
    let res = fut.or_cancel(cancel_handle).await;

    if let Some(cancel_rid) = cancel_rid
      && let Ok(res) = state.borrow_mut().resource_table.take_any(cancel_rid)
    {
      res.close();
    };

    res?.context_path("writefile", &path)?;
  } else {
    fut.await.context_path("writefile", &path)?;
  }

  Ok(())
}

#[op2(stack_trace)]
pub fn op_fs_read_file_sync(
  state: &mut OpState,
  #[string] path: &str,
  #[smi] flags: Option<i32>,
) -> Result<Uint8Array, FsOpsError> {
  let path = Path::new(path);
  let options = if let Some(flags) = flags {
    OpenOptions::from(flags)
  } else {
    OpenOptions::read()
  };

  let fs = state.borrow::<FileSystemRc>().clone();
  let path = state
    .borrow::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(path),
      open_options_to_access_kind(&options),
      Some("Deno.readFileSync()"),
    )?;

  let buf = fs
    .read_file_sync(&path, options)
    .context_path("readfile", &path)?;

  // todo(https://github.com/denoland/deno/issues/27107): do not clone here
  Ok(buf.into_owned().to_vec().into())
}

#[op2(stack_trace)]
pub async fn op_fs_read_file_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  #[smi] cancel_rid: Option<ResourceId>,
  #[smi] flags: Option<i32>,
) -> Result<Uint8Array, FsOpsError> {
  let path = PathBuf::from(path);
  let options = if let Some(flags) = flags {
    OpenOptions::from(flags)
  } else {
    OpenOptions::read()
  };

  let (fs, cancel_handle, path) = {
    let state = state.borrow();
    let cancel_handle = cancel_rid
      .and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
    let path = state
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(path),
        open_options_to_access_kind(&options),
        Some("Deno.readFile()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), cancel_handle, path)
  };

  let fut = fs.read_file_async(path.as_owned(), options);

  let buf = if let Some(cancel_handle) = cancel_handle {
    let res = fut.or_cancel(cancel_handle).await;

    if let Some(cancel_rid) = cancel_rid
      && let Ok(res) = state.borrow_mut().resource_table.take_any(cancel_rid)
    {
      res.close();
    };

    res?.context_path("readfile", &path)?
  } else {
    fut.await.context_path("readfile", &path)?
  };

  // todo(https://github.com/denoland/deno/issues/27107): do not clone here
  Ok(buf.into_owned().to_vec().into())
}

#[op2(stack_trace)]
pub fn op_fs_read_file_text_sync(
  state: &mut OpState,
  #[string] path: &str,
) -> Result<FastString, FsOpsError> {
  let path = Path::new(path);

  let fs = state.borrow::<FileSystemRc>().clone();
  let path = state
    .borrow::<deno_permissions::PermissionsContainer>()
    .check_open(
      Cow::Borrowed(path),
      OpenAccessKind::Read,
      Some("Deno.readFileSync()"),
    )?;
  let str = fs
    .read_text_file_lossy_sync(&path)
    .context_path("readfile", &path)?;
  Ok(match str {
    Cow::Borrowed(text) => FastString::from_static(text),
    Cow::Owned(value) => value.into(),
  })
}

#[op2(stack_trace)]
pub async fn op_fs_read_file_text_async(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
  #[smi] cancel_rid: Option<ResourceId>,
) -> Result<FastString, FsOpsError> {
  let path = PathBuf::from(path);

  let (fs, cancel_handle, path) = {
    let state = state.borrow_mut();
    let cancel_handle = cancel_rid
      .and_then(|rid| state.resource_table.get::<CancelHandle>(rid).ok());
    let path = state
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Owned(path),
        OpenAccessKind::Read,
        Some("Deno.readFile()"),
      )?;
    (state.borrow::<FileSystemRc>().clone(), cancel_handle, path)
  };

  let fut = fs.read_text_file_lossy_async(path.as_owned());

  let str = if let Some(cancel_handle) = cancel_handle {
    let res = fut.or_cancel(cancel_handle).await;

    if let Some(cancel_rid) = cancel_rid
      && let Ok(res) = state.borrow_mut().resource_table.take_any(cancel_rid)
    {
      res.close();
    };

    res?.context_path("readfile", &path)?
  } else {
    fut.await.context_path("readfile", &path)?
  };

  Ok(match str {
    Cow::Borrowed(text) => FastString::from_static(text),
    Cow::Owned(value) => value.into(),
  })
}

fn to_seek_from(offset: i64, whence: i32) -> Result<SeekFrom, FsOpsError> {
  let seek_from = match whence {
    0 => SeekFrom::Start(offset as u64),
    1 => SeekFrom::Current(offset),
    2 => SeekFrom::End(offset),
    _ => {
      return Err(FsOpsErrorKind::InvalidSeekMode(whence).into_box());
    }
  };
  Ok(seek_from)
}

#[op2(fast)]
#[number]
pub fn op_fs_seek_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[number] offset: i64,
  #[smi] whence: i32,
) -> Result<u64, FsOpsError> {
  let pos = to_seek_from(offset, whence)?;
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  let cursor = file.seek_sync(pos)?;
  Ok(cursor)
}

#[op2]
#[number]
pub async fn op_fs_seek_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  #[number] offset: i64,
  #[smi] whence: i32,
) -> Result<u64, FsOpsError> {
  let pos = to_seek_from(offset, whence)?;
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  let cursor = file.seek_async(pos).await?;
  Ok(cursor)
}

#[op2(fast)]
pub fn op_fs_file_sync_data_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.datasync_sync()?;
  Ok(())
}

#[op2]
pub async fn op_fs_file_sync_data_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.datasync_async().await?;
  Ok(())
}

#[op2(fast)]
pub fn op_fs_file_sync_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.sync_sync()?;
  Ok(())
}

#[op2]
pub async fn op_fs_file_sync_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.sync_async().await?;
  Ok(())
}

#[op2(fast)]
pub fn op_fs_file_stat_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[buffer] stat_out_buf: &mut [u32],
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  if let Some(path) = file.maybe_path() {
    _ = state
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Borrowed(path),
        OpenAccessKind::Read,
        Some("Deno.FsFile.prototype.statSync()"),
      )?;
  }
  let stat = file.stat_sync()?;
  let serializable_stat = SerializableStat::from(stat);
  serializable_stat.write(stat_out_buf);
  Ok(())
}

#[op2]
pub async fn op_fs_file_stat_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
) -> Result<SerializableStat, FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  if let Some(path) = file.maybe_path() {
    _ = state
      .borrow()
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Borrowed(path),
        OpenAccessKind::Read,
        Some("Deno.FsFile.prototype.stat()"),
      )?;
  }
  let stat = file.stat_async().await?;
  Ok(stat.into())
}

#[op2(fast)]
pub fn op_fs_flock_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  exclusive: bool,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.lock_sync(exclusive)?;
  Ok(())
}

#[op2]
pub async fn op_fs_flock_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  exclusive: bool,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.lock_async(exclusive).await?;
  Ok(())
}

#[op2(fast)]
pub fn op_fs_flock_try_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  exclusive: bool,
) -> Result<bool, FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  Ok(file.try_lock_sync(exclusive)?)
}

#[op2]
pub async fn op_fs_flock_try_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  exclusive: bool,
) -> Result<bool, FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  Ok(file.try_lock_async(exclusive).await?)
}

#[op2(fast)]
pub fn op_fs_funlock_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.unlock_sync()?;
  Ok(())
}

#[op2]
pub async fn op_fs_funlock_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.unlock_async().await?;
  Ok(())
}

#[op2(fast)]
pub fn op_fs_ftruncate_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[number] len: u64,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  file.truncate_sync(len)?;
  Ok(())
}

#[op2]
pub async fn op_fs_file_truncate_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  #[number] len: u64,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  file.truncate_async(len).await?;
  Ok(())
}

#[op2(fast)]
pub fn op_fs_futime_sync(
  state: &mut OpState,
  #[smi] rid: ResourceId,
  #[number] atime_secs: i64,
  #[smi] atime_nanos: u32,
  #[number] mtime_secs: i64,
  #[smi] mtime_nanos: u32,
) -> Result<(), FsOpsError> {
  let file =
    FileResource::get_file(state, rid).map_err(FsOpsErrorKind::Resource)?;
  if let Some(path) = file.maybe_path() {
    _ = state
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Borrowed(path),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.FsFile.prototype.utimeSync()"),
      )?;
  }
  file.utime_sync(atime_secs, atime_nanos, mtime_secs, mtime_nanos)?;
  Ok(())
}

#[op2]
pub async fn op_fs_futime_async(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,
  #[number] atime_secs: i64,
  #[smi] atime_nanos: u32,
  #[number] mtime_secs: i64,
  #[smi] mtime_nanos: u32,
) -> Result<(), FsOpsError> {
  let file = FileResource::get_file(&state.borrow(), rid)
    .map_err(FsOpsErrorKind::Resource)?;
  if let Some(path) = file.maybe_path() {
    _ = state
      .borrow()
      .borrow::<deno_permissions::PermissionsContainer>()
      .check_open(
        Cow::Borrowed(path),
        OpenAccessKind::WriteNoFollow,
        Some("Deno.FsFile.prototype.utime()"),
      )?;
  }
  file
    .utime_async(atime_secs, atime_nanos, mtime_secs, mtime_nanos)
    .await?;
  Ok(())
}

#[derive(Debug, deno_error::JsError)]
#[class(inherit)]
pub struct OperationError {
  operation: &'static str,
  kind: OperationErrorKind,
  #[inherit]
  pub err: FsError,
}

impl std::fmt::Display for OperationError {
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
    if let FsError::Io(e) = &self.err {
      std::fmt::Display::fmt(&e, f)?;
      f.write_str(": ")?;
    }

    f.write_str(self.operation)?;

    match &self.kind {
      OperationErrorKind::Bare => Ok(()),
      OperationErrorKind::WithPath(path) => write!(f, " '{}'", path),
      OperationErrorKind::WithTwoPaths(from, to) => {
        write!(f, " '{}' -> '{}'", from, to)
      }
    }
  }
}

impl std::error::Error for OperationError {
  fn source(&self) -> Option<&(dyn Error + 'static)> {
    if let FsError::Io(err) = &self.err {
      Some(err)
    } else {
      None
    }
  }
}

#[derive(Debug)]
pub enum OperationErrorKind {
  Bare,
  WithPath(String),
  WithTwoPaths(String, String),
}

trait MapErrContext {
  type R;

  fn context_fn<F>(self, f: F) -> Self::R
  where
    F: FnOnce(FsError) -> OperationError;

  fn context(self, desc: &'static str) -> Self::R;

  fn context_path<'a>(
    self,
    operation: &'static str,
    path: impl AsRef<PathWithRequested<'a>>,
  ) -> Self::R;

  fn context_two_path<'a>(
    self,
    operation: &'static str,
    from: impl AsRef<PathWithRequested<'a>>,
    to: impl AsRef<PathWithRequested<'a>>,
  ) -> Self::R;
}

impl<T> MapErrContext for Result<T, FsError> {
  type R = Result<T, FsOpsError>;

  fn context_fn<F>(self, f: F) -> Self::R
  where
    F: FnOnce(FsError) -> OperationError,
  {
    self.map_err(|err| FsOpsErrorKind::OperationError(f(err)).into_box())
  }

  fn context(self, operation: &'static str) -> Self::R {
    self.context_fn(move |err| OperationError {
      operation,
      kind: OperationErrorKind::Bare,
      err,
    })
  }

  fn context_path<'a>(
    self,
    operation: &'static str,
    path: impl AsRef<PathWithRequested<'a>>,
  ) -> Self::R {
    self.context_fn(|err| OperationError {
      operation,
      kind: OperationErrorKind::WithPath(path.as_ref().display().to_string()),
      err,
    })
  }

  fn context_two_path<'a>(
    self,
    operation: &'static str,
    oldpath: impl AsRef<PathWithRequested<'a>>,
    newpath: impl AsRef<PathWithRequested<'a>>,
  ) -> Self::R {
    self.context_fn(|err| OperationError {
      operation,
      kind: OperationErrorKind::WithTwoPaths(
        oldpath.as_ref().display().to_string(),
        newpath.as_ref().display().to_string(),
      ),
      err,
    })
  }
}

fn path_into_string(s: std::ffi::OsString) -> Result<String, FsOpsError> {
  s.into_string()
    .map_err(|e| FsOpsErrorKind::InvalidUtf8(e).into_box())
}

macro_rules! create_struct_writer {
  (pub struct $name:ident { $($field:ident: $type:ty),* $(,)? }) => {
    impl $name {
      fn write(self, buf: &mut [u32]) {
        let mut offset = 0;
        $(
          let value = self.$field as u64;
          buf[offset] = value as u32;
          buf[offset + 1] = (value >> 32) as u32;
          #[allow(unused_assignments, reason = "last assignment is unused")]
          {
            offset += 2;
          }
        )*
      }
    }

    #[derive(ToV8)]
    pub struct $name {
      $($field: $type),*
    }
  };
}

create_struct_writer! {
  pub struct SerializableStat {
    is_file: bool,
    is_directory: bool,
    is_symlink: bool,
    size: u64,
    // In milliseconds, like JavaScript. Available on both Unix or Windows.
    mtime_set: bool,
    mtime: i64,
    atime_set: bool,
    atime: i64,
    birthtime_set: bool,
    birthtime: i64,
    ctime_set: bool,
    ctime: i64,
    // Stats below are platform dependent.
    // On Unix, they are always available.
    // On Windows, the `*_set` fields are used
    // to indicate if the corresponding field is resolved.
    dev: u64,
    ino_set: bool,
    ino: u64,
    mode: u32,
    nlink_set: bool,
    nlink: u64,
    uid: u32,
    gid: u32,
    rdev: u64,
    blksize: u64,
    blocks_set: bool,
    blocks: u64,
    is_block_device: bool,
    is_char_device: bool,
    is_fifo: bool,
    is_socket: bool,
  }
}

impl From<FsStat> for SerializableStat {
  fn from(stat: FsStat) -> Self {
    SerializableStat {
      is_file: stat.is_file,
      is_directory: stat.is_directory,
      is_symlink: stat.is_symlink,
      size: stat.size,

      mtime_set: stat.mtime.is_some(),
      mtime: stat.mtime.unwrap_or(0),
      atime_set: stat.atime.is_some(),
      atime: stat.atime.unwrap_or(0),
      birthtime_set: stat.birthtime.is_some(),
      birthtime: stat.birthtime.unwrap_or(0),
      ctime_set: stat.ctime.is_some(),
      ctime: stat.ctime.unwrap_or(0),

      dev: stat.dev,
      ino_set: stat.ino.is_some(),
      ino: stat.ino.unwrap_or(0),
      mode: stat.mode,
      nlink_set: stat.nlink.is_some(),
      nlink: stat.nlink.unwrap_or(0),
      uid: stat.uid,
      gid: stat.gid,
      rdev: stat.rdev,
      blksize: stat.blksize,
      blocks_set: stat.blocks.is_some(),
      blocks: stat.blocks.unwrap_or(0),
      is_block_device: stat.is_block_device,
      is_char_device: stat.is_char_device,
      is_fifo: stat.is_fifo,
      is_socket: stat.is_socket,
    }
  }
}
