Skip to main content

alef_core/
hash.rs

1//! Content hashing and generated-file headers.
2//!
3//! Every file produced by alef gets a standard header that identifies it as
4//! generated, tells agents/developers how to fix issues, and embeds a blake3
5//! content hash so `alef verify` can detect staleness without external state.
6
7const HASH_PREFIX: &str = "alef:hash:";
8
9/// The standard header text (without comment delimiters).
10/// Used by [`header`] to produce language-specific comment blocks.
11const HEADER_BODY: &str = "\
12This file is auto-generated by alef — DO NOT EDIT.
13To regenerate: alef generate
14To verify freshness: alef verify --exit-code
15Issues & docs: https://github.com/kreuzberg-dev/alef";
16
17/// Comment style for the generated header.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum CommentStyle {
20    /// `// line comment`  (Rust, Go, Java, C#, TypeScript, C, PHP)
21    DoubleSlash,
22    /// `# line comment`   (Python, Ruby, Elixir, R, TOML, Shell, Makefile)
23    Hash,
24    /// `/* block comment */` (C headers)
25    Block,
26}
27
28/// Return the standard alef header as a comment block.
29///
30/// ```text
31/// // This file is auto-generated by alef — DO NOT EDIT.
32/// // To regenerate: alef generate
33/// // To verify freshness: alef verify --exit-code
34/// // Issues & docs: https://github.com/kreuzberg-dev/alef
35/// ```
36pub fn header(style: CommentStyle) -> String {
37    match style {
38        CommentStyle::DoubleSlash => HEADER_BODY.lines().map(|l| format!("// {l}\n")).collect(),
39        CommentStyle::Hash => HEADER_BODY.lines().map(|l| format!("# {l}\n")).collect(),
40        CommentStyle::Block => {
41            let mut out = String::from("/*\n");
42            for line in HEADER_BODY.lines() {
43                out.push_str(&format!(" * {line}\n"));
44            }
45            out.push_str(" */\n");
46            out
47        }
48    }
49}
50
51/// The marker string that `inject_hash_line` and `extract_hash` look for.
52/// Every alef-generated header contains this on the first line.
53const HEADER_MARKER: &str = "auto-generated by alef";
54
55/// Blake3 hash of a content string, returned as hex.
56pub fn hash_content(content: &str) -> String {
57    blake3::hash(content.as_bytes()).to_hex().to_string()
58}
59
60/// Inject an `alef:hash:<hex>` line immediately after the first header marker
61/// line found in the first 10 lines.  The comment syntax is inferred from the
62/// marker line itself.
63///
64/// If no marker line is found, the content is returned unchanged.
65pub fn inject_hash_line(content: &str, hash: &str) -> String {
66    let mut result = String::with_capacity(content.len() + 80);
67    let mut injected = false;
68
69    for (i, line) in content.lines().enumerate() {
70        result.push_str(line);
71        result.push('\n');
72
73        if !injected && i < 10 && line.contains(HEADER_MARKER) {
74            let trimmed = line.trim();
75            let hash_line = if trimmed.starts_with("<!--") {
76                // XML comment: inject hash line as XML comment
77                format!("<!-- {HASH_PREFIX}{hash} -->")
78            } else if trimmed.starts_with("//") {
79                format!("// {HASH_PREFIX}{hash}")
80            } else if trimmed.starts_with('#') {
81                format!("# {HASH_PREFIX}{hash}")
82            } else if trimmed.starts_with("/*") || trimmed.starts_with(" *") || trimmed.ends_with("*/") {
83                format!(" * {HASH_PREFIX}{hash}")
84            } else {
85                format!("// {HASH_PREFIX}{hash}")
86            };
87            result.push_str(&hash_line);
88            result.push('\n');
89            injected = true;
90        }
91    }
92
93    // Preserve original trailing-newline behavior.
94    if !content.ends_with('\n') && result.ends_with('\n') {
95        result.pop();
96    }
97
98    result
99}
100
101/// Extract the hash from an `alef:hash:<hex>` token in the first 10 lines.
102pub fn extract_hash(content: &str) -> Option<String> {
103    for (i, line) in content.lines().enumerate() {
104        if i >= 10 {
105            break;
106        }
107        if let Some(pos) = line.find(HASH_PREFIX) {
108            let rest = &line[pos + HASH_PREFIX.len()..];
109            // Trim trailing comment closers and whitespace.
110            let hex = rest.trim().trim_end_matches("*/").trim_end_matches("-->").trim();
111            if !hex.is_empty() {
112                return Some(hex.to_string());
113            }
114        }
115    }
116    None
117}
118
119/// Strip the `alef:hash:` line from content (for fallback comparison).
120pub fn strip_hash_line(content: &str) -> String {
121    let mut result = String::with_capacity(content.len());
122    for line in content.lines() {
123        if line.contains(HASH_PREFIX) {
124            continue;
125        }
126        result.push_str(line);
127        result.push('\n');
128    }
129    // Preserve original trailing-newline behavior.
130    if !content.ends_with('\n') && result.ends_with('\n') {
131        result.pop();
132    }
133    result
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_header_double_slash() {
142        let h = header(CommentStyle::DoubleSlash);
143        assert!(h.contains("// This file is auto-generated by alef"));
144        assert!(h.contains("// Issues & docs: https://github.com/kreuzberg-dev/alef"));
145    }
146
147    #[test]
148    fn test_header_hash() {
149        let h = header(CommentStyle::Hash);
150        assert!(h.contains("# This file is auto-generated by alef"));
151    }
152
153    #[test]
154    fn test_header_block() {
155        let h = header(CommentStyle::Block);
156        assert!(h.starts_with("/*\n"));
157        assert!(h.contains(" * This file is auto-generated by alef"));
158        assert!(h.ends_with(" */\n"));
159    }
160
161    #[test]
162    fn test_inject_and_extract_rust() {
163        let h = header(CommentStyle::DoubleSlash);
164        let content = format!("{h}use foo;\n");
165        let hash = hash_content(&content);
166        let injected = inject_hash_line(&content, &hash);
167        assert!(injected.contains(HASH_PREFIX));
168        assert_eq!(extract_hash(&injected), Some(hash));
169    }
170
171    #[test]
172    fn test_inject_and_extract_python() {
173        let h = header(CommentStyle::Hash);
174        let content = format!("{h}import foo\n");
175        let hash = hash_content(&content);
176        let injected = inject_hash_line(&content, &hash);
177        assert!(injected.contains(&format!("# {HASH_PREFIX}")));
178        assert_eq!(extract_hash(&injected), Some(hash));
179    }
180
181    #[test]
182    fn test_inject_and_extract_c_block() {
183        let h = header(CommentStyle::Block);
184        let content = format!("{h}#include <stdio.h>\n");
185        let hash = hash_content(&content);
186        let injected = inject_hash_line(&content, &hash);
187        assert!(injected.contains(HASH_PREFIX));
188        assert_eq!(extract_hash(&injected), Some(hash));
189    }
190
191    #[test]
192    fn test_inject_php_line2() {
193        let h = header(CommentStyle::DoubleSlash);
194        let content = format!("<?php\n{h}namespace Foo;\n");
195        let hash = hash_content(&content);
196        let injected = inject_hash_line(&content, &hash);
197        let lines: Vec<&str> = injected.lines().collect();
198        assert_eq!(lines[0], "<?php");
199        assert!(lines[1].contains(HEADER_MARKER));
200        assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
201        assert_eq!(extract_hash(&injected), Some(hash));
202    }
203
204    #[test]
205    fn test_no_header_returns_unchanged() {
206        let content = "fn main() {}\n";
207        let injected = inject_hash_line(content, "abc123");
208        assert_eq!(injected, content);
209        assert_eq!(extract_hash(&injected), None);
210    }
211
212    #[test]
213    fn test_strip_hash_line() {
214        let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
215        let stripped = strip_hash_line(content);
216        assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
217    }
218
219    #[test]
220    fn test_roundtrip() {
221        let h = header(CommentStyle::Hash);
222        let original = format!("{h}import sys\n");
223        let hash = hash_content(&original);
224        let injected = inject_hash_line(&original, &hash);
225        let stripped = strip_hash_line(&injected);
226        assert_eq!(stripped, original);
227        assert_eq!(hash_content(&stripped), hash);
228    }
229}