Skip to main content

dk_core/
lib.rs

1/// The version of the dk-core crate (set at compile time).
2pub const VERSION: &str = env!("CARGO_PKG_VERSION");
3
4pub mod error;
5pub mod types;
6
7pub use error::{Error, Result};
8pub use types::*;
9
10// ── String sanitization ──
11
12/// Strip null bytes from strings before protobuf/JSON serialization.
13/// Tree-sitter AST parsing can produce null bytes from lossy UTF-8 conversion;
14/// these break protobuf string fields and JSON encoding.
15pub fn sanitize_for_proto(s: &str) -> String {
16    s.replace('\0', "")
17}
18
19// ── Git author helpers ──
20
21/// Strip characters that would corrupt a raw git commit-object header.
22/// Removes null bytes, newlines, and angle brackets (git author/email delimiters).
23fn sanitize_author_field(s: &str) -> String {
24    s.chars()
25        .filter(|c| !matches!(c, '\0' | '\n' | '\r' | '<' | '>'))
26        .collect()
27}
28
29/// Resolve the effective git author name and email for a merge commit.
30/// Falls back to the agent identity when the caller supplies empty or
31/// all-stripped strings. Sanitization runs BEFORE the emptiness check
32/// so that inputs like "\n" correctly fall back to the agent identity.
33pub fn resolve_author(name: &str, email: &str, agent: &str) -> (String, String) {
34    let safe_agent = sanitize_author_field(agent);
35    let sanitized_name = sanitize_author_field(name);
36    let effective_name = if sanitized_name.is_empty() {
37        safe_agent.clone()
38    } else {
39        sanitized_name
40    };
41    let sanitized_email = sanitize_author_field(email);
42    let effective_email = if sanitized_email.is_empty() {
43        format!("{}@dkod.dev", safe_agent)
44    } else {
45        sanitized_email
46    };
47    (effective_name, effective_email)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn sanitize_for_proto_strips_null_bytes() {
56        assert_eq!(sanitize_for_proto("hello\0world"), "helloworld");
57        assert_eq!(sanitize_for_proto("\0\0"), "");
58        assert_eq!(sanitize_for_proto("clean"), "clean");
59    }
60
61    #[test]
62    fn sanitize_for_proto_preserves_valid_utf8() {
63        assert_eq!(sanitize_for_proto("fn résumé()"), "fn résumé()");
64        assert_eq!(sanitize_for_proto("日本語"), "日本語");
65        assert_eq!(sanitize_for_proto("bad\u{FFFD}char"), "bad\u{FFFD}char");
66    }
67
68    #[test]
69    fn resolve_author_uses_supplied_values() {
70        let (name, email) = resolve_author("Alice", "alice@example.com", "agent-1");
71        assert_eq!(name, "Alice");
72        assert_eq!(email, "alice@example.com");
73    }
74
75    #[test]
76    fn resolve_author_falls_back_to_agent() {
77        let (name, email) = resolve_author("", "", "agent-1");
78        assert_eq!(name, "agent-1");
79        assert_eq!(email, "agent-1@dkod.dev");
80    }
81
82    #[test]
83    fn resolve_author_sanitizes_newlines_and_nulls() {
84        let (name, email) = resolve_author("Al\nice\0", "al\rice@\nex.com", "agent-1");
85        assert_eq!(name, "Alice");
86        assert_eq!(email, "alice@ex.com");
87    }
88
89    #[test]
90    fn resolve_author_falls_back_when_input_is_only_stripped_chars() {
91        let (name, email) = resolve_author("\n", "\r\0", "agent-1");
92        assert_eq!(name, "agent-1");
93        assert_eq!(email, "agent-1@dkod.dev");
94    }
95
96    #[test]
97    fn resolve_author_strips_angle_brackets() {
98        let (name, email) = resolve_author("Alice <hacker>", "a<b>c@ex.com", "agent-1");
99        assert_eq!(name, "Alice hacker");
100        assert_eq!(email, "abc@ex.com");
101    }
102}