Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): implement fs.statfs() #22862

Merged
merged 14 commits into from Mar 13, 2024
2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -196,7 +196,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.
Expand Down
2 changes: 2 additions & 0 deletions ext/node/lib.rs
Expand Up @@ -255,6 +255,7 @@ deno_core::extension!(deno_node,
ops::fs::op_node_fs_exists_sync<P>,
ops::fs::op_node_cp_sync<P>,
ops::fs::op_node_cp<P>,
ops::fs::op_node_statfs<P>,
ops::winerror::op_node_sys_to_uv_error,
ops::v8::op_v8_cached_data_version_tag,
ops::v8::op_v8_get_heap_statistics,
Expand Down Expand Up @@ -372,6 +373,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",
Expand Down
138 changes: 138 additions & 0 deletions ext/node/ops/fs.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -78,3 +79,140 @@ 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<P>(
state: Rc<RefCell<OpState>>,
#[string] path: String,
bigint: bool,
) -> Result<StatFs, AnyError>
where
P: NodePermissions + 'static,
{
{
let mut state = state.borrow_mut();
state
.borrow_mut::<P>()
.check_read_with_api_name(Path::new(&path), Some("node:fs.statfs"))?;
state
.borrow_mut::<P>()
.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("Cargo.toml").canonicalize().unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't canonicalize() fail if Cargo.toml is not present?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we need this. The user path argument is shadowed by this hardcoded path to Cargo.toml

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must have copy-pasted this from my windows playground repo... that's frustrating there was no lint error.

let root = path
.ancestors()
.last()
.ok_or(anyhow!("Path has no root."))
.unwrap();
let root = OsStr::new(root).encode_wide().collect::<Vec<_>>();
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."))
}
}
56 changes: 56 additions & 0 deletions 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);
10 changes: 10 additions & 0 deletions ext/node/polyfills/fs.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -156,6 +161,7 @@ const promises = {
symlink: symlinkPromise,
lstat: lstatPromise,
stat: statPromise,
statfs: statfsPromise,
link: linkPromise,
unlink: unlinkPromise,
chmod: chmodPromise,
Expand Down Expand Up @@ -253,6 +259,8 @@ export default {
stat,
Stats,
statSync,
statfs,
statfsSync,
symlink,
symlinkSync,
truncate,
Expand Down Expand Up @@ -354,6 +362,8 @@ export {
rmdirSync,
rmSync,
stat,
statfs,
statfsSync,
Stats,
statSync,
symlink,
Expand Down
1 change: 1 addition & 0 deletions ext/node/polyfills/internal/primordials.mjs
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is BigInt not available in primordials from ext:core/mod.js? If so we should fix it 😬

export const ObjectAssign = Object.assign;
export const ObjectCreate = Object.create;
export const ObjectHasOwn = Object.hasOwn;
Expand Down
1 change: 1 addition & 0 deletions tests/integration/node_unit_tests.rs
Expand Up @@ -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,
Expand Down
64 changes: 64 additions & 0 deletions tests/unit_node/_fs/_fs_statfs_test.ts
@@ -0,0 +1,64 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { statfs, statfsSync, StatsFsBase } from "node:fs";
import { assertEquals } from "@std/assert/mod.ts";
import * as path from "@std/path/mod.ts";

function assertStatFs(statFs: StatsFsBase<unknown>, { 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<StatsFsBase<unknown>>((resolve, reject) => {
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<StatsFsBase<unknown>>((resolve, reject) => {
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 = statfsSync(filePath);
assertStatFs(statFs);
},
});

Deno.test({
name: "fs.statfsSync() bigint",
fn() {
const statFs = statfsSync(filePath, { bigint: true });
assertStatFs(statFs, { bigint: true });
},
});