Skip to content

Release v1.1.0: unify backend traits, seal them, add DynAsyncFileExt#51

Merged
al8n merged 6 commits intomainfrom
1.1.0
Apr 28, 2026
Merged

Release v1.1.0: unify backend traits, seal them, add DynAsyncFileExt#51
al8n merged 6 commits intomainfrom
1.1.0

Conversation

@al8n
Copy link
Copy Markdown
Owner

@al8n al8n commented Apr 28, 2026

Summary

Unify the per-backend FileExt / AsyncFileExt traits into a single crate-root trait, seal both, and add an object-safe DynAsyncFileExt mirror so callers can hold fs4 file handles behind dyn (e.g. Box<dyn DynAsyncFileExt>).

  • Single crate-root traits. Previously the file_ext! / async_file_ext! macros generated a fresh pub trait FileExt / AsyncFileExt per backend module, so fs4::tokio::AsyncFileExt and fs4::async_std::AsyncFileExt were distinct types. Now there is one fs4::FileExt and one fs4::AsyncFileExt defined directly in lib.rs; the per-backend modules (fs4::tokio, fs4::async_std, fs4::smol, fs4::fs_err2, fs4::fs_err3, fs4::fs_err2_tokio, fs4::fs_err3_tokio) re-export the unified trait. Method-call sites that imported the trait via use continue to compile unchanged.
  • Reference blanket impls. New impl<F: FileExt + ?Sized> FileExt for &F and impl<F: AsyncFileExt + ?Sized> AsyncFileExt for &F, so the extension methods are now callable through shared references.
  • Sealed. FileExt, AsyncFileExt, and DynAsyncFileExt now carry a private sealed::Sealed supertrait; the implementing set is closed to the concrete file types fs4 supports plus their & references.
  • DynAsyncFileExt. AsyncFileExt uses RPITIT (-> impl Future<Output = ...>) and is therefore not object-safe. The new DynAsyncFileExt mirrors the same operations behind BoxFuture<'_, T> (a pub type alias for Pin<Box<dyn Future<Output = T> + Send + 'a>>) so it can be used as a trait object (Box<dyn DynAsyncFileExt>, &dyn DynAsyncFileExt). The impl is generated per-backend in the async_file_ext! macro so each concrete file type's Send-ness is checked at build time. Includes its own &F blanket impl. Sealed. Re-exported alongside AsyncFileExt from each async backend submodule.
  • #[inline(always)] on the thin delegating methods (excluded under tarpaulin).
  • rustfmt: add imports_granularity = \"Crate\".

FileExt (sync) is already object-safe — all methods return concrete Result<T> — so no DynFileExt is needed; dyn FileExt works directly. The crate continues to compile on wasm32-wasi* (which has neither cfg(unix) nor cfg(windows)); on those targets it degrades to just FsStats + TryLockError as before.

Semver

Minor bump (1.1.0). The new blanket impls and DynAsyncFileExt are additions. Trait-identity unification (the per-backend traits are now the same trait) and the new Sealed supertrait can in principle break downstream code that relied on the distinctness of the per-backend traits or implemented FileExt/AsyncFileExt on its own wrapper type — both patterns are uncommon in practice. The mainstream consumer pattern (calling methods through use fs4::<backend>::{FileExt,AsyncFileExt}) is preserved.

Full details in CHANGELOG.md.

Test plan

  • `cargo check --all-features`
  • `cargo check --no-default-features`
  • `cargo check --all-features --tests`
  • `cargo check --target wasm32-wasip1` and `cargo check --target wasm32-wasip1 --no-default-features`
  • `cargo test --all-features --lib` (90 passing)
  • CI matrix green (ubuntu / macos / windows, MSRV 1.75 + each-feature)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.64%. Comparing base (f5e148e) to head (df476ee).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #51      +/-   ##
==========================================
+ Coverage   98.27%   98.64%   +0.37%     
==========================================
  Files          11       11              
  Lines         753      887     +134     
==========================================
+ Hits          740      875     +135     
+ Misses         13       12       -1     
Files with missing lines Coverage Δ
src/file_ext/async_impl.rs 100.00% <100.00%> (ø)
src/file_ext/sync_impl.rs 100.00% <100.00%> (ø)
src/lib.rs 100.00% <100.00%> (ø)
src/unix/async_impl.rs 100.00% <ø> (ø)

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 16bab4b...df476ee. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@al8n al8n changed the title 1.1.0 Release v1.1.0: unify backend traits, seal them, add DynAsyncFileExt Apr 28, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR prepares the fs4 1.1.0 release by unifying the sync/async extension traits at the crate root, sealing them to allow future API evolution, and adding an object-safe async extension trait.

