Skip to main content

rust_stdf/
stdf_file.rs

1//
2// stdf_file.rs
3// Author: noonchen - chennoon233@foxmail.com
4// Created Date: October 3rd 2022
5// -----
6// Last Modified: Wed Aug 19 2026
7// Modified By: noonchen
8// -----
9// Copyright (c) 2022 noonchen
10//
11
12use crate::records::*;
13use crate::stdf_codec::*;
14use crate::stdf_error::{StdfError, StdfErrorKind};
15#[cfg(feature = "bzip")]
16use bzip2::bufread::BzDecoder;
17#[cfg(feature = "gzip")]
18use flate2::bufread::GzDecoder;
19use std::io::{self, BufReader, SeekFrom}; // struct or enum
20use std::io::{BufRead, Read, Seek};
21use std::{fs, path::Path}; // trait
22#[cfg(feature = "zipfile")]
23use zip::{read::ZipFile, ZipArchive};
24
25/// `Unsafe` struct for coupling
26/// file and `ZipArchive`
27/// in order to get access to
28/// the same `ZipFile`
29#[cfg(feature = "zipfile")]
30pub(crate) struct ZipBundle<R> {
31    // put `ZipFile` on top
32    // to ensure it is dropped
33    // before `ZipArchive`
34    file: Option<ZipFile<'static>>,
35    archive: Box<ZipArchive<R>>,
36}
37
38#[allow(clippy::large_enum_variant)]
39pub(crate) enum StdfStream<R> {
40    Binary(R),
41    #[cfg(feature = "gzip")]
42    Gz(GzDecoder<R>),
43    #[cfg(feature = "bzip")]
44    Bz(BzDecoder<R>),
45    #[cfg(feature = "zipfile")]
46    Zip(ZipBundle<R>),
47}
48
49/// STDF Reader
50///
51/// This reader can process STDF datalogs of Version V4 and V4-2007
52///
53/// Supported compression:
54///  - Uncompressed
55///  - Gzip (.gz)
56///  - Bzip (.bz2)
57///
58/// # Example
59///
60/// ```
61/// use rust_stdf::{stdf_file::*, stdf_record_type::*, StdfRecord};
62///
63/// let stdf_path = "demo_file.stdf";
64/// let mut reader = match StdfReader::new(&stdf_path) {
65///     Ok(r) => r,
66///     Err(e) => {
67///         println!("{}", e);
68///         return;
69///     }
70/// };
71///
72/// // we will count total DUT# in the file
73/// // and put test result of PTR named
74/// // "continuity test" in a vector.
75/// let mut dut_count: u64 = 0;
76/// let mut continuity_rlt = vec![];
77///
78/// // use type filter to work on certain types,
79/// // use `|` to combine multiple typs
80/// let rec_types = REC_PIR | REC_PTR;
81/// // iterator starts from current file position,
82/// // if file hits EOF, it will NOT redirect to 0.
83/// for rec in reader
84///     .get_record_iter()
85///     .map(|x| x.unwrap())
86///     .filter(|x| x.is_type(rec_types))
87/// {
88///     match rec {
89///         StdfRecord::PIR(_) => {dut_count += 1;}
90///         StdfRecord::PTR(ref ptr_rec) => {
91///             if ptr_rec.test_txt == "continuity test" {
92///                 continuity_rlt.push(ptr_rec.result);
93///             }
94///         }
95///         _ => {}
96///     }
97/// }
98/// println!("Total duts {} \n continuity result {:?}",
99///         dut_count,
100///         continuity_rlt);
101/// ```
102pub struct StdfReader<R> {
103    endianness: ByteOrder,
104    stream: StdfStream<R>,
105}
106
107/// Iterator over [`StdfRecord`]s, created by [`StdfReader::get_record_iter`].
108///
109/// Each item is a fully parsed [`StdfRecord`].
110pub struct RecordIter<'a, R> {
111    inner: &'a mut StdfReader<R>,
112    // reused across records, bytes are only borrowed during parsing
113    buffer: Vec<u8>,
114}
115
116/// Iterator over unprocessed STDF records, created by [`StdfReader::get_rawdata_iter`].
117///
118/// Each item is an unprocessed [`RawDataElement`] that holds an owned record data buffer.
119pub struct RawDataIter<'a, R> {
120    offset: u64,
121    inner: &'a mut StdfReader<R>,
122}
123
124/// Iterator over unprocessed STDF records that borrows the record bytes,
125/// created by [`StdfReader::get_rawdata_view_iter`].
126///
127/// Each item is an unprocessed [`RawDataElementView`] that borrows record data buffer.
128///
129/// This iterator outperforms [`RawDataIter`] because there is no allocation, but
130/// it cannot implement [`Iterator`], so it can only be driven by `next`.
131///
132/// The yielded item is only valid until the following `next` call.
133///
134/// # Example
135///
136/// ```no_run
137/// use rust_stdf::{stdf_file::*, stdf_record_type::*, StdfRecordView};
138///
139/// let mut reader = StdfReader::new("demo_file.stdf").unwrap();
140/// let mut iter = reader.get_rawdata_view_iter();
141/// while let Some(item) = iter.next() {
142///     let raw = item.unwrap();
143///     if !raw.is_type(REC_PTR) {
144///         continue;
145///     }
146///     let rec_view: StdfRecordView = (&raw).into();
147///     if let StdfRecordView::PTR(ptr) = rec_view {
148///         println!("{}", ptr.result());
149///     }
150/// }
151/// ```
152pub struct RawDataViewIter<'a, R> {
153    offset: u64,
154    inner: &'a mut StdfReader<R>,
155    // reused across records, bytes are borrowed by the yielded view
156    buffer: Vec<u8>,
157}
158
159// implementations
160
161impl StdfReader<BufReader<fs::File>> {
162    /// Open the given file and return a StdfReader, if successful
163    #[inline(always)]
164    pub fn new<P>(path: P) -> Result<Self, StdfError>
165    where
166        P: AsRef<Path>,
167    {
168        // determine the compress type by file extension
169        let path_string = path.as_ref().display().to_string();
170        let file_ext = path_string.rsplit('.').next();
171        let compress_type = match file_ext {
172            Some(ext) => match ext {
173                #[cfg(feature = "gzip")]
174                "gz" => CompressType::GzipCompressed,
175                #[cfg(feature = "bzip")]
176                "bz2" => CompressType::BzipCompressed,
177                #[cfg(feature = "zipfile")]
178                "zip" => CompressType::ZipCompressed,
179                _ => CompressType::Uncompressed,
180            },
181            None => CompressType::Uncompressed,
182        };
183        let fp = fs::OpenOptions::new().read(true).open(path)?;
184        let br = BufReader::with_capacity(2 << 20, fp);
185        StdfReader::from(br, &compress_type)
186    }
187}
188
189impl<R: BufRead + Seek> StdfReader<R> {
190    /// Consume a input stream and generate a StdfReader, if successful
191    #[inline(always)]
192    pub fn from(in_stream: R, compress_type: &CompressType) -> Result<Self, StdfError> {
193        let mut stream = match compress_type {
194            #[cfg(feature = "gzip")]
195            CompressType::GzipCompressed => StdfStream::Gz(GzDecoder::new(in_stream)),
196            #[cfg(feature = "bzip")]
197            CompressType::BzipCompressed => StdfStream::Bz(BzDecoder::new(in_stream)),
198            #[cfg(feature = "zipfile")]
199            CompressType::ZipCompressed => StdfStream::Zip(ZipBundle::new(in_stream, 0)?),
200            _ => StdfStream::Binary(in_stream),
201        };
202
203        // read FAR header from file
204        let mut buf = [0u8; 4];
205        stream.read_exact(&mut buf)?;
206        // parse header assuming little endian
207        let far_header = RecordHeader::new().read_from_bytes(&buf, &ByteOrder::LittleEndian)?;
208        let endianness = match far_header.len {
209            2 => Ok(ByteOrder::LittleEndian),
210            512 => Ok(ByteOrder::BigEndian),
211            _ => Err(StdfError::new(
212                StdfErrorKind::InvalidStdfFile,
213                String::from("Cannot determine endianness"),
214            )),
215        }?;
216        // check if it's FAR
217        if (far_header.typ, far_header.sub) != (0, 10) {
218            return Err(StdfError::new(
219                StdfErrorKind::InvalidStdfFile,
220                format!(
221                    "FAR header (0, 10) expected, but {:?} is found",
222                    (far_header.typ, far_header.sub)
223                ),
224            ));
225        }
226        // restore file position
227        // current flate2 does not support fseek, we need to consume
228        // old stream and create a new one.
229        // If seek is supported, this function can be replaced by:
230        //
231        // stream.seek(SeekFrom::Start(0))?;
232        //
233        stream = rewind_stream_position(stream)?;
234
235        Ok(StdfReader { endianness, stream })
236    }
237
238    #[inline(always)]
239    fn read_header(&mut self) -> Result<RecordHeader, StdfError> {
240        let mut buf = [0u8; 4];
241        self.stream.read_exact(&mut buf)?;
242        RecordHeader::new().read_from_bytes(&buf, &self.endianness)
243    }
244
245    /// Return an iterator for [`StdfRecord`].
246    ///
247    /// The iterator yields records starting from the current
248    /// file position onward.
249    #[inline(always)]
250    pub fn get_record_iter(&mut self) -> RecordIter<'_, R> {
251        RecordIter {
252            inner: self,
253            buffer: Vec::with_capacity(1024),
254        }
255    }
256
257    /// Return an iterator for unprocessed STDF bytes [`RawDataElement`].
258    ///
259    /// The iterator yields records starting from the current file position
260    /// onward, and the `offset` field is counted relative to that position.
261    #[inline(always)]
262    pub fn get_rawdata_iter(&mut self) -> RawDataIter<'_, R> {
263        RawDataIter {
264            offset: 0,
265            inner: self,
266        }
267    }
268
269    /// Return a buffer-reusing iterator for unprocessed STDF bytes [`RawDataElementView`].
270    ///
271    /// Unlike [`get_rawdata_iter`](Self::get_rawdata_iter), the yielded
272    /// [`RawDataElementView`] borrows the iterator's reused buffer,
273    /// so it performs no per-record allocation. See [`RawDataViewIter`] for usage.
274    ///
275    /// The iterator yields records starting from the current file position
276    /// onward, and the `offset` field is counted relative to that position.
277    #[inline(always)]
278    pub fn get_rawdata_view_iter(&mut self) -> RawDataViewIter<'_, R> {
279        RawDataViewIter {
280            offset: 0,
281            inner: self,
282            buffer: Vec::with_capacity(1024),
283        }
284    }
285}
286
287#[cfg(feature = "zipfile")]
288impl<R: BufRead + Seek> ZipBundle<R> {
289    /// the following code is modified from this SO post:
290    /// https://stackoverflow.com/questions/67823680/open-a-single-file-from-a-zip-archive-and-pass-on-as-read-instance/
291    pub(crate) fn new(stream: R, file_index: usize) -> Result<ZipBundle<R>, StdfError> {
292        let archive = ZipArchive::new(stream)?;
293        let mut archive = Box::new(archive);
294
295        // SAFETY: fakes a 'static borrow of `archive`. Sound because `archive` is
296        // boxed (address stable across the move below), `file` drops before
297        // `archive` (whose Drop reads it), and `archive` is never aliased while
298        // `file` is alive (reopen_file clears `file` before re-borrowing).
299        let file = unsafe {
300            std::mem::transmute::<ZipFile<'_>, ZipFile<'static>>(archive.by_index(file_index)?)
301        };
302        Ok(ZipBundle {
303            archive,
304            file: Some(file),
305        })
306    }
307
308    pub(crate) fn reopen_file(&mut self, file_index: usize) -> Result<(), StdfError> {
309        // Drop the outstanding borrow before re-borrowing `archive`.
310        self.file = None;
311        // SAFETY: same invariants as `new`. `file` was just cleared above.
312        let file = unsafe {
313            std::mem::transmute::<ZipFile<'_>, ZipFile<'static>>(self.archive.by_index(file_index)?)
314        };
315        self.file = Some(file);
316        Ok(())
317    }
318}
319
320#[cfg(feature = "zipfile")]
321impl<R: BufRead + Seek> Read for ZipBundle<R> {
322    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
323        self.file.as_mut().unwrap().read(buf)
324    }
325}
326
327impl<R: BufRead + Seek> StdfStream<R> {
328    #[cfg(feature = "atdf")]
329    #[inline(always)]
330    pub(crate) fn read_until(&mut self, delim: u8, buf: &mut Vec<u8>) -> io::Result<usize> {
331        match self {
332            StdfStream::Binary(bstream) => bstream.read_until(delim, buf),
333            #[cfg(feature = "gzip")]
334            StdfStream::Gz(gzstream) => general_read_until(gzstream, delim, buf),
335            #[cfg(feature = "bzip")]
336            StdfStream::Bz(bzstream) => general_read_until(bzstream, delim, buf),
337            #[cfg(feature = "zipfile")]
338            StdfStream::Zip(zipstream) => general_read_until(zipstream, delim, buf),
339        }
340    }
341}
342
343impl<R: BufRead + Seek> Read for StdfStream<R> {
344    #[inline(always)]
345    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
346        match self {
347            StdfStream::Binary(bstream) => bstream.read(buf),
348            #[cfg(feature = "gzip")]
349            StdfStream::Gz(gzstream) => gzstream.read(buf),
350            #[cfg(feature = "bzip")]
351            StdfStream::Bz(bzstream) => bzstream.read(buf),
352            #[cfg(feature = "zipfile")]
353            StdfStream::Zip(zipstream) => zipstream.read(buf),
354        }
355    }
356}
357
358// impl<R: Seek> Seek for StdfStream<R> {
359//     fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
360//         match self {
361//             StdfStream::Binary(bstream) => bstream.seek(pos),
362//             // arm that does not support seek
363//             _ => Ok(0),
364//         }
365//     }
366// }
367
368impl<R: BufRead + Seek> Iterator for RecordIter<'_, R> {
369    type Item = Result<StdfRecord, StdfError>;
370
371    #[inline(always)]
372    fn next(&mut self) -> Option<Self::Item> {
373        let header = match self.inner.read_header() {
374            Ok(h) => h,
375            Err(e) => {
376                return match e.kind() {
377                    // only 2 error will be returned by `read_header`
378                    // Eof indicates normal EOF, UnexpectedEof indicates unexpected EOF
379                    Some(StdfErrorKind::Eof) => None,
380                    _ => Some(Err(e)),
381                };
382            }
383        };
384        // grow only when this record is larger than any seen so far, then
385        // fill and parse just the [..len] prefix
386        let len = header.len as usize;
387        if self.buffer.len() < len {
388            self.buffer.resize(len, 0);
389        }
390        if let Err(io_e) = self.inner.stream.read_exact(&mut self.buffer[..len]) {
391            return Some(Err(StdfError::new(StdfErrorKind::Io, io_e.to_string())));
392        }
393
394        let mut rec = StdfRecord::new_from_header(header);
395        rec.read_from_bytes(&self.buffer[..len], &self.inner.endianness);
396        Some(Ok(rec))
397    }
398}
399
400impl<R: BufRead + Seek> Iterator for RawDataIter<'_, R> {
401    type Item = Result<RawDataElement, StdfError>;
402
403    #[inline(always)]
404    fn next(&mut self) -> Option<Self::Item> {
405        let header = match self.inner.read_header() {
406            Ok(h) => h,
407            Err(e) => {
408                return match e.kind() {
409                    // Eof indicates normal EOF, UnexpectedEof indicates unexpected EOF
410                    Some(StdfErrorKind::Eof) => None,
411                    _ => Some(Err(e)),
412                };
413            }
414        };
415        // advance position by 4 after reading a header successfully
416        self.offset += 4;
417        let data_offset = self.offset;
418        // create a buffer to store record raw data
419        let mut buffer = vec![0u8; header.len as usize];
420        if let Err(io_e) = self.inner.stream.read_exact(&mut buffer) {
421            return Some(Err(StdfError::new(StdfErrorKind::Io, io_e.to_string())));
422        }
423        self.offset += header.len as u64;
424
425        Some(Ok(RawDataElement {
426            offset: data_offset,
427            header,
428            raw_data: buffer,
429            byte_order: self.inner.endianness,
430        }))
431    }
432}
433
434impl<R: BufRead + Seek> RawDataViewIter<'_, R> {
435    /// Advance the iterator, returning the next [`RawDataElementView`], or `None` at EOF.
436    ///
437    /// The returned item will be invalidated by the following call.
438    ///
439    /// It mirrors [`Iterator::next`] but cannot be part of the [`Iterator`] trait
440    /// because the item borrows from `self`.
441    #[inline(always)]
442    #[allow(clippy::should_implement_trait)] // lending iterator: item borrows `self`
443    pub fn next(&mut self) -> Option<Result<RawDataElementView<'_>, StdfError>> {
444        let header = match self.inner.read_header() {
445            Ok(h) => h,
446            Err(e) => {
447                return match e.kind() {
448                    // Eof indicates normal EOF, UnexpectedEof indicates unexpected EOF
449                    Some(StdfErrorKind::Eof) => None,
450                    _ => Some(Err(e)),
451                };
452            }
453        };
454        // advance position by 4 after reading a header successfully
455        self.offset += 4;
456        let data_offset = self.offset;
457        // grow only when this record is larger than any seen so far, then
458        // fill just the [..len] prefix and borrow it in the view
459        let len = header.len as usize;
460        if self.buffer.len() < len {
461            self.buffer.resize(len, 0);
462        }
463        if let Err(io_e) = self.inner.stream.read_exact(&mut self.buffer[..len]) {
464            return Some(Err(StdfError::new(StdfErrorKind::Io, io_e.to_string())));
465        }
466        self.offset += header.len as u64;
467
468        Some(Ok(RawDataElementView {
469            offset: data_offset,
470            header,
471            raw_data: &self.buffer[..len],
472            byte_order: self.inner.endianness,
473        }))
474    }
475}
476
477// help functions
478
479#[inline(always)]
480pub(crate) fn rewind_stream_position<R: BufRead + Seek>(
481    old_stream: StdfStream<R>,
482) -> Result<StdfStream<R>, StdfError> {
483    let new_stream = match old_stream {
484        StdfStream::Binary(mut br) => {
485            br.seek(SeekFrom::Start(0))?;
486            StdfStream::Binary(br)
487        }
488        #[cfg(feature = "gzip")]
489        StdfStream::Gz(gzr) => {
490            // get the inner handle and create a new stream after seek
491            let mut fp = gzr.into_inner();
492            fp.seek(SeekFrom::Start(0))?;
493            StdfStream::Gz(GzDecoder::new(fp))
494        }
495        #[cfg(feature = "bzip")]
496        StdfStream::Bz(bzr) => {
497            // get the inner handle and create a new stream after seek
498            let mut fp = bzr.into_inner();
499            fp.seek(SeekFrom::Start(0))?;
500            StdfStream::Bz(BzDecoder::new(fp))
501        }
502        #[cfg(feature = "zipfile")]
503        StdfStream::Zip(mut zipr) => {
504            zipr.reopen_file(0)?;
505            StdfStream::Zip(zipr)
506        }
507    };
508    Ok(new_stream)
509}
510
511#[cfg(all(feature = "atdf", any(feature = "gzip", feature = "bzip",)))]
512#[inline(always)]
513fn general_read_until<T: Read>(r: &mut T, delim: u8, buf: &mut Vec<u8>) -> io::Result<usize> {
514    let mut one_byte = [0u8; 1];
515    let mut n: usize = 0;
516    loop {
517        // read one byte at a time
518        match r.read(&mut one_byte) {
519            Ok(num) => {
520                if num == 0 {
521                    // EOF reached
522                    break;
523                }
524            }
525            Err(e) => return Err(e),
526        };
527        buf.extend_from_slice(&one_byte);
528        n += 1;
529        // break at delimiter
530        if delim == one_byte[0] {
531            break;
532        }
533    }
534    Ok(n)
535}