chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
258
third_party/pi/packages/env/daemon/src/fs.rs
vendored
Normal file
258
third_party/pi/packages/env/daemon/src/fs.rs
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
//! File operations with Node's semantics where the Rust standard library differs (docs/semantics.md).
|
||||
|
||||
use crate::errors::Failure;
|
||||
use crate::sys;
|
||||
use serde_json::{Value, json};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File, Metadata};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
pub type Outcome<T> = Result<T, Failure>;
|
||||
|
||||
/// `{ name, kind, size, mtimeSec, mtimeNsec, dev, ino }` of lstat or fstat metadata.
|
||||
pub fn info(path: &str, metadata: &Metadata) -> Value {
|
||||
let file_type = metadata.file_type();
|
||||
let kind = if file_type.is_file() {
|
||||
"file"
|
||||
} else if file_type.is_dir() {
|
||||
"directory"
|
||||
} else if file_type.is_symlink() {
|
||||
"symlink"
|
||||
} else {
|
||||
"other"
|
||||
};
|
||||
let name = Path::new(path)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let (mtime_sec, mtime_nsec) = sys::mtime(metadata);
|
||||
let (dev, ino) = sys::identity(metadata);
|
||||
json!({
|
||||
"name": name,
|
||||
"kind": kind,
|
||||
"size": metadata.len(),
|
||||
"mtimeSec": mtime_sec,
|
||||
"mtimeNsec": mtime_nsec,
|
||||
"dev": dev,
|
||||
"ino": ino,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lstat(path: &str) -> Outcome<Value> {
|
||||
fs::symlink_metadata(path)
|
||||
.map(|metadata| info(path, &metadata))
|
||||
.map_err(|error| Failure::io(&error, "lstat", path))
|
||||
}
|
||||
|
||||
pub fn realpath(path: &str) -> Outcome<Value> {
|
||||
sys::realpath(path)
|
||||
.map(|resolved| json!({ "path": resolved }))
|
||||
.map_err(|error| Failure::io(&error, "realpath", path))
|
||||
}
|
||||
|
||||
/// Node's recursive `mkdir`: an existing directory is fine, an existing file is `EEXIST`, a file in the way of a
|
||||
/// parent is `ENOTDIR`.
|
||||
pub fn mkdir_recursive(path: &Path) -> io::Result<()> {
|
||||
let is_dir = |path: &Path| {
|
||||
fs::metadata(path)
|
||||
.map(|metadata| metadata.is_dir())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
match fs::create_dir(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
|
||||
if is_dir(path) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
let Some(parent) = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
else {
|
||||
return Err(error);
|
||||
};
|
||||
match mkdir_recursive(parent) {
|
||||
Err(parent_error) if parent_error.kind() == io::ErrorKind::AlreadyExists => {
|
||||
return Err(sys::synthetic("ENOTDIR"));
|
||||
}
|
||||
other => other?,
|
||||
}
|
||||
match fs::create_dir(path) {
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists && is_dir(path) => {
|
||||
Ok(())
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mkdir(path: &str, recursive: bool) -> Outcome<Value> {
|
||||
let result = if recursive {
|
||||
mkdir_recursive(Path::new(path))
|
||||
} else {
|
||||
fs::create_dir(path)
|
||||
};
|
||||
result
|
||||
.map(|()| json!({}))
|
||||
.map_err(|error| Failure::io(&error, "mkdir", path))
|
||||
}
|
||||
|
||||
/// `writeFile`/`appendFile`: create missing parents like Node's recursive `mkdir`, then write. Returns the open file
|
||||
/// for further chunks.
|
||||
pub fn write(path: &str, append: bool, parents: bool, content: &[u8]) -> Outcome<File> {
|
||||
if let Some(parent) = Path::new(path)
|
||||
.parent()
|
||||
.filter(|_| parents)
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
mkdir_recursive(parent)
|
||||
.map_err(|error| Failure::io(&error, "mkdir", &parent.to_string_lossy()))?;
|
||||
}
|
||||
let mode = if append {
|
||||
sys::OpenMode::Append
|
||||
} else {
|
||||
sys::OpenMode::Write
|
||||
};
|
||||
let mut file = sys::open(path, mode).map_err(|error| Failure::io(&error, "open", path))?;
|
||||
file.write_all(content)
|
||||
.map_err(|error| Failure::io(&error, "write", path))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn open_existing_for_write(path: &str) -> Outcome<File> {
|
||||
sys::open(path, sys::OpenMode::ReadWrite).map_err(|error| Failure::io(&error, "open", path))
|
||||
}
|
||||
|
||||
pub fn truncate(path: &str, size: u64) -> Outcome<Value> {
|
||||
let file = open_existing_for_write(path)?;
|
||||
file.set_len(size)
|
||||
.map_err(|error| Failure::io(&error, "ftruncate", path))?;
|
||||
Ok(json!({}))
|
||||
}
|
||||
|
||||
pub fn fsync(path: &str) -> Outcome<Value> {
|
||||
// POSIX refuses to open a directory for writing; check first so Windows reports the same.
|
||||
if fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) {
|
||||
return Err(Failure::new(
|
||||
"EISDIR",
|
||||
format!("EISDIR: illegal operation on a directory, open '{path}'"),
|
||||
)
|
||||
.path(path));
|
||||
}
|
||||
let file = open_existing_for_write(path)?;
|
||||
file.sync_all()
|
||||
.map_err(|error| Failure::io(&error, "fsync", path))?;
|
||||
Ok(json!({}))
|
||||
}
|
||||
|
||||
pub fn rename(from: &str, to: &str) -> Outcome<Value> {
|
||||
sys::rename(from, to)
|
||||
.map(|()| json!({}))
|
||||
.map_err(|error| Failure::io_between(&error, "rename", from, Some(to)))
|
||||
}
|
||||
|
||||
/// Node's `fs.rm`: missing paths fail unless `force`; a directory without `recursive` fails with `ERR_FS_EISDIR`.
|
||||
pub fn rm(path: &str, recursive: bool, force: bool) -> Outcome<Value> {
|
||||
let metadata = match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if force && error.kind() == io::ErrorKind::NotFound => return Ok(json!({})),
|
||||
Err(error) => return Err(Failure::io(&error, "lstat", path)),
|
||||
};
|
||||
let result = if metadata.is_dir() {
|
||||
if !recursive {
|
||||
return Err(Failure::new(
|
||||
"ERR_FS_EISDIR",
|
||||
format!("Path is a directory: rm returned EISDIR (is a directory) {path}"),
|
||||
)
|
||||
.path(path));
|
||||
}
|
||||
fs::remove_dir_all(path)
|
||||
} else {
|
||||
sys::remove_file(path)
|
||||
};
|
||||
result
|
||||
.map(|()| json!({}))
|
||||
.map_err(|error| Failure::io(&error, "rm", path))
|
||||
}
|
||||
|
||||
/// A new directory named `prefix` plus six random characters, as libuv's `mkdtemp` creates it.
|
||||
pub fn mkdtemp(prefix: &str) -> Outcome<Value> {
|
||||
sys::mkdtemp(prefix)
|
||||
.map(|path| json!({ "path": path }))
|
||||
.map_err(|error| Failure::io(&error, "mkdtemp", &format!("{prefix}XXXXXX")))
|
||||
}
|
||||
|
||||
/// Open a regular file for positional reads; directories fail with `EISDIR`, other non-regular files with
|
||||
/// `NOT_REGULAR`, and with `no_follow` a final symbolic link with `SYMLINK`.
|
||||
pub fn open_reader(path: &str, no_follow: bool) -> Outcome<(File, Value)> {
|
||||
let file = sys::open_reader(path, no_follow)?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.map_err(|error| Failure::io(&error, "fstat", path))?;
|
||||
if metadata.is_dir() {
|
||||
return Err(
|
||||
Failure::new("EISDIR", "EISDIR: illegal operation on a directory, read").path(path),
|
||||
);
|
||||
}
|
||||
if !metadata.is_file() {
|
||||
return Err(Failure::new("NOT_REGULAR", "Not a regular file").path(path));
|
||||
}
|
||||
let info = info(path, &metadata);
|
||||
Ok((file, info))
|
||||
}
|
||||
|
||||
/// Up to `length` bytes at `offset`; fewer only at the end of the file.
|
||||
pub fn pread(file: &File, path: &str, offset: u64, length: usize) -> Outcome<Vec<u8>> {
|
||||
let mut buffer = vec![0u8; length];
|
||||
let mut filled = 0;
|
||||
while filled < length {
|
||||
match sys::read_at(file, &mut buffer[filled..], offset + filled as u64) {
|
||||
Ok(0) => break,
|
||||
Ok(read) => filled += read,
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(Failure::io(&error, "read", path)),
|
||||
}
|
||||
}
|
||||
buffer.truncate(filled);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Up to `length` bytes from the current position: one `read` call, as Node's `readFile` makes for files of unknown
|
||||
/// size (FIFOs, devices).
|
||||
pub fn read(mut file: &File, path: &str, length: usize) -> Outcome<Vec<u8>> {
|
||||
let mut buffer = vec![0u8; length];
|
||||
loop {
|
||||
match file.read(&mut buffer) {
|
||||
Ok(read) => {
|
||||
buffer.truncate(read);
|
||||
return Ok(buffer);
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(Failure::io(&error, "read", path)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One directory entry as Node lists it: the name decoded as UTF-8 (invalid bytes replaced), and lstat of the
|
||||
/// directory joined with that decoded name, so a name that is not valid UTF-8 reports `ENOENT` as in Node. `raw`
|
||||
/// carries the original bytes of such a name, which libuv sorts by.
|
||||
pub fn dir_entry(dir: &str, file_name: &OsStr) -> Value {
|
||||
let name = file_name.to_string_lossy().into_owned();
|
||||
let entry_path = Path::new(dir).join(&name).to_string_lossy().into_owned();
|
||||
let mut entry = match fs::symlink_metadata(&entry_path) {
|
||||
Ok(metadata) => json!({ "name": name, "info": info(&entry_path, &metadata) }),
|
||||
Err(error) => {
|
||||
json!({ "name": name, "error": Failure::io(&error, "lstat", &entry_path).to_json() })
|
||||
}
|
||||
};
|
||||
if let Some(raw) = sys::raw_name(file_name).filter(|raw| *raw != name.as_bytes()) {
|
||||
entry["raw"] = json!(raw);
|
||||
}
|
||||
entry
|
||||
}
|
||||
Reference in New Issue
Block a user