1//===-- llvm/Debuginfod/Debuginfod.cpp - Debuginfod client library --------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8///
9/// \file
10///
11/// This file contains several definitions for the debuginfod client and server.
12/// For the client, this file defines the fetchInfo function. For the server,
13/// this file defines the DebuginfodLogEntry and DebuginfodServer structs, as
14/// well as the DebuginfodLog, DebuginfodCollection classes. The fetchInfo
15/// function retrieves any of the three supported artifact types: (executable,
16/// debuginfo, source file) associated with a build-id from debuginfod servers.
17/// If a source file is to be fetched, its absolute path must be specified in
18/// the Description argument to fetchInfo. The DebuginfodLogEntry,
19/// DebuginfodLog, and DebuginfodCollection are used by the DebuginfodServer to
20/// scan the local filesystem for binaries and serve the debuginfod protocol.
21///
22//===----------------------------------------------------------------------===//
23
24#include "llvm/Debuginfod/Debuginfod.h"
25#include "llvm/ADT/StringExtras.h"
26#include "llvm/ADT/StringRef.h"
27#include "llvm/BinaryFormat/Magic.h"
28#include "llvm/DebugInfo/DWARF/DWARFContext.h"
29#include "llvm/DebugInfo/Symbolize/Symbolize.h"
30#include "llvm/Debuginfod/HTTPClient.h"
31#include "llvm/Object/BuildID.h"
32#include "llvm/Object/ELFObjectFile.h"
33#include "llvm/Support/CachePruning.h"
34#include "llvm/Support/Caching.h"
35#include "llvm/Support/Errc.h"
36#include "llvm/Support/Error.h"
37#include "llvm/Support/FileUtilities.h"
38#include "llvm/Support/MemoryBuffer.h"
39#include "llvm/Support/Path.h"
40#include "llvm/Support/ThreadPool.h"
41#include "llvm/Support/xxhash.h"
42
43#include <atomic>
44#include <optional>
45#include <thread>
46
47namespace llvm {
48
49using llvm::object::BuildIDRef;
50
51namespace {
52std::optional<SmallVector<StringRef>> DebuginfodUrls;
53// Many Readers/Single Writer lock protecting the global debuginfod URL list.
54llvm::sys::RWMutex UrlsMutex;
55} // namespace
56
57std::string getDebuginfodCacheKey(llvm::StringRef S) {
58 return utostr(X: xxh3_64bits(data: S));
59}
60
61// Returns a binary BuildID as a normalized hex string.
62// Uses lowercase for compatibility with common debuginfod servers.
63static std::string buildIDToString(BuildIDRef ID) {
64 return llvm::toHex(Input: ID, /*LowerCase=*/true);
65}
66
67bool canUseDebuginfod() {
68 return HTTPClient::isAvailable() && !getDefaultDebuginfodUrls().empty();
69}
70
71SmallVector<StringRef> getDefaultDebuginfodUrls() {
72 std::shared_lock<llvm::sys::RWMutex> ReadGuard(UrlsMutex);
73 if (!DebuginfodUrls) {
74 // Only read from the environment variable if the user hasn't already
75 // set the value.
76 ReadGuard.unlock();
77 std::unique_lock<llvm::sys::RWMutex> WriteGuard(UrlsMutex);
78 DebuginfodUrls = SmallVector<StringRef>();
79 if (const char *DebuginfodUrlsEnv = std::getenv(name: "DEBUGINFOD_URLS")) {
80 StringRef(DebuginfodUrlsEnv)
81 .split(A&: DebuginfodUrls.value(), Separator: " ", MaxSplit: -1, KeepEmpty: false);
82 }
83 WriteGuard.unlock();
84 ReadGuard.lock();
85 }
86 return DebuginfodUrls.value();
87}
88
89// Set the default debuginfod URL list, override the environment variable.
90void setDefaultDebuginfodUrls(const SmallVector<StringRef> &URLs) {
91 std::unique_lock<llvm::sys::RWMutex> WriteGuard(UrlsMutex);
92 DebuginfodUrls = URLs;
93}
94
95/// Finds a default local file caching directory for the debuginfod client,
96/// first checking DEBUGINFOD_CACHE_PATH.
97Expected<std::string> getDefaultDebuginfodCacheDirectory() {
98 if (const char *CacheDirectoryEnv = std::getenv(name: "DEBUGINFOD_CACHE_PATH"))
99 return CacheDirectoryEnv;
100
101 SmallString<64> CacheDirectory;
102 if (!sys::path::cache_directory(result&: CacheDirectory))
103 return createStringError(
104 EC: errc::io_error, S: "Unable to determine appropriate cache directory.");
105 sys::path::append(path&: CacheDirectory, a: "llvm-debuginfod", b: "client");
106 return std::string(CacheDirectory);
107}
108
109std::chrono::milliseconds getDefaultDebuginfodTimeout() {
110 long Timeout;
111 const char *DebuginfodTimeoutEnv = std::getenv(name: "DEBUGINFOD_TIMEOUT");
112 if (DebuginfodTimeoutEnv &&
113 to_integer(S: StringRef(DebuginfodTimeoutEnv).trim(), Num&: Timeout, Base: 10))
114 return std::chrono::milliseconds(Timeout * 1000);
115
116 return std::chrono::milliseconds(90 * 1000);
117}
118
119/// The following functions fetch a debuginfod artifact to a file in a local
120/// cache and return the cached file path. They first search the local cache,
121/// followed by the debuginfod servers.
122
123std::string getDebuginfodSourceUrlPath(BuildIDRef ID,
124 StringRef SourceFilePath) {
125 SmallString<64> UrlPath;
126 sys::path::append(path&: UrlPath, style: sys::path::Style::posix, a: "buildid",
127 b: buildIDToString(ID), c: "source",
128 d: sys::path::convert_to_slash(path: SourceFilePath));
129 return std::string(UrlPath);
130}
131
132Expected<std::string> getCachedOrDownloadSource(BuildIDRef ID,
133 StringRef SourceFilePath) {
134 std::string UrlPath = getDebuginfodSourceUrlPath(ID, SourceFilePath);
135 return getCachedOrDownloadArtifact(UniqueKey: getDebuginfodCacheKey(S: UrlPath), UrlPath);
136}
137
138std::string getDebuginfodExecutableUrlPath(BuildIDRef ID) {
139 SmallString<64> UrlPath;
140 sys::path::append(path&: UrlPath, style: sys::path::Style::posix, a: "buildid",
141 b: buildIDToString(ID), c: "executable");
142 return std::string(UrlPath);
143}
144
145Expected<std::string> getCachedOrDownloadExecutable(BuildIDRef ID) {
146 std::string UrlPath = getDebuginfodExecutableUrlPath(ID);
147 return getCachedOrDownloadArtifact(UniqueKey: getDebuginfodCacheKey(S: UrlPath), UrlPath);
148}
149
150std::string getDebuginfodDebuginfoUrlPath(BuildIDRef ID) {
151 SmallString<64> UrlPath;
152 sys::path::append(path&: UrlPath, style: sys::path::Style::posix, a: "buildid",
153 b: buildIDToString(ID), c: "debuginfo");
154 return std::string(UrlPath);
155}
156
157Expected<std::string> getCachedOrDownloadDebuginfo(BuildIDRef ID) {
158 std::string UrlPath = getDebuginfodDebuginfoUrlPath(ID);
159 return getCachedOrDownloadArtifact(UniqueKey: getDebuginfodCacheKey(S: UrlPath), UrlPath);
160}
161
162// General fetching function.
163Expected<std::string> getCachedOrDownloadArtifact(StringRef UniqueKey,
164 StringRef UrlPath) {
165 SmallString<10> CacheDir;
166
167 Expected<std::string> CacheDirOrErr = getDefaultDebuginfodCacheDirectory();
168 if (!CacheDirOrErr)
169 return CacheDirOrErr.takeError();
170 CacheDir = *CacheDirOrErr;
171
172 return getCachedOrDownloadArtifact(UniqueKey, UrlPath, CacheDirectoryPath: CacheDir,
173 DebuginfodUrls: getDefaultDebuginfodUrls(),
174 Timeout: getDefaultDebuginfodTimeout());
175}
176
177namespace {
178
179/// A simple handler which streams the returned data to a cache file. The cache
180/// file is only created if a 200 OK status is observed.
181class StreamedHTTPResponseHandler : public HTTPResponseHandler {
182 using CreateStreamFn =
183 std::function<Expected<std::unique_ptr<CachedFileStream>>()>;
184 CreateStreamFn CreateStream;
185 HTTPClient &Client;
186 std::unique_ptr<CachedFileStream> FileStream;
187
188public:
189 StreamedHTTPResponseHandler(CreateStreamFn CreateStream, HTTPClient &Client)
190 : CreateStream(CreateStream), Client(Client) {}
191
192 /// Must be called exactly once after the writes have been completed
193 /// but before the StreamedHTTPResponseHandler object is destroyed.
194 Error commit();
195
196 virtual ~StreamedHTTPResponseHandler() = default;
197
198 Error handleBodyChunk(StringRef BodyChunk) override;
199};
200
201} // namespace
202
203Error StreamedHTTPResponseHandler::handleBodyChunk(StringRef BodyChunk) {
204 if (!FileStream) {
205 unsigned Code = Client.responseCode();
206 if (Code && Code != 200)
207 return Error::success();
208 Expected<std::unique_ptr<CachedFileStream>> FileStreamOrError =
209 CreateStream();
210 if (!FileStreamOrError)
211 return FileStreamOrError.takeError();
212 FileStream = std::move(*FileStreamOrError);
213 }
214 *FileStream->OS << BodyChunk;
215 return Error::success();
216}
217
218Error StreamedHTTPResponseHandler::commit() {
219 if (FileStream)
220 return FileStream->commit();
221 return Error::success();
222}
223
224// An over-accepting simplification of the HTTP RFC 7230 spec.
225static bool isHeader(StringRef S) {
226 StringRef Name;
227 StringRef Value;
228 std::tie(args&: Name, args&: Value) = S.split(Separator: ':');
229 if (Name.empty() || Value.empty())
230 return false;
231 return all_of(Range&: Name, P: [](char C) { return llvm::isPrint(C) && C != ' '; }) &&
232 all_of(Range&: Value, P: [](char C) { return llvm::isPrint(C) || C == '\t'; });
233}
234
235static SmallVector<std::string, 0> getHeaders() {
236 const char *Filename = getenv(name: "DEBUGINFOD_HEADERS_FILE");
237 if (!Filename)
238 return {};
239 ErrorOr<std::unique_ptr<MemoryBuffer>> HeadersFile =
240 MemoryBuffer::getFile(Filename, /*IsText=*/true);
241 if (!HeadersFile)
242 return {};
243
244 SmallVector<std::string, 0> Headers;
245 uint64_t LineNumber = 0;
246 for (StringRef Line : llvm::split(Str: (*HeadersFile)->getBuffer(), Separator: '\n')) {
247 LineNumber++;
248 Line.consume_back(Suffix: "\r");
249 if (!isHeader(S: Line)) {
250 if (!all_of(Range&: Line, P: llvm::isSpace))
251 WithColor::warning()
252 << "could not parse debuginfod header: " << Filename << ':'
253 << LineNumber << '\n';
254 continue;
255 }
256 Headers.emplace_back(Args&: Line);
257 }
258 return Headers;
259}
260
261Expected<std::string> getCachedOrDownloadArtifact(
262 StringRef UniqueKey, StringRef UrlPath, StringRef CacheDirectoryPath,
263 ArrayRef<StringRef> DebuginfodUrls, std::chrono::milliseconds Timeout) {
264 SmallString<64> AbsCachedArtifactPath;
265 sys::path::append(path&: AbsCachedArtifactPath, a: CacheDirectoryPath,
266 b: "llvmcache-" + UniqueKey);
267
268 Expected<FileCache> CacheOrErr =
269 localCache(CacheNameRef: "Debuginfod-client", TempFilePrefixRef: ".debuginfod-client", CacheDirectoryPathRef: CacheDirectoryPath);
270 if (!CacheOrErr)
271 return CacheOrErr.takeError();
272
273 FileCache Cache = *CacheOrErr;
274 // We choose an arbitrary Task parameter as we do not make use of it.
275 unsigned Task = 0;
276 Expected<AddStreamFn> CacheAddStreamOrErr = Cache(Task, UniqueKey, "");
277 if (!CacheAddStreamOrErr)
278 return CacheAddStreamOrErr.takeError();
279 AddStreamFn &CacheAddStream = *CacheAddStreamOrErr;
280 if (!CacheAddStream)
281 return std::string(AbsCachedArtifactPath);
282 // The artifact was not found in the local cache, query the debuginfod
283 // servers.
284 if (!HTTPClient::isAvailable())
285 return createStringError(EC: errc::io_error,
286 S: "No working HTTP client is available.");
287
288 if (!HTTPClient::IsInitialized)
289 return createStringError(
290 EC: errc::io_error,
291 S: "A working HTTP client is available, but it is not initialized. To "
292 "allow Debuginfod to make HTTP requests, call HTTPClient::initialize() "
293 "at the beginning of main.");
294
295 HTTPClient Client;
296 Client.setTimeout(Timeout);
297 for (StringRef ServerUrl : DebuginfodUrls) {
298 SmallString<64> ArtifactUrl;
299 sys::path::append(path&: ArtifactUrl, style: sys::path::Style::posix, a: ServerUrl, b: UrlPath);
300
301 // Perform the HTTP request and if successful, write the response body to
302 // the cache.
303 {
304 StreamedHTTPResponseHandler Handler(
305 [&]() { return CacheAddStream(Task, ""); }, Client);
306 HTTPRequest Request(ArtifactUrl);
307 Request.Headers = getHeaders();
308 Error Err = Client.perform(Request, Handler);
309 if (Err)
310 return std::move(Err);
311 if ((Err = Handler.commit()))
312 return std::move(Err);
313
314 unsigned Code = Client.responseCode();
315 if (Code && Code != 200)
316 continue;
317 }
318
319 Expected<CachePruningPolicy> PruningPolicyOrErr =
320 parseCachePruningPolicy(PolicyStr: std::getenv(name: "DEBUGINFOD_CACHE_POLICY"));
321 if (!PruningPolicyOrErr)
322 return PruningPolicyOrErr.takeError();
323 pruneCache(Path: CacheDirectoryPath, Policy: *PruningPolicyOrErr);
324
325 // Return the path to the artifact on disk.
326 return std::string(AbsCachedArtifactPath);
327 }
328
329 return createStringError(EC: errc::argument_out_of_domain, S: "build id not found");
330}
331
332DebuginfodLogEntry::DebuginfodLogEntry(const Twine &Message)
333 : Message(Message.str()) {}
334
335void DebuginfodLog::push(const Twine &Message) {
336 push(Entry: DebuginfodLogEntry(Message));
337}
338
339void DebuginfodLog::push(DebuginfodLogEntry Entry) {
340 {
341 std::lock_guard<std::mutex> Guard(QueueMutex);
342 LogEntryQueue.push(x: Entry);
343 }
344 QueueCondition.notify_one();
345}
346
347DebuginfodLogEntry DebuginfodLog::pop() {
348 {
349 std::unique_lock<std::mutex> Guard(QueueMutex);
350 // Wait for messages to be pushed into the queue.
351 QueueCondition.wait(lock&: Guard, p: [&] { return !LogEntryQueue.empty(); });
352 }
353 std::lock_guard<std::mutex> Guard(QueueMutex);
354 if (!LogEntryQueue.size())
355 llvm_unreachable("Expected message in the queue.");
356
357 DebuginfodLogEntry Entry = LogEntryQueue.front();
358 LogEntryQueue.pop();
359 return Entry;
360}
361
362DebuginfodCollection::DebuginfodCollection(ArrayRef<StringRef> PathsRef,
363 DebuginfodLog &Log,
364 ThreadPoolInterface &Pool,
365 double MinInterval)
366 : Log(Log), Pool(Pool), MinInterval(MinInterval) {
367 for (StringRef Path : PathsRef)
368 Paths.push_back(Elt: Path.str());
369}
370
371Error DebuginfodCollection::update() {
372 std::lock_guard<sys::Mutex> Guard(UpdateMutex);
373 if (UpdateTimer.isRunning())
374 UpdateTimer.stopTimer();
375 UpdateTimer.clear();
376 for (const std::string &Path : Paths) {
377 Log.push(Message: "Updating binaries at path " + Path);
378 if (Error Err = findBinaries(Path))
379 return Err;
380 }
381 Log.push(Message: "Updated collection");
382 UpdateTimer.startTimer();
383 return Error::success();
384}
385
386Expected<bool> DebuginfodCollection::updateIfStale() {
387 if (!UpdateTimer.isRunning())
388 return false;
389 UpdateTimer.stopTimer();
390 double Time = UpdateTimer.getTotalTime().getWallTime();
391 UpdateTimer.startTimer();
392 if (Time < MinInterval)
393 return false;
394 if (Error Err = update())
395 return std::move(Err);
396 return true;
397}
398
399Error DebuginfodCollection::updateForever(std::chrono::milliseconds Interval) {
400 while (true) {
401 if (Error Err = update())
402 return Err;
403 std::this_thread::sleep_for(rtime: Interval);
404 }
405 llvm_unreachable("updateForever loop should never end");
406}
407
408static bool hasELFMagic(StringRef FilePath) {
409 file_magic Type;
410 std::error_code EC = identify_magic(path: FilePath, result&: Type);
411 if (EC)
412 return false;
413 switch (Type) {
414 case file_magic::elf:
415 case file_magic::elf_relocatable:
416 case file_magic::elf_executable:
417 case file_magic::elf_shared_object:
418 case file_magic::elf_core:
419 return true;
420 default:
421 return false;
422 }
423}
424
425Error DebuginfodCollection::findBinaries(StringRef Path) {
426 std::error_code EC;
427 sys::fs::recursive_directory_iterator I(Twine(Path), EC), E;
428 std::mutex IteratorMutex;
429 ThreadPoolTaskGroup IteratorGroup(Pool);
430 for (unsigned WorkerIndex = 0; WorkerIndex < Pool.getMaxConcurrency();
431 WorkerIndex++) {
432 IteratorGroup.async(F: [&, this]() -> void {
433 std::string FilePath;
434 while (true) {
435 {
436 // Check if iteration is over or there is an error during iteration
437 std::lock_guard<std::mutex> Guard(IteratorMutex);
438 if (I == E || EC)
439 return;
440 // Grab a file path from the directory iterator and advance the
441 // iterator.
442 FilePath = I->path();
443 I.increment(ec&: EC);
444 }
445
446 // Inspect the file at this path to determine if it is debuginfo.
447 if (!hasELFMagic(FilePath))
448 continue;
449
450 Expected<object::OwningBinary<object::Binary>> BinOrErr =
451 object::createBinary(Path: FilePath);
452
453 if (!BinOrErr) {
454 consumeError(Err: BinOrErr.takeError());
455 continue;
456 }
457 object::Binary *Bin = std::move(BinOrErr.get().getBinary());
458 if (!Bin->isObject())
459 continue;
460
461 // TODO: Support non-ELF binaries
462 object::ELFObjectFileBase *Object =
463 dyn_cast<object::ELFObjectFileBase>(Val: Bin);
464 if (!Object)
465 continue;
466
467 BuildIDRef ID = getBuildID(Obj: Object);
468 if (ID.empty())
469 continue;
470
471 std::string IDString = buildIDToString(ID);
472 if (Object->hasDebugInfo()) {
473 std::lock_guard<sys::RWMutex> DebugBinariesGuard(DebugBinariesMutex);
474 (void)DebugBinaries.try_emplace(Key: IDString, Args: std::move(FilePath));
475 } else {
476 std::lock_guard<sys::RWMutex> BinariesGuard(BinariesMutex);
477 (void)Binaries.try_emplace(Key: IDString, Args: std::move(FilePath));
478 }
479 }
480 });
481 }
482 IteratorGroup.wait();
483 std::unique_lock<std::mutex> Guard(IteratorMutex);
484 if (EC)
485 return errorCodeToError(EC);
486 return Error::success();
487}
488
489Expected<std::optional<std::string>>
490DebuginfodCollection::getBinaryPath(BuildIDRef ID) {
491 Log.push(Message: "getting binary path of ID " + buildIDToString(ID));
492 std::shared_lock<sys::RWMutex> Guard(BinariesMutex);
493 auto Loc = Binaries.find(Key: buildIDToString(ID));
494 if (Loc != Binaries.end()) {
495 std::string Path = Loc->getValue();
496 return Path;
497 }
498 return std::nullopt;
499}
500
501Expected<std::optional<std::string>>
502DebuginfodCollection::getDebugBinaryPath(BuildIDRef ID) {
503 Log.push(Message: "getting debug binary path of ID " + buildIDToString(ID));
504 std::shared_lock<sys::RWMutex> Guard(DebugBinariesMutex);
505 auto Loc = DebugBinaries.find(Key: buildIDToString(ID));
506 if (Loc != DebugBinaries.end()) {
507 std::string Path = Loc->getValue();
508 return Path;
509 }
510 return std::nullopt;
511}
512
513Expected<std::string> DebuginfodCollection::findBinaryPath(BuildIDRef ID) {
514 {
515 // Check collection; perform on-demand update if stale.
516 Expected<std::optional<std::string>> PathOrErr = getBinaryPath(ID);
517 if (!PathOrErr)
518 return PathOrErr.takeError();
519 std::optional<std::string> Path = *PathOrErr;
520 if (!Path) {
521 Expected<bool> UpdatedOrErr = updateIfStale();
522 if (!UpdatedOrErr)
523 return UpdatedOrErr.takeError();
524 if (*UpdatedOrErr) {
525 // Try once more.
526 PathOrErr = getBinaryPath(ID);
527 if (!PathOrErr)
528 return PathOrErr.takeError();
529 Path = *PathOrErr;
530 }
531 }
532 if (Path)
533 return *Path;
534 }
535
536 // Try federation.
537 Expected<std::string> PathOrErr = getCachedOrDownloadExecutable(ID);
538 if (!PathOrErr)
539 consumeError(Err: PathOrErr.takeError());
540
541 // Fall back to debug binary.
542 return findDebugBinaryPath(ID);
543}
544
545Expected<std::string> DebuginfodCollection::findDebugBinaryPath(BuildIDRef ID) {
546 // Check collection; perform on-demand update if stale.
547 Expected<std::optional<std::string>> PathOrErr = getDebugBinaryPath(ID);
548 if (!PathOrErr)
549 return PathOrErr.takeError();
550 std::optional<std::string> Path = *PathOrErr;
551 if (!Path) {
552 Expected<bool> UpdatedOrErr = updateIfStale();
553 if (!UpdatedOrErr)
554 return UpdatedOrErr.takeError();
555 if (*UpdatedOrErr) {
556 // Try once more.
557 PathOrErr = getBinaryPath(ID);
558 if (!PathOrErr)
559 return PathOrErr.takeError();
560 Path = *PathOrErr;
561 }
562 }
563 if (Path)
564 return *Path;
565
566 // Try federation.
567 return getCachedOrDownloadDebuginfo(ID);
568}
569
570DebuginfodServer::DebuginfodServer(DebuginfodLog &Log,
571 DebuginfodCollection &Collection)
572 : Log(Log), Collection(Collection) {
573 cantFail(
574 Err: Server.get(UrlPathPattern: R"(/buildid/(.*)/debuginfo)", Handler: [&](HTTPServerRequest Request) {
575 Log.push(Message: "GET " + Request.UrlPath);
576 std::string IDString;
577 if (!tryGetFromHex(Input: Request.UrlPathMatches[0], Output&: IDString)) {
578 Request.setResponse(
579 {.Code: 404, .ContentType: "text/plain", .Body: "Build ID is not a hex string\n"});
580 return;
581 }
582 object::BuildID ID(IDString.begin(), IDString.end());
583 Expected<std::string> PathOrErr = Collection.findDebugBinaryPath(ID);
584 if (Error Err = PathOrErr.takeError()) {
585 consumeError(Err: std::move(Err));
586 Request.setResponse({.Code: 404, .ContentType: "text/plain", .Body: "Build ID not found\n"});
587 return;
588 }
589 streamFile(Request, FilePath: *PathOrErr);
590 }));
591 cantFail(
592 Err: Server.get(UrlPathPattern: R"(/buildid/(.*)/executable)", Handler: [&](HTTPServerRequest Request) {
593 Log.push(Message: "GET " + Request.UrlPath);
594 std::string IDString;
595 if (!tryGetFromHex(Input: Request.UrlPathMatches[0], Output&: IDString)) {
596 Request.setResponse(
597 {.Code: 404, .ContentType: "text/plain", .Body: "Build ID is not a hex string\n"});
598 return;
599 }
600 object::BuildID ID(IDString.begin(), IDString.end());
601 Expected<std::string> PathOrErr = Collection.findBinaryPath(ID);
602 if (Error Err = PathOrErr.takeError()) {
603 consumeError(Err: std::move(Err));
604 Request.setResponse({.Code: 404, .ContentType: "text/plain", .Body: "Build ID not found\n"});
605 return;
606 }
607 streamFile(Request, FilePath: *PathOrErr);
608 }));
609}
610
611} // namespace llvm
612