const fn path_byte(value: u8, normalize_path: bool) -> u8 {
if normalize_path && value == b'\\' {
b'/'
} else {
value
}
}
const fn sha256_digest(
first: &[u8],
second: &[u8],
separator: bool,
normalize_path: bool,
) -> [u8; 32] {
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
let separator_len = if separator { 1 } else { 0 };
let length = first.len() + separator_len + second.len();
let blocks = (length + 9).div_ceil(64);
let padded = blocks * 64;
let bits = (length as u64) * 8;
let mut state = [
0x6a09e667_u32,
0xbb67ae85,
0x3c6ef372,
0xa54ff53a,
0x510e527f,
0x9b05688c,
0x1f83d9ab,
0x5be0cd19,
];
let mut block = 0;
while block < blocks {
let mut words = [0_u32; 64];
let mut i = 0;
while i < 16 {
let mut value = 0_u32;
let mut byte = 0;
while byte < 4 {
let offset = block * 64 + i * 4 + byte;
let input = if offset < length {
if offset < first.len() {
path_byte(first[offset], normalize_path)
} else if separator && offset == first.len() {
0
} else {
path_byte(second[offset - first.len() - separator_len], normalize_path)
}
} else if offset == length {
0x80
} else if offset >= padded - 8 {
((bits >> ((padded - 1 - offset) * 8)) & 0xff) as u8
} else {
0
};
value |= (input as u32) << (24 - byte * 8);
byte += 1;
}
words[i] = value;
i += 1;
}
while i < 64 {
let a = words[i - 15].rotate_right(7)
^ words[i - 15].rotate_right(18)
^ (words[i - 15] >> 3);
let b = words[i - 2].rotate_right(17)
^ words[i - 2].rotate_right(19)
^ (words[i - 2] >> 10);
words[i] = words[i - 16]
.wrapping_add(a)
.wrapping_add(words[i - 7])
.wrapping_add(b);
i += 1;
}
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h) = (
state[0], state[1], state[2], state[3], state[4], state[5], state[6], state[7],
);
i = 0;
while i < 64 {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let t1 = h
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(K[i])
.wrapping_add(words[i]);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let maj = (a & b) ^ (a & c) ^ (b & c);
let t2 = s0.wrapping_add(maj);
h = g;
g = f;
f = e;
e = d.wrapping_add(t1);
d = c;
c = b;
b = a;
a = t1.wrapping_add(t2);
i += 1;
}
state[0] = state[0].wrapping_add(a);
state[1] = state[1].wrapping_add(b);
state[2] = state[2].wrapping_add(c);
state[3] = state[3].wrapping_add(d);
state[4] = state[4].wrapping_add(e);
state[5] = state[5].wrapping_add(f);
state[6] = state[6].wrapping_add(g);
state[7] = state[7].wrapping_add(h);
block += 1;
}
let mut output = [0_u8; 32];
let mut i = 0;
while i < 8 {
let bytes = state[i].to_be_bytes();
output[i * 4] = bytes[0];
output[i * 4 + 1] = bytes[1];
output[i * 4 + 2] = bytes[2];
output[i * 4 + 3] = bytes[3];
i += 1;
}
output
}
pub(super) const fn sha256_prefix(first: &[u8], second: &[u8], separator: bool) -> [u8; 16] {
let digest = sha256_digest(first, second, separator, false);
sha256_prefix_of(digest)
}
pub(super) const fn sha256_path_prefix(first: &[u8], second: &[u8], separator: bool) -> [u8; 16] {
let digest = sha256_digest(first, second, separator, true);
sha256_prefix_of(digest)
}
const fn sha256_prefix_of(digest: [u8; 32]) -> [u8; 16] {
let mut output = [0_u8; 16];
let mut i = 0;
while i < 16 {
output[i] = digest[i];
i += 1;
}
output
}
pub fn sha256_hex(input: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let digest = sha256_digest(input, &[], false, false);
let mut out = String::with_capacity(64);
for byte in digest {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub(super) const fn is_separator(byte: u8) -> bool {
byte == b'/' || byte == b'\\'
}
pub(super) const fn strip_leading_separator(value: &str) -> &str {
let bytes = value.as_bytes();
if bytes.is_empty() || !is_separator(bytes[0]) {
return value;
}
let (_, rest) = bytes.split_at(1);
match core::str::from_utf8(rest) {
Ok(text) => text,
Err(_) => value,
}
}
#[cfg(test)]
mod tests {
use super::{sha256_hex, sha256_path_prefix, sha256_prefix};
fn oracle(input: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(input);
hasher.finalize().into()
}
fn hex(bytes: &[u8]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(DIGITS[(byte >> 4) as usize] as char);
out.push(DIGITS[(byte & 0x0f) as usize] as char);
}
out
}
fn filler(length: usize) -> Vec<u8> {
(0..length)
.map(|index| (index as u8).wrapping_mul(31).wrapping_add(7))
.collect()
}
#[test]
fn matches_sha2_across_lengths_and_padding_boundaries() {
let mut lengths: Vec<usize> = (0..=200).collect();
lengths.extend([55, 56, 57, 63, 64, 65, 119, 120, 127, 128, 1_000, 1_048_576]);
for length in lengths {
let input = filler(length);
assert_eq!(
sha256_hex(&input),
hex(&oracle(&input)),
"single-slice digest differed at length {length}"
);
}
}
#[test]
fn prefix_matches_sha2_over_the_two_slice_composition() {
let pairs: [(&[u8], &[u8]); 5] = [
(b"control/control.rs", b"Control"),
(b"app", b"control/control.rs"),
(b"", b"root"),
(b"<root>", b"root"),
(b"a", b"bc"),
];
for (first, second) in pairs {
let mut separated = first.to_vec();
separated.push(0);
separated.extend_from_slice(second);
let mut joined = first.to_vec();
joined.extend_from_slice(second);
assert_eq!(
sha256_prefix(first, second, true),
oracle(&separated)[..16],
"separated composition differed for {first:?} / {second:?}"
);
assert_eq!(
sha256_prefix(first, second, false),
oracle(&joined)[..16],
"unseparated composition differed for {first:?} / {second:?}"
);
}
}
#[test]
fn path_prefix_matches_sha2_over_folded_two_slice_composition() {
let pairs: [(&[u8], &[u8]); 3] = [
(b"app", b"control\\control.rs"),
(b"a\\b", b"c\\d"),
(b"control/control.rs", b"Control"),
];
for (first, second) in pairs {
let mut folded_first = first.to_vec();
for byte in &mut folded_first {
if *byte == b'\\' {
*byte = b'/';
}
}
let mut folded_second = second.to_vec();
for byte in &mut folded_second {
if *byte == b'\\' {
*byte = b'/';
}
}
let mut folded = folded_first;
folded.push(0);
folded.extend_from_slice(&folded_second);
assert_eq!(
sha256_path_prefix(first, second, true),
oracle(&folded)[..16],
"folded composition differed for {first:?} / {second:?}"
);
}
}
}