// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2018-2023 the Deno authors. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use serde::{Deserialize, Serialize, Serializer}; use serde_repr::{Deserialize_repr, Serialize_repr}; use tauri::{ ipc::{CommandScope, GlobalScope}, path::BaseDirectory, utils::config::FsScope, Manager, Resource, ResourceId, Runtime, Webview, }; use std::{ borrow::Cow, fs::File, io::{BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, str::FromStr, sync::Mutex, time::{SystemTime, UNIX_EPOCH}, }; use crate::{scope::Entry, Error, SafeFilePath}; #[derive(Debug, thiserror::Error)] pub enum CommandError { #[error(transparent)] Anyhow(#[from] anyhow::Error), #[error(transparent)] Plugin(#[from] Error), #[error(transparent)] Tauri(#[from] tauri::Error), #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] UrlParseError(#[from] url::ParseError), #[cfg(feature = "watch")] #[error(transparent)] Watcher(#[from] notify::Error), } impl From for CommandError { fn from(value: String) -> Self { Self::Anyhow(anyhow::anyhow!(value)) } } impl From<&str> for CommandError { fn from(value: &str) -> Self { Self::Anyhow(anyhow::anyhow!(value.to_string())) } } impl Serialize for CommandError { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { if let Self::Anyhow(err) = self { serializer.serialize_str(format!("{err:#}").as_ref()) } else { serializer.serialize_str(self.to_string().as_ref()) } } } pub type CommandResult = std::result::Result; #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BaseOptions { base_dir: Option, } #[tauri::command] pub fn create( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.and_then(|o| o.base_dir), )?; let file = File::create(&resolved_path).map_err(|e| { format!( "failed to create file at path: {} with error: {e}", resolved_path.display() ) })?; let rid = webview.resources_table().add(StdFileResource::new(file)); Ok(rid) } #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenOptions { #[serde(flatten)] base: BaseOptions, #[serde(flatten)] options: crate::OpenOptions, } #[tauri::command] pub fn open( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let (file, _path) = resolve_file( &webview, &global_scope, &command_scope, path, if let Some(opts) = options { OpenOptions { base: opts.base, options: opts.options, } } else { OpenOptions { base: BaseOptions { base_dir: None }, options: crate::OpenOptions { read: true, write: false, truncate: false, create: false, create_new: false, append: false, mode: None, custom_flags: None, }, } }, )?; let rid = webview.resources_table().add(StdFileResource::new(file)); Ok(rid) } #[tauri::command] pub fn close(webview: Webview, rid: ResourceId) -> CommandResult<()> { webview.resources_table().close(rid).map_err(Into::into) } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CopyFileOptions { from_path_base_dir: Option, to_path_base_dir: Option, } #[tauri::command] pub async fn copy_file( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, from_path: SafeFilePath, to_path: SafeFilePath, options: Option, ) -> CommandResult<()> { let resolved_from_path = resolve_path( &webview, &global_scope, &command_scope, from_path, options.as_ref().and_then(|o| o.from_path_base_dir), )?; let resolved_to_path = resolve_path( &webview, &global_scope, &command_scope, to_path, options.as_ref().and_then(|o| o.to_path_base_dir), )?; std::fs::copy(&resolved_from_path, &resolved_to_path).map_err(|e| { format!( "failed to copy file from path: {}, to path: {} with error: {e}", resolved_from_path.display(), resolved_to_path.display() ) })?; Ok(()) } #[derive(Debug, Clone, Deserialize)] pub struct MkdirOptions { #[serde(flatten)] base: BaseOptions, #[allow(unused)] mode: Option, recursive: Option, } #[tauri::command] pub fn mkdir( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult<()> { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.as_ref().and_then(|o| o.base.base_dir), )?; let mut builder = std::fs::DirBuilder::new(); builder.recursive(options.as_ref().and_then(|o| o.recursive).unwrap_or(false)); #[cfg(unix)] { use std::os::unix::fs::DirBuilderExt; let mode = options.as_ref().and_then(|o| o.mode).unwrap_or(0o777) & 0o777; builder.mode(mode); } builder .create(&resolved_path) .map_err(|e| { format!( "failed to create directory at path: {} with error: {e}", resolved_path.display() ) }) .map_err(Into::into) } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct DirEntry { pub name: String, pub is_directory: bool, pub is_file: bool, pub is_symlink: bool, } #[tauri::command] pub async fn read_dir( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult> { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.as_ref().and_then(|o| o.base_dir), )?; let entries = std::fs::read_dir(&resolved_path).map_err(|e| { format!( "failed to read directory at path: {} with error: {e}", resolved_path.display() ) })?; let entries = entries .filter_map(|entry| { let entry = entry.ok()?; let name = entry.file_name().into_string().ok()?; let metadata = entry.file_type(); macro_rules! method_or_false { ($method:ident) => { if let Ok(metadata) = &metadata { metadata.$method() } else { false } }; } Some(DirEntry { name, is_file: method_or_false!(is_file), is_directory: method_or_false!(is_dir), is_symlink: method_or_false!(is_symlink), }) }) .collect(); Ok(entries) } #[tauri::command] pub async fn read( webview: Webview, rid: ResourceId, len: usize, ) -> CommandResult { let mut data = vec![0; len]; let file = webview.resources_table().get::(rid)?; let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data)) .map_err(|e| format!("faied to read bytes from file with error: {e}"))?; // This is an optimization to include the number of read bytes (as bigendian bytes) // at the end of returned vector so we can use `tauri::ipc::Response` // and avoid serialization overhead of separate values. #[cfg(target_pointer_width = "16")] let nread = { let nread = nread.to_be_bytes(); let mut out = [0; 8]; out[6..].copy_from_slice(&nread); out }; #[cfg(target_pointer_width = "32")] let nread = { let nread = nread.to_be_bytes(); let mut out = [0; 8]; out[4..].copy_from_slice(&nread); out }; #[cfg(target_pointer_width = "64")] let nread = nread.to_be_bytes(); data.extend(nread); Ok(tauri::ipc::Response::new(data)) } #[tauri::command] pub async fn read_file( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let (mut file, path) = resolve_file( &webview, &global_scope, &command_scope, path, OpenOptions { base: BaseOptions { base_dir: options.as_ref().and_then(|o| o.base_dir), }, options: crate::OpenOptions { read: true, ..Default::default() }, }, )?; let mut contents = Vec::new(); file.read_to_end(&mut contents).map_err(|e| { format!( "failed to read file as text at path: {} with error: {e}", path.display() ) })?; Ok(tauri::ipc::Response::new(contents)) } // TODO, remove in v3, rely on `read_file` command instead #[tauri::command] pub async fn read_text_file( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { read_file(webview, global_scope, command_scope, path, options).await } #[tauri::command] pub fn read_text_file_lines( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.as_ref().and_then(|o| o.base_dir), )?; let file = File::open(&resolved_path).map_err(|e| { format!( "failed to open file at path: {} with error: {e}", resolved_path.display() ) })?; let lines = BufReader::new(file); let rid = webview.resources_table().add(StdLinesResource::new(lines)); Ok(rid) } #[tauri::command] pub async fn read_text_file_lines_next( webview: Webview, rid: ResourceId, ) -> CommandResult { let mut resource_table = webview.resources_table(); let lines = resource_table.get::(rid)?; let ret = StdLinesResource::with_lock(&lines, |lines| -> CommandResult> { // This is an optimization to include wether we finished iteration or not (1 or 0) // at the end of returned vector so we can use `tauri::ipc::Response` // and avoid serialization overhead of separate values. match lines.next() { Some(Ok(mut bytes)) => { bytes.push(false as u8); Ok(bytes) } Some(Err(_)) => Ok(vec![false as u8]), None => { resource_table.close(rid)?; Ok(vec![true as u8]) } } }); ret.map(tauri::ipc::Response::new) } #[derive(Debug, Clone, Deserialize)] pub struct RemoveOptions { #[serde(flatten)] base: BaseOptions, recursive: Option, } #[tauri::command] pub fn remove( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult<()> { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.as_ref().and_then(|o| o.base.base_dir), )?; let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { format!( "failed to get metadata of path: {} with error: {e}", resolved_path.display() ) })?; let file_type = metadata.file_type(); // taken from deno source code: https://github.com/denoland/deno/blob/429759fe8b4207240709c240a8344d12a1e39566/runtime/ops/fs.rs#L728 let res = if file_type.is_file() { std::fs::remove_file(&resolved_path) } else if options.as_ref().and_then(|o| o.recursive).unwrap_or(false) { std::fs::remove_dir_all(&resolved_path) } else if file_type.is_symlink() { #[cfg(unix)] { std::fs::remove_file(&resolved_path) } #[cfg(not(unix))] { use std::os::windows::fs::MetadataExt; const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x00000010; if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { std::fs::remove_dir(&resolved_path) } else { std::fs::remove_file(&resolved_path) } } } else if file_type.is_dir() { std::fs::remove_dir(&resolved_path) } else { // pipes, sockets, etc... std::fs::remove_file(&resolved_path) }; res.map_err(|e| { format!( "failed to remove path: {} with error: {e}", resolved_path.display() ) }) .map_err(Into::into) } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenameOptions { new_path_base_dir: Option, old_path_base_dir: Option, } #[tauri::command] pub fn rename( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, old_path: SafeFilePath, new_path: SafeFilePath, options: Option, ) -> CommandResult<()> { let resolved_old_path = resolve_path( &webview, &global_scope, &command_scope, old_path, options.as_ref().and_then(|o| o.old_path_base_dir), )?; let resolved_new_path = resolve_path( &webview, &global_scope, &command_scope, new_path, options.as_ref().and_then(|o| o.new_path_base_dir), )?; std::fs::rename(&resolved_old_path, &resolved_new_path) .map_err(|e| { format!( "failed to rename old path: {} to new path: {} with error: {e}", resolved_old_path.display(), resolved_new_path.display() ) }) .map_err(Into::into) } #[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] #[repr(u16)] pub enum SeekMode { Start = 0, Current = 1, End = 2, } #[tauri::command] pub async fn seek( webview: Webview, rid: ResourceId, offset: i64, whence: SeekMode, ) -> CommandResult { use std::io::{Seek, SeekFrom}; let file = webview.resources_table().get::(rid)?; StdFileResource::with_lock(&file, |mut file| { file.seek(match whence { SeekMode::Start => SeekFrom::Start(offset as u64), SeekMode::Current => SeekFrom::Current(offset), SeekMode::End => SeekFrom::End(offset), }) }) .map_err(|e| format!("failed to seek file with error: {e}")) .map_err(Into::into) } #[cfg(target_os = "android")] fn get_metadata std::io::Result>( metadata_fn: F, webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { match path { SafeFilePath::Url(url) => { let (file, path) = resolve_file( webview, global_scope, command_scope, SafeFilePath::Url(url), OpenOptions { base: BaseOptions { base_dir: None }, options: crate::OpenOptions { read: true, ..Default::default() }, }, )?; file.metadata().map_err(|e| { format!( "failed to get metadata of path: {} with error: {e}", path.display() ) .into() }) } SafeFilePath::Path(p) => get_fs_metadata( metadata_fn, webview, global_scope, command_scope, SafeFilePath::Path(p), options, ), } } #[cfg(not(target_os = "android"))] fn get_metadata std::io::Result>( metadata_fn: F, webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { get_fs_metadata( metadata_fn, webview, global_scope, command_scope, path, options, ) } fn get_fs_metadata std::io::Result>( metadata_fn: F, webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let resolved_path = resolve_path( webview, global_scope, command_scope, path, options.as_ref().and_then(|o| o.base_dir), )?; let metadata = metadata_fn(&resolved_path).map_err(|e| { format!( "failed to get metadata of path: {} with error: {e}", resolved_path.display() ) })?; Ok(metadata) } #[tauri::command] pub fn stat( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let metadata = get_metadata( |p| std::fs::metadata(p), &webview, &global_scope, &command_scope, path, options, )?; Ok(get_stat(metadata)) } #[tauri::command] pub fn lstat( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let metadata = get_metadata( |p| std::fs::symlink_metadata(p), &webview, &global_scope, &command_scope, path, options, )?; Ok(get_stat(metadata)) } #[tauri::command] pub fn fstat(webview: Webview, rid: ResourceId) -> CommandResult { let file = webview.resources_table().get::(rid)?; let metadata = StdFileResource::with_lock(&file, |file| file.metadata()) .map_err(|e| format!("failed to get metadata of file with error: {e}"))?; Ok(get_stat(metadata)) } #[tauri::command] pub async fn truncate( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, len: Option, options: Option, ) -> CommandResult<()> { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.as_ref().and_then(|o| o.base_dir), )?; let f = std::fs::OpenOptions::new() .write(true) .open(&resolved_path) .map_err(|e| { format!( "failed to open file at path: {} with error: {e}", resolved_path.display() ) })?; f.set_len(len.unwrap_or(0)) .map_err(|e| { format!( "failed to truncate file at path: {} with error: {e}", resolved_path.display() ) }) .map_err(Into::into) } #[tauri::command] pub async fn ftruncate( webview: Webview, rid: ResourceId, len: Option, ) -> CommandResult<()> { let file = webview.resources_table().get::(rid)?; StdFileResource::with_lock(&file, |file| file.set_len(len.unwrap_or(0))) .map_err(|e| format!("failed to truncate file with error: {e}")) .map_err(Into::into) } #[tauri::command] pub async fn write( webview: Webview, rid: ResourceId, data: Vec, ) -> CommandResult { let file = webview.resources_table().get::(rid)?; StdFileResource::with_lock(&file, |mut file| file.write(&data)) .map_err(|e| format!("failed to write bytes to file with error: {e}")) .map_err(Into::into) } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WriteFileOptions { #[serde(flatten)] base: BaseOptions, #[serde(default)] append: bool, #[serde(default = "default_create_value")] create: bool, #[serde(default)] create_new: bool, #[allow(unused)] mode: Option, } fn default_create_value() -> bool { true } #[tauri::command] pub async fn write_file( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, request: tauri::ipc::Request<'_>, ) -> CommandResult<()> { let data = match request.body() { tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( data.iter() .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) .collect(), ), _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), }; let path = request .headers() .get("path") .ok_or_else(|| anyhow::anyhow!("missing file path").into()) .and_then(|p| { percent_encoding::percent_decode(p.as_ref()) .decode_utf8() .map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into()) }) .and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?; let options: Option = request .headers() .get("options") .and_then(|p| p.to_str().ok()) .and_then(|opts| serde_json::from_str(opts).ok()); let (mut file, path) = resolve_file( &webview, &global_scope, &command_scope, path, if let Some(opts) = options { OpenOptions { base: opts.base, options: crate::OpenOptions { read: false, write: true, create: opts.create, truncate: !opts.append, append: opts.append, create_new: opts.create_new, mode: opts.mode, custom_flags: None, }, } } else { OpenOptions { base: BaseOptions { base_dir: None }, options: crate::OpenOptions { read: false, write: true, truncate: true, create: true, create_new: false, append: false, mode: None, custom_flags: None, }, } }, )?; file.write_all(&data) .map_err(|e| { format!( "failed to write bytes to file at path: {} with error: {e}", path.display() ) }) .map_err(Into::into) } // TODO, remove in v3, rely on `write_file` command instead #[tauri::command] pub async fn write_text_file( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, request: tauri::ipc::Request<'_>, ) -> CommandResult<()> { write_file(webview, global_scope, command_scope, request).await } #[tauri::command] pub fn exists( webview: Webview, global_scope: GlobalScope, command_scope: CommandScope, path: SafeFilePath, options: Option, ) -> CommandResult { let resolved_path = resolve_path( &webview, &global_scope, &command_scope, path, options.as_ref().and_then(|o| o.base_dir), )?; Ok(resolved_path.exists()) } #[cfg(not(target_os = "android"))] pub fn resolve_file( webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, open_options: OpenOptions, ) -> CommandResult<(File, PathBuf)> { resolve_file_in_fs(webview, global_scope, command_scope, path, open_options) } fn resolve_file_in_fs( webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, open_options: OpenOptions, ) -> CommandResult<(File, PathBuf)> { let path = resolve_path( webview, global_scope, command_scope, path, open_options.base.base_dir, )?; let file = std::fs::OpenOptions::from(open_options.options) .open(&path) .map_err(|e| { format!( "failed to open file at path: {} with error: {e}", path.display() ) })?; Ok((file, path)) } #[cfg(target_os = "android")] pub fn resolve_file( webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, open_options: OpenOptions, ) -> CommandResult<(File, PathBuf)> { use crate::FsExt; match path { SafeFilePath::Url(url) => { let path = url.as_str().into(); let file = webview .fs() .open(SafeFilePath::Url(url), open_options.options)?; Ok((file, path)) } SafeFilePath::Path(path) => resolve_file_in_fs( webview, global_scope, command_scope, SafeFilePath::Path(path), open_options, ), } } pub fn resolve_path( webview: &Webview, global_scope: &GlobalScope, command_scope: &CommandScope, path: SafeFilePath, base_dir: Option, ) -> CommandResult { let path = path.into_path()?; let path = if let Some(base_dir) = base_dir { webview.path().resolve(&path, base_dir)? } else { path }; let fs_scope = webview.state::(); let scope = tauri::scope::fs::Scope::new( webview, &FsScope::Scope { allow: global_scope .allows() .iter() .filter_map(|e| e.path.clone()) .chain(command_scope.allows().iter().filter_map(|e| e.path.clone())) .collect(), deny: global_scope .denies() .iter() .filter_map(|e| e.path.clone()) .chain(command_scope.denies().iter().filter_map(|e| e.path.clone())) .collect(), require_literal_leading_dot: fs_scope.require_literal_leading_dot, }, )?; let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix)); if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot) || is_forbidden(&scope, &path, require_literal_leading_dot) { return Err(CommandError::Plugin(Error::PathForbidden(path))); } if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) { Ok(path) } else { Err(CommandError::Plugin(Error::PathForbidden(path))) } } fn is_forbidden>( scope: &tauri::fs::Scope, path: P, require_literal_leading_dot: bool, ) -> bool { let path = path.as_ref(); let path = if path.is_symlink() { match std::fs::read_link(path) { Ok(p) => p, Err(_) => return false, } } else { path.to_path_buf() }; let path = if !path.exists() { crate::Result::Ok(path) } else { std::fs::canonicalize(path).map_err(Into::into) }; if let Ok(path) = path { let path: PathBuf = path.components().collect(); scope.forbidden_patterns().iter().any(|p| { p.matches_path_with( &path, glob::MatchOptions { // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` // see: require_literal_separator: true, require_literal_leading_dot, ..Default::default() }, ) }) } else { false } } struct StdFileResource(Mutex); impl StdFileResource { fn new(file: File) -> Self { Self(Mutex::new(file)) } fn with_lock R>(&self, mut f: F) -> R { let file = self.0.lock().unwrap(); f(&file) } } impl Resource for StdFileResource {} /// Same as [std::io::Lines] but with bytes struct LinesBytes(T); impl Iterator for LinesBytes { type Item = std::io::Result>; fn next(&mut self) -> Option>> { let mut buf = Vec::new(); match self.0.read_until(b'\n', &mut buf) { Ok(0) => None, Ok(_n) => { if buf.last() == Some(&b'\n') { buf.pop(); if buf.last() == Some(&b'\r') { buf.pop(); } } Some(Ok(buf)) } Err(e) => Some(Err(e)), } } } struct StdLinesResource(Mutex>>); impl StdLinesResource { fn new(lines: BufReader) -> Self { Self(Mutex::new(LinesBytes(lines))) } fn with_lock>) -> R>(&self, mut f: F) -> R { let mut lines = self.0.lock().unwrap(); f(&mut lines) } } impl Resource for StdLinesResource {} // taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913 #[inline] fn to_msec(maybe_time: std::result::Result) -> Option { match maybe_time { Ok(time) => { let msec = time .duration_since(UNIX_EPOCH) .map(|t| t.as_millis() as u64) .unwrap_or_else(|err| err.duration().as_millis() as u64); Some(msec) } Err(_) => None, } } // taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L926 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct FileInfo { is_file: bool, is_directory: bool, is_symlink: bool, size: u64, // In milliseconds, like JavaScript. Available on both Unix or Windows. mtime: Option, atime: Option, birthtime: Option, readonly: bool, // Following are only valid under Windows. file_attribues: Option, // Following are only valid under Unix. dev: Option, ino: Option, mode: Option, nlink: Option, uid: Option, gid: Option, rdev: Option, blksize: Option, blocks: Option, } // taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L950 #[inline(always)] fn get_stat(metadata: std::fs::Metadata) -> FileInfo { // Unix stat member (number types only). 0 if not on unix. macro_rules! usm { ($member:ident) => {{ #[cfg(unix)] { Some(metadata.$member()) } #[cfg(not(unix))] { None } }}; } #[cfg(unix)] use std::os::unix::fs::MetadataExt; #[cfg(windows)] use std::os::windows::fs::MetadataExt; FileInfo { is_file: metadata.is_file(), is_directory: metadata.is_dir(), is_symlink: metadata.file_type().is_symlink(), size: metadata.len(), // In milliseconds, like JavaScript. Available on both Unix or Windows. mtime: to_msec(metadata.modified()), atime: to_msec(metadata.accessed()), birthtime: to_msec(metadata.created()), readonly: metadata.permissions().readonly(), // Following are only valid under Windows. #[cfg(windows)] file_attribues: Some(metadata.file_attributes()), #[cfg(not(windows))] file_attribues: None, // 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), blksize: usm!(blksize), blocks: usm!(blocks), } } #[cfg(test)] mod test { use std::io::{BufRead, BufReader}; use super::LinesBytes; #[test] fn safe_file_path_parse() { use super::SafeFilePath; assert!(matches!( serde_json::from_str::("\"C:/Users\""), Ok(SafeFilePath::Path(_)) )); assert!(matches!( serde_json::from_str::("\"file:///C:/Users\""), Ok(SafeFilePath::Url(_)) )); } #[test] fn test_lines_bytes() { let base = String::from("line 1\nline2\nline 3\nline 4"); let bytes = base.as_bytes(); let string1 = base.lines().collect::(); let string2 = BufReader::new(bytes) .lines() .map_while(Result::ok) .collect::(); let string3 = LinesBytes(BufReader::new(bytes)) .flatten() .flat_map(String::from_utf8) .collect::(); assert_eq!(string1, string2); assert_eq!(string1, string3); assert_eq!(string2, string3); } }