From d2b2b7a26f98c9dfc329a2b887dedab1567066a5 Mon Sep 17 00:00:00 2001 From: krakotay Date: Fri, 17 Jul 2026 07:58:02 +0800 Subject: [PATCH] Move CSS hashing back to bun_css --- Cargo.lock | 3 -- src/base64/Cargo.toml | 5 --- src/base64/lib.rs | 55 ------------------------------ src/bundler/LinkerContext.rs | 2 +- src/css/css_modules.rs | 66 +++++++++++++++++++++++++++++++----- 5 files changed, 59 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9e9083c85b3..4ab28a8aa438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,12 +242,9 @@ version = "0.0.0" dependencies = [ "bitflags", "bstr", - "bumpalo", - "bun_alloc", "bun_collections", "bun_core", "bun_simdutf_sys", - "bun_wyhash", "const_format", "enum-map", "enumset", diff --git a/src/base64/Cargo.toml b/src/base64/Cargo.toml index 22733158267d..5c3c8ccbba1f 100644 --- a/src/base64/Cargo.toml +++ b/src/base64/Cargo.toml @@ -24,8 +24,3 @@ thiserror.workspace = true bun_simdutf_sys.workspace = true bun_collections.workspace = true -# `wyhash_url_safe` (CSS-modules / dependency placeholder hasher) — leaf crates, -# no cycle. -bun_wyhash.workspace = true -bumpalo.workspace = true -bun_alloc.workspace = true diff --git a/src/base64/lib.rs b/src/base64/lib.rs index 3a48546c0cdb..50c2fffaec3f 100644 --- a/src/base64/lib.rs +++ b/src/base64/lib.rs @@ -979,58 +979,3 @@ pub mod zig_base64 { } } } - -// ────────────────────────────────────────────────────────────────────────── -// LAYERING: hoisted from `bun_css::css_modules::hash` so `bun_bundler` can -// call the *same* implementation without taking a hard dep on `bun_css` (and -// without re-implementing the hash, which would diverge — see review of -// `LinkerContext.rs::css_modules_hash_shim`). `bun_css` re-exports this as -// `css_modules::hash` for its in-crate callers. -// -// Behavior: wyhash(u64) of the formatted args, -// truncated to u32, url-safe-base64-encoded into a bump-allocated slice. If -// `at_start` and the first encoded byte is a digit, prefix `_` (CSS idents -// can't start with a digit). -// ────────────────────────────────────────────────────────────────────────── - -// TODO: replace with bun's hash -pub fn wyhash_url_safe<'a>( - bump: &'a bun_alloc::Arena, - args: core::fmt::Arguments<'_>, - at_start: bool, -) -> &'a [u8] { - use std::io::Write as _; - - let mut hasher = bun_wyhash::Wyhash11::init(0); - // Write into a scratch Vec then hash; freed immediately. - let mut fmt_str: Vec = Vec::with_capacity(128); - write!(&mut fmt_str, "{}", args).expect("unreachable"); - hasher.update(&fmt_str); - - let h: u32 = hasher.final_() as u32; // @truncate - let h_bytes: [u8; 4] = h.to_le_bytes(); - - let encode_len = simdutf_encode_len_url_safe(h_bytes.len()); - - // Always alloc a fresh slice from the arena. - // PERF: no buffer reuse for large encode_len — profile if hot. - let slice_to_write: &mut [u8] = - bump.alloc_slice_fill_default(encode_len + usize::from(at_start)); - - let base64_encoded_hash_len = encode_url_safe(slice_to_write, &h_bytes); - - let base64_encoded_hash = &slice_to_write[0..base64_encoded_hash_len]; - - if at_start - && !base64_encoded_hash.is_empty() - && base64_encoded_hash[0] >= b'0' - && base64_encoded_hash[0] <= b'9' - { - // Overlapping copy, dest > src → copy_within. - slice_to_write.copy_within(0..base64_encoded_hash_len, 1); - slice_to_write[0] = b'_'; - return &slice_to_write[0..base64_encoded_hash_len + 1]; - } - - &slice_to_write[0..base64_encoded_hash_len] -} diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index ce75be300a4c..124854299c2f 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -2397,7 +2397,7 @@ impl<'a> LinkerContext<'a> { let original_name: &[u8] = symbol.original_name.slice(); // The hash itself is short-lived; use a scratch bump. let scratch = ::bun_alloc::Arena::new(); - let path_hash = ::bun_base64::wyhash_url_safe( + let path_hash = ::bun_css::css_modules::hash( &scratch, // use path relative to cwd for determinism format_args!("{}", bstr::BStr::new(&source.path.pretty)), diff --git a/src/css/css_modules.rs b/src/css/css_modules.rs index 2fa394cd183a..7e38c70d52fc 100644 --- a/src/css/css_modules.rs +++ b/src/css/css_modules.rs @@ -1,4 +1,4 @@ -use core::fmt::Arguments; +use core::fmt::{Arguments, Write as _}; use bun_alloc::Arena as Bump; use bun_alloc::{ArenaVec as BumpVec, ArenaVecExt as _}; @@ -465,12 +465,62 @@ impl<'a> CssModuleReference<'a> { } } -/// LAYERING: canonical implementation lives in `bun_base64::wyhash_url_safe` -/// (a leaf crate) so `bun_bundler::LinkerContext::mangle_local_css` can call -/// the *same* hasher without depending on `bun_css`. Re-export here so -/// in-crate callers (`dependencies.rs`, `rules/import.rs`) keep the -/// `css_modules::hash` path. -#[inline] +struct WyhashWriter<'a>(&'a mut bun_wyhash::Wyhash11); + +impl core::fmt::Write for WyhashWriter<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.0.update(s.as_bytes()); + Ok(()) + } +} + +/// Formats the arguments into the legacy Wyhash11 algorithm and encodes its +/// low 32 bits as unpadded base64url. This output is embedded in generated CSS +/// names and dependency placeholders, so the algorithm and byte order are +/// compatibility-sensitive. pub fn hash<'a>(bump: &'a Bump, args: Arguments<'_>, at_start: bool) -> &'a [u8] { - bun_base64::wyhash_url_safe(bump, args, at_start) + let mut hasher = bun_wyhash::Wyhash11::init(0); + WyhashWriter(&mut hasher) + .write_fmt(args) + .expect("infallible: WyhashWriter::write_str always succeeds"); + + let hash_bytes = (hasher.final_() as u32).to_le_bytes(); + let encoded_len = bun_base64::url_safe_encode_len(&hash_bytes); + let encoded_start = usize::from(at_start); + let output = bump.alloc_slice_fill_default(encoded_start + encoded_len); + let written = bun_base64::encode_url_safe(&mut output[encoded_start..], &hash_bytes); + debug_assert_eq!(written, encoded_len); + + if at_start && output[encoded_start].is_ascii_digit() { + output[0] = b'_'; + &output[..encoded_start + written] + } else { + &output[encoded_start..encoded_start + written] + } +} + +#[cfg(test)] +mod hash_tests { + use super::*; + + #[test] + fn formatted_fragments_match_one_shot_hash() { + const INPUT: &[u8] = b"0123456789abcdefghijklmnopqrstu_more-than-one-wyhash-block"; + + let bump = Bump::new(); + let actual = hash( + &bump, + format_args!( + "{}_{}", + bstr::BStr::new(b"0123456789abcdefghijklmnopqrstu"), + bstr::BStr::new(b"more-than-one-wyhash-block") + ), + false, + ); + + let hash_bytes = (bun_wyhash::Wyhash11::hash(0, INPUT) as u32).to_le_bytes(); + let mut expected = [0; 6]; + let written = bun_base64::encode_url_safe(&mut expected, &hash_bytes); + assert_eq!(actual, &expected[..written]); + } }