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 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 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 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 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 }
215 }
216 } else if path.starts_with(DWZ_DEBUG_INFO_PATH) {
217 contains_dwz = true;
218 }
219 }
220 }
221
222 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 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 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(§ion) {
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}