1use 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#[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 pub fn with_maximum_output_bytes(mut self, bytes: u64) -> Self {
39 self.maximum_output_bytes = Some(bytes);
40 self
41 }
42 pub fn with_preserve_checksum_suspect(mut self, preserve: bool) -> Self {
46 self.preserve_checksum_suspect = preserve;
47 self
48 }
49 pub fn with_maximum_records(mut self, count: usize) -> Self {
52 self.maximum_records = count;
53 self
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize))]
60pub enum RecoveryStatus {
61 Primary,
63 Redundant,
65 SuspectPrimary,
67 SuspectRedundant,
69 ZeroFilled,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize))]
76#[non_exhaustive]
77pub struct RecoveryRange {
78 pub first_chunk: u64,
80 pub chunk_count: u64,
82 pub logical_offset: u64,
84 pub byte_count: u64,
86 pub status: RecoveryStatus,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[non_exhaustive]
94pub struct RecoveryNotice {
95 pub segment_index: usize,
97 pub segment_offset: u64,
99 pub message: String,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106#[non_exhaustive]
107pub struct RecoveryProgress {
108 pub chunks_processed: u64,
110 pub chunks_total: u64,
112 pub bytes_written: u64,
114 pub bytes_total: u64,
116 pub last_status: Option<RecoveryStatus>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize))]
123#[non_exhaustive]
124pub struct RecoveryReport {
125 pub logical_size: u64,
127 pub chunks_total: u64,
129 pub chunks_primary: u64,
131 pub chunks_redundant: u64,
133 pub chunks_zero_filled: u64,
135 pub chunks_checksum_suspect: u64,
137 pub bytes_recovered: u64,
139 pub bytes_zero_filled: u64,
141 pub ranges: Vec<RecoveryRange>,
143 pub omitted_chunk_records: u64,
145 pub notices: Vec<RecoveryNotice>,
147 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#[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 pub fn open(path: impl AsRef<Path>, options: RecoveryOptions) -> Result<Self> {
185 Self::open_segments(discover_segments(path.as_ref())?, options)
186 }
187
188 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, §ions)? != 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, §ion.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, §ions, 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 pub fn segment_paths(&self) -> &[PathBuf] {
330 &self.paths
331 }
332
333 pub fn media_size(&self) -> u64 {
335 self.logical_size
336 }
337
338 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 pub fn recover_to_writer(&self, output: &mut impl Write) -> Result<RecoveryReport> {
351 self.recover_with_progress(output, |_| ControlFlow::Continue(()))
352 }
353
354 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 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 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}