mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-25 21:37:32 +00:00
feat: merge fs and std crate (#1203)
* feat: merge fs and std crate * fix: errors
This commit is contained in:
parent
04f688e122
commit
8ca6c8118c
41 changed files with 449 additions and 325 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tinymist-std"
|
||||
description = "Additional functions wrapping Rust's std library."
|
||||
description = "Additional functions wrapping Rust's standard library."
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -10,32 +10,54 @@ repository.workspace = true
|
|||
|
||||
[dependencies]
|
||||
|
||||
comemo.workspace = true
|
||||
ecow.workspace = true
|
||||
parking_lot.workspace = true
|
||||
web-time.workspace = true
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
js-sys = { workspace = true, optional = true }
|
||||
|
||||
bitvec = { version = "1" }
|
||||
dashmap = { version = "5" }
|
||||
# tiny-skia-path.workspace = true
|
||||
|
||||
path-clean.workspace = true
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
bitvec.workspace = true
|
||||
comemo.workspace = true
|
||||
dashmap.workspace = true
|
||||
ecow.workspace = true
|
||||
fxhash.workspace = true
|
||||
log.workspace = true
|
||||
path-clean.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
siphasher.workspace = true
|
||||
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_repr = "0.1"
|
||||
serde_repr.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
siphasher.workspace = true
|
||||
web-time.workspace = true
|
||||
tempfile = { workspace = true, optional = true }
|
||||
same-file = { workspace = true, optional = true }
|
||||
|
||||
# feature = "web"
|
||||
js-sys = { workspace = true, optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
|
||||
# feature = "rkyv"
|
||||
rkyv = { workspace = true, optional = true }
|
||||
|
||||
# feature = "typst"
|
||||
typst = { workspace = true, optional = true }
|
||||
typst-shim = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
hex.workspace = true
|
||||
|
||||
|
|
@ -48,13 +70,11 @@ typst = ["dep:typst", "dep:typst-shim"]
|
|||
|
||||
rkyv = ["dep:rkyv", "rkyv/alloc", "rkyv/archive_le"]
|
||||
rkyv-validation = ["dep:rkyv", "rkyv/validation"]
|
||||
# flat-vector = ["rkyv", "rkyv-validation"]
|
||||
|
||||
__web = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
web = ["__web"]
|
||||
system = []
|
||||
system = ["dep:tempfile", "dep:same-file"]
|
||||
bi-hash = []
|
||||
item-dashmap = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! A map that shards items by their fingerprint.
|
||||
|
||||
use std::{collections::HashMap, num::NonZeroU32};
|
||||
|
||||
use crate::hash::Fingerprint;
|
||||
|
|
@ -31,12 +33,13 @@ fn default_shard_size() -> NonZeroU32 {
|
|||
|
||||
type FMapBase<V> = parking_lot::RwLock<HashMap<Fingerprint, V>>;
|
||||
|
||||
/// A map that shards items by their fingerprint.
|
||||
/// A map that shards items by their fingerprint. This is faster
|
||||
/// than the dashmap in some cases.
|
||||
///
|
||||
/// It is fast since a fingerprint could split items into different shards
|
||||
/// efficiently.
|
||||
///
|
||||
/// Note: If a fingerprint is calculated from a hash function, it is not
|
||||
/// Note: If a fingerprint is not calculated from a hash function, it is not
|
||||
/// guaranteed that the fingerprint is evenly distributed. Thus, in that case,
|
||||
/// the performance of this map is not guaranteed.
|
||||
pub struct FingerprintMap<V> {
|
||||
|
|
@ -76,6 +79,7 @@ impl<V> FingerprintMap<V> {
|
|||
.flat_map(|shard| shard.into_inner().into_iter())
|
||||
}
|
||||
|
||||
/// Get the shard
|
||||
pub fn shard(&self, fg: Fingerprint) -> &FMapBase<V> {
|
||||
let shards = &self.shards;
|
||||
let route_idx = (fg.lower32() & self.mask) as usize;
|
||||
|
|
@ -92,6 +96,7 @@ impl<V> FingerprintMap<V> {
|
|||
&mut self.shards
|
||||
}
|
||||
|
||||
/// Checks if the map is empty.
|
||||
pub fn contains_key(&self, fg: &Fingerprint) -> bool {
|
||||
self.shard(*fg).read().contains_key(fg)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! This module contains the implementation of the abstract data types.
|
||||
|
||||
pub mod fmap;
|
||||
pub use fmap::FingerprintMap;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
//! <https://github.com/rust-analyzer/rowan/blob/v0.16.1/src/cow_mut.rs>
|
||||
//!
|
||||
//! This module provides a `CowMut` type, which is a mutable version of `Cow`.
|
||||
//! Although it is strange that we can have a `CowMut`, because it should "copy
|
||||
//! on write", we also don't love the `Cow` API and use `Cow` without even
|
||||
//! touching its `DerefMut` feature.
|
||||
|
||||
/// A mutable version of [Cow][`std::borrow::Cow`].
|
||||
#[derive(Debug)]
|
||||
pub enum CowMut<'a, T> {
|
||||
/// An owned data.
|
||||
Owned(T),
|
||||
/// A borrowed mut data.
|
||||
Borrowed(&'a mut T),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
//!todo: move to core/src/hash.rs
|
||||
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::hash::item_hash128;
|
||||
|
||||
pub trait StaticHash128 {
|
||||
fn get_hash(&self) -> u128;
|
||||
}
|
||||
|
||||
impl Hash for dyn StaticHash128 {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
state.write_u128(self.get_hash());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HashedTrait<T: ?Sized> {
|
||||
hash: u128,
|
||||
t: Box<T>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized> HashedTrait<T> {
|
||||
pub fn new(hash: u128, t: Box<T>) -> Self {
|
||||
Self { hash, t }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for HashedTrait<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.t
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Hash for HashedTrait<T> {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
state.write_u128(self.hash);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash + Default + 'static> Default for HashedTrait<T> {
|
||||
fn default() -> Self {
|
||||
let t = T::default();
|
||||
Self {
|
||||
hash: item_hash128(&t),
|
||||
t: Box::new(t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> StaticHash128 for HashedTrait<T> {
|
||||
fn get_hash(&self) -> u128 {
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ use serde_with::{
|
|||
};
|
||||
use serde_with::{DeserializeAs, SerializeAs};
|
||||
|
||||
/// A marker type for serializing and deserializing `Cow<[u8]>` as base64.
|
||||
pub struct AsCowBytes;
|
||||
|
||||
type StdBase64 = Base64<Standard, Padded>;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ use std::{path::Path, sync::Arc};
|
|||
|
||||
pub use takable::*;
|
||||
|
||||
mod hash;
|
||||
pub use hash::*;
|
||||
|
||||
pub mod cow_mut;
|
||||
|
||||
mod query;
|
||||
|
|
@ -17,6 +14,9 @@ pub use read::*;
|
|||
mod marker;
|
||||
pub use marker::*;
|
||||
|
||||
/// An immutable string.
|
||||
pub type ImmutStr = Arc<str>;
|
||||
/// An immutable byte slice.
|
||||
pub type ImmutBytes = Arc<[u8]>;
|
||||
/// An immutable path.
|
||||
pub type ImmutPath = Arc<Path>;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub struct QueryRef<Res, Err, QueryContext = ()> {
|
|||
}
|
||||
|
||||
impl<T, E, QC> QueryRef<T, E, QC> {
|
||||
/// Create a new query reference with the given value.
|
||||
pub fn with_value(value: T) -> Self {
|
||||
let cell = OnceLock::new();
|
||||
cell.get_or_init(|| Ok(value));
|
||||
|
|
@ -24,6 +25,8 @@ impl<T, E, QC> QueryRef<T, E, QC> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new query reference with the given context to execute the
|
||||
/// query.
|
||||
pub fn with_context(ctx: QC) -> Self {
|
||||
Self {
|
||||
ctx: Mutex::new(Some(ctx)),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/// A trait for reading all data from a source into a buffer.
|
||||
pub trait ReadAllOnce {
|
||||
/// Reads all data from the source into the buffer.
|
||||
fn read_all(self, buf: &mut Vec<u8>) -> std::io::Result<usize>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
//! The debug location that can be used to locate a position in a document or a
|
||||
//! file.
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -38,6 +41,7 @@ pub type RawSourceSpan = u64;
|
|||
/// See [`CharPosition`] for the definition of the position inside a file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileLocation {
|
||||
/// The file path.
|
||||
pub filepath: String,
|
||||
}
|
||||
|
||||
|
|
@ -73,11 +77,14 @@ impl From<Option<(usize, usize)>> for CharPosition {
|
|||
/// See [`CharPosition`] for the definition of the position inside a file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SourceLocation {
|
||||
/// The file path.
|
||||
pub filepath: String,
|
||||
/// The position in the file.
|
||||
pub pos: CharPosition,
|
||||
}
|
||||
|
||||
impl SourceLocation {
|
||||
/// Create a new source location.
|
||||
pub fn from_flat(
|
||||
flat: FlatSourceLocation,
|
||||
i: &impl std::ops::Index<usize, Output = FileLocation>,
|
||||
|
|
@ -94,16 +101,20 @@ impl SourceLocation {
|
|||
/// See [`CharPosition`] for the definition of the position inside a file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FlatSourceLocation {
|
||||
/// The file path.
|
||||
pub filepath: u32,
|
||||
/// The position in the file.
|
||||
pub pos: CharPosition,
|
||||
}
|
||||
|
||||
// /// A resolved file range.
|
||||
// ///
|
||||
// /// See [`CharPosition`] for the definition of the position inside a file.
|
||||
/// A resolved file range.
|
||||
///
|
||||
/// See [`CharPosition`] for the definition of the position inside a file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CharRange {
|
||||
/// The start position.
|
||||
pub start: CharPosition,
|
||||
/// The end position.
|
||||
pub end: CharPosition,
|
||||
}
|
||||
|
||||
|
|
@ -117,12 +128,14 @@ impl fmt::Display for CharRange {
|
|||
}
|
||||
}
|
||||
|
||||
// /// A resolved source (text) range.
|
||||
// ///
|
||||
// /// See [`CharPosition`] for the definition of the position inside a file.
|
||||
/// A resolved source (text) range.
|
||||
///
|
||||
/// See [`CharPosition`] for the definition of the position inside a file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SourceRange {
|
||||
/// The file path.
|
||||
pub path: String,
|
||||
/// The range in the file.
|
||||
pub range: CharRange,
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +157,10 @@ mod typst_ext {
|
|||
/// text or string content.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SourceSpanOffset {
|
||||
/// The source span.
|
||||
pub span: SourceSpan,
|
||||
/// The offset relative to the start of the span. This is usually useful
|
||||
/// if the location is not a span created by the parser.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! Error handling utilities for the `tinymist` crate.
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use ecow::EcoString;
|
||||
|
|
@ -5,12 +7,17 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::debug_loc::CharRange;
|
||||
|
||||
/// The severity of a diagnostic message, following the LSP specification.
|
||||
#[derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum DiagSeverity {
|
||||
/// An error message.
|
||||
Error = 1,
|
||||
/// A warning message.
|
||||
Warning = 2,
|
||||
/// An information message.
|
||||
Information = 3,
|
||||
/// A hint message.
|
||||
Hint = 4,
|
||||
}
|
||||
|
||||
|
|
@ -26,30 +33,41 @@ impl fmt::Display for DiagSeverity {
|
|||
}
|
||||
|
||||
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic>
|
||||
/// The `owner` and `source` fields are not included in the struct, but they
|
||||
/// could be added to `ErrorImpl::arguments`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiagMessage {
|
||||
/// The typst package specifier.
|
||||
pub package: String,
|
||||
/// The file path relative to the root of the workspace or the package.
|
||||
pub path: String,
|
||||
/// The diagnostic message.
|
||||
pub message: String,
|
||||
/// The severity of the diagnostic message.
|
||||
pub severity: DiagSeverity,
|
||||
/// The char range in the file. The position encoding must be negotiated.
|
||||
pub range: Option<CharRange>,
|
||||
// These field could be added to ErrorImpl::arguments
|
||||
// owner: Option<ImmutStr>,
|
||||
// source: ImmutStr,
|
||||
}
|
||||
|
||||
impl DiagMessage {}
|
||||
|
||||
/// ALl kind of errors that can occur in the `tinymist` crate.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum ErrKind {
|
||||
/// No message.
|
||||
None,
|
||||
/// A string message.
|
||||
Msg(EcoString),
|
||||
/// A source diagnostic message.
|
||||
Diag(Box<DiagMessage>),
|
||||
/// An inner error.
|
||||
Inner(Error),
|
||||
}
|
||||
|
||||
/// A trait to convert an error kind into an error kind.
|
||||
pub trait ErrKindExt {
|
||||
/// Convert the error kind into an error kind.
|
||||
fn to_error_kind(self) -> ErrKind;
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +83,12 @@ impl ErrKindExt for std::io::Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for std::str::Utf8Error {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for String {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.into())
|
||||
|
|
@ -101,11 +125,27 @@ impl ErrKindExt for serde_json::Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for anyhow::Error {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for Error {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal error implementation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ErrorImpl {
|
||||
/// A static error identifier.
|
||||
loc: &'static str,
|
||||
/// The kind of error.
|
||||
kind: ErrKind,
|
||||
arguments: Box<[(&'static str, String)]>,
|
||||
/// Additional extractable arguments for the error.
|
||||
args: Option<Box<[(&'static str, String)]>>,
|
||||
}
|
||||
|
||||
/// This type represents all possible errors that can occur in typst.ts
|
||||
|
|
@ -118,43 +158,65 @@ pub struct Error {
|
|||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(loc: &'static str, kind: ErrKind, arguments: Box<[(&'static str, String)]>) -> Self {
|
||||
/// Creates a new error.
|
||||
pub fn new(
|
||||
loc: &'static str,
|
||||
kind: ErrKind,
|
||||
args: Option<Box<[(&'static str, String)]>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
err: Box::new(ErrorImpl {
|
||||
loc,
|
||||
kind,
|
||||
arguments,
|
||||
}),
|
||||
err: Box::new(ErrorImpl { loc, kind, args }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the location of the error.
|
||||
pub fn loc(&self) -> &'static str {
|
||||
self.err.loc
|
||||
}
|
||||
|
||||
/// Returns the kind of the error.
|
||||
pub fn kind(&self) -> &ErrKind {
|
||||
&self.err.kind
|
||||
}
|
||||
|
||||
/// Returns the arguments of the error.
|
||||
pub fn arguments(&self) -> &[(&'static str, String)] {
|
||||
&self.err.arguments
|
||||
self.err.args.as_deref().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let err = &self.err;
|
||||
match &err.kind {
|
||||
ErrKind::Msg(msg) => write!(f, "{}: {} with {:?}", err.loc, msg, err.arguments),
|
||||
ErrKind::Diag(diag) => {
|
||||
write!(f, "{}: {} with {:?}", err.loc, diag.message, err.arguments)
|
||||
|
||||
if err.loc.is_empty() {
|
||||
match &err.kind {
|
||||
ErrKind::Msg(msg) => write!(f, "{msg} with {:?}", err.args),
|
||||
ErrKind::Diag(diag) => {
|
||||
write!(f, "{} with {:?}", diag.message, err.args)
|
||||
}
|
||||
ErrKind::Inner(e) => write!(f, "{e} with {:?}", err.args),
|
||||
ErrKind::None => write!(f, "error with {:?}", err.args),
|
||||
}
|
||||
} else {
|
||||
match &err.kind {
|
||||
ErrKind::Msg(msg) => write!(f, "{}: {} with {:?}", err.loc, msg, err.args),
|
||||
ErrKind::Diag(diag) => {
|
||||
write!(f, "{}: {} with {:?}", err.loc, diag.message, err.args)
|
||||
}
|
||||
ErrKind::Inner(e) => write!(f, "{}: {} with {:?}", err.loc, e, err.args),
|
||||
ErrKind::None => write!(f, "{}: with {:?}", err.loc, err.args),
|
||||
}
|
||||
ErrKind::Inner(e) => write!(f, "{}: {} with {:?}", err.loc, e, err.arguments),
|
||||
ErrKind::None => write!(f, "{}: with {:?}", err.loc, err.arguments),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Error::new("", e.to_string().to_error_kind(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
|
|
@ -178,47 +240,94 @@ impl From<&Error> for wasm_bindgen::JsValue {
|
|||
}
|
||||
}
|
||||
|
||||
/// The result type used in the `tinymist` crate.
|
||||
pub type Result<T, Err = Error> = std::result::Result<T, Err>;
|
||||
|
||||
/// A trait to add context to a result.
|
||||
pub trait WithContext<T>: Sized {
|
||||
/// Add a context to the result.
|
||||
fn context(self, loc: &'static str) -> Result<T>;
|
||||
|
||||
/// Add a context to the result with additional arguments.
|
||||
fn with_context<F>(self, loc: &'static str, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Option<Box<[(&'static str, String)]>>;
|
||||
}
|
||||
|
||||
impl<T, E: ErrKindExt> WithContext<T> for Result<T, E> {
|
||||
fn context(self, loc: &'static str) -> Result<T> {
|
||||
self.map_err(|e| Error::new(loc, e.to_error_kind(), None))
|
||||
}
|
||||
|
||||
fn with_context<F>(self, loc: &'static str, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Option<Box<[(&'static str, String)]>>,
|
||||
{
|
||||
self.map_err(|e| Error::new(loc, e.to_error_kind(), f()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WithContext<T> for Option<T> {
|
||||
fn context(self, loc: &'static str) -> Result<T> {
|
||||
self.ok_or_else(|| Error::new(loc, ErrKind::None, None))
|
||||
}
|
||||
|
||||
fn with_context<F>(self, loc: &'static str, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Option<Box<[(&'static str, String)]>>,
|
||||
{
|
||||
self.ok_or_else(|| Error::new(loc, ErrKind::None, f()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to add context to a result without a specific error type.
|
||||
pub trait WithContextUntyped<T>: Sized {
|
||||
/// Add a context to the result.
|
||||
fn context_ut(self, loc: &'static str) -> Result<T>;
|
||||
|
||||
/// Add a context to the result with additional arguments.
|
||||
fn with_context_ut<F>(self, loc: &'static str, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Option<Box<[(&'static str, String)]>>;
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> WithContextUntyped<T> for Result<T, E> {
|
||||
fn context_ut(self, loc: &'static str) -> Result<T> {
|
||||
self.map_err(|e| Error::new(loc, ErrKind::Msg(ecow::eco_format!("{e}")), None))
|
||||
}
|
||||
|
||||
fn with_context_ut<F>(self, loc: &'static str, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Option<Box<[(&'static str, String)]>>,
|
||||
{
|
||||
self.map_err(|e| Error::new(loc, ErrKind::Msg(ecow::eco_format!("{e}")), f()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The error prelude.
|
||||
pub mod prelude {
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use super::ErrKindExt;
|
||||
use crate::Error;
|
||||
|
||||
pub type ZResult<T> = Result<T, Error>;
|
||||
|
||||
pub trait WithContext<T>: Sized {
|
||||
fn context(self, loc: &'static str) -> ZResult<T>;
|
||||
|
||||
fn with_context<F>(self, loc: &'static str, f: F) -> ZResult<T>
|
||||
where
|
||||
F: FnOnce() -> Box<[(&'static str, String)]>;
|
||||
}
|
||||
|
||||
impl<T, E: ErrKindExt> WithContext<T> for Result<T, E> {
|
||||
fn context(self, loc: &'static str) -> ZResult<T> {
|
||||
self.map_err(|e| Error::new(loc, e.to_error_kind(), Box::new([])))
|
||||
}
|
||||
|
||||
fn with_context<F>(self, loc: &'static str, f: F) -> ZResult<T>
|
||||
where
|
||||
F: FnOnce() -> Box<[(&'static str, String)]>,
|
||||
{
|
||||
self.map_err(|e| Error::new(loc, e.to_error_kind(), f()))
|
||||
}
|
||||
}
|
||||
pub use super::{WithContext, WithContextUntyped};
|
||||
pub use crate::Result;
|
||||
|
||||
pub fn map_string_err<T: ToString>(loc: &'static str) -> impl Fn(T) -> Error {
|
||||
move |e| Error::new(loc, e.to_string().to_error_kind(), Box::new([]))
|
||||
move |e| Error::new(loc, e.to_string().to_error_kind(), None)
|
||||
}
|
||||
|
||||
pub fn map_into_err<S: ErrKindExt, T: Into<S>>(loc: &'static str) -> impl Fn(T) -> Error {
|
||||
move |e| Error::new(loc, e.into().to_error_kind(), Box::new([]))
|
||||
move |e| Error::new(loc, e.into().to_error_kind(), None)
|
||||
}
|
||||
|
||||
pub fn map_err<T: ErrKindExt>(loc: &'static str) -> impl Fn(T) -> Error {
|
||||
move |e| Error::new(loc, e.to_error_kind(), Box::new([]))
|
||||
move |e| Error::new(loc, e.to_error_kind(), None)
|
||||
}
|
||||
|
||||
pub fn wrap_err(loc: &'static str) -> impl Fn(Error) -> Error {
|
||||
move |e| Error::new(loc, crate::ErrKind::Inner(e), Box::new([]))
|
||||
move |e| Error::new(loc, crate::ErrKind::Inner(e), None)
|
||||
}
|
||||
|
||||
pub fn map_string_err_with_args<
|
||||
|
|
@ -226,13 +335,13 @@ pub mod prelude {
|
|||
Args: IntoIterator<Item = (&'static str, String)>,
|
||||
>(
|
||||
loc: &'static str,
|
||||
arguments: Args,
|
||||
args: Args,
|
||||
) -> impl FnOnce(T) -> Error {
|
||||
move |e| {
|
||||
Error::new(
|
||||
loc,
|
||||
e.to_string().to_error_kind(),
|
||||
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
|
||||
Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -243,49 +352,49 @@ pub mod prelude {
|
|||
Args: IntoIterator<Item = (&'static str, String)>,
|
||||
>(
|
||||
loc: &'static str,
|
||||
arguments: Args,
|
||||
args: Args,
|
||||
) -> impl FnOnce(T) -> Error {
|
||||
move |e| {
|
||||
Error::new(
|
||||
loc,
|
||||
e.into().to_error_kind(),
|
||||
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
|
||||
Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_err_with_args<T: ErrKindExt, Args: IntoIterator<Item = (&'static str, String)>>(
|
||||
loc: &'static str,
|
||||
arguments: Args,
|
||||
args: Args,
|
||||
) -> impl FnOnce(T) -> Error {
|
||||
move |e| {
|
||||
Error::new(
|
||||
loc,
|
||||
e.to_error_kind(),
|
||||
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
|
||||
Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrap_err_with_args<Args: IntoIterator<Item = (&'static str, String)>>(
|
||||
loc: &'static str,
|
||||
arguments: Args,
|
||||
args: Args,
|
||||
) -> impl FnOnce(Error) -> Error {
|
||||
move |e| {
|
||||
Error::new(
|
||||
loc,
|
||||
crate::ErrKind::Inner(e),
|
||||
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
|
||||
Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _error_once(loc: &'static str, args: Box<[(&'static str, String)]>) -> Error {
|
||||
Error::new(loc, crate::ErrKind::None, args)
|
||||
Error::new(loc, crate::ErrKind::None, Some(args))
|
||||
}
|
||||
|
||||
pub fn _msg(loc: &'static str, msg: EcoString) -> Error {
|
||||
Error::new(loc, crate::ErrKind::Msg(msg), Box::new([]))
|
||||
Error::new(loc, crate::ErrKind::Msg(msg), None)
|
||||
}
|
||||
|
||||
pub use ecow::eco_format as _eco_format;
|
||||
|
|
|
|||
6
crates/tinymist-std/src/fs.rs
Normal file
6
crates/tinymist-std/src/fs.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//! Filesystem support for tinymist.
|
||||
|
||||
#[cfg(feature = "system")]
|
||||
pub mod flock;
|
||||
#[cfg(feature = "system")]
|
||||
pub mod paths;
|
||||
563
crates/tinymist-std/src/fs/flock.rs
Normal file
563
crates/tinymist-std/src/fs/flock.rs
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
//! Upstream: <https://github.com/rust-lang/cargo/blob/rust-1.83.0/src/cargo/util/flock.rs>
|
||||
//! File-locking support.
|
||||
//!
|
||||
//! This module defines the [`Filesystem`] type which is an abstraction over a
|
||||
//! filesystem, ensuring that access to the filesystem is only done through
|
||||
//! coordinated locks.
|
||||
//!
|
||||
//! The [`FileLock`] type represents a locked file, and provides access to the
|
||||
//! file.
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Display, Path, PathBuf};
|
||||
|
||||
use self::sys::*;
|
||||
use super::paths;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
|
||||
/// A locked file.
|
||||
///
|
||||
/// This provides access to file while holding a lock on the file. This type
|
||||
/// implements the [`Read`], [`Write`], and [`Seek`] traits to provide access
|
||||
/// to the underlying file.
|
||||
///
|
||||
/// Locks are either shared (multiple processes can access the file) or
|
||||
/// exclusive (only one process can access the file).
|
||||
///
|
||||
/// This type is created via methods on the [`Filesystem`] type.
|
||||
///
|
||||
/// When this value is dropped, the lock will be released.
|
||||
#[derive(Debug)]
|
||||
pub struct FileLock {
|
||||
f: Option<File>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileLock {
|
||||
/// Returns the underlying file handle of this lock.
|
||||
pub fn file(&self) -> &File {
|
||||
self.f.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// Returns the underlying path that this lock points to.
|
||||
///
|
||||
/// Note that special care must be taken to ensure that the path is not
|
||||
/// referenced outside the lifetime of this lock.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Returns the parent path containing this file
|
||||
pub fn parent(&self) -> &Path {
|
||||
self.path.parent().unwrap()
|
||||
}
|
||||
|
||||
/// Removes all sibling files to this locked file.
|
||||
///
|
||||
/// This can be useful if a directory is locked with a sentinel file but it
|
||||
/// needs to be cleared out as it may be corrupt.
|
||||
pub fn remove_siblings(&self) -> Result<()> {
|
||||
let path = self.path();
|
||||
for entry in path.parent().unwrap().read_dir()? {
|
||||
let entry = entry?;
|
||||
if Some(&entry.file_name()[..]) == path.file_name() {
|
||||
continue;
|
||||
}
|
||||
let kind = entry.file_type()?;
|
||||
if kind.is_dir() {
|
||||
paths::remove_dir_all(entry.path())?;
|
||||
} else {
|
||||
paths::remove_file(entry.path())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for FileLock {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.file().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for FileLock {
|
||||
fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
|
||||
self.file().seek(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for FileLock {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.file().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.file().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FileLock {
|
||||
fn drop(&mut self) {
|
||||
if let Some(f) = self.f.take() {
|
||||
if let Err(e) = unlock(&f) {
|
||||
log::warn!("failed to release lock: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A "filesystem" is intended to be a globally shared, hence locked, resource
|
||||
/// in Cargo.
|
||||
///
|
||||
/// The `Path` of a filesystem cannot be learned unless it's done in a locked
|
||||
/// fashion, and otherwise functions on this structure are prepared to handle
|
||||
/// concurrent invocations across multiple instances of Cargo.
|
||||
///
|
||||
/// The methods on `Filesystem` that open files return a [`FileLock`] which
|
||||
/// holds the lock, and that type provides methods for accessing the
|
||||
/// underlying file.
|
||||
///
|
||||
/// If the blocking methods (like [`Filesystem::open_ro_shared`]) detect that
|
||||
/// they will block, then they will display a message to the user letting them
|
||||
/// know it is blocked. There are non-blocking variants starting with the
|
||||
/// `try_` prefix like [`Filesystem::try_open_ro_shared_create`].
|
||||
///
|
||||
/// The behavior of locks acquired by the `Filesystem` depend on the operating
|
||||
/// system. On unix-like system, they are advisory using [`flock`], and thus
|
||||
/// not enforced against processes which do not try to acquire the lock. On
|
||||
/// Windows, they are mandatory using [`LockFileEx`], enforced against all
|
||||
/// processes.
|
||||
///
|
||||
/// This **does not** guarantee that a lock is acquired. In some cases, for
|
||||
/// example on filesystems that don't support locking, it will return a
|
||||
/// [`FileLock`] even though the filesystem lock was not acquired. This is
|
||||
/// intended to provide a graceful fallback instead of refusing to work.
|
||||
/// Usually there aren't multiple processes accessing the same resource. In
|
||||
/// that case, it is the user's responsibility to not run concurrent
|
||||
/// processes.
|
||||
///
|
||||
/// [`flock`]: https://linux.die.net/man/2/flock
|
||||
/// [`LockFileEx`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Filesystem {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl Filesystem {
|
||||
/// Creates a new filesystem to be rooted at the given path.
|
||||
pub fn new(path: PathBuf) -> Filesystem {
|
||||
Filesystem { root: path }
|
||||
}
|
||||
|
||||
/// Like `Path::join`, creates a new filesystem rooted at this filesystem
|
||||
/// joined with the given path.
|
||||
pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
|
||||
Filesystem::new(self.root.join(other))
|
||||
}
|
||||
|
||||
/// Like `Path::push`, pushes a new path component onto this filesystem.
|
||||
pub fn push<T: AsRef<Path>>(&mut self, other: T) {
|
||||
self.root.push(other);
|
||||
}
|
||||
|
||||
/// Consumes this filesystem and returns the underlying `PathBuf`.
|
||||
///
|
||||
/// Note that this is a relatively dangerous operation and should be used
|
||||
/// with great caution!.
|
||||
pub fn into_path_unlocked(self) -> PathBuf {
|
||||
self.root
|
||||
}
|
||||
|
||||
/// Returns the underlying `Path`.
|
||||
///
|
||||
/// Note that this is a relatively dangerous operation and should be used
|
||||
/// with great caution!.
|
||||
pub fn as_path_unlocked(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// Creates the directory pointed to by this filesystem.
|
||||
///
|
||||
/// Handles errors where other Cargo processes are also attempting to
|
||||
/// concurrently create this directory.
|
||||
pub fn create_dir(&self) -> Result<()> {
|
||||
paths::create_dir_all(&self.root)
|
||||
}
|
||||
|
||||
/// Returns an adaptor that can be used to print the path of this
|
||||
/// filesystem.
|
||||
pub fn display(&self) -> Display<'_> {
|
||||
self.root.display()
|
||||
}
|
||||
|
||||
/// Opens read-write exclusive access to a file, returning the locked
|
||||
/// version of a file.
|
||||
///
|
||||
/// This function will create a file at `path` if it doesn't already exist
|
||||
/// (including intermediate directories), and then it will acquire an
|
||||
/// exclusive lock on `path`. If the process must block waiting for the
|
||||
/// lock, the `msg` is printed to stderr.
|
||||
///
|
||||
/// The returned file can be accessed to look at the path and also has
|
||||
/// read/write access to the underlying file.
|
||||
pub fn open_rw_exclusive_create<P>(&self, path: P, msg: &str) -> Result<FileLock>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut opts = OpenOptions::new();
|
||||
opts.read(true).write(true).create(true);
|
||||
let (path, f) = self.open(path.as_ref(), &opts, true)?;
|
||||
acquire(msg, &path, &|| try_lock_exclusive(&f), &|| {
|
||||
lock_exclusive(&f)
|
||||
})?;
|
||||
Ok(FileLock { f: Some(f), path })
|
||||
}
|
||||
|
||||
/// A non-blocking version of [`Filesystem::open_rw_exclusive_create`].
|
||||
///
|
||||
/// Returns `None` if the operation would block due to another process
|
||||
/// holding the lock.
|
||||
pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
) -> Result<Option<FileLock>> {
|
||||
let mut opts = OpenOptions::new();
|
||||
opts.read(true).write(true).create(true);
|
||||
let (path, f) = self.open(path.as_ref(), &opts, true)?;
|
||||
if try_acquire(&path, &|| try_lock_exclusive(&f))? {
|
||||
Ok(Some(FileLock { f: Some(f), path }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens read-only shared access to a file, returning the locked version of
|
||||
/// a file.
|
||||
///
|
||||
/// This function will fail if `path` doesn't already exist, but if it does
|
||||
/// then it will acquire a shared lock on `path`. If the process must block
|
||||
/// waiting for the lock, the `msg` is printed to stderr.
|
||||
///
|
||||
/// The returned file can be accessed to look at the path and also has read
|
||||
/// access to the underlying file. Any writes to the file will return an
|
||||
/// error.
|
||||
pub fn open_ro_shared<P>(&self, path: P, msg: &str) -> Result<FileLock>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let (path, f) = self.open(path.as_ref(), OpenOptions::new().read(true), false)?;
|
||||
acquire(msg, &path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
|
||||
Ok(FileLock { f: Some(f), path })
|
||||
}
|
||||
|
||||
/// Opens read-only shared access to a file, returning the locked version of
|
||||
/// a file.
|
||||
///
|
||||
/// Compared to [`Filesystem::open_ro_shared`], this will create the file
|
||||
/// (and any directories in the parent) if the file does not already
|
||||
/// exist.
|
||||
pub fn open_ro_shared_create<P: AsRef<Path>>(&self, path: P, msg: &str) -> Result<FileLock> {
|
||||
let mut opts = OpenOptions::new();
|
||||
opts.read(true).write(true).create(true);
|
||||
let (path, f) = self.open(path.as_ref(), &opts, true)?;
|
||||
acquire(msg, &path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
|
||||
Ok(FileLock { f: Some(f), path })
|
||||
}
|
||||
|
||||
/// A non-blocking version of [`Filesystem::open_ro_shared_create`].
|
||||
///
|
||||
/// Returns `None` if the operation would block due to another process
|
||||
/// holding the lock.
|
||||
pub fn try_open_ro_shared_create<P: AsRef<Path>>(&self, path: P) -> Result<Option<FileLock>> {
|
||||
let mut opts = OpenOptions::new();
|
||||
opts.read(true).write(true).create(true);
|
||||
let (path, f) = self.open(path.as_ref(), &opts, true)?;
|
||||
if try_acquire(&path, &|| try_lock_shared(&f))? {
|
||||
Ok(Some(FileLock { f: Some(f), path }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> Result<(PathBuf, File)> {
|
||||
let path = self.root.join(path);
|
||||
let f = opts
|
||||
.open(&path)
|
||||
.or_else(|e| {
|
||||
// If we were requested to create this file, and there was a
|
||||
// NotFound error, then that was likely due to missing
|
||||
// intermediate directories. Try creating them and try again.
|
||||
if e.kind() == io::ErrorKind::NotFound && create {
|
||||
paths::create_dir_all(path.parent().unwrap())?;
|
||||
Ok(opts.open(&path)?)
|
||||
} else {
|
||||
Err(anyhow::Error::from(e))
|
||||
}
|
||||
})
|
||||
.with_context(|| format!("failed to open: {}", path.display()))?;
|
||||
Ok((path, f))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Path> for Filesystem {
|
||||
fn eq(&self, other: &Path) -> bool {
|
||||
self.root == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Filesystem> for Path {
|
||||
fn eq(&self, other: &Filesystem) -> bool {
|
||||
self == other.root
|
||||
}
|
||||
}
|
||||
|
||||
fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> Result<bool> {
|
||||
// File locking on Unix is currently implemented via `flock`, which is known
|
||||
// to be broken on NFS. We could in theory just ignore errors that happen on
|
||||
// NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
|
||||
// forever**, even if the "non-blocking" flag is passed!
|
||||
//
|
||||
// As a result, we just skip all file locks entirely on NFS mounts. That
|
||||
// should avoid calling any `flock` functions at all, and it wouldn't work
|
||||
// there anyway.
|
||||
//
|
||||
// [1]: https://github.com/rust-lang/cargo/issues/2615
|
||||
if is_on_nfs_mount(path) {
|
||||
log::info!("{path:?} appears to be an NFS mount, not trying to lock");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
match lock_try() {
|
||||
Ok(()) => return Ok(true),
|
||||
|
||||
// In addition to ignoring NFS which is commonly not working we also
|
||||
// just ignore locking on filesystems that look like they don't
|
||||
// implement file locking.
|
||||
Err(e) if error_unsupported(&e) => return Ok(true),
|
||||
|
||||
Err(e) => {
|
||||
if !error_contended(&e) {
|
||||
let e = anyhow::Error::from(e);
|
||||
let cx = format!("failed to lock file: {}", path.display());
|
||||
return Err(e.context(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Acquires a lock on a file in a "nice" manner.
|
||||
///
|
||||
/// Almost all long-running blocking actions in Cargo have a status message
|
||||
/// associated with them as we're not sure how long they'll take. Whenever a
|
||||
/// conflicted file lock happens, this is the case (we're not sure when the lock
|
||||
/// will be released).
|
||||
///
|
||||
/// This function will acquire the lock on a `path`, printing out a nice message
|
||||
/// to the console if we have to wait for it. It will first attempt to use `try`
|
||||
/// to acquire a lock on the crate, and in the case of contention it will emit a
|
||||
/// status message based on `msg` to [`GlobalContext`]'s shell, and then use
|
||||
/// `block` to block waiting to acquire a lock.
|
||||
///
|
||||
/// Returns an error if the lock could not be acquired or if any error other
|
||||
/// than a contention error happens.
|
||||
fn acquire(
|
||||
msg: &str,
|
||||
path: &Path,
|
||||
lock_try: &dyn Fn() -> io::Result<()>,
|
||||
lock_block: &dyn Fn() -> io::Result<()>,
|
||||
) -> Result<()> {
|
||||
if try_acquire(path, lock_try)? {
|
||||
return Ok(());
|
||||
}
|
||||
log::info!("waiting for file lock on {msg}");
|
||||
|
||||
lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
|
||||
fn is_on_nfs_mount(path: &Path) -> bool {
|
||||
use std::ffi::CString;
|
||||
use std::mem;
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// SAFETY: this is implemented by the cargo
|
||||
unsafe {
|
||||
let mut buf: libc::statfs = mem::zeroed();
|
||||
let r = libc::statfs(path.as_ptr(), &mut buf);
|
||||
|
||||
r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
|
||||
fn is_on_nfs_mount(_path: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod sys {
|
||||
use std::fs::File;
|
||||
use std::io::{Error, Result};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
pub(super) fn lock_shared(file: &File) -> Result<()> {
|
||||
flock(file, libc::LOCK_SH)
|
||||
}
|
||||
|
||||
pub(super) fn lock_exclusive(file: &File) -> Result<()> {
|
||||
flock(file, libc::LOCK_EX)
|
||||
}
|
||||
|
||||
pub(super) fn try_lock_shared(file: &File) -> Result<()> {
|
||||
flock(file, libc::LOCK_SH | libc::LOCK_NB)
|
||||
}
|
||||
|
||||
pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
|
||||
flock(file, libc::LOCK_EX | libc::LOCK_NB)
|
||||
}
|
||||
|
||||
pub(super) fn unlock(file: &File) -> Result<()> {
|
||||
flock(file, libc::LOCK_UN)
|
||||
}
|
||||
|
||||
pub(super) fn error_contended(err: &Error) -> bool {
|
||||
err.raw_os_error() == Some(libc::EWOULDBLOCK)
|
||||
}
|
||||
|
||||
pub(super) fn error_unsupported(err: &Error) -> bool {
|
||||
match err.raw_os_error() {
|
||||
// Unfortunately, depending on the target, these may or may not be the same.
|
||||
// For targets in which they are the same, the duplicate pattern causes a warning.
|
||||
#[allow(unreachable_patterns)]
|
||||
Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
|
||||
Some(libc::ENOSYS) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "solaris"))]
|
||||
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
|
||||
// SAFETY: this is implemented by the cargo
|
||||
let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
|
||||
if ret < 0 {
|
||||
Err(Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "solaris")]
|
||||
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
|
||||
// Solaris lacks flock(), so try to emulate using fcntl()
|
||||
let mut flock = libc::flock {
|
||||
l_type: 0,
|
||||
l_whence: 0,
|
||||
l_start: 0,
|
||||
l_len: 0,
|
||||
l_sysid: 0,
|
||||
l_pid: 0,
|
||||
l_pad: [0, 0, 0, 0],
|
||||
};
|
||||
flock.l_type = if flag & libc::LOCK_UN != 0 {
|
||||
libc::F_UNLCK
|
||||
} else if flag & libc::LOCK_EX != 0 {
|
||||
libc::F_WRLCK
|
||||
} else if flag & libc::LOCK_SH != 0 {
|
||||
libc::F_RDLCK
|
||||
} else {
|
||||
panic!("unexpected flock() operation")
|
||||
};
|
||||
|
||||
let mut cmd = libc::F_SETLKW;
|
||||
if (flag & libc::LOCK_NB) != 0 {
|
||||
cmd = libc::F_SETLK;
|
||||
}
|
||||
|
||||
let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
|
||||
|
||||
if ret < 0 {
|
||||
Err(Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod sys {
|
||||
use std::fs::File;
|
||||
use std::io::{Error, Result};
|
||||
use std::mem;
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
|
||||
};
|
||||
|
||||
pub(super) fn lock_shared(file: &File) -> Result<()> {
|
||||
lock_file(file, 0)
|
||||
}
|
||||
|
||||
pub(super) fn lock_exclusive(file: &File) -> Result<()> {
|
||||
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
|
||||
}
|
||||
|
||||
pub(super) fn try_lock_shared(file: &File) -> Result<()> {
|
||||
lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
|
||||
}
|
||||
|
||||
pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
|
||||
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
|
||||
}
|
||||
|
||||
pub(super) fn error_contended(err: &Error) -> bool {
|
||||
err.raw_os_error() == Some(ERROR_LOCK_VIOLATION as i32)
|
||||
}
|
||||
|
||||
pub(super) fn error_unsupported(err: &Error) -> bool {
|
||||
err.raw_os_error() == Some(ERROR_INVALID_FUNCTION as i32)
|
||||
}
|
||||
|
||||
pub(super) fn unlock(file: &File) -> Result<()> {
|
||||
// SAFETY: this is implemented by the cargo
|
||||
unsafe {
|
||||
let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
|
||||
if ret == 0 {
|
||||
Err(Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_file(file: &File, flags: u32) -> Result<()> {
|
||||
// SAFETY: this is implemented by the cargo
|
||||
unsafe {
|
||||
let mut overlapped = mem::zeroed();
|
||||
let ret = LockFileEx(
|
||||
file.as_raw_handle() as HANDLE,
|
||||
flags,
|
||||
0,
|
||||
!0,
|
||||
!0,
|
||||
&mut overlapped,
|
||||
);
|
||||
if ret == 0 {
|
||||
Err(Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
891
crates/tinymist-std/src/fs/paths.rs
Normal file
891
crates/tinymist-std/src/fs/paths.rs
Normal file
|
|
@ -0,0 +1,891 @@
|
|||
//! Upstream: <https://github.com/rust-lang/cargo/blob/rust-1.83.0/crates/cargo-util/src/paths.rs>
|
||||
//! Various utilities for working with files and paths.
|
||||
|
||||
use std::env;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fs::{self, File, Metadata, OpenOptions};
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::iter;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
/// Joins paths into a string suitable for the `PATH` environment variable.
|
||||
///
|
||||
/// This is equivalent to [`std::env::join_paths`], but includes a more
|
||||
/// detailed error message. The given `env` argument is the name of the
|
||||
/// environment variable this is will be used for, which is included in the
|
||||
/// error message.
|
||||
pub fn join_paths<T: AsRef<OsStr>>(paths: &[T], env: &str) -> Result<OsString> {
|
||||
env::join_paths(paths.iter()).with_context(|| {
|
||||
let mut message = format!(
|
||||
"failed to join paths from `${env}` together\n\n\
|
||||
Check if any of path segments listed below contain an \
|
||||
unterminated quote character or path separator:"
|
||||
);
|
||||
for path in paths {
|
||||
use std::fmt::Write;
|
||||
write!(&mut message, "\n {:?}", Path::new(path)).unwrap();
|
||||
}
|
||||
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the name of the environment variable used for searching for
|
||||
/// dynamic libraries.
|
||||
pub fn dylib_path_envvar() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
"PATH"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
// When loading and linking a dynamic library or bundle, dlopen
|
||||
// searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
|
||||
// DYLD_FALLBACK_LIBRARY_PATH.
|
||||
// In the Mach-O format, a dynamic library has an "install path."
|
||||
// Clients linking against the library record this path, and the
|
||||
// dynamic linker, dyld, uses it to locate the library.
|
||||
// dyld searches DYLD_LIBRARY_PATH *before* the install path.
|
||||
// dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
|
||||
// find the library in the install path.
|
||||
// Setting DYLD_LIBRARY_PATH can easily have unintended
|
||||
// consequences.
|
||||
//
|
||||
// Also, DYLD_LIBRARY_PATH appears to have significant performance
|
||||
// penalty starting in 10.13. Cargo's testsuite ran more than twice as
|
||||
// slow with it on CI.
|
||||
"DYLD_FALLBACK_LIBRARY_PATH"
|
||||
} else if cfg!(target_os = "aix") {
|
||||
"LIBPATH"
|
||||
} else {
|
||||
"LD_LIBRARY_PATH"
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of directories that are searched for dynamic libraries.
|
||||
///
|
||||
/// Note that some operating systems will have defaults if this is empty that
|
||||
/// will need to be dealt with.
|
||||
pub fn dylib_path() -> Vec<PathBuf> {
|
||||
match env::var_os(dylib_path_envvar()) {
|
||||
Some(var) => env::split_paths(&var).collect(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a path, removing things like `.` and `..`.
|
||||
///
|
||||
/// CAUTION: This does not resolve symlinks (unlike
|
||||
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
|
||||
/// behavior at times. This should be used carefully. Unfortunately,
|
||||
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
|
||||
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
||||
/// needs to improve on.
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
Component::Prefix(..) => unreachable!(),
|
||||
Component::RootDir => {
|
||||
ret.push(component.as_os_str());
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Returns the absolute path of where the given executable is located based
|
||||
/// on searching the `PATH` environment variable.
|
||||
///
|
||||
/// Returns an error if it cannot be found.
|
||||
pub fn resolve_executable(exec: &Path) -> Result<PathBuf> {
|
||||
if exec.components().count() == 1 {
|
||||
let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
|
||||
let candidates = env::split_paths(&paths).flat_map(|path| {
|
||||
let candidate = path.join(exec);
|
||||
let with_exe = if env::consts::EXE_EXTENSION.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(candidate.with_extension(env::consts::EXE_EXTENSION))
|
||||
};
|
||||
iter::once(candidate).chain(with_exe)
|
||||
});
|
||||
for candidate in candidates {
|
||||
if candidate.is_file() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("no executable for `{}` found in PATH", exec.display())
|
||||
} else {
|
||||
Ok(exec.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns metadata for a file (follows symlinks).
|
||||
///
|
||||
/// Equivalent to [`std::fs::metadata`] with better error messages.
|
||||
pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
|
||||
let path = path.as_ref();
|
||||
std::fs::metadata(path)
|
||||
.with_context(|| format!("failed to load metadata for path `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Returns metadata for a file without following symlinks.
|
||||
///
|
||||
/// Equivalent to [`std::fs::metadata`] with better error messages.
|
||||
pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
|
||||
let path = path.as_ref();
|
||||
std::fs::symlink_metadata(path)
|
||||
.with_context(|| format!("failed to load metadata for path `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Reads a file to a string.
|
||||
///
|
||||
/// Equivalent to [`std::fs::read_to_string`] with better error messages.
|
||||
pub fn read(path: &Path) -> Result<String> {
|
||||
match String::from_utf8(read_bytes(path)?) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a file into a bytes vector.
|
||||
///
|
||||
/// Equivalent to [`std::fs::read`] with better error messages.
|
||||
pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
|
||||
fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Writes a file to disk.
|
||||
///
|
||||
/// Equivalent to [`std::fs::write`] with better error messages.
|
||||
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
fs::write(path, contents.as_ref())
|
||||
.with_context(|| format!("failed to write `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Writes a file to disk atomically.
|
||||
///
|
||||
/// write_atomic uses tempfile::persist to accomplish atomic writes.
|
||||
pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
// On unix platforms, get the permissions of the original file. Copy only the
|
||||
// user/group/other read/write/execute permission bits. The tempfile lib
|
||||
// defaults to an initial mode of 0o600, and we'll set the proper
|
||||
// permissions after creating the file.
|
||||
#[cfg(unix)]
|
||||
let perms = path.metadata().ok().map(|meta| {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// these constants are u16 on macOS
|
||||
#[allow(clippy::useless_conversion)]
|
||||
let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
|
||||
let mode = meta.permissions().mode() & mask;
|
||||
|
||||
std::fs::Permissions::from_mode(mode)
|
||||
});
|
||||
|
||||
let mut tmp = TempFileBuilder::new()
|
||||
.prefix(path.file_name().unwrap())
|
||||
.tempfile_in(path.parent().unwrap())?;
|
||||
tmp.write_all(contents.as_ref())?;
|
||||
|
||||
// On unix platforms, set the permissions on the newly created file. We can use
|
||||
// fchmod (called by the std lib; subject to change) which ignores the umask
|
||||
// so that the new file has the same permissions as the old file.
|
||||
#[cfg(unix)]
|
||||
if let Some(perms) = perms {
|
||||
tmp.as_file().set_permissions(perms)?;
|
||||
}
|
||||
|
||||
tmp.persist(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Equivalent to [`write()`], but does not write anything if the file contents
|
||||
/// are identical to the given contents.
|
||||
pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
||||
(|| -> Result<()> {
|
||||
let contents = contents.as_ref();
|
||||
let mut f = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
// todo: cargo doesn't have `truncate` in the options, but it is
|
||||
// suggested by clippy
|
||||
.truncate(false)
|
||||
.open(&path)?;
|
||||
let mut orig = Vec::new();
|
||||
f.read_to_end(&mut orig)?;
|
||||
if orig != contents {
|
||||
f.set_len(0)?;
|
||||
f.seek(io::SeekFrom::Start(0))?;
|
||||
f.write_all(contents)?;
|
||||
}
|
||||
Ok(())
|
||||
})()
|
||||
.with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Equivalent to [`write()`], but appends to the end instead of replacing the
|
||||
/// contents.
|
||||
pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
|
||||
(|| -> Result<()> {
|
||||
let mut f = OpenOptions::new().append(true).create(true).open(path)?;
|
||||
|
||||
f.write_all(contents)?;
|
||||
Ok(())
|
||||
})()
|
||||
.with_context(|| format!("failed to write `{}`", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new file.
|
||||
pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
|
||||
let path = path.as_ref();
|
||||
File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Opens an existing file.
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
|
||||
let path = path.as_ref();
|
||||
File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Converts a path to UTF-8 bytes.
|
||||
pub fn path2bytes(path: &Path) -> Result<&[u8]> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
Ok(path.as_os_str().as_bytes())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
match path.as_os_str().to_str() {
|
||||
Some(s) => Ok(s.as_bytes()),
|
||||
None => Err(anyhow::format_err!(
|
||||
"invalid non-unicode path: {}",
|
||||
path.display()
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts UTF-8 bytes to a path.
|
||||
pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
Ok(PathBuf::from(OsStr::from_bytes(bytes)))
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::str;
|
||||
match str::from_utf8(bytes) {
|
||||
Ok(s) => Ok(PathBuf::from(s)),
|
||||
Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator that walks up the directory hierarchy towards the root.
|
||||
///
|
||||
/// Each item is a [`Path`]. It will start with the given path, finishing at
|
||||
/// the root. If the `stop_root_at` parameter is given, it will stop at the
|
||||
/// given path (which will be the last item).
|
||||
pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
|
||||
PathAncestors::new(path, stop_root_at)
|
||||
}
|
||||
|
||||
/// An iterator that walks up the directory hierarchy towards the root.
|
||||
pub struct PathAncestors<'a> {
|
||||
current: Option<&'a Path>,
|
||||
stop_at: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl<'a> PathAncestors<'a> {
|
||||
fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
|
||||
let stop_at = env::var("__CARGO_TEST_ROOT")
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| stop_root_at.map(|p| p.to_path_buf()));
|
||||
PathAncestors {
|
||||
current: Some(path),
|
||||
//HACK: avoid reading `~/.cargo/config` when testing Cargo itself.
|
||||
stop_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PathAncestors<'a> {
|
||||
type Item = &'a Path;
|
||||
|
||||
fn next(&mut self) -> Option<&'a Path> {
|
||||
if let Some(path) = self.current {
|
||||
self.current = path.parent();
|
||||
|
||||
if let Some(ref stop_at) = self.stop_at {
|
||||
if path == stop_at {
|
||||
self.current = None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
|
||||
pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
|
||||
_create_dir_all(p.as_ref())
|
||||
}
|
||||
|
||||
fn _create_dir_all(p: &Path) -> Result<()> {
|
||||
fs::create_dir_all(p)
|
||||
.with_context(|| format!("failed to create directory `{}`", p.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Equivalent to [`std::fs::remove_dir_all`] with better error messages.
|
||||
///
|
||||
/// This does *not* follow symlinks.
|
||||
pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
|
||||
_remove_dir_all(p.as_ref()).or_else(|prev_err| {
|
||||
// `std::fs::remove_dir_all` is highly specialized for different platforms
|
||||
// and may be more reliable than a simple walk. We try the walk first in
|
||||
// order to report more detailed errors.
|
||||
fs::remove_dir_all(p.as_ref()).with_context(|| {
|
||||
format!(
|
||||
"{:?}\n\nError: failed to remove directory `{}`",
|
||||
prev_err,
|
||||
p.as_ref().display(),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn _remove_dir_all(p: &Path) -> Result<()> {
|
||||
if symlink_metadata(p)?.is_symlink() {
|
||||
return remove_file(p);
|
||||
}
|
||||
let entries = p
|
||||
.read_dir()
|
||||
.with_context(|| format!("failed to read directory `{}`", p.display()))?;
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if entry.file_type()?.is_dir() {
|
||||
remove_dir_all(&path)?;
|
||||
} else {
|
||||
remove_file(&path)?;
|
||||
}
|
||||
}
|
||||
remove_dir(p)
|
||||
}
|
||||
|
||||
/// Equivalent to [`std::fs::remove_dir`] with better error messages.
|
||||
pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
|
||||
_remove_dir(p.as_ref())
|
||||
}
|
||||
|
||||
fn _remove_dir(p: &Path) -> Result<()> {
|
||||
fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Equivalent to [`std::fs::remove_file`] with better error messages.
|
||||
///
|
||||
/// If the file is readonly, this will attempt to change the permissions to
|
||||
/// force the file to be deleted.
|
||||
/// On Windows, if the file is a symlink to a directory, this will attempt to
|
||||
/// remove the symlink itself.
|
||||
pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
|
||||
_remove_file(p.as_ref())
|
||||
}
|
||||
|
||||
fn _remove_file(p: &Path) -> Result<()> {
|
||||
// For Windows, we need to check if the file is a symlink to a directory
|
||||
// and remove the symlink itself by calling `remove_dir` instead of
|
||||
// `remove_file`.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::fs::FileTypeExt;
|
||||
let metadata = symlink_metadata(p)?;
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_symlink_dir() {
|
||||
return remove_symlink_dir_with_permission_check(p);
|
||||
}
|
||||
}
|
||||
|
||||
remove_file_with_permission_check(p)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
|
||||
remove_with_permission_check(fs::remove_dir, p)
|
||||
.with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
|
||||
}
|
||||
|
||||
fn remove_file_with_permission_check(p: &Path) -> Result<()> {
|
||||
remove_with_permission_check(fs::remove_file, p)
|
||||
.with_context(|| format!("failed to remove file `{}`", p.display()))
|
||||
}
|
||||
|
||||
fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
|
||||
where
|
||||
F: Fn(P) -> io::Result<()>,
|
||||
P: AsRef<Path> + Clone,
|
||||
{
|
||||
match remove_func(p.clone()) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => {
|
||||
if e.kind() == io::ErrorKind::PermissionDenied
|
||||
&& set_not_readonly(p.as_ref()).unwrap_or(false)
|
||||
{
|
||||
remove_func(p)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_not_readonly(p: &Path) -> io::Result<bool> {
|
||||
let mut perms = p.metadata()?.permissions();
|
||||
if !perms.readonly() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
perms.set_mode(0o640);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
#[allow(clippy::permissions_set_readonly_false)]
|
||||
{
|
||||
perms.set_readonly(false);
|
||||
}
|
||||
|
||||
fs::set_permissions(p, perms)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Hardlink (file) or symlink (dir) src to dst if possible, otherwise copy it.
|
||||
///
|
||||
/// If the destination already exists, it is removed before linking.
|
||||
pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
|
||||
let src = src.as_ref();
|
||||
let dst = dst.as_ref();
|
||||
_link_or_copy(src, dst)
|
||||
}
|
||||
|
||||
fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
|
||||
log::debug!("linking {} to {}", src.display(), dst.display());
|
||||
if same_file::is_same_file(src, dst).unwrap_or(false) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// NB: we can't use dst.exists(), as if dst is a broken symlink,
|
||||
// dst.exists() will return false. This is problematic, as we still need to
|
||||
// unlink dst in this case. symlink_metadata(dst).is_ok() will tell us
|
||||
// whether dst exists *without* following symlinks, which is what we want.
|
||||
if fs::symlink_metadata(dst).is_ok() {
|
||||
remove_file(dst)?;
|
||||
}
|
||||
|
||||
let link_result = if src.is_dir() {
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::os::redox::fs::symlink;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::symlink;
|
||||
#[cfg(windows)]
|
||||
// FIXME: This should probably panic or have a copy fallback. Symlinks
|
||||
// are not supported in all windows environments. Currently symlinking
|
||||
// is only used for .dSYM directories on macos, but this shouldn't be
|
||||
// accidentally relied upon.
|
||||
use std::os::windows::fs::symlink_dir as symlink;
|
||||
|
||||
let dst_dir = dst.parent().unwrap();
|
||||
let src = if src.starts_with(dst_dir) {
|
||||
src.strip_prefix(dst_dir).unwrap()
|
||||
} else {
|
||||
src
|
||||
};
|
||||
symlink(src, dst)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
// There seems to be a race condition with APFS when hard-linking
|
||||
// binaries. Gatekeeper does not have signing or hash information
|
||||
// stored in kernel when running the process. Therefore killing it.
|
||||
// This problem does not appear when copying files as kernel has
|
||||
// time to process it. Note that: fs::copy on macos is using
|
||||
// CopyOnWrite (syscall fclonefileat) which should be as fast as
|
||||
// hardlinking. See these issues for the details:
|
||||
//
|
||||
// * https://github.com/rust-lang/cargo/issues/7821
|
||||
// * https://github.com/rust-lang/cargo/issues/10060
|
||||
fs::copy(src, dst).map_or_else(
|
||||
|e| {
|
||||
if e.raw_os_error()
|
||||
.map_or(false, |os_err| os_err == 35 /* libc::EAGAIN */)
|
||||
{
|
||||
log::info!("copy failed {e:?}. falling back to fs::hard_link");
|
||||
|
||||
// Working around an issue copying too fast with zfs (probably related to
|
||||
// https://github.com/openzfsonosx/zfs/issues/809)
|
||||
// See https://github.com/rust-lang/cargo/issues/13838
|
||||
fs::hard_link(src, dst)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
|_| Ok(()),
|
||||
)
|
||||
} else {
|
||||
fs::hard_link(src, dst)
|
||||
};
|
||||
link_result
|
||||
.or_else(|err| {
|
||||
log::debug!("link failed {err}. falling back to fs::copy");
|
||||
fs::copy(src, dst).map(|_| ())
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to link or copy `{}` to `{}`",
|
||||
src.display(),
|
||||
dst.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies a file from one location to another.
|
||||
///
|
||||
/// Equivalent to [`std::fs::copy`] with better error messages.
|
||||
pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
fs::copy(from, to)
|
||||
.with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
|
||||
}
|
||||
|
||||
/// Strips `base` from `path`.
|
||||
///
|
||||
/// This canonicalizes both paths before stripping. This is useful if the
|
||||
/// paths are obtained in different ways, and one or the other may or may not
|
||||
/// have been normalized in some way.
|
||||
pub fn strip_prefix_canonical<P: AsRef<Path>>(
|
||||
path: P,
|
||||
base: P,
|
||||
) -> Result<PathBuf, std::path::StripPrefixError> {
|
||||
// Not all filesystems support canonicalize. Just ignore if it doesn't work.
|
||||
let safe_canonicalize = |path: &Path| match path.canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::warn!("cannot canonicalize {path:?}: {e:?}");
|
||||
path.to_path_buf()
|
||||
}
|
||||
};
|
||||
let canon_path = safe_canonicalize(path.as_ref());
|
||||
let canon_base = safe_canonicalize(base.as_ref());
|
||||
canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
|
||||
}
|
||||
|
||||
/// Creates an excluded from cache directory atomically with its parents as
|
||||
/// needed.
|
||||
///
|
||||
/// The atomicity only covers creating the leaf directory and exclusion from
|
||||
/// cache. Any missing parent directories will not be created in an atomic
|
||||
/// manner.
|
||||
///
|
||||
/// This function is idempotent and in addition to that it won't exclude ``p``
|
||||
/// from cache if it already exists.
|
||||
pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
|
||||
let path = p.as_ref();
|
||||
if path.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let parent = path.parent().unwrap();
|
||||
let base = path.file_name().unwrap();
|
||||
create_dir_all(parent)?;
|
||||
// We do this in two steps (first create a temporary directory and exclude
|
||||
// it from backups, then rename it to the desired name. If we created the
|
||||
// directory directly where it should be and then excluded it from backups
|
||||
// we would risk a situation where cargo is interrupted right after the
|
||||
// directory creation but before the exclusion the directory would remain
|
||||
// non-excluded from backups because we only perform exclusion right after
|
||||
// we created the directory ourselves.
|
||||
//
|
||||
// We need the tempdir created in parent instead of $TMP, because only then we
|
||||
// can be easily sure that rename() will succeed (the new name needs to be
|
||||
// on the same mount point as the old one).
|
||||
let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
|
||||
exclude_from_backups(tempdir.path());
|
||||
exclude_from_content_indexing(tempdir.path());
|
||||
// Previously std::fs::create_dir_all() (through paths::create_dir_all()) was
|
||||
// used here to create the directory directly and fs::create_dir_all()
|
||||
// explicitly treats the directory being created concurrently by another
|
||||
// thread or process as success, hence the check below to follow the
|
||||
// existing behavior. If we get an error at rename() and suddenly the
|
||||
// directory (which didn't exist a moment earlier) exists we can infer from
|
||||
// it's another cargo process doing work.
|
||||
if let Err(e) = fs::rename(tempdir.path(), path) {
|
||||
if !path.exists() {
|
||||
return Err(anyhow::Error::from(e))
|
||||
.with_context(|| format!("failed to create directory `{}`", path.display()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark an existing directory as excluded from backups and indexing.
|
||||
///
|
||||
/// Errors in marking it are ignored.
|
||||
pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
|
||||
let path = p.as_ref();
|
||||
exclude_from_backups(path);
|
||||
exclude_from_content_indexing(path);
|
||||
}
|
||||
|
||||
/// Marks the directory as excluded from archives/backups.
|
||||
///
|
||||
/// This is recommended to prevent derived/temporary files from bloating
|
||||
/// backups. There are two mechanisms used to achieve this right now:
|
||||
///
|
||||
/// * A dedicated resource property excluding from Time Machine backups on macOS
|
||||
/// * CACHEDIR.TAG files supported by various tools in a platform-independent
|
||||
/// way
|
||||
fn exclude_from_backups(path: &Path) {
|
||||
exclude_from_time_machine(path);
|
||||
let file = path.join("CACHEDIR.TAG");
|
||||
if !file.exists() {
|
||||
let _ = std::fs::write(
|
||||
file,
|
||||
"Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
",
|
||||
);
|
||||
// Similarly to exclude_from_time_machine() we ignore errors here as
|
||||
// it's an optional feature.
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the directory as excluded from content indexing.
|
||||
///
|
||||
/// This is recommended to prevent the content of derived/temporary files from
|
||||
/// being indexed. This is very important for Windows users, as the live content
|
||||
/// indexing significantly slows cargo's I/O operations.
|
||||
///
|
||||
/// This is currently a no-op on non-Windows platforms.
|
||||
fn exclude_from_content_indexing(path: &Path) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::iter::once;
|
||||
use std::os::windows::prelude::OsStrExt;
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
GetFileAttributesW, SetFileAttributesW, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
|
||||
};
|
||||
|
||||
let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
|
||||
// SAFETY: this is implemented by the cargo
|
||||
unsafe {
|
||||
SetFileAttributesW(
|
||||
path.as_ptr(),
|
||||
GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = path;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn exclude_from_time_machine(_: &Path) {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// Marks files or directories as excluded from Time Machine on macOS
|
||||
fn exclude_from_time_machine(path: &Path) {
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::{number, string, url};
|
||||
use std::ptr;
|
||||
|
||||
// For compatibility with 10.7 a string is used instead of global
|
||||
// kCFURLIsExcludedFromBackupKey
|
||||
let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
|
||||
let path = url::CFURL::from_path(path, false);
|
||||
if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
|
||||
unsafe {
|
||||
url::CFURLSetResourcePropertyForKey(
|
||||
path.as_concrete_TypeRef(),
|
||||
is_excluded_key.as_concrete_TypeRef(),
|
||||
number::kCFBooleanTrue as *const _,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Errors are ignored, since it's an optional feature and failure
|
||||
// doesn't prevent Cargo from working
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::join_paths;
|
||||
use super::write;
|
||||
use super::write_atomic;
|
||||
|
||||
#[test]
|
||||
fn write_works() {
|
||||
let original_contents = "[dependencies]\nfoo = 0.1.0";
|
||||
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let path = tmpdir.path().join("Cargo.toml");
|
||||
write(&path, original_contents).unwrap();
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(contents, original_contents);
|
||||
}
|
||||
#[test]
|
||||
fn write_atomic_works() {
|
||||
let original_contents = "[dependencies]\nfoo = 0.1.0";
|
||||
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let path = tmpdir.path().join("Cargo.toml");
|
||||
write_atomic(&path, original_contents).unwrap();
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(contents, original_contents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn write_atomic_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
let perms = u32::from(libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH);
|
||||
let original_perms = std::fs::Permissions::from_mode(perms);
|
||||
|
||||
let tmp = tempfile::Builder::new().tempfile().unwrap();
|
||||
|
||||
// need to set the permissions after creating the file to avoid umask
|
||||
tmp.as_file()
|
||||
.set_permissions(original_perms.clone())
|
||||
.unwrap();
|
||||
|
||||
// after this call, the file at `tmp.path()` will not be the same as the file
|
||||
// held by `tmp`
|
||||
write_atomic(tmp.path(), "new").unwrap();
|
||||
assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
|
||||
|
||||
let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
|
||||
assert_eq!(original_perms.mode(), new_perms.mode() & mask);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_lists_paths_on_error() {
|
||||
let valid_paths = vec!["/testing/one", "/testing/two"];
|
||||
// does not fail on valid input
|
||||
let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
|
||||
let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"failed to join paths from `$TESTING2` together\n\n\
|
||||
Check if any of path segments listed below contain an \
|
||||
unterminated quote character or path separator:\
|
||||
\n \"/testing/one\"\
|
||||
\n \"/testing/t:wo/three\"\
|
||||
"
|
||||
);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
|
||||
let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"failed to join paths from `$TESTING2` together\n\n\
|
||||
Check if any of path segments listed below contain an \
|
||||
unterminated quote character or path separator:\
|
||||
\n \"/testing/one\"\
|
||||
\n \"/testing/t\\\"wo/three\"\
|
||||
"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_remove_symlink_dir() {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::os::windows::fs::symlink_dir;
|
||||
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let dir_path = tmpdir.path().join("testdir");
|
||||
let symlink_path = tmpdir.path().join("symlink");
|
||||
|
||||
fs::create_dir(&dir_path).unwrap();
|
||||
|
||||
symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
|
||||
|
||||
assert!(symlink_path.exists());
|
||||
|
||||
assert!(remove_file(symlink_path.clone()).is_ok());
|
||||
|
||||
assert!(!symlink_path.exists());
|
||||
assert!(dir_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_remove_symlink_file() {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::os::windows::fs::symlink_file;
|
||||
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let file_path = tmpdir.path().join("testfile");
|
||||
let symlink_path = tmpdir.path().join("symlink");
|
||||
|
||||
fs::write(&file_path, b"test").unwrap();
|
||||
|
||||
symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
|
||||
|
||||
assert!(symlink_path.exists());
|
||||
|
||||
assert!(remove_file(symlink_path.clone()).is_ok());
|
||||
|
||||
assert!(!symlink_path.exists());
|
||||
assert!(file_path.exists());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
//! The hash extension module. It provides extra concepts like `Fingerprint` and
|
||||
//! `HashedTrait`.
|
||||
|
||||
use core::fmt;
|
||||
use std::{
|
||||
any::Any,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
|
||||
use base64::Engine;
|
||||
use fxhash::FxHasher32;
|
||||
|
|
@ -11,12 +13,13 @@ use siphasher::sip128::{Hasher128, SipHasher13};
|
|||
#[cfg(feature = "rkyv")]
|
||||
use rkyv::{Archive, Deserialize as rDeser, Serialize as rSer};
|
||||
|
||||
use crate::error::prelude::ZResult;
|
||||
use crate::error::prelude::Result;
|
||||
|
||||
pub(crate) type FxBuildHasher = std::hash::BuildHasherDefault<FxHasher>;
|
||||
pub use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
|
||||
// pub type FxIndexSet<K> = indexmap::IndexSet<K, FxHasher>;
|
||||
// pub type FxIndexMap<K, V> = indexmap::IndexMap<K, V, FxHasher>;
|
||||
/// A dashmap that uses the FxHasher as the underlying hasher.
|
||||
pub type FxDashMap<K, V> = dashmap::DashMap<K, V, FxBuildHasher>;
|
||||
|
||||
/// See <https://github.com/rust-lang/rust/blob/master/compiler/rustc_hir/src/stable_hash_impls.rs#L22>
|
||||
|
|
@ -85,7 +88,7 @@ impl Fingerprint {
|
|||
}
|
||||
|
||||
/// Creates a new `Fingerprint` from a svg id that **doesn't have prefix**.
|
||||
pub fn try_from_str(s: &str) -> ZResult<Self> {
|
||||
pub fn try_from_str(s: &str) -> Result<Self> {
|
||||
let bytes = base64::engine::general_purpose::STANDARD_NO_PAD
|
||||
.decode(&s.as_bytes()[..11])
|
||||
.expect("invalid base64 string");
|
||||
|
|
@ -135,9 +138,11 @@ pub struct FingerprintSipHasher {
|
|||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// The base hasher for the [`FingerprintSipHasher`].
|
||||
pub type FingerprintSipHasherBase = SipHasher13;
|
||||
|
||||
impl FingerprintSipHasher {
|
||||
/// Get the fast hash value and the underlying data.
|
||||
pub fn fast_hash(&self) -> (u32, &Vec<u8>) {
|
||||
let mut inner = FxHasher32::default();
|
||||
self.data.hash(&mut inner);
|
||||
|
|
@ -187,6 +192,7 @@ pub struct FingerprintBuilder {
|
|||
|
||||
#[cfg(not(feature = "bi-hash"))]
|
||||
impl FingerprintBuilder {
|
||||
/// Resolve the fingerprint without checking the conflict.
|
||||
pub fn resolve_unchecked<T: Hash>(&self, item: &T) -> Fingerprint {
|
||||
let mut s = FingerprintSipHasher { data: Vec::new() };
|
||||
item.hash(&mut s);
|
||||
|
|
@ -194,6 +200,7 @@ impl FingerprintBuilder {
|
|||
fingerprint
|
||||
}
|
||||
|
||||
/// Resolve the fingerprint and check the conflict.
|
||||
pub fn resolve<T: Hash + 'static>(&self, item: &T) -> Fingerprint {
|
||||
let mut s = FingerprintSipHasher { data: Vec::new() };
|
||||
item.type_id().hash(&mut s);
|
||||
|
|
@ -216,6 +223,7 @@ impl FingerprintBuilder {
|
|||
|
||||
#[cfg(feature = "bi-hash")]
|
||||
impl FingerprintBuilder {
|
||||
/// Resolve the fingerprint without checking the conflict.
|
||||
pub fn resolve_unchecked<T: Hash>(&self, item: &T) -> Fingerprint {
|
||||
let mut s = FingerprintSipHasher { data: Vec::new() };
|
||||
item.hash(&mut s);
|
||||
|
|
@ -233,6 +241,7 @@ impl FingerprintBuilder {
|
|||
fingerprint
|
||||
}
|
||||
|
||||
/// Resolve the fingerprint and check the conflict.
|
||||
pub fn resolve<T: Hash + 'static>(&self, item: &T) -> Fingerprint {
|
||||
let mut s = FingerprintSipHasher { data: Vec::new() };
|
||||
item.type_id().hash(&mut s);
|
||||
|
|
@ -297,6 +306,66 @@ pub fn hash64<T: Hash + ?Sized>(v: &T) -> u64 {
|
|||
// todo: rustc hash doesn't have 32-bit hash
|
||||
pub use fxhash::hash32;
|
||||
|
||||
/// A trait that provides a static prehashed 128-bit hash.
|
||||
pub trait StaticHash128 {
|
||||
/// Get the prehashed 128-bit hash.
|
||||
fn get_hash(&self) -> u128;
|
||||
}
|
||||
|
||||
impl Hash for dyn StaticHash128 {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
state.write_u128(self.get_hash());
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that provides a static prehashed 64-bit hash for any internal `T`.
|
||||
///
|
||||
/// Please ensure that the `T` is really mapped to the hash. Use it at your own
|
||||
/// risk.
|
||||
pub struct HashedTrait<T: ?Sized> {
|
||||
hash: u128,
|
||||
t: Box<T>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized> HashedTrait<T> {
|
||||
/// Create a new `HashedTrait` with the given hash and the trait object.
|
||||
pub fn new(hash: u128, t: Box<T>) -> Self {
|
||||
Self { hash, t }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for HashedTrait<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.t
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Hash for HashedTrait<T> {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
state.write_u128(self.hash);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash + Default + 'static> Default for HashedTrait<T> {
|
||||
fn default() -> Self {
|
||||
let t = T::default();
|
||||
Self {
|
||||
hash: item_hash128(&t),
|
||||
t: Box::new(t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> StaticHash128 for HashedTrait<T> {
|
||||
fn get_hash(&self) -> u128 {
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint() {
|
||||
let t = Fingerprint::from_pair(0, 1);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#![allow(missing_docs)]
|
||||
//! Additional functions wrapping Rust's standard library.
|
||||
|
||||
pub mod adt;
|
||||
pub mod debug_loc;
|
||||
pub mod error;
|
||||
pub mod fs;
|
||||
pub mod hash;
|
||||
pub mod path;
|
||||
pub mod time;
|
||||
|
|
@ -11,7 +12,7 @@ pub(crate) mod concepts;
|
|||
|
||||
pub use concepts::*;
|
||||
|
||||
pub use error::{ErrKind, Error};
|
||||
pub use error::{ErrKind, Error, Result};
|
||||
|
||||
#[cfg(feature = "typst")]
|
||||
pub use typst_shim;
|
||||
|
|
@ -19,8 +20,8 @@ pub use typst_shim;
|
|||
#[cfg(feature = "rkyv")]
|
||||
use rkyv::{Archive, Deserialize as rDeser, Serialize as rSer};
|
||||
|
||||
/// The local id of a svg item.
|
||||
/// This id is only unique within the svg document.
|
||||
/// The local id of an item.
|
||||
/// This id is only unique within a task or process.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "rkyv", derive(Archive, rDeser, rSer))]
|
||||
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! Path utilities.
|
||||
|
||||
use std::path::{Component, Path};
|
||||
|
||||
pub use path_clean::PathClean;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! Cross platform time utilities.
|
||||
|
||||
pub use std::time::SystemTime as Time;
|
||||
pub use web_time::Duration;
|
||||
pub use web_time::Instant;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue