Skip to main content

gobby_code/index/
hasher.rs

1//! Content hashing for incremental indexing.
2//! Ports logic from src/gobby/code_index/hasher.py.
3
4use std::path::Path;
5
6/// SHA-256 hash of entire file contents.
7pub fn file_content_hash(path: &Path) -> anyhow::Result<String> {
8    Ok(gobby_core::indexing::file_content_hash(path)?)
9}
10
11/// SHA-256 hash of in-memory file contents.
12pub fn content_hash(source: &[u8]) -> String {
13    gobby_core::indexing::content_hash(source)
14}
15
16/// SHA-256 hash of a byte slice (symbol source).
17pub fn symbol_content_hash(source: &[u8], start: usize, end: usize) -> anyhow::Result<String> {
18    let slice = source.get(start..end).ok_or_else(|| {
19        anyhow::anyhow!(
20            "invalid byte range {}..{} for source len {}",
21            start,
22            end,
23            source.len()
24        )
25    })?;
26    Ok(gobby_core::indexing::content_hash(slice))
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use proptest::prelude::*;
33
34    #[test]
35    fn file_content_hash_delegates_to_gobby_core() {
36        let tmp = tempfile::NamedTempFile::new().expect("tempfile");
37        std::fs::write(tmp.path(), b"hash me\n").expect("write file");
38
39        let actual = file_content_hash(tmp.path()).expect("hash via wrapper");
40        let expected =
41            gobby_core::indexing::file_content_hash(tmp.path()).expect("hash via gobby-core");
42        assert_eq!(actual, expected);
43
44        let source = include_str!("hasher.rs");
45        let delegate = ["gobby_core", "::indexing::file_content_hash"].concat();
46        let local_buffer = format!("let mut buf = [0u8; {}]", 64 * 1024);
47        assert!(source.contains(&delegate));
48        assert!(!source.contains(&local_buffer));
49    }
50
51    #[test]
52    fn content_hash_delegates_to_gobby_core() {
53        let source = b"hash me from memory\n";
54
55        assert_eq!(
56            content_hash(source),
57            gobby_core::indexing::content_hash(source)
58        );
59    }
60
61    proptest! {
62        #[test]
63        fn content_hash_matches_gobby_core_for_arbitrary_bytes(
64            source in proptest::collection::vec(any::<u8>(), 0..4096),
65        ) {
66            prop_assert_eq!(
67                content_hash(&source),
68                gobby_core::indexing::content_hash(&source)
69            );
70        }
71
72        #[test]
73        fn symbol_content_hash_matches_gobby_core_for_valid_slices(
74            source in proptest::collection::vec(any::<u8>(), 0..4096),
75            raw_start in 0usize..4096,
76            raw_len in 0usize..4096,
77        ) {
78            let start = raw_start.min(source.len());
79            let end = (start + raw_len).min(source.len());
80            let actual = symbol_content_hash(&source, start, end).expect("valid slice hashes");
81            let expected = gobby_core::indexing::content_hash(&source[start..end]);
82
83            prop_assert_eq!(actual, expected);
84        }
85    }
86}