Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/base64/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 0 additions & 55 deletions src/base64/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> = 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]
}
2 changes: 1 addition & 1 deletion src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
66 changes: 58 additions & 8 deletions src/css/css_modules.rs
Original file line number Diff line number Diff line change
@@ -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 _};
Expand Down Expand Up @@ -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]);
}
}