Skip to main content

macho_core/model/addr/
map.rs

1use crate::error::{Error, Result};
2use crate::model::addr::types::{Rva, ThinFileOffset, Va};
3
4/// One validated file-to-virtual mapping.
5#[derive(Debug, Clone)]
6pub struct MappingEntry {
7    file_offset: ThinFileOffset,
8    file_size: u64,
9    vm_addr: Va,
10    vm_size: u64,
11}
12
13impl MappingEntry {
14    /// Construct a mapping after checking file and virtual range arithmetic.
15    pub fn try_new(
16        file_offset: ThinFileOffset,
17        file_size: u64,
18        vm_addr: Va,
19        vm_size: u64,
20    ) -> Result<Self> {
21        file_offset
22            .0
23            .checked_add(file_size)
24            .ok_or_else(|| Error::address("mapping file range overflows"))?;
25        vm_addr
26            .0
27            .checked_add(vm_size)
28            .ok_or_else(|| Error::address("mapping virtual range overflows"))?;
29        if file_size > vm_size {
30            return Err(Error::address(format!(
31                "mapping file size {file_size:#x} exceeds virtual size {vm_size:#x}"
32            )));
33        }
34        Ok(Self {
35            file_offset,
36            file_size,
37            vm_addr,
38            vm_size,
39        })
40    }
41
42    /// Thin-image-relative file start.
43    pub fn file_offset(&self) -> ThinFileOffset {
44        self.file_offset
45    }
46
47    /// File-backed length.
48    pub fn file_size(&self) -> u64 {
49        self.file_size
50    }
51
52    /// Virtual start address.
53    pub fn vm_addr(&self) -> Va {
54        self.vm_addr
55    }
56
57    /// Virtual range length.
58    pub fn vm_size(&self) -> u64 {
59        self.vm_size
60    }
61}
62
63/// Sorted, non-overlapping file and virtual address mappings.
64#[derive(Debug, Clone)]
65pub struct AddressMap {
66    entries: Vec<MappingEntry>,
67}
68
69impl AddressMap {
70    /// Validate, stably sort, and construct an address map.
71    pub fn try_new(mut entries: Vec<MappingEntry>) -> Result<Self> {
72        entries.sort_by_key(|entry| (entry.file_offset.0, entry.vm_addr.0));
73        for pair in entries.windows(2) {
74            let left_file_end = pair[0]
75                .file_offset
76                .0
77                .checked_add(pair[0].file_size)
78                .ok_or_else(|| Error::address("mapping file range overflows"))?;
79            if pair[0].file_size != 0
80                && pair[1].file_size != 0
81                && pair[1].file_offset.0 < left_file_end
82            {
83                return Err(Error::address("mapping file ranges overlap"));
84            }
85        }
86        let mut virtual_order: Vec<_> = entries.iter().collect();
87        virtual_order.sort_by_key(|entry| entry.vm_addr.0);
88        for pair in virtual_order.windows(2) {
89            let left_vm_end = pair[0]
90                .vm_addr
91                .0
92                .checked_add(pair[0].vm_size)
93                .ok_or_else(|| Error::address("mapping virtual range overflows"))?;
94            if pair[0].vm_size != 0 && pair[1].vm_size != 0 && pair[1].vm_addr.0 < left_vm_end {
95                return Err(Error::address("mapping virtual ranges overlap"));
96            }
97        }
98        Ok(Self { entries })
99    }
100
101    /// Validated mappings in stable file-offset order.
102    pub fn entries(&self) -> &[MappingEntry] {
103        &self.entries
104    }
105
106    /// Convert a thin-image-relative file offset to a virtual address.
107    pub fn thin_offset_to_va(&self, offset: ThinFileOffset) -> Result<Va> {
108        for entry in &self.entries {
109            if entry.file_size == 0 {
110                continue;
111            }
112            let Some(relative) = offset.0.checked_sub(entry.file_offset.0) else {
113                continue;
114            };
115            if relative < entry.file_size {
116                return entry
117                    .vm_addr
118                    .0
119                    .checked_add(relative)
120                    .map(Va)
121                    .ok_or_else(|| Error::address("mapped virtual address overflows"));
122            }
123        }
124        Err(Error::address(format!(
125            "file offset {offset} is not mapped to any segment"
126        )))
127    }
128
129    /// Convert a virtual address to a thin-image-relative file offset.
130    pub fn va_to_thin_offset(&self, va: Va) -> Result<ThinFileOffset> {
131        for entry in &self.entries {
132            if entry.vm_size == 0 {
133                continue;
134            }
135            let Some(relative) = va.0.checked_sub(entry.vm_addr.0) else {
136                continue;
137            };
138            if relative < entry.vm_size {
139                if relative >= entry.file_size {
140                    return Err(Error::address(format!(
141                        "VA {va} maps to zero-fill region (beyond file-backed portion)"
142                    )));
143                }
144                return entry
145                    .file_offset
146                    .0
147                    .checked_add(relative)
148                    .map(ThinFileOffset)
149                    .ok_or_else(|| Error::address("mapped file offset overflows"));
150            }
151        }
152        Err(Error::address(format!(
153            "VA {va} is not mapped to any segment"
154        )))
155    }
156
157    /// Convert an RVA to a VA with checked arithmetic.
158    pub fn rva_to_va(rva: Rva, image_base: Va) -> Result<Va> {
159        image_base
160            .0
161            .checked_add(rva.0)
162            .map(Va)
163            .ok_or_else(|| Error::address("RVA to VA conversion overflows"))
164    }
165
166    /// Convert a VA to an RVA, rejecting addresses before the image base.
167    pub fn va_to_rva(va: Va, image_base: Va) -> Result<Rva> {
168        va.0.checked_sub(image_base.0)
169            .map(Rva)
170            .ok_or_else(|| Error::address("VA precedes image base"))
171    }
172
173    /// Convert an RVA to a thin-image-relative file offset.
174    pub fn rva_to_thin_offset(&self, rva: Rva, image_base: Va) -> Result<ThinFileOffset> {
175        let va = Self::rva_to_va(rva, image_base)?;
176        self.va_to_thin_offset(va)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    fn entry(file: u64, size: u64, va: u64) -> MappingEntry {
185        MappingEntry::try_new(ThinFileOffset(file), size, Va(va), size).unwrap()
186    }
187
188    fn test_map() -> AddressMap {
189        AddressMap::try_new(vec![
190            entry(0, 0x1000, 0x1_0000_0000),
191            entry(0x1000, 0x2000, 0x1_0000_1000),
192        ])
193        .unwrap()
194    }
195
196    #[test]
197    fn conversions_are_checked_and_round_trip() {
198        let map = test_map();
199        let va = map.thin_offset_to_va(ThinFileOffset(0x1500)).unwrap();
200        assert_eq!(va, Va(0x1_0000_1500));
201        assert_eq!(map.va_to_thin_offset(va).unwrap(), ThinFileOffset(0x1500));
202        assert_eq!(
203            AddressMap::va_to_rva(va, Va(0x1_0000_0000)).unwrap(),
204            Rva(0x1500)
205        );
206    }
207
208    #[test]
209    fn overlap_and_overflow_are_rejected() {
210        assert!(
211            AddressMap::try_new(vec![entry(0, 0x1000, 0x1000), entry(0x800, 0x1000, 0x3000)])
212                .is_err()
213        );
214        assert!(MappingEntry::try_new(ThinFileOffset(u64::MAX), 2, Va(0), 2).is_err());
215    }
216}