// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Some deserializer fields are only used on Unix and Windows build fails without it use super::dispatch_json::{blocking_json, Deserialize, JsonOp, Value}; use super::io::{FileMetadata, StreamResource, StreamResourceHolder}; use crate::fs as deno_fs; use crate::op_error::OpError; use crate::ops::dispatch_json::JsonResult; use crate::state::State; use deno_core::*; use futures::future::FutureExt; use remove_dir_all::remove_dir_all; use std; use std::convert::From; use std::fs; use std::io::SeekFrom; use std::path::Path; use std::time::UNIX_EPOCH; use tokio; #[cfg(unix)] use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt}; pub fn init(i: &mut Isolate, s: &State) { i.register_op("op_open", s.stateful_json_op(op_open)); i.register_op("op_seek", s.stateful_json_op(op_seek)); i.register_op("op_umask", s.stateful_json_op(op_umask)); i.register_op("op_chdir", s.stateful_json_op(op_chdir)); i.register_op("op_mkdir", s.stateful_json_op(op_mkdir)); i.register_op("op_chmod", s.stateful_json_op(op_chmod)); i.register_op("op_chown", s.stateful_json_op(op_chown)); i.register_op("op_remove", s.stateful_json_op(op_remove)); i.register_op("op_copy_file", s.stateful_json_op(op_copy_file)); i.register_op("op_stat", s.stateful_json_op(op_stat)); i.register_op("op_realpath", s.stateful_json_op(op_realpath)); i.register_op("op_read_dir", s.stateful_json_op(op_read_dir)); i.register_op("op_rename", s.stateful_json_op(op_rename)); i.register_op("op_link", s.stateful_json_op(op_link)); i.register_op("op_symlink", s.stateful_json_op(op_symlink)); i.register_op("op_read_link", s.stateful_json_op(op_read_link)); i.register_op("op_truncate", s.stateful_json_op(op_truncate)); i.register_op("op_make_temp_dir", s.stateful_json_op(op_make_temp_dir)); i.register_op("op_make_temp_file", s.stateful_json_op(op_make_temp_file)); i.register_op("op_cwd", s.stateful_json_op(op_cwd)); i.register_op("op_utime", s.stateful_json_op(op_utime)); } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OpenArgs { promise_id: Option, path: String, options: Option, open_mode: Option, mode: Option, } #[derive(Deserialize, Default, Debug)] #[serde(rename_all = "camelCase")] #[serde(default)] struct OpenOptions { read: bool, write: bool, create: bool, truncate: bool, append: bool, create_new: bool, } fn op_open( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: OpenArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; let state_ = state.clone(); let mut open_options = if let Some(mode) = args.mode { #[allow(unused_mut)] let mut std_options = fs::OpenOptions::new(); // mode only used if creating the file on Unix // if not specified, defaults to 0o666 #[cfg(unix)] std_options.mode(mode & 0o777); #[cfg(not(unix))] let _ = mode; // avoid unused warning tokio::fs::OpenOptions::from(std_options) } else { tokio::fs::OpenOptions::new() }; if let Some(options) = args.options { if options.read { state.check_read(&path)?; } if options.write || options.append { state.check_write(&path)?; } open_options .read(options.read) .create(options.create) .write(options.write) .truncate(options.truncate) .append(options.append) .create_new(options.create_new); } else if let Some(open_mode) = args.open_mode { let open_mode = open_mode.as_ref(); match open_mode { "r" => { state.check_read(&path)?; } "w" | "a" | "x" => { state.check_write(&path)?; } &_ => { state.check_read(&path)?; state.check_write(&path)?; } }; match open_mode { "r" => { open_options.read(true); } "r+" => { open_options.read(true).write(true); } "w" => { open_options.create(true).write(true).truncate(true); } "w+" => { open_options .read(true) .create(true) .write(true) .truncate(true); } "a" => { open_options.create(true).append(true); } "a+" => { open_options.read(true).create(true).append(true); } "x" => { open_options.create_new(true).write(true); } "x+" => { open_options.create_new(true).read(true).write(true); } &_ => { // TODO: this should be type error return Err(OpError::other("Unknown open mode.".to_string())); } } } else { return Err(OpError::other( "Open requires either openMode or options.".to_string(), )); }; let is_sync = args.promise_id.is_none(); let fut = async move { let fs_file = open_options.open(path).await?; let mut state = state_.borrow_mut(); let rid = state.resource_table.add( "fsFile", Box::new(StreamResourceHolder::new(StreamResource::FsFile( fs_file, FileMetadata::default(), ))), ); Ok(json!(rid)) }; if is_sync { let buf = futures::executor::block_on(fut)?; Ok(JsonOp::Sync(buf)) } else { Ok(JsonOp::Async(fut.boxed_local())) } } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SeekArgs { promise_id: Option, rid: i32, offset: i32, whence: i32, } fn op_seek( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: SeekArgs = serde_json::from_value(args)?; let rid = args.rid as u32; let offset = args.offset; let whence = args.whence as u32; // Translate seek mode to Rust repr. let seek_from = match whence { 0 => SeekFrom::Start(offset as u64), 1 => SeekFrom::Current(i64::from(offset)), 2 => SeekFrom::End(i64::from(offset)), _ => { return Err(OpError::type_error(format!( "Invalid seek mode: {}", whence ))); } }; let state = state.borrow(); let resource_holder = state .resource_table .get::(rid) .ok_or_else(OpError::bad_resource_id)?; let tokio_file = match resource_holder.resource { StreamResource::FsFile(ref file, _) => file, _ => return Err(OpError::bad_resource_id()), }; let mut file = futures::executor::block_on(tokio_file.try_clone())?; let fut = async move { let pos = file.seek(seek_from).await?; Ok(json!(pos)) }; if args.promise_id.is_none() { let buf = futures::executor::block_on(fut)?; Ok(JsonOp::Sync(buf)) } else { Ok(JsonOp::Async(fut.boxed_local())) } } #[derive(Deserialize)] struct UmaskArgs { mask: Option, } fn op_umask( _state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: UmaskArgs = serde_json::from_value(args)?; // TODO implement umask for Windows // see https://github.com/nodejs/node/blob/master/src/node_process_methods.cc // and https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/reference/umask?view=vs-2019 #[cfg(not(unix))] { let _ = args.mask; // avoid unused warning. return Err(OpError::not_implemented()); } #[cfg(unix)] { use nix::sys::stat::mode_t; use nix::sys::stat::umask; use nix::sys::stat::Mode; let r = if let Some(mask) = args.mask { // If mask provided, return previous. umask(Mode::from_bits_truncate(mask as mode_t)) } else { // If no mask provided, we query the current. Requires two syscalls. let prev = umask(Mode::from_bits_truncate(0o777)); let _ = umask(prev); prev }; Ok(JsonOp::Sync(json!(r.bits() as u32))) } } #[derive(Deserialize)] struct ChdirArgs { directory: String, } fn op_chdir( _state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: ChdirArgs = serde_json::from_value(args)?; std::env::set_current_dir(&args.directory)?; Ok(JsonOp::Sync(json!({}))) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MkdirArgs { promise_id: Option, path: String, recursive: bool, mode: Option, } fn op_mkdir( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: MkdirArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; let mode = args.mode.unwrap_or(0o777); state.check_write(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); deno_fs::mkdir(&path, mode, args.recursive)?; Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ChmodArgs { promise_id: Option, path: String, #[allow(unused)] mode: u32, } fn op_chmod( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: ChmodArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; state.check_write(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_chmod {}", path.display()); // Still check file/dir exists on windows let _metadata = fs::metadata(&path)?; #[cfg(unix)] { let mut permissions = _metadata.permissions(); permissions.set_mode(args.mode); fs::set_permissions(&path, permissions)?; } Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ChownArgs { promise_id: Option, path: String, uid: u32, gid: u32, } fn op_chown( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: ChownArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; state.check_write(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_chown {}", path.display()); deno_fs::chown(args.path.as_ref(), args.uid, args.gid)?; Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct RemoveArgs { promise_id: Option, path: String, recursive: bool, } fn op_remove( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: RemoveArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; let recursive = args.recursive; state.check_write(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_remove {}", path.display()); let metadata = fs::symlink_metadata(&path)?; let file_type = metadata.file_type(); if file_type.is_file() || file_type.is_symlink() { fs::remove_file(&path)?; } else if recursive { remove_dir_all(&path)?; } else { fs::remove_dir(&path)?; } Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CopyFileArgs { promise_id: Option, from: String, to: String, } fn op_copy_file( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: CopyFileArgs = serde_json::from_value(args)?; let from = deno_fs::resolve_from_cwd(Path::new(&args.from))?; let to = deno_fs::resolve_from_cwd(Path::new(&args.to))?; state.check_read(&from)?; state.check_write(&to)?; debug!("op_copy_file {} {}", from.display(), to.display()); let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput // See https://github.com/rust-lang/rust/issues/54800 // Once the issue is resolved, we should remove this workaround. if cfg!(unix) && !from.is_file() { return Err(OpError::not_found("File not found".to_string())); } // returns size of from as u64 (we ignore) fs::copy(&from, &to)?; Ok(json!({})) }) } macro_rules! to_seconds { ($time:expr) => {{ // Unwrap is safe here as if the file is before the unix epoch // something is very wrong. $time .and_then(|t| Ok(t.duration_since(UNIX_EPOCH).unwrap().as_secs())) .unwrap_or(0) }}; } #[inline(always)] fn get_stat_json( metadata: fs::Metadata, maybe_name: Option, ) -> JsonResult { // Unix stat member (number types only). 0 if not on unix. macro_rules! usm { ($member: ident) => {{ #[cfg(unix)] { metadata.$member() } #[cfg(not(unix))] { 0 } }}; } let mut json_val = json!({ "isFile": metadata.is_file(), "isSymlink": metadata.file_type().is_symlink(), "size": metadata.len(), // In seconds. Available on both Unix or Windows. "modified":to_seconds!(metadata.modified()), "accessed":to_seconds!(metadata.accessed()), "created":to_seconds!(metadata.created()), // Following are only valid under Unix. "dev": usm!(dev), "ino": usm!(ino), "mode": usm!(mode), "nlink": usm!(nlink), "uid": usm!(uid), "gid": usm!(gid), "rdev": usm!(rdev), // TODO(kevinkassimo): *time_nsec requires BigInt. // Probably should be treated as String if we need to add them. "blksize": usm!(blksize), "blocks": usm!(blocks), }); // "name" is an optional field by our design. if let Some(name) = maybe_name { if let serde_json::Value::Object(ref mut m) = json_val { m.insert("name".to_owned(), json!(name)); } } Ok(json_val) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct StatArgs { promise_id: Option, path: String, lstat: bool, } fn op_stat( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: StatArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; let lstat = args.lstat; state.check_read(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_stat {} {}", path.display(), lstat); let metadata = if lstat { fs::symlink_metadata(&path)? } else { fs::metadata(&path)? }; get_stat_json(metadata, None) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct RealpathArgs { promise_id: Option, path: String, } fn op_realpath( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: RealpathArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; state.check_read(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_realpath {}", path.display()); // corresponds to the realpath on Unix and // CreateFile and GetFinalPathNameByHandle on Windows let realpath = fs::canonicalize(&path)?; let mut realpath_str = realpath.to_str().unwrap().to_owned().replace("\\", "/"); if cfg!(windows) { realpath_str = realpath_str.trim_start_matches("//?/").to_string(); } Ok(json!(realpath_str)) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ReadDirArgs { promise_id: Option, path: String, } fn op_read_dir( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: ReadDirArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; state.check_read(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_read_dir {}", path.display()); let entries: Vec<_> = fs::read_dir(path)? .filter_map(|entry| { let entry = entry.unwrap(); let metadata = entry.metadata().unwrap(); // Not all filenames can be encoded as UTF-8. Skip those for now. if let Some(filename) = entry.file_name().to_str() { let filename = Some(filename.to_owned()); Some(get_stat_json(metadata, filename).unwrap()) } else { None } }) .collect(); Ok(json!({ "entries": entries })) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct RenameArgs { promise_id: Option, oldpath: String, newpath: String, } fn op_rename( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: RenameArgs = serde_json::from_value(args)?; let oldpath = deno_fs::resolve_from_cwd(Path::new(&args.oldpath))?; let newpath = deno_fs::resolve_from_cwd(Path::new(&args.newpath))?; state.check_read(&oldpath)?; state.check_write(&oldpath)?; state.check_write(&newpath)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_rename {} {}", oldpath.display(), newpath.display()); fs::rename(&oldpath, &newpath)?; Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct LinkArgs { promise_id: Option, oldname: String, newname: String, } fn op_link( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: LinkArgs = serde_json::from_value(args)?; let oldname = deno_fs::resolve_from_cwd(Path::new(&args.oldname))?; let newname = deno_fs::resolve_from_cwd(Path::new(&args.newname))?; state.check_read(&oldname)?; state.check_write(&newname)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_link {} {}", oldname.display(), newname.display()); std::fs::hard_link(&oldname, &newname)?; Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SymlinkArgs { promise_id: Option, oldname: String, newname: String, } fn op_symlink( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: SymlinkArgs = serde_json::from_value(args)?; let oldname = deno_fs::resolve_from_cwd(Path::new(&args.oldname))?; let newname = deno_fs::resolve_from_cwd(Path::new(&args.newname))?; state.check_write(&newname)?; // TODO Use type for Windows. if cfg!(not(unix)) { return Err(OpError::other("Not implemented".to_string())); } let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_symlink {} {}", oldname.display(), newname.display()); #[cfg(unix)] std::os::unix::fs::symlink(&oldname, &newname)?; Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ReadLinkArgs { promise_id: Option, path: String, } fn op_read_link( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: ReadLinkArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; state.check_read(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_read_link {}", path.display()); let path = fs::read_link(&path)?; let path_str = path.to_str().unwrap(); Ok(json!(path_str)) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct TruncateArgs { promise_id: Option, path: String, len: u64, } fn op_truncate( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: TruncateArgs = serde_json::from_value(args)?; let path = deno_fs::resolve_from_cwd(Path::new(&args.path))?; let len = args.len; state.check_write(&path)?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_truncate {} {}", path.display(), len); let f = fs::OpenOptions::new().write(true).open(&path)?; f.set_len(len)?; Ok(json!({})) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MakeTempArgs { promise_id: Option, dir: Option, prefix: Option, suffix: Option, } fn op_make_temp_dir( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: MakeTempArgs = serde_json::from_value(args)?; let dir = args .dir .map(|s| deno_fs::resolve_from_cwd(Path::new(&s)).unwrap()); let prefix = args.prefix.map(String::from); let suffix = args.suffix.map(String::from); state .check_write(dir.clone().unwrap_or_else(std::env::temp_dir).as_path())?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { // TODO(piscisaureus): use byte vector for paths, not a string. // See https://github.com/denoland/deno/issues/627. // We can't assume that paths are always valid utf8 strings. let path = deno_fs::make_temp( // Converting Option to Option<&str> dir.as_ref().map(|x| &**x), prefix.as_ref().map(|x| &**x), suffix.as_ref().map(|x| &**x), true, )?; let path_str = path.to_str().unwrap(); Ok(json!(path_str)) }) } fn op_make_temp_file( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: MakeTempArgs = serde_json::from_value(args)?; let dir = args .dir .map(|s| deno_fs::resolve_from_cwd(Path::new(&s)).unwrap()); let prefix = args.prefix.map(String::from); let suffix = args.suffix.map(String::from); state .check_write(dir.clone().unwrap_or_else(std::env::temp_dir).as_path())?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { // TODO(piscisaureus): use byte vector for paths, not a string. // See https://github.com/denoland/deno/issues/627. // We can't assume that paths are always valid utf8 strings. let path = deno_fs::make_temp( // Converting Option to Option<&str> dir.as_ref().map(|x| &**x), prefix.as_ref().map(|x| &**x), suffix.as_ref().map(|x| &**x), false, )?; let path_str = path.to_str().unwrap(); Ok(json!(path_str)) }) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct UtimeArgs { promise_id: Option, path: String, atime: u64, mtime: u64, } fn op_utime( state: &State, args: Value, _zero_copy: Option, ) -> Result { let args: UtimeArgs = serde_json::from_value(args)?; state.check_write(Path::new(&args.path))?; let is_sync = args.promise_id.is_none(); blocking_json(is_sync, move || { debug!("op_utime {} {} {}", args.path, args.atime, args.mtime); utime::set_file_times(args.path, args.atime, args.mtime)?; Ok(json!({})) }) } fn op_cwd( _state: &State, _args: Value, _zero_copy: Option, ) -> Result { let path = std::env::current_dir()?; let path_str = path.into_os_string().into_string().unwrap(); Ok(JsonOp::Sync(json!(path_str))) }