fst_reader/
reader.rs

1// Copyright 2023 The Regents of the University of California
2// Copyright 2024 Cornell University
3// released under BSD 3-Clause License
4// author: Kevin Laeufer <laeufer@cornell.edu>
5
6use crate::io::*;
7use crate::types::*;
8use std::io::{BufRead, Read, Seek, SeekFrom, Write};
9
10/// Reads in a FST file.
11pub struct FstReader<R: BufRead + Seek> {
12    input: InputVariant<R>,
13    meta: MetaData,
14}
15
16enum InputVariant<R: BufRead + Seek> {
17    Original(R),
18    // Uncompressed(BufReader<std::fs::File>),
19    UncompressedInMem(std::io::Cursor<Vec<u8>>),
20}
21
22/// Filter the changes by time and/or signals
23///
24/// The time filter is inclusive, i.e. it includes all changes in `start..=end`.
25pub struct FstFilter {
26    pub start: u64,
27    pub end: Option<u64>,
28    pub include: Option<Vec<FstSignalHandle>>,
29}
30
31impl FstFilter {
32    pub fn all() -> Self {
33        FstFilter {
34            start: 0,
35            end: None,
36            include: None,
37        }
38    }
39
40    pub fn new(start: u64, end: u64, signals: Vec<FstSignalHandle>) -> Self {
41        FstFilter {
42            start,
43            end: Some(end),
44            include: Some(signals),
45        }
46    }
47
48    pub fn filter_time(start: u64, end: u64) -> Self {
49        FstFilter {
50            start,
51            end: Some(end),
52            include: None,
53        }
54    }
55
56    pub fn filter_signals(signals: Vec<FstSignalHandle>) -> Self {
57        FstFilter {
58            start: 0,
59            end: None,
60            include: Some(signals),
61        }
62    }
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct FstHeader {
67    /// time of first sample
68    pub start_time: u64,
69    /// time of last sample
70    pub end_time: u64,
71    /// number of variables in the design
72    pub var_count: u64,
73    /// the highest signal handle; indicates the number of unique signals
74    pub max_handle: u64,
75    /// human readable version string
76    pub version: String,
77    /// human readable times stamp
78    pub date: String,
79    /// the exponent of the timescale; timescale will be 10^(exponent) seconds
80    pub timescale_exponent: i8,
81}
82
83impl<R: BufRead + Seek> FstReader<R> {
84    /// Reads in the FST file meta-data.
85    pub fn open(input: R) -> Result<Self> {
86        Self::open_internal(input, false)
87    }
88
89    pub fn open_and_read_time_table(input: R) -> Result<Self> {
90        Self::open_internal(input, true)
91    }
92
93    fn open_internal(mut input: R, read_time_table: bool) -> Result<Self> {
94        let uncompressed_input = uncompress_gzip_wrapper(&mut input)?;
95        match uncompressed_input {
96            UncompressGzipWrapper::None => {
97                let mut header_reader = HeaderReader::new(input);
98                header_reader.read(read_time_table)?;
99                let (input, meta) = header_reader.into_input_and_meta_data().unwrap();
100                Ok(FstReader {
101                    input: InputVariant::Original(input),
102                    meta,
103                })
104            }
105            UncompressGzipWrapper::InMemory(uc) => {
106                let mut header_reader = HeaderReader::new(uc);
107                header_reader.read(read_time_table)?;
108                let (uc2, meta) = header_reader.into_input_and_meta_data().unwrap();
109                Ok(FstReader {
110                    input: InputVariant::UncompressedInMem(uc2),
111                    meta,
112                })
113            }
114        }
115    }
116
117    pub fn get_header(&self) -> FstHeader {
118        FstHeader {
119            start_time: self.meta.header.start_time,
120            end_time: self.meta.header.end_time,
121            var_count: self.meta.header.var_count,
122            max_handle: self.meta.header.max_var_id_code,
123            version: self.meta.header.version.clone(),
124            date: self.meta.header.date.clone(),
125            timescale_exponent: self.meta.header.timescale_exponent,
126        }
127    }
128
129    pub fn get_time_table(&self) -> Option<&[u64]> {
130        match &self.meta.time_table {
131            Some(table) => Some(table),
132            None => None,
133        }
134    }
135
136    /// Reads the hierarchy and calls callback for every item.
137    pub fn read_hierarchy(&mut self, callback: impl FnMut(FstHierarchyEntry)) -> Result<()> {
138        match &mut self.input {
139            InputVariant::Original(input) => read_hierarchy(input, &self.meta, callback),
140            InputVariant::UncompressedInMem(input) => read_hierarchy(input, &self.meta, callback),
141        }
142    }
143
144    /// Read signal values for a specific time interval.
145    pub fn read_signals(
146        &mut self,
147        filter: &FstFilter,
148        callback: impl FnMut(u64, FstSignalHandle, FstSignalValue),
149    ) -> Result<()> {
150        // convert user filters
151        let signal_count = self.meta.signals.len();
152        let signal_mask = if let Some(signals) = &filter.include {
153            let mut signal_mask = BitMask::repeat(false, signal_count);
154            for sig in signals {
155                let signal_idx = sig.get_index();
156                signal_mask.set(signal_idx, true);
157            }
158            signal_mask
159        } else {
160            // include all
161            BitMask::repeat(true, signal_count)
162        };
163        let data_filter = DataFilter {
164            start: filter.start,
165            end: filter.end.unwrap_or(self.meta.header.end_time),
166            signals: signal_mask,
167        };
168
169        // build and run reader
170        match &mut self.input {
171            InputVariant::Original(input) => {
172                read_signals(input, &self.meta, &data_filter, callback)
173            }
174            InputVariant::UncompressedInMem(input) => {
175                read_signals(input, &self.meta, &data_filter, callback)
176            }
177        }
178    }
179}
180
181pub enum FstSignalValue<'a> {
182    String(&'a [u8]),
183    Real(f64),
184}
185
186/// Quickly scans an input to see if it could be a FST file.
187pub fn is_fst_file(input: &mut (impl Read + Seek)) -> bool {
188    let is_fst = matches!(internal_check_fst_file(input), Ok(true));
189    // try to reset input
190    let _ = input.seek(SeekFrom::Start(0));
191    is_fst
192}
193
194/// Returns an error or false if not an fst. Returns Ok(true) only if we think it is an fst.
195fn internal_check_fst_file(input: &mut (impl Read + Seek)) -> Result<bool> {
196    let mut seen_header = false;
197
198    // try to iterate over all blocks
199    loop {
200        let block_tpe = match read_block_tpe(input) {
201            Err(ReaderError::Io(_)) => {
202                break;
203            }
204            Err(other) => return Err(other),
205            Ok(tpe) => tpe,
206        };
207        let section_length = read_u64(input)?;
208        match block_tpe {
209            BlockType::GZipWrapper => return Ok(true),
210            BlockType::Header => {
211                seen_header = true;
212            }
213            BlockType::Skip if section_length == 0 => {
214                break;
215            }
216            _ => {}
217        }
218        input.seek(SeekFrom::Current((section_length as i64) - 8))?;
219    }
220    Ok(seen_header)
221}
222
223fn read_hierarchy(
224    input: &mut (impl Read + Seek),
225    meta: &MetaData,
226    mut callback: impl FnMut(FstHierarchyEntry),
227) -> Result<()> {
228    input.seek(SeekFrom::Start(meta.hierarchy_offset))?;
229    let bytes = read_hierarchy_bytes(input, meta.hierarchy_compression)?;
230    let mut input = bytes.as_slice();
231    let mut handle_count = 0u32;
232    while let Some(entry) = read_hierarchy_entry(&mut input, &mut handle_count)? {
233        callback(entry);
234    }
235    Ok(())
236}
237
238fn read_signals(
239    input: &mut (impl Read + Seek),
240    meta: &MetaData,
241    filter: &DataFilter,
242    mut callback: impl FnMut(u64, FstSignalHandle, FstSignalValue),
243) -> Result<()> {
244    let mut reader = DataReader {
245        input,
246        meta,
247        filter,
248        callback: &mut callback,
249    };
250    reader.read()
251}
252
253enum UncompressGzipWrapper {
254    None,
255    // TempFile(BufReader<std::fs::File>),
256    InMemory(std::io::Cursor<Vec<u8>>),
257}
258
259/// Checks to see if the whole file is compressed in which case it is decompressed
260/// to a temp file which is returned.
261fn uncompress_gzip_wrapper(input: &mut (impl Read + Seek)) -> Result<UncompressGzipWrapper> {
262    let block_tpe = read_block_tpe(input)?;
263    if block_tpe != BlockType::GZipWrapper {
264        // no gzip wrapper
265        input.seek(SeekFrom::Start(0))?;
266        Ok(UncompressGzipWrapper::None)
267    } else {
268        // uncompress
269        let section_length = read_u64(input)?;
270        let uncompress_length = read_u64(input)? as usize;
271        if section_length == 0 {
272            return Err(ReaderError::NotFinishedCompressing());
273        }
274
275        // TODO: add back the ability to uncompress to a temporary file without adding a dependency
276        // we always decompress into memory
277        let mut target = vec![];
278        decompress_gz_in_chunks(input, uncompress_length, &mut target)?;
279        let new_input = std::io::Cursor::new(target);
280        Ok(UncompressGzipWrapper::InMemory(new_input))
281    }
282}
283
284fn decompress_gz_in_chunks(
285    input: &mut (impl Read + Seek),
286    mut remaining: usize,
287    target: &mut impl Write,
288) -> Result<()> {
289    read_gzip_header(input)?;
290    let mut buf_in = vec![0u8; 32768 / 2]; // FST_GZIO_LEN
291    let mut buf_out = vec![0u8; 32768 * 2]; // FST_GZIO_LEN
292
293    let mut state = miniz_oxide::inflate::stream::InflateState::new(miniz_oxide::DataFormat::Raw);
294    let mut buf_in_remaining = 0;
295    while remaining > 0 {
296        // load more bytes into the input buffer
297        buf_in_remaining += input.read(&mut buf_in[buf_in_remaining..])?;
298        debug_assert!(
299            buf_in_remaining > 0,
300            "ran out of input data while gzip decompressing"
301        );
302
303        // decompress them
304        let res = miniz_oxide::inflate::stream::inflate(
305            &mut state,
306            &buf_in[0..buf_in_remaining],
307            buf_out.as_mut_slice(),
308            miniz_oxide::MZFlush::None,
309        );
310
311        println!("{res:?}\nremaining={remaining}");
312
313        match res.status {
314            Ok(status) => {
315                // move bytes that were not consumed to the start of the buffer and update the length
316                buf_in.copy_within(res.bytes_consumed..buf_in_remaining, 0);
317                buf_in_remaining -= res.bytes_consumed;
318
319                // write decompressed output
320                let out_bytes = std::cmp::min(res.bytes_written, remaining);
321                remaining -= out_bytes;
322                target.write_all(&buf_out[..out_bytes])?;
323
324                match status {
325                    miniz_oxide::MZStatus::Ok => {
326                        // nothing to do
327                    }
328                    miniz_oxide::MZStatus::StreamEnd => {
329                        debug_assert_eq!(remaining, 0);
330                        return Ok(());
331                    }
332                    miniz_oxide::MZStatus::NeedDict => {
333                        todo!("hande NeedDict status");
334                    }
335                }
336            }
337            Err(err) => {
338                return Err(ReaderError::GZipBody(format!("{err:?}")));
339            }
340        }
341    }
342
343    Ok(())
344}
345
346#[derive(Debug)]
347struct MetaData {
348    header: Header,
349    signals: Vec<SignalInfo>,
350    #[allow(dead_code)]
351    blackouts: Vec<BlackoutData>,
352    data_sections: Vec<DataSectionInfo>,
353    float_endian: FloatingPointEndian,
354    hierarchy_compression: HierarchyCompression,
355    hierarchy_offset: u64,
356    time_table: Option<Vec<u64>>,
357}
358
359pub type Result<T> = std::result::Result<T, ReaderError>;
360
361struct HeaderReader<R: Read + Seek> {
362    input: R,
363    header: Option<Header>,
364    signals: Option<Vec<SignalInfo>>,
365    blackouts: Option<Vec<BlackoutData>>,
366    data_sections: Vec<DataSectionInfo>,
367    float_endian: FloatingPointEndian,
368    hierarchy: Option<(HierarchyCompression, u64)>,
369    time_table: Option<Vec<u64>>,
370}
371
372impl<R: Read + Seek> HeaderReader<R> {
373    fn new(input: R) -> Self {
374        HeaderReader {
375            input,
376            header: None,
377            signals: None,
378            blackouts: None,
379            data_sections: Vec::default(),
380            float_endian: FloatingPointEndian::Little,
381            hierarchy: None,
382            time_table: None,
383        }
384    }
385
386    fn read_data(&mut self, tpe: &BlockType) -> Result<()> {
387        let file_offset = self.input.stream_position()?;
388        // this is the data section
389        let section_length = read_u64(&mut self.input)?;
390        let start_time = read_u64(&mut self.input)?;
391        let end_time = read_u64(&mut self.input)?;
392        let mem_required_for_traversal = read_u64(&mut self.input)?;
393
394        // optional: read the time table
395        if let Some(table) = &mut self.time_table {
396            let (_, mut time_chain) =
397                read_time_table(&mut self.input, file_offset, section_length)?;
398            // in the first section, we might need to include the start time
399            let is_first_section = table.is_empty();
400            if is_first_section && time_chain[0] > start_time {
401                table.push(start_time);
402            }
403            table.append(&mut time_chain);
404            self.input.seek(SeekFrom::Start(file_offset + 4 * 8))?;
405        }
406        // go to the end of the section
407        self.skip(section_length, 4 * 8)?;
408        let kind = DataSectionKind::from_block_type(tpe).unwrap();
409        let info = DataSectionInfo {
410            file_offset,
411            start_time,
412            end_time,
413            kind,
414            mem_required_for_traversal,
415        };
416        self.data_sections.push(info);
417        Ok(())
418    }
419
420    fn skip(&mut self, section_length: u64, already_read: i64) -> Result<u64> {
421        Ok(self
422            .input
423            .seek(SeekFrom::Current((section_length as i64) - already_read))?)
424    }
425
426    fn read_hierarchy(&mut self, compression: HierarchyCompression) -> Result<()> {
427        let file_offset = self.input.stream_position()?;
428        // this is the data section
429        let section_length = read_u64(&mut self.input)?;
430        self.skip(section_length, 8)?;
431        assert!(
432            self.hierarchy.is_none(),
433            "Only a single hierarchy block is expected!"
434        );
435        self.hierarchy = Some((compression, file_offset));
436        Ok(())
437    }
438
439    fn read(&mut self, read_time_table: bool) -> Result<()> {
440        if read_time_table {
441            self.time_table = Some(Vec::new());
442        }
443        loop {
444            let block_tpe = match read_block_tpe(&mut self.input) {
445                Err(ReaderError::Io(_)) => {
446                    break;
447                }
448                Err(other) => return Err(other),
449                Ok(tpe) => tpe,
450            };
451
452            dbg!(&block_tpe);
453
454            match block_tpe {
455                BlockType::Header => {
456                    let (header, endian) = read_header(&mut self.input)?;
457                    self.header = Some(header);
458                    self.float_endian = endian;
459                }
460                BlockType::VcData => self.read_data(&block_tpe)?,
461                BlockType::VcDataDynamicAlias => self.read_data(&block_tpe)?,
462                BlockType::VcDataDynamicAlias2 => self.read_data(&block_tpe)?,
463                BlockType::Blackout => {
464                    self.blackouts = Some(read_blackout(&mut self.input)?);
465                }
466                BlockType::Geometry => {
467                    self.signals = Some(read_geometry(&mut self.input)?);
468                }
469                BlockType::Hierarchy => self.read_hierarchy(HierarchyCompression::ZLib)?,
470                BlockType::HierarchyLZ4 => self.read_hierarchy(HierarchyCompression::Lz4)?,
471                BlockType::HierarchyLZ4Duo => self.read_hierarchy(HierarchyCompression::Lz4Duo)?,
472                BlockType::GZipWrapper => panic!("GZip Wrapper should have been handled earlier!"),
473                BlockType::Skip => {
474                    let section_length = read_u64(&mut self.input)?;
475                    if section_length == 0 {
476                        break;
477                    }
478                    self.skip(section_length, 8)?;
479                }
480            };
481        }
482
483        if self.signals.is_none() {
484            return Err(ReaderError::MissingGeometry());
485        }
486
487        if self.hierarchy.is_none() {
488            return Err(ReaderError::MissingHierarchy());
489        }
490
491        Ok(())
492    }
493
494    fn into_input_and_meta_data(mut self) -> Result<(R, MetaData)> {
495        self.input.seek(SeekFrom::Start(0))?;
496        let meta = MetaData {
497            header: self.header.unwrap(),
498            signals: self.signals.unwrap(),
499            blackouts: self.blackouts.unwrap_or_default(),
500            data_sections: self.data_sections,
501            float_endian: self.float_endian,
502            hierarchy_compression: self.hierarchy.unwrap().0,
503            hierarchy_offset: self.hierarchy.unwrap().1,
504            time_table: self.time_table,
505        };
506        Ok((self.input, meta))
507    }
508}
509
510struct DataReader<'a, R: Read + Seek, F: FnMut(u64, FstSignalHandle, FstSignalValue)> {
511    input: &'a mut R,
512    meta: &'a MetaData,
513    filter: &'a DataFilter,
514    callback: &'a mut F,
515}
516
517impl<R: Read + Seek, F: FnMut(u64, FstSignalHandle, FstSignalValue)> DataReader<'_, R, F> {
518    fn read_value_changes(
519        &mut self,
520        section_kind: DataSectionKind,
521        section_start: u64,
522        section_length: u64,
523        time_section_length: u64,
524        time_table: &[u64],
525    ) -> Result<()> {
526        let (max_handle, _) = read_variant_u64(&mut self.input)?;
527        let vc_start = self.input.stream_position()?;
528        let packtpe = ValueChangePackType::from_u8(read_u8(&mut self.input)?);
529        // the chain length is right in front of the time section
530        let chain_len_offset = section_start + section_length - time_section_length - 8;
531        let signal_offsets = read_signal_locs(
532            &mut self.input,
533            chain_len_offset,
534            section_kind,
535            max_handle,
536            vc_start,
537        )?;
538
539        // read data and create a bunch of pointers
540        let mut mu: Vec<u8> = Vec::new();
541        let mut head_pointer = vec![0u32; max_handle as usize];
542        let mut length_remaining = vec![0u32; max_handle as usize];
543        let mut scatter_pointer = vec![0u32; max_handle as usize];
544        let mut tc_head = vec![0u32; std::cmp::max(1, time_table.len())];
545
546        for entry in signal_offsets.iter() {
547            // is the signal supposed to be included?
548            if self.filter.signals.is_set(entry.signal_idx) {
549                // read all signal values
550                self.input.seek(SeekFrom::Start(vc_start + entry.offset))?;
551                let mut bytes =
552                    read_packed_signal_value_bytes(&mut self.input, entry.len, packtpe)?;
553
554                // read first time delta
555                let len = self.meta.signals[entry.signal_idx].len();
556                let tdelta = if len == 1 {
557                    read_one_bit_signal_time_delta(&bytes, 0)?
558                } else {
559                    read_multi_bit_signal_time_delta(&bytes, 0)?
560                };
561
562                // remember where we stored the signal data and how long it is
563                head_pointer[entry.signal_idx] = mu.len() as u32;
564                length_remaining[entry.signal_idx] = bytes.len() as u32;
565                mu.append(&mut bytes);
566
567                // remember at what time step we will read this signal
568                scatter_pointer[entry.signal_idx] = tc_head[tdelta];
569                tc_head[tdelta] = entry.signal_idx as u32 + 1; // index to handle
570            }
571        }
572
573        let mut buffer = Vec::new();
574
575        for (time_id, time) in time_table.iter().enumerate() {
576            // while we cannot ignore signal changes before the start of the window
577            // (since the signal might retain values for multiple cycles),
578            // signal changes after our window are completely useless
579            if *time > self.filter.end {
580                break;
581            }
582
583            let eof_error = || {
584                ReaderError::Io(std::io::Error::new(
585                    std::io::ErrorKind::UnexpectedEof,
586                    "unexpected eof",
587                ))
588            };
589
590            // handles cannot be zero
591            while tc_head[time_id] != 0 {
592                let signal_id = (tc_head[time_id] - 1) as usize; // convert handle to index
593                let mut mu_slice = &mu.as_slice()[head_pointer[signal_id] as usize..];
594                let (vli, skiplen) = read_variant_u32(&mut mu_slice)?;
595                let signal_len = self.meta.signals[signal_id].len();
596                let signal_handle = FstSignalHandle::from_index(signal_id);
597                let len = match signal_len {
598                    1 => {
599                        let value = one_bit_signal_value_to_char(vli);
600                        let value_buf = [value];
601                        (self.callback)(*time, signal_handle, FstSignalValue::String(&value_buf));
602                        0 // no additional bytes consumed
603                    }
604                    0 => {
605                        let (len, skiplen2) = read_variant_u32(&mut mu_slice)?;
606                        let value = mu_slice.get(..len as usize).ok_or_else(eof_error)?;
607                        (self.callback)(*time, signal_handle, FstSignalValue::String(value));
608                        len + skiplen2
609                    }
610                    len => {
611                        let signal_len = len as usize;
612                        if !self.meta.signals[signal_id].is_real() {
613                            let (value, len) = if (vli & 1) == 0 {
614                                // if bit0 is zero -> 2-state
615                                let read_len = signal_len.div_ceil(8);
616                                let bytes = mu_slice.get(..read_len).ok_or_else(eof_error)?;
617                                multi_bit_digital_signal_to_chars(bytes, signal_len, &mut buffer);
618                                (buffer.as_slice(), read_len as u32)
619                            } else {
620                                let value = mu_slice.get(..signal_len).ok_or_else(eof_error)?;
621                                (value, len)
622                            };
623                            (self.callback)(*time, signal_handle, FstSignalValue::String(value));
624                            len
625                        } else {
626                            assert_eq!(vli & 1, 1, "TODO: implement support for rare packed case");
627                            let value = read_f64(&mut mu_slice, self.meta.float_endian)?;
628                            (self.callback)(*time, signal_handle, FstSignalValue::Real(value));
629                            8
630                        }
631                    }
632                };
633
634                // update pointers
635                let total_skiplen = skiplen + len;
636                // advance "slice" for signal values
637                head_pointer[signal_id] += total_skiplen;
638                length_remaining[signal_id] -= total_skiplen;
639                // find the next signal to read in this time step
640                tc_head[time_id] = scatter_pointer[signal_id];
641                // invalidate pointer
642                scatter_pointer[signal_id] = 0;
643
644                // is there more data for this signal in the current block?
645                if length_remaining[signal_id] > 0 {
646                    let tdelta = if signal_len == 1 {
647                        read_one_bit_signal_time_delta(&mu, head_pointer[signal_id])?
648                    } else {
649                        read_multi_bit_signal_time_delta(&mu, head_pointer[signal_id])?
650                    };
651
652                    // point to the next time step
653                    scatter_pointer[signal_id] = tc_head[time_id + tdelta];
654                    tc_head[time_id + tdelta] = (signal_id + 1) as u32; // store handle
655                }
656            }
657        }
658
659        Ok(())
660    }
661
662    fn read(&mut self) -> Result<()> {
663        let sections = self.meta.data_sections.clone();
664        // filter out any sections which are not in our time window
665        let relevant_sections = sections
666            .iter()
667            .filter(|s| self.filter.end >= s.start_time && s.end_time >= self.filter.start);
668        for (sec_num, section) in relevant_sections.enumerate() {
669            // skip to section
670            self.input.seek(SeekFrom::Start(section.file_offset))?;
671            let section_length = read_u64(&mut self.input)?;
672
673            // verify meta-data
674            let start_time = read_u64(&mut self.input)?;
675            let end_time = read_u64(&mut self.input)?;
676            assert_eq!(start_time, section.start_time);
677            assert_eq!(end_time, section.end_time);
678            let is_first_section = sec_num == 0;
679
680            // read the time table
681            let (time_section_length, time_table) =
682                read_time_table(&mut self.input, section.file_offset, section_length)?;
683
684            // only read frame if this is the first section and there is no other data for
685            // the start time
686            if is_first_section && (time_table.is_empty() || time_table[0] > start_time) {
687                read_frame(
688                    &mut self.input,
689                    section.file_offset,
690                    section_length,
691                    &self.meta.signals,
692                    &self.filter.signals,
693                    self.meta.float_endian,
694                    start_time,
695                    self.callback,
696                )?;
697            } else {
698                skip_frame(&mut self.input, section.file_offset)?;
699            }
700
701            self.read_value_changes(
702                section.kind,
703                section.file_offset,
704                section_length,
705                time_section_length,
706                &time_table,
707            )?;
708        }
709
710        Ok(())
711    }
712}