diff --git a/Cargo.toml b/Cargo.toml index 8013baf..6d120cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,9 @@ members = [ # xmas-js "package-manager", + "bundler", "modules", + "vsys", "repl", ] @@ -30,6 +32,8 @@ rsquickjs = { version = "0.10.0", path = "rsquickjs" } xmas-js-modules = { path = "modules" } xmas-package-manager = { path = "package-manager" } +xmas-vsys = { path = "vsys" } +xmas-bundler = { path = "bundler" } [dependencies] xmas-js-modules = { workspace = true } diff --git a/README.md b/README.md index ef88945..4679432 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,81 @@ Traditional System Scripts Modern System Scripts with Xmas.JS --- +## πŸ—οΈ Virtual System Layer + +Xmas.JS uses a **pluggable virtual system layer** called `vsys` to abstract all system-level operations. This enables: + +- πŸ”’ **Sandboxed execution** for serverless/edge computing +- πŸ’Ύ **Custom filesystem** implementations (in-memory, virtual, restricted) +- 🌐 **Custom network** implementations (proxied, restricted, mocked) +- οΏ½ **Custom module loading** (load from DB, bundle, remote URL) +- πŸ” **Fine-grained permissions** control + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ modules (JS Binding Layer) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ fs/mod β”‚ β”‚http/mod β”‚ β”‚module/ β”‚ β”‚Other JS Modules β”‚ β”‚ +β”‚ β”‚(ModuleDefβ”‚ β”‚(ModuleDefβ”‚ β”‚loader β”‚ β”‚(Only registration, β”‚ β”‚ +β”‚ β”‚ only) β”‚ β”‚ only) β”‚ β”‚resolver β”‚ β”‚ calls vsys) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ vsys (Virtual System Layer) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ pub struct VsysVTable { β”‚β”‚ +β”‚ β”‚ // Filesystem β”‚β”‚ +β”‚ β”‚ pub fs_read, fs_write, fs_stat, fs_readdir, ... β”‚β”‚ +β”‚ β”‚ // Network β”‚β”‚ +β”‚ β”‚ pub http_request, dns_lookup, ... β”‚β”‚ +β”‚ β”‚ // Module Loading (key for serverless!) β”‚β”‚ +β”‚ β”‚ pub module_resolve, module_load, module_exists, ... β”‚β”‚ +β”‚ β”‚ // Permissions β”‚β”‚ +β”‚ β”‚ pub check_fs_permission, check_net_permission, ... β”‚β”‚ +β”‚ β”‚ } β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Default Impl β”‚ β”‚ User Custom Impl β”‚ β”‚ +β”‚ β”‚ (std::fs, tokio, β”‚ OR β”‚ (VFS, sandboxed, β”‚ β”‚ +β”‚ β”‚ hyper, etc.) β”‚ β”‚ in-memory, etc.) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### What Problems Does vsys Solve? + +| Scenario | Problem Without vsys | With vsys | +| ---------------------- | --------------------------------------------------- | ------------------------------------------------- | +| **Serverless/Edge** | Runtime has full system access, security risk | Sandboxed execution, only expose what you allow | +| **Multi-tenant SaaS** | Tenant A can access Tenant B's files | Each tenant gets isolated virtual filesystem | +| **Database Scripting** | Scripts need real filesystem, deployment complexity | Virtual FS backed by database, zero external deps | +| **Bundled Deploy** | Need node_modules on disk, slow cold start | Load modules from single bundle or remote URL | +| **Testing** | Need real network/files, slow and flaky tests | Mock everything, fast and deterministic | +| **Embedded/IoT** | Heavy system dependencies | Minimal footprint, platform-agnostic | +| **Game Scripting** | Lua-style sandboxing is complex | Built-in isolation, expose only game APIs | + +### Example: Secure Serverless Function + +```rust +// User's untrusted code can only: +// - Read from /app/data (virtual, mapped to S3) +// - Make HTTP requests to allowlisted domains +// - Load modules from pre-bundled package (no filesystem access) +// - No filesystem writes, no arbitrary network access +let runtime = XmasRuntime::new() + .with_vsys(VsysVTable::new() + .fs_read_only(s3_virtual_fs("/app/data")) + .module_loader(bundled_modules("app.bundle")) + .net_allowlist(&["api.example.com", "cdn.example.com"]) + .deny_all_else() + ); +``` +--- + ## πŸ“¦ Installation ### 🚧 From Binary (Coming soon ❄️) diff --git a/TODO.md b/TODO.md index e2031fc..87053a7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ + # WASM i decided to use WAMR, @@ -144,4 +145,25 @@ Global methods / properties: - [x] Repl - [ ] complete method/property name - [ ] complete globalThis property name - - [x] package manager commands \ No newline at end of file + - [x] package manager commands + +--- + +# Vsys (Virtual System Layer) + + +- [x] vsys crate +- [x] FsVTable +- [x] Permissions +- [x] ModuleLoaderVTable +- [x] modules/src/fs + + +- [ ] modules/src/module/package/resolver.rs + - `fs::read`, `Path::is_file()`, `Path::is_dir()`, `Path::exists()`, `read_link()` +- [ ] modules/src/module/package/loader.rs - `std::fs::read`, `File::open` +- [ ] modules/src/module/module/require.rs - `fs::read_to_string` +- [ ] NetVTable +- [ ] EnvVTable + +--- diff --git a/bundler/Cargo.toml b/bundler/Cargo.toml new file mode 100644 index 0000000..93f5eed --- /dev/null +++ b/bundler/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "xmas-bundler" +version = "0.1.0" +edition = "2021" + +[dependencies] +rolldown = { git = "https://github.com/rolldown/rolldown", package = "rolldown", tag = "v1.0.0-beta.57", features = [ + "experimental", +] } +clap = { version = "4.5.4", features = ["derive"] } +thiserror = "2.0.17" diff --git a/bundler/src/lib.rs b/bundler/src/lib.rs new file mode 100644 index 0000000..e2bb73b --- /dev/null +++ b/bundler/src/lib.rs @@ -0,0 +1,169 @@ +//! Bundler module powered by Rolldown +//! +//! Rolldown is a fast Rust-based bundler that's Rollup-compatible and designed for Vite. +//! It provides 10-30x faster bundling than Rollup with full plugin ecosystem support. +//! +//! Features: +//! - Fast Rust-based bundling +//! - Rollup-compatible API +//! - Built-in minification +//! - Tree-shaking +//! - Code splitting +//! - Source maps + +use std::path::PathBuf; + +use clap::{Parser, ValueEnum}; +use thiserror::Error; + +/// Errors that can occur during bundling +#[derive(Error, Debug)] +pub enum BundleError { + #[error("Bundling failed: {0}")] + BundleFailed(String), + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Rolldown feature not enabled")] + FeatureNotEnabled, +} + +/// Result type for bundler operations +pub type BundleResult = Result; + +/// Configuration for the bundler +#[derive(Debug, Clone, Parser)] +#[command(name = "bundle", about = "Bundle TypeScript/JavaScript files")] +pub struct BundleConfig { + /// Entry point(s) for the bundle + #[arg(required = true)] + pub entry: Vec, + + /// Output directory + #[arg(short = 'o', long, default_value = "dist")] + pub output_dir: PathBuf, + + /// Output filename + #[arg(short = 'n', long)] + pub output_filename: Option, + + /// Enable minification + #[arg(short = 'm', long)] + pub minify: bool, + + /// Enable source maps + #[arg(short = 's', long)] + pub source_map: bool, + + /// Target format (esm, cjs, iife) + #[arg(short = 'f', long, default_value = "esm")] + pub format: BundleFormat, + + /// Enable tree-shaking + #[arg(long, default_value = "true")] + pub tree_shake: bool, + + /// External modules (won't be bundled) + #[arg(short = 'e', long)] + pub external: Vec, +} + +/// Bundle output format +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)] +pub enum BundleFormat { + /// ES Module format + #[default] + Esm, + /// CommonJS format + Cjs, + /// Immediately Invoked Function Expression + Iife, +} + +impl Default for BundleConfig { + fn default() -> Self { + Self { + entry: Vec::new(), + output_dir: PathBuf::from("dist"), + output_filename: None, + minify: false, + source_map: false, + format: BundleFormat::Esm, + tree_shake: true, + external: Vec::new(), + } + } +} + +/// Bundle TypeScript/JavaScript files using Rolldown +pub async fn bundle(config: BundleConfig) -> BundleResult<()> { + use rolldown::{Bundler, BundlerOptions, InputItem, OutputFormat}; + + // Convert entry points to InputItem + let input_items: Vec = config + .entry + .iter() + .enumerate() + .map(|(idx, path)| { + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(&format!("entry{}", idx)) + .to_string(); + + InputItem { + name: Some(name), + import: path.to_string_lossy().to_string(), + } + }) + .collect(); + + // Convert format + let output_format = match config.format { + BundleFormat::Esm => OutputFormat::Esm, + BundleFormat::Cjs => OutputFormat::Cjs, + BundleFormat::Iife => OutputFormat::Iife, + }; + + // Create bundler with options + let bundler = Bundler::new(BundlerOptions { + input: Some(input_items), + dir: Some(config.output_dir.to_string_lossy().to_string()), + format: Some(output_format), + minify: Some(rolldown::RawMinifyOptions::Bool(config.minify)), + sourcemap: config.source_map.then(|| rolldown::SourceMapType::File), + external: if config.external.is_empty() { + None + } else { + Some(rolldown::IsExternal::from(config.external.clone())) + }, + ..Default::default() + }); + + // Run bundler + let output = bundler + .map_err(|e| BundleError::BundleFailed(e.to_string()))? + .write() + .await + .map_err(|e| BundleError::BundleFailed(format!("Rolldown bundling failed: {:?}", e)))?; + + for w in output.warnings { + eprintln!("Warning: {}", w); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config() { + let config = BundleConfig::default(); + assert_eq!(config.format, BundleFormat::Esm); + assert!(config.tree_shake); + assert!(!config.minify); + } +} diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 3fa5175..beda3a2 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["quickjs", "ecmascript", "javascript", "es6", "es2020"] [dependencies] rsquickjs = { workspace = true, features = ["macro", "either", "phf"] } +xmas-vsys = { path = "../vsys" } tracing = "0.1.44" itoa = "1.0.15" ryu = "1.0.20" @@ -31,11 +32,13 @@ chrono-tz = { version = "0.10", default-features = false, optional = true } iana-time-zone = { version = "0.1", optional = true } # source -oxc = { version = "0.105.0", optional = true, features = [ +oxc = { version = "^0.103.0", optional = true, features = [ "transformer", "codegen", "semantic", ] } +oxc_resolver = "=11.16.0" +thiserror = "*" itertools = "0.14.0" either = "1.15.0" ring = "0.17.14" diff --git a/modules/src/fetch/security.rs b/modules/src/fetch/security.rs index ad22ed3..2c1f1f1 100644 --- a/modules/src/fetch/security.rs +++ b/modules/src/fetch/security.rs @@ -4,38 +4,9 @@ use rsquickjs::{Ctx, Error, Exception, Result}; use crate::permissions; pub fn ensure_url_access(ctx: &Ctx<'_>, uri: &Uri) -> Result<()> { - let permissions = ctx.userdata::().unwrap(); - match &permissions.net { - permissions::BlackOrWhiteList::BlackList(items) => { - if url_match( - &items - .iter() - .map(|e| Uri::try_from(e)) - .filter_map(|ruri| match ruri { - Ok(uri) => Some(uri), - Err(_) => None, - }) - .collect::>(), - uri, - ) { - return Err(url_restricted_error(ctx, "URL denied", uri)); - } - } - permissions::BlackOrWhiteList::WhiteList(items) => { - if !url_match( - &items - .iter() - .map(|e| Uri::try_from(e)) - .filter_map(|ruri| match ruri { - Ok(uri) => Some(uri), - Err(_) => None, - }) - .collect::>(), - uri, - ) { - return Err(url_restricted_error(ctx, "URL not allowed", uri)); - } - } + let host = uri.host().unwrap_or_default(); + if !permissions::check_net_permission(ctx, host) { + return Err(url_restricted_error(ctx, "URL not allowed", uri)); } // if let Some(allow_list) = HTTP_ALLOW_LIST.get() { // if !url_match(allow_list, uri) { diff --git a/modules/src/fs.rs b/modules/src/fs.rs new file mode 100644 index 0000000..68cd0f5 --- /dev/null +++ b/modules/src/fs.rs @@ -0,0 +1,937 @@ +//! Node.js-compatible filesystem module using vsys FsVTable +//! +//! All filesystem operations are delegated to the vsys virtual filesystem layer, +//! enabling sandboxed execution and custom filesystem implementations. + +use std::path::Path; + +use crate::buffer::Buffer; +use crate::permissions::get_vsys; +use crate::utils::module::{export_default, ModuleInfo}; +use crate::utils::object::ObjectExt; + +use either::Either; +use rsquickjs::class::{Trace, Tracer}; +use rsquickjs::function::Opt; +use rsquickjs::prelude::{Async, Func}; +use rsquickjs::JsLifetime; +use rsquickjs::{ + module::{Declarations, Exports, ModuleDef}, + Class, Ctx, Error, Exception, FromJs, IntoJs, Object, Result, Value, +}; +use xmas_vsys::fs::{FileStat, FileType, OpenOptions}; + +// Re-export constants +pub const CONSTANT_F_OK: u32 = 0; +pub const CONSTANT_R_OK: u32 = 4; +pub const CONSTANT_W_OK: u32 = 2; +pub const CONSTANT_X_OK: u32 = 1; + +// ============================================================================ +// Helper macros and functions +// ============================================================================ + +/// Get vsys and check fs permission, return error if denied +fn check_permission<'js>(ctx: &Ctx<'js>, path: &Path) -> Result> { + let vsys = + get_vsys(ctx).ok_or_else(|| Exception::throw_message(ctx, "Vsys not initialized"))?; + + if !vsys.permissions().check_fs(path) { + return Err(Exception::throw_message( + ctx, + "Permission denied. Cannot access the file", + )); + } + + Ok(vsys) +} + +// ============================================================================ +// Stats class +// ============================================================================ + +#[derive(Clone)] +#[rsquickjs::class] +pub struct Stats { + inner: FileStat, +} + +impl<'js> Trace<'js> for Stats { + fn trace<'a>(&self, _: Tracer<'a, 'js>) {} +} + +unsafe impl<'js> JsLifetime<'js> for Stats { + type Changed<'to> = Stats; +} + +#[rsquickjs::methods] +impl Stats { + #[qjs(get)] + pub fn size(&self) -> u64 { + self.inner.size + } + + #[qjs(get)] + pub fn mode(&self) -> u32 { + self.inner.mode + } + + #[qjs(get)] + pub fn uid(&self) -> u32 { + self.inner.uid + } + + #[qjs(get)] + pub fn gid(&self) -> u32 { + self.inner.gid + } + + #[qjs(rename = "isFile")] + pub fn is_file(&self) -> bool { + self.inner.is_file() + } + + #[qjs(rename = "isDirectory")] + pub fn is_directory(&self) -> bool { + self.inner.is_dir() + } + + #[qjs(rename = "isSymbolicLink")] + pub fn is_symbolic_link(&self) -> bool { + self.inner.is_symlink() + } + + #[qjs(get)] + pub fn mtime(&self) -> Option { + self.inner.modified.map(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs_f64() * 1000.0) + .unwrap_or(0.0) + }) + } + + #[qjs(get)] + pub fn atime(&self) -> Option { + self.inner.accessed.map(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs_f64() * 1000.0) + .unwrap_or(0.0) + }) + } + + #[qjs(get)] + pub fn ctime(&self) -> Option { + self.inner.created.map(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs_f64() * 1000.0) + .unwrap_or(0.0) + }) + } + + #[qjs(get)] + pub fn birthtime(&self) -> Option { + self.ctime() + } +} + +// ============================================================================ +// Dirent class +// ============================================================================ + +#[derive(Clone)] +#[rsquickjs::class] +pub struct Dirent { + name: String, + file_type: FileType, +} + +impl<'js> Trace<'js> for Dirent { + fn trace<'a>(&self, _: Tracer<'a, 'js>) {} +} + +unsafe impl<'js> JsLifetime<'js> for Dirent { + type Changed<'to> = Dirent; +} + +#[rsquickjs::methods] +impl Dirent { + #[qjs(get)] + pub fn name(&self) -> &str { + &self.name + } + + #[qjs(rename = "isFile")] + pub fn is_file(&self) -> bool { + self.file_type == FileType::File + } + + #[qjs(rename = "isDirectory")] + pub fn is_directory(&self) -> bool { + self.file_type == FileType::Directory + } + + #[qjs(rename = "isSymbolicLink")] + pub fn is_symbolic_link(&self) -> bool { + self.file_type == FileType::Symlink + } +} + +// ============================================================================ +// FileHandle class +// ============================================================================ + +#[rsquickjs::class] +pub struct FileHandle { + handle: Option, + #[allow(dead_code)] + path: String, +} + +impl<'js> Trace<'js> for FileHandle { + fn trace<'a>(&self, _: Tracer<'a, 'js>) {} +} + +unsafe impl<'js> JsLifetime<'js> for FileHandle { + type Changed<'to> = FileHandle; +} + +#[rsquickjs::methods] +impl FileHandle { + pub async fn read<'js>(&mut self, ctx: Ctx<'js>, size: Opt) -> Result> { + let handle = self + .handle + .as_mut() + .ok_or_else(|| Exception::throw_message(&ctx, "File handle is closed"))?; + + let size = size.0.unwrap_or(4096); + let mut buf = vec![0u8; size]; + + let n = handle + .read(&mut buf) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + buf.truncate(n); + Buffer(buf).into_js(&ctx) + } + + pub async fn write<'js>(&mut self, ctx: Ctx<'js>, data: Value<'js>) -> Result { + let handle = self + .handle + .as_mut() + .ok_or_else(|| Exception::throw_message(&ctx, "File handle is closed"))?; + + let bytes = crate::utils::bytes::ObjectBytes::from(&ctx, &data)?; + let buf = bytes.as_bytes(&ctx)?; + + handle + .write(buf) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string())) + } + + pub async fn close(&mut self) -> Result<()> { + self.handle.take(); + Ok(()) + } + + pub fn stat<'js>(&self, ctx: Ctx<'js>) -> Result { + let handle = self + .handle + .as_ref() + .ok_or_else(|| Exception::throw_message(&ctx, "File handle is closed"))?; + + let stat = handle + .stat() + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(Stats { inner: stat }) + } +} + +// ============================================================================ +// Options structs +// ============================================================================ + +pub struct ReadFileOptions { + pub encoding: Option, +} + +impl<'js> FromJs<'js> for ReadFileOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value + .as_object() + .ok_or(Error::new_from_js(value.type_name(), "Object"))?; + let encoding = obj.get_optional::<_, String>("encoding")?; + Ok(Self { encoding }) + } +} + +pub struct WriteFileOptions { + pub mode: Option, +} + +impl<'js> FromJs<'js> for WriteFileOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value + .as_object() + .ok_or(Error::new_from_js(value.type_name(), "Object"))?; + let mode = obj.get_optional::<_, u32>("mode")?; + Ok(Self { mode }) + } +} + +pub struct MkdirOptions { + pub recursive: bool, + #[allow(dead_code)] + pub mode: Option, +} + +impl Default for MkdirOptions { + fn default() -> Self { + Self { + recursive: false, + mode: None, + } + } +} + +impl<'js> FromJs<'js> for MkdirOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value + .as_object() + .ok_or(Error::new_from_js(value.type_name(), "Object"))?; + let recursive = obj.get_optional::<_, bool>("recursive")?.unwrap_or(false); + let mode = obj.get_optional::<_, u32>("mode")?; + Ok(Self { recursive, mode }) + } +} + +pub struct ReaddirOptions { + pub with_file_types: bool, +} + +impl Default for ReaddirOptions { + fn default() -> Self { + Self { + with_file_types: false, + } + } +} + +impl<'js> FromJs<'js> for ReaddirOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value + .as_object() + .ok_or(Error::new_from_js(value.type_name(), "Object"))?; + let with_file_types = obj + .get_optional::<_, bool>("withFileTypes")? + .unwrap_or(false); + Ok(Self { with_file_types }) + } +} + +pub struct RmOptions { + pub recursive: bool, + pub force: bool, +} + +impl Default for RmOptions { + fn default() -> Self { + Self { + recursive: false, + force: false, + } + } +} + +impl<'js> FromJs<'js> for RmOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value + .as_object() + .ok_or(Error::new_from_js(value.type_name(), "Object"))?; + let recursive = obj.get_optional::<_, bool>("recursive")?.unwrap_or(false); + let force = obj.get_optional::<_, bool>("force")?.unwrap_or(false); + Ok(Self { recursive, force }) + } +} + +// ============================================================================ +// Async fs functions (for promises) +// ============================================================================ + +pub async fn access(ctx: Ctx<'_>, path: String, mode: Opt) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + let mode = mode.0.unwrap_or(CONSTANT_F_OK); + + (vsys.fs().access)(path_obj, mode).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub async fn read_file( + ctx: Ctx<'_>, + path: String, + options: Opt>, +) -> Result> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let bytes = + (vsys.fs().read)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + let buffer = Buffer(bytes); + + if let Some(opts) = options.0 { + let encoding = match opts { + Either::Left(enc) => Some(enc), + Either::Right(opts) => opts.encoding, + }; + if let Some(enc) = encoding { + return buffer.to_string(&ctx, &enc).and_then(|s| s.into_js(&ctx)); + } + } + + buffer.into_js(&ctx) +} + +pub async fn write_file<'js>( + ctx: Ctx<'js>, + path: String, + data: Value<'js>, + options: Opt>, +) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let bytes = crate::utils::bytes::ObjectBytes::from(&ctx, &data)?; + let buf = bytes.as_bytes(&ctx)?; + + (vsys.fs().write)(path_obj, buf).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + #[cfg(unix)] + if let Some(Either::Right(opts)) = options.0 { + if let Some(mode) = opts.mode { + (vsys.fs().set_mode)(path_obj, mode) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + } + } + #[cfg(not(unix))] + let _ = options; + + Ok(()) +} + +pub async fn rename(ctx: Ctx<'_>, old_path: String, new_path: String) -> Result<()> { + let old = Path::new(&old_path); + let new = Path::new(&new_path); + let vsys = check_permission(&ctx, old)?; + + (vsys.fs().rename)(old, new).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub async fn read_dir<'js>( + ctx: Ctx<'js>, + path: String, + options: Opt, +) -> Result> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let entries = (vsys.fs().read_dir)(path_obj) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + let with_file_types = options.0.map(|o| o.with_file_types).unwrap_or(false); + + if with_file_types { + let arr = rsquickjs::Array::new(ctx.clone())?; + for (i, entry) in entries.into_iter().enumerate() { + let dirent = Dirent { + name: entry.name, + file_type: entry.file_type, + }; + arr.set(i, Class::instance(ctx.clone(), dirent)?)?; + } + arr.into_js(&ctx) + } else { + let names: Vec = entries.into_iter().map(|e| e.name).collect(); + names.into_js(&ctx) + } +} + +pub async fn mkdir(ctx: Ctx<'_>, path: String, options: Opt) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + let opts = options.0.unwrap_or_default(); + + let result = if opts.recursive { + (vsys.fs().create_dir_all)(path_obj) + } else { + (vsys.fs().create_dir)(path_obj) + }; + + result.map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + #[cfg(unix)] + if let Some(mode) = opts.mode { + (vsys.fs().set_mode)(path_obj, mode) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + } + + Ok(()) +} + +pub async fn mkdtemp(ctx: Ctx<'_>, prefix: String) -> Result { + let vsys = + get_vsys(&ctx).ok_or_else(|| Exception::throw_message(&ctx, "Vsys not initialized"))?; + + let path = + (vsys.fs().mkdtemp)(&prefix).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(path.to_string_lossy().into_owned()) +} + +pub async fn rmfile(ctx: Ctx<'_>, path: String, options: Opt) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + let opts = options.0.unwrap_or_default(); + + let result = if opts.recursive { + (vsys.fs().remove_dir_all)(path_obj) + } else { + (vsys.fs().remove_file)(path_obj) + }; + + match result { + Ok(()) => Ok(()), + Err(_) if opts.force => Ok(()), // Ignore errors in force mode + Err(e) => Err(Exception::throw_message(&ctx, &e.to_string())), + } +} + +pub async fn rmdir(ctx: Ctx<'_>, path: String) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + (vsys.fs().remove_dir)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub async fn stat_fn(ctx: Ctx<'_>, path: String) -> Result { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let stat = + (vsys.fs().stat)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(Stats { inner: stat }) +} + +pub async fn lstat_fn(ctx: Ctx<'_>, path: String) -> Result { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let stat = + (vsys.fs().lstat)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(Stats { inner: stat }) +} + +pub async fn chmod(ctx: Ctx<'_>, path: String, mode: u32) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + (vsys.fs().set_mode)(path_obj, mode).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub async fn symlink(ctx: Ctx<'_>, target: String, path: String) -> Result<()> { + let target_obj = Path::new(&target); + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + (vsys.fs().symlink)(target_obj, path_obj) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub async fn open( + ctx: Ctx<'_>, + path: String, + flags: Opt, + mode: Opt, +) -> Result { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let flags = flags.0.unwrap_or_else(|| "r".to_string()); + let mut options = OpenOptions::new(); + + match flags.as_str() { + "r" => { + options = options.read(true); + } + "r+" => { + options = options.read(true).write(true); + } + "w" => { + options = options.write(true).create(true).truncate(true); + } + "w+" => { + options = options.read(true).write(true).create(true).truncate(true); + } + "a" => { + options = options.append(true).create(true); + } + "a+" => { + options = options.read(true).append(true).create(true); + } + "wx" | "xw" => { + options = options.write(true).create_new(true); + } + _ => { + options = options.read(true); + } + } + + if let Some(m) = mode.0 { + options = options.mode(m); + } + + let handle = (vsys.fs().open)(path_obj, &options) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(FileHandle { + handle: Some(handle), + path, + }) +} + +// ============================================================================ +// Sync fs functions +// ============================================================================ + +pub fn access_sync(ctx: Ctx<'_>, path: String, mode: Opt) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + let mode = mode.0.unwrap_or(CONSTANT_F_OK); + + (vsys.fs().access)(path_obj, mode).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub fn read_file_sync( + ctx: Ctx<'_>, + path: String, + options: Opt>, +) -> Result> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let bytes = + (vsys.fs().read)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + let buffer = Buffer(bytes); + + if let Some(opts) = options.0 { + let encoding = match opts { + Either::Left(enc) => Some(enc), + Either::Right(opts) => opts.encoding, + }; + if let Some(enc) = encoding { + return buffer.to_string(&ctx, &enc).and_then(|s| s.into_js(&ctx)); + } + } + + buffer.into_js(&ctx) +} + +pub fn write_file_sync<'js>( + ctx: Ctx<'js>, + path: String, + data: Value<'js>, + options: Opt>, +) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let bytes = crate::utils::bytes::ObjectBytes::from(&ctx, &data)?; + let buf = bytes.as_bytes(&ctx)?; + + (vsys.fs().write)(path_obj, buf).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + #[cfg(unix)] + if let Some(Either::Right(opts)) = options.0 { + if let Some(mode) = opts.mode { + (vsys.fs().set_mode)(path_obj, mode) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + } + } + #[cfg(not(unix))] + let _ = options; + + Ok(()) +} + +pub fn rename_sync(ctx: Ctx<'_>, old_path: String, new_path: String) -> Result<()> { + let old = Path::new(&old_path); + let new = Path::new(&new_path); + let vsys = check_permission(&ctx, old)?; + + (vsys.fs().rename)(old, new).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub fn read_dir_sync<'js>( + ctx: Ctx<'js>, + path: String, + options: Opt, +) -> Result> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let entries = (vsys.fs().read_dir)(path_obj) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + let with_file_types = options.0.map(|o| o.with_file_types).unwrap_or(false); + + if with_file_types { + let arr = rsquickjs::Array::new(ctx.clone())?; + for (i, entry) in entries.into_iter().enumerate() { + let dirent = Dirent { + name: entry.name, + file_type: entry.file_type, + }; + arr.set(i, Class::instance(ctx.clone(), dirent)?)?; + } + arr.into_js(&ctx) + } else { + let names: Vec = entries.into_iter().map(|e| e.name).collect(); + names.into_js(&ctx) + } +} + +pub fn mkdir_sync(ctx: Ctx<'_>, path: String, options: Opt) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + let opts = options.0.unwrap_or_default(); + + let result = if opts.recursive { + (vsys.fs().create_dir_all)(path_obj) + } else { + (vsys.fs().create_dir)(path_obj) + }; + + result.map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + #[cfg(unix)] + if let Some(mode) = opts.mode { + (vsys.fs().set_mode)(path_obj, mode) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + } + + Ok(()) +} + +pub fn mkdtemp_sync(ctx: Ctx<'_>, prefix: String) -> Result { + let vsys = + get_vsys(&ctx).ok_or_else(|| Exception::throw_message(&ctx, "Vsys not initialized"))?; + + let path = + (vsys.fs().mkdtemp)(&prefix).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(path.to_string_lossy().into_owned()) +} + +pub fn rmfile_sync(ctx: Ctx<'_>, path: String, options: Opt) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + let opts = options.0.unwrap_or_default(); + + let result = if opts.recursive { + (vsys.fs().remove_dir_all)(path_obj) + } else { + (vsys.fs().remove_file)(path_obj) + }; + + match result { + Ok(()) => Ok(()), + Err(_) if opts.force => Ok(()), + Err(e) => Err(Exception::throw_message(&ctx, &e.to_string())), + } +} + +pub fn rmdir_sync(ctx: Ctx<'_>, path: String) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + (vsys.fs().remove_dir)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub fn stat_fn_sync(ctx: Ctx<'_>, path: String) -> Result { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let stat = + (vsys.fs().stat)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(Stats { inner: stat }) +} + +pub fn lstat_fn_sync(ctx: Ctx<'_>, path: String) -> Result { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + let stat = + (vsys.fs().lstat)(path_obj).map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?; + + Ok(Stats { inner: stat }) +} + +pub fn chmod_sync(ctx: Ctx<'_>, path: String, mode: u32) -> Result<()> { + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + (vsys.fs().set_mode)(path_obj, mode).map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +pub fn symlink_sync(ctx: Ctx<'_>, target: String, path: String) -> Result<()> { + let target_obj = Path::new(&target); + let path_obj = Path::new(&path); + let vsys = check_permission(&ctx, path_obj)?; + + (vsys.fs().symlink)(target_obj, path_obj) + .map_err(|e| Exception::throw_message(&ctx, &e.to_string())) +} + +// ============================================================================ +// Module definitions +// ============================================================================ + +pub struct FsPromisesModule; + +impl ModuleDef for FsPromisesModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("access")?; + declare.declare("open")?; + declare.declare("readFile")?; + declare.declare("writeFile")?; + declare.declare("rename")?; + declare.declare("readdir")?; + declare.declare("mkdir")?; + declare.declare("mkdtemp")?; + declare.declare("rm")?; + declare.declare("rmdir")?; + declare.declare("stat")?; + declare.declare("lstat")?; + declare.declare("constants")?; + declare.declare("chmod")?; + declare.declare("symlink")?; + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + let globals = ctx.globals(); + Class::::define(&globals)?; + Class::::define(&globals)?; + Class::::define(&globals)?; + + export_default(ctx, exports, |default| { + export_promises(ctx, default)?; + Ok(()) + }) + } +} + +impl From for ModuleInfo { + fn from(val: FsPromisesModule) -> Self { + ModuleInfo { + name: "fs/promises", + module: val, + } + } +} + +pub struct FsModule; + +impl ModuleDef for FsModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("promises")?; + declare.declare("accessSync")?; + declare.declare("mkdirSync")?; + declare.declare("mkdtempSync")?; + declare.declare("readdirSync")?; + declare.declare("readFileSync")?; + declare.declare("rmdirSync")?; + declare.declare("rmSync")?; + declare.declare("statSync")?; + declare.declare("lstatSync")?; + declare.declare("writeFileSync")?; + declare.declare("constants")?; + declare.declare("chmodSync")?; + declare.declare("renameSync")?; + declare.declare("symlinkSync")?; + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + let globals = ctx.globals(); + Class::::define(&globals)?; + Class::::define(&globals)?; + Class::::define(&globals)?; + + export_default(ctx, exports, |default| { + let promises = Object::new(ctx.clone())?; + export_promises(ctx, &promises)?; + export_constants(ctx, default)?; + + default.set("promises", promises)?; + default.set("accessSync", Func::from(access_sync))?; + default.set("mkdirSync", Func::from(mkdir_sync))?; + default.set("mkdtempSync", Func::from(mkdtemp_sync))?; + default.set("readdirSync", Func::from(read_dir_sync))?; + default.set("readFileSync", Func::from(read_file_sync))?; + default.set("rmdirSync", Func::from(rmdir_sync))?; + default.set("rmSync", Func::from(rmfile_sync))?; + default.set("statSync", Func::from(stat_fn_sync))?; + default.set("lstatSync", Func::from(lstat_fn_sync))?; + default.set("writeFileSync", Func::from(write_file_sync))?; + default.set("chmodSync", Func::from(chmod_sync))?; + default.set("renameSync", Func::from(rename_sync))?; + default.set("symlinkSync", Func::from(symlink_sync))?; + Ok(()) + }) + } +} + +fn export_promises<'js>(ctx: &Ctx<'js>, exports: &Object<'js>) -> Result<()> { + export_constants(ctx, exports)?; + exports.set("access", Func::from(Async(access)))?; + exports.set("open", Func::from(Async(open)))?; + exports.set("readFile", Func::from(Async(read_file)))?; + exports.set("writeFile", Func::from(Async(write_file)))?; + exports.set("rename", Func::from(Async(rename)))?; + exports.set("readdir", Func::from(Async(read_dir)))?; + exports.set("mkdir", Func::from(Async(mkdir)))?; + exports.set("mkdtemp", Func::from(Async(mkdtemp)))?; + exports.set("rm", Func::from(Async(rmfile)))?; + exports.set("rmdir", Func::from(Async(rmdir)))?; + exports.set("stat", Func::from(Async(stat_fn)))?; + exports.set("lstat", Func::from(Async(lstat_fn)))?; + exports.set("chmod", Func::from(Async(chmod)))?; + exports.set("symlink", Func::from(Async(symlink)))?; + Ok(()) +} + +fn export_constants<'js>(ctx: &Ctx<'js>, exports: &Object<'js>) -> Result<()> { + let constants = Object::new(ctx.clone())?; + constants.set("F_OK", CONSTANT_F_OK)?; + constants.set("R_OK", CONSTANT_R_OK)?; + constants.set("W_OK", CONSTANT_W_OK)?; + constants.set("X_OK", CONSTANT_X_OK)?; + exports.set("constants", constants)?; + Ok(()) +} + +impl From for ModuleInfo { + fn from(val: FsModule) -> Self { + ModuleInfo { + name: "fs", + module: val, + } + } +} diff --git a/modules/src/fs/access.rs b/modules/src/fs/access.rs deleted file mode 100644 index b2a9d5d..0000000 --- a/modules/src/fs/access.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::{ - fs::Metadata, - path::{self, Path}, -}; - -#[allow(dead_code, unused_imports)] -use super::{CONSTANT_F_OK, CONSTANT_R_OK, CONSTANT_W_OK, CONSTANT_X_OK}; -use crate::{permissions, utils::result::ResultExt}; -use rsquickjs::{prelude::Opt, Ctx, Exception, Result}; -use tokio::fs; - -// if !check_could_ctx_access_permission(&ctx, &path) { -// return Err(Exception::throw_message( -// &ctx, -// "Permission denied. Cannot access the file", -// )); -// } -pub fn check_could_ctx_access_permission(ctx: &Ctx, path: &Path) -> bool { - let user_permissions = ctx.userdata::().unwrap(); - let file_permission = &user_permissions.fs; - let mut white_list = false; - let items = match file_permission { - permissions::BlackOrWhiteList::BlackList(items) => items, - permissions::BlackOrWhiteList::WhiteList(items) => { - white_list = true; - items - } - }; - - // split path to pattern path and normal path - let mut normal_paths = vec![]; - let mut pattern_paths = vec![]; - for item in items { - if item.contains('*') { - pattern_paths.push( - path::Path::new(&item[..item.len() - 1]) - .canonicalize() - .unwrap_or_else(|_| path::PathBuf::from(item)), - ); - } else { - normal_paths.push( - path::Path::new(item) - .canonicalize() - .unwrap_or_else(|_| path::PathBuf::from(item)), - ); - } - } - - let canonical_path = match path.canonicalize() { - Ok(p) => p, - Err(_) => return false, - }; - - if let Some(p) = canonical_path.as_path().parent() { - let found = pattern_paths.iter().find(|x| x == &p).is_some(); - if white_list { - return found; - } else { - return !found; - } - } - - let found = normal_paths - .iter() - .find(|x| *x == &canonical_path) - .is_some(); - if white_list { - return found; - } else { - return !found; - } -} - -pub async fn access(ctx: Ctx<'_>, path: String, mode: Opt) -> Result<()> { - let metadata = fs::metadata(&path).await.or_throw_msg( - &ctx, - &["No such file or directory \"", &path, "\""].concat(), - )?; - if !check_could_ctx_access_permission(&ctx, Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - verify_metadata(&ctx, mode, metadata) -} - -pub fn access_sync(ctx: Ctx<'_>, path: String, mode: Opt) -> Result<()> { - let metadata = std::fs::metadata(path.clone()).or_throw_msg( - &ctx, - &["No such file or directory \"", &path, "\""].concat(), - )?; - - if !check_could_ctx_access_permission(&ctx, Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - verify_metadata(&ctx, mode, metadata) -} - -fn verify_metadata(ctx: &Ctx, mode: Opt, metadata: Metadata) -> Result<()> { - let permissions = metadata.permissions(); - - let mode = mode.unwrap_or(CONSTANT_F_OK); - - if mode & CONSTANT_W_OK != 0 && permissions.readonly() { - return Err(Exception::throw_message( - ctx, - "Permission denied. File not writable", - )); - } - - if mode & CONSTANT_X_OK != 0 { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if permissions.mode() & 0o100 == 0 { - return Err(Exception::throw_message( - ctx, - "Permission denied. File not executable", - )); - } - } - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10; - // Get the file attributes - let file_attributes = metadata.file_attributes(); - - // Check if the file has execute permissions - if file_attributes & FILE_ATTRIBUTE_DIRECTORY == 0 { - return Err(Exception::throw_message(ctx, "Permission denied")); - } - } - } - - Ok(()) -} diff --git a/modules/src/fs/chmod.rs b/modules/src/fs/chmod.rs deleted file mode 100644 index fcd7835..0000000 --- a/modules/src/fs/chmod.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::fs::access::check_could_ctx_access_permission; -#[cfg(unix)] -use crate::utils::result::ResultExt; -use rsquickjs::{Ctx, Exception, Result}; -#[cfg(unix)] -use std::os::unix::prelude::PermissionsExt; -use std::path::Path; - -#[cfg(unix)] -pub(crate) fn chmod_error(path: &str) -> String { - ["Can't set permissions of \"", path, "\""].concat() -} - -pub(crate) async fn set_mode(ctx: Ctx<'_>, path: &str, mode: u32) -> Result<()> { - #[cfg(unix)] - { - tokio::fs::set_permissions(path, PermissionsExt::from_mode(mode)) - .await - .or_throw_msg(&ctx, &chmod_error(path))?; - } - #[cfg(not(unix))] - { - _ = ctx; - _ = path; - _ = mode; - } - Ok(()) -} - -pub(crate) fn set_mode_sync(ctx: Ctx<'_>, path: &str, mode: u32) -> Result<()> { - #[cfg(unix)] - { - std::fs::set_permissions(path, PermissionsExt::from_mode(mode)) - .or_throw_msg(&ctx, &chmod_error(path))?; - } - #[cfg(not(unix))] - { - _ = ctx; - _ = path; - _ = mode; - } - Ok(()) -} - -pub async fn chmod(ctx: Ctx<'_>, path: String, mode: u32) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - set_mode(ctx, &path, mode).await -} - -pub fn chmod_sync(ctx: Ctx<'_>, path: String, mode: u32) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - set_mode_sync(ctx, &path, mode) -} diff --git a/modules/src/fs/file_handle.rs b/modules/src/fs/file_handle.rs deleted file mode 100644 index 7f1465d..0000000 --- a/modules/src/fs/file_handle.rs +++ /dev/null @@ -1,916 +0,0 @@ -use std::borrow::Cow; -use std::path::PathBuf; - -use super::read_file; -use super::stats::Stats; -use crate::buffer::{ArrayBufferView, Buffer}; -use crate::utils::encoding::Encoder; -use crate::utils::{ - object::ObjectExt, - result::{OptionExt, ResultExt}, -}; -use either::Either; -use rsquickjs::function::Opt; -use rsquickjs::{Ctx, Error, Exception, FromJs, Null, Object, Result, Value}; -use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom}; - -const DEFAULT_BUFFER_SIZE: usize = 16384; -const DEFAULT_ENCODING: &str = "utf8"; - -#[allow(dead_code)] -#[rsquickjs::class] -#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] -pub struct FileHandle { - #[qjs(skip_trace)] - file: Option, - #[qjs(skip_trace)] - path: PathBuf, -} - -impl FileHandle { - pub fn new(file: File, path: PathBuf) -> Self { - Self { - file: Some(file), - path, - } - } - - fn file(&self, ctx: &Ctx<'_>) -> Result<&File> { - self.file.as_ref().or_throw_msg(ctx, "FileHandle is closed") - } - - fn file_mut(&mut self, ctx: &Ctx<'_>) -> Result<&mut File> { - self.file.as_mut().or_throw_msg(ctx, "FileHandle is closed") - } -} - -#[rsquickjs::methods(rename_all = "camelCase")] -impl FileHandle { - #[allow(unused_variables)] - async fn chmod(&self, ctx: Ctx<'_>, mode: u32) -> Result<()> { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let perm = std::fs::Permissions::from_mode(mode); - self.file(&ctx)? - .set_permissions(perm) - .await - .or_throw_msg(&ctx, "Can't modify file permissions")?; - } - Ok(()) - } - - #[allow(unused_variables)] - async fn chown(&self, ctx: Ctx<'_>, uid: u32, gid: u32) -> Result<()> { - #[cfg(unix)] - { - let path = self.path.clone(); - tokio::task::spawn_blocking(move || { - std::os::unix::fs::chown(&path, Some(uid), Some(gid)) - }) - .await - .or_throw(&ctx)? - .or_throw_msg(&ctx, "Can't modify file owner")?; - } - Ok(()) - } - - async fn close(&mut self) { - if let Some(file) = self.file.take() { - drop(file.into_std().await); - } - } - - async fn datasync(&self, ctx: Ctx<'_>) -> Result<()> { - self.file(&ctx)? - .sync_data() - .await - .or_throw_msg(&ctx, "Can't sync file data")?; - Ok(()) - } - - #[qjs(get)] - async fn fd(&self, ctx: Ctx<'_>) -> Result { - #[cfg(unix)] - { - use std::os::fd::AsRawFd; - Ok(self.file(&ctx)?.as_raw_fd()) - } - #[cfg(windows)] - { - use std::os::windows::io::AsRawHandle; - let handle = self.file(&ctx)?.as_raw_handle(); - Ok(handle as i32) - } - #[cfg(not(any(unix, windows)))] - { - Ok(0) - } - } - - async fn read<'js>( - &mut self, - ctx: Ctx<'js>, - buffer_or_options: Opt, ReadOptions<'js>>>, - options_or_offset: Opt, usize>>, - length: Opt, - position: Opt>, // -1 is not supported - ) -> Result> { - let options_1 = match buffer_or_options.0 { - Some(Either::Left(buffer)) => ReadOptions { - buffer: Some(buffer), - ..Default::default() - }, - Some(Either::Right(options)) => options, - None => ReadOptions::default(), - }; - let options_2 = match options_or_offset.0 { - Some(Either::Left(options)) => options, - Some(Either::Right(offset)) => ReadOptions { - offset: Some(offset), - ..Default::default() - }, - None => ReadOptions::default(), - }; - - let mut buffer = options_1 - .buffer - .or(options_2.buffer) - .unwrap_or_else_ok(|| { - ArrayBufferView::from_buffer(&ctx, Buffer::alloc(DEFAULT_BUFFER_SIZE)) - })?; - let offset = options_1.offset.or(options_2.offset).unwrap_or(0); - let length = options_1 - .length - .or(options_2.length) - .or(length.0) - .unwrap_or_else(|| buffer.len() - offset); - let position = options_1 - .position - .or(options_2.position) - .or(position.0.flatten()); - validate_length_offset(&ctx, length, offset, buffer.len())?; - - // It is not safe to pass the buffer from `ArrayBufferView` to `File::read` - // since the read is done in a different thread and we cannot garantee - // that multiple read calls are not done with the same buffer. - // Ideally, we should make our own version of `BufReader` to reuse the buffer - // instead of doing an allocation on each read. - let mut buf = vec![0u8; length]; - let file = self.file_mut(&ctx)?; - - // Tokio doesn't offer an API for positional reads. This means we have - // to seek to the position, read the file, and then seek back to the original - // position. See https://github.com/tokio-rs/tokio/issues/699 - let mut cursor = None; - if let Some(position) = position { - cursor = Some( - file.seek(SeekFrom::Current(0)) - .await - .or_throw_msg(&ctx, "Can't get cursor")?, - ); - file.seek(SeekFrom::Start(position)) - .await - .or_throw_msg(&ctx, "Can't seek file")?; - } - - let bytes_read = file - .read(&mut buf) - .await - .or_throw_msg(&ctx, "Failed to read file")?; - - // Reset the file at the original position. If there is an error while - // resetting the cursor, we close the file pre-emptively since future - // reads would be invalid. - if let Some(cursor) = cursor { - if let Err(err) = file - .seek(SeekFrom::Start(cursor)) - .await - .or_throw_msg(&ctx, "Failed to reset cursor") - { - self.close().await; - return Err(err); - } - } - - let dst_buf = buffer - .as_bytes_mut() - .or_throw_msg(&ctx, "Buffer is detached")?; - dst_buf[offset..].copy_from_slice(&buf); - - let result = Object::new(ctx)?; - result.set("bytesRead", bytes_read)?; - result.set("buffer", buffer)?; - Ok(result) - } - - async fn read_file<'js>( - &mut self, - ctx: Ctx<'js>, - options: Opt>, - ) -> Result> { - let size = self - .file(&ctx)? - .metadata() - .await - .map(|m| m.len() as usize) - .ok(); - let mut bytes = Vec::new(); - bytes - .try_reserve_exact(size.unwrap_or(0)) - .or_throw_msg(&ctx, "Out of memory")?; - - self.file_mut(&ctx)? - .read_to_end(&mut bytes) - .await - .or_throw_msg(&ctx, "Failed to read file")?; - read_file::handle_read_file_bytes(&ctx, options, bytes) - } - - async fn stat(&self, ctx: Ctx<'_>) -> Result { - let metadata = self - .file(&ctx)? - .metadata() - .await - .or_throw_msg(&ctx, "Can't stat file")?; - Ok(Stats::new(metadata)) - } - - async fn sync(&self, ctx: Ctx<'_>) -> Result<()> { - self.file(&ctx)? - .sync_all() - .await - .or_throw_msg(&ctx, "Can't sync file") - } - - async fn truncate(&mut self, ctx: Ctx<'_>, len: Opt) -> Result<()> { - let len = len.0.unwrap_or(0); - self.file_mut(&ctx)? - .set_len(len) - .await - .or_throw_msg(&ctx, "Can't truncate file") - } - - // Setting times not supported in tokio - // See https://github.com/tokio-rs/tokio/issues/6368 - // async fn utimes(&mut self, ctx: Ctx<'_>, atime: Value<'_>, mtime: Value<'_>) -> Result<()> - - async fn write<'js>( - &mut self, - ctx: Ctx<'js>, - buffer_or_string: Either, String>, - offset_or_options_or_position: Opt, WriteOptions>>, - length_or_encoding: Opt>, - position: Opt>, - ) -> Result> { - let mut options = match offset_or_options_or_position.0 { - Some(Either::Left(Either::Left(offset_or_position))) => { - if buffer_or_string.is_left() { - WriteOptions { - offset: Some(offset_or_position), - ..Default::default() - } - } else { - WriteOptions::default() - } - } - Some(Either::Right(options)) => options, - _ => WriteOptions::default(), - }; - if let Some(Either::Left(length)) = length_or_encoding.0 { - options.length = Some(length); - } - - let buffer = match &buffer_or_string { - Either::Left(buffer) => { - let buffer = buffer.as_bytes().or_throw_msg(&ctx, "Buffer is detached")?; - Cow::Borrowed(buffer) - } - Either::Right(string) => { - let encoding = length_or_encoding - .0 - .and_then(|e| e.right()) - .unwrap_or_else(|| DEFAULT_ENCODING.to_string()); - let buffer = Encoder::from_str(&encoding) - .and_then(|enc| enc.decode_from_string(string.clone())) - .or_throw(&ctx)?; - Cow::Owned(buffer) - } - }; - - let offset = options.offset.unwrap_or(0); - let length = options.length.unwrap_or(buffer.len() - offset); - let position = options.position.or(position.0.flatten()); - validate_length_offset(&ctx, length, offset, buffer.len())?; - - let file = self.file_mut(&ctx)?; - - // Tokio doesn't offer an API for positional writes. This means we have - // to seek to the position, write to the file, and then seek back to the original - // position. See https://github.com/tokio-rs/tokio/issues/699 - let mut cursor = None; - if let Some(position) = position { - cursor = Some( - file.seek(SeekFrom::Current(0)) - .await - .or_throw_msg(&ctx, "Can't get cursor")?, - ); - file.seek(SeekFrom::Start(position)) - .await - .or_throw_msg(&ctx, "Can't seek file")?; - } - - file.write_all(&buffer[offset..length]) - .await - .or_throw_msg(&ctx, "Failed to write to file")?; - - // Reset the file at the original position. If there is an error while - // resetting the cursor, we close the file pre-emptively since future - // writes would be invalid. - if let Some(cursor) = cursor { - if let Err(err) = file - .seek(SeekFrom::Start(cursor)) - .await - .or_throw_msg(&ctx, "Failed to reset cursor") - { - self.close().await; - return Err(err); - } - } - - let result = Object::new(ctx)?; - result.set("bytesWritten", length)?; - result.set("buffer", buffer_or_string)?; - Ok(result) - } - - async fn write_file<'js>( - &mut self, - ctx: Ctx<'js>, - data: Either, String>, - options_or_encoding: Opt>, - ) -> Result<()> { - let file = self.file_mut(&ctx)?; - - // Always overwrite the whole file - file.set_len(0) - .await - .or_throw_msg(&ctx, "Failed to truncate file")?; - - let encoding = match options_or_encoding.0 { - Some(Either::Left(options)) => options.encoding, - Some(Either::Right(encoding)) => Some(encoding), - _ => None, - } - .unwrap_or_else(|| DEFAULT_ENCODING.to_string()); - - let buffer = match &data { - Either::Left(buffer) => { - let buffer = buffer.as_bytes().or_throw_msg(&ctx, "Buffer is detached")?; - Cow::Borrowed(buffer) - } - Either::Right(string) => { - let buffer = Encoder::from_str(&encoding) - .and_then(|enc| enc.decode_from_string(string.clone())) - .or_throw(&ctx)?; - Cow::Owned(buffer) - } - }; - - file.write_all(&buffer) - .await - .or_throw_msg(&ctx, "Failed to write to file")?; - Ok(()) - } -} - -fn validate_length_offset( - ctx: &Ctx<'_>, - length: usize, - offset: usize, - buffer_length: usize, -) -> Result<()> { - if offset > buffer_length { - return Err(Exception::throw_range( - ctx, - &format!("offset ({}) <= {}", offset, buffer_length), - )); - } - if length > buffer_length - offset { - return Err(Exception::throw_range( - ctx, - &format!("length ({}) <= {}", length, buffer_length - offset), - )); - } - Ok(()) -} - -#[derive(Default)] -struct ReadOptions<'js> { - buffer: Option>, - offset: Option, - length: Option, - position: Option, -} - -impl<'js> FromJs<'js> for ReadOptions<'js> { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let buffer = obj.get_optional::<_, ArrayBufferView<'js>>("buffer")?; - let offset = obj.get_optional::<_, usize>("offset")?; - let length = obj.get_optional::<_, usize>("length")?; - let position = obj.get_optional::<_, u64>("position")?; - - Ok(Self { - buffer, - offset, - length, - position, - }) - } -} - -#[derive(Default)] -struct WriteOptions { - offset: Option, - length: Option, - position: Option, -} - -impl<'js> FromJs<'js> for WriteOptions { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let offset = obj.get_optional::<_, usize>("offset")?; - let length = obj.get_optional::<_, usize>("length")?; - let position = obj.get_optional::<_, u64>("position")?; - - Ok(Self { - offset, - length, - position, - }) - } -} - -#[derive(Default)] -struct WriteFileOptions { - encoding: Option, -} - -impl<'js> FromJs<'js> for WriteFileOptions { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let encoding = obj.get_optional::<_, String>("encoding")?; - - Ok(Self { encoding }) - } -} - -#[cfg(test)] -mod tests { - use crate::buffer; - use crate::utils::test::{call_test, call_test_err, test_async_with, ModuleEvaluator}; - use rsquickjs::{CatchResultExt, CaughtError}; - use tokio::fs::OpenOptions; - - use super::*; - - async fn given_file(content: &str, options: &mut OpenOptions) -> (File, PathBuf) { - // Create file - let path = crate::utils::test::given_file(content).await; - - // Open in right mode - let file = options.open(&path).await.unwrap(); - (file, path) - } - - #[tokio::test] - async fn test_file_handle_read() { - let (file, path) = given_file("Hello World", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const buffer = new ArrayBuffer(4096); - const view = new Uint8Array(buffer); - const read = await filehandle.read(view); - return Array.from(view); - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::, _>(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert!(result.starts_with(b"Hello World")); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_read_concurrent() { - let (file_a, path_a) = given_file(&"a".repeat(20000), OpenOptions::new().read(true)).await; - let (file_b, path_b) = given_file(&"b".repeat(20000), OpenOptions::new().read(true)).await; - let path_a_1 = path_a.clone(); - let path_b_1 = path_b.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandleA, filehandleB) { - const buffer = new ArrayBuffer(10000); - const view = new Uint8Array(buffer); - const read = await Promise.all([filehandleA.read(view), filehandleB.read(view)]); - return Array.from(view); - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::, _>(&ctx, &module, (FileHandle::new(file_a, path_a_1), FileHandle::new(file_b, path_b_1))).await; - - assert_eq!(result.len(), 10000); - if result.iter().all(|&b| b == b'a') { - println!("All a"); - } else if result.iter().all(|&b| b == b'b') { - println!("All b"); - } else { - println!("Mixed"); - } - }) - }) - .await; - - tokio::fs::remove_file(&path_a).await.unwrap(); - tokio::fs::remove_file(&path_b).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_read_position() { - let (file, path) = given_file("Hello World", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const buffer = new ArrayBuffer(4096); - const view = new Uint8Array(buffer); - await filehandle.read(view, { position: 6 }); - await filehandle.read(view, { offset: 5 }); - return Array.from(view); - } - "#, - ) - .await - .catch(&ctx) - .unwrap(); - - let result = - call_test::, _>(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert!(result.starts_with(b"WorldHello World")); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_read_subarray() { - let (file, path) = given_file("Hello World", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const buffer = new ArrayBuffer(4096); - const view = new Uint8Array(buffer); - const subarray = view.subarray(3, 8); - const read = await filehandle.read(subarray); - return Array.from(view); - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::, _>(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert!(result.starts_with(b"\x00\x00\x00Hello\x00")); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_read_buffer() { - let (file, path) = given_file("Hello World", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - buffer::init(&ctx).unwrap(); - - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const buffer = new ArrayBuffer(4096); - const view = new Uint8Array(buffer); - await filehandle.read(view, { length: 2000, offset: 3000 }); - } - "#, - ) - .await - .unwrap(); - - let error = call_test_err::<(), _>(&ctx, &module, (FileHandle::new(file, path_1),)) - .await - .unwrap_err(); - - let CaughtError::Exception(exception) = error else { - panic!("Expected exception"); - }; - - assert_eq!(exception.message().unwrap(), "length (2000) <= 1096"); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_read_out_of_range() { - let (file, path) = given_file("Hello World", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - buffer::init(&ctx).unwrap(); - - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const buffer = Buffer.alloc(4096); - const read = await filehandle.read(buffer); - return Array.from(buffer); - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::, _>(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert!(result.starts_with(b"Hello World")); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_read_file() { - let (file, path) = given_file("Hello World", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const data = await filehandle.readFile("utf8"); - return data; - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert_eq!(result, "Hello World"); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_handle_write() { - let (file, path) = given_file("", OpenOptions::new().write(true)).await; - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const { bytesWritten } = await filehandle.write("Hello World", null, "utf8"); - await filehandle.sync(); - return bytesWritten; - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert_eq!(result, 11); - }) - }) - .await; - - let file_content = tokio::fs::read(&path).await.unwrap(); - tokio::fs::remove_file(&path).await.unwrap(); - assert_eq!(file_content, b"Hello World"); - } - - #[tokio::test] - async fn test_file_handle_write_position() { - let (file, path) = given_file("", OpenOptions::new().write(true)).await; - let path_1 = path.clone(); - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - const { bytesWritten } = await filehandle.write("Hello World", null, "utf8", 4); - await filehandle.write("a", null, "utf8"); - await filehandle.sync(); - return bytesWritten; - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert_eq!(result, 11); - }) - }) - .await; - - let file_content = tokio::fs::read(&path).await.unwrap(); - tokio::fs::remove_file(&path).await.unwrap(); - assert_eq!(file_content, b"a\x00\x00\x00Hello World"); - } - - #[tokio::test] - async fn test_file_handle_write_out_of_range() { - let (file, path) = given_file("", OpenOptions::new().write(true)).await; - let path_1 = path.clone(); - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - await filehandle.write("Hello World", { offset: 5, length: 20 }); - } - "#, - ) - .await - .unwrap(); - - let error = call_test_err::<(), _>(&ctx, &module, (FileHandle::new(file, path_1),)) - .await - .unwrap_err(); - - let CaughtError::Exception(exception) = error else { - panic!("Expected exception"); - }; - - assert_eq!(exception.message().unwrap(), "length (20) <= 6"); - }) - }) - .await; - - let file_content = tokio::fs::read(&path).await.unwrap(); - tokio::fs::remove_file(&path).await.unwrap(); - assert_eq!(file_content, b""); - } - - #[tokio::test] - async fn test_file_handle_write_file() { - let (file, path) = given_file( - "Other very very very very long Data", - OpenOptions::new().write(true), - ) - .await; - let path_1 = path.clone(); - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - await filehandle.writeFile("Hello World", "utf8"); - await filehandle.sync(); - } - "#, - ) - .await - .unwrap(); - - call_test::<(), _>(&ctx, &module, (FileHandle::new(file, path_1),)).await; - }) - }) - .await; - - let file_content = tokio::fs::read(&path).await.unwrap(); - tokio::fs::remove_file(&path).await.unwrap(); - assert_eq!(file_content, b"Hello World"); - } - - #[tokio::test] - async fn test_file_handle_fd() { - let (file, path) = given_file("", OpenOptions::new().read(true)).await; - let path_1 = path.clone(); - test_async_with(|ctx| { - Box::pin(async move { - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - export async function test(filehandle) { - return filehandle.fd; - } - "#, - ) - .await - .unwrap(); - - let result = - call_test::(&ctx, &module, (FileHandle::new(file, path_1),)).await; - - assert!(result > 0); - }) - }) - .await; - - tokio::fs::remove_file(&path).await.unwrap(); - } -} diff --git a/modules/src/fs/mkdir.rs b/modules/src/fs/mkdir.rs deleted file mode 100644 index 680ba1d..0000000 --- a/modules/src/fs/mkdir.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::path::Path; - -use super::chmod::{set_mode, set_mode_sync}; - -use crate::utils::result::ResultExt; -use crate::{fs::access::check_could_ctx_access_permission, path::resolve_path}; -use ring::rand::{SecureRandom, SystemRandom}; -use rsquickjs::Exception; -use rsquickjs::{function::Opt, Ctx, Object, Result}; -use tokio::fs; - -pub async fn mkdir<'js>(ctx: Ctx<'js>, path: String, options: Opt>) -> Result { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let (recursive, mode, path) = get_params(&path, options)?; - - if recursive { - fs::create_dir_all(&path).await - } else { - fs::create_dir(&path).await - } - .or_throw_msg(&ctx, &["Can't create dir \"", &path, "\""].concat())?; - - set_mode(ctx, &path, mode).await?; - - Ok(path) -} - -pub fn mkdir_sync<'js>(ctx: Ctx<'js>, path: String, options: Opt>) -> Result { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let (recursive, mode, path) = get_params(&path, options)?; - - if recursive { - std::fs::create_dir_all(&path) - } else { - std::fs::create_dir(&path) - } - .or_throw_msg(&ctx, &["Can't create dir \"", &path, "\""].concat())?; - - set_mode_sync(ctx, &path, mode)?; - - Ok(path) -} - -fn get_params(path: &str, options: Opt) -> Result<(bool, u32, String)> { - let mut recursive = false; - let mut mode = 0o777; - - if let Some(options) = options.0 { - recursive = options.get("recursive").unwrap_or_default(); - mode = options.get("mode").unwrap_or(0o777); - } - let path = resolve_path([path])?; - Ok((recursive, mode, path)) -} - -const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - -fn random_chars(len: usize) -> String { - let random = SystemRandom::new(); - - let mut bytes = vec![0u8; len]; - random.fill(&mut bytes).unwrap(); - bytes - .iter() - .map(|&byte| { - let idx = (byte as usize) % CHARS.len(); - CHARS[idx] as char - }) - .collect::() -} - -pub async fn mkdtemp(ctx: Ctx<'_>, prefix: String) -> Result { - let path = [prefix.as_str(), random_chars(6).as_str()].join(","); - fs::create_dir_all(&path) - .await - .or_throw_msg(&ctx, &["Can't create dir \"", &path, "\""].concat())?; - Ok(path) -} - -pub fn mkdtemp_sync(ctx: Ctx<'_>, prefix: String) -> Result { - let path = [prefix.as_str(), random_chars(6).as_str()].join(","); - std::fs::create_dir_all(&path) - .or_throw_msg(&ctx, &["Can't create dir \"", &path, "\""].concat())?; - Ok(path) -} diff --git a/modules/src/fs/mod.rs b/modules/src/fs/mod.rs deleted file mode 100644 index 3db7dbd..0000000 --- a/modules/src/fs/mod.rs +++ /dev/null @@ -1,183 +0,0 @@ -pub mod access; -pub mod chmod; -pub mod file_handle; -pub mod mkdir; -pub mod open; -pub mod read_dir; -pub mod read_file; -pub mod rename; -pub mod rm; -pub mod stats; -pub mod symlink; -pub mod write_file; - -pub const CONSTANT_F_OK: u32 = 0; -pub const CONSTANT_R_OK: u32 = 4; -pub const CONSTANT_W_OK: u32 = 2; -pub const CONSTANT_X_OK: u32 = 1; - -pub struct FsPromisesModule; -use crate::utils::module::{export_default, ModuleInfo}; - -use self::access::{access, access_sync}; -use self::chmod::{chmod, chmod_sync}; -use self::file_handle::FileHandle; -use self::mkdir::{mkdir, mkdir_sync, mkdtemp, mkdtemp_sync}; -use self::open::open; -use self::read_dir::{read_dir, read_dir_sync, Dirent}; -use self::read_file::{read_file, read_file_sync}; -use self::rename::{rename, rename_sync}; -use self::rm::{rmdir, rmdir_sync, rmfile, rmfile_sync}; -use self::stats::{lstat_fn, lstat_fn_sync, stat_fn, stat_fn_sync, Stats}; -use self::symlink::{symlink, symlink_sync}; -use self::write_file::{write_file, write_file_sync}; -use rsquickjs::{ - module::{Declarations, Exports, ModuleDef}, - prelude::{Async, Func}, -}; -use rsquickjs::{Class, Ctx, Object, Result}; -impl ModuleDef for FsPromisesModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("access")?; - declare.declare("open")?; - declare.declare("readFile")?; - declare.declare("writeFile")?; - declare.declare("rename")?; - declare.declare("readdir")?; - declare.declare("mkdir")?; - declare.declare("mkdtemp")?; - declare.declare("rm")?; - declare.declare("rmdir")?; - declare.declare("stat")?; - declare.declare("lstat")?; - declare.declare("constants")?; - declare.declare("chmod")?; - declare.declare("symlink")?; - - declare.declare("default")?; - - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - let globals = ctx.globals(); - - Class::::define(&globals)?; - Class::::define(&globals)?; - Class::::define(&globals)?; - - export_default(ctx, exports, |default| { - export_promises(ctx, default)?; - - Ok(()) - }) - } -} - -impl From for ModuleInfo { - fn from(val: FsPromisesModule) -> Self { - ModuleInfo { - name: "fs/promises", - module: val, - } - } -} - -pub struct FsModule; - -impl ModuleDef for FsModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("promises")?; - declare.declare("accessSync")?; - declare.declare("mkdirSync")?; - declare.declare("mkdtempSync")?; - declare.declare("readdirSync")?; - declare.declare("readFileSync")?; - declare.declare("rmdirSync")?; - declare.declare("rmSync")?; - declare.declare("statSync")?; - declare.declare("lstatSync")?; - declare.declare("writeFileSync")?; - declare.declare("constants")?; - declare.declare("chmodSync")?; - declare.declare("renameSync")?; - declare.declare("symlinkSync")?; - - declare.declare("default")?; - - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - let globals = ctx.globals(); - - Class::::define(&globals)?; - Class::::define(&globals)?; - Class::::define(&globals)?; - - export_default(ctx, exports, |default| { - let promises = Object::new(ctx.clone())?; - export_promises(ctx, &promises)?; - export_constants(ctx, default)?; - - default.set("promises", promises)?; - default.set("accessSync", Func::from(access_sync))?; - default.set("mkdirSync", Func::from(mkdir_sync))?; - default.set("mkdtempSync", Func::from(mkdtemp_sync))?; - default.set("readdirSync", Func::from(read_dir_sync))?; - default.set("readFileSync", Func::from(read_file_sync))?; - default.set("rmdirSync", Func::from(rmdir_sync))?; - default.set("rmSync", Func::from(rmfile_sync))?; - default.set("statSync", Func::from(stat_fn_sync))?; - default.set("lstatSync", Func::from(lstat_fn_sync))?; - default.set("writeFileSync", Func::from(write_file_sync))?; - default.set("chmodSync", Func::from(chmod_sync))?; - default.set("renameSync", Func::from(rename_sync))?; - default.set("symlinkSync", Func::from(symlink_sync))?; - - Ok(()) - }) - } -} - -fn export_promises<'js>(ctx: &Ctx<'js>, exports: &Object<'js>) -> Result<()> { - export_constants(ctx, exports)?; - - exports.set("access", Func::from(Async(access)))?; - exports.set("open", Func::from(Async(open)))?; - exports.set("readFile", Func::from(Async(read_file)))?; - exports.set("writeFile", Func::from(Async(write_file)))?; - exports.set("rename", Func::from(Async(rename)))?; - exports.set("readdir", Func::from(Async(read_dir)))?; - exports.set("mkdir", Func::from(Async(mkdir)))?; - exports.set("mkdtemp", Func::from(Async(mkdtemp)))?; - exports.set("rm", Func::from(Async(rmfile)))?; - exports.set("rmdir", Func::from(Async(rmdir)))?; - exports.set("stat", Func::from(Async(stat_fn)))?; - exports.set("lstat", Func::from(Async(lstat_fn)))?; - exports.set("chmod", Func::from(Async(chmod)))?; - exports.set("symlink", Func::from(Async(symlink)))?; - - Ok(()) -} - -fn export_constants<'js>(ctx: &Ctx<'js>, exports: &Object<'js>) -> Result<()> { - let constants = Object::new(ctx.clone())?; - constants.set("F_OK", CONSTANT_F_OK)?; - constants.set("R_OK", CONSTANT_R_OK)?; - constants.set("W_OK", CONSTANT_W_OK)?; - constants.set("X_OK", CONSTANT_X_OK)?; - - exports.set("constants", constants)?; - - Ok(()) -} - -impl From for ModuleInfo { - fn from(val: FsModule) -> Self { - ModuleInfo { - name: "fs", - module: val, - } - } -} diff --git a/modules/src/fs/open.rs b/modules/src/fs/open.rs deleted file mode 100644 index 198ab8e..0000000 --- a/modules/src/fs/open.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::path::{Path, PathBuf}; - -use crate::{fs::access::check_could_ctx_access_permission, utils::result::ResultExt}; -use rsquickjs::{function::Opt, Ctx, Exception, Result}; -use tokio::fs::OpenOptions; - -use super::file_handle::FileHandle; - -pub async fn open( - ctx: Ctx<'_>, - path: String, - flags: Opt, - mode: Opt, -) -> Result { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let mut options = OpenOptions::new(); - match flags.0.as_deref().unwrap_or("r") { - // We are not supporting the sync modes - "a" => options.append(true).create(true), - "ax" => options.append(true).create_new(true), - "a+" => options.append(true).read(true), - "r" => options.read(true), - "r+" => options.read(true).write(true), - "w" => options.write(true).create(true).truncate(true), - "wx" => options.write(true).create_new(true), - "w+" => options.write(true).read(true).create(true).truncate(true), - "wx+" => options.write(true).read(true).create_new(true), - flags => { - return Err(Exception::throw_message( - &ctx, - &["Invalid flags '", flags, "'"].concat(), - )) - } - }; - #[cfg(unix)] - { - let mode = mode.0.unwrap_or(0o666); - options.mode(mode); - } - #[cfg(not(unix))] - { - _ = mode; - } - - let path = PathBuf::from(path); - let file = options - .open(&path) - .await - .or_throw_msg(&ctx, "Cannot open file")?; - - Ok(FileHandle::new(file, path)) -} - -#[cfg(test)] -mod tests { - use crate::buffer; - use crate::utils::test::{call_test, given_file, test_async_with, ModuleEvaluator}; - - use crate::fs::FsPromisesModule; - - #[tokio::test] - async fn test_file_handle_read() { - let path = given_file("Hello World") - .await - .to_string_lossy() - .to_string(); - let path_1 = path.clone(); - - test_async_with(|ctx| { - Box::pin(async move { - buffer::init(&ctx).unwrap(); - ModuleEvaluator::eval_rust::(ctx.clone(), "fs/promises") - .await - .unwrap(); - - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - import { open } from 'fs/promises'; - - export async function test(path) { - let filehandle = null; - try { - filehandle = await open(path, 'r+'); - let { buffer } = await filehandle.read(); - return Array.from(buffer); - } finally { - await filehandle?.close(); - } - } - "#, - ) - .await - .unwrap(); - - let result = call_test::, _>(&ctx, &module, (path_1,)).await; - - assert!(result.starts_with(b"Hello World")); - }) - }) - .await; - - tokio::fs::remove_file(path).await.unwrap(); - } -} diff --git a/modules/src/fs/read_dir.rs b/modules/src/fs/read_dir.rs deleted file mode 100644 index 215d007..0000000 --- a/modules/src/fs/read_dir.rs +++ /dev/null @@ -1,208 +0,0 @@ -#[cfg(unix)] -use std::os::unix::fs::FileTypeExt; -use std::{fs::Metadata, path::PathBuf}; - -use crate::path::{ends_with_sep, CURRENT_DIR_STR}; -use crate::utils::fs::DirectoryWalker; -use rsquickjs::{ - atom::PredefinedAtom, prelude::Opt, Array, Class, Ctx, IntoJs, Object, Result, Value, -}; - -#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] -#[rsquickjs::class] -pub struct Dirent { - #[qjs(skip_trace)] - metadata: Metadata, -} - -#[rsquickjs::methods(rename_all = "camelCase")] -impl Dirent { - pub fn is_file(&self) -> bool { - self.metadata.is_file() - } - pub fn is_directory(&self) -> bool { - self.metadata.is_dir() - } - - pub fn is_symbolic_link(&self) -> bool { - self.metadata.is_symlink() - } - - #[qjs(rename = "isFIFO")] - pub fn is_fifo(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_fifo() - } - #[cfg(not(unix))] - { - false - } - } - - pub fn is_block_device(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_block_device() - } - #[cfg(not(unix))] - { - false - } - } - - pub fn is_character_device(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_char_device() - } - #[cfg(not(unix))] - { - false - } - } - - pub fn is_socket(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_socket() - } - #[cfg(not(unix))] - { - false - } - } -} - -struct ReadDirItem { - name: String, - metadata: Option, -} - -pub struct ReadDir { - items: Vec, - root: String, -} - -impl<'js> IntoJs<'js> for ReadDir { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - let arr = Array::new(ctx.clone())?; - for (index, item) in self.items.into_iter().enumerate() { - if let Some(metadata) = item.metadata { - let dirent = Dirent { metadata }; - - let dirent = Class::instance(ctx.clone(), dirent)?; - dirent.set(PredefinedAtom::Name, item.name)?; - dirent.set("parentPath", &self.root)?; - arr.set(index, dirent)?; - } else { - arr.set(index, item.name)?; - } - } - arr.into_js(ctx) - } -} - -pub async fn read_dir(mut path: String, options: Opt>) -> Result { - let (with_file_types, skip_root_pos, mut directory_walker) = - process_options_and_create_directory_walker(&mut path, options); - - let mut items = Vec::with_capacity(64); - - while let Some((child, metadata)) = directory_walker.walk().await? { - append_directory_and_metadata_to_vec( - with_file_types, - skip_root_pos, - &mut items, - child, - metadata, - ); - } - - items.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); - - Ok(ReadDir { items, root: path }) -} - -pub fn read_dir_sync(mut path: String, options: Opt>) -> Result { - let (with_file_types, skip_root_pos, mut directory_walker) = - process_options_and_create_directory_walker(&mut path, options); - - let mut items = Vec::with_capacity(64); - while let Some((child, metadata)) = directory_walker.walk_sync()? { - append_directory_and_metadata_to_vec( - with_file_types, - skip_root_pos, - &mut items, - child, - metadata, - ); - } - - items.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); - - Ok(ReadDir { items, root: path }) -} - -type OptionsAndDirectoryWalker = (bool, usize, DirectoryWalker bool>); - -fn process_options_and_create_directory_walker( - path: &mut String, - options: Opt, -) -> OptionsAndDirectoryWalker { - let mut with_file_types = false; - let mut is_recursive = false; - - if let Some(options) = options.0 { - with_file_types = options - .get("withFileTypes") - .ok() - .and_then(|file_types: Value| file_types.as_bool()) - .unwrap_or_default(); - - is_recursive = options - .get("recursive") - .ok() - .and_then(|recursive: Value| recursive.as_bool()) - .unwrap_or_default(); - }; - - if ends_with_sep(path) { - path.pop(); - } - - let skip_root_pos = { - match path.as_str() { - // . | ./ - "." | CURRENT_DIR_STR => path.len(), - // path - _ => path.len() + 1, - } - }; - - let mut directory_walker: DirectoryWalker bool> = - DirectoryWalker::new(PathBuf::from(&path), |_| true); - - if is_recursive { - directory_walker.set_recursive(true); - } - (with_file_types, skip_root_pos, directory_walker) -} - -fn append_directory_and_metadata_to_vec( - with_file_types: bool, - skip_root_pos: usize, - items: &mut Vec, - child: PathBuf, - metadata: Metadata, -) { - let metadata = if with_file_types { - Some(metadata) - } else { - None - }; - - let name = child.into_os_string().to_string_lossy()[skip_root_pos..].to_string(); - - items.push(ReadDirItem { name, metadata }) -} diff --git a/modules/src/fs/read_file.rs b/modules/src/fs/read_file.rs deleted file mode 100644 index 6dc49a7..0000000 --- a/modules/src/fs/read_file.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::buffer::Buffer; -use crate::fs::access::check_could_ctx_access_permission; -use crate::utils::{object::ObjectExt, result::ResultExt}; -use either::Either; -use rsquickjs::Exception; -use rsquickjs::{function::Opt, Ctx, Error, FromJs, IntoJs, Result, Value}; -use std::path::Path; -use tokio::fs; - -pub async fn read_file( - ctx: Ctx<'_>, - path: String, - options: Opt>, -) -> Result> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let bytes = fs::read(&path) - .await - .or_throw_msg(&ctx, &["Can't read \"", &path, "\""].concat())?; - - handle_read_file_bytes(&ctx, options, bytes) -} - -pub fn read_file_sync( - ctx: Ctx<'_>, - path: String, - options: Opt>, -) -> Result> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let bytes = - std::fs::read(&path).or_throw_msg(&ctx, &["Can't read \"", &path, "\""].concat())?; - - handle_read_file_bytes(&ctx, options, bytes) -} - -pub fn handle_read_file_bytes<'a>( - ctx: &Ctx<'a>, - options: Opt>, - bytes: Vec, -) -> Result> { - let buffer = Buffer(bytes); - - if let Some(options) = options.0 { - let encoding = match options { - Either::Left(encoding) => Some(encoding), - Either::Right(options) => options.encoding, - }; - - if let Some(encoding) = encoding { - return buffer - .to_string(ctx, &encoding) - .and_then(|s| s.into_js(ctx)); - } - } - - buffer.into_js(ctx) -} - -pub struct ReadFileOptions { - pub encoding: Option, -} - -impl<'js> FromJs<'js> for ReadFileOptions { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let encoding = obj.get_optional::<_, String>("encoding")?; - - Ok(Self { encoding }) - } -} diff --git a/modules/src/fs/rename.rs b/modules/src/fs/rename.rs deleted file mode 100644 index da9e076..0000000 --- a/modules/src/fs/rename.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::path::Path; - -use crate::{fs::access::check_could_ctx_access_permission, utils::result::ResultExt}; -use rsquickjs::{Ctx, Exception, Result}; - -pub(crate) fn rename_error(from: &str, to: &str) -> String { - [ - "Can't rename file/folder from \"", - from, - "\" to \"", - to, - "\"", - ] - .concat() -} - -pub async fn rename(ctx: Ctx<'_>, old_path: String, new_path: String) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&old_path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - tokio::fs::rename(&old_path, &new_path) - .await - .or_throw_msg(&ctx, &rename_error(&old_path, &new_path))?; - Ok(()) -} - -pub fn rename_sync(ctx: Ctx<'_>, old_path: String, new_path: String) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&old_path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - std::fs::rename(&old_path, &new_path) - .or_throw_msg(&ctx, &rename_error(&old_path, &new_path))?; - Ok(()) -} diff --git a/modules/src/fs/rm.rs b/modules/src/fs/rm.rs deleted file mode 100644 index d3c8a6d..0000000 --- a/modules/src/fs/rm.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::path::Path; - -use crate::{fs::access::check_could_ctx_access_permission, utils::result::ResultExt}; -use rsquickjs::{function::Opt, Ctx, Exception, Object, Result}; -use tokio::fs; - -#[allow(clippy::manual_async_fn)] -pub async fn rmdir<'js>(ctx: Ctx<'js>, path: String, options: Opt>) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let recursive = get_params_rm_dir(options); - - if recursive { - fs::remove_dir_all(&path).await - } else { - fs::remove_dir(&path).await - } - .or_throw_msg(&ctx, &["Can't remove dir \"", &path, "\""].concat())?; - - Ok(()) -} - -#[allow(clippy::manual_async_fn)] -pub fn rmdir_sync<'js>(ctx: Ctx<'js>, path: String, options: Opt>) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let recursive = get_params_rm_dir(options); - - if recursive { - std::fs::remove_dir_all(&path) - } else { - std::fs::remove_dir(&path) - } - .or_throw_msg(&ctx, &["Can't remove dir \"", &path, "\""].concat())?; - - Ok(()) -} - -pub async fn rmfile<'js>(ctx: Ctx<'js>, path: String, options: Opt>) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let (recursive, force) = get_params_rm(options); - - let res = async move { - let is_dir = fs::metadata(&path) - .await - .map(|metadata| metadata.is_dir()) - .or_throw(&ctx)?; - - (if is_dir && recursive { - fs::remove_dir_all(&path).await - } else if is_dir && !recursive { - fs::remove_dir(&path).await - } else { - fs::remove_file(&path).await - }) - .or_throw_msg(&ctx, &["Can't remove file \"", &path, "\""].concat())?; - - Ok(()) - } - .await; - - if !force { - return res; - } - - Ok(()) -} - -pub fn rmfile_sync<'js>(ctx: Ctx<'js>, path: String, options: Opt>) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let (recursive, force) = get_params_rm(options); - - let res = (|| -> Result<()> { - let is_dir = std::fs::metadata(&path).map(|metadata| metadata.is_dir())?; - - (if is_dir && recursive { - std::fs::remove_dir_all(&path) - } else if is_dir && !recursive { - std::fs::remove_dir(&path) - } else { - std::fs::remove_file(&path) - })?; - - Ok(()) - })(); - - if !force { - return res; - } - - Ok(()) -} - -fn get_params_rm_dir(options: Opt) -> bool { - let mut recursive = false; - - if let Some(options) = options.0 { - recursive = options.get("recursive").unwrap_or_default(); - } - recursive -} - -fn get_params_rm(options: Opt) -> (bool, bool) { - let mut recursive = false; - let mut force = false; - - if let Some(options) = options.0 { - recursive = options.get("recursive").unwrap_or_default(); - force = options.get("force").unwrap_or_default(); - } - (recursive, force) -} diff --git a/modules/src/fs/stats.rs b/modules/src/fs/stats.rs deleted file mode 100644 index 7b192b6..0000000 --- a/modules/src/fs/stats.rs +++ /dev/null @@ -1,359 +0,0 @@ -#[cfg(unix)] -use std::os::unix::fs::FileTypeExt; -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; -#[allow(unused_imports)] -use std::{ - fs::Metadata, - time::{Duration, SystemTime}, -}; - -use crate::utils::result::ResultExt; -use rsquickjs::{Ctx, Result}; -use tokio::fs; - -// The Stats implementation is very much based on Unix. The Windows implementation -// tries its best to mimic the implementation of libuv since it is the standard. -// See: https://github.com/libuv/libuv/blob/90648ea3e55125a5a819b32106da6462da310da6/src/win/fs.c -// -// By comparison, the Deno implementation is very basic and doesn't even try much. -// See: https://github.com/denoland/deno/blob/c9da27e147d0681724dd647593abbaa46417feb7/ext/io/fs.rs#L114-L182 -// -// This implementation doesn't handle files created before UNIX_EPOCH. - -#[rsquickjs::class] -#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] -pub struct Stats { - #[qjs(skip_trace)] - metadata: Metadata, -} - -#[rsquickjs::methods(rename_all = "camelCase")] -impl Stats { - #[qjs(skip)] - pub fn new(metadata: Metadata) -> Self { - Self { metadata } - } - - #[qjs(get, enumerable)] - pub fn dev(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.dev() - } - #[cfg(not(unix))] - { - // Unstable feature, see https://github.com/rust-lang/rust/issues/63010 - 0 - } - } - - #[qjs(get, enumerable)] - pub fn ino(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.ino() - } - #[cfg(not(unix))] - { - // Unstable feature, see https://github.com/rust-lang/rust/issues/63010 - 0 - } - } - - #[qjs(get, enumerable)] - pub fn mode(&self) -> u32 { - #[cfg(unix)] - { - self.metadata.mode() - } - #[cfg(not(unix))] - { - 0o666 - } - } - - #[qjs(get, enumerable)] - pub fn nlink(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.nlink() - } - #[cfg(not(unix))] - { - // Unstable feature, see https://github.com/rust-lang/rust/issues/63010 - 1 - } - } - - #[qjs(get, enumerable)] - pub fn uid(&self) -> u32 { - #[cfg(unix)] - { - self.metadata.uid() - } - #[cfg(not(unix))] - { - 0 - } - } - - #[qjs(get, enumerable)] - pub fn gid(&self) -> u32 { - #[cfg(unix)] - { - self.metadata.gid() - } - #[cfg(not(unix))] - { - 0 - } - } - - #[qjs(get, enumerable)] - pub fn rdev(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.rdev() - } - #[cfg(not(unix))] - { - 0 - } - } - - #[qjs(get, enumerable)] - pub fn size(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.size() - } - #[cfg(windows)] - { - if self.metadata.is_dir() { - 0 - } else { - self.metadata.file_size() - } - } - #[cfg(not(any(unix, windows)))] - { - 0 - } - } - - #[qjs(get, enumerable)] - pub fn blksize(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.blksize() - } - #[cfg(not(unix))] - { - 4096 - } - } - - #[qjs(get, enumerable)] - pub fn blocks(&self) -> u64 { - #[cfg(unix)] - { - self.metadata.blocks() - } - #[cfg(not(unix))] - { - 0 - } - } - - #[qjs(get, enumerable)] - pub fn atime_ms(&self, ctx: Ctx<'_>) -> Result { - #[cfg(unix)] - { - _ = ctx; - Ok(self.metadata.atime_nsec() as u64 / 1e6 as u64) - } - #[cfg(not(unix))] - { - self.metadata.accessed().map(to_msec).or_throw(&ctx) - } - } - - #[qjs(get, enumerable)] - pub fn mtime_ms(&self, ctx: Ctx<'_>) -> Result { - #[cfg(unix)] - { - _ = ctx; - Ok(self.metadata.mtime_nsec() as u64 / 1e6 as u64) - } - #[cfg(not(unix))] - { - self.metadata.modified().map(to_msec).or_throw(&ctx) - } - } - - #[qjs(get, enumerable)] - pub fn ctime_ms(&self, ctx: Ctx<'_>) -> Result { - #[cfg(unix)] - { - _ = ctx; - Ok(self.metadata.ctime_nsec() as u64 / 1e6 as u64) - } - #[cfg(not(unix))] - { - self.metadata.modified().map(to_msec).or_throw(&ctx) - } - } - - #[qjs(get, enumerable)] - pub fn birthtime_ms(&self, ctx: Ctx<'_>) -> Result { - self.metadata - .created() - .or_throw(&ctx) - .and_then(|c| c.elapsed().or_throw(&ctx)) - .map(|d| d.as_millis() as u64) - } - - #[qjs(get, enumerable)] - pub fn atime(&self, ctx: Ctx<'_>) -> Result { - self.metadata.accessed().or_throw(&ctx) - } - - #[qjs(get, enumerable)] - pub fn mtime(&self, ctx: Ctx<'_>) -> Result { - self.metadata.modified().or_throw(&ctx) - } - - #[qjs(get, enumerable)] - pub fn ctime(&self, ctx: Ctx<'_>) -> Result { - #[cfg(unix)] - { - _ = ctx; - Ok(SystemTime::UNIX_EPOCH + Duration::from_nanos(self.metadata.ctime_nsec() as u64)) - } - #[cfg(not(unix))] - { - self.metadata.modified().or_throw(&ctx) - } - } - - #[qjs(get, enumerable)] - pub fn birthtime(&self, ctx: Ctx<'_>) -> Result { - self.metadata.created().or_throw(&ctx) - } - - pub fn is_file(&self) -> bool { - self.metadata.is_file() - } - - /// @deprecated Use `is_directory` instead - pub fn is_dir(&self) -> bool { - self.metadata.is_dir() - } - - pub fn is_directory(&self) -> bool { - self.metadata.is_dir() - } - - /// @deprecated Use `is_symbolic_link` instead - pub fn is_symlink(&self) -> bool { - self.metadata.is_symlink() - } - - pub fn is_symbolic_link(&self) -> bool { - self.metadata.is_symlink() - } - - #[qjs(rename = "isFIFO")] - pub fn is_fifo(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_fifo() - } - #[cfg(not(unix))] - { - false - } - } - - pub fn is_block_device(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_block_device() - } - #[cfg(not(unix))] - { - false - } - } - - pub fn is_character_device(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_char_device() - } - #[cfg(not(unix))] - { - false - } - } - - pub fn is_socket(&self) -> bool { - #[cfg(unix)] - { - self.metadata.file_type().is_socket() - } - #[cfg(not(unix))] - { - false - } - } -} - -pub async fn stat_fn(ctx: Ctx<'_>, path: String) -> Result { - let metadata = fs::metadata(&path) - .await - .or_throw_msg(&ctx, &["Can't stat \"", &path, "\""].concat())?; - - let stats = Stats::new(metadata); - - Ok(stats) -} - -pub fn stat_fn_sync(ctx: Ctx<'_>, path: String) -> Result { - let metadata = - std::fs::metadata(&path).or_throw_msg(&ctx, &["Can't stat \"", &path, "\""].concat())?; - - let stats = Stats::new(metadata); - - Ok(stats) -} - -pub async fn lstat_fn(ctx: Ctx<'_>, path: String) -> Result { - let metadata = fs::symlink_metadata(&path) - .await - .or_throw_msg(&ctx, &["Can't lstat \"", &path, "\""].concat())?; - - let stats = Stats::new(metadata); - - Ok(stats) -} - -pub fn lstat_fn_sync(ctx: Ctx<'_>, path: String) -> Result { - let metadata = std::fs::symlink_metadata(&path) - .or_throw_msg(&ctx, &["Can't lstat \"", &path, "\""].concat())?; - - let stats = Stats::new(metadata); - - Ok(stats) -} - -#[allow(dead_code)] -#[inline(always)] -fn to_msec(time: SystemTime) -> u64 { - time.duration_since(SystemTime::UNIX_EPOCH) - .map(|t| t.as_millis() as u64) - .unwrap_or_else(|err| err.duration().as_millis() as u64) -} diff --git a/modules/src/fs/symlink.rs b/modules/src/fs/symlink.rs deleted file mode 100644 index ed4e19a..0000000 --- a/modules/src/fs/symlink.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{io, path::Path}; - -use crate::{fs::access::check_could_ctx_access_permission, utils::result::ResultExt}; -use rsquickjs::{function::Opt, Ctx, Exception, Result}; - -fn symlink_blocking(target: &str, path: &str, type_value: Option) -> io::Result<()> { - #[cfg(unix)] - { - _ = type_value; - std::os::unix::fs::symlink(target, path) - } - #[cfg(windows)] - { - let type_str = match type_value.as_deref() { - Some(t @ ("file" | "dir" | "junction")) => t, - _ => { - if std::fs::metadata(target) - .map(|m| m.is_dir()) - .unwrap_or(false) - { - "dir" - } else { - "file" - } - } - }; - match type_str { - "junction" => junction::create(target, path), - "dir" => std::os::windows::fs::symlink_dir(target, path), - _ => std::os::windows::fs::symlink_file(target, path), - } - } -} - -pub async fn symlink<'js>( - ctx: Ctx<'js>, - target: String, - path: String, - type_value: Opt, -) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let path_clone = path.clone(); - - tokio::task::spawn_blocking(move || symlink_blocking(&target, &path_clone, type_value.0)) - .await - .map_err(io::Error::other)? - .or_throw_msg(&ctx, &["Can't create symlink \"", &path, "\""].concat()) -} - -pub fn symlink_sync<'js>( - ctx: Ctx<'js>, - target: String, - path: String, - type_value: Opt, -) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - symlink_blocking(&target, &path, type_value.0) - .or_throw_msg(&ctx, &["Can't create symlink \"", &path, "\""].concat()) -} diff --git a/modules/src/fs/write_file.rs b/modules/src/fs/write_file.rs deleted file mode 100644 index abeabf7..0000000 --- a/modules/src/fs/write_file.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::path::Path; - -use crate::utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; -use either::Either; -use rsquickjs::Exception; -use rsquickjs::{function::Opt, Ctx, Error, FromJs, Result, Value}; -use tokio::fs; -use tokio::io::AsyncWriteExt; - -use crate::fs::access::check_could_ctx_access_permission; - -pub async fn write_file<'js>( - ctx: Ctx<'js>, - path: String, - data: Value<'js>, - options: Opt>, -) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let write_error_message = &["Can't write file \"", &path, "\""].concat(); - - let mut file = fs::File::create(&path) - .await - .or_throw_msg(&ctx, write_error_message)?; - - #[cfg(unix)] - if let Some(Either::Right(opts)) = options.0 { - use std::os::unix::fs::PermissionsExt; - - let perm = PermissionsExt::from_mode(opts.mode.unwrap_or(0o666)); - file.set_permissions(perm) - .await - .or_throw_msg(&ctx, write_error_message)?; - } - #[cfg(not(unix))] - { - _ = options; - if let Some(Either::Right(opts)) = options.0 { - _ = opts.mode; - } - } - - let bytes = ObjectBytes::from(&ctx, &data)?; - file.write_all(bytes.as_bytes(&ctx)?) - .await - .or_throw_msg(&ctx, write_error_message)?; - file.flush().await.or_throw_msg(&ctx, write_error_message)?; - - Ok(()) -} - -pub fn write_file_sync<'js>( - ctx: Ctx<'js>, - path: String, - bytes: ObjectBytes<'js>, - options: Opt>, -) -> Result<()> { - if !check_could_ctx_access_permission(&ctx, &Path::new(&path)) { - return Err(Exception::throw_message( - &ctx, - "Permission denied. Cannot access the file", - )); - } - let write_error_message = &["Can't write file \"", &path, "\""].concat(); - std::fs::write(&path, bytes.as_bytes(&ctx)?).or_throw_msg(&ctx, write_error_message)?; - - #[cfg(unix)] - { - if let Some(Either::Right(opts)) = options.0 { - use std::os::unix::fs::PermissionsExt; - - std::fs::set_permissions(path, PermissionsExt::from_mode(opts.mode.unwrap_or(0o666))) - .or_throw_msg(&ctx, write_error_message)?; - } - } - #[cfg(not(unix))] - { - _ = options; - if let Some(Either::Right(opts)) = options.0 { - _ = opts.mode; - } - } - - Ok(()) -} - -pub(crate) struct WriteFileOptions { - pub mode: Option, -} - -impl<'js> FromJs<'js> for WriteFileOptions { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let mode = obj.get_optional::<_, u32>("mode")?; - - Ok(Self { mode }) - } -} diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 1efe077..385caea 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -53,12 +53,12 @@ pub mod utils; pub fn init( ctx: &rsquickjs::Ctx, - permissions: permissions::Permissions, + vsys: std::sync::Arc, #[cfg(feature = "console")] log_type: console::LogType, ) -> rsquickjs::Result<()> { navigator::init(ctx)?; utils::primordials::BasePrimordials::init(ctx)?; - permissions::init(ctx.clone(), permissions)?; + permissions::init(ctx.clone(), vsys)?; exceptions::init(ctx)?; async_hooks::init(ctx)?; text::init(ctx)?; @@ -66,6 +66,7 @@ pub fn init( module::module::init(ctx)?; buffer::init(ctx)?; timers::init(ctx)?; + #[cfg(feature = "crypto")] { crypto::init(ctx)?; diff --git a/modules/src/module/module/loader.rs b/modules/src/module/module/loader.rs index 8156149..86c4d25 100644 --- a/modules/src/module/module/loader.rs +++ b/modules/src/module/module/loader.rs @@ -11,7 +11,7 @@ use rsquickjs::{ prelude::{Func, Opt}, Ctx, Error, Module, Object, Result, Value, }; -use tracing::trace; +use tracing::info; use super::{Hook, ModuleHookState}; @@ -42,11 +42,11 @@ impl ModuleLoader { impl Loader for ModuleLoader { fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result> { - trace!("Try load '{}'", name); + info!("Try load '{}'", name); let (short_circuit, next_load, source) = module_hook_load(ctx, name)?; if short_circuit { - trace!("+- Loading module via ShortCircuit: {}\n", name); + info!("β›„πŸ₯• Loading module via ShortCircuit: {}\n", name); return match source { AnyOf2::A(s) => Module::declare(ctx.clone(), name, s), AnyOf2::B(b) => Module::declare(ctx.clone(), name, b.as_bytes(ctx)?), @@ -59,12 +59,12 @@ impl Loader for ModuleLoader { .ok_or_else(|| Error::new_loading(name))?; if next_load { - trace!("| Determined as `nextResolve`: {}", name); + info!("❄️ Determined as `nextResolve`: {}", name); } else { - trace!("| Determined as `NormalCircuit`: {}", name); + info!("❄️ Determined as `NormalCircuit`: {}", name); } - trace!("+- Loading module: {}\n", name); + info!("β›„πŸ₯• Loading module: {}\n", name); (load)(ctx.clone(), Vec::from(name)) } } diff --git a/modules/src/module/module/require.rs b/modules/src/module/module/require.rs index a6690fa..3638b80 100644 --- a/modules/src/module/module/require.rs +++ b/modules/src/module/module/require.rs @@ -5,7 +5,7 @@ use crate::utils::json::parse::json_parse; use crate::utils::{ctx::CtxExt, io::BYTECODE_FILE_EXT, provider::ProviderType}; use rsquickjs::{atom::PredefinedAtom, qjs, Ctx, Filter, Function, Module, Object, Result, Value}; use tokio::time::Instant; -use tracing::trace; +use tracing::{debug, info}; use crate::module::package::resolver::require_resolve; use crate::module::CJS_IMPORT_PREFIX; @@ -15,7 +15,7 @@ use super::{ModuleNames, RequireState}; pub fn require(ctx: Ctx<'_>, specifier: String) -> Result> { let globals = ctx.globals(); - let hooked_fn: Option = globals.get("__require_hook").ok(); + // let hooked_fn: Option = globals.get("__require_hook").ok(); struct Args<'js>(Ctx<'js>); let Args(ctx) = Args(ctx); @@ -30,7 +30,7 @@ pub fn require(ctx: Ctx<'_>, specifier: String) -> Result> { let is_json = specifier.ends_with(".json"); - trace!("Before specifier: {}", specifier); + debug!("Before specifier: {}", specifier); let import_specifier: Rc = if !is_cjs_import { let is_bytecode = specifier.ends_with(BYTECODE_FILE_EXT); @@ -52,8 +52,7 @@ pub fn require(ctx: Ctx<'_>, specifier: String) -> Result> { let module_name = module_name.trim_start_matches(CJS_IMPORT_PREFIX); let abs_path = resolve_path([module_name].iter())?; - let resolved_path = - require_resolve(&ctx, &specifier, &abs_path, hooked_fn, false)?.into_owned(); + let resolved_path = require_resolve(&ctx, &specifier, &abs_path, false)?.into_owned(); import_name = resolved_path.into(); if is_bytecode_or_json { import_name.clone() @@ -66,7 +65,7 @@ pub fn require(ctx: Ctx<'_>, specifier: String) -> Result> { specifier.into() }; - trace!("After specifier: {}", import_specifier); + debug!("After specifier: {}", import_specifier); let binding = ctx.userdata::>().unwrap(); let mut state = binding.borrow_mut(); @@ -87,7 +86,7 @@ pub fn require(ctx: Ctx<'_>, specifier: String) -> Result> { return Ok(value); } - trace!("Require: {}", import_specifier); + info!("Require: {}", import_specifier); let obj = Object::new(ctx.clone())?; state.progress.insert(import_name.clone(), obj.clone()); diff --git a/modules/src/module/module/resolver.rs b/modules/src/module/module/resolver.rs index bac4793..55770d3 100644 --- a/modules/src/module/module/resolver.rs +++ b/modules/src/module/module/resolver.rs @@ -10,7 +10,7 @@ use rsquickjs::{ prelude::{Func, Opt}, Ctx, Error, Object, Result, Value, }; -use tracing::trace; +use tracing::{debug, info}; use crate::module::CJS_IMPORT_PREFIX; @@ -36,23 +36,23 @@ impl Resolver for ModuleResolver { let base = base.trim_start_matches(CJS_IMPORT_PREFIX); - trace!("Try resolve '{}' from '{}'", name, base); + debug!("Try resolve '{}' from '{}'", name, base); let (short_circuit, next_resolve, x) = module_hook_resolve(ctx, name, base)?; if short_circuit { - trace!("+- Resolved by `ShortCircuit`: {}", x); + info!("β›„πŸ₯• Resolved by `ShortCircuit`: {}", x); return Ok(x); } if next_resolve { - trace!("| Determined as `nextResolve`: {}", x); + info!("❄️ Determined as `nextResolve`: {}", x); } else { - trace!("| Determined as `NormalCircuit`: {}", x); + info!("❄️ Determined as `NormalCircuit`: {}", x); } if self.modules.contains(&x) { - trace!("+- Resolved by `NativeModule`: {}", x); + info!("β›„πŸ₯• Resolved by `NativeModule`: {}", x); Ok(x) } else { Err(Error::new_resolving(base, x)) @@ -61,7 +61,7 @@ impl Resolver for ModuleResolver { } pub fn module_hook_resolve<'js>(ctx: &Ctx<'js>, x: &str, y: &str) -> Result<(bool, bool, String)> { - trace!("| module_hook_resolve(x, y):({}, {})", x, y); + info!("❄️ module_hook_resolve(x, y):({}, {})", x, y); let bind_state = ctx.userdata::>().or_throw(ctx)?; let hooks = Rc::new(bind_state.borrow().hooks.clone()); diff --git a/modules/src/module/package/loader.rs b/modules/src/module/package/loader.rs index dd20ee4..da4fb83 100644 --- a/modules/src/module/package/loader.rs +++ b/modules/src/module/package/loader.rs @@ -1,7 +1,7 @@ use std::{fs::File, io::Read}; use rsquickjs::{loader::Loader, Ctx, Function, Module, Object, Result, Value}; -use tracing::trace; +use tracing::info; use crate::module::{CJS_IMPORT_PREFIX, CJS_LOADER_PREFIX}; @@ -74,7 +74,7 @@ impl PackageLoader { let (from_cjs_import, is_cjs, normalized_name, path) = Self::normalize_name(name); - trace!("+- Loading module: {}\n", normalized_name); + info!("β›„πŸ₯• Loading module: {}\n", normalized_name); //json files can never be from CJS imports as they are handled by require if !from_cjs_import { @@ -109,7 +109,7 @@ impl PackageLoader { impl Loader for PackageLoader { fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result> { - trace!("Try load '{}'", name); + info!("Try load '{}'", name); let (module, url) = Self::load_module(name, ctx)?; if let Some(url) = url { let meta: Object = module.meta()?; diff --git a/modules/src/module/package/resolver.rs b/modules/src/module/package/resolver.rs index fd8143c..57489f9 100644 --- a/modules/src/module/package/resolver.rs +++ b/modules/src/module/package/resolver.rs @@ -14,7 +14,7 @@ use crate::utils::{ }; use rsquickjs::{loader::Resolver, Ctx, Error, Function, Result}; use simd_json::{derived::ValueObjectAccessAsScalar, BorrowedValue}; -use tracing::trace; +use tracing::{debug, info}; use crate::module::{CJS_IMPORT_PREFIX, CJS_LOADER_PREFIX}; use crate::path; @@ -96,9 +96,9 @@ impl Resolver for PackageResolver { let base = base.trim_start_matches(CJS_IMPORT_PREFIX); - trace!("Try resolve '{}' from '{}'", name, base); + debug!("Try resolve '{}' from '{}'", name, base); - require_resolve(ctx, name, base, None, true).map(|name| name.into_owned()) + require_resolve(ctx, name, base, true).map(|name| name.into_owned()) } } @@ -109,7 +109,6 @@ pub fn require_resolve<'a>( ctx: &Ctx<'_>, x: &'a str, y: &str, - hooked_fn: Option>, is_esm: bool, ) -> Result> { // trim schema @@ -127,14 +126,7 @@ pub fn require_resolve<'a>( }; let y = y.as_str(); - trace!("require_resolve(x, y):({}, {})", x, y); - - // 1'. If X is a bytecode cache, - if let Some(hooked_resolve) = hooked_fn { - if let Ok(path) = hooked_resolve.call::<_, String>((x, y)) { - return Ok(path.into()); - } - } + info!("require_resolve(x, y):({}, {})", x, y); //fast path for when we have supported extensions let (_, ext_name) = path::name_extname(x); @@ -183,12 +175,12 @@ pub fn require_resolve<'a>( // a. LOAD_AS_FILE(Y + X) if let Ok(Some(path)) = load_as_file(ctx, y_plus_x.clone()) { - trace!("+- Resolved by `LOAD_AS_FILE`: {}", path); + info!("β›„πŸ₯• Resolved by `LOAD_AS_FILE`: {}", path); return to_abs_path(path); } else { // b. LOAD_AS_DIRECTORY(Y + X) if let Ok(Some(path)) = load_as_directory(ctx, y_plus_x) { - trace!("+- Resolved by `LOAD_AS_DIRECTORY`: {}", path); + info!("β›„πŸ₯• Resolved by `LOAD_AS_DIRECTORY`: {}", path); return to_abs_path(path); } } @@ -201,26 +193,26 @@ pub fn require_resolve<'a>( if x.starts_with('#') { // a. LOAD_PACKAGE_IMPORTS(X, dirname(Y)) if let Ok(Some(path)) = load_package_imports(ctx, x, &dirname_y) { - trace!("+- Resolved by `LOAD_PACKAGE_IMPORTS`: {}", path); + info!("β›„πŸ₯• Resolved by `LOAD_PACKAGE_IMPORTS`: {}", path); return Ok(path.into()); } } // 5. LOAD_PACKAGE_SELF(X, dirname(Y)) if let Ok(Some(path)) = load_package_self(ctx, x, &dirname_y, is_esm) { - trace!("+- Resolved by `LOAD_PACKAGE_SELF`: {}", path); + info!("β›„πŸ₯• Resolved by `LOAD_PACKAGE_SELF`: {}", path); return to_abs_path(path.into()); } // 6. LOAD_NODE_MODULES(X, dirname(Y)) if let Some(path) = load_node_modules(ctx, x, dirname_y, is_esm) { - trace!("+- Resolved by `LOAD_NODE_MODULES`: {}", path); + info!("β›„πŸ₯• Resolved by `LOAD_NODE_MODULES`: {}", path); return Ok(path); } // 6.5. LOAD_AS_FILE(X) if let Ok(Some(path)) = load_as_file(ctx, Rc::new(x.to_owned())) { - trace!("+- Resolved by `LOAD_AS_FILE`: {}", path); + info!("β›„πŸ₯• Resolved by `LOAD_AS_FILE`: {}", path); return to_abs_path(path); } @@ -229,7 +221,7 @@ pub fn require_resolve<'a>( } fn resolved_by_file_exists(path: Cow<'_, str>) -> Result> { - trace!("+- Resolved by `FILE`: {}", path); + info!("β›„πŸ₯• Resolved by `FILE`: {}", path); to_abs_path(path) } @@ -245,11 +237,11 @@ fn to_abs_path(path: Cow<'_, str>) -> Result> { // LOAD_AS_FILE(X) fn load_as_file<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> { - trace!("| load_as_file(x): {}", x); + info!("❄️ load_as_file(x): {}", x); // 1. If X is a file, load X as its file extension format. STOP if Path::new(x.as_ref()).is_file() { - trace!("| load_as_file(1): {}", x); + info!("❄️ load_as_file(1): {}", x); return Ok(Some(rc_string_to_cow(x))); } @@ -271,7 +263,7 @@ fn load_as_file<'a>(ctx: &Ctx<'_>, x: Rc) -> Result> // b. If no scope was found None => { // 1. MAYBE_DETECT_AND_LOAD(X.js) - trace!("| load_as_file(2.b.1): {}", current_file); + info!("❄️ load_as_file(2.b.1): {}", current_file); return Ok(Some(current_file.into())); } Some(path) => { @@ -283,14 +275,14 @@ fn load_as_file<'a>(ctx: &Ctx<'_>, x: Rc) -> Result> // 1. If the "type" field is "module", load X.js as an ECMAScript module. STOP. // 2. If the "type" field is "commonjs", load X.js as an CommonJS module. STOP. if _type == "module" || _type == "commonjs" { - trace!("| load_as_file(2.c.[1|2]): {}", current_file); + info!("❄️ load_as_file(2.c.[1|2]): {}", current_file); return Ok(Some(current_file.into())); } } } } // d. MAYBE_DETECT_AND_LOAD(X.js) - trace!("| load_as_file(2.d): {}", current_file); + info!("❄️ load_as_file(2.d): {}", current_file); return Ok(Some(current_file.into())); } base_file = Some(current_file); @@ -302,7 +294,7 @@ fn load_as_file<'a>(ctx: &Ctx<'_>, x: Rc) -> Result> current_file.truncate(base_file_length); current_file.push_str(".json"); if Path::new(¤t_file).is_file() { - trace!("| load_as_file(3): {}", current_file); + info!("❄️ load_as_file(3): {}", current_file); return Ok(Some(current_file.into())); } } @@ -314,7 +306,7 @@ fn load_as_file<'a>(ctx: &Ctx<'_>, x: Rc) -> Result> // LOAD_INDEX(X) fn load_index<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> { - trace!("| load_index(x): {}", x); + info!("❄️ load_index(x): {}", x); let mut base_file = String::with_capacity(x.len() + "/index".len() + 4); base_file.push_str(x.as_ref()); @@ -333,7 +325,7 @@ fn load_index<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> match find_the_closest_package_scope(&x) { // b. If no scope was found, load X/index.js as a CommonJS module. STOP. None => { - trace!("| load_index(1.b): {}", file); + info!("❄️ load_index(1.b): {}", file); return Ok(Some(file.into())); } // c. If the SCOPE/package.json contains "type" field, @@ -344,12 +336,12 @@ fn load_index<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> if let Some(_type) = get_string_field(&package_json, "type") { // 1. If the "type" field is "module", load X/index.js as an ECMAScript module. STOP. if _type == "module" { - trace!("| load_index(1.c.1): {}", file); + info!("❄️ load_index(1.c.1): {}", file); return Ok(Some(file.into())); } } // 2. Else, load X/index.js as an CommonJS module. STOP. - trace!("| load_index(1.c.2): {}", file); + info!("❄️ load_index(1.c.2): {}", file); return Ok(Some(file.into())); } } @@ -364,7 +356,7 @@ fn load_index<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> file.truncate(base_file_length); file.push_str(".json"); if Path::new(&file).is_file() { - trace!("| load_index(2): {}", file); + info!("❄️ load_index(2): {}", file); return Ok(Some(file.into())); } } @@ -376,7 +368,7 @@ fn load_index<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> // LOAD_AS_DIRECTORY(X) fn load_as_directory<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> { - trace!("| load_as_directory(x): {}", x); + info!("❄️ load_as_directory(x): {}", x); // 1. If X/package.json is a file, let file = [&x, "/package.json"].concat(); @@ -390,12 +382,12 @@ fn load_as_directory<'a>(ctx: &Ctx<'_>, x: Rc) -> Result(ctx: &Ctx<'_>, x: Rc) -> Result( start: String, is_esm: bool, ) -> Option> { - trace!("| load_node_modules(x, start): ({}, {})", x, start); + info!("❄️ load_node_modules(x, start): ({}, {})", x, start); fn search_dir<'a>(ctx: &Ctx<'_>, dir: &str, x: &str, is_esm: bool) -> Option> { // a. LOAD_PACKAGE_EXPORTS(X, DIR) if let Ok(path) = load_package_exports(ctx, x, dir, is_esm) { - trace!("| load_node_modules(2.a): {}", path); + info!("❄️ load_node_modules(2.a): {}", path); return Some(path); } let dir_slash_x = Rc::new([dir, "/", x].concat()); // b. LOAD_AS_FILE(DIR/X) if let Ok(Some(path)) = load_as_file(ctx, dir_slash_x.clone()) { - trace!("| load_node_modules(2.b): {}", path); + info!("❄️ load_node_modules(2.b): {}", path); return Some(path); } // c. LOAD_AS_DIRECTORY(DIR/X) if let Ok(Some(path)) = load_as_directory(ctx, dir_slash_x.clone()) { - trace!("| load_node_modules(2.c): {}", path); + info!("❄️ load_node_modules(2.c): {}", path); return Some(path); } None @@ -520,7 +512,7 @@ fn load_node_modules<'a>( // LOAD_PACKAGE_IMPORTS(X, DIR) fn load_package_imports(ctx: &Ctx<'_>, x: &str, dir: &str) -> Result> { - trace!("| load_package_imports(x, dir): ({}, {})", x, dir); + info!("❄️ load_package_imports(x, dir): ({}, {})", x, dir); // 1. Find the closest package scope SCOPE to DIR. // 2. If no scope was found, return. @@ -537,7 +529,7 @@ fn load_package_imports(ctx: &Ctx<'_>, x: &str, dir: &str) -> Resultdefined in the ESM resolver. // 6. RESOLVE_ESM_MATCH(MATCH). if let Some(module_path) = package_imports_resolve(&package_json, x) { - trace!("| load_package_imports(6): {}", module_path); + info!("❄️ load_package_imports(6): {}", module_path); let dir = path.as_ref().trim_end_matches("package.json"); let module_path = to_abs_path(correct_extensions([dir, module_path].concat()))?; return Ok(Some(module_path.into())); @@ -554,7 +546,7 @@ fn load_package_exports<'a>( dir: &str, is_esm: bool, ) -> Result> { - trace!("| load_package_exports(x, dir): ({}, {})", x, dir); + info!("❄️ load_package_exports(x, dir): ({}, {})", x, dir); //1. Try to interpret X as a combination of NAME and SUBPATH where the name // may have a @scope/ prefix and the subpath begins with a slash (`/`). let mut n = 1; @@ -570,10 +562,9 @@ fn load_package_exports<'a>( let mut package_json_exists; loop { - trace!( + info!( "| split name and scope(name, scope): ({}, {})", - name, - scope + name, scope ); package_json_path.push_str(scope); package_json_path.push_str("/package.json"); @@ -668,7 +659,7 @@ fn load_package_exports<'a>( // LOAD_PACKAGE_SELF(X, DIR) fn load_package_self(ctx: &Ctx<'_>, x: &str, dir: &str, is_esm: bool) -> Result> { - trace!("| load_package_self(x, dir): ({}, {})", x, dir); + info!("❄️ load_package_self(x, dir): ({}, {})", x, dir); let mut n = 1; let (mut name, mut scope, mut is_last) = get_name_and_scope(x, n); @@ -685,10 +676,9 @@ fn load_package_self(ctx: &Ctx<'_>, x: &str, dir: &str, is_esm: bool) -> Result< package_json = simd_json::to_borrowed_value(&mut package_json_file).or_throw(ctx)?; // 3. If the SCOPE/package.json "exports" is null or undefined, return. loop { - trace!( + info!( "| split name and scope(name, scope): ({}, {})", - name, - scope + name, scope ); // 4. If the SCOPE/package.json "name" is not the first segment of X, return. if is_exports_field_exists(&package_json) { @@ -712,7 +702,7 @@ fn load_package_self(ctx: &Ctx<'_>, x: &str, dir: &str, is_esm: bool) -> Result< // 6. RESOLVE_ESM_MATCH(MATCH) if let Ok((path, resolve_path, _)) = package_exports_resolve(&package_json, name, is_esm) { let path = resolve_path.unwrap_or_else(|| path.to_string()); - trace!("| load_package_self(2.c): {}", path); + info!("❄️ load_package_self(2.c): {}", path); let dir = package_json_path.trim_end_matches("package.json"); let module_path = correct_extensions([dir, &path].concat()); return Ok(Some(module_path.into())); diff --git a/modules/src/permissions.rs b/modules/src/permissions.rs index 764495d..48627d4 100644 --- a/modules/src/permissions.rs +++ b/modules/src/permissions.rs @@ -1,42 +1,86 @@ -#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] -/// Struct representing permissions for filesystem, network, and environment access. -/// **WARNING**: by default, no permissions are granted (all whitelists are empty). -pub struct Permissions { - pub fs: BlackOrWhiteList, - pub net: BlackOrWhiteList, - pub env: BlackOrWhiteList, - pub stdio: bool, -} - -#[derive(Debug, PartialEq, Clone, Hash, Eq, rsquickjs::class::Trace, rsquickjs::JsLifetime)] -pub enum BlackOrWhiteList { - BlackList(Vec), - WhiteList(Vec), -} - -impl Default for Permissions { - fn default() -> Self { - Self { - fs: BlackOrWhiteList::WhiteList(vec![]), - net: BlackOrWhiteList::WhiteList(vec![]), - env: BlackOrWhiteList::WhiteList(vec![]), - stdio: false, - } - } +//! Permissions module - now delegates to Vsys stored in context +//! +//! Vsys is stored in the JS context and permissions are accessed through it. + +use std::path::Path; +use std::sync::Arc; + +use rsquickjs::class::{Trace, Tracer}; +use rsquickjs::JsLifetime; + +// Re-export vsys types +pub use xmas_vsys::fs::FsVTable; +pub use xmas_vsys::permissions::{BlackOrWhiteList, Permissions}; +pub use xmas_vsys::Vsys; + +/// Wrapper to store Vsys in JS context with required trait implementations +#[derive(Clone)] +pub struct VsysContext(pub Arc); + +impl<'js> Trace<'js> for VsysContext { + fn trace<'a>(&self, _: Tracer<'a, 'js>) {} } -impl Permissions { - pub fn allow_all() -> Self { - Self { - fs: BlackOrWhiteList::BlackList(vec![]), - net: BlackOrWhiteList::BlackList(vec![]), - env: BlackOrWhiteList::BlackList(vec![]), - stdio: true, - } +unsafe impl<'js> JsLifetime<'js> for VsysContext { + type Changed<'to> = VsysContext; +} + +impl std::ops::Deref for VsysContext { + type Target = Vsys; + + fn deref(&self) -> &Self::Target { + &self.0 } } -pub fn init(ctx: rsquickjs::Ctx<'_>, permissions: Permissions) -> rsquickjs::Result<()> { - ctx.store_userdata(permissions)?; +/// Initialize the context with a Vsys instance +pub fn init(ctx: rsquickjs::Ctx<'_>, vsys: Arc) -> rsquickjs::Result<()> { + ctx.store_userdata(VsysContext(vsys))?; Ok(()) } + +/// Helper to get Vsys from context +pub fn get_vsys(ctx: &rsquickjs::Ctx<'_>) -> Option> { + ctx.userdata::().map(|v| v.0.clone()) +} + +/// Helper to check filesystem permission from context +pub fn check_fs_permission(ctx: &rsquickjs::Ctx<'_>, path: &Path) -> bool { + get_vsys(ctx) + .map(|v| v.permissions().check_fs(path)) + .unwrap_or(false) +} + +/// Helper to check network permission from context +pub fn check_net_permission(ctx: &rsquickjs::Ctx<'_>, host: &str) -> bool { + get_vsys(ctx) + .map(|v| v.permissions().check_net(host)) + .unwrap_or(false) +} + +/// Helper to get FsVTable from context +/// Returns the filesystem vtable from the Vsys instance, or None if not initialized +pub fn get_fs(ctx: &rsquickjs::Ctx<'_>) -> Option> { + get_vsys(ctx) +} + +/// Execute a filesystem operation using the vtable from context +/// This is a convenience macro-like function that handles the common pattern +/// of getting vsys, checking permission, and calling the fs operation +pub fn with_fs(ctx: &rsquickjs::Ctx<'_>, path: &Path, op: F) -> rsquickjs::Result +where + F: FnOnce(&FsVTable) -> xmas_vsys::error::VsysResult, +{ + let vsys = get_vsys(ctx).ok_or_else(|| { + rsquickjs::Error::new_from_js("undefined", "Vsys not initialized in context") + })?; + + if !vsys.permissions().check_fs(path) { + return Err(rsquickjs::Exception::throw_message( + ctx, + "Permission denied", + )); + } + + op(vsys.fs()).map_err(|e| rsquickjs::Exception::throw_message(ctx, &e.to_string())) +} diff --git a/repl/Cargo.toml b/repl/Cargo.toml index c67c403..1c2ae96 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -10,8 +10,10 @@ keywords = ["quickjs", "ecmascript", "javascript", "es6", "es2020"] [dependencies] rsquickjs = { workspace = true, features = ["macro"] } +xmas-vsys = { workspace = true } xmas-js-modules = { workspace = true } xmas-package-manager = { workspace = true } +xmas-bundler = { workspace = true } clap = { version = "4.5.4", features = ["derive"] } deno_task_shell = "0.26.1" tracing-subscriber = "0.3.17" @@ -21,6 +23,7 @@ tokio = { version = "1", features = ["full"] } anyhow = "1.0.100" syntect = "5.3.0" colored = "3.0.0" + # itoa = "1.0.15" # ryu = "1.0.20" # simd-json = "0.17.0" diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 6c76fe0..1eebf4d 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -13,11 +13,14 @@ use rustyline::{CompletionType, Config, EditMode, Editor}; use std::any; use std::io::stdout; use std::ptr::NonNull; +use std::sync::Arc; use syntect::easy::HighlightLines; use syntect::highlighting::{Style, Theme, ThemeSet}; use syntect::parsing::{SyntaxDefinition, SyntaxReference, SyntaxSet, SyntaxSetBuilder}; use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; use xmas_js_modules::console::write_log; +use xmas_js_modules::module::package::loader::PackageLoader; +use xmas_js_modules::module::package::resolver::PackageResolver; use xmas_js_modules::permissions::Permissions; use xmas_js_modules::utils::ctx::CtxExtension; use xmas_js_modules::utils::result::ResultExt; @@ -152,13 +155,18 @@ fn print_version() { "for getting help".cyan() ); println!( - "⛷️\t{}", - "CTRL+D for save and exit, CTRL+C for interrupt exit".cyan() + "⛷️\t{}{}{}{}\n", + "CTRL+D".cyan().bold(), + " for save and exit ".cyan(), + "CTRL+C".cyan().bold(), + " for interrupt exit".cyan() ); } pub async fn repl() -> anyhow::Result<()> { - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt::Subscriber::builder() + .with_max_level(tracing::Level::WARN) + .init(); let config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::List) @@ -190,8 +198,17 @@ pub async fn repl() -> anyhow::Result<()> { let context = AsyncContext::full(&runtime).await?; print_version(); let allocator = xmas_js_modules::script::allocator(); + let (resolver, loader, ga) = + xmas_js_modules::module::module_builder::ModuleBuilder::default().build(); + runtime + .set_loader((resolver, PackageResolver), (loader, PackageLoader)) + .await; rsquickjs::async_with!(context => |ctx| { - xmas_js_modules::init(&ctx, Permissions::allow_all(), xmas_js_modules::console::LogType::Stdio)?; + let vsys = xmas_vsys::Vsys::builder() + .permissions(Permissions::allow_all()) + .build(); + xmas_js_modules::init(&ctx, Arc::new(vsys), xmas_js_modules::console::LogType::Stdio)?; + ga.attach(&ctx)?; let t = ctx.get_background_task_poller(); loop { let readline = rl.readline("πŸŽ„ >> "); @@ -207,6 +224,8 @@ pub async fn repl() -> anyhow::Result<()> { println!("❄️\t{} - Clear the console", "/clear".cyan().bold()); println!("❄️\t{} - Package manager commands", "/pm".cyan().bold()); println!("❄️\t{} - Cross platform shell commands", "/$".cyan().bold()); + println!("❄️\t{} - Bundle JavaScript/TypeScript files", "/bun".cyan().bold()); + }, "version" => { print_version(); @@ -245,6 +264,13 @@ pub async fn repl() -> anyhow::Result<()> { eprintln!("{}: Shell command exited with code {}", "Error".red().bold(), exit_code); } } + else if args[0] == "bun" { + if let Ok(cmd) = xmas_bundler::BundleConfig::try_parse_from(&args) { + let _ = xmas_bundler::bundle(cmd).await; + } else { + eprintln!("{}: Invalid bundler command", "Error".red().bold()); + } + } else { eprintln!("{}: Unknown command '{}'", "Error".red().bold(), cmd); } diff --git a/vsys/Cargo.toml b/vsys/Cargo.toml new file mode 100644 index 0000000..05e33f8 --- /dev/null +++ b/vsys/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xmas-vsys" +version = "0.1.0" +edition = "2021" +description = "Virtual System Layer for Xmas.JS - pluggable fs/net/module abstraction" +license = "Apache-2.0 OR GPL-3.0" + +[dependencies] +tracing = "0.1.40" +simd-json = "0.14" +serde_json = "1.0" +uuid = { version = "1.0", features = ["v4"] } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[dev-dependencies] +tempfile = "3.10" + +[features] +default = [] diff --git a/vsys/src/error.rs b/vsys/src/error.rs new file mode 100644 index 0000000..134c330 --- /dev/null +++ b/vsys/src/error.rs @@ -0,0 +1,113 @@ +//! Error types for vsys operations + +use std::ffi::CString; +use std::fmt; +use std::io; + +/// Result type for vsys operations +pub type VsysResult = Result; + +/// Error type for vsys operations +#[derive(Debug)] +pub enum VsysError { + /// I/O error from the underlying system + Io(io::Error), + /// Permission denied + PermissionDenied(String), + /// File or resource not found + NotFound(String), + /// Operation not supported by this vsys implementation + NotSupported(String), + /// Invalid argument + InvalidArgument(String), + /// Module resolution error + ModuleResolution { specifier: String, message: String }, + /// Module loading error + ModuleLoad { path: String, message: String }, + /// Custom error with code + Custom { code: i32, message: String }, +} + +impl fmt::Display for VsysError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VsysError::Io(e) => write!(f, "I/O error: {}", e), + VsysError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg), + VsysError::NotFound(msg) => write!(f, "Not found: {}", msg), + VsysError::NotSupported(msg) => write!(f, "Not supported: {}", msg), + VsysError::InvalidArgument(msg) => write!(f, "Invalid argument: {}", msg), + VsysError::ModuleResolution { specifier, message } => { + write!(f, "Cannot resolve module '{}': {}", specifier, message) + } + VsysError::ModuleLoad { path, message } => { + write!(f, "Cannot load module '{}': {}", path, message) + } + VsysError::Custom { code, message } => { + write!(f, "Error (code {}): {}", code, message) + } + } + } +} + +impl std::error::Error for VsysError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + VsysError::Io(e) => Some(e), + _ => None, + } + } +} + +impl From for VsysError { + fn from(e: io::Error) -> Self { + match e.kind() { + io::ErrorKind::NotFound => VsysError::NotFound(e.to_string()), + io::ErrorKind::PermissionDenied => VsysError::PermissionDenied(e.to_string()), + _ => VsysError::Io(e), + } + } +} + +// C ABI compatible error representation +#[repr(C)] +pub struct CVsysError { + pub code: i32, + pub message: *mut i8, // C string, caller must free +} + +impl CVsysError { + pub const OK: i32 = 0; + pub const ERR_IO: i32 = -1; + pub const ERR_PERMISSION_DENIED: i32 = -2; + pub const ERR_NOT_FOUND: i32 = -3; + pub const ERR_NOT_SUPPORTED: i32 = -4; + pub const ERR_INVALID_ARGUMENT: i32 = -5; + pub const ERR_MODULE_RESOLUTION: i32 = -6; + pub const ERR_MODULE_LOAD: i32 = -7; + + pub fn ok() -> Self { + Self { + code: Self::OK, + message: std::ptr::null_mut(), + } + } + + pub fn from_error(e: &VsysError) -> Self { + let (code, msg) = match e { + VsysError::Io(_) => (Self::ERR_IO, e.to_string()), + VsysError::PermissionDenied(_) => (Self::ERR_PERMISSION_DENIED, e.to_string()), + VsysError::NotFound(_) => (Self::ERR_NOT_FOUND, e.to_string()), + VsysError::NotSupported(_) => (Self::ERR_NOT_SUPPORTED, e.to_string()), + VsysError::InvalidArgument(_) => (Self::ERR_INVALID_ARGUMENT, e.to_string()), + VsysError::ModuleResolution { .. } => (Self::ERR_MODULE_RESOLUTION, e.to_string()), + VsysError::ModuleLoad { .. } => (Self::ERR_MODULE_LOAD, e.to_string()), + VsysError::Custom { code, .. } => (*code, e.to_string()), + }; + + let c_string = CString::new(msg).unwrap_or_else(|_| CString::new("Unknown error").unwrap()); + Self { + code, + message: c_string.into_raw(), + } + } +} diff --git a/vsys/src/fs.rs b/vsys/src/fs.rs new file mode 100644 index 0000000..4bb3eef --- /dev/null +++ b/vsys/src/fs.rs @@ -0,0 +1,871 @@ +//! Filesystem virtual table for vsys +//! +//! This module provides a pluggable filesystem abstraction layer. +//! By default, it uses the real filesystem (std::fs / tokio::fs), +//! but can be replaced with custom implementations. + +use std::fs::Metadata; +use std::path::Path; +use std::time::SystemTime; + +use crate::error::{VsysError, VsysResult}; + +/// File type information +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileType { + File, + Directory, + Symlink, + Other, +} + +/// File statistics (platform-independent subset) +#[derive(Debug, Clone)] +pub struct FileStat { + pub file_type: FileType, + pub size: u64, + pub readonly: bool, + pub modified: Option, + pub accessed: Option, + pub created: Option, + /// Unix mode (0 on Windows) + pub mode: u32, + /// Unix uid (0 on Windows) + pub uid: u32, + /// Unix gid (0 on Windows) + pub gid: u32, +} + +impl FileStat { + /// Create from std::fs::Metadata + pub fn from_metadata(metadata: &Metadata) -> Self { + let file_type = if metadata.is_file() { + FileType::File + } else if metadata.is_dir() { + FileType::Directory + } else if metadata.is_symlink() { + FileType::Symlink + } else { + FileType::Other + }; + + #[cfg(unix)] + let (mode, uid, gid) = { + use std::os::unix::fs::MetadataExt; + (metadata.mode(), metadata.uid(), metadata.gid()) + }; + + #[cfg(not(unix))] + let (mode, uid, gid) = (0o666, 0, 0); + + Self { + file_type, + size: metadata.len(), + readonly: metadata.permissions().readonly(), + modified: metadata.modified().ok(), + accessed: metadata.accessed().ok(), + created: metadata.created().ok(), + mode, + uid, + gid, + } + } + + pub fn is_file(&self) -> bool { + self.file_type == FileType::File + } + + pub fn is_dir(&self) -> bool { + self.file_type == FileType::Directory + } + + pub fn is_symlink(&self) -> bool { + self.file_type == FileType::Symlink + } +} + +/// Directory entry +#[derive(Debug, Clone)] +pub struct DirEntry { + pub name: String, + pub file_type: FileType, +} + +/// File open options +#[derive(Debug, Clone, Default)] +pub struct OpenOptions { + pub read: bool, + pub write: bool, + pub append: bool, + pub truncate: bool, + pub create: bool, + pub create_new: bool, + pub mode: u32, +} + +impl OpenOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn read(mut self, read: bool) -> Self { + self.read = read; + self + } + + pub fn write(mut self, write: bool) -> Self { + self.write = write; + self + } + + pub fn append(mut self, append: bool) -> Self { + self.append = append; + self + } + + pub fn truncate(mut self, truncate: bool) -> Self { + self.truncate = truncate; + self + } + + pub fn create(mut self, create: bool) -> Self { + self.create = create; + self + } + + pub fn create_new(mut self, create_new: bool) -> Self { + self.create_new = create_new; + self + } + + pub fn mode(mut self, mode: u32) -> Self { + self.mode = mode; + self + } +} + +/// Seek position for file handles +#[derive(Debug, Clone, Copy)] +pub enum SeekFrom { + Start(u64), + End(i64), + Current(i64), +} + +impl From for std::io::SeekFrom { + fn from(seek: SeekFrom) -> Self { + match seek { + SeekFrom::Start(n) => std::io::SeekFrom::Start(n), + SeekFrom::End(n) => std::io::SeekFrom::End(n), + SeekFrom::Current(n) => std::io::SeekFrom::Current(n), + } + } +} + +/// File handle - opaque wrapper around a file descriptor/handle +/// +/// This uses a Box to allow different implementations +/// while maintaining a consistent interface. +pub struct FsHandle { + inner: Box, +} + +impl FsHandle { + pub fn new(inner: T) -> Self { + Self { + inner: Box::new(inner), + } + } + + pub fn read(&mut self, buf: &mut [u8]) -> VsysResult { + self.inner.read(buf) + } + + pub fn write(&mut self, buf: &[u8]) -> VsysResult { + self.inner.write(buf) + } + + pub fn seek(&mut self, pos: SeekFrom) -> VsysResult { + self.inner.seek(pos) + } + + pub fn sync_all(&self) -> VsysResult<()> { + self.inner.sync_all() + } + + pub fn sync_data(&self) -> VsysResult<()> { + self.inner.sync_data() + } + + pub fn stat(&self) -> VsysResult { + self.inner.stat() + } + + pub fn set_len(&self, size: u64) -> VsysResult<()> { + self.inner.set_len(size) + } + + pub fn set_permissions(&self, readonly: bool) -> VsysResult<()> { + self.inner.set_permissions(readonly) + } + + #[cfg(unix)] + pub fn set_mode(&self, mode: u32) -> VsysResult<()> { + self.inner.set_mode(mode) + } + + #[cfg(not(unix))] + pub fn set_mode(&self, _mode: u32) -> VsysResult<()> { + Ok(()) + } +} + +/// Trait for file handle operations +pub trait FsHandleOps { + fn read(&mut self, buf: &mut [u8]) -> VsysResult; + fn write(&mut self, buf: &[u8]) -> VsysResult; + fn seek(&mut self, pos: SeekFrom) -> VsysResult; + fn sync_all(&self) -> VsysResult<()>; + fn sync_data(&self) -> VsysResult<()>; + fn stat(&self) -> VsysResult; + fn set_len(&self, size: u64) -> VsysResult<()>; + fn set_permissions(&self, readonly: bool) -> VsysResult<()>; + fn set_mode(&self, mode: u32) -> VsysResult<()>; +} + +/// Default file handle implementation using std::fs::File +pub struct StdFsHandle { + file: std::fs::File, +} + +impl StdFsHandle { + pub fn new(file: std::fs::File) -> Self { + Self { file } + } +} + +impl FsHandleOps for StdFsHandle { + fn read(&mut self, buf: &mut [u8]) -> VsysResult { + use std::io::Read; + self.file.read(buf).map_err(Into::into) + } + + fn write(&mut self, buf: &[u8]) -> VsysResult { + use std::io::Write; + self.file.write(buf).map_err(Into::into) + } + + fn seek(&mut self, pos: SeekFrom) -> VsysResult { + use std::io::Seek; + self.file.seek(pos.into()).map_err(Into::into) + } + + fn sync_all(&self) -> VsysResult<()> { + self.file.sync_all().map_err(Into::into) + } + + fn sync_data(&self) -> VsysResult<()> { + self.file.sync_data().map_err(Into::into) + } + + fn stat(&self) -> VsysResult { + let metadata = self.file.metadata()?; + Ok(FileStat::from_metadata(&metadata)) + } + + fn set_len(&self, size: u64) -> VsysResult<()> { + self.file.set_len(size).map_err(Into::into) + } + + fn set_permissions(&self, readonly: bool) -> VsysResult<()> { + let mut perms = self.file.metadata()?.permissions(); + perms.set_readonly(readonly); + self.file.set_permissions(perms).map_err(Into::into) + } + + #[cfg(unix)] + fn set_mode(&self, mode: u32) -> VsysResult<()> { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(mode); + self.file.set_permissions(perms).map_err(Into::into) + } + + #[cfg(not(unix))] + fn set_mode(&self, _mode: u32) -> VsysResult<()> { + Ok(()) + } +} + +/// Filesystem operations vtable +/// +/// All functions are safe Rust function pointers. For C ABI compatibility, +/// wrap these in extern "C" functions when needed. +pub struct FsVTable { + // Read operations + pub read: fn(path: &Path) -> VsysResult>, + pub read_to_string: fn(path: &Path) -> VsysResult, + pub stat: fn(path: &Path) -> VsysResult, + pub lstat: fn(path: &Path) -> VsysResult, + pub read_dir: fn(path: &Path) -> VsysResult>, + pub read_link: fn(path: &Path) -> VsysResult, + pub exists: fn(path: &Path) -> bool, + pub is_file: fn(path: &Path) -> bool, + pub is_dir: fn(path: &Path) -> bool, + + // Write operations + pub write: fn(path: &Path, data: &[u8]) -> VsysResult<()>, + pub append: fn(path: &Path, data: &[u8]) -> VsysResult<()>, + pub create_dir: fn(path: &Path) -> VsysResult<()>, + pub create_dir_all: fn(path: &Path) -> VsysResult<()>, + pub remove_file: fn(path: &Path) -> VsysResult<()>, + pub remove_dir: fn(path: &Path) -> VsysResult<()>, + pub remove_dir_all: fn(path: &Path) -> VsysResult<()>, + pub rename: fn(from: &Path, to: &Path) -> VsysResult<()>, + pub copy: fn(from: &Path, to: &Path) -> VsysResult, + pub symlink: fn(original: &Path, link: &Path) -> VsysResult<()>, + pub truncate: fn(path: &Path, size: u64) -> VsysResult<()>, + + // Access check (F_OK=0, R_OK=4, W_OK=2, X_OK=1) + pub access: fn(path: &Path, mode: u32) -> VsysResult<()>, + + // Temp directory + pub mkdtemp: fn(prefix: &str) -> VsysResult, + + // Permissions + pub set_permissions: fn(path: &Path, readonly: bool) -> VsysResult<()>, + pub set_mode: fn(path: &Path, mode: u32) -> VsysResult<()>, + pub chown: fn(path: &Path, uid: u32, gid: u32) -> VsysResult<()>, + + // Canonicalize + pub canonicalize: fn(path: &Path) -> VsysResult, + + // File handle operations + pub open: fn(path: &Path, options: &OpenOptions) -> VsysResult, +} + +impl Default for FsVTable { + fn default() -> Self { + Self { + // Read operations + read: default_read, + read_to_string: default_read_to_string, + stat: default_stat, + lstat: default_lstat, + read_dir: default_read_dir, + read_link: default_read_link, + exists: default_exists, + is_file: default_is_file, + is_dir: default_is_dir, + + // Write operations + write: default_write, + append: default_append, + create_dir: default_create_dir, + create_dir_all: default_create_dir_all, + remove_file: default_remove_file, + remove_dir: default_remove_dir, + remove_dir_all: default_remove_dir_all, + rename: default_rename, + copy: default_copy, + symlink: default_symlink, + truncate: default_truncate, + + // Access check + access: default_access, + + // Temp directory + mkdtemp: default_mkdtemp, + + // Permissions + set_permissions: default_set_permissions, + set_mode: default_set_mode, + chown: default_chown, + + // Canonicalize + canonicalize: default_canonicalize, + + // File handle + open: default_open, + } + } +} + +impl FsVTable { + /// Create a vtable that denies all operations + pub fn deny_all() -> Self { + Self { + read: |_| Err(VsysError::PermissionDenied("fs read denied".into())), + read_to_string: |_| Err(VsysError::PermissionDenied("fs read denied".into())), + stat: |_| Err(VsysError::PermissionDenied("fs stat denied".into())), + lstat: |_| Err(VsysError::PermissionDenied("fs lstat denied".into())), + read_dir: |_| Err(VsysError::PermissionDenied("fs readdir denied".into())), + read_link: |_| Err(VsysError::PermissionDenied("fs readlink denied".into())), + exists: |_| false, + is_file: |_| false, + is_dir: |_| false, + write: |_, _| Err(VsysError::PermissionDenied("fs write denied".into())), + append: |_, _| Err(VsysError::PermissionDenied("fs append denied".into())), + create_dir: |_| Err(VsysError::PermissionDenied("fs mkdir denied".into())), + create_dir_all: |_| Err(VsysError::PermissionDenied("fs mkdir denied".into())), + remove_file: |_| Err(VsysError::PermissionDenied("fs remove denied".into())), + remove_dir: |_| Err(VsysError::PermissionDenied("fs rmdir denied".into())), + remove_dir_all: |_| Err(VsysError::PermissionDenied("fs rmdir denied".into())), + rename: |_, _| Err(VsysError::PermissionDenied("fs rename denied".into())), + copy: |_, _| Err(VsysError::PermissionDenied("fs copy denied".into())), + symlink: |_, _| Err(VsysError::PermissionDenied("fs symlink denied".into())), + truncate: |_, _| Err(VsysError::PermissionDenied("fs truncate denied".into())), + access: |_, _| Err(VsysError::PermissionDenied("fs access denied".into())), + mkdtemp: |_| Err(VsysError::PermissionDenied("fs mkdtemp denied".into())), + set_permissions: |_, _| Err(VsysError::PermissionDenied("fs chmod denied".into())), + set_mode: |_, _| Err(VsysError::PermissionDenied("fs chmod denied".into())), + chown: |_, _, _| Err(VsysError::PermissionDenied("fs chown denied".into())), + canonicalize: |_| Err(VsysError::PermissionDenied("fs canonicalize denied".into())), + open: |_, _| Err(VsysError::PermissionDenied("fs open denied".into())), + } + } + + /// Create a read-only vtable + pub fn read_only() -> Self { + let mut vtable = Self::default(); + vtable.write = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.append = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.create_dir = |_| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.create_dir_all = |_| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.remove_file = |_| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.remove_dir = |_| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.remove_dir_all = |_| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.rename = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.copy = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.symlink = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.truncate = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.mkdtemp = |_| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.set_permissions = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.set_mode = |_, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable.chown = |_, _, _| Err(VsysError::PermissionDenied("fs is read-only".into())); + vtable + } +} + +// Default implementations using std::fs + +fn default_read(path: &Path) -> VsysResult> { + std::fs::read(path).map_err(Into::into) +} + +fn default_read_to_string(path: &Path) -> VsysResult { + std::fs::read_to_string(path).map_err(Into::into) +} + +fn default_stat(path: &Path) -> VsysResult { + let metadata = std::fs::metadata(path)?; + Ok(FileStat::from_metadata(&metadata)) +} + +fn default_lstat(path: &Path) -> VsysResult { + let metadata = std::fs::symlink_metadata(path)?; + Ok(FileStat::from_metadata(&metadata)) +} + +fn default_read_dir(path: &Path) -> VsysResult> { + let entries = std::fs::read_dir(path)? + .filter_map(|entry| { + let entry = entry.ok()?; + let file_type = entry.file_type().ok()?; + let ft = if file_type.is_file() { + FileType::File + } else if file_type.is_dir() { + FileType::Directory + } else if file_type.is_symlink() { + FileType::Symlink + } else { + FileType::Other + }; + Some(DirEntry { + name: entry.file_name().to_string_lossy().into_owned(), + file_type: ft, + }) + }) + .collect(); + Ok(entries) +} + +fn default_read_link(path: &Path) -> VsysResult { + std::fs::read_link(path).map_err(Into::into) +} + +fn default_exists(path: &Path) -> bool { + path.exists() +} + +fn default_is_file(path: &Path) -> bool { + path.is_file() +} + +fn default_is_dir(path: &Path) -> bool { + path.is_dir() +} + +fn default_write(path: &Path, data: &[u8]) -> VsysResult<()> { + std::fs::write(path, data).map_err(Into::into) +} + +fn default_create_dir(path: &Path) -> VsysResult<()> { + std::fs::create_dir(path).map_err(Into::into) +} + +fn default_create_dir_all(path: &Path) -> VsysResult<()> { + std::fs::create_dir_all(path).map_err(Into::into) +} + +fn default_remove_file(path: &Path) -> VsysResult<()> { + std::fs::remove_file(path).map_err(Into::into) +} + +fn default_remove_dir(path: &Path) -> VsysResult<()> { + std::fs::remove_dir(path).map_err(Into::into) +} + +fn default_remove_dir_all(path: &Path) -> VsysResult<()> { + std::fs::remove_dir_all(path).map_err(Into::into) +} + +fn default_rename(from: &Path, to: &Path) -> VsysResult<()> { + std::fs::rename(from, to).map_err(Into::into) +} + +fn default_copy(from: &Path, to: &Path) -> VsysResult { + std::fs::copy(from, to).map_err(Into::into) +} + +#[cfg(unix)] +fn default_symlink(original: &Path, link: &Path) -> VsysResult<()> { + std::os::unix::fs::symlink(original, link).map_err(Into::into) +} + +#[cfg(windows)] +fn default_symlink(original: &Path, link: &Path) -> VsysResult<()> { + // On Windows, we need to determine if it's a file or directory symlink + if original.is_dir() { + std::os::windows::fs::symlink_dir(original, link).map_err(Into::into) + } else { + std::os::windows::fs::symlink_file(original, link).map_err(Into::into) + } +} + +fn default_set_permissions(path: &Path, readonly: bool) -> VsysResult<()> { + let mut perms = std::fs::metadata(path)?.permissions(); + perms.set_readonly(readonly); + std::fs::set_permissions(path, perms).map_err(Into::into) +} + +#[cfg(unix)] +fn default_set_mode(path: &Path, mode: u32) -> VsysResult<()> { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(mode); + std::fs::set_permissions(path, perms).map_err(Into::into) +} + +#[cfg(not(unix))] +fn default_set_mode(_path: &Path, _mode: u32) -> VsysResult<()> { + // No-op on non-Unix systems + Ok(()) +} + +fn default_canonicalize(path: &Path) -> VsysResult { + std::fs::canonicalize(path).map_err(Into::into) +} + +fn default_append(path: &Path, data: &[u8]) -> VsysResult<()> { + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path)?; + file.write_all(data)?; + Ok(()) +} + +fn default_truncate(path: &Path, size: u64) -> VsysResult<()> { + let file = std::fs::OpenOptions::new().write(true).open(path)?; + file.set_len(size)?; + Ok(()) +} + +fn default_access(path: &Path, mode: u32) -> VsysResult<()> { + // F_OK = 0: Check existence + // R_OK = 4: Check read permission + // W_OK = 2: Check write permission + // X_OK = 1: Check execute permission + const F_OK: u32 = 0; + const R_OK: u32 = 4; + const W_OK: u32 = 2; + const X_OK: u32 = 1; + + let metadata = std::fs::metadata(path)?; + + // F_OK - just check existence (already done by metadata) + if mode == F_OK { + return Ok(()); + } + + let perms = metadata.permissions(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let file_mode = perms.mode(); + let uid = unsafe { libc::getuid() }; + let gid = unsafe { libc::getgid() }; + + // Get file owner info + use std::os::unix::fs::MetadataExt; + let file_uid = metadata.uid(); + let file_gid = metadata.gid(); + + // Determine which permission bits to check (owner, group, or other) + let shift = if uid == file_uid { + 6 // owner bits + } else if gid == file_gid { + 3 // group bits + } else { + 0 // other bits + }; + + if (mode & R_OK) != 0 && (file_mode >> shift) & 4 == 0 { + return Err(VsysError::PermissionDenied("read permission denied".into())); + } + if (mode & W_OK) != 0 && (file_mode >> shift) & 2 == 0 { + return Err(VsysError::PermissionDenied( + "write permission denied".into(), + )); + } + if (mode & X_OK) != 0 && (file_mode >> shift) & 1 == 0 { + return Err(VsysError::PermissionDenied( + "execute permission denied".into(), + )); + } + } + + #[cfg(not(unix))] + { + // On Windows, just check readonly for write access + if (mode & W_OK) != 0 && perms.readonly() { + return Err(VsysError::PermissionDenied( + "write permission denied".into(), + )); + } + // X_OK doesn't apply meaningfully on Windows + } + + Ok(()) +} + +fn default_mkdtemp(prefix: &str) -> VsysResult { + use std::env; + let temp_dir = env::temp_dir(); + let unique_name = format!("{}{}", prefix, uuid::Uuid::new_v4().simple()); + let dir_path = temp_dir.join(unique_name); + std::fs::create_dir_all(&dir_path)?; + Ok(dir_path) +} + +#[cfg(unix)] +fn default_chown(path: &Path, uid: u32, gid: u32) -> VsysResult<()> { + use std::os::unix::ffi::OsStrExt; + let c_path = std::ffi::CString::new(path.as_os_str().as_bytes()) + .map_err(|_| VsysError::Custom("invalid path".into()))?; + let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) }; + if result == 0 { + Ok(()) + } else { + Err(std::io::Error::last_os_error().into()) + } +} + +#[cfg(not(unix))] +fn default_chown(_path: &Path, _uid: u32, _gid: u32) -> VsysResult<()> { + // No-op on non-Unix systems + Ok(()) +} + +fn default_open(path: &Path, options: &OpenOptions) -> VsysResult { + let mut std_options = std::fs::OpenOptions::new(); + std_options + .read(options.read) + .write(options.write) + .append(options.append) + .truncate(options.truncate) + .create(options.create) + .create_new(options.create_new); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + if options.mode != 0 { + std_options.mode(options.mode); + } + } + + let file = std_options.open(path)?; + Ok(FsHandle::new(StdFsHandle::new(file))) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_default_fs_read_write() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test.txt"); + + let vtable = FsVTable::default(); + + // Write + (vtable.write)(&file_path, b"hello world").unwrap(); + + // Read + let data = (vtable.read)(&file_path).unwrap(); + assert_eq!(data, b"hello world"); + + // Read to string + let text = (vtable.read_to_string)(&file_path).unwrap(); + assert_eq!(text, "hello world"); + + // Stat + let stat = (vtable.stat)(&file_path).unwrap(); + assert!(stat.is_file()); + assert_eq!(stat.size, 11); + } + + #[test] + fn test_deny_all_fs() { + let vtable = FsVTable::deny_all(); + + let result = (vtable.read)(Path::new("/tmp/test")); + assert!(result.is_err()); + + assert!(!(vtable.exists)(Path::new("/tmp"))); + } + + #[test] + fn test_read_only_fs() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test.txt"); + + // Create file first + std::fs::write(&file_path, b"test").unwrap(); + + let vtable = FsVTable::read_only(); + + // Read should work + let data = (vtable.read)(&file_path).unwrap(); + assert_eq!(data, b"test"); + + // Write should fail + let result = (vtable.write)(&file_path, b"new data"); + assert!(result.is_err()); + } + + #[test] + fn test_append() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("append_test.txt"); + + let vtable = FsVTable::default(); + + // Write initial content + (vtable.write)(&file_path, b"hello").unwrap(); + + // Append more content + (vtable.append)(&file_path, b" world").unwrap(); + + // Read and verify + let data = (vtable.read_to_string)(&file_path).unwrap(); + assert_eq!(data, "hello world"); + } + + #[test] + fn test_truncate() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("truncate_test.txt"); + + let vtable = FsVTable::default(); + + // Write content + (vtable.write)(&file_path, b"hello world").unwrap(); + + // Truncate to 5 bytes + (vtable.truncate)(&file_path, 5).unwrap(); + + // Read and verify + let data = (vtable.read)(&file_path).unwrap(); + assert_eq!(data, b"hello"); + } + + #[test] + fn test_access() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("access_test.txt"); + + let vtable = FsVTable::default(); + + // File doesn't exist yet + assert!((vtable.access)(&file_path, 0).is_err()); + + // Create file + (vtable.write)(&file_path, b"test").unwrap(); + + // F_OK should succeed now + assert!((vtable.access)(&file_path, 0).is_ok()); + } + + #[test] + fn test_mkdtemp() { + let vtable = FsVTable::default(); + + let temp_dir = (vtable.mkdtemp)("xmas_test_").unwrap(); + + // Directory should exist + assert!(temp_dir.is_dir()); + assert!(temp_dir + .file_name() + .unwrap() + .to_str() + .unwrap() + .starts_with("xmas_test_")); + + // Cleanup + std::fs::remove_dir_all(&temp_dir).unwrap(); + } + + #[test] + fn test_open_and_handle() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("handle_test.txt"); + + let vtable = FsVTable::default(); + + // Open for writing + let options = OpenOptions::new().write(true).create(true); + let mut handle = (vtable.open)(&file_path, &options).unwrap(); + + // Write through handle + handle.write(b"hello from handle").unwrap(); + handle.sync_all().unwrap(); + + // Open for reading + let options = OpenOptions::new().read(true); + let mut handle = (vtable.open)(&file_path, &options).unwrap(); + + // Read through handle + let mut buf = vec![0u8; 100]; + let n = handle.read(&mut buf).unwrap(); + assert_eq!(&buf[..n], b"hello from handle"); + + // Stat through handle + let stat = handle.stat().unwrap(); + assert_eq!(stat.size, 17); + } +} diff --git a/vsys/src/lib.rs b/vsys/src/lib.rs new file mode 100644 index 0000000..24e6558 --- /dev/null +++ b/vsys/src/lib.rs @@ -0,0 +1,156 @@ +//! # vsys - Virtual System Layer for Xmas.JS +//! +//! `vsys` provides a pluggable abstraction layer for all system-level operations, +//! enabling sandboxed execution, custom filesystem/network implementations, and +//! fine-grained permission control. +//! +//! ## Design Goals +//! +//! - **C ABI compatible**: All function pointers use `extern "C"` for FFI compatibility +//! - **Runtime swappable**: Change implementation at runtime +//! - **Zero-cost when static**: Compiler can inline when implementation is known +//! - **No trait objects**: Avoids dynamic dispatch overhead +//! +//! ## Usage +//! +//! ```rust,ignore +//! // Default: Use real filesystem and network +//! let vsys = Vsys::default(); +//! +//! // Sandboxed: Custom implementations +//! let vsys = Vsys::builder() +//! .fs(custom_fs_vtable()) +//! .permissions(restricted_permissions()) +//! .build(); +//! ``` + +pub mod error; +pub mod fs; +pub mod module_loader; +pub mod permissions; + +use std::sync::Arc; + +pub use error::{VsysError, VsysResult}; +pub use fs::FsVTable; +pub use module_loader::ModuleLoaderVTable; +pub use permissions::{BlackOrWhiteList, Permissions}; + +/// The main vsys context that holds all virtual system tables. +/// +/// This is the central point for all system operations. It can be stored +/// in the JS runtime context and accessed by all modules. +#[derive(Clone)] +pub struct Vsys { + /// Filesystem operations vtable + pub fs: Arc, + /// Module loader/resolver vtable + pub module_loader: Arc, + /// Permissions configuration + pub permissions: Permissions, +} + +impl Default for Vsys { + fn default() -> Self { + Self { + fs: Arc::new(FsVTable::default()), + module_loader: Arc::new(ModuleLoaderVTable::default()), + permissions: Permissions::allow_all(), + } + } +} + +impl Vsys { + /// Create a new Vsys with default (real system) implementations + pub fn new() -> Self { + Self::default() + } + + /// Create a new builder for customizing Vsys + pub fn builder() -> VsysBuilder { + VsysBuilder::default() + } + + /// Create a sandboxed Vsys with no permissions + pub fn sandboxed() -> Self { + Self { + fs: Arc::new(FsVTable::deny_all()), + module_loader: Arc::new(ModuleLoaderVTable::default()), + permissions: Permissions::default(), // deny all by default + } + } + + /// Get a reference to the filesystem vtable + #[inline] + pub fn fs(&self) -> &FsVTable { + &self.fs + } + + /// Get a reference to the module loader vtable + #[inline] + pub fn module_loader(&self) -> &ModuleLoaderVTable { + &self.module_loader + } + + /// Get a reference to the permissions configuration + #[inline] + pub fn permissions(&self) -> &Permissions { + &self.permissions + } +} + +/// Builder for constructing a customized Vsys instance +#[derive(Default)] +pub struct VsysBuilder { + fs: Option, + module_loader: Option, + permissions: Option, +} + +impl VsysBuilder { + pub fn fs(mut self, fs: FsVTable) -> Self { + self.fs = Some(fs); + self + } + + pub fn module_loader(mut self, loader: ModuleLoaderVTable) -> Self { + self.module_loader = Some(loader); + self + } + + pub fn permissions(mut self, permissions: Permissions) -> Self { + self.permissions = Some(permissions); + self + } + + pub fn build(self) -> Vsys { + Vsys { + fs: Arc::new(self.fs.unwrap_or_default()), + module_loader: Arc::new(self.module_loader.unwrap_or_default()), + permissions: self.permissions.unwrap_or_else(Permissions::allow_all), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_vsys() { + let vsys = Vsys::default(); + assert!(vsys.permissions.stdio); + } + + #[test] + fn test_sandboxed_vsys() { + let vsys = Vsys::sandboxed(); + assert!(!vsys.permissions.stdio); + } + + #[test] + fn test_builder() { + let vsys = Vsys::builder().permissions(Permissions::default()).build(); + assert!(!vsys.permissions.stdio); + } +} diff --git a/vsys/src/module_loader.rs b/vsys/src/module_loader.rs new file mode 100644 index 0000000..80fdc68 --- /dev/null +++ b/vsys/src/module_loader.rs @@ -0,0 +1,564 @@ +//! Module loader virtual table for vsys +//! +//! This module provides a pluggable module loading/resolution abstraction. +//! The module loader uses the vsys FsVTable for all filesystem operations, +//! making it fully virtualizable for sandboxed environments. +//! +//! # Design +//! +//! The module loader vtable takes a reference to the parent Vsys for all operations. +//! This allows the loader to: +//! - Use the virtual filesystem (FsVTable) for file operations +//! - Check permissions before loading modules +//! - Support custom module sources (bundled, remote, in-memory) + +use std::path::{Path, PathBuf}; + +use crate::error::{VsysError, VsysResult}; +use crate::fs::FsVTable; + +/// Module format +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModuleFormat { + /// ECMAScript module (import/export) + ESM, + /// CommonJS module (require/module.exports) + CJS, + /// JSON file + Json, + /// Binary/bytecode + Binary, +} + +/// Resolved module information +#[derive(Debug, Clone)] +pub struct ResolvedModule { + /// Resolved absolute path or URL + pub path: String, + /// Module format + pub format: ModuleFormat, + /// Whether this is a built-in/native module + pub is_builtin: bool, + /// Whether this is a CommonJS module that needs wrapping for ESM + pub needs_cjs_wrapper: bool, +} + +/// Loaded module source +#[derive(Debug, Clone)] +pub struct ModuleSource { + /// Module source code or binary + pub source: Vec, + /// Module format + pub format: ModuleFormat, + /// Original path/URL + pub path: String, +} + +/// Module loader/resolver vtable +/// +/// This provides the core module loading functionality that can be customized. +/// All functions receive a reference to `FsVTable` to perform filesystem operations, +/// ensuring the module loader respects the virtual filesystem abstraction. +/// +/// # C ABI Compatibility +/// +/// All function pointers use simple types and can be safely called from C. +/// The `FsVTable` pointer allows the loader to perform filesystem operations +/// through the virtual layer. +pub struct ModuleLoaderVTable { + /// Resolve a module specifier to an absolute path + /// + /// # Arguments + /// * `fs` - The filesystem vtable to use for file operations + /// * `specifier` - The import specifier (e.g., "./foo", "lodash", "node:fs") + /// * `referrer` - The path of the module doing the import + /// * `is_esm` - Whether this is an ESM import (vs CommonJS require) + /// + /// # Returns + /// Resolved module information or error + pub resolve: fn( + fs: &FsVTable, + specifier: &str, + referrer: &str, + is_esm: bool, + ) -> VsysResult, + + /// Load a module's source code + /// + /// # Arguments + /// * `fs` - The filesystem vtable to use for file operations + /// * `path` - The resolved path from `resolve` + /// + /// # Returns + /// Module source or error + pub load: fn(fs: &FsVTable, path: &str) -> VsysResult, + + /// Check if a module exists at the given path + /// + /// # Arguments + /// * `fs` - The filesystem vtable to use for file operations + /// * `path` - The path to check + pub exists: fn(fs: &FsVTable, path: &str) -> bool, + + /// Check if a specifier is a built-in module + pub is_builtin: fn(specifier: &str) -> bool, + + /// List all built-in module names + pub list_builtins: fn() -> Vec, + + /// Find the closest package.json from a directory + /// + /// # Arguments + /// * `fs` - The filesystem vtable to use for file operations + /// * `start_dir` - The directory to start searching from + /// + /// # Returns + /// Path to package.json if found + pub find_package_json: fn(fs: &FsVTable, start_dir: &str) -> Option, + + /// Read and parse package.json + /// + /// # Arguments + /// * `fs` - The filesystem vtable to use for file operations + /// * `path` - Path to package.json + /// + /// # Returns + /// Parsed package.json as JSON value + pub read_package_json: fn(fs: &FsVTable, path: &str) -> VsysResult, +} + +impl Default for ModuleLoaderVTable { + fn default() -> Self { + Self { + resolve: default_resolve, + load: default_load, + exists: default_exists, + is_builtin: default_is_builtin, + list_builtins: default_list_builtins, + find_package_json: default_find_package_json, + read_package_json: default_read_package_json, + } + } +} + +impl ModuleLoaderVTable { + /// Create a loader that only allows built-in modules + pub fn builtins_only() -> Self { + Self { + resolve: builtins_only_resolve, + load: builtins_only_load, + exists: |_, _| false, + is_builtin: default_is_builtin, + list_builtins: default_list_builtins, + find_package_json: |_, _| None, + read_package_json: |_, _| { + Err(VsysError::ModuleResolution { + specifier: String::new(), + message: "Filesystem access not allowed".to_string(), + }) + }, + } + } +} + +// Supported file extensions +const JS_EXTENSIONS: &[&str] = &[".js", ".mjs", ".cjs"]; +#[allow(dead_code)] +const TS_EXTENSIONS: &[&str] = &[".ts", ".mts", ".cts", ".tsx", ".jsx"]; +const ALL_EXTENSIONS: &[&str] = &[ + ".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx", ".jsx", ".json", +]; + +// Built-in modules (node: prefix) +const BUILTIN_MODULES: &[&str] = &[ + "assert", + "async_hooks", + "buffer", + "child_process", + "cluster", + "console", + "constants", + "crypto", + "dgram", + "dns", + "domain", + "events", + "fs", + "fs/promises", + "http", + "http2", + "https", + "inspector", + "module", + "net", + "os", + "path", + "perf_hooks", + "process", + "punycode", + "querystring", + "readline", + "repl", + "stream", + "stream/web", + "string_decoder", + "sys", + "timers", + "timers/promises", + "tls", + "trace_events", + "tty", + "url", + "util", + "v8", + "vm", + "wasi", + "worker_threads", + "zlib", +]; + +fn default_is_builtin(specifier: &str) -> bool { + let name = specifier.strip_prefix("node:").unwrap_or(specifier); + BUILTIN_MODULES.contains(&name) +} + +fn default_list_builtins() -> Vec { + BUILTIN_MODULES.iter().map(|s| s.to_string()).collect() +} + +fn default_resolve( + fs: &FsVTable, + specifier: &str, + referrer: &str, + is_esm: bool, +) -> VsysResult { + // Handle node: prefix + if specifier.starts_with("node:") || default_is_builtin(specifier) { + let name = specifier.strip_prefix("node:").unwrap_or(specifier); + return Ok(ResolvedModule { + path: name.to_string(), + format: ModuleFormat::ESM, + is_builtin: true, + needs_cjs_wrapper: false, + }); + } + + // Handle file:// URLs + let specifier = specifier.strip_prefix("file://").unwrap_or(specifier); + + // Determine if relative or bare specifier + let is_relative = + specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/'); + + if is_relative { + // Resolve relative to referrer + let referrer_path = Path::new(referrer); + let base_dir = referrer_path.parent().unwrap_or(Path::new(".")); + let resolved = base_dir.join(specifier); + + // Try to resolve with extensions + if let Some((path, format, is_cjs)) = try_resolve_file(fs, &resolved, is_esm) { + return Ok(ResolvedModule { + path: path.to_string_lossy().into_owned(), + format, + is_builtin: false, + needs_cjs_wrapper: is_cjs && is_esm, + }); + } + + return Err(VsysError::ModuleResolution { + specifier: specifier.to_string(), + message: format!("Cannot find module '{}'", specifier), + }); + } + + // Bare specifier - try node_modules resolution + if let Some((path, format, is_cjs)) = try_resolve_node_modules(fs, specifier, referrer, is_esm) + { + return Ok(ResolvedModule { + path: path.to_string_lossy().into_owned(), + format, + is_builtin: false, + needs_cjs_wrapper: is_cjs && is_esm, + }); + } + + Err(VsysError::ModuleResolution { + specifier: specifier.to_string(), + message: format!("Cannot find package '{}'", specifier), + }) +} + +/// Check if a path is a file using the virtual fs +fn is_file(fs: &FsVTable, path: &Path) -> bool { + (fs.is_file)(path) +} + +/// Check if a path is a directory using the virtual fs +fn is_dir(fs: &FsVTable, path: &Path) -> bool { + (fs.is_dir)(path) +} + +/// Check if a path exists using the virtual fs +fn path_exists(fs: &FsVTable, path: &Path) -> bool { + (fs.exists)(path) +} + +fn try_resolve_file( + fs: &FsVTable, + path: &Path, + _is_esm: bool, +) -> Option<(PathBuf, ModuleFormat, bool)> { + // Try exact path + if is_file(fs, path) { + let format = detect_format(path); + let is_cjs = matches!(format, ModuleFormat::CJS); + return Some((path.to_path_buf(), format, is_cjs)); + } + + // Try with extensions + for ext in ALL_EXTENSIONS { + let with_ext = path.with_extension(&ext[1..]); // Remove leading dot + if is_file(fs, &with_ext) { + let format = detect_format(&with_ext); + let is_cjs = matches!(format, ModuleFormat::CJS); + return Some((with_ext, format, is_cjs)); + } + } + + // Try as directory with index + if is_dir(fs, path) { + for ext in ALL_EXTENSIONS { + let index = path.join(format!("index{}", ext)); + if is_file(fs, &index) { + let format = detect_format(&index); + let is_cjs = matches!(format, ModuleFormat::CJS); + return Some((index, format, is_cjs)); + } + } + } + + None +} + +fn try_resolve_node_modules( + fs: &FsVTable, + specifier: &str, + referrer: &str, + is_esm: bool, +) -> Option<(PathBuf, ModuleFormat, bool)> { + let referrer_path = Path::new(referrer); + let mut current = referrer_path.parent(); + + while let Some(dir) = current { + let node_modules = dir.join("node_modules").join(specifier); + + // Try package.json main field + let package_json = node_modules.join("package.json"); + if is_file(fs, &package_json) { + if let Ok(content) = (fs.read)(&package_json) { + if let Ok(json) = serde_json::from_slice::(&content) { + // Determine if CJS based on type field + let is_cjs = json + .get("type") + .and_then(|t| t.as_str()) + .map(|t| t != "module") + .unwrap_or(true); + + // Try "exports", "module", "main" fields in order + let main_field = if is_esm { + json.get("exports") + .and_then(|e| e.get(".")) + .and_then(|d| d.get("import")) + .or_else(|| json.get("module")) + .or_else(|| json.get("main")) + .and_then(|v| v.as_str()) + } else { + json.get("exports") + .and_then(|e| e.get(".")) + .and_then(|d| d.get("require")) + .or_else(|| json.get("main")) + .and_then(|v| v.as_str()) + }; + + if let Some(main) = main_field { + let main_path = node_modules.join(main); + if let Some((resolved, format, _)) = + try_resolve_file(fs, &main_path, is_esm) + { + return Some((resolved, format, is_cjs)); + } + } + + // Try index.js as fallback + for ext in JS_EXTENSIONS { + let index = node_modules.join(format!("index{}", ext)); + if is_file(fs, &index) { + let format = detect_format(&index); + return Some((index, format, is_cjs)); + } + } + } + } + } + + // Try direct file resolution + if let Some(resolved) = try_resolve_file(fs, &node_modules, is_esm) { + return Some(resolved); + } + + current = dir.parent(); + } + + None +} + +fn detect_format(path: &Path) -> ModuleFormat { + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + match ext { + "mjs" | "mts" => ModuleFormat::ESM, + "cjs" | "cts" => ModuleFormat::CJS, + "json" => ModuleFormat::Json, + "js" | "ts" | "tsx" | "jsx" => { + // Default to ESM for now + // In production, should check package.json type field + ModuleFormat::ESM + } + _ => ModuleFormat::Binary, + } +} + +fn default_load(fs: &FsVTable, path: &str) -> VsysResult { + // Built-in modules are handled separately + if default_is_builtin(path) { + return Err(VsysError::ModuleLoad { + path: path.to_string(), + message: "Built-in modules should be loaded by the runtime".to_string(), + }); + } + + let path_obj = Path::new(path); + let source = (fs.read)(path_obj)?; + let format = detect_format(path_obj); + + Ok(ModuleSource { + source, + format, + path: path.to_string(), + }) +} + +fn default_exists(fs: &FsVTable, path: &str) -> bool { + path_exists(fs, Path::new(path)) +} + +fn default_find_package_json(fs: &FsVTable, start_dir: &str) -> Option { + let mut current_dir = PathBuf::from(start_dir); + loop { + let package_json_path = current_dir.join("package.json"); + if path_exists(fs, &package_json_path) { + return Some(package_json_path.to_string_lossy().into_owned()); + } + if !current_dir.pop() { + break; + } + } + None +} + +fn default_read_package_json(fs: &FsVTable, path: &str) -> VsysResult { + let path_obj = Path::new(path); + let content = (fs.read)(path_obj)?; + serde_json::from_slice(&content).map_err(|e| VsysError::ModuleLoad { + path: path.to_string(), + message: format!("Failed to parse package.json: {}", e), + }) +} + +fn builtins_only_resolve( + fs: &FsVTable, + specifier: &str, + _referrer: &str, + _is_esm: bool, +) -> VsysResult { + let _ = fs; // unused in builtins-only mode + if default_is_builtin(specifier) { + let name = specifier.strip_prefix("node:").unwrap_or(specifier); + return Ok(ResolvedModule { + path: name.to_string(), + format: ModuleFormat::ESM, + is_builtin: true, + needs_cjs_wrapper: false, + }); + } + + Err(VsysError::ModuleResolution { + specifier: specifier.to_string(), + message: "Only built-in modules are allowed".to_string(), + }) +} + +fn builtins_only_load(fs: &FsVTable, path: &str) -> VsysResult { + let _ = fs; // unused in builtins-only mode + Err(VsysError::ModuleLoad { + path: path.to_string(), + message: "Only built-in modules are allowed".to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fs::FsVTable; + + #[test] + fn test_is_builtin() { + assert!(default_is_builtin("fs")); + assert!(default_is_builtin("node:fs")); + assert!(default_is_builtin("path")); + assert!(!default_is_builtin("lodash")); + assert!(!default_is_builtin("./foo")); + } + + #[test] + fn test_resolve_builtin() { + let vtable = ModuleLoaderVTable::default(); + let fs = FsVTable::default(); + let result = (vtable.resolve)(&fs, "node:fs", "/app/index.js", true).unwrap(); + assert!(result.is_builtin); + assert_eq!(result.path, "fs"); + } + + #[test] + fn test_builtins_only() { + let vtable = ModuleLoaderVTable::builtins_only(); + let fs = FsVTable::default(); + + // Built-in should work + let result = (vtable.resolve)(&fs, "fs", "/app/index.js", true); + assert!(result.is_ok()); + + // Non-builtin should fail + let result = (vtable.resolve)(&fs, "./foo", "/app/index.js", true); + assert!(result.is_err()); + } + + #[test] + fn test_detect_format() { + assert_eq!(detect_format(Path::new("foo.mjs")), ModuleFormat::ESM); + assert_eq!(detect_format(Path::new("foo.cjs")), ModuleFormat::CJS); + assert_eq!(detect_format(Path::new("foo.json")), ModuleFormat::Json); + assert_eq!(detect_format(Path::new("foo.js")), ModuleFormat::ESM); + } + + #[test] + fn test_resolved_module_cjs_wrapper() { + let resolved = ResolvedModule { + path: "/app/lib.cjs".to_string(), + format: ModuleFormat::CJS, + is_builtin: false, + needs_cjs_wrapper: true, + }; + assert!(resolved.needs_cjs_wrapper); + } +} diff --git a/vsys/src/permissions.rs b/vsys/src/permissions.rs new file mode 100644 index 0000000..976d1c8 --- /dev/null +++ b/vsys/src/permissions.rs @@ -0,0 +1,226 @@ +//! Permissions configuration for vsys +//! +//! This module provides fine-grained permission control for filesystem, +//! network, and environment access. + +use std::path::{Path, PathBuf}; + +/// Black or white list for permission checking +#[derive(Debug, PartialEq, Clone, Hash, Eq)] +pub enum BlackOrWhiteList { + /// Allow all except items in the list + BlackList(Vec), + /// Deny all except items in the list (default: empty = deny all) + WhiteList(Vec), +} + +impl Default for BlackOrWhiteList { + fn default() -> Self { + // Default to deny all (empty whitelist) + Self::WhiteList(vec![]) + } +} + +impl BlackOrWhiteList { + /// Create a blacklist (allow all except listed) + pub fn blacklist(items: Vec) -> Self { + Self::BlackList(items) + } + + /// Create a whitelist (deny all except listed) + pub fn whitelist(items: Vec) -> Self { + Self::WhiteList(items) + } + + /// Allow all (empty blacklist) + pub fn allow_all() -> Self { + Self::BlackList(vec![]) + } + + /// Deny all (empty whitelist) + pub fn deny_all() -> Self { + Self::WhiteList(vec![]) + } + + /// Check if a path is allowed + pub fn check_path(&self, path: &Path) -> bool { + let canonical_path = match path.canonicalize() { + Ok(p) => p, + Err(_) => return false, + }; + + let (is_whitelist, items) = match self { + BlackOrWhiteList::BlackList(items) => (false, items), + BlackOrWhiteList::WhiteList(items) => (true, items), + }; + + // Separate pattern paths (ending with *) and normal paths + let mut normal_paths = Vec::new(); + let mut pattern_paths = Vec::new(); + + for item in items { + if item.ends_with('*') { + let pattern = &item[..item.len() - 1]; + if let Ok(p) = Path::new(pattern).canonicalize() { + pattern_paths.push(p); + } else { + pattern_paths.push(PathBuf::from(pattern)); + } + } else if let Ok(p) = Path::new(item).canonicalize() { + normal_paths.push(p); + } else { + normal_paths.push(PathBuf::from(item)); + } + } + + // Check pattern paths (directory prefixes) + for pattern in &pattern_paths { + if canonical_path.starts_with(pattern) { + return is_whitelist; + } + } + + // Check exact paths + let found = normal_paths.iter().any(|p| p == &canonical_path); + + if is_whitelist { + found + } else { + !found + } + } + + /// Check if a host/URL is allowed + pub fn check_host(&self, host: &str) -> bool { + let (is_whitelist, items) = match self { + BlackOrWhiteList::BlackList(items) => (false, items), + BlackOrWhiteList::WhiteList(items) => (true, items), + }; + + // Check for wildcard patterns + for item in items { + if item.starts_with("*.") { + // Wildcard subdomain match + let suffix = &item[1..]; // ".example.com" + if host.ends_with(suffix) || host == &item[2..] { + return is_whitelist; + } + } else if item == host { + return is_whitelist; + } + } + + // No match found + !is_whitelist + } +} + +/// Struct representing permissions for filesystem, network, and environment access. +/// +/// **WARNING**: by default, no permissions are granted (all whitelists are empty). +#[derive(Debug, Clone, Default)] +pub struct Permissions { + /// Filesystem access permissions + pub fs: BlackOrWhiteList, + /// Network access permissions + pub net: BlackOrWhiteList, + /// Environment variable access permissions + pub env: BlackOrWhiteList, + /// Standard I/O (console) access + pub stdio: bool, +} + +impl Permissions { + /// Create permissions that allow everything + pub fn allow_all() -> Self { + Self { + fs: BlackOrWhiteList::allow_all(), + net: BlackOrWhiteList::allow_all(), + env: BlackOrWhiteList::allow_all(), + stdio: true, + } + } + + /// Create permissions that deny everything (default) + pub fn deny_all() -> Self { + Self::default() + } + + /// Check if filesystem access to path is allowed + pub fn check_fs(&self, path: &Path) -> bool { + self.fs.check_path(path) + } + + /// Check if network access to host is allowed + pub fn check_net(&self, host: &str) -> bool { + self.net.check_host(host) + } + + /// Check if environment variable access is allowed + pub fn check_env(&self, var_name: &str) -> bool { + let (is_whitelist, items) = match &self.env { + BlackOrWhiteList::BlackList(items) => (false, items), + BlackOrWhiteList::WhiteList(items) => (true, items), + }; + + let found = items.iter().any(|item| item == var_name); + + if is_whitelist { + found + } else { + !found + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_allow_all_permissions() { + let perm = Permissions::allow_all(); + assert!(perm.stdio); + assert!(perm.check_net("example.com")); + assert!(perm.check_env("PATH")); + } + + #[test] + fn test_deny_all_permissions() { + let perm = Permissions::deny_all(); + assert!(!perm.stdio); + assert!(!perm.check_net("example.com")); + assert!(!perm.check_env("PATH")); + } + + #[test] + fn test_whitelist_net() { + let perm = Permissions { + net: BlackOrWhiteList::whitelist(vec!["api.example.com".to_string()]), + ..Default::default() + }; + assert!(perm.check_net("api.example.com")); + assert!(!perm.check_net("other.com")); + } + + #[test] + fn test_blacklist_net() { + let perm = Permissions { + net: BlackOrWhiteList::blacklist(vec!["evil.com".to_string()]), + ..Default::default() + }; + assert!(!perm.check_net("evil.com")); + assert!(perm.check_net("good.com")); + } + + #[test] + fn test_wildcard_net() { + let perm = Permissions { + net: BlackOrWhiteList::whitelist(vec!["*.example.com".to_string()]), + ..Default::default() + }; + assert!(perm.check_net("api.example.com")); + assert!(perm.check_net("example.com")); + assert!(!perm.check_net("other.com")); + } +}