Skip to main content

disk_forensic/
layout.rs

1//! Bridge an analyzed image's [`DiskReport`] into the [`livedisk`] layout model,
2//! so the proportional partition bar renders for evidence files (E01, VMDK, …)
3//! exactly as it does for a live disk. This is presentation glue only — the
4//! authoritative structural analysis stays in the per-scheme reports; here we
5//! map partition extents (LBA × sector → byte offsets) onto the unified
6//! [`PhysicalDisk`]/[`Partition`] shape `livedisk` already knows how to draw.
7
8use livedisk::{Partition, PhysicalDisk};
9
10use crate::DiskReport;
11
12/// Convert a disk analysis into a [`PhysicalDisk`] for rendering. `name` labels
13/// the disk (typically the image path) and `size_bytes` is the decoded media
14/// size used for the bar's denominator and trailing free space.
15#[must_use]
16pub fn from_report(report: &DiskReport, name: &str, size_bytes: u64) -> PhysicalDisk {
17    let (sector, partitions) = match report {
18        DiskReport::Gpt(mbr) => gpt_partitions(mbr),
19        DiskReport::Mbr(mbr) => mbr_partitions(mbr),
20        DiskReport::Apm(apm) => apm_partitions(apm),
21    };
22    // The bar's denominator is the disk capacity. Use the decoded media size, but
23    // never let it fall below the partitions' own extent — a truncated or sparse
24    // image can be shorter than the layout its table declares.
25    let extent = partitions
26        .iter()
27        .map(|p| p.start_offset + p.size_bytes)
28        .max()
29        .unwrap_or(0);
30    PhysicalDisk {
31        device_path: name.to_string(),
32        name: name.to_string(),
33        size_bytes: size_bytes.max(extent),
34        logical_sector_size: sector,
35        physical_sector_size: sector,
36        model: None,
37        serial: None,
38        removable: false,
39        read_only: false,
40        synthesized: false,
41        partitions,
42    }
43}
44
45fn part(name: String, start: u64, size: u64, ty: String, label: Option<String>) -> Partition {
46    Partition {
47        device_path: String::new(),
48        name,
49        start_offset: start,
50        size_bytes: size,
51        partition_type: (!ty.is_empty()).then_some(ty),
52        mount_point: None,
53        filesystem: None,
54        label,
55    }
56}
57
58fn gpt_partitions(mbr: &mbr_partition_forensic::MbrAnalysis) -> (u32, Vec<Partition>) {
59    let Some(gpt) = &mbr.gpt else {
60        return (512, Vec::new());
61    };
62    let sector = gpt.sector_size;
63    let parts = gpt
64        .partitions
65        .iter()
66        .enumerate()
67        .map(|(i, p)| {
68            let ty = p
69                .type_name()
70                .map_or_else(|| p.type_guid.to_string(), ToString::to_string);
71            let label = (!p.name.is_empty()).then(|| p.name.clone());
72            part(
73                format!("part{}", i + 1),
74                p.first_lba * sector,
75                (p.last_lba - p.first_lba + 1) * sector,
76                ty,
77                label,
78            )
79        })
80        .collect();
81    (sector as u32, parts)
82}
83
84fn mbr_partitions(mbr: &mbr_partition_forensic::MbrAnalysis) -> (u32, Vec<Partition>) {
85    const SECTOR: u64 = 512;
86    let parts = mbr
87        .partitions
88        .iter()
89        .map(|p| {
90            part(
91                format!("part{}", p.index),
92                p.lba_start * SECTOR,
93                (p.lba_end - p.lba_start + 1) * SECTOR,
94                p.declared_type.name().to_string(),
95                None,
96            )
97        })
98        .collect();
99    (SECTOR as u32, parts)
100}
101
102fn apm_partitions(apm: &apm_partition_forensic::ApmAnalysis) -> (u32, Vec<Partition>) {
103    let bs = apm.block_size as u64;
104    let parts = apm
105        .partitions
106        .iter()
107        .enumerate()
108        .map(|(i, p)| {
109            let name = if p.name.is_empty() {
110                format!("part{}", i + 1)
111            } else {
112                p.name.clone()
113            };
114            part(
115                name,
116                p.start_block as u64 * bs,
117                (p.end_block() as u64 - p.start_block as u64 + 1) * bs,
118                p.type_name.clone(),
119                None,
120            )
121        })
122        .collect();
123    (bs as u32, parts)
124}