cadmpeg_core/decode/space.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Address-space identifiers and stable descriptors.
3//!
4//! Every byte a decode reads belongs to exactly one address space: the root
5//! input, an inflated entry, a reconstructed stream. A [`SpaceId`] names one
6//! space within a single decode session; a [`View`](crate::decode::View)
7//! carries the id so error locations fall out of the type. Coordinates are
8//! absolute within a space: offset zero is that space's first byte.
9//!
10//! A [`SpaceId`] is session-local. [`SpaceDescriptor`] records the stable
11//! label, parent, and derivation needed to resolve a location into a
12//! root-to-leaf [`ResolvedAddress`] before the decode context is dropped.
13
14/// Names one address space within a single decode.
15///
16/// Ids are dense and assigned in registration order; the root is always
17/// [`SpaceId::ROOT`]. Error locations use the id to qualify offsets.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct SpaceId(u32);
20
21impl SpaceId {
22 /// The root input space, registered first by every decode.
23 pub const ROOT: SpaceId = SpaceId(0);
24
25 /// Creates a session-local address-space identifier.
26 pub(crate) const fn from_index(index: u32) -> Self {
27 Self(index)
28 }
29
30 /// Returns the dense index of this space.
31 pub fn index(self) -> u32 {
32 self.0
33 }
34}
35
36/// A half-open byte range `[start, end)` within one space.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct ByteRange {
39 /// Inclusive start offset.
40 pub start: u64,
41 /// Exclusive end offset.
42 pub end: u64,
43}
44
45/// How a registered space was derived from its parent.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum SpaceDerivation {
48 /// The root input image.
49 Root,
50 /// A stored (uncompressed) child range borrowed from the parent.
51 StoredSlice {
52 /// Parent space.
53 parent: SpaceId,
54 /// Range in the parent space.
55 range: ByteRange,
56 },
57 /// Decompressed or otherwise expanded output from a parent range.
58 Expanded {
59 /// Parent space that supplied the compressed bytes.
60 parent: SpaceId,
61 /// Compressed source range in the parent space.
62 source_range: ByteRange,
63 },
64 /// Concatenation of several parent windows.
65 Concatenated {
66 /// Parent spaces, in concatenation order.
67 parents: Vec<SpaceId>,
68 },
69}
70
71/// Stable description of one registered address space.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct SpaceDescriptor {
74 /// Session-local identifier.
75 pub id: SpaceId,
76 /// Stable label (archive member name, stream path, or `"root"`).
77 pub label: String,
78 /// How this space was derived.
79 pub derivation: SpaceDerivation,
80}
81
82/// One step in a root-to-leaf resolved address.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct AddressStep {
85 /// Stable label for this step.
86 pub label: String,
87 /// Whether this step is the root, a stored member, or an expansion.
88 pub kind: AddressStepKind,
89}
90
91/// Kind of one address step, for inspect replay.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum AddressStepKind {
94 /// Root input file.
95 Root,
96 /// Stored archive member (borrowed parent bytes).
97 StoredMember,
98 /// Expanded (inflated) archive member.
99 ExpandedMember,
100}
101
102/// Owned root-to-leaf address that survives the decode session.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ResolvedAddress {
105 /// Path from root to the space that owns the offset.
106 pub steps: Vec<AddressStep>,
107 /// Absolute offset within the leaf space.
108 pub offset: u64,
109}
110
111impl ResolvedAddress {
112 /// Formats the address as a stable path string.
113 pub fn path(&self) -> String {
114 let mut out = String::new();
115 for (index, step) in self.steps.iter().enumerate() {
116 if index > 0 {
117 out.push('/');
118 }
119 out.push_str(&step.label);
120 }
121 out.push('@');
122 out.push_str(&self.offset.to_string());
123 out
124 }
125
126 /// Returns `cadmpeg inspect` commands that replay this byte location.
127 ///
128 /// Nested archive members emit `inspect extract` then `inspect hex` on the
129 /// extracted member. Root-only addresses emit `inspect hex` on the file.
130 pub fn inspect_commands(&self, file: &str) -> Vec<String> {
131 let leaf = self.steps.last();
132 match leaf.map(|step| step.kind) {
133 None | Some(AddressStepKind::Root) => {
134 vec![format!(
135 "cadmpeg inspect hex {file} --offset {} --len 64",
136 self.offset
137 )]
138 }
139 Some(AddressStepKind::StoredMember | AddressStepKind::ExpandedMember) => {
140 let member = leaf.expect("leaf present").label.clone();
141 let extracted = format!("{file}.member");
142 vec![
143 format!("cadmpeg inspect extract {file} {member} -o {extracted}"),
144 format!(
145 "cadmpeg inspect hex {extracted} --offset {} --len 64",
146 self.offset
147 ),
148 ]
149 }
150 }
151 }
152}
153
154/// Resolves a session-local location against a descriptor table.
155pub fn resolve_address(
156 descriptors: &[SpaceDescriptor],
157 location: super::error::SourceLocation,
158) -> ResolvedAddress {
159 let mut steps = Vec::new();
160 let mut current = location.space;
161 let mut guard = 0u32;
162 while guard < descriptors.len() as u32 + 1 {
163 guard = guard.saturating_add(1);
164 let Some(descriptor) = descriptors.iter().find(|entry| entry.id == current) else {
165 break;
166 };
167 let kind = match descriptor.derivation {
168 SpaceDerivation::Root => AddressStepKind::Root,
169 SpaceDerivation::StoredSlice { .. } => AddressStepKind::StoredMember,
170 SpaceDerivation::Expanded { .. } | SpaceDerivation::Concatenated { .. } => {
171 AddressStepKind::ExpandedMember
172 }
173 };
174 steps.push(AddressStep {
175 label: descriptor.label.clone(),
176 kind,
177 });
178 match descriptor.derivation {
179 SpaceDerivation::Root => break,
180 SpaceDerivation::StoredSlice { parent, .. }
181 | SpaceDerivation::Expanded { parent, .. } => {
182 current = parent;
183 }
184 SpaceDerivation::Concatenated { ref parents } => {
185 if let Some(parent) = parents.first().copied() {
186 current = parent;
187 } else {
188 break;
189 }
190 }
191 }
192 }
193 steps.reverse();
194 if steps.is_empty() {
195 steps.push(AddressStep {
196 label: "root".into(),
197 kind: AddressStepKind::Root,
198 });
199 }
200 ResolvedAddress {
201 steps,
202 offset: location.offset,
203 }
204}