Changes:

  • Consolidates per-backend FileExt / AsyncFileExt traits into crate-root fs4::FileExt and fs4::AsyncFileExt, with backend modules re-exporting the unified traits.
  • Seals the extension traits and adds reference blanket impls plus a new object-safe DynAsyncFileExt backed by boxed futures.
  • Updates tests/imports and rustfmt configuration; bumps version and changelog for 1.1.0.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/lib.rs Introduces sealed/unified FileExt/AsyncFileExt, adds DynAsyncFileExt and boxed future alias, updates backend re-exports.
src/file_ext/sync_impl.rs Refactors macro to implement crate-root FileExt and sealing; adjusts test imports.
src/file_ext/async_impl.rs Refactors macro to implement crate-root AsyncFileExt and adds DynAsyncFileExt impl; adjusts test imports.
src/file_ext/async_impl/tokio_impl.rs Removes now-unneeded backend trait import in tests.
src/file_ext/async_impl/async_std_impl.rs Removes now-unneeded backend trait import in tests.
src/file_ext/async_impl/smol_impl.rs Removes now-unneeded backend trait import in tests.
src/file_ext/async_impl/fs_err2_tokio_impl.rs Removes now-unneeded backend trait import in tests.
src/file_ext/async_impl/fs_err3_tokio_impl.rs Removes now-unneeded backend trait import in tests.
src/unix/async_impl.rs Updates test macro imports to use crate-root AsyncFileExt.
src/unix/async_impl/tokio_impl.rs Removes now-unneeded backend trait import in tests.
src/unix/async_impl/async_std_impl.rs Removes now-unneeded backend trait import in tests.
src/unix/async_impl/smol_impl.rs Removes now-unneeded backend trait import in tests.
src/unix/async_impl/fs_err2_tokio_impl.rs Removes now-unneeded backend trait import in tests.
src/unix/async_impl/fs_err3_tokio_impl.rs Removes now-unneeded backend trait import in tests.
rustfmt.toml Configures rustfmt to format imports at crate granularity.
Cargo.toml Bumps crate version to 1.1.0.
CHANGELOG.md Adds 1.1.0 release notes describing the trait consolidation, sealing, and new dyn trait.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib.rs
Comment on lines +301 to +317
/// This is the blocking counterpart of [`FileExt::try_lock`]. It mirrors
/// [`std::fs::File::lock`].
fn lock(&self) -> Result<()>;

/// Attempts to acquire a shared lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors
/// [`std::fs::File::try_lock_shared`].
fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;

/// Attempts to acquire an exclusive lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

These docs reference std::fs::File::try_lock_shared / std::fs::File::try_lock, which do not exist in std. Update the links/text to point to the corresponding fs4::FileExt methods (or remove the std::fs::File references) so the docs don't contain dead/incorrect links.

Suggested change
/// This is the blocking counterpart of [`FileExt::try_lock`]. It mirrors
/// [`std::fs::File::lock`].
fn lock(&self) -> Result<()>;
/// Attempts to acquire a shared lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors
/// [`std::fs::File::try_lock_shared`].
fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;
/// Attempts to acquire an exclusive lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
/// This is the blocking counterpart of [`FileExt::try_lock`].
fn lock(&self) -> Result<()>;
/// Attempts to acquire a shared lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. This is the non-blocking counterpart
/// of [`FileExt::lock_shared`].
fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;
/// Attempts to acquire an exclusive lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. This is the non-blocking counterpart
/// of [`FileExt::lock`].

Copilot uses AI. Check for mistakes.
Comment thread src/lib.rs
fn lock_shared(&self) -> Result<()>;

/// Acquires an exclusive lock on the file, blocking until the lock can be
/// acquired. Mirrors [`std::fs::File::lock`].
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This doc comment says the method mirrors std::fs::File::lock, but std::fs::File has no such inherent method. Consider referencing fs4::FileExt::lock/fs4::AsyncFileExt::lock instead, or describe the semantics without pointing at std APIs that don't exist.

Suggested change
/// acquired. Mirrors [`std::fs::File::lock`].
/// acquired using the platform's file-locking primitive.

