Skip to main content

microsandbox_utils/index/
reader.rs

1//! Sidecar index reader for lower layer acceleration.
2//!
3//! Provides zero-copy, mmap-based access to the binary layer index generated at
4//! OCI layer extraction time. See `layer-index-format.md` for the wire format.
5//!
6//! The index is an optimization — if missing or corrupt, the overlay falls back
7//! to live syscalls. `MmapIndex::open()` returns `None` on any validation failure.
8
9use std::path::Path;
10
11use super::{
12    DIR_FLAG_OPAQUE, DirRecord, ENTRY_FLAG_WHITEOUT, EntryRecord, HardlinkRef, INDEX_MAGIC,
13    INDEX_VERSION, IndexHeader,
14};
15
16//--------------------------------------------------------------------------------------------------
17// Types
18//--------------------------------------------------------------------------------------------------
19
20/// Zero-copy reader for a mmap'd layer sidecar index.
21///
22/// Immutable after construction. The mmap region is `PROT_READ | MAP_PRIVATE`,
23/// so this is `Send + Sync`.
24pub struct MmapIndex {
25    /// Pointer to the start of the mmap'd region.
26    ptr: *const u8,
27    /// Total length of the mmap'd region in bytes.
28    len: usize,
29    /// Byte offset of the string pool within the mmap'd region.
30    pool_offset: usize,
31}
32
33/// Iterator over tombstone names packed in the string pool.
34///
35/// Format: `[u16 len][name bytes]` repeated `count` times.
36pub struct TombstoneIter<'a> {
37    data: &'a [u8],
38    remaining: u16,
39}
40
41// SAFETY: The mmap'd region is read-only and never modified after construction.
42unsafe impl Send for MmapIndex {}
43unsafe impl Sync for MmapIndex {}
44
45//--------------------------------------------------------------------------------------------------
46// Methods
47//--------------------------------------------------------------------------------------------------
48
49impl MmapIndex {
50    /// Open and validate a sidecar index file.
51    ///
52    /// Returns `None` if the file doesn't exist, is too small, or fails any
53    /// validation check (magic, version, checksum, size consistency).
54    pub fn open(path: &Path) -> Option<Self> {
55        use scopeguard::ScopeGuard;
56
57        let file = std::fs::File::open(path).ok()?;
58        let metadata = file.metadata().ok()?;
59        let file_len = metadata.len() as usize;
60
61        // Must be at least as large as the header.
62        if file_len < size_of::<IndexHeader>() {
63            return None;
64        }
65
66        // mmap the file read-only.
67        use std::os::fd::AsRawFd;
68        let raw = unsafe {
69            libc::mmap(
70                std::ptr::null_mut(),
71                file_len,
72                libc::PROT_READ,
73                libc::MAP_PRIVATE,
74                file.as_raw_fd(),
75                0,
76            )
77        };
78        if raw == libc::MAP_FAILED {
79            return None;
80        }
81        let ptr = raw as *const u8;
82
83        // Auto-unmap on early return; defused on success.
84        let guard = scopeguard::guard((ptr, file_len), |(p, len)| unsafe {
85            libc::munmap(p as *mut libc::c_void, len);
86        });
87
88        // Validate header.
89        let header = unsafe { &*(ptr as *const IndexHeader) };
90        if header.magic != INDEX_MAGIC || header.version != INDEX_VERSION {
91            return None;
92        }
93
94        // Compute pool offset and verify total size matches file size.
95        let header_size = size_of::<IndexHeader>();
96        let pool_offset = header_size
97            + (header.dir_count as usize) * size_of::<DirRecord>()
98            + (header.entry_count as usize) * size_of::<EntryRecord>()
99            + (header.hardlink_ref_count as usize) * size_of::<HardlinkRef>();
100        let expected_size = pool_offset + header.string_pool_size as usize;
101        if expected_size != file_len {
102            return None;
103        }
104
105        // Verify CRC32C checksum incrementally without heap allocation.
106        // The checksum field (bytes 28..32) is treated as zeroed for computation.
107        let data = unsafe { std::slice::from_raw_parts(ptr, file_len) };
108        let crc = crc32c::crc32c(&data[..28]);
109        let crc = crc32c::crc32c_append(crc, &[0u8; 4]);
110        let crc = crc32c::crc32c_append(crc, &data[header_size..]);
111        if crc != header.checksum {
112            return None;
113        }
114
115        // Defuse the guard — ownership transfers to Self.
116        ScopeGuard::into_inner(guard);
117
118        Some(Self {
119            ptr,
120            len: file_len,
121            pool_offset,
122        })
123    }
124
125    /// Get the header.
126    fn header(&self) -> &IndexHeader {
127        unsafe { &*(self.ptr as *const IndexHeader) }
128    }
129
130    /// Get the directory records table (sorted by path).
131    pub fn dir_records(&self) -> &[DirRecord] {
132        let header = self.header();
133        let offset = size_of::<IndexHeader>();
134        let count = header.dir_count as usize;
135        unsafe { std::slice::from_raw_parts(self.ptr.add(offset) as *const DirRecord, count) }
136    }
137
138    /// Get the entry records table.
139    fn entry_records(&self) -> &[EntryRecord] {
140        let header = self.header();
141        let offset =
142            size_of::<IndexHeader>() + (header.dir_count as usize) * size_of::<DirRecord>();
143        let count = header.entry_count as usize;
144        unsafe { std::slice::from_raw_parts(self.ptr.add(offset) as *const EntryRecord, count) }
145    }
146
147    /// Get a string from the string pool by offset and length.
148    pub fn get_str(&self, off: u32, len: u16) -> &[u8] {
149        self.pool_slice(off as usize, len as usize)
150    }
151
152    /// Get a string from the string pool by u32 offset and u32 length.
153    fn get_str_u32(&self, off: u32, len: u32) -> &[u8] {
154        self.pool_slice(off as usize, len as usize)
155    }
156
157    /// Get a slice from the string pool by offset and length.
158    fn pool_slice(&self, off: usize, len: usize) -> &[u8] {
159        let start = self.pool_offset + off;
160        let end = start + len;
161        if end > self.len {
162            return b"";
163        }
164        unsafe { std::slice::from_raw_parts(self.ptr.add(start), len) }
165    }
166
167    /// Find a directory by path. Returns `(index, &DirRecord)`.
168    ///
169    /// Binary search on the sorted directory table.
170    pub fn find_dir(&self, path: &[u8]) -> Option<(usize, &DirRecord)> {
171        let dirs = self.dir_records();
172        let idx = dirs
173            .binary_search_by(|rec| self.get_str(rec.path_off, rec.path_len).cmp(path))
174            .ok()?;
175        Some((idx, &dirs[idx]))
176    }
177
178    /// Find an entry within a directory by name.
179    ///
180    /// Binary search on the entry slice for this directory.
181    pub fn find_entry<'a>(&'a self, dir: &DirRecord, name: &[u8]) -> Option<&'a EntryRecord> {
182        let entries = self.dir_entries(dir);
183        let idx = entries
184            .binary_search_by(|rec| self.get_str(rec.name_off, rec.name_len).cmp(name))
185            .ok()?;
186        Some(&entries[idx])
187    }
188
189    /// Get the contiguous entry slice for a directory.
190    pub fn dir_entries(&self, dir: &DirRecord) -> &[EntryRecord] {
191        let all = self.entry_records();
192        let start = dir.first_entry as usize;
193        let end = start + dir.entry_count as usize;
194        if end > all.len() {
195            return &[];
196        }
197        &all[start..end]
198    }
199
200    /// Check if a directory is opaque.
201    pub fn is_opaque(&self, dir: &DirRecord) -> bool {
202        dir.flags & DIR_FLAG_OPAQUE != 0
203    }
204
205    /// Check if a name is whited out in a directory.
206    pub fn has_whiteout(&self, dir: &DirRecord, name: &[u8]) -> bool {
207        if let Some(entry) = self.find_entry(dir, name) {
208            entry.flags & ENTRY_FLAG_WHITEOUT != 0
209        } else {
210            false
211        }
212    }
213
214    /// Iterate overflow tombstone names for a directory.
215    pub fn tombstone_names<'a>(&'a self, dir: &DirRecord) -> TombstoneIter<'a> {
216        if dir.tombstone_count == 0 {
217            return TombstoneIter {
218                data: &[],
219                remaining: 0,
220            };
221        }
222
223        // Tombstone data is in the string pool: packed [u16 len][name bytes]...
224        let start = self.pool_offset + dir.tombstone_off as usize;
225        // We don't know the exact end, but it's bounded by the file length.
226        let data = if start < self.len {
227            unsafe { std::slice::from_raw_parts(self.ptr.add(start), self.len - start) }
228        } else {
229            &[]
230        };
231
232        TombstoneIter {
233            data,
234            remaining: dir.tombstone_count,
235        }
236    }
237
238    /// Get the hardlink reference table.
239    pub fn hardlink_refs(&self) -> &[HardlinkRef] {
240        let header = self.header();
241        let count = header.hardlink_ref_count as usize;
242        let offset = self.pool_offset - count * size_of::<HardlinkRef>();
243        unsafe { std::slice::from_raw_parts(self.ptr.add(offset) as *const HardlinkRef, count) }
244    }
245
246    /// Find all aliases of a hardlinked file by host inode number.
247    pub fn find_aliases(&self, ino: u64) -> &[HardlinkRef] {
248        let refs = self.hardlink_refs();
249        let start = refs.partition_point(|r| r.host_ino < ino);
250        let end = start
251            + refs[start..]
252                .iter()
253                .take_while(|r| r.host_ino == ino)
254                .count();
255        &refs[start..end]
256    }
257
258    /// Get a hardlink ref's path string.
259    pub fn hardlink_path(&self, href: &HardlinkRef) -> &[u8] {
260        self.get_str_u32(href.path_off, href.path_len)
261    }
262}
263
264//--------------------------------------------------------------------------------------------------
265// Trait Implementations
266//--------------------------------------------------------------------------------------------------
267
268impl<'a> Iterator for TombstoneIter<'a> {
269    type Item = &'a [u8];
270
271    fn next(&mut self) -> Option<Self::Item> {
272        if self.remaining == 0 || self.data.len() < 2 {
273            return None;
274        }
275        let len = u16::from_le_bytes([self.data[0], self.data[1]]) as usize;
276        self.data = &self.data[2..];
277        if self.data.len() < len {
278            self.remaining = 0;
279            return None;
280        }
281        let name = &self.data[..len];
282        self.data = &self.data[len..];
283        self.remaining -= 1;
284        Some(name)
285    }
286}
287
288impl Drop for MmapIndex {
289    fn drop(&mut self) {
290        if !self.ptr.is_null() && self.len > 0 {
291            unsafe {
292                libc::munmap(self.ptr as *mut libc::c_void, self.len);
293            }
294        }
295    }
296}