composefs/erofs/
debug.rs

1//! Debug utilities for analyzing EROFS images.
2//!
3//! This module provides tools for inspecting and debugging EROFS filesystem
4//! images, including detailed structure dumping and space usage analysis.
5
6use std::{
7    cmp::Ordering,
8    collections::BTreeMap,
9    ffi::OsStr,
10    fmt,
11    mem::discriminant,
12    os::unix::ffi::OsStrExt,
13    path::{Path, PathBuf},
14};
15
16use anyhow::Result;
17use zerocopy::FromBytes;
18
19use super::{
20    format::{self, CompactInodeHeader, ComposefsHeader, ExtendedInodeHeader, Superblock},
21    reader::{DataBlock, DirectoryBlock, Image, Inode, InodeHeader, InodeOps, InodeType, XAttr},
22};
23
24/// Converts any reference to a thin pointer (as usize)
25/// Used for address calculations in various outputs
26macro_rules! addr {
27    ($ref: expr) => {
28        &raw const (*$ref) as *const u8 as usize
29    };
30}
31
32macro_rules! write_with_offset {
33    ($fmt: expr, $base: expr, $label: expr, $ref: expr) => {{
34        let offset = addr!($ref) - addr!($base);
35        writeln!($fmt, "{offset:+8x}     {}: {:?}", $label, $ref)
36    }};
37}
38
39macro_rules! write_fields {
40    ($fmt: expr, $base: expr, $struct: expr, $field: ident) => {{
41        let value = &$struct.$field;
42        let default = if false { value } else { &Default::default() };
43        if value != default {
44            write_with_offset!($fmt, $base, stringify!($field), value)?;
45        }
46    }};
47    ($fmt: expr, $base: expr, $struct: expr, $head: ident; $($tail: ident);+) => {{
48        write_fields!($fmt, $base, $struct, $head);
49        write_fields!($fmt, $base, $struct, $($tail);+);
50    }};
51}
52
53fn utf8_or_hex(data: &[u8]) -> String {
54    if let Ok(string) = std::str::from_utf8(data) {
55        format!("{string:?}")
56    } else {
57        hex::encode(data)
58    }
59}
60
61// This is basically just a fancy fat pointer type
62enum SegmentType<'img> {
63    Header(&'img ComposefsHeader),
64    Superblock(&'img Superblock),
65    CompactInode(&'img Inode<CompactInodeHeader>),
66    ExtendedInode(&'img Inode<ExtendedInodeHeader>),
67    XAttr(&'img XAttr),
68    DataBlock(&'img DataBlock),
69    DirectoryBlock(&'img DirectoryBlock),
70}
71
72// TODO: Something for `enum_dispatch` would be good here, but I couldn't get it working...
73impl SegmentType<'_> {
74    fn addr(&self) -> usize {
75        match self {
76            SegmentType::Header(h) => addr!(*h),
77            SegmentType::Superblock(sb) => addr!(*sb),
78            SegmentType::CompactInode(i) => addr!(*i),
79            SegmentType::ExtendedInode(i) => addr!(*i),
80            SegmentType::XAttr(x) => addr!(*x),
81            SegmentType::DataBlock(b) => addr!(*b),
82            SegmentType::DirectoryBlock(b) => addr!(*b),
83        }
84    }
85
86    fn size(&self) -> usize {
87        match self {
88            SegmentType::Header(h) => size_of_val(*h),
89            SegmentType::Superblock(sb) => size_of_val(*sb),
90            SegmentType::CompactInode(i) => size_of_val(*i),
91            SegmentType::ExtendedInode(i) => size_of_val(*i),
92            SegmentType::XAttr(x) => size_of_val(*x),
93            SegmentType::DataBlock(b) => size_of_val(*b),
94            SegmentType::DirectoryBlock(b) => size_of_val(*b),
95        }
96    }
97
98    fn typename(&self) -> &'static str {
99        match self {
100            SegmentType::Header(..) => "header",
101            SegmentType::Superblock(..) => "superblock",
102            SegmentType::CompactInode(..) => "compact inode",
103            SegmentType::ExtendedInode(..) => "extended inode",
104            SegmentType::XAttr(..) => "shared xattr",
105            SegmentType::DataBlock(..) => "data block",
106            SegmentType::DirectoryBlock(..) => "directory block",
107        }
108    }
109}
110
111struct ImageVisitor<'img> {
112    image: &'img Image<'img>,
113    visited: BTreeMap<usize, (SegmentType<'img>, Vec<Box<Path>>)>,
114}
115
116impl<'img> ImageVisitor<'img> {
117    fn note(&mut self, segment: SegmentType<'img>, path: Option<&Path>) -> bool {
118        let offset = segment.addr() - self.image.image.as_ptr() as usize;
119        match self.visited.entry(offset) {
120            std::collections::btree_map::Entry::Occupied(mut e) => {
121                let (existing, paths) = e.get_mut();
122                // TODO: figure out pointer value equality...
123                assert_eq!(discriminant(existing), discriminant(&segment));
124                assert_eq!(existing.addr(), segment.addr());
125                assert_eq!(existing.size(), segment.size());
126                if let Some(path) = path {
127                    paths.push(Box::from(path));
128                }
129                true
130            }
131            std::collections::btree_map::Entry::Vacant(e) => {
132                let mut paths = vec![];
133                if let Some(path) = path {
134                    paths.push(Box::from(path));
135                }
136                e.insert((segment, paths));
137                false
138            }
139        }
140    }
141
142    fn visit_directory_block(&mut self, block: &DirectoryBlock, path: &Path) {
143        for entry in block.entries() {
144            if entry.name == b"." || entry.name == b".." {
145                // TODO: maybe we want to follow those and let deduplication happen
146                continue;
147            }
148            self.visit_inode(
149                entry.header.inode_offset.get(),
150                &path.join(OsStr::from_bytes(entry.name)),
151            );
152        }
153    }
154
155    fn visit_inode(&mut self, id: u64, path: &Path) {
156        let inode = self.image.inode(id);
157        let segment = match inode {
158            InodeType::Compact(inode) => SegmentType::CompactInode(inode),
159            InodeType::Extended(inode) => SegmentType::ExtendedInode(inode),
160        };
161        if self.note(segment, Some(path)) {
162            // TODO: maybe we want to throw an error if we detect loops
163            /* already processed */
164            return;
165        }
166
167        if let Some(xattrs) = inode.xattrs() {
168            for id in xattrs.shared() {
169                self.note(
170                    SegmentType::XAttr(self.image.shared_xattr(id.get())),
171                    Some(path),
172                );
173            }
174        }
175
176        if inode.mode().is_dir() {
177            if let Some(inline) = inode.inline() {
178                let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
179                self.visit_directory_block(inline_block, path);
180            }
181
182            for id in inode.blocks(self.image.blkszbits) {
183                let block = self.image.directory_block(id);
184                self.visit_directory_block(block, path);
185                self.note(SegmentType::DirectoryBlock(block), Some(path));
186            }
187        } else {
188            for id in inode.blocks(self.image.blkszbits) {
189                let block = self.image.data_block(id);
190                self.note(SegmentType::DataBlock(block), Some(path));
191            }
192        }
193    }
194
195    fn visit_image(
196        image: &'img Image<'img>,
197    ) -> BTreeMap<usize, (SegmentType<'img>, Vec<Box<Path>>)> {
198        let mut this = Self {
199            image,
200            visited: BTreeMap::new(),
201        };
202        this.note(SegmentType::Header(image.header), None);
203        this.note(SegmentType::Superblock(image.sb), None);
204        this.visit_inode(image.sb.root_nid.get() as u64, &PathBuf::from("/"));
205        this.visited
206    }
207}
208
209impl fmt::Debug for XAttr {
210    // Injective (ie: accounts for every byte in the input)
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(
213            f,
214            "({} {} {}) {}{} = {}",
215            self.header.name_index,
216            self.header.name_len,
217            self.header.value_size,
218            std::str::from_utf8(format::XATTR_PREFIXES[self.header.name_index as usize]).unwrap(),
219            utf8_or_hex(self.suffix()),
220            utf8_or_hex(self.value()),
221        )?;
222        if self.padding().iter().any(|c| *c != 0) {
223            write!(f, " {:?}", self.padding())?;
224        }
225        Ok(())
226    }
227}
228
229impl fmt::Debug for CompactInodeHeader {
230    // Injective (ie: accounts for every byte in the input)
231    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
232        writeln!(f, "CompactInodeHeader")?;
233        write_fields!(f, self, self,
234            format; xattr_icount; mode; reserved; size; u; ino; uid; gid; nlink; reserved2);
235        Ok(())
236    }
237}
238
239impl fmt::Debug for ExtendedInodeHeader {
240    // Injective (ie: accounts for every byte in the input)
241    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
242        writeln!(f, "ExtendedInodeHeader")?;
243        write_fields!(f, self, self,
244            format; xattr_icount; mode; reserved; size; u; ino; uid;
245            gid; mtime; mtime_nsec; nlink; reserved2);
246        Ok(())
247    }
248}
249
250fn hexdump(f: &mut impl fmt::Write, data: &[u8], rel: usize) -> fmt::Result {
251    let start = match rel {
252        0 => 0,
253        ptr => data.as_ptr() as usize - ptr,
254    };
255    let end = start + data.len();
256    let start_row = start / 16;
257    let end_row = end.div_ceil(16);
258
259    for row in start_row..end_row {
260        let row_start = row * 16;
261        let row_end = row * 16 + 16;
262        write!(f, "{row_start:+8x}  ")?;
263
264        for idx in row_start..row_end {
265            if start <= idx && idx < end {
266                write!(f, "{:02x} ", data[idx - start])?;
267            } else {
268                write!(f, "   ")?;
269            }
270            if idx % 8 == 7 {
271                write!(f, " ")?;
272            }
273        }
274        write!(f, "|")?;
275
276        for idx in row_start..row_end {
277            if start <= idx && idx < end {
278                let c = data[idx - start];
279                if c.is_ascii() && !c.is_ascii_control() {
280                    write!(f, "{}", c as char)?;
281                } else {
282                    write!(f, ".")?;
283                }
284            } else {
285                write!(f, " ")?;
286            }
287        }
288        writeln!(f, "|")?;
289    }
290
291    Ok(())
292}
293
294impl<T: fmt::Debug + InodeHeader> fmt::Debug for Inode<T> {
295    // Injective (ie: accounts for every byte in the input)
296    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
297        fmt::Debug::fmt(&self.header, f)?;
298
299        if let Some(xattrs) = self.xattrs() {
300            write_fields!(f, self, xattrs.header, name_filter; shared_count; reserved);
301
302            if !xattrs.shared().is_empty() {
303                write_with_offset!(f, self, "shared xattrs", xattrs.shared())?;
304            }
305
306            for xattr in xattrs.local() {
307                write_with_offset!(f, self, "xattr", xattr)?;
308            }
309        }
310
311        // We want to print one of four things for inline data:
312        //   - no data: print nothing
313        //   - directory data: dump the entries
314        //   - small inline text string: print it
315        //   - otherwise, hexdump
316        let Some(inline) = self.inline() else {
317            // No inline data
318            return Ok(());
319        };
320
321        // Directory dump
322        if self.header.mode().is_dir() {
323            let dir = DirectoryBlock::ref_from_bytes(inline).unwrap();
324            let offset = addr!(dir) - addr!(self);
325            return write!(
326                f,
327                "     +{offset:02x} --- inline directory entries ---{dir:#?}"
328            );
329        }
330
331        // Small string (<= 128 bytes, utf8, no control characters).
332        if inline.len() <= 128 && !inline.iter().any(|c| c.is_ascii_control()) {
333            if let Ok(string) = std::str::from_utf8(inline) {
334                return write_with_offset!(f, self, "inline", string);
335            }
336        }
337
338        // Else, hexdump data block
339        hexdump(f, inline, &raw const self.header as usize)
340    }
341}
342
343impl fmt::Debug for DirectoryBlock {
344    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
345        for entry in self.entries() {
346            writeln!(f)?;
347            write_fields!(f, self, entry.header, inode_offset; name_offset; file_type; reserved);
348            writeln!(
349                f,
350                "{:+8x}     # name: {}",
351                entry.header.name_offset.get(),
352                utf8_or_hex(entry.name)
353            )?;
354        }
355        // TODO: trailing junk inside of st_size
356        // TODO: padding up to block or inode boundary
357        Ok(())
358    }
359}
360
361impl fmt::Debug for DataBlock {
362    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
363        hexdump(f, &self.0, 0)
364    }
365}
366
367impl fmt::Debug for ComposefsHeader {
368    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
369        writeln!(f, "ComposefsHeader")?;
370        write_fields!(f, self, self,
371            magic; flags; version; composefs_version; unused
372        );
373        Ok(())
374    }
375}
376
377impl fmt::Debug for Superblock {
378    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
379        writeln!(f, "Superblock")?;
380        write_fields!(f, self, self,
381            magic; checksum; feature_compat; blkszbits; extslots; root_nid; inos; build_time;
382            build_time_nsec; blocks; meta_blkaddr; xattr_blkaddr; uuid; volume_name;
383            feature_incompat; available_compr_algs; extra_devices; devt_slotoff; dirblkbits;
384            xattr_prefix_count; xattr_prefix_start; packed_nid; xattr_filter_reserved; reserved2
385        );
386        Ok(())
387    }
388}
389
390fn addto<T: Clone + Eq + Ord>(map: &mut BTreeMap<T, usize>, key: &T, count: usize) {
391    if let Some(value) = map.get_mut(key) {
392        *value += count;
393    } else {
394        map.insert(key.clone(), count);
395    }
396}
397
398/// Dumps unassigned or padding regions in the image
399///
400/// Distinguishes between zero-filled padding and unknown content.
401pub fn dump_unassigned(
402    output: &mut impl std::io::Write,
403    offset: usize,
404    unassigned: &[u8],
405) -> Result<()> {
406    if unassigned.iter().all(|c| *c == 0) {
407        writeln!(output, "{offset:08x} Padding")?;
408        writeln!(
409            output,
410            "{:+8x}     # {} nul bytes",
411            unassigned.len(),
412            unassigned.len()
413        )?;
414        writeln!(output)?;
415    } else {
416        writeln!(output, "{offset:08x} Unknown content")?;
417        let mut dump = String::new();
418        hexdump(&mut dump, unassigned, 0)?;
419        writeln!(output, "{dump}")?;
420    }
421    Ok(())
422}
423
424/// Dumps a detailed debug view of an EROFS image
425///
426/// Walks the entire image structure, outputting formatted information about
427/// all inodes, blocks, xattrs, and padding. Also produces space usage statistics.
428pub fn debug_img(output: &mut impl std::io::Write, data: &[u8]) -> Result<()> {
429    let image = Image::open(data);
430    let visited = ImageVisitor::visit_image(&image);
431
432    let inode_start = (image.sb.meta_blkaddr.get() as usize) << image.sb.blkszbits;
433    let xattr_start = (image.sb.xattr_blkaddr.get() as usize) << image.sb.blkszbits;
434
435    let mut space_stats = BTreeMap::new();
436    let mut padding_stats = BTreeMap::new();
437
438    let mut last_segment_type = "";
439    let mut offset = 0;
440    for (start, (segment, paths)) in visited {
441        let segment_type = segment.typename();
442        addto(&mut space_stats, &segment_type, segment.size());
443
444        match offset.cmp(&start) {
445            Ordering::Less => {
446                dump_unassigned(output, offset, &data[offset..start])?;
447                addto(
448                    &mut padding_stats,
449                    &(last_segment_type, segment_type),
450                    start - offset,
451                );
452                offset = start;
453            }
454            Ordering::Greater => {
455                writeln!(output, "*** Overlapping segments!")?;
456                writeln!(output)?;
457                offset = start;
458            }
459            _ => {}
460        }
461
462        last_segment_type = segment_type;
463
464        for path in paths {
465            writeln!(
466                output,
467                "# Filename {}",
468                utf8_or_hex(path.as_os_str().as_bytes())
469            )?;
470        }
471
472        match segment {
473            SegmentType::Header(header) => {
474                writeln!(output, "{offset:08x} {header:?}")?;
475            }
476            SegmentType::Superblock(sb) => {
477                writeln!(output, "{offset:08x} {sb:?}")?;
478            }
479            SegmentType::CompactInode(inode) => {
480                writeln!(output, "# nid #{}", (offset - inode_start) / 32)?;
481                writeln!(output, "{offset:08x} {inode:#?}")?;
482            }
483            SegmentType::ExtendedInode(inode) => {
484                writeln!(output, "# nid #{}", (offset - inode_start) / 32)?;
485                writeln!(output, "{offset:08x} {inode:#?}")?;
486            }
487            SegmentType::XAttr(xattr) => {
488                writeln!(output, "# xattr #{}", (offset - xattr_start) / 4)?;
489                writeln!(output, "{offset:08x} {xattr:?}")?;
490            }
491            SegmentType::DirectoryBlock(block) => {
492                writeln!(output, "# block #{}", offset / image.block_size)?;
493                writeln!(output, "{offset:08x} Directory block{block:?}")?;
494            }
495            SegmentType::DataBlock(block) => {
496                writeln!(output, "# block #{}", offset / image.block_size)?;
497                writeln!(output, "{offset:08x} Data block\n{block:?}")?;
498            }
499        }
500
501        offset += segment.size();
502    }
503
504    if offset < data.len() {
505        let unassigned = &data[offset..];
506        dump_unassigned(output, offset, unassigned)?;
507        addto(
508            &mut padding_stats,
509            &(last_segment_type, "eof"),
510            unassigned.len(),
511        );
512        offset = data.len();
513        writeln!(output)?;
514    }
515
516    if offset > data.len() {
517        writeln!(output, "*** Segments past EOF!")?;
518        offset = data.len();
519    }
520
521    writeln!(output, "Space statistics (total size {offset}B):")?;
522    for (key, value) in space_stats {
523        writeln!(
524            output,
525            "  {key} = {value}B, {:.2}%",
526            (100. * value as f64) / (offset as f64)
527        )?;
528    }
529    for ((from, to), value) in padding_stats {
530        writeln!(
531            output,
532            "  padding {from} -> {to} = {value}B, {:.2}%",
533            (100. * value as f64) / (offset as f64)
534        )?;
535    }
536
537    Ok(())
538}