Copilot uses AI. Check for mistakes.
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs
Comment on lines +445 to +449
impl<F: AsyncFileExt + ?Sized> AsyncFileExt for &F {
#[cfg_attr(not(tarpaulin), inline(always))]
async fn allocated_size(&self) -> Result<u64> {
(*self).allocated_size().await
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The blanket impl impl<F: AsyncFileExt + ?Sized> AsyncFileExt for &F is recursively calling itself (e.g. (*self).allocated_size().await resolves to the same AsyncFileExt for &F impl again). This will cause infinite recursion when using AsyncFileExt methods on references. Prefer fully-qualified calls to the underlying F implementation (e.g. <F as AsyncFileExt>::allocated_size(*self).await, and similarly for allocate/unlock_async).

Copilot uses AI. Check for mistakes.
Comment thread src/lib.rs
Comment on lines +301 to +317
/// This is the blocking counterpart of [`FileExt::try_lock`]. It mirrors
/// [`std::fs::File::lock`].
fn lock(&self) -> Result<()>;

/// Attempts to acquire a shared lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors
/// [`std::fs::File::try_lock_shared`].
fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;

/// Attempts to acquire an exclusive lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

These docs reference std::fs::File::lock, but the standard library File type does not provide inherent lock/try_lock* methods. Consider updating the wording/link to refer to fs4::FileExt::lock/try_lock (or describe the behavior without pointing at std).

Suggested change
/// This is the blocking counterpart of [`FileExt::try_lock`]. It mirrors
/// [`std::fs::File::lock`].
fn lock(&self) -> Result<()>;
/// Attempts to acquire a shared lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors
/// [`std::fs::File::try_lock_shared`].
fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;
/// Attempts to acquire an exclusive lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
/// This is the blocking counterpart of [`FileExt::try_lock`].
fn lock(&self) -> Result<()>;
/// Attempts to acquire a shared lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked.
fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;
/// Attempts to acquire an exclusive lock on the file, without blocking.
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib.rs
fn lock_shared(&self) -> Result<()>;

/// Acquires an exclusive lock on the file, blocking until the lock can be
/// acquired. Mirrors [`std::fs::File::lock`].
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This doc comment references std::fs::File::lock, which doesn’t exist, so the intra-doc link will be broken and the statement is misleading. Consider rewording to describe the behavior without referencing a non-existent std method.

Suggested change
/// acquired. Mirrors [`std::fs::File::lock`].
/// acquired.

Copilot uses AI. Check for mistakes.
Comment thread src/lib.rs
Comment on lines +242 to +246
mod sealed {
pub trait Sealed {}

impl<F: Sealed + ?Sized> Sealed for &F {}
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

FileExt/AsyncFileExt/DynAsyncFileExt (and the sealed module / BoxFuture) are currently defined unconditionally, even though earlier in this file and in the PR description they’re described as only being available/meaningful on unix/windows targets (and feature-gated backends). This changes the public API surface on targets like wasm32-wasi*/--no-default-features (e.g. use fs4::*; will now pull in these traits) and makes the “degrades to just FsStats + TryLockError” statement inaccurate. Consider gating these definitions with #[cfg(any(unix, windows))] (and cfg_sync!/cfg_async! as appropriate), or update the docs/description to match the new always-present API.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib.rs
Comment on lines +242 to +279
mod sealed {
pub trait Sealed {}

impl<F: Sealed + ?Sized> Sealed for &F {}
}

/// Extension trait for file which provides allocation and locking methods.
///
/// This trait is sealed and cannot be implemented for types outside of `fs4`.
///
/// ## Notes on File Locks
///
/// This library provides whole-file locks in both shared (read) and exclusive
/// (read-write) varieties.
///
/// File locks are a cross-platform hazard since the file lock APIs exposed by
/// operating system kernels vary in subtle and not-so-subtle ways.
///
/// The API exposed by this library can be safely used across platforms as long
/// as the following rules are followed:
///
/// * Multiple locks should not be created on an individual `File` instance
/// concurrently.
/// * Duplicated files should not be locked without great care.
/// * Files to be locked should be opened with at least read or write
/// permissions.
/// * File locks may only be relied upon to be advisory.
///
/// File locks are released automatically when the file handle is closed (for
/// example when the owning `File` is dropped), so calling [`FileExt::unlock`]
/// explicitly is optional.
///
/// File locks are implemented with
/// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and
/// [`LockFileEx`](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex)
/// on Windows.
pub trait FileExt: sealed::Sealed {
/// Returns the amount of physical space allocated for a file.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

FileExt is now defined without #[cfg(any(unix, windows))]/feature gating, so it becomes part of the public API even on targets where the crate previously compiled down to only FsStats + TryLockError (e.g. wasm32-wasi*) or with --no-default-features. If the intent is to preserve the previous platform/feature-specific API surface (as described in the PR), gate sealed/FileExt (and similarly AsyncFileExt/DynAsyncFileExt/BoxFuture) behind the same cfg conditions used for the backends, or update the docs/PR description to match the new always-present traits.

Copilot uses AI. Check for mistakes.
Comment thread src/lib.rs
///
/// Returns `Ok(())` if the lock was acquired, or
/// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
/// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

[std::fs::File::try_lock] is not a valid rustdoc link/method (no such API in std), so this documentation is currently broken/misleading. Update the reference to a real item or rephrase to describe semantics without linking to std methods.

Suggested change
/// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
/// if the file is currently locked. This is the non-blocking counterpart
/// of [`FileExt::lock`].

Copilot uses AI. Check for mistakes.
@al8n al8n merged commit 5f81cf2 into main Apr 28, 2026
31 checks passed
@al8n al8n deleted the 1.1.0 branch April 28, 2026 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants