1use core::fmt;
12
13use mcp_conformance_core::trace::TraceEvent;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Limits {
19 pub max_events: usize,
21 pub max_line_bytes: usize,
23}
24
25impl Default for Limits {
26 fn default() -> Self {
27 Self {
28 max_events: 100_000,
31 max_line_bytes: 1024 * 1024,
32 }
33 }
34}
35
36#[derive(Debug)]
40#[non_exhaustive]
41pub enum TraceParseError {
42 LineTooLong {
44 line: usize,
46 length: usize,
48 limit: usize,
50 },
51 TooManyEvents {
53 limit: usize,
55 },
56 BlankLine {
59 line: usize,
61 },
62 Malformed {
64 line: usize,
66 source: serde_json::Error,
68 },
69 ByteOrderMark,
79 NotJsonLines {
86 array: bool,
88 },
89 NonMonotonicSeq {
91 line: usize,
93 seq: u64,
95 previous: u64,
97 },
98}
99
100impl fmt::Display for TraceParseError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::LineTooLong {
104 line,
105 length,
106 limit,
107 } => write!(
108 f,
109 "line {line}: record is {length} bytes, exceeding the {limit}-byte limit"
110 ),
111 Self::TooManyEvents { limit } => {
112 write!(f, "trace exceeds the {limit}-event limit")
113 }
114 Self::BlankLine { line } => {
115 write!(
116 f,
117 "line {line}: blank line (JSON Lines forbids blank records)"
118 )
119 }
120 Self::Malformed { line, source } => {
121 write!(f, "line {line}: not a valid trace event: {source}")
122 }
123 Self::ByteOrderMark => write!(
124 f,
125 "the document begins with a UTF-8 byte-order mark (EF BB BF), which JSON \
126 Lines does not permit; strip those three bytes and re-run"
127 ),
128 Self::NotJsonLines { array } => {
129 let fix = if *array {
130 "it is a JSON array — one event per element, so `jq -c '.[]' <file>` converts it"
131 } else {
132 "it is one pretty-printed JSON object — `jq -c . <file>` puts it on one line"
133 };
134 write!(
135 f,
136 "the document is a single JSON value, not JSON Lines (one event per line): {fix}"
137 )
138 }
139 Self::NonMonotonicSeq {
140 line,
141 seq,
142 previous,
143 } => write!(
144 f,
145 "line {line}: seq {seq} is not greater than the previous event's seq {previous}"
146 ),
147 }
148 }
149}
150
151impl core::error::Error for TraceParseError {
152 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
153 match self {
154 Self::Malformed { source, .. } => Some(source),
155 _ => None,
156 }
157 }
158}
159
160pub fn parse_trace(input: &str, limits: &Limits) -> Result<Vec<TraceEvent>, TraceParseError> {
177 if input.starts_with('\u{feff}') {
180 return Err(TraceParseError::ByteOrderMark);
181 }
182 let mut events = Vec::new();
183 let mut previous_seq: Option<u64> = None;
184 for (index, line) in input.lines().enumerate() {
185 let line_number = index + 1;
186 if line.len() > limits.max_line_bytes {
187 return Err(TraceParseError::LineTooLong {
188 line: line_number,
189 length: line.len(),
190 limit: limits.max_line_bytes,
191 });
192 }
193 if line.trim().is_empty() {
194 return Err(whole_document_is_json(input)
198 .unwrap_or(TraceParseError::BlankLine { line: line_number }));
199 }
200 if events.len() >= limits.max_events {
201 return Err(TraceParseError::TooManyEvents {
202 limit: limits.max_events,
203 });
204 }
205 let event: TraceEvent = serde_json::from_str(line).map_err(|source| {
206 whole_document_is_json(input).unwrap_or(TraceParseError::Malformed {
207 line: line_number,
208 source,
209 })
210 })?;
211 if let Some(previous) = previous_seq
212 && event.seq <= previous
213 {
214 return Err(TraceParseError::NonMonotonicSeq {
215 line: line_number,
216 seq: event.seq,
217 previous,
218 });
219 }
220 previous_seq = Some(event.seq);
221 events.push(event);
222 }
223 Ok(events)
224}
225
226fn whole_document_is_json(input: &str) -> Option<TraceParseError> {
239 let value: serde_json::Value = serde_json::from_str(input).ok()?;
240 let array = value.is_array();
241 let spans_lines = input.lines().filter(|line| !line.trim().is_empty()).count() > 1;
242 (array || spans_lines).then_some(TraceParseError::NotJsonLines { array })
243}
244
245#[cfg(test)]
246#[allow(clippy::unwrap_used)]
247mod tests {
248 use super::*;
249
250 const VALID_EVENT: &str = r#"{"seq":0,"direction":"client-to-server","transport":"stdio","kind":"lifecycle","event":"transport-open"}"#;
251
252 #[test]
255 fn a_leading_byte_order_mark_is_named_rather_than_pointed_at() {
256 let document = format!("\u{feff}{VALID_EVENT}");
257 let error = parse_trace(&document, &Limits::default()).unwrap_err();
258 assert!(matches!(error, TraceParseError::ByteOrderMark), "{error:?}");
259 let message = error.to_string();
260 assert!(message.contains("byte-order mark"), "{message}");
261 assert!(message.contains("EF BB BF"), "{message}");
262 let inside = format!("{VALID_EVENT}\n\u{feff}{VALID_EVENT}");
265 assert!(matches!(
266 parse_trace(&inside, &Limits::default()).unwrap_err(),
267 TraceParseError::Malformed { line: 2, .. }
268 ));
269 }
270
271 #[test]
272 fn a_json_document_is_told_apart_from_json_lines() {
273 let pretty = "{\n \"seq\": 0,\n \"direction\": \"client-to-server\",\n \"transport\": \"stdio\",\n \"kind\": \"lifecycle\",\n \"event\": \"transport-open\"\n}";
276 let error = parse_trace(pretty, &Limits::default()).unwrap_err();
277 assert!(
278 matches!(error, TraceParseError::NotJsonLines { array: false }),
279 "{error:?}"
280 );
281 assert!(error.to_string().contains("jq -c ."), "{error}");
282
283 for document in [format!("[{VALID_EVENT}]"), format!("[\n {VALID_EVENT}\n]")] {
285 let error = parse_trace(&document, &Limits::default()).unwrap_err();
286 assert!(
287 matches!(error, TraceParseError::NotJsonLines { array: true }),
288 "{error:?}"
289 );
290 assert!(error.to_string().contains("jq -c '.[]'"), "{error}");
291 }
292 }
293
294 #[test]
295 fn one_record_and_a_stray_newline_is_still_a_blank_line() {
296 let document = format!("{VALID_EVENT}\n\n");
300 assert!(matches!(
301 parse_trace(&document, &Limits::default()).unwrap_err(),
302 TraceParseError::BlankLine { line: 2 }
303 ));
304 }
305
306 #[test]
307 fn a_one_line_object_that_is_not_an_event_keeps_serdes_message() {
308 let error = parse_trace(r#"{"hello":"world"}"#, &Limits::default()).unwrap_err();
311 assert!(
312 matches!(error, TraceParseError::Malformed { line: 1, .. }),
313 "{error:?}"
314 );
315 assert!(error.to_string().contains("missing field `seq`"), "{error}");
316 }
317
318 #[test]
319 fn a_genuinely_broken_line_still_gets_its_line_number() {
320 let document = format!("{VALID_EVENT}\nnot json\n");
323 let error = parse_trace(&document, &Limits::default()).unwrap_err();
324 assert!(
325 matches!(error, TraceParseError::Malformed { line: 2, .. }),
326 "{error:?}"
327 );
328
329 let gapped = format!("{VALID_EVENT}\n\n{VALID_EVENT}\n");
331 assert!(matches!(
332 parse_trace(&gapped, &Limits::default()).unwrap_err(),
333 TraceParseError::BlankLine { line: 2 }
334 ));
335 }
336
337 #[test]
338 fn parses_valid_lines_and_empty_documents() {
339 assert!(parse_trace("", &Limits::default()).unwrap().is_empty());
340 let one = parse_trace(VALID_EVENT, &Limits::default()).unwrap();
341 assert_eq!(one.len(), 1);
342 let with_newline = format!("{VALID_EVENT}\n");
344 assert_eq!(
345 parse_trace(&with_newline, &Limits::default())
346 .unwrap()
347 .len(),
348 1
349 );
350 }
351
352 #[test]
353 fn rejects_blank_interior_lines() {
354 let doc = format!("{VALID_EVENT}\n\n");
355 assert!(matches!(
356 parse_trace(&doc, &Limits::default()),
357 Err(TraceParseError::BlankLine { line: 2 })
358 ));
359 }
360
361 #[test]
362 fn rejects_oversized_lines() {
363 let limits = Limits {
364 max_line_bytes: 16,
365 ..Limits::default()
366 };
367 assert!(matches!(
368 parse_trace(VALID_EVENT, &limits),
369 Err(TraceParseError::LineTooLong { line: 1, .. })
370 ));
371 }
372
373 #[test]
374 fn rejects_too_many_events() {
375 let limits = Limits {
376 max_events: 1,
377 ..Limits::default()
378 };
379 let second = VALID_EVENT.replace("\"seq\":0", "\"seq\":1");
380 let doc = format!("{VALID_EVENT}\n{second}");
381 assert!(matches!(
382 parse_trace(&doc, &limits),
383 Err(TraceParseError::TooManyEvents { limit: 1 })
384 ));
385 }
386
387 #[test]
388 fn rejects_malformed_records_with_line_numbers() {
389 let doc = format!("{VALID_EVENT}\n{{\"seq\":1}}");
390 match parse_trace(&doc, &Limits::default()) {
391 Err(TraceParseError::Malformed { line, .. }) => assert_eq!(line, 2),
392 other => panic!("expected malformed at line 2, got {other:?}"),
393 }
394 }
395
396 #[test]
397 fn rejects_non_monotonic_seq() {
398 let duplicate = format!("{VALID_EVENT}\n{VALID_EVENT}");
399 assert!(matches!(
400 parse_trace(&duplicate, &Limits::default()),
401 Err(TraceParseError::NonMonotonicSeq {
402 line: 2,
403 seq: 0,
404 previous: 0
405 })
406 ));
407 }
408
409 #[test]
410 fn error_messages_are_line_addressed() {
411 let doc = format!("{VALID_EVENT}\nnot json");
412 let error = parse_trace(&doc, &Limits::default()).unwrap_err();
413 assert!(error.to_string().starts_with("line 2:"), "{error}");
414 }
415
416 #[test]
417 fn line_exactly_at_the_byte_limit_is_accepted() {
418 let limits = Limits {
420 max_line_bytes: VALID_EVENT.len(),
421 ..Limits::default()
422 };
423 assert_eq!(parse_trace(VALID_EVENT, &limits).unwrap().len(), 1);
424 }
425
426 #[test]
427 fn error_source_is_exposed_for_malformed_records_only() {
428 use core::error::Error as _;
429 let malformed = parse_trace("nope", &Limits::default()).unwrap_err();
430 assert!(malformed.source().is_some());
431 let blank = parse_trace(" \n", &Limits::default()).unwrap_err();
432 assert!(blank.source().is_none());
433 }
434}