feat: first cut on M0 - base structure of project
This commit is contained in:
48
crates/bds-core/src/util/checksum.rs
Normal file
48
crates/bds-core/src/util/checksum.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Compute a hex-encoded SHA-256 hash of the given content.
|
||||
pub fn content_hash(content: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(content);
|
||||
let result = hasher.finalize();
|
||||
hex::encode(result)
|
||||
}
|
||||
|
||||
// sha2 doesn't include hex encoding, so we use a tiny inline helper.
|
||||
mod hex {
|
||||
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
|
||||
|
||||
pub fn encode(bytes: impl AsRef<[u8]>) -> String {
|
||||
let bytes = bytes.as_ref();
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
s.push(HEX_CHARS[(b >> 4) as usize] as char);
|
||||
s.push(HEX_CHARS[(b & 0xf) as usize] as char);
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_hash() {
|
||||
// SHA-256 of "hello"
|
||||
let hash = content_hash(b"hello");
|
||||
assert_eq!(
|
||||
hash,
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_hash() {
|
||||
let hash = content_hash(b"");
|
||||
assert_eq!(
|
||||
hash,
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
);
|
||||
}
|
||||
}
|
||||
5
crates/bds-core/src/util/mod.rs
Normal file
5
crates/bds-core/src/util/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod slug;
|
||||
mod checksum;
|
||||
|
||||
pub use slug::slugify;
|
||||
pub use checksum::content_hash;
|
||||
61
crates/bds-core/src/util/slug.rs
Normal file
61
crates/bds-core/src/util/slug.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use deunicode::deunicode;
|
||||
|
||||
/// Generate a URL-safe slug from a title string.
|
||||
///
|
||||
/// Transliterates Unicode to ASCII, lowercases, replaces non-alphanumeric
|
||||
/// chars with hyphens, and collapses/trims hyphens.
|
||||
pub fn slugify(input: &str) -> String {
|
||||
let ascii = deunicode(input);
|
||||
let lowered = ascii.to_lowercase();
|
||||
let mut slug = String::with_capacity(lowered.len());
|
||||
let mut prev_hyphen = true; // avoid leading hyphen
|
||||
for c in lowered.chars() {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
slug.push(c);
|
||||
prev_hyphen = false;
|
||||
} else if !prev_hyphen {
|
||||
slug.push('-');
|
||||
prev_hyphen = true;
|
||||
}
|
||||
}
|
||||
// Trim trailing hyphen
|
||||
if slug.ends_with('-') {
|
||||
slug.pop();
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_slug() {
|
||||
assert_eq!(slugify("Hello World"), "hello-world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_slug() {
|
||||
assert_eq!(slugify("Über die Brücke"), "uber-die-brucke");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn special_chars() {
|
||||
assert_eq!(slugify("What's up? (2024)"), "what-s-up-2024");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_clean() {
|
||||
assert_eq!(slugify("already-clean"), "already-clean");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input() {
|
||||
assert_eq!(slugify(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_special_chars() {
|
||||
assert_eq!(slugify("a --- b"), "a-b");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user