diff --git a/Cargo.toml b/Cargo.toml index 38f04c5ae7..4677e4e494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,7 +199,7 @@ nix = "=0.26.2" fwdansi = "=1.1.0" junction = "=0.2.0" winapi = "=0.3.9" -windows-sys = { version = "0.48.0", features = ["Win32_Media"] } +windows-sys = { version = "0.48.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem"] } winres = "=0.1.12" # NB: the `bench` and `release` profiles must remain EXACTLY the same. diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f8c9dfc883..f4541f8866 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -255,6 +255,7 @@ deno_core::extension!(deno_node, ops::fs::op_node_fs_exists_sync

, ops::fs::op_node_cp_sync

, ops::fs::op_node_cp

, + ops::fs::op_node_statfs

, ops::winerror::op_node_sys_to_uv_error, ops::v8::op_v8_cached_data_version_tag, ops::v8::op_v8_get_heap_statistics, @@ -373,6 +374,7 @@ deno_core::extension!(deno_node, "_fs/_fs_rm.ts", "_fs/_fs_rmdir.ts", "_fs/_fs_stat.ts", + "_fs/_fs_statfs.js", "_fs/_fs_symlink.ts", "_fs/_fs_truncate.ts", "_fs/_fs_unlink.ts", diff --git a/ext/node/ops/fs.rs b/ext/node/ops/fs.rs index c5ae2371e2..28d95eabe5 100644 --- a/ext/node/ops/fs.rs +++ b/ext/node/ops/fs.rs @@ -9,6 +9,7 @@ use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_fs::FileSystemRc; +use serde::Serialize; use crate::NodePermissions; @@ -78,3 +79,139 @@ where fs.cp_async(path, new_path).await?; Ok(()) } + +#[derive(Debug, Serialize)] +pub struct StatFs { + #[serde(rename = "type")] + pub typ: u64, + pub bsize: u64, + pub blocks: u64, + pub bfree: u64, + pub bavail: u64, + pub files: u64, + pub ffree: u64, +} + +#[op2] +#[serde] +pub fn op_node_statfs

