1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
//! Regression tests for the core path/case text primitives:
//! `winpath` drive-letter classification, `ascii_ci` case-insensitive
//! affix/substring matching, and `git_lfs` pointer recognition.
//!
//! Every assertion pins a CONCRETE expected boolean so a semantic drift in any
//! primitive (e.g. the `has_windows_drive_prefix` vs `is_windows_absolute`
//! same-name-divergence trap, or ASCII-only case folding leaking into
//! non-ASCII bytes) fails loudly here.
use keyhog_core::ascii_ci::{
contains_bytes_ignore_ascii_case, contains_ignore_ascii_case, ends_with_ignore_ascii_case,
starts_with_ignore_ascii_case,
};
use keyhog_core::git_lfs::{
is_git_lfs_oid_line, is_git_lfs_pointer, is_git_lfs_size_line, is_git_lfs_version_line,
GIT_LFS_VERSION_LINE,
};
use keyhog_core::winpath::{has_windows_drive_prefix, is_windows_absolute};
// ---------------------------------------------------------------------------
// winpath: the two DISTINCT predicates and their exact divergence.
// ---------------------------------------------------------------------------
/// The load-bearing divergence table. For each input the BROAD predicate
/// (`has_windows_drive_prefix`) and the STRICT predicate (`is_windows_absolute`)
/// must return exactly these booleans. `C:\`/`C:/` are both true/true; the
/// drive-relative `C:relative` is the trap case: prefix=true, absolute=false.
#[test]
fn winpath_divergence_table_exact() {
// (input, expected has_windows_drive_prefix, expected is_windows_absolute)
let cases: &[(&str, bool, bool)] = &[
("C:\\Windows\\System32", true, true), // backslash absolute
("C:/Users/me", true, true), // forward-slash absolute
("C:relative", true, false), // drive-relative: prefix yes, absolute NO
("C:", true, false), // bare drive: prefix yes, absolute NO
("/unix/path", false, false), // unix absolute: neither
("bare", false, false), // bare relative: neither
];
for &(input, expect_prefix, expect_abs) in cases {
assert_eq!(
has_windows_drive_prefix(input),
expect_prefix,
"has_windows_drive_prefix({input:?})"
);
assert_eq!(
is_windows_absolute(input),
expect_abs,
"is_windows_absolute({input:?})"
);
}
}
/// `C:\` and `C:/` are the only two absolute forms; the strict predicate treats
/// both separators identically and rejects a non-separator third byte.
#[test]
fn winpath_absolute_requires_separator_third_byte() {
assert!(is_windows_absolute("C:\\a"));
assert!(is_windows_absolute("C:/a"));
// Third byte is a letter, not a separator -> drive-relative, not absolute.
assert!(!is_windows_absolute("C:x"));
// ...but it IS still drive-prefixed.
assert!(has_windows_drive_prefix("C:x"));
}
/// The strict set is a strict SUBSET of the broad set: every absolute path is
/// drive-prefixed, but not vice versa. Verified as an implication over inputs.
#[test]
fn winpath_strict_implies_broad() {
let absolutes = ["C:/a", "C:\\a", "z:/x", "D:\\dir\\file"];
for p in absolutes {
assert!(is_windows_absolute(p), "expected absolute: {p:?}");
assert!(
has_windows_drive_prefix(p),
"absolute must imply prefix: {p:?}"
);
}
// Drive-prefixed but NOT absolute -> the subset is strict.
for p in ["C:", "C:rel", "q:file"] {
assert!(has_windows_drive_prefix(p), "expected prefix: {p:?}");
assert!(!is_windows_absolute(p), "must NOT be absolute: {p:?}");
}
}
/// Adversarial/boundary: a leading digit is not a drive letter, a leading colon
/// is not a prefix, empty is neither, and lowercase drive letters are accepted.
#[test]
fn winpath_boundary_and_adversarial() {
assert!(!has_windows_drive_prefix("1:oops")); // digit is not alphabetic
assert!(!is_windows_absolute("1:/oops"));
assert!(!has_windows_drive_prefix(":C")); // colon first
assert!(!has_windows_drive_prefix("")); // empty
assert!(!is_windows_absolute("")); // empty
assert!(!has_windows_drive_prefix("C")); // single letter, no colon
assert!(has_windows_drive_prefix("z:file")); // lowercase drive letter
assert!(is_windows_absolute("d:/data")); // lowercase absolute
}
// ---------------------------------------------------------------------------
// ascii_ci: starts_with / ends_with / contains, ASCII case only.
// ---------------------------------------------------------------------------
/// starts_with_ignore_ascii_case folds ASCII case for the prefix, and an empty
/// prefix always matches while an over-long prefix never does.
#[test]
fn ascii_ci_starts_with_exact() {
assert!(starts_with_ignore_ascii_case("HTTPS://host", "https"));
assert!(starts_with_ignore_ascii_case("Bearer TOKEN", "bEaReR"));
assert!(!starts_with_ignore_ascii_case("Authorization", "bearer"));
// Empty prefix always matches.
assert!(starts_with_ignore_ascii_case("anything", ""));
// Prefix longer than value never matches.
assert!(!starts_with_ignore_ascii_case("ab", "abc"));
}
/// ends_with_ignore_ascii_case (byte-based) folds ASCII case for the suffix;
/// empty suffix always matches, an over-long suffix never does, and a suffix
/// that appears only at the FRONT does not match.
#[test]
fn ascii_ci_ends_with_exact() {
assert!(ends_with_ignore_ascii_case(b"archive.TAR.gz", b".tar.GZ"));
assert!(ends_with_ignore_ascii_case(b"EXAMPLE", b"example"));
assert!(ends_with_ignore_ascii_case(b"anything", b"")); // empty suffix
assert!(ends_with_ignore_ascii_case(b"", b"")); // empty/empty
assert!(!ends_with_ignore_ascii_case(b".gz", b"archive.gz")); // suffix longer
assert!(!ends_with_ignore_ascii_case(b"file.json", b".yaml")); // no match
assert!(!ends_with_ignore_ascii_case(b"yaml.file", b"yaml")); // front only
}
/// contains_ignore_ascii_case folds ASCII case for the needle; empty needle
/// always matches; boundary needle == whole value matches; over-long fails.
#[test]
fn ascii_ci_contains_exact() {
assert!(contains_ignore_ascii_case(
"my SECRET_key here",
"secret_KEY"
));
assert!(contains_ignore_ascii_case("PREFIXvalue", "prefix"));
assert!(contains_ignore_ascii_case("suffixVALUE", "value"));
assert!(contains_ignore_ascii_case("exact", "EXACT")); // whole-value needle
assert!(contains_ignore_ascii_case("anything", "")); // empty needle
assert!(!contains_ignore_ascii_case("short", "longerneedle")); // over-long
assert!(!contains_ignore_ascii_case("password", "secret")); // absent
}
/// contains_bytes_ignore_ascii_case matches contains_ignore_ascii_case for the
/// same inputs, taking a byte-slice needle.
#[test]
fn ascii_ci_contains_bytes_matches_str_variant() {
let value = "AWS_ACCESS_key_ID";
assert!(contains_bytes_ignore_ascii_case(value, b"access_KEY"));
assert!(contains_bytes_ignore_ascii_case(value, b"")); // empty needle
assert!(!contains_bytes_ignore_ascii_case(value, b"secret"));
// Parity with the &str variant on a shared needle.
assert_eq!(
contains_bytes_ignore_ascii_case(value, b"aws_access"),
contains_ignore_ascii_case(value, "aws_access"),
);
}
/// Adversarial: case folding is ASCII-ONLY. Non-ASCII letters are compared
/// byte-exact, so an upper/lower mismatch on a multibyte character does NOT
/// fold. The ASCII letters around it still fold.
#[test]
fn ascii_ci_non_ascii_is_not_folded() {
// "é" (U+00E9, bytes C3 A9) vs "É" (U+00C9, bytes C3 89) differ in their
// second UTF-8 byte, which is not an ASCII letter, so ASCII-only folding
// cannot equate them: the substring search FAILS.
assert!(!contains_ignore_ascii_case(
"caf\u{00e9}_key",
"CAF\u{00c9}_KEY"
));
// Byte-identical non-ASCII char + ASCII case fold around it: SUCCEEDS.
assert!(contains_ignore_ascii_case(
"caf\u{00e9}_KEY",
"CAF\u{00e9}_key"
));
// ends_with: differing-case non-ASCII char does not fold (é vs É).
assert!(!ends_with_ignore_ascii_case(
"caf\u{00e9}".as_bytes(),
"\u{00c9}".as_bytes()
));
// ends_with: identical non-ASCII char with ASCII case folding before it.
assert!(ends_with_ignore_ascii_case(
"CAF\u{00e9}".as_bytes(),
"caf\u{00e9}".as_bytes()
));
}
// ---------------------------------------------------------------------------
// git_lfs: pointer recognition, real pointer vs near-misses.
// ---------------------------------------------------------------------------
fn real_pointer() -> String {
// 64 lowercase hex chars for the sha256 oid.
let oid = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
assert_eq!(oid.len(), 64, "test fixture oid must be 64 hex chars");
format!("version https://git-lfs.github.com/spec/v1\noid sha256:{oid}\nsize 12345\n")
}
/// The canonical version-line constant is byte-exact, and recognition folds
/// ASCII case and trims surrounding whitespace.
#[test]
fn git_lfs_version_line_exact() {
assert_eq!(
GIT_LFS_VERSION_LINE,
"version https://git-lfs.github.com/spec/v1"
);
assert!(is_git_lfs_version_line(GIT_LFS_VERSION_LINE.as_bytes()));
// Surrounding ASCII whitespace is trimmed.
assert!(is_git_lfs_version_line(
b" version https://git-lfs.github.com/spec/v1 "
));
// Case-insensitive (content classification, not byte-exact parsing).
assert!(is_git_lfs_version_line(
b"VERSION HTTPS://GIT-LFS.GITHUB.COM/SPEC/V1"
));
// A different spec URL is NOT the version line.
assert!(!is_git_lfs_version_line(
b"version https://git-lfs.github.com/spec/v2"
));
assert!(!is_git_lfs_version_line(b"oid sha256:deadbeef"));
}
/// oid-line recognition requires exactly 64 hex chars after the `oid sha256:`
/// prefix; 63 or 65 chars, or a non-hex char, are rejected.
#[test]
fn git_lfs_oid_line_length_and_hex_boundary() {
let hex64 = "a".repeat(64);
assert!(is_git_lfs_oid_line(
format!("oid sha256:{hex64}").as_bytes()
));
// 63 hex -> too short.
let hex63 = "a".repeat(63);
assert!(!is_git_lfs_oid_line(
format!("oid sha256:{hex63}").as_bytes()
));
// 65 hex -> too long.
let hex65 = "a".repeat(65);
assert!(!is_git_lfs_oid_line(
format!("oid sha256:{hex65}").as_bytes()
));
// 64 chars but one is non-hex ('g').
let mut nonhex = "a".repeat(63);
nonhex.push('g');
assert!(!is_git_lfs_oid_line(
format!("oid sha256:{nonhex}").as_bytes()
));
// Wrong prefix (md5 instead of sha256).
assert!(!is_git_lfs_oid_line(format!("oid md5:{hex64}").as_bytes()));
}
/// size-line recognition requires the `size ` prefix and a non-empty run of
/// ASCII digits.
#[test]
fn git_lfs_size_line_boundary() {
assert!(is_git_lfs_size_line(b"size 12345"));
assert!(is_git_lfs_size_line(b" size 0 ")); // trimmed, single digit ok
assert!(!is_git_lfs_size_line(b"size ")); // empty numeric body
assert!(!is_git_lfs_size_line(b"size 12x")); // non-digit in body
assert!(!is_git_lfs_size_line(b"size")); // missing space+body
assert!(!is_git_lfs_size_line(b"length 12345")); // wrong key
}
/// A well-formed whole pointer file is recognized; optional `ext-*` lines
/// between `version` and `oid` are tolerated.
#[test]
fn git_lfs_pointer_positive() {
assert!(is_git_lfs_pointer(real_pointer().as_bytes()));
// ext-* line tolerated between version and oid.
let oid = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
let with_ext = format!(
"version https://git-lfs.github.com/spec/v1\next-0-foo sha256:bar\noid sha256:{oid}\nsize 42\n"
);
assert!(is_git_lfs_pointer(with_ext.as_bytes()));
// CRLF line endings are handled (split on \r and \n).
let crlf = real_pointer().replace('\n', "\r\n");
assert!(is_git_lfs_pointer(crlf.as_bytes()));
}
/// Near-misses: missing size, out-of-order (size before oid), truncated oid,
/// and plain non-pointer content are all rejected. A false positive here would
/// suppress a real credential, so strictness is load-bearing.
#[test]
fn git_lfs_pointer_near_misses_rejected() {
let oid = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
// Missing size line.
let no_size = format!("version https://git-lfs.github.com/spec/v1\noid sha256:{oid}\n");
assert!(!is_git_lfs_pointer(no_size.as_bytes()));
// Out of order: size before oid (spec mandates oid then size).
let out_of_order =
format!("version https://git-lfs.github.com/spec/v1\nsize 12345\noid sha256:{oid}\n");
assert!(!is_git_lfs_pointer(out_of_order.as_bytes()));
// Truncated oid (63 hex) -> oid line not recognized -> no pointer.
let short = "a".repeat(63);
let bad_oid =
format!("version https://git-lfs.github.com/spec/v1\noid sha256:{short}\nsize 1\n");
assert!(!is_git_lfs_pointer(bad_oid.as_bytes()));
// Missing version line.
let no_version = format!("oid sha256:{oid}\nsize 12345\n");
assert!(!is_git_lfs_pointer(no_version.as_bytes()));
// Plain text that merely mentions git-lfs is not a pointer.
assert!(!is_git_lfs_pointer(
b"This repo uses git-lfs for large blobs.\n"
));
// Empty content.
assert!(!is_git_lfs_pointer(b""));
}