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 pick_fields<'a, V>(
21 keys: impl IntoIterator<Item = &'a str>,
22 mut lookup: impl FnMut(&str) -> Option<V>,
23) -> std::collections::BTreeMap<String, V> {
24 let mut selected = std::collections::BTreeMap::new();
25 for key in keys {
26 if !selected.contains_key(key) {
27 if let Some(value) = lookup(key) {
28 selected.insert(key.to_owned(), value);
29 }
30 }
31 }
32 selected
33}
34
35pub fn hex_encode(input: &[u8]) -> String {
36 hex::encode(input)
37}
38
39pub fn hex_decode_text(input: &str) -> Result<String, String> {
40 hex::decode(input.as_bytes())
41 .map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
42 .map_err(|error| format!("hex decode error: {error}"))
43}
44
45pub fn trim_text(input: &str) -> &str {
46 input.trim()
47}
48
49pub fn replace_text(input: &str, old: &str, new: &str) -> String {
50 input.replace(old, new)
51}
52
53pub fn starts_with_text(input: &str, prefix: &str) -> bool {
54 input.starts_with(prefix)
55}
56
57pub fn ends_with_text(input: &str, suffix: &str) -> bool {
58 input.ends_with(suffix)
59}
60
61pub fn sha256_hex(input: &[u8]) -> String {
62 use sha2::Digest;
63 hex::encode(sha2::Sha256::digest(input))
64}
65
66pub fn join_path_segments<I, S>(segments: I) -> String
71where
72 I: IntoIterator<Item = S>,
73 S: AsRef<str>,
74{
75 let mut joined = String::new();
76 for segment in segments {
77 let segment = segment.as_ref().replace('\\', "/");
78 if segment.is_empty() {
79 continue;
80 }
81 let absolute = segment.starts_with('/')
82 || segment
83 .as_bytes()
84 .get(1..3)
85 .is_some_and(|suffix| suffix == b":/");
86 if absolute {
87 joined = segment;
88 continue;
89 }
90 if !joined.is_empty() && !joined.ends_with('/') {
91 joined.push('/');
92 }
93 joined.push_str(segment.trim_start_matches('/'));
94 }
95 joined
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn hex_text_semantics_cover_control_bytes_and_invalid_input() {
104 assert_eq!(hex_encode(b"\0\x01\x02"), "000102");
105 assert_eq!(hex_decode_text("000102").unwrap(), "\0\x01\x02");
106 assert!(hex_decode_text("abc").is_err());
107 }
108
109 #[test]
110 fn string_primitives_preserve_rust_unicode_semantics() {
111 assert_eq!(trim_text(" \tHarn\n"), "Harn");
112 assert_eq!(replace_text("aλa", "a", "β"), "βλβ");
113 assert!(starts_with_text("Portable Harn", "Portable"));
114 assert!(ends_with_text("Portable Harn", "Harn"));
115 assert_eq!(
116 sha256_hex(b"abc"),
117 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
118 );
119 }
120
121 #[test]
122 fn path_join_is_target_independent_for_posix_windows_and_unc_roots() {
123 assert_eq!(
124 join_path_segments(["/repo", ".harn", "state.json"]),
125 "/repo/.harn/state.json"
126 );
127 assert_eq!(
128 join_path_segments([r"C:\repo", ".harn", "state.json"]),
129 "C:/repo/.harn/state.json"
130 );
131 assert_eq!(
132 join_path_segments([r"\\server\share", "state.json"]),
133 "//server/share/state.json"
134 );
135 assert_eq!(
136 join_path_segments(["ignored", "/absolute", "file"]),
137 "/absolute/file"
138 );
139 }
140}