( + state: Rc>, + #[string] path: String, + bigint: bool, +) -> Result +where + P: NodePermissions + 'static, +{ + { + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_read_with_api_name(Path::new(&path), Some("node:fs.statfs"))?; + state + .borrow_mut::

() + .check_sys("statfs", "node:fs.statfs")?; + } + #[cfg(unix)] + { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let path = OsStr::new(&path); + let mut cpath = path.as_bytes().to_vec(); + cpath.push(0); + if bigint { + #[cfg(not(target_os = "macos"))] + // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. + let (code, result) = unsafe { + let mut result: libc::statfs64 = std::mem::zeroed(); + (libc::statfs64(cpath.as_ptr() as _, &mut result), result) + }; + #[cfg(target_os = "macos")] + // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. + let (code, result) = unsafe { + let mut result: libc::statfs = std::mem::zeroed(); + (libc::statfs(cpath.as_ptr() as _, &mut result), result) + }; + if code == -1 { + return Err(std::io::Error::last_os_error().into()); + } + Ok(StatFs { + typ: result.f_type as _, + bsize: result.f_bsize as _, + blocks: result.f_blocks as _, + bfree: result.f_bfree as _, + bavail: result.f_bavail as _, + files: result.f_files as _, + ffree: result.f_ffree as _, + }) + } else { + // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. + let (code, result) = unsafe { + let mut result: libc::statfs = std::mem::zeroed(); + (libc::statfs(cpath.as_ptr() as _, &mut result), result) + }; + if code == -1 { + return Err(std::io::Error::last_os_error().into()); + } + Ok(StatFs { + typ: result.f_type as _, + bsize: result.f_bsize as _, + blocks: result.f_blocks as _, + bfree: result.f_bfree as _, + bavail: result.f_bavail as _, + files: result.f_files as _, + ffree: result.f_ffree as _, + }) + } + } + #[cfg(windows)] + { + use deno_core::anyhow::anyhow; + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceW; + + let _ = bigint; + // Using a vfs here doesn't make sense, it won't align with the windows API + // call below. + #[allow(clippy::disallowed_methods)] + let path = Path::new(&path).canonicalize()?; + let root = path + .ancestors() + .last() + .ok_or(anyhow!("Path has no root."))?; + let root = OsStr::new(root).encode_wide().collect::>(); + let mut sectors_per_cluster = 0; + let mut bytes_per_sector = 0; + let mut available_clusters = 0; + let mut total_clusters = 0; + // SAFETY: Normal GetDiskFreeSpaceW usage. + let code = unsafe { + GetDiskFreeSpaceW( + root.as_ptr(), + &mut sectors_per_cluster, + &mut bytes_per_sector, + &mut available_clusters, + &mut total_clusters, + ) + }; + if code == 0 { + return Err(std::io::Error::last_os_error().into()); + } + Ok(StatFs { + typ: 0, + bsize: (bytes_per_sector * sectors_per_cluster) as _, + blocks: total_clusters as _, + bfree: available_clusters as _, + bavail: available_clusters as _, + files: 0, + ffree: 0, + }) + } + #[cfg(not(any(unix, windows)))] + { + let _ = path; + let _ = bigint; + Err(anyhow!("Unsupported platform.")) + } +} diff --git a/ext/node/polyfills/_fs/_fs_statfs.js b/ext/node/polyfills/_fs/_fs_statfs.js new file mode 100644 index 0000000000..51da1ed684 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_statfs.js @@ -0,0 +1,56 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { BigInt } from "ext:deno_node/internal/primordials.mjs"; +import { op_node_statfs } from "ext:core/ops"; +import { promisify } from "ext:deno_node/internal/util.mjs"; + +class StatFs { + type; + bsize; + blocks; + bfree; + bavail; + files; + ffree; + constructor(type, bsize, blocks, bfree, bavail, files, ffree) { + this.type = type; + this.bsize = bsize; + this.blocks = blocks; + this.bfree = bfree; + this.bavail = bavail; + this.files = files; + this.ffree = ffree; + } +} + +export function statfs(path, options, callback) { + if (typeof options === "function") { + callback = options; + options = {}; + } + try { + const res = statfsSync(path, options); + callback(null, res); + } catch (err) { + callback(err, null); + } +} + +export function statfsSync(path, options) { + const bigint = typeof options?.bigint === "boolean" ? options.bigint : false; + const statFs = op_node_statfs( + path, + bigint, + ); + return new StatFs( + bigint ? BigInt(statFs.type) : statFs.type, + bigint ? BigInt(statFs.bsize) : statFs.bsize, + bigint ? BigInt(statFs.blocks) : statFs.blocks, + bigint ? BigInt(statFs.bfree) : statFs.bfree, + bigint ? BigInt(statFs.bavail) : statFs.bavail, + bigint ? BigInt(statFs.files) : statFs.files, + bigint ? BigInt(statFs.ffree) : statFs.ffree, + ); +} + +export const statfsPromise = promisify(statfs); diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts index bf43dd92e9..bdf7e4aa62 100644 --- a/ext/node/polyfills/fs.ts +++ b/ext/node/polyfills/fs.ts @@ -75,6 +75,11 @@ import { Stats, statSync, } from "ext:deno_node/_fs/_fs_stat.ts"; +import { + statfs, + statfsPromise, + statfsSync, +} from "ext:deno_node/_fs/_fs_statfs.js"; import { symlink, symlinkPromise, @@ -156,6 +161,7 @@ const promises = { symlink: symlinkPromise, lstat: lstatPromise, stat: statPromise, + statfs: statfsPromise, link: linkPromise, unlink: unlinkPromise, chmod: chmodPromise, @@ -253,6 +259,8 @@ export default { stat, Stats, statSync, + statfs, + statfsSync, symlink, symlinkSync, truncate, @@ -354,6 +362,8 @@ export { rmdirSync, rmSync, stat, + statfs, + statfsSync, Stats, statSync, symlink, diff --git a/ext/node/polyfills/internal/primordials.mjs b/ext/node/polyfills/internal/primordials.mjs index d3726cf45d..f1e775bc53 100644 --- a/ext/node/polyfills/internal/primordials.mjs +++ b/ext/node/polyfills/internal/primordials.mjs @@ -12,6 +12,7 @@ export const ArrayPrototypeSlice = (that, ...args) => that.slice(...args); export const ArrayPrototypeSome = (that, ...args) => that.some(...args); export const ArrayPrototypeSort = (that, ...args) => that.sort(...args); export const ArrayPrototypeUnshift = (that, ...args) => that.unshift(...args); +export const BigInt = globalThis.BigInt; export const ObjectAssign = Object.assign; export const ObjectCreate = Object.create; export const ObjectHasOwn = Object.hasOwn; diff --git a/tests/integration/node_unit_tests.rs b/tests/integration/node_unit_tests.rs index 2fd7e78f66..fc636e807f 100644 --- a/tests/integration/node_unit_tests.rs +++ b/tests/integration/node_unit_tests.rs @@ -44,6 +44,7 @@ util::unit_test_factory!( _fs_rm_test = _fs / _fs_rm_test, _fs_rmdir_test = _fs / _fs_rmdir_test, _fs_stat_test = _fs / _fs_stat_test, + _fs_statfs_test = _fs / _fs_statfs_test, _fs_symlink_test = _fs / _fs_symlink_test, _fs_truncate_test = _fs / _fs_truncate_test, _fs_unlink_test = _fs / _fs_unlink_test, diff --git a/tests/unit_node/_fs/_fs_statfs_test.ts b/tests/unit_node/_fs/_fs_statfs_test.ts new file mode 100644 index 0000000000..fde1c8fed9 --- /dev/null +++ b/tests/unit_node/_fs/_fs_statfs_test.ts @@ -0,0 +1,77 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import * as fs from "node:fs"; +import { assertEquals, assertRejects } from "@std/assert/mod.ts"; +import * as path from "@std/path/mod.ts"; + +function assertStatFs( + statFs: fs.StatsFsBase, + { bigint = false } = {}, +) { + assertEquals(statFs.constructor.name, "StatFs"); + const expectedType = bigint ? "bigint" : "number"; + assertEquals(typeof statFs.type, expectedType); + assertEquals(typeof statFs.bsize, expectedType); + assertEquals(typeof statFs.blocks, expectedType); + assertEquals(typeof statFs.bfree, expectedType); + assertEquals(typeof statFs.bavail, expectedType); + assertEquals(typeof statFs.files, expectedType); + assertEquals(typeof statFs.ffree, expectedType); + if (Deno.build.os == "windows") { + assertEquals(statFs.type, bigint ? 0n : 0); + assertEquals(statFs.files, bigint ? 0n : 0); + assertEquals(statFs.ffree, bigint ? 0n : 0); + } +} + +const filePath = path.fromFileUrl(import.meta.url); + +Deno.test({ + name: "fs.statfs()", + async fn() { + await new Promise>((resolve, reject) => { + fs.statfs(filePath, (err, statFs) => { + if (err) reject(err); + resolve(statFs); + }); + }).then((statFs) => assertStatFs(statFs)); + }, +}); + +Deno.test({ + name: "fs.statfs() bigint", + async fn() { + await new Promise>((resolve, reject) => { + fs.statfs(filePath, { bigint: true }, (err, statFs) => { + if (err) reject(err); + resolve(statFs); + }); + }).then((statFs) => assertStatFs(statFs, { bigint: true })); + }, +}); + +Deno.test({ + name: "fs.statfsSync()", + fn() { + const statFs = fs.statfsSync(filePath); + assertStatFs(statFs); + }, +}); + +Deno.test({ + name: "fs.statfsSync() bigint", + fn() { + const statFs = fs.statfsSync(filePath, { bigint: true }); + assertStatFs(statFs, { bigint: true }); + }, +}); + +Deno.test({ + name: "fs.statfs() non-existent path", + async fn() { + const nonExistentPath = path.join(filePath, "../non-existent"); + await assertRejects(async () => { + await fs.promises.statfs(nonExistentPath); + }, "NotFound"); + }, +});