1mod regex;
7mod secret_scan;
8
9pub use regex::{
10 regex_captures, regex_matches, regex_replace, regex_split, RegexCapture,
11 MAX_REGEX_PATTERN_BYTES,
12};
13pub use secret_scan::{
14 compiled_secret_patterns, scan_secrets, secret_patterns_compiled, CompiledSecretPattern,
15 SecretFinding,
16};
17
18pub fn hex_encode(input: &[u8]) -> String {
19 hex::encode(input)
20}
21
22pub fn hex_decode_text(input: &str) -> Result<String, String> {
23 hex::decode(input.as_bytes())
24 .map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
25 .map_err(|error| format!("hex decode error: {error}"))
26}
27
28pub fn trim_text(input: &str) -> &str {
29 input.trim()
30}
31
32pub fn replace_text(input: &str, old: &str, new: &str) -> String {
33 input.replace(old, new)
34}
35
36pub fn starts_with_text(input: &str, prefix: &str) -> bool {
37 input.starts_with(prefix)
38}
39
40pub fn ends_with_text(input: &str, suffix: &str) -> bool {
41 input.ends_with(suffix)
42}
43
44pub fn sha256_hex(input: &[u8]) -> String {
45 use sha2::Digest;
46 hex::encode(sha2::Sha256::digest(input))
47}
48
49pub fn join_path_segments<I, S>(segments: I) -> String
54where
55 I: IntoIterator<Item = S>,
56 S: AsRef<str>,
57{
58 let mut joined = String::new();
59 for segment in segments {
60 let segment = segment.as_ref().replace('\\', "/");
61 if segment.is_empty() {
62 continue;
63 }
64 let absolute = segment.starts_with('/')
65 || segment
66 .as_bytes()
67 .get(1..3)
68 .is_some_and(|suffix| suffix == b":/");
69 if absolute {
70 joined = segment;
71 continue;
72 }
73 if !joined.is_empty() && !joined.ends_with('/') {
74 joined.push('/');
75 }
76 joined.push_str(segment.trim_start_matches('/'));
77 }
78 joined
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn hex_text_semantics_cover_control_bytes_and_invalid_input() {
87 assert_eq!(hex_encode(b"\0\x01\x02"), "000102");
88 assert_eq!(hex_decode_text("000102").unwrap(), "\0\x01\x02");
89 assert!(hex_decode_text("abc").is_err());
90 }
91
92 #[test]
93 fn string_primitives_preserve_rust_unicode_semantics() {
94 assert_eq!(trim_text(" \tHarn\n"), "Harn");
95 assert_eq!(replace_text("aλa", "a", "β"), "βλβ");
96 assert!(starts_with_text("Portable Harn", "Portable"));
97 assert!(ends_with_text("Portable Harn", "Harn"));
98 assert_eq!(
99 sha256_hex(b"abc"),
100 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
101 );
102 }
103
104 #[test]
105 fn path_join_is_target_independent_for_posix_windows_and_unc_roots() {
106 assert_eq!(
107 join_path_segments(["/repo", ".harn", "state.json"]),
108 "/repo/.harn/state.json"
109 );
110 assert_eq!(
111 join_path_segments([r"C:\repo", ".harn", "state.json"]),
112 "C:/repo/.harn/state.json"
113 );
114 assert_eq!(
115 join_path_segments([r"\\server\share", "state.json"]),
116 "//server/share/state.json"
117 );
118 assert_eq!(
119 join_path_segments(["ignored", "/absolute", "file"]),
120 "/absolute/file"
121 );
122 }
123}