Skip to main content

ewf_image/image/
recovery.rs

1//! Conservative recovery of physical, unencrypted zlib/raw EWF1 images.
2use std::io::Write;
3use std::ops::ControlFlow;
4
5use super::{
6    Section, decode_ewf1_entry, ewf1, matching_sectors_section, sectors_section_containing_offset,
7    validate_present_adler32_checksum, validate_present_ewf1_media_checksum,
8    validate_present_table_entries_checksum, validate_raw_chunk_checksum,
9};
10use crate::decode::{ChunkEncoding, decode_chunk, validate_encoded_size};
11use crate::index::{TableRange, TableRangeKind, logical_chunk_count};
12use crate::reader_statistics::ReaderStatisticsCollector;
13use crate::segment::discover_segments;
14use crate::{CompressionMethod, EwfError, Result, SegmentSource};
15use std::path::{Path, PathBuf};
16
17/// Controls recovery output size, suspect-data handling, and report retention.
18#[derive(Debug, Clone)]
19#[must_use]
20pub struct RecoveryOptions {
21    maximum_output_bytes: Option<u64>,
22    preserve_checksum_suspect: bool,
23    maximum_records: usize,
24}
25
26impl Default for RecoveryOptions {
27    fn default() -> Self {
28        Self {
29            maximum_output_bytes: None,
30            preserve_checksum_suspect: false,
31            maximum_records: 1024,
32        }
33    }
34}
35
36impl RecoveryOptions {
37    /// Rejects geometry exceeding this output limit before creating output.
38    pub fn with_maximum_output_bytes(mut self, bytes: u64) -> Self {
39        self.maximum_output_bytes = Some(bytes);
40        self
41    }
42    /// Permits decoded raw bytes with a bad checksum, or bytes addressed by a
43    /// checksum-damaged table, only when a validated alternate cannot recover them.
44    /// Such bytes are always marked suspect. The default substitutes zeros.
45    pub fn with_preserve_checksum_suspect(mut self, preserve: bool) -> Self {
46        self.preserve_checksum_suspect = preserve;
47        self
48    }
49    /// Bounds retained notices and outcome ranges independently. Counters and
50    /// streaming progress remain complete when records are omitted.
51    pub fn with_maximum_records(mut self, count: usize) -> Self {
52        self.maximum_records = count;
53        self
54    }
55}
56
57/// Provenance of one recovered logical chunk.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize))]
60pub enum RecoveryStatus {
61    /// Validated bytes addressed by the primary table.
62    Primary,
63    /// Validated bytes addressed by the redundant table.
64    Redundant,
65    /// Primary-table bytes retained despite a data or table checksum failure.
66    SuspectPrimary,
67    /// Redundant-table bytes retained despite a data or table checksum failure.
68    SuspectRedundant,
69    /// Unrecoverable bytes replaced with zeros.
70    ZeroFilled,
71}
72
73/// Consecutive chunks sharing the same recovery outcome.
74#[derive(Debug, Clone, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize))]
76#[non_exhaustive]
77pub struct RecoveryRange {
78    /// First logical chunk index.
79    pub first_chunk: u64,
80    /// Number of consecutive chunks.
81    pub chunk_count: u64,
82    /// Logical byte offset.
83    pub logical_offset: u64,
84    /// Logical byte length.
85    pub byte_count: u64,
86    /// Recovery provenance for this range.
87    pub status: RecoveryStatus,
88}
89
90/// Structural damage or missing metadata encountered while locating recoverable data.
91#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[non_exhaustive]
94pub struct RecoveryNotice {
95    /// Zero-based source segment index.
96    pub segment_index: usize,
97    /// Source segment byte offset.
98    pub segment_offset: u64,
99    /// Diagnostic message.
100    pub message: String,
101}
102
103/// Streaming recovery progress. The initial event has no last status.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106#[non_exhaustive]
107pub struct RecoveryProgress {
108    /// Number of emitted logical chunks.
109    pub chunks_processed: u64,
110    /// Total declared logical chunks.
111    pub chunks_total: u64,
112    /// Logical bytes written, including substituted zeros.
113    pub bytes_written: u64,
114    /// Declared logical output size.
115    pub bytes_total: u64,
116    /// Outcome of the most recently emitted chunk.
117    pub last_status: Option<RecoveryStatus>,
118}
119
120/// Completed recovery accounting. Recovery does not establish image authenticity.
121#[derive(Debug, Clone, PartialEq, Eq)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize))]
123#[non_exhaustive]
124pub struct RecoveryReport {
125    /// Declared output size.
126    pub logical_size: u64,
127    /// Total output chunks.
128    pub chunks_total: u64,
129    /// Chunks from the primary table, including explicitly retained suspect data.
130    pub chunks_primary: u64,
131    /// Chunks from the redundant table, including explicitly retained suspect data.
132    pub chunks_redundant: u64,
133    /// Chunks replaced with zeros.
134    pub chunks_zero_filled: u64,
135    /// Subset of primary/redundant chunks retained despite checksum failures.
136    pub chunks_checksum_suspect: u64,
137    /// Bytes read from evidence, including explicitly retained suspect data.
138    pub bytes_recovered: u64,
139    /// Bytes substituted with zeros.
140    pub bytes_zero_filled: u64,
141    /// Bounded, coalesced outcome records.
142    pub ranges: Vec<RecoveryRange>,
143    /// Chunks whose outcome records were omitted due to the retention limit.
144    pub omitted_chunk_records: u64,
145    /// Bounded structural notices from opening.
146    pub notices: Vec<RecoveryNotice>,
147    /// Number of omitted structural notices.
148    pub omitted_notices: u64,
149}
150
151#[derive(Debug)]
152struct Candidate {
153    range: TableRange,
154    data_start: u64,
155    trusted_table: bool,
156}
157#[derive(Debug)]
158struct RecoveryGroup {
159    first_chunk: u64,
160    count: u64,
161    primary: Option<Candidate>,
162    redundant: Option<Candidate>,
163}
164
165/// Read-only recovery plan for physical, unencrypted raw/zlib EWF1 segments.
166///
167/// Opens a reliable descriptor-chain prefix and requires consistent, validated
168/// media geometry. It does not carve lost descriptors, infer missing middle
169/// segments, recover encrypted images, or change the normal strict reader.
170#[derive(Debug)]
171pub struct EwfRecovery {
172    sources: Vec<SegmentSource>,
173    paths: Vec<PathBuf>,
174    groups: Vec<RecoveryGroup>,
175    chunk_size: u64,
176    logical_size: u64,
177    options: RecoveryOptions,
178    notices: Vec<RecoveryNotice>,
179    omitted_notices: u64,
180}
181
182impl EwfRecovery {
183    /// Discovers siblings and opens a recovery plan without creating output.
184    pub fn open(path: impl AsRef<Path>, options: RecoveryOptions) -> Result<Self> {
185        Self::open_segments(discover_segments(path.as_ref())?, options)
186    }
187
188    /// Opens explicitly ordered source paths. Numbering gaps are rejected because
189    /// EWF1 tables do not provide enough information to place later segments safely.
190    pub fn open_segments<P: AsRef<Path>>(
191        paths: impl IntoIterator<Item = P>,
192        options: RecoveryOptions,
193    ) -> Result<Self> {
194        let paths: Vec<_> = paths
195            .into_iter()
196            .map(|path| path.as_ref().to_path_buf())
197            .collect();
198        if paths.is_empty() {
199            return Err(EwfError::NoSegments("empty recovery segment list".into()));
200        }
201        let mut recovery = Self {
202            sources: Vec::new(),
203            paths,
204            groups: Vec::new(),
205            chunk_size: 0,
206            logical_size: 0,
207            options,
208            notices: Vec::new(),
209            omitted_notices: 0,
210        };
211        let mut next_chunk = 0;
212        let mut set_identifier = None;
213        for index in 0..recovery.paths.len() {
214            let source = SegmentSource::from_file(std::fs::File::open(&recovery.paths[index])?)?;
215            let mut header_bytes = [0; ewf1::FILE_HEADER_SIZE];
216            source.read_exact_at(&mut header_bytes, 0)?;
217            if header_bytes[..8] != ewf1::EVF_SIGNATURE || header_bytes[8] != 1 {
218                return Err(EwfError::Unsupported(
219                    "recovery currently requires physical unencrypted raw/zlib EWF1".into(),
220                ));
221            }
222            let header = ewf1::FileHeader::parse(&header_bytes)?;
223            if usize::from(header.segment_number) != index + 1 {
224                return Err(EwfError::Malformed(
225                    "recovery segment numbering is incomplete or out of order".into(),
226                ));
227            }
228            let sections = recovery.scan_sections(&source, index)?;
229            if index + 1 < recovery.paths.len()
230                && sections.last().is_none_or(|section| {
231                    !matches!(section.desc.section_type.as_str(), "next" | "done")
232                })
233            {
234                return Err(EwfError::Malformed(format!(
235                    "cannot place later segments after an incomplete descriptor chain in segment {}",
236                    index + 1
237                )));
238            }
239            if super::detect_ewf1_compression_method(header, &sections)? != CompressionMethod::Zlib
240            {
241                return Err(EwfError::Unsupported(
242                    "recovery compression profile is not supported".into(),
243                ));
244            }
245            if sections
246                .iter()
247                .any(|section| section.desc.section_type == "x_encryption")
248            {
249                return Err(EwfError::Unsupported(
250                    "encrypted recovery is not implemented".into(),
251                ));
252            }
253            if let Some(section) = sections.iter().find(|section| {
254                matches!(
255                    section.desc.section_type.as_str(),
256                    "volume" | "disk" | "data"
257                )
258            }) {
259                if section.data_size > 4096 {
260                    return Err(EwfError::Unsupported(
261                        "unrecognized recovery media geometry layout".into(),
262                    ));
263                }
264                let bytes = source_bytes(&source, section.data_offset, section.data_size)?;
265                validate_present_ewf1_media_checksum(&bytes, &section.desc.section_type)?;
266                let volume = ewf1::Volume::parse(&bytes)?;
267                if volume.smart {
268                    return Err(EwfError::Unsupported(
269                        "SMART recovery is not implemented".into(),
270                    ));
271                }
272                let chunk_size = volume.chunk_size()?;
273                let logical_size = volume.logical_size()?;
274                if chunk_size == 0
275                    || chunk_size > super::MAX_CHUNK_SIZE
276                    || logical_chunk_count(logical_size, chunk_size)?
277                        != u64::from(volume.chunk_count)
278                {
279                    return Err(EwfError::Malformed(
280                        "recovery media geometry is inconsistent".into(),
281                    ));
282                }
283                if index == 0 {
284                    recovery.chunk_size = chunk_size;
285                    recovery.logical_size = logical_size;
286                    set_identifier = volume.set_identifier;
287                } else if chunk_size != recovery.chunk_size
288                    || logical_size != recovery.logical_size
289                    || volume.set_identifier != set_identifier
290                {
291                    return Err(EwfError::Malformed(
292                        "recovery segment media geometry or set identifier differs".into(),
293                    ));
294                }
295            } else if index == 0 {
296                return Err(EwfError::Malformed(
297                    "recovery requires intact first-segment media geometry".into(),
298                ));
299            }
300            if recovery
301                .options
302                .maximum_output_bytes
303                .is_some_and(|limit| recovery.logical_size > limit)
304            {
305                return Err(std::io::Error::new(
306                    std::io::ErrorKind::InvalidInput,
307                    "recovery exceeds configured output limit",
308                )
309                .into());
310            }
311            let before = next_chunk;
312            recovery.collect_tables(&source, &sections, index, &mut next_chunk)?;
313            if next_chunk == before && index + 1 < recovery.paths.len() {
314                return Err(EwfError::Malformed(
315                    "cannot position later recovery segments after missing table coverage".into(),
316                ));
317            }
318            recovery.sources.push(source);
319        }
320        if next_chunk > logical_chunk_count(recovery.logical_size, recovery.chunk_size)? {
321            return Err(EwfError::Malformed(
322                "recovery tables exceed declared media size".into(),
323            ));
324        }
325        Ok(recovery)
326    }
327
328    /// Returns source paths in logical segment order.
329    pub fn segment_paths(&self) -> &[PathBuf] {
330        &self.paths
331    }
332
333    /// Returns the declared size of a recovered raw output.
334    pub fn media_size(&self) -> u64 {
335        self.logical_size
336    }
337
338    /// Creates a new raw output file. Existing paths, including aliases of source
339    /// segments, are never overwritten. Errors leave a partial output for inspection.
340    pub fn recover_to_path(&self, path: impl AsRef<Path>) -> Result<RecoveryReport> {
341        let mut output = std::fs::OpenOptions::new()
342            .write(true)
343            .create_new(true)
344            .open(path)?;
345        self.recover_to_writer(&mut output)
346    }
347
348    /// Writes recovered raw bytes to a caller-owned destination. The caller must
349    /// keep this destination separate from the source evidence.
350    pub fn recover_to_writer(&self, output: &mut impl Write) -> Result<RecoveryReport> {
351        self.recover_with_progress(output, |_| ControlFlow::Continue(()))
352    }
353
354    /// Recovers with one initial event and one event per emitted chunk. Returning
355    /// `Break(())` stops with `Aborted`; already-written bytes remain in the output.
356    pub fn recover_with_progress(
357        &self,
358        output: &mut impl Write,
359        mut progress: impl FnMut(RecoveryProgress) -> ControlFlow<()>,
360    ) -> Result<RecoveryReport> {
361        let total = logical_chunk_count(self.logical_size, self.chunk_size)?;
362        let mut event = RecoveryProgress {
363            chunks_processed: 0,
364            chunks_total: total,
365            bytes_written: 0,
366            bytes_total: self.logical_size,
367            last_status: None,
368        };
369        if progress(event).is_break() {
370            return Err(EwfError::Aborted);
371        }
372        let mut report = RecoveryReport {
373            logical_size: self.logical_size,
374            chunks_total: total,
375            chunks_primary: 0,
376            chunks_redundant: 0,
377            chunks_zero_filled: 0,
378            chunks_checksum_suspect: 0,
379            bytes_recovered: 0,
380            bytes_zero_filled: 0,
381            ranges: Vec::new(),
382            omitted_chunk_records: 0,
383            notices: self.notices.clone(),
384            omitted_notices: self.omitted_notices,
385        };
386        for id in 0..total {
387            let size = self.chunk_size.min(self.logical_size - event.bytes_written) as usize;
388            let (bytes, status) = self.recover_chunk(id, size);
389            output.write_all(&bytes)?;
390            match status {
391                RecoveryStatus::Primary | RecoveryStatus::SuspectPrimary => {
392                    report.chunks_primary += 1;
393                }
394                RecoveryStatus::Redundant | RecoveryStatus::SuspectRedundant => {
395                    report.chunks_redundant += 1;
396                }
397                RecoveryStatus::ZeroFilled => report.chunks_zero_filled += 1,
398            }
399            if matches!(
400                status,
401                RecoveryStatus::SuspectPrimary | RecoveryStatus::SuspectRedundant
402            ) {
403                report.chunks_checksum_suspect += 1;
404            }
405            if status == RecoveryStatus::ZeroFilled {
406                report.bytes_zero_filled += size as u64;
407            } else {
408                report.bytes_recovered += size as u64;
409            }
410            if let Some(last) = report.ranges.last_mut().filter(|range| {
411                range.status == status && range.first_chunk + range.chunk_count == id
412            }) {
413                last.chunk_count += 1;
414                last.byte_count += size as u64;
415            } else if report.ranges.len() < self.options.maximum_records {
416                report.ranges.push(RecoveryRange {
417                    first_chunk: id,
418                    chunk_count: 1,
419                    logical_offset: event.bytes_written,
420                    byte_count: size as u64,
421                    status,
422                });
423            } else {
424                report.omitted_chunk_records += 1;
425            }
426            event.chunks_processed += 1;
427            event.bytes_written += size as u64;
428            event.last_status = Some(status);
429            if progress(event).is_break() {
430                return Err(EwfError::Aborted);
431            }
432        }
433        output.flush()?;
434        Ok(report)
435    }
436
437    fn notice(&mut self, index: usize, offset: u64, message: impl Into<String>) {
438        if self.notices.len() < self.options.maximum_records {
439            self.notices.push(RecoveryNotice {
440                segment_index: index,
441                segment_offset: offset,
442                message: message.into(),
443            });
444        } else {
445            self.omitted_notices += 1;
446        }
447    }
448
449    fn scan_sections(&mut self, source: &SegmentSource, index: usize) -> Result<Vec<Section>> {
450        let mut sections = Vec::new();
451        let mut offset = ewf1::FILE_HEADER_SIZE as u64;
452        loop {
453            if source.len().saturating_sub(offset) < 76 {
454                self.notice(index, offset, "descriptor chain is truncated");
455                break;
456            }
457            let bytes = source_bytes(source, offset, 76)?;
458            if let Err(error) =
459                validate_present_adler32_checksum(&bytes, 72, 72, "EWF1 section descriptor")
460            {
461                self.notice(index, offset, error.to_string());
462                break;
463            }
464            let desc = ewf1::SectionDescriptor::parse(&bytes, offset)?;
465            let size = match desc.data_size() {
466                Ok(size) => size,
467                Err(error) => {
468                    self.notice(index, offset, error.to_string());
469                    break;
470                }
471            };
472            let data_offset = offset + 76;
473            let data_size = size.min(source.len() - data_offset);
474            let next = desc.next;
475            let terminal = matches!(desc.section_type.as_str(), "done" | "next");
476            let incomplete = desc.section_type != "done";
477            sections.push(Section {
478                desc,
479                data_offset,
480                data_size,
481            });
482            if size > data_size {
483                self.notice(index, offset, "section payload is truncated");
484                break;
485            }
486            if terminal {
487                if incomplete && index + 1 == self.paths.len() {
488                    self.notice(
489                        index,
490                        offset,
491                        "segment has an incomplete acquisition marker",
492                    );
493                }
494                break;
495            }
496            if next < data_offset + size || next <= offset {
497                self.notice(
498                    index,
499                    offset,
500                    "descriptor chain ends or does not advance beyond the current section",
501                );
502                break;
503            }
504            offset = next;
505        }
506        Ok(sections)
507    }
508
509    fn collect_tables(
510        &mut self,
511        source: &SegmentSource,
512        sections: &[Section],
513        index: usize,
514        next_chunk: &mut u64,
515    ) -> Result<()> {
516        let first_group = self.groups.len();
517        if !sections
518            .iter()
519            .any(|section| section.desc.section_type == "sectors")
520            && sections
521                .iter()
522                .any(|section| matches!(section.desc.section_type.as_str(), "table" | "table2"))
523        {
524            return Err(EwfError::Unsupported(
525                "recovery requires separate sectors sections".into(),
526            ));
527        }
528        let mut tables = sections
529            .iter()
530            .filter(|section| matches!(section.desc.section_type.as_str(), "table" | "table2"))
531            .peekable();
532        while let Some(section) = tables.next() {
533            let primary_is_redundant = section.desc.section_type == "table2";
534            let first = self.table_candidate(source, sections, section, index);
535            let mut second = None;
536            if !primary_is_redundant
537                && tables
538                    .peek()
539                    .is_some_and(|next| next.desc.section_type == "table2")
540            {
541                let mirror = tables.peek().expect("peeked table");
542                // Group only matching geometry, or a valid table2 replacing an unreadable header.
543                let identity = table_identity(source, section).ok();
544                let mirror_identity = table_identity(source, mirror).ok();
545                if identity.zip(mirror_identity).is_none_or(|(a, b)| a == b) {
546                    second = self.table_candidate(source, sections, mirror, index);
547                    tables.next();
548                }
549            }
550            let count = first
551                .as_ref()
552                .or(second.as_ref())
553                .map(|candidate| candidate.range.chunk_count);
554            let Some(count) = count else {
555                // Without a count, later ranges cannot be positioned safely.
556                if tables.peek().is_some() || index + 1 < self.paths.len() {
557                    return Err(EwfError::Malformed(
558                        "unrecoverable table header prevents placement of later chunks".into(),
559                    ));
560                }
561                break;
562            };
563            let first_chunk = *next_chunk;
564            *next_chunk = next_chunk
565                .checked_add(count)
566                .ok_or_else(|| EwfError::Malformed("recovery chunk count overflow".into()))?;
567            self.groups.push(if primary_is_redundant {
568                RecoveryGroup {
569                    first_chunk,
570                    count,
571                    primary: None,
572                    redundant: first,
573                }
574            } else {
575                RecoveryGroup {
576                    first_chunk,
577                    count,
578                    primary: first,
579                    redundant: second,
580                }
581            });
582        }
583        for sectors in sections
584            .iter()
585            .filter(|section| section.desc.section_type == "sectors" && section.data_size > 0)
586        {
587            let candidates = || {
588                self.groups[first_group..]
589                    .iter()
590                    .flat_map(|group| group.primary.iter().chain(&group.redundant))
591            };
592            if !candidates().any(|candidate| candidate.data_start == sectors.data_offset)
593                && (index + 1 < self.paths.len()
594                    || candidates().any(|candidate| candidate.data_start > sectors.data_offset))
595            {
596                return Err(EwfError::Malformed(
597                    "unindexed sectors prevent placement of later recovery chunks".into(),
598                ));
599            }
600        }
601        Ok(())
602    }
603
604    fn table_candidate(
605        &mut self,
606        source: &SegmentSource,
607        sections: &[Section],
608        section: &Section,
609        index: usize,
610    ) -> Option<Candidate> {
611        match parse_candidate(source, sections, section, index) {
612            Ok(candidate) => {
613                if !candidate.trusted_table {
614                    self.notice(
615                        index,
616                        section.data_offset,
617                        "table entry checksum failed; addresses are suspect",
618                    );
619                }
620                Some(candidate)
621            }
622            Err(error) => {
623                self.notice(index, section.data_offset, error.to_string());
624                None
625            }
626        }
627    }
628
629    fn recover_chunk(&self, id: u64, size: usize) -> (Vec<u8>, RecoveryStatus) {
630        let index = self
631            .groups
632            .partition_point(|group| group.first_chunk + group.count <= id);
633        let mut suspect = None;
634        if let Some(group) = self
635            .groups
636            .get(index)
637            .filter(|group| group.first_chunk <= id)
638        {
639            for (candidate, clean, damaged) in [
640                (
641                    &group.primary,
642                    RecoveryStatus::Primary,
643                    RecoveryStatus::SuspectPrimary,
644                ),
645                (
646                    &group.redundant,
647                    RecoveryStatus::Redundant,
648                    RecoveryStatus::SuspectRedundant,
649                ),
650            ] {
651                if let Some(candidate) = candidate
652                    && let Ok((bytes, checksum_ok)) = read_candidate(
653                        &self.sources[candidate.range.segment_index],
654                        candidate,
655                        id - group.first_chunk,
656                        size,
657                        self.chunk_size,
658                    )
659                {
660                    if checksum_ok && candidate.trusted_table {
661                        return (bytes, clean);
662                    }
663                    if self.options.preserve_checksum_suspect && suspect.is_none() {
664                        suspect = Some((bytes, damaged));
665                    }
666                }
667            }
668        }
669        suspect.unwrap_or_else(|| (vec![0; size], RecoveryStatus::ZeroFilled))
670    }
671}
672
673fn source_bytes(source: &SegmentSource, offset: u64, size: u64) -> Result<Vec<u8>> {
674    let mut bytes = vec![
675        0;
676        usize::try_from(size).map_err(|_| EwfError::Malformed(
677            "recovery read size exceeds usize".into()
678        ))?
679    ];
680    source.read_exact_at(&mut bytes, offset)?;
681    Ok(bytes)
682}
683
684fn table_identity(source: &SegmentSource, section: &Section) -> Result<(u64, u64)> {
685    if section.data_size < 24 {
686        return Err(EwfError::Malformed(
687            "recovery table header is truncated".into(),
688        ));
689    }
690    let header = source_bytes(source, section.data_offset, 24)?;
691    validate_present_adler32_checksum(&header, 20, 20, "EWF1 table header")?;
692    Ok((
693        u64::from(u32::from_le_bytes(
694            header[..4].try_into().expect("header read"),
695        )),
696        u64::from_le_bytes(header[8..16].try_into().expect("header read")),
697    ))
698}
699
700fn parse_candidate(
701    source: &SegmentSource,
702    sections: &[Section],
703    section: &Section,
704    segment_index: usize,
705) -> Result<Candidate> {
706    let (count, base) = table_identity(source, section)?;
707    let entries_offset = section.data_offset + 24;
708    if count == 0 || count * 4 > section.data_size - 24 {
709        return Err(EwfError::Malformed(
710            "recovery table entries are missing or truncated".into(),
711        ));
712    }
713    let first = source_bytes(source, entries_offset, 4)?;
714    let first_offset = base
715        .checked_add(u64::from(
716            u32::from_le_bytes(first.try_into().expect("entry read")) & 0x7fff_ffff,
717        ))
718        .ok_or_else(|| EwfError::Malformed("recovery first chunk offset overflow".into()))?;
719    let sectors = matching_sectors_section(sections, base)
720        .or_else(|| sectors_section_containing_offset(sections, first_offset))
721        .ok_or_else(|| {
722            EwfError::Unsupported("recovery requires a separate sectors section".into())
723        })?;
724    let entries_end = entries_offset + count * 4;
725    let trusted_table = if entries_end + 4 <= section.data_offset + section.data_size {
726        validate_present_table_entries_checksum(
727            &mut source.cursor(),
728            entries_offset,
729            count * 4,
730            entries_end,
731            "recovery table",
732            &ReaderStatisticsCollector::new(false),
733        )
734        .is_ok()
735    } else {
736        true
737    };
738    Ok(Candidate {
739        range: TableRange {
740            kind: TableRangeKind::Ewf1,
741            segment_index,
742            first_chunk: 0,
743            chunk_count: count,
744            entries_offset,
745            base_offset: base,
746            data_end: Some(
747                sectors
748                    .data_offset
749                    .checked_add(sectors.desc.data_size()?)
750                    .ok_or_else(|| {
751                        EwfError::Malformed("recovery sectors extent overflow".into())
752                    })?,
753            ),
754            ewf1_allow_large_compressed_chunks: false,
755            ewf1_compression_method: Some(CompressionMethod::Zlib),
756            ewf2_compression_method: None,
757        },
758        data_start: sectors.data_offset,
759        trusted_table,
760    })
761}
762
763fn read_candidate(
764    source: &SegmentSource,
765    candidate: &Candidate,
766    local: u64,
767    size: usize,
768    chunk_size: u64,
769) -> Result<(Vec<u8>, bool)> {
770    let range = &candidate.range;
771    let read_entry = |index: u64| -> Result<u32> {
772        Ok(u32::from_le_bytes(
773            source_bytes(source, range.entries_offset + index * 4, 4)?
774                .try_into()
775                .expect("entry read"),
776        ))
777    };
778    let raw = read_entry(local)?;
779    let next = (local + 1 < range.chunk_count)
780        .then(|| read_entry(local + 1))
781        .transpose()?;
782    let entry = decode_ewf1_entry(range, raw, chunk_size, next, local + 1 == range.chunk_count)?;
783    let end = if let Some(raw) = next {
784        decode_ewf1_entry(range, raw, chunk_size, None, local + 2 == range.chunk_count)?.offset
785    } else {
786        range.data_end.expect("recovery candidate has data end")
787    };
788    if entry.offset < candidate.data_start
789        || end > range.data_end.expect("candidate has data end")
790        || end <= entry.offset
791    {
792        return Err(EwfError::Malformed(
793            "recovery chunk is outside its sectors section".into(),
794        ));
795    }
796    let encoding = if entry.compressed {
797        ChunkEncoding::Zlib
798    } else {
799        ChunkEncoding::Raw
800    };
801    validate_encoded_size(end - entry.offset, chunk_size, encoding)?;
802    let bytes = source_bytes(source, entry.offset, end - entry.offset)?;
803    let checksum_ok = if encoding != ChunkEncoding::Raw {
804        true
805    } else if bytes.len() == size + 4 || bytes.len() as u64 == chunk_size + 4 {
806        validate_raw_chunk_checksum(&bytes, bytes.len() - 4).is_ok()
807    } else if bytes.len() == size || bytes.len() as u64 == chunk_size {
808        true
809    } else {
810        return Err(EwfError::Malformed(
811            "recovery raw chunk length is not a recognized layout".into(),
812        ));
813    };
814    Ok((decode_chunk(&bytes, encoding, size)?, checksum_ok))
815}