Skip to main content

pcf_debug/model/
layout.rs

1//! The physical byte-layout model: every region of the file, plus the gaps and
2//! overlaps between them, derived from a [`Walk`](super::walk::Walk).
3
4use pcf::{FileHeader, ENTRY_SIZE, HEADER_SIZE, TABLE_HEADER_SIZE};
5
6use super::diag::{DiagKind, Diagnostic, Severity};
7use super::walk::{BlockView, Walk};
8
9/// What a physical byte range is.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum RegionKind {
12    FileHeader,
13    TableBlockHeader {
14        block_index: usize,
15    },
16    EntryArray {
17        block_index: usize,
18        entry_count: u8,
19    },
20    PartitionData {
21        uid: [u8; 16],
22        partition_type: u32,
23        used: u64,
24        max: u64,
25    },
26    /// Reserved-but-unused tail of a partition (`max_length - used_bytes`).
27    Slack {
28        uid: [u8; 16],
29    },
30    /// Dead space covered by no declared region.
31    Gap,
32}
33
34impl RegionKind {
35    /// A single-letter glyph used in the compact ASCII strip.
36    pub fn glyph(&self) -> char {
37        match self {
38            RegionKind::FileHeader => 'H',
39            RegionKind::TableBlockHeader { .. } => 'T',
40            RegionKind::EntryArray { .. } => 'E',
41            RegionKind::PartitionData { .. } => 'D',
42            RegionKind::Slack { .. } => '_',
43            RegionKind::Gap => '.',
44        }
45    }
46
47    /// Stable short name used by the `--region` filter and HTML classes.
48    pub fn short(&self) -> &'static str {
49        match self {
50            RegionKind::FileHeader => "header",
51            RegionKind::TableBlockHeader { .. } => "tableheader",
52            RegionKind::EntryArray { .. } => "entries",
53            RegionKind::PartitionData { .. } => "data",
54            RegionKind::Slack { .. } => "slack",
55            RegionKind::Gap => "gap",
56        }
57    }
58}
59
60/// One contiguous physical byte range.
61#[derive(Debug, Clone)]
62pub struct Region {
63    pub start: u64,
64    pub len: u64,
65    pub kind: RegionKind,
66    pub label: String,
67}
68
69impl Region {
70    pub fn end(&self) -> u64 {
71        self.start.saturating_add(self.len)
72    }
73}
74
75/// The full physical model of one file.
76#[derive(Debug, Clone)]
77pub struct LayoutMap {
78    pub file_len: u64,
79    pub header: Option<FileHeader>,
80    pub blocks: Vec<BlockView>,
81    /// Sorted by start; gaps materialised, overlaps recorded as diagnostics.
82    pub regions: Vec<Region>,
83    pub diagnostics: Vec<Diagnostic>,
84}
85
86/// Build the layout model from a walk of the file.
87pub fn build(walk: &Walk) -> LayoutMap {
88    let mut diagnostics = walk.diagnostics.clone();
89    let mut regions: Vec<Region> = Vec::new();
90
91    // Header.
92    if walk.file_len >= HEADER_SIZE {
93        regions.push(Region {
94            start: 0,
95            len: HEADER_SIZE,
96            kind: RegionKind::FileHeader,
97            label: "file header".into(),
98        });
99    }
100
101    // Per-block header + entry array, and per-partition data + slack.
102    for b in &walk.blocks {
103        regions.push(Region {
104            start: b.offset,
105            len: TABLE_HEADER_SIZE,
106            kind: RegionKind::TableBlockHeader {
107                block_index: b.index,
108            },
109            label: format!("block {} header", b.index),
110        });
111        let count = b.header.partition_count;
112        if count > 0 {
113            regions.push(Region {
114                start: b.offset + TABLE_HEADER_SIZE,
115                len: count as u64 * ENTRY_SIZE,
116                kind: RegionKind::EntryArray {
117                    block_index: b.index,
118                    entry_count: count,
119                },
120                label: format!("block {} entries (x{count})", b.index),
121            });
122        }
123
124        for ev in &b.entries {
125            let e = &ev.entry;
126            let label = e.label_string().unwrap_or_default();
127            if e.used_bytes > 0 {
128                regions.push(Region {
129                    start: e.start_offset,
130                    len: e.used_bytes,
131                    kind: RegionKind::PartitionData {
132                        uid: e.uid,
133                        partition_type: e.partition_type,
134                        used: e.used_bytes,
135                        max: e.max_length,
136                    },
137                    label: format!("data: {label}"),
138                });
139            }
140            let slack = e.max_length.saturating_sub(e.used_bytes);
141            if slack > 0 {
142                regions.push(Region {
143                    start: e.start_offset + e.used_bytes,
144                    len: slack,
145                    kind: RegionKind::Slack { uid: e.uid },
146                    label: format!("slack: {label}"),
147                });
148            }
149        }
150    }
151
152    // Sort by start (then by length so zero-length regions sort first).
153    regions.sort_by(|a, b| a.start.cmp(&b.start).then(a.len.cmp(&b.len)));
154
155    // Walk the sorted list to materialise gaps and record overlaps.
156    let mut out: Vec<Region> = Vec::with_capacity(regions.len());
157    let mut covered_end: u64 = 0;
158    for r in regions.into_iter() {
159        if r.start > covered_end {
160            let gap_len = r.start - covered_end;
161            diagnostics.push(Diagnostic {
162                severity: Severity::Info,
163                kind: DiagKind::Gap {
164                    start: covered_end,
165                    len: gap_len,
166                },
167                message: format!("{gap_len} dead byte(s) at {covered_end:#x}"),
168            });
169            out.push(Region {
170                start: covered_end,
171                len: gap_len,
172                kind: RegionKind::Gap,
173                label: "gap".into(),
174            });
175        } else if r.start < covered_end && r.len > 0 {
176            let ov = covered_end - r.start;
177            diagnostics.push(Diagnostic {
178                severity: Severity::Warning,
179                kind: DiagKind::Overlap {
180                    start: r.start,
181                    len: ov.min(r.len),
182                },
183                message: format!(
184                    "region '{}' at {:#x} overlaps the preceding region",
185                    r.label, r.start
186                ),
187            });
188        }
189        covered_end = covered_end.max(r.end());
190        out.push(r);
191    }
192    if covered_end < walk.file_len {
193        let gap_len = walk.file_len - covered_end;
194        diagnostics.push(Diagnostic {
195            severity: Severity::Info,
196            kind: DiagKind::Gap {
197                start: covered_end,
198                len: gap_len,
199            },
200            message: format!("{gap_len} trailing dead byte(s) at {covered_end:#x}"),
201        });
202        out.push(Region {
203            start: covered_end,
204            len: gap_len,
205            kind: RegionKind::Gap,
206            label: "trailing gap".into(),
207        });
208    }
209
210    LayoutMap {
211        file_len: walk.file_len,
212        header: walk.header,
213        blocks: walk.blocks.clone(),
214        regions: out,
215        diagnostics,
216    }
217}