Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 63 additions & 16 deletions src/jsc/webcore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,16 +778,17 @@
// ────────────────────────────────────────────────────────────────────

/// A blob store referencing a file on disk.
#[derive(Clone)]
pub struct File {
pub pathlike: PathOrFileDescriptor,
pub mime_type: MimeType,
pub is_atty: Option<bool>,
pub mode: bun_sys::Mode,
pub seekable: Option<bool>,
pub max_size: SizeType,
/// Milliseconds since ECMAScript epoch.
pub last_modified: crate::JSTimeType,
/// Milliseconds since ECMAScript epoch. Atomic: worker-thread
/// `ReadFile` tasks write it while the JS thread reads it
/// (overlapping `file.bytes()` calls share one `Store`).
Comment thread
robobun marked this conversation as resolved.
pub last_modified: core::sync::atomic::AtomicU64,
}

impl Default for File {
Expand All @@ -799,7 +800,26 @@
mode: 0,
seekable: None,
max_size: MAX_SIZE,
last_modified: crate::INIT_TIMESTAMP,
last_modified: core::sync::atomic::AtomicU64::new(crate::INIT_TIMESTAMP),
}
}
}

impl Clone for File {
fn clone(&self) -> Self {
Self {
pathlike: self.pathlike.clone(),
mime_type: self.mime_type.clone(),
is_atty: self.is_atty,
mode: self.mode,
seekable: self.seekable,
max_size: self.max_size,
// Snapshot the atomic via `Relaxed`; `Clone` is a per-thread
// value copy, not a memory-ordering sync point.
Comment thread
robobun marked this conversation as resolved.
last_modified: core::sync::atomic::AtomicU64::new(
self.last_modified
.load(core::sync::atomic::Ordering::Relaxed),
),
}
}
}
Expand Down Expand Up @@ -1048,15 +1068,21 @@
core::mem::ManuallyDrop::new(self).ptr.as_ptr()
}

/// Mutable access to `data` through the shared handle. The caller
/// must ensure no
/// other `&mut` to the same `Store` is live (single-threaded JS
/// event-loop discipline).
/// Mutable access to `data` through the shared handle.
///
/// # Safety
/// No other reference (`&Store`, `&mut Store`, `&Data`, `&mut Data`)
/// to the same pointee may be live for the duration of the returned
/// borrow — on this thread or any other. The same contract governs
/// the sibling `unsafe fn`s that mint `&mut Store` access:
/// `blob_store_mut`/`set_blob_content_type` in `webcore::body`, and
/// `BlobExt::shared_view_raw`/`set_is_ascii_flag`/`resolve_file_stat`
/// in `webcore::blob`.

Check warning on line 1080 in src/jsc/webcore_types.rs

View check run for this annotation

Claude / Claude Code Review

data_mut doc groups resolve_file_stat under BlobExt:: prefix, but it is a free function

The `data_mut` `# Safety` doc's sibling enumeration reads "`BlobExt::shared_view_raw`/`set_is_ascii_flag`/`resolve_file_stat` in `webcore::blob`", but `resolve_file_stat` is a module-level free `unsafe fn resolve_file_stat(store: &StoreRef)` (Blob.rs:6203), not a `BlobExt` method — the slash-grouping mis-scopes it under the `BlobExt::` prefix. Suggest "`BlobExt::shared_view_raw`/`set_is_ascii_flag` and `resolve_file_stat` in `webcore::blob`". Comment accuracy only — no runtime impact.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
#[allow(clippy::mut_from_ref)]
pub fn data_mut(&self) -> &mut Data {
// SAFETY: caller guarantees no other `&mut` to this `Store` is
// live; see doc comment.
pub unsafe fn data_mut(&self) -> &mut Data {
// SAFETY: precondition — no aliasing `&`/`&mut` to the pointee is
// live for the returned borrow's duration (see fn doc).
unsafe { &mut (*self.as_ptr()).data }
}
}
Expand Down Expand Up @@ -1107,11 +1133,32 @@
}
impl Eq for StoreRef {}

// SAFETY: `Store`'s refcount is atomic and its payload is either
// immutable-after-init or guarded by callers.
// SAFETY: `Store`'s refcount is atomic; the `Data` payload is mutated
// only under `data_mut`'s exclusivity precondition (move, don't share).
// CAVEAT — `Data::S3` holds `Rc<S3Credentials>` (non-atomic refcount,
// shared with JS-thread state via `Rc::clone(s3.get_credentials())` in
// `Blob.rs`), but worker-pool tasks only carry `Data::File`/`Data::Bytes`
// stores; S3 I/O stays on the JS thread. If an S3 store ever crosses
// threads, make that `Rc` an `Arc`.
unsafe impl Send for StoreRef {}
// SAFETY: `Store::ref_count` is atomic and `&StoreRef` only derefs to
// `&Store`.
unsafe impl Sync for StoreRef {}
// Intentionally NOT `Sync`: two threads sharing `&StoreRef` could each
// mint `&mut Data` via `data_mut`. Dropping `Sync` closes that direct
// shape; cloned handles (`Send`) and `Blob: Sync` still route around
// it, so the load-bearing guard remains `data_mut`'s precondition,
// discharged in writing at every call site.
Comment thread
robobun marked this conversation as resolved.

// Compile-time trip-wire: if `StoreRef` ever gains `Sync`, both blanket
// impls of `_NotSyncCheck` apply and `_NOT_SYNC` fails to compile with
// "conflicting impls" (same pattern as
// `src/runtime/shell/subproc.rs` `__pipe_reader_thread_confined`).
Comment thread
robobun marked this conversation as resolved.
mod __store_ref_not_sync {
use super::StoreRef;
trait _NotSyncCheck<A> {
const OK: () = ();
}
impl<T: ?Sized> _NotSyncCheck<()> for T {}
impl<T: ?Sized + Sync> _NotSyncCheck<u8> for T {}
const _NOT_SYNC: () = <StoreRef as _NotSyncCheck<_>>::OK;
}
}
pub use store::{Store, StoreRef};
Loading
Loading