1use super::{Anchor, ByteSet, Finder};
2use std::ops::Range;
3
4#[derive(Default)]
5pub struct Jwt {}
6
7impl Jwt {
8 fn is_base64url(b: u8) -> bool {
13 b.is_ascii_alphanumeric() || b == b'-' || b == b'_'
14 }
15
16 fn run_end(input: &[u8], start: usize) -> usize {
18 let mut pos = start;
19 while pos < input.len() && Self::is_base64url(input[pos]) {
20 pos += 1;
21 }
22 pos
23 }
24
25 fn match_end(input: &[u8], pos: usize) -> Option<usize> {
28 let header_end = Self::run_end(input, pos);
31 if header_end - pos < 4 || &input[pos..pos + 3] != b"eyJ" {
32 return None;
33 }
34 if header_end >= input.len() || input[header_end] != b'.' {
35 return None;
36 }
37 let payload_start = header_end + 1;
41 if payload_start >= input.len() || input[payload_start] != b'e' {
42 return None;
43 }
44 let payload_end = Self::run_end(input, payload_start);
45 if payload_end - payload_start < 3 {
46 return None;
47 }
48 if payload_end >= input.len() || input[payload_end] != b'.' {
49 return None;
50 }
51 let sig_start = payload_end + 1;
55 let sig_end = Self::run_end(input, sig_start);
56 if sig_end == sig_start {
57 return Some(sig_start);
58 }
59 if sig_end - sig_start < 4 {
60 return None;
61 }
62 if sig_end < input.len() && input[sig_end] == b'.' {
65 return None;
66 }
67 Some(sig_end)
68 }
69}
70
71impl Finder for Jwt {
72 fn line_agnostic(&self) -> bool {
73 true
76 }
77
78 fn id(&self) -> &'static str {
79 "jwt"
80 }
81
82 fn dispatchable(&self) -> bool {
83 true
84 }
85
86 fn could_start_at(&self, byte: u8) -> bool {
87 byte == b'e'
88 }
89
90 fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
91 !Self::is_base64url(prev)
92 }
93
94 fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
95 next == b'y'
96 }
97
98 fn anchor(&self) -> Option<Anchor> {
99 Some(Anchor::new(ByteSet::from_bytes(b"J"), ByteSet::from_bytes(b"ey")).back(2))
102 }
103
104 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
105 if input[pos] != b'e' {
106 return None;
107 }
108 if pos > 0 && Self::is_base64url(input[pos - 1]) {
111 return None;
112 }
113 Self::match_end(input, pos).map(|end| pos..end)
114 }
115
116 fn find(&self, s: &str) -> Option<Range<usize>> {
117 let input = s.as_bytes();
118 let mut idx = 0;
119
120 while idx < input.len() {
121 if input[idx] == b'e'
123 && (idx == 0 || !Self::is_base64url(input[idx - 1]))
124 && let Some(end) = Self::match_end(input, idx)
125 {
126 return Some(idx..end);
127 }
128 idx += 1;
129 }
130
131 None
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 const JWT_HS256: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
141
142 const JWT_RS256: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_5RU8VuLeas";
144
145 #[test]
146 fn id_should_return_jwt() {
147 let finder = Jwt::default();
148 assert_eq!("jwt", finder.id());
149 }
150
151 #[test]
152 fn find_should_extract_jwt() {
153 let finder = Jwt::default();
154 let input = format!("token: {}", JWT_HS256);
155 let range = finder.find(&input).unwrap();
156 assert_eq!(JWT_HS256, &input[range]);
157 }
158
159 #[test]
160 fn find_should_extract_jwt_at_start() {
161 let finder = Jwt::default();
162 let input = format!("{} is the token", JWT_HS256);
163 let range = finder.find(&input).unwrap();
164 assert_eq!(JWT_HS256, &input[range]);
165 }
166
167 #[test]
168 fn find_should_extract_rs256_jwt() {
169 let finder = Jwt::default();
170 let range = finder.find(JWT_RS256).unwrap();
171 assert_eq!(JWT_RS256, &JWT_RS256[range]);
172 }
173
174 #[test]
175 fn find_should_extract_jwt_in_text() {
176 let finder = Jwt::default();
177 let input = format!("Authorization: Bearer {} end", JWT_HS256);
178 let range = finder.find(&input).unwrap();
179 assert_eq!(JWT_HS256, &input[range]);
180 }
181
182 #[test]
183 fn find_should_reject_two_segments() {
184 let finder = Jwt::default();
185 assert!(
186 finder
187 .find("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0")
188 .is_none()
189 );
190 }
191
192 #[test]
193 fn find_should_reject_non_eyj_prefix() {
194 let finder = Jwt::default();
195 assert!(finder.find("abc.def.ghi").is_none());
196 }
197
198 #[test]
199 fn find_should_handle_empty_input() {
200 let finder = Jwt::default();
201 assert!(finder.find("").is_none());
202 }
203
204 #[test]
205 fn find_should_reject_preceded_by_alphanumeric() {
206 let finder = Jwt::default();
207 let input = format!("x{}", JWT_HS256);
208 assert!(finder.find(&input).is_none());
209 }
210
211 #[test]
212 fn find_should_extract_multiple_jwts_iteratively() {
213 let finder = Jwt::default();
214 let input = format!("{} and {}", JWT_HS256, JWT_RS256);
215
216 let mut results = Vec::new();
217 let mut idx = 0;
218 while idx < input.len() {
219 if let Some(range) = finder.find(&input[idx..]) {
220 results.push(input[idx + range.start..idx + range.end].to_string());
221 idx += range.end;
222 } else {
223 break;
224 }
225 }
226
227 assert_eq!(vec![JWT_HS256, JWT_RS256], results);
228 }
229
230 #[test]
231 fn find_should_reject_short_segments() {
232 let finder = Jwt::default();
233 assert!(finder.find("eyJ.ab.cd").is_none());
234 }
235
236 #[test]
237 fn try_at_valid_jwt() {
238 let finder = Jwt::default();
239 let input = JWT_HS256.as_bytes();
240 assert_eq!(finder.try_at(input, 0), Some(0..input.len()));
241 }
242
243 #[test]
244 fn try_at_preceded_by_base64() {
245 let finder = Jwt::default();
246 let input = format!("x{}", JWT_HS256);
247 assert!(finder.try_at(input.as_bytes(), 1).is_none());
248 }
249
250 #[test]
251 fn try_at_non_e() {
252 let finder = Jwt::default();
253 assert!(finder.try_at(b"abc", 0).is_none());
254 }
255
256 #[test]
257 fn try_at_single_e() {
258 let finder = Jwt::default();
259 assert!(finder.try_at(b"e", 0).is_none());
260 }
261
262 #[test]
263 fn try_at_eyj_only() {
264 let finder = Jwt::default();
265 assert!(finder.try_at(b"eyJ", 0).is_none());
266 }
267
268 #[test]
269 fn find_four_segments_with_non_json_tail_yields_none() {
270 let finder = Jwt::default();
274 let input = format!("{}.extra", JWT_HS256);
275 assert!(finder.find(&input).is_none());
276 }
277
278 #[test]
279 fn find_four_eyj_segments_finds_inner_jwt() {
280 let finder = Jwt::default();
281 let input = "eyJab.eyJcd.eyJef.extra";
282 let range = finder.find(input).unwrap();
283 assert_eq!("eyJcd.eyJef.extra", &input[range]);
284 }
285
286 #[test]
287 fn find_should_extract_jwt_after_space() {
288 let finder = Jwt::default();
289 let input = format!(" {}", JWT_HS256);
290 let range = finder.find(&input).unwrap();
291 assert_eq!(JWT_HS256, &input[range]);
292 }
293}