Skip to main content

debuginfod_rs/
lib.rs

1use std::collections::HashMap;
2use std::io::{BufRead, BufReader, Read};
3use std::sync::mpsc::channel;
4use std::sync::Arc;
5
6use anyhow::{anyhow, Context};
7use bytesize::ByteSize;
8use bzip2::bufread::BzDecoder;
9use cpio::NewcReader;
10use elf::abi::SHT_NOBITS;
11use elf::endian::AnyEndian;
12use elf::ElfBytes;
13use flate2::read::GzDecoder;
14use itertools::Itertools;
15use log::{info, warn};
16use path_absolutize::*;
17use rayon::prelude::*;
18use rpm::CompressionType;
19use walkdir::WalkDir;
20use xz2::read::XzDecoder;
21
22extern crate log;
23
24pub const DEBUG_INFO_PATH: &str = "/usr/lib/debug";
25const DWZ_DEBUG_INFO_PATH: &str = "/usr/lib/debug/.dwz/";
26const DEBUG_INFO_BUILD_ID_PATH: &str = "/usr/lib/debug/.build-id/";
27const BUILD_ID_ELF_PREFIX: [u8; 8] = [0x03, 0x0, 0x0, 0x0, 0x47, 0x4e, 0x55, 0x0];
28const BUILD_CHARS: usize = 20;
29
30pub type BuildId = [u8; BUILD_CHARS];
31
32#[derive(Debug)]
33enum RPMKind {
34    Binary,
35    DebugInfo { build_ids: HashMap<BuildId, String> },
36    DebugSource,
37}
38
39#[derive(Debug)]
40struct RPMFile {
41    arch: String,
42    source_rpm: String,
43    name: String,
44
45    path: String,
46    kind: RPMKind,
47}
48
49#[derive(Debug)]
50pub struct DebugInfoRPM {
51    pub rpm_path: String,
52    pub binary_rpm_path: Option<String>,
53    pub source_rpm: Option<String>,
54
55    pub build_id_to_path: HashMap<BuildId, String>,
56}
57
58pub struct Server {
59    pub root_path: String,
60    pub debug_info_rpms: Vec<Arc<DebugInfoRPM>>,
61
62    pub build_ids: HashMap<BuildId, Arc<DebugInfoRPM>>,
63    pub total_byte_size: u64,
64}
65
66impl Server {
67    pub fn new(root_folder: &str) -> Self {
68        Server {
69            root_path: root_folder.to_string(),
70            debug_info_rpms: Vec::new(),
71            build_ids: HashMap::new(),
72            total_byte_size: 0,
73        }
74    }
75
76    pub fn walk(&mut self) {
77        let mut files = Vec::new();
78        for entry in WalkDir::new(self.root_path.clone()) {
79            let entry = entry.unwrap();
80            if entry.metadata().unwrap().is_file()
81                && entry.path().extension().is_some_and(|e| e == "rpm")
82            {
83                let path = entry.path().to_str();
84                match path {
85                    Some(path) => {
86                        files.push(path.to_string());
87                    }
88                    None => warn!("invalid RPM file path {entry:?}"),
89                }
90            }
91        }
92
93        self.total_byte_size = files
94            .iter()
95            .map(|f| std::fs::metadata(f).unwrap().len())
96            .sum();
97        info!(
98            "walking {} RPM files ({})",
99            files.len(),
100            ByteSize(self.total_byte_size)
101        );
102
103        let (rx, tx) = channel();
104
105        files.par_iter().for_each_with(rx, |rx, path| {
106            let _ = rx.send(self.analyze_file(path));
107        });
108
109        let mut rpms = Vec::new();
110
111        for item in tx.iter() {
112            match item {
113                Ok(rpm_file) => rpms.push(rpm_file),
114                Err(error) => warn!("could not analyze RPM: {error}"),
115            }
116        }
117
118        // First iterate the source RPM filies and create a map we can later use for construction
119        // of the DebugInfoRPM entires.
120        let mut source_rpm_map = HashMap::new();
121        for rpm in &rpms {
122            if let RPMKind::DebugSource = rpm.kind {
123                source_rpm_map.insert((&rpm.arch, &rpm.source_rpm), rpm);
124            }
125        }
126
127        // Second iterate the binary RPM files and also create a map. We need to include canonical
128        // package name in the map.
129        let mut binary_rpm_map = HashMap::new();
130        for rpm in &rpms {
131            if let RPMKind::Binary = rpm.kind {
132                binary_rpm_map.insert((&rpm.arch, &rpm.source_rpm, &rpm.name), rpm);
133            }
134        }
135
136        // Now we can construct DebugInfoRPM entries and find the corresponding Binary and DebugSource packages.
137        for rpm in &rpms {
138            if let RPMKind::DebugInfo { build_ids } = &rpm.kind {
139                let debug_info = Arc::new(DebugInfoRPM {
140                    rpm_path: rpm.path.clone(),
141                    binary_rpm_path: binary_rpm_map
142                        .get(&(&rpm.arch, &rpm.source_rpm, &rpm.name))
143                        .map(|r| r.path.clone()),
144                    source_rpm: source_rpm_map
145                        .get(&(&rpm.arch, &rpm.source_rpm))
146                        .map(|r| r.path.clone()),
147                    build_id_to_path: build_ids.clone(),
148                });
149
150                self.debug_info_rpms.push(debug_info.clone());
151                // Construct the Server state build-id mapping to DebugInfoRPM entries.
152                for build_id in debug_info.build_id_to_path.keys() {
153                    self.build_ids.insert(*build_id, debug_info.clone());
154                }
155            }
156        }
157    }
158
159    fn analyze_file(&self, rpm_path: &str) -> anyhow::Result<RPMFile> {
160        let rpm_file = std::fs::File::open(rpm_path)?;
161        let mut buf_reader = std::io::BufReader::new(rpm_file);
162        let header = rpm::PackageMetadata::parse(&mut buf_reader)?;
163
164        let name = header.get_name()?;
165        let is_debug_info_rpm = name.ends_with("-debuginfo");
166        let canonical_name = name.strip_suffix("-debuginfo").unwrap_or(name).to_string();
167
168        let source_rpm = header.get_source_rpm()?.to_string();
169        let arch = header.get_arch()?.to_string();
170        let rpm_path = rpm_path.to_string();
171
172        let mut build_ids = HashMap::new();
173
174        let mut contains_dwz = false;
175        for file_entry in header.get_file_entries()? {
176            let path = file_entry.path;
177            if is_debug_info_rpm {
178                if path.starts_with(DEBUG_INFO_BUILD_ID_PATH)
179                    && path.extension().is_some_and(|e| e == "debug")
180                {
181                    let mut build_id = path
182                        .parent()
183                        .context("parent must exist")?
184                        .file_name()
185                        .context("direct name must exist")?
186                        .to_str()
187                        .context("filename should be valid")?
188                        .to_string();
189                    build_id.push_str(
190                        path.file_stem()
191                            .context("file stem expected")?
192                            .to_str()
193                            .context("valid path expected")?,
194                    );
195                    let build_id = self.parse_build_id(&build_id);
196                    match build_id {
197                        Ok(build_id) => {
198                            let target = path
199                                .parent()
200                                .context("filename must have a parent")?
201                                .join(file_entry.linkto.clone());
202                            build_ids.insert(
203                                build_id,
204                                target
205                                    .as_path()
206                                    .absolutize()?
207                                    .to_str()
208                                    .context("symlink target path must be valid")?
209                                    .to_string(),
210                            );
211                        }
212                        Err(_error) => {
213                            // warn!("{rpm_path} {path:?} {_error}");
214                        }
215                    }
216                } else if path.starts_with(DWZ_DEBUG_INFO_PATH) {
217                    contains_dwz = true;
218                }
219            }
220        }
221
222        // Right now, there is a missing symlink from a build-id to the .dwz files in the RPM container and
223        // so we need to parse it in the ELF file.
224        if contains_dwz {
225            if let Some((build_id, path)) = self.get_build_id_for_dwz(&rpm_path) {
226                build_ids.insert(build_id, path);
227            }
228        }
229
230        let kind = if is_debug_info_rpm {
231            RPMKind::DebugInfo { build_ids }
232        } else if name.ends_with("-debugsource") {
233            RPMKind::DebugSource
234        } else {
235            RPMKind::Binary
236        };
237        Ok(RPMFile {
238            arch,
239            source_rpm,
240            name: canonical_name,
241            path: rpm_path,
242            kind,
243        })
244    }
245
246    fn get_rpm_file_stream(
247        &self,
248        path: &str,
249        file_selector: impl Fn(&str) -> bool,
250    ) -> anyhow::Result<(NewcReader<impl Read>, String)> {
251        let rpm_file = std::fs::File::open(path).context("cannot open RPM file")?;
252
253        let mut buf_reader = std::io::BufReader::new(rpm_file);
254        let header = rpm::PackageMetadata::parse(&mut buf_reader)?;
255        let compressor = header.get_payload_compressor();
256        let mut decoder: Box<dyn BufRead> = match compressor? {
257            CompressionType::Zstd => Box::new(BufReader::new(
258                zstd::stream::Decoder::new(buf_reader).context("ZSTD decoded failed")?,
259            )),
260            CompressionType::Gzip => Box::new(BufReader::new(GzDecoder::new(buf_reader))),
261            CompressionType::Bzip2 => Box::new(BufReader::new(BzDecoder::new(buf_reader))),
262            CompressionType::Xz => Box::new(BufReader::new(XzDecoder::new(buf_reader))),
263            CompressionType::None => Box::new(buf_reader),
264        };
265
266        loop {
267            let archive = NewcReader::new(decoder).context("CPIO decoder failed")?;
268            let entry = archive.entry();
269            if entry.is_trailer() {
270                break;
271            }
272            let mut name = entry.name().to_string();
273            if name.starts_with('.') {
274                name = String::from_iter(name.chars().skip(1));
275            }
276            let file_size = entry.file_size() as usize;
277
278            if file_selector(&name) && file_size > 0 {
279                return Ok((archive, name.clone()));
280            } else {
281                decoder = archive.finish().unwrap();
282            }
283        }
284
285        Err(anyhow!("file not found in the archive"))
286    }
287
288    fn get_build_id_for_dwz(&self, file: &str) -> Option<(BuildId, String)> {
289        // For now, let's parse '.note.gnu.build-id' section without any ELF library.
290        // Luckily, the created .dwz files (e.g. /usr/lib/debug/.dwz/foo.x86_64) have only a limited
291        // number of ELF sections and the note is section is at the very beginning.
292        //
293        // See SHT_NOTE for a more detail specification. Our note contains "GNU\0" followed by the Build-Id.
294
295        if let Ok((mut stream, name)) =
296            self.get_rpm_file_stream(file, |name| name.starts_with(DWZ_DEBUG_INFO_PATH))
297        {
298            let mut data = vec![0; 256];
299            let _ = stream.read_exact(&mut data);
300            let mut heystack = data.as_slice();
301            for _ in 0..(data.len() - BUILD_ID_ELF_PREFIX.len() - BUILD_CHARS) {
302                if heystack.starts_with(&BUILD_ID_ELF_PREFIX) {
303                    let build_id = heystack
304                        .iter()
305                        .skip(BUILD_ID_ELF_PREFIX.len())
306                        .take(BUILD_CHARS)
307                        .copied()
308                        .collect_vec();
309                    let build_id = BuildId::try_from(build_id);
310                    if let Ok(build_id) = build_id {
311                        return Some((build_id, name));
312                    } else {
313                        break;
314                    }
315                } else {
316                    // Shift the heystack by one byte and continue
317                    heystack = &heystack[1..];
318                }
319            }
320        }
321
322        None
323    }
324
325    pub fn get_binary_rpm_for_build_id(&self, build_id: &BuildId) -> Option<(String, String)> {
326        if let Some(debug_info_rpm) = self.build_ids.get(build_id) {
327            if let Some(filename) = debug_info_rpm.build_id_to_path.get(build_id) {
328                let filename = filename
329                    .strip_suffix(".debug")
330                    .unwrap()
331                    .strip_prefix(DEBUG_INFO_PATH)
332                    .unwrap()
333                    .to_string();
334                if let Some(binary_rpm_path) = &debug_info_rpm.binary_rpm_path {
335                    return Some((binary_rpm_path.clone(), filename));
336                }
337            }
338        }
339
340        None
341    }
342
343    pub fn read_rpm_file(&self, rpm_file: &str, file: &str) -> Option<Vec<u8>> {
344        info!("reading RPM file {rpm_file}");
345        if let Ok((mut stream, _)) = self.get_rpm_file_stream(rpm_file, |f| f == file) {
346            info!("found RPM file: {file}");
347            let mut content = Vec::new();
348            let _ = stream.read_to_end(&mut content);
349            Some(content)
350        } else {
351            None
352        }
353    }
354
355    pub fn read_rpm_file_section(
356        &self,
357        rpm_file: &str,
358        file: &str,
359        section: &str,
360    ) -> Option<Vec<u8>> {
361        if let Some(data) = self.read_rpm_file(rpm_file, file) {
362            if let Ok(elf_file) = ElfBytes::<AnyEndian>::minimal_parse(data.as_slice()) {
363                if let Ok(section) = elf_file.section_header_by_name(section) {
364                    let section = section?;
365                    if section.sh_type == SHT_NOBITS {
366                        return None;
367                    }
368
369                    if let Ok(section_data) = elf_file.section_data(&section) {
370                        let mut result = Vec::new();
371                        section_data.0.clone_into(&mut result);
372                        return Some(result);
373                    }
374                }
375            }
376        }
377        None
378    }
379
380    pub fn parse_build_id(&self, id: &str) -> anyhow::Result<BuildId> {
381        let array = hex::decode(id)?;
382        if array.len() != BUILD_CHARS {
383            Err(anyhow!(
384                "Invalid build-id length: {}, expected {BUILD_CHARS}",
385                array.len()
386            ))
387        } else {
388            Ok(BuildId::try_from(array.as_slice())?)
389        }
390    }
391}