1use super::{
13 format::{
14 BUILD_SCOPE_JSON_PATH, CARGO_METADATA_JSON_PATH, OutputDict, RECORD_OPTS_JSON_PATH,
15 RUN_LOG_FILE_NAME, STDERR_DICT_PATH, STDOUT_DICT_PATH, STORE_ZIP_FILE_NAME,
16 TEST_LIST_JSON_PATH,
17 },
18 summary::{RecordOpts, TestEventSummary},
19};
20use crate::{
21 errors::RecordReadError,
22 output_spec::RecordingSpec,
23 record::format::{RERUN_INFO_JSON_PATH, RecordedBuildScope, RerunInfo},
24 redact::SizeDisplay,
25 user_config::elements::MAX_MAX_OUTPUT_SIZE,
26};
27use bytesize::ByteSize;
28use camino::{Utf8Path, Utf8PathBuf};
29use debug_ignore::DebugIgnore;
30use eazip::Archive;
31use nextest_metadata::TestListSummary;
32use std::{
33 fs::File,
34 io::{self, BufRead, BufReader, Read},
35};
36
37pub trait StoreReader {
43 fn read_cargo_metadata(&mut self) -> Result<String, RecordReadError>;
45
46 fn read_test_list(&mut self) -> Result<TestListSummary, RecordReadError>;
48
49 fn read_record_opts(&mut self) -> Result<RecordOpts, RecordReadError>;
51
52 fn read_build_scope(&mut self) -> Result<RecordedBuildScope, RecordReadError>;
54
55 fn read_rerun_info(&mut self) -> Result<Option<RerunInfo>, RecordReadError>;
59
60 fn load_dictionaries(&mut self) -> Result<(), RecordReadError>;
64
65 fn read_output(&mut self, file_name: &str) -> Result<Vec<u8>, RecordReadError>;
74
75 fn extract_file_to_path(
80 &mut self,
81 store_path: &str,
82 output_path: &Utf8Path,
83 ) -> Result<u64, RecordReadError>;
84}
85
86#[derive(Debug)]
91pub struct RecordReader {
92 run_dir: Utf8PathBuf,
93 archive: Option<DebugIgnore<Archive<BufReader<File>>>>,
94 stdout_dict: Option<Vec<u8>>,
96 stderr_dict: Option<Vec<u8>>,
98}
99
100impl RecordReader {
101 pub fn open(run_dir: &Utf8Path) -> Result<Self, RecordReadError> {
105 if !run_dir.exists() {
106 return Err(RecordReadError::RunNotFound {
107 path: run_dir.to_owned(),
108 });
109 }
110
111 Ok(Self {
112 run_dir: run_dir.to_owned(),
113 archive: None,
114 stdout_dict: None,
115 stderr_dict: None,
116 })
117 }
118
119 pub fn run_dir(&self) -> &Utf8Path {
121 &self.run_dir
122 }
123
124 fn ensure_archive(&mut self) -> Result<&mut Archive<BufReader<File>>, RecordReadError> {
126 if self.archive.is_none() {
127 let store_path = self.run_dir.join(STORE_ZIP_FILE_NAME);
128 let file = File::open(&store_path).map_err(|error| RecordReadError::OpenArchive {
129 path: store_path.clone(),
130 error,
131 })?;
132 let archive = Archive::new(BufReader::new(file)).map_err(|error| {
133 RecordReadError::ParseArchive {
134 path: store_path,
135 error,
136 }
137 })?;
138 self.archive = Some(DebugIgnore(archive));
139 }
140 Ok(self.archive.as_mut().expect("archive was just set"))
141 }
142
143 fn read_archive_file(&mut self, file_name: &str) -> Result<Vec<u8>, RecordReadError> {
153 let limit = MAX_MAX_OUTPUT_SIZE.as_u64();
154 let archive = self.ensure_archive()?;
155 let mut file =
156 archive
157 .get_by_name(file_name)
158 .ok_or_else(|| RecordReadError::FileNotFound {
159 file_name: file_name.to_string(),
160 })?;
161
162 let claimed_size = file.metadata().uncompressed_size;
163 if claimed_size > limit {
164 return Err(RecordReadError::FileTooLarge {
165 file_name: file_name.to_string(),
166 size: claimed_size,
167 limit,
168 });
169 }
170
171 let capacity = usize::try_from(claimed_size).unwrap_or(usize::MAX);
172 let mut contents = Vec::with_capacity(capacity);
173
174 file.read()
177 .and_then(|reader| reader.take(limit).read_to_end(&mut contents))
178 .map_err(|error| RecordReadError::Decompress {
179 file_name: file_name.to_string(),
180 error,
181 })?;
182
183 let actual_size = contents.len() as u64;
184 if actual_size != claimed_size {
185 return Err(RecordReadError::SizeMismatch {
186 file_name: file_name.to_string(),
187 claimed_size,
188 actual_size,
189 });
190 }
191
192 Ok(contents)
193 }
194
195 pub fn read_cargo_metadata(&mut self) -> Result<String, RecordReadError> {
197 let bytes = self.read_archive_file(CARGO_METADATA_JSON_PATH)?;
198 String::from_utf8(bytes).map_err(|e| RecordReadError::Decompress {
199 file_name: CARGO_METADATA_JSON_PATH.to_string(),
200 error: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
201 })
202 }
203
204 pub fn read_test_list(&mut self) -> Result<TestListSummary, RecordReadError> {
206 let bytes = self.read_archive_file(TEST_LIST_JSON_PATH)?;
207 serde_json::from_slice(&bytes).map_err(|error| RecordReadError::DeserializeMetadata {
208 file_name: TEST_LIST_JSON_PATH.to_string(),
209 error,
210 })
211 }
212
213 pub fn read_record_opts(&mut self) -> Result<RecordOpts, RecordReadError> {
215 let bytes = self.read_archive_file(RECORD_OPTS_JSON_PATH)?;
216 serde_json::from_slice(&bytes).map_err(|error| RecordReadError::DeserializeMetadata {
217 file_name: RECORD_OPTS_JSON_PATH.to_string(),
218 error,
219 })
220 }
221
222 pub fn read_build_scope(&mut self) -> Result<RecordedBuildScope, RecordReadError> {
224 let bytes = self.read_archive_file(BUILD_SCOPE_JSON_PATH)?;
225 serde_json::from_slice(&bytes).map_err(|error| RecordReadError::DeserializeMetadata {
226 file_name: BUILD_SCOPE_JSON_PATH.to_string(),
227 error,
228 })
229 }
230
231 pub fn read_rerun_info(&mut self) -> Result<Option<RerunInfo>, RecordReadError> {
236 match self.read_archive_file(RERUN_INFO_JSON_PATH) {
237 Ok(bytes) => {
238 let info = serde_json::from_slice(&bytes).map_err(|error| {
239 RecordReadError::DeserializeMetadata {
240 file_name: RERUN_INFO_JSON_PATH.to_string(),
241 error,
242 }
243 })?;
244 Ok(Some(info))
245 }
246 Err(RecordReadError::FileNotFound { .. }) => {
247 Ok(None)
249 }
250 Err(e) => Err(e),
251 }
252 }
253
254 pub fn load_dictionaries(&mut self) -> Result<(), RecordReadError> {
263 self.stdout_dict = Some(self.read_archive_file(STDOUT_DICT_PATH)?);
264 self.stderr_dict = Some(self.read_archive_file(STDERR_DICT_PATH)?);
265 Ok(())
266 }
267
268 pub fn events(&self) -> Result<RunLogEventIter<BufReader<LogDecoder>>, RecordReadError> {
272 let log_path = self.run_dir.join(RUN_LOG_FILE_NAME);
273 let file = File::open(&log_path).map_err(|error| RecordReadError::OpenRunLog {
274 path: log_path.clone(),
275 error,
276 })?;
277 let decoder =
278 zstd::stream::Decoder::new(file).map_err(|error| RecordReadError::OpenRunLog {
279 path: log_path,
280 error,
281 })?;
282 Ok(RunLogEventIter::new(BufReader::new(decoder)))
283 }
284
285 pub fn read_output(&mut self, file_name: &str) -> Result<Vec<u8>, RecordReadError> {
297 let path = format!("out/{file_name}");
298 let compressed = self.read_archive_file(&path)?;
299 let limit = MAX_MAX_OUTPUT_SIZE.as_u64();
300
301 let dict_bytes = self.get_dict_for_output(file_name).ok_or_else(|| {
305 RecordReadError::UnknownOutputType {
306 file_name: file_name.to_owned(),
307 }
308 })?;
309
310 decompress_with_dict(&compressed, dict_bytes, limit).map_err(|error| {
311 RecordReadError::Decompress {
312 file_name: path,
313 error,
314 }
315 })
316 }
317
318 fn get_dict_for_output(&self, file_name: &str) -> Option<&[u8]> {
327 match OutputDict::for_output_file_name(file_name) {
328 OutputDict::Stdout => Some(
329 self.stdout_dict
330 .as_ref()
331 .expect("load_dictionaries must be called first"),
332 ),
333 OutputDict::Stderr => Some(
334 self.stderr_dict
335 .as_ref()
336 .expect("load_dictionaries must be called first"),
337 ),
338 OutputDict::None => None,
339 }
340 }
341}
342
343impl StoreReader for RecordReader {
344 fn read_cargo_metadata(&mut self) -> Result<String, RecordReadError> {
345 RecordReader::read_cargo_metadata(self)
346 }
347
348 fn read_test_list(&mut self) -> Result<TestListSummary, RecordReadError> {
349 RecordReader::read_test_list(self)
350 }
351
352 fn read_record_opts(&mut self) -> Result<RecordOpts, RecordReadError> {
353 RecordReader::read_record_opts(self)
354 }
355
356 fn read_build_scope(&mut self) -> Result<RecordedBuildScope, RecordReadError> {
357 RecordReader::read_build_scope(self)
358 }
359
360 fn read_rerun_info(&mut self) -> Result<Option<RerunInfo>, RecordReadError> {
361 RecordReader::read_rerun_info(self)
362 }
363
364 fn load_dictionaries(&mut self) -> Result<(), RecordReadError> {
365 RecordReader::load_dictionaries(self)
366 }
367
368 fn read_output(&mut self, file_name: &str) -> Result<Vec<u8>, RecordReadError> {
369 RecordReader::read_output(self, file_name)
370 }
371
372 fn extract_file_to_path(
373 &mut self,
374 store_path: &str,
375 output_path: &Utf8Path,
376 ) -> Result<u64, RecordReadError> {
377 let archive = self.ensure_archive()?;
378 let mut file =
379 archive
380 .get_by_name(store_path)
381 .ok_or_else(|| RecordReadError::FileNotFound {
382 file_name: store_path.to_owned(),
383 })?;
384
385 let mut output_file =
386 File::create(output_path).map_err(|error| RecordReadError::ExtractFile {
387 store_path: store_path.to_owned(),
388 output_path: output_path.to_owned(),
389 error,
390 })?;
391
392 let mut reader = file
394 .read()
395 .map_err(|error| RecordReadError::ReadArchiveFile {
396 file_name: store_path.to_owned(),
397 error,
398 })?;
399
400 io::copy(&mut reader, &mut output_file).map_err(|error| RecordReadError::ExtractFile {
401 store_path: store_path.to_owned(),
402 output_path: output_path.to_owned(),
403 error,
404 })
405 }
406}
407
408pub(super) fn decompress_with_dict(
413 compressed: &[u8],
414 dict_bytes: &[u8],
415 limit: u64,
416) -> std::io::Result<Vec<u8>> {
417 let dict = zstd::dict::DecoderDictionary::copy(dict_bytes);
418 let decoder = zstd::stream::Decoder::with_prepared_dictionary(compressed, &dict)?;
419 let mut decompressed = Vec::new();
420 decoder
421 .take(limit.saturating_add(1))
422 .read_to_end(&mut decompressed)?;
423 if decompressed.len() as u64 > limit {
424 return Err(io::Error::new(
425 io::ErrorKind::InvalidData,
426 format!(
427 "decompressed size exceeds the limit of {}",
428 SizeDisplay(limit)
429 ),
430 ));
431 }
432 Ok(decompressed)
433}
434
435type LogDecoder = zstd::stream::Decoder<'static, BufReader<File>>;
437
438const MAX_RUN_LOG_LINE_SIZE: ByteSize = MAX_MAX_OUTPUT_SIZE;
443
444#[derive(Debug)]
448pub struct RunLogEventIter<R> {
449 reader: DebugIgnore<R>,
450 line_buf: Vec<u8>,
451 line_number: usize,
452 limit: ByteSize,
453}
454
455impl<R: BufRead> RunLogEventIter<R> {
456 pub(super) fn new(reader: R) -> Self {
457 Self {
458 reader: DebugIgnore(reader),
459 line_buf: Vec::new(),
460 line_number: 0,
461 limit: MAX_RUN_LOG_LINE_SIZE,
462 }
463 }
464}
465
466impl<R: BufRead> Iterator for RunLogEventIter<R> {
467 type Item = Result<TestEventSummary<RecordingSpec>, RecordReadError>;
468
469 fn next(&mut self) -> Option<Self::Item> {
470 let limit = self.limit.as_u64();
471 loop {
472 self.line_buf.clear();
473 self.line_number += 1;
474
475 match self
477 .reader
478 .by_ref()
479 .take(limit.saturating_add(1))
480 .read_until(b'\n', &mut self.line_buf)
481 {
482 Ok(0) => return None,
483 Ok(_) => {
484 let line = self.line_buf.strip_suffix(b"\n").unwrap_or(&self.line_buf);
485 if line.len() as u64 > limit {
486 return Some(Err(RecordReadError::RunLogLineTooLong {
487 line_number: self.line_number,
488 limit: self.limit,
489 }));
490 }
491 let line = line.trim_ascii();
492 if line.is_empty() {
493 continue;
494 }
495 return Some(serde_json::from_slice(line).map_err(|error| {
496 RecordReadError::ParseEvent {
497 line_number: self.line_number,
498 error,
499 }
500 }));
501 }
502 Err(error) => {
503 return Some(Err(RecordReadError::ReadRunLog {
504 line_number: self.line_number,
505 error,
506 }));
507 }
508 }
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use crate::record::recorder::compress_with_dict;
517
518 #[test]
519 fn test_run_log_line_limit() {
520 #[derive(Debug, PartialEq)]
521 enum LineResult {
522 End,
523 TooLong { line_number: usize },
524 ParseError { line_number: usize },
525 }
526
527 let limit = ByteSize::b(8);
528 let cases: [(&str, &[u8], LineResult); 8] = [
529 ("empty input", b"", LineResult::End),
530 ("only blank lines", b"\n\n \n", LineResult::End),
531 (
532 "line at limit",
533 b"12345678\n",
534 LineResult::ParseError { line_number: 1 },
535 ),
536 (
537 "line at limit without newline",
538 b"12345678",
539 LineResult::ParseError { line_number: 1 },
540 ),
541 (
542 "line over limit",
543 b"123456789\n",
544 LineResult::TooLong { line_number: 1 },
545 ),
546 (
547 "line over limit without newline",
548 b"123456789",
549 LineResult::TooLong { line_number: 1 },
550 ),
551 (
552 "line over limit after a blank line",
553 b"\n123456789\n",
554 LineResult::TooLong { line_number: 2 },
555 ),
556 (
557 "multibyte character crossing the limit",
558 "1234567\u{e9}\n".as_bytes(),
559 LineResult::TooLong { line_number: 1 },
560 ),
561 ];
562
563 for (description, input, expected) in cases {
564 let mut iter = RunLogEventIter {
565 reader: DebugIgnore(io::Cursor::new(input)),
566 line_buf: Vec::new(),
567 line_number: 0,
568 limit,
569 };
570 let actual = match iter.next() {
571 None => LineResult::End,
572 Some(Err(RecordReadError::RunLogLineTooLong {
573 line_number,
574 limit: error_limit,
575 })) => {
576 assert_eq!(error_limit, limit, "{description}: error limit");
577 LineResult::TooLong { line_number }
578 }
579 Some(Err(RecordReadError::ParseEvent { line_number, .. })) => {
580 LineResult::ParseError { line_number }
581 }
582 Some(other) => panic!("{description}: unexpected result {other:?}"),
583 };
584 assert_eq!(actual, expected, "{description}");
585 assert!(
586 iter.line_buf.len() as u64 <= limit.as_u64() + 1,
587 "{description}: buffered {} bytes, more than limit + 1",
588 iter.line_buf.len()
589 );
590 }
591 }
592
593 #[test]
594 fn test_decompress_with_dict_limit() {
595 let limit = 64;
596 let dict = OutputDict::Stdout
597 .dict_bytes()
598 .expect("stdout dictionary exists");
599 for (len, within_limit) in [
600 (0, true),
601 (63, true),
602 (64, true),
603 (65, false),
604 (4096, false),
605 ] {
606 let data = vec![b'x'; len];
607 let compressed = compress_with_dict(&data, dict).expect("compressed data");
608 let result = decompress_with_dict(&compressed, dict, limit);
609 if within_limit {
610 assert_eq!(
611 result.expect("decompression within the limit succeeds"),
612 data,
613 "len {len}: round-trips"
614 );
615 } else {
616 let error = result.expect_err("decompression over the limit fails");
617 assert_eq!(
618 error.kind(),
619 io::ErrorKind::InvalidData,
620 "len {len}: error kind"
621 );
622 }
623 }
624 }
625
626 #[test]
627 fn test_record_reader_nonexistent_dir() {
628 let result = RecordReader::open(Utf8Path::new("/nonexistent/path"));
629 assert!(matches!(result, Err(RecordReadError::RunNotFound { .. })));
630 }
631}