1//===- OffloadBundle.cpp - Utilities for offload bundles---*- C++ -*-===//
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#include "llvm/Object/OffloadBundle.h"
10#include "llvm/BinaryFormat/Magic.h"
11#include "llvm/IR/Module.h"
12#include "llvm/IRReader/IRReader.h"
13#include "llvm/MC/StringTableBuilder.h"
14#include "llvm/Object/Archive.h"
15#include "llvm/Object/Binary.h"
16#include "llvm/Object/COFF.h"
17#include "llvm/Object/ELFObjectFile.h"
18#include "llvm/Object/Error.h"
19#include "llvm/Object/IRObjectFile.h"
20#include "llvm/Object/ObjectFile.h"
21#include "llvm/Support/BinaryStreamReader.h"
22#include "llvm/Support/SourceMgr.h"
23#include "llvm/Support/Timer.h"
24
25using namespace llvm;
26using namespace llvm::object;
27
28static TimerGroup OffloadBundlerTimerGroup("Offload Bundler Timer Group",
29 "Timer group for offload bundler");
30
31// Extract an Offload bundle (usually a Offload Bundle) from a fat_bin
32// section.
33Error extractOffloadBundle(MemoryBufferRef Contents, uint64_t SectionOffset,
34 StringRef FileName,
35 SmallVectorImpl<OffloadBundleFatBin> &Bundles) {
36
37 size_t Offset = 0;
38 size_t NextbundleStart = 0;
39 StringRef Magic;
40 std::unique_ptr<MemoryBuffer> Buffer;
41
42 // There could be multiple offloading bundles stored at this section.
43 while ((NextbundleStart != StringRef::npos) &&
44 (Offset < Contents.getBuffer().size())) {
45 Buffer =
46 MemoryBuffer::getMemBuffer(InputData: Contents.getBuffer().drop_front(N: Offset), BufferName: "",
47 /*RequiresNullTerminator=*/false);
48
49 if (identify_magic(magic: (*Buffer).getBuffer()) ==
50 file_magic::offload_bundle_compressed) {
51 Magic = "CCOB";
52 // Decompress this bundle first.
53 NextbundleStart = (*Buffer).getBuffer().find(Str: Magic, From: Magic.size());
54 if (NextbundleStart == StringRef::npos)
55 NextbundleStart = (*Buffer).getBuffer().size();
56
57 ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
58 MemoryBuffer::getMemBuffer(
59 InputData: (*Buffer).getBuffer().take_front(N: NextbundleStart), BufferName: FileName,
60 RequiresNullTerminator: false);
61 if (std::error_code EC = CodeOrErr.getError())
62 return createFileError(F: FileName, EC);
63
64 Expected<std::unique_ptr<MemoryBuffer>> DecompressedBufferOrErr =
65 CompressedOffloadBundle::decompress(Input: **CodeOrErr, VerboseStream: nullptr);
66 if (!DecompressedBufferOrErr)
67 return createStringError(S: "failed to decompress input: " +
68 toString(E: DecompressedBufferOrErr.takeError()));
69
70 auto FatBundleOrErr = OffloadBundleFatBin::create(
71 **DecompressedBufferOrErr, SectionOffset: Offset, FileName, Decompress: true);
72 if (!FatBundleOrErr)
73 return FatBundleOrErr.takeError();
74
75 // Add current Bundle to list.
76 Bundles.emplace_back(Args: std::move(**FatBundleOrErr));
77
78 } else if (identify_magic(magic: (*Buffer).getBuffer()) ==
79 file_magic::offload_bundle) {
80 // Create the OffloadBundleFatBin object. This will also create the Bundle
81 // Entry list info.
82 auto FatBundleOrErr = OffloadBundleFatBin::create(
83 *Buffer, SectionOffset: SectionOffset + Offset, FileName);
84 if (!FatBundleOrErr)
85 return FatBundleOrErr.takeError();
86
87 // Add current Bundle to list.
88 Bundles.emplace_back(Args: std::move(**FatBundleOrErr));
89
90 Magic = "__CLANG_OFFLOAD_BUNDLE__";
91 NextbundleStart = (*Buffer).getBuffer().find(Str: Magic, From: Magic.size());
92 }
93
94 if (NextbundleStart != StringRef::npos)
95 Offset += NextbundleStart;
96 }
97
98 return Error::success();
99}
100
101Error OffloadBundleFatBin::readEntries(StringRef Buffer,
102 uint64_t SectionOffset) {
103 uint64_t NumOfEntries = 0;
104
105 BinaryStreamReader Reader(Buffer, llvm::endianness::little);
106
107 // Read the Magic String first.
108 StringRef Magic;
109 if (auto EC = Reader.readFixedString(Dest&: Magic, Length: 24))
110 return errorCodeToError(EC: object_error::parse_failed);
111
112 // Read the number of Code Objects (Entries) in the current Bundle.
113 if (auto EC = Reader.readInteger(Dest&: NumOfEntries))
114 return errorCodeToError(EC: object_error::parse_failed);
115
116 NumberOfEntries = NumOfEntries;
117
118 // For each Bundle Entry (code object).
119 for (uint64_t I = 0; I < NumOfEntries; I++) {
120 uint64_t EntrySize;
121 uint64_t EntryOffset;
122 uint64_t EntryIDSize;
123 StringRef EntryID;
124
125 if (Error Err = Reader.readInteger(Dest&: EntryOffset))
126 return Err;
127
128 if (Error Err = Reader.readInteger(Dest&: EntrySize))
129 return Err;
130
131 if (Error Err = Reader.readInteger(Dest&: EntryIDSize))
132 return Err;
133
134 if (Error Err = Reader.readFixedString(Dest&: EntryID, Length: EntryIDSize))
135 return Err;
136
137 auto Entry = std::make_unique<OffloadBundleEntry>(
138 args: EntryOffset + SectionOffset, args&: EntrySize, args&: EntryIDSize, args&: EntryID);
139
140 Entries.push_back(Elt: *Entry);
141 }
142
143 return Error::success();
144}
145
146Expected<std::unique_ptr<OffloadBundleFatBin>>
147OffloadBundleFatBin::create(MemoryBufferRef Buf, uint64_t SectionOffset,
148 StringRef FileName, bool Decompress) {
149 if (Buf.getBufferSize() < 24)
150 return errorCodeToError(EC: object_error::parse_failed);
151
152 // Check for magic bytes.
153 if ((identify_magic(magic: Buf.getBuffer()) != file_magic::offload_bundle) &&
154 (identify_magic(magic: Buf.getBuffer()) !=
155 file_magic::offload_bundle_compressed))
156 return errorCodeToError(EC: object_error::parse_failed);
157
158 std::unique_ptr<OffloadBundleFatBin> TheBundle(
159 new OffloadBundleFatBin(Buf, FileName));
160
161 // Read the Bundle Entries.
162 Error Err =
163 TheBundle->readEntries(Buffer: Buf.getBuffer(), SectionOffset: Decompress ? 0 : SectionOffset);
164 if (Err)
165 return Err;
166
167 return std::move(TheBundle);
168}
169
170Error OffloadBundleFatBin::extractBundle(const ObjectFile &Source) {
171 // This will extract all entries in the Bundle.
172 for (OffloadBundleEntry &Entry : Entries) {
173
174 if (Entry.Size == 0)
175 continue;
176
177 // create output file name. Which should be
178 // <fileName>-offset<Offset>-size<Size>.co"
179 std::string Str = getFileName().str() + "-offset" + itostr(X: Entry.Offset) +
180 "-size" + itostr(X: Entry.Size) + ".co";
181 if (Error Err = object::extractCodeObject(Source, Offset: Entry.Offset, Size: Entry.Size,
182 OutputFileName: StringRef(Str)))
183 return Err;
184 }
185
186 return Error::success();
187}
188
189Error object::extractOffloadBundleFatBinary(
190 const ObjectFile &Obj, SmallVectorImpl<OffloadBundleFatBin> &Bundles) {
191 // Ignore unsupported object formats.
192 if (!Obj.isELF() && !Obj.isCOFF())
193 return Error::success();
194
195 // Iterate through Sections until we find an offload_bundle section.
196 for (SectionRef Sec : Obj.sections()) {
197 Expected<StringRef> Buffer = Sec.getContents();
198 if (!Buffer)
199 return Buffer.takeError();
200
201 // If it does not start with the reserved suffix, just skip this section.
202 if ((llvm::identify_magic(magic: *Buffer) == file_magic::offload_bundle) ||
203 (llvm::identify_magic(magic: *Buffer) ==
204 file_magic::offload_bundle_compressed)) {
205
206 uint64_t SectionOffset = 0;
207 if (Obj.isELF()) {
208 SectionOffset = ELFSectionRef(Sec).getOffset();
209 } else if (Obj.isCOFF()) // TODO: add COFF Support.
210 return createStringError(EC: object_error::parse_failed,
211 S: "COFF object files not supported");
212
213 MemoryBufferRef Contents(*Buffer, Obj.getFileName());
214 if (Error Err = extractOffloadBundle(Contents, SectionOffset,
215 FileName: Obj.getFileName(), Bundles))
216 return Err;
217 }
218 }
219 return Error::success();
220}
221
222Error object::extractCodeObject(const ObjectFile &Source, int64_t Offset,
223 int64_t Size, StringRef OutputFileName) {
224 Expected<std::unique_ptr<FileOutputBuffer>> BufferOrErr =
225 FileOutputBuffer::create(FilePath: OutputFileName, Size);
226
227 if (!BufferOrErr)
228 return BufferOrErr.takeError();
229
230 Expected<MemoryBufferRef> InputBuffOrErr = Source.getMemoryBufferRef();
231 if (Error Err = InputBuffOrErr.takeError())
232 return Err;
233
234 std::unique_ptr<FileOutputBuffer> Buf = std::move(*BufferOrErr);
235 std::copy(first: InputBuffOrErr->getBufferStart() + Offset,
236 last: InputBuffOrErr->getBufferStart() + Offset + Size,
237 result: Buf->getBufferStart());
238 if (Error E = Buf->commit())
239 return E;
240
241 return Error::success();
242}
243
244Error object::extractCodeObject(const MemoryBufferRef Buffer, int64_t Offset,
245 int64_t Size, StringRef OutputFileName) {
246 Expected<std::unique_ptr<FileOutputBuffer>> BufferOrErr =
247 FileOutputBuffer::create(FilePath: OutputFileName, Size);
248 if (!BufferOrErr)
249 return BufferOrErr.takeError();
250
251 std::unique_ptr<FileOutputBuffer> Buf = std::move(*BufferOrErr);
252 std::copy(first: Buffer.getBufferStart() + Offset,
253 last: Buffer.getBufferStart() + Offset + Size, result: Buf->getBufferStart());
254
255 return Buf->commit();
256}
257
258// given a file name, offset, and size, extract data into a code object file,
259// into file "<SourceFile>-offset<Offset>-size<Size>.co".
260Error object::extractOffloadBundleByURI(StringRef URIstr) {
261 // create a URI object
262 Expected<std::unique_ptr<OffloadBundleURI>> UriOrErr(
263 OffloadBundleURI::createOffloadBundleURI(Str: URIstr, Type: FILE_URI));
264 if (!UriOrErr)
265 return UriOrErr.takeError();
266
267 OffloadBundleURI &Uri = **UriOrErr;
268 std::string OutputFile = Uri.FileName.str();
269 OutputFile +=
270 "-offset" + itostr(X: Uri.Offset) + "-size" + itostr(X: Uri.Size) + ".co";
271
272 // Create an ObjectFile object from uri.file_uri.
273 auto ObjOrErr = ObjectFile::createObjectFile(ObjectPath: Uri.FileName);
274 if (!ObjOrErr)
275 return ObjOrErr.takeError();
276
277 auto Obj = ObjOrErr->getBinary();
278 if (Error Err =
279 object::extractCodeObject(Source: *Obj, Offset: Uri.Offset, Size: Uri.Size, OutputFileName: OutputFile))
280 return Err;
281
282 return Error::success();
283}
284
285// Utility function to format numbers with commas.
286static std::string formatWithCommas(unsigned long long Value) {
287 std::string Num = std::to_string(val: Value);
288 int InsertPosition = Num.length() - 3;
289 while (InsertPosition > 0) {
290 Num.insert(pos: InsertPosition, s: ",");
291 InsertPosition -= 3;
292 }
293 return Num;
294}
295
296Expected<std::unique_ptr<MemoryBuffer>>
297CompressedOffloadBundle::compress(compression::Params P,
298 const MemoryBuffer &Input, uint16_t Version,
299 raw_ostream *VerboseStream) {
300 if (!compression::zstd::isAvailable() && !compression::zlib::isAvailable())
301 return createStringError(Fmt: "compression not supported.");
302 Timer HashTimer("Hash Calculation Timer", "Hash calculation time",
303 OffloadBundlerTimerGroup);
304 if (VerboseStream)
305 HashTimer.startTimer();
306 MD5 Hash;
307 MD5::MD5Result Result;
308 Hash.update(Str: Input.getBuffer());
309 Hash.final(Result);
310 uint64_t TruncatedHash = Result.low();
311 if (VerboseStream)
312 HashTimer.stopTimer();
313
314 SmallVector<uint8_t, 0> CompressedBuffer;
315 auto BufferUint8 = ArrayRef<uint8_t>(
316 reinterpret_cast<const uint8_t *>(Input.getBuffer().data()),
317 Input.getBuffer().size());
318 Timer CompressTimer("Compression Timer", "Compression time",
319 OffloadBundlerTimerGroup);
320 if (VerboseStream)
321 CompressTimer.startTimer();
322 compression::compress(P, Input: BufferUint8, Output&: CompressedBuffer);
323 if (VerboseStream)
324 CompressTimer.stopTimer();
325
326 uint16_t CompressionMethod = static_cast<uint16_t>(P.format);
327
328 // Store sizes in 64-bit variables first.
329 uint64_t UncompressedSize64 = Input.getBuffer().size();
330 uint64_t TotalFileSize64;
331
332 // Calculate total file size based on version.
333 if (Version == 2) {
334 // For V2, ensure the sizes don't exceed 32-bit limit.
335 if (UncompressedSize64 > std::numeric_limits<uint32_t>::max())
336 return createStringError(Fmt: "uncompressed size (%llu) exceeds version 2 "
337 "unsigned 32-bit integer limit",
338 Vals: UncompressedSize64);
339 TotalFileSize64 = MagicNumber.size() + sizeof(uint32_t) + sizeof(Version) +
340 sizeof(CompressionMethod) + sizeof(uint32_t) +
341 sizeof(TruncatedHash) + CompressedBuffer.size();
342 if (TotalFileSize64 > std::numeric_limits<uint32_t>::max())
343 return createStringError(Fmt: "total file size (%llu) exceeds version 2 "
344 "unsigned 32-bit integer limit",
345 Vals: TotalFileSize64);
346
347 } else { // Version 3.
348 TotalFileSize64 = MagicNumber.size() + sizeof(uint64_t) + sizeof(Version) +
349 sizeof(CompressionMethod) + sizeof(uint64_t) +
350 sizeof(TruncatedHash) + CompressedBuffer.size();
351 }
352
353 SmallVector<char, 0> FinalBuffer;
354 raw_svector_ostream OS(FinalBuffer);
355 OS << MagicNumber;
356 OS.write(Ptr: reinterpret_cast<const char *>(&Version), Size: sizeof(Version));
357 OS.write(Ptr: reinterpret_cast<const char *>(&CompressionMethod),
358 Size: sizeof(CompressionMethod));
359
360 // Write size fields according to version.
361 if (Version == 2) {
362 uint32_t TotalFileSize32 = static_cast<uint32_t>(TotalFileSize64);
363 uint32_t UncompressedSize32 = static_cast<uint32_t>(UncompressedSize64);
364 OS.write(Ptr: reinterpret_cast<const char *>(&TotalFileSize32),
365 Size: sizeof(TotalFileSize32));
366 OS.write(Ptr: reinterpret_cast<const char *>(&UncompressedSize32),
367 Size: sizeof(UncompressedSize32));
368 } else { // Version 3.
369 OS.write(Ptr: reinterpret_cast<const char *>(&TotalFileSize64),
370 Size: sizeof(TotalFileSize64));
371 OS.write(Ptr: reinterpret_cast<const char *>(&UncompressedSize64),
372 Size: sizeof(UncompressedSize64));
373 }
374
375 OS.write(Ptr: reinterpret_cast<const char *>(&TruncatedHash),
376 Size: sizeof(TruncatedHash));
377 OS.write(Ptr: reinterpret_cast<const char *>(CompressedBuffer.data()),
378 Size: CompressedBuffer.size());
379
380 if (VerboseStream) {
381 auto MethodUsed = P.format == compression::Format::Zstd ? "zstd" : "zlib";
382 double CompressionRate =
383 static_cast<double>(UncompressedSize64) / CompressedBuffer.size();
384 double CompressionTimeSeconds = CompressTimer.getTotalTime().getWallTime();
385 double CompressionSpeedMBs =
386 (UncompressedSize64 / (1024.0 * 1024.0)) / CompressionTimeSeconds;
387 *VerboseStream << "Compressed bundle format version: " << Version << "\n"
388 << "Total file size (including headers): "
389 << formatWithCommas(Value: TotalFileSize64) << " bytes\n"
390 << "Compression method used: " << MethodUsed << "\n"
391 << "Compression level: " << P.level << "\n"
392 << "Binary size before compression: "
393 << formatWithCommas(Value: UncompressedSize64) << " bytes\n"
394 << "Binary size after compression: "
395 << formatWithCommas(Value: CompressedBuffer.size()) << " bytes\n"
396 << "Compression rate: " << format(Fmt: "%.2lf", Vals: CompressionRate)
397 << "\n"
398 << "Compression ratio: "
399 << format(Fmt: "%.2lf%%", Vals: 100.0 / CompressionRate) << "\n"
400 << "Compression speed: "
401 << format(Fmt: "%.2lf MB/s", Vals: CompressionSpeedMBs) << "\n"
402 << "Truncated MD5 hash: " << format_hex(N: TruncatedHash, Width: 16)
403 << "\n";
404 }
405
406 return MemoryBuffer::getMemBufferCopy(
407 InputData: StringRef(FinalBuffer.data(), FinalBuffer.size()));
408}
409
410// Use packed structs to avoid padding, such that the structs map the serialized
411// format.
412LLVM_PACKED_START
413union RawCompressedBundleHeader {
414 struct CommonFields {
415 uint32_t Magic;
416 uint16_t Version;
417 uint16_t Method;
418 };
419
420 struct V1Header {
421 CommonFields Common;
422 uint32_t UncompressedFileSize;
423 uint64_t Hash;
424 };
425
426 struct V2Header {
427 CommonFields Common;
428 uint32_t FileSize;
429 uint32_t UncompressedFileSize;
430 uint64_t Hash;
431 };
432
433 struct V3Header {
434 CommonFields Common;
435 uint64_t FileSize;
436 uint64_t UncompressedFileSize;
437 uint64_t Hash;
438 };
439
440 CommonFields Common;
441 V1Header V1;
442 V2Header V2;
443 V3Header V3;
444};
445LLVM_PACKED_END
446
447// Helper method to get header size based on version.
448static size_t getHeaderSize(uint16_t Version) {
449 switch (Version) {
450 case 1:
451 return sizeof(RawCompressedBundleHeader::V1Header);
452 case 2:
453 return sizeof(RawCompressedBundleHeader::V2Header);
454 case 3:
455 return sizeof(RawCompressedBundleHeader::V3Header);
456 default:
457 llvm_unreachable("Unsupported version");
458 }
459}
460
461Expected<CompressedOffloadBundle::CompressedBundleHeader>
462CompressedOffloadBundle::CompressedBundleHeader::tryParse(StringRef Blob) {
463 assert(Blob.size() >= sizeof(RawCompressedBundleHeader::CommonFields));
464 assert(identify_magic(Blob) == file_magic::offload_bundle_compressed);
465
466 RawCompressedBundleHeader Header;
467 std::memcpy(dest: &Header, src: Blob.data(), n: std::min(a: Blob.size(), b: sizeof(Header)));
468
469 CompressedBundleHeader Normalized;
470 Normalized.Version = Header.Common.Version;
471
472 size_t RequiredSize = getHeaderSize(Version: Normalized.Version);
473
474 if (Blob.size() < RequiredSize)
475 return createStringError(Fmt: "compressed bundle header size too small");
476
477 switch (Normalized.Version) {
478 case 1:
479 Normalized.UncompressedFileSize = Header.V1.UncompressedFileSize;
480 Normalized.Hash = Header.V1.Hash;
481 break;
482 case 2:
483 Normalized.FileSize = Header.V2.FileSize;
484 Normalized.UncompressedFileSize = Header.V2.UncompressedFileSize;
485 Normalized.Hash = Header.V2.Hash;
486 break;
487 case 3:
488 Normalized.FileSize = Header.V3.FileSize;
489 Normalized.UncompressedFileSize = Header.V3.UncompressedFileSize;
490 Normalized.Hash = Header.V3.Hash;
491 break;
492 default:
493 return createStringError(Fmt: "unknown compressed bundle version");
494 }
495
496 // Determine compression format.
497 switch (Header.Common.Method) {
498 case static_cast<uint16_t>(compression::Format::Zlib):
499 case static_cast<uint16_t>(compression::Format::Zstd):
500 Normalized.CompressionFormat =
501 static_cast<compression::Format>(Header.Common.Method);
502 break;
503 default:
504 return createStringError(Fmt: "unknown compressing method");
505 }
506
507 return Normalized;
508}
509
510Expected<std::unique_ptr<MemoryBuffer>>
511CompressedOffloadBundle::decompress(const MemoryBuffer &Input,
512 raw_ostream *VerboseStream) {
513 StringRef Blob = Input.getBuffer();
514
515 // Check minimum header size (using V1 as it's the smallest).
516 if (Blob.size() < sizeof(RawCompressedBundleHeader::CommonFields))
517 return MemoryBuffer::getMemBufferCopy(InputData: Blob);
518
519 if (identify_magic(magic: Blob) != file_magic::offload_bundle_compressed) {
520 if (VerboseStream)
521 *VerboseStream << "Uncompressed bundle\n";
522 return MemoryBuffer::getMemBufferCopy(InputData: Blob);
523 }
524
525 Expected<CompressedBundleHeader> HeaderOrErr =
526 CompressedBundleHeader::tryParse(Blob);
527 if (!HeaderOrErr)
528 return HeaderOrErr.takeError();
529
530 const CompressedBundleHeader &Normalized = *HeaderOrErr;
531 unsigned ThisVersion = Normalized.Version;
532 size_t HeaderSize = getHeaderSize(Version: ThisVersion);
533
534 compression::Format CompressionFormat = Normalized.CompressionFormat;
535
536 size_t TotalFileSize = Normalized.FileSize.value_or(u: 0);
537 size_t UncompressedSize = Normalized.UncompressedFileSize;
538 auto StoredHash = Normalized.Hash;
539
540 Timer DecompressTimer("Decompression Timer", "Decompression time",
541 OffloadBundlerTimerGroup);
542 if (VerboseStream)
543 DecompressTimer.startTimer();
544
545 SmallVector<uint8_t, 0> DecompressedData;
546 StringRef CompressedData =
547 Blob.substr(Start: HeaderSize, N: TotalFileSize - HeaderSize);
548
549 if (Error DecompressionError = compression::decompress(
550 F: CompressionFormat, Input: arrayRefFromStringRef(Input: CompressedData),
551 Output&: DecompressedData, UncompressedSize))
552 return createStringError(S: "could not decompress embedded file contents: " +
553 toString(E: std::move(DecompressionError)));
554
555 if (VerboseStream) {
556 DecompressTimer.stopTimer();
557
558 double DecompressionTimeSeconds =
559 DecompressTimer.getTotalTime().getWallTime();
560
561 // Recalculate MD5 hash for integrity check.
562 Timer HashRecalcTimer("Hash Recalculation Timer", "Hash recalculation time",
563 OffloadBundlerTimerGroup);
564 HashRecalcTimer.startTimer();
565 MD5 Hash;
566 MD5::MD5Result Result;
567 Hash.update(Data: ArrayRef<uint8_t>(DecompressedData));
568 Hash.final(Result);
569 uint64_t RecalculatedHash = Result.low();
570 HashRecalcTimer.stopTimer();
571 bool HashMatch = (StoredHash == RecalculatedHash);
572
573 double CompressionRate =
574 static_cast<double>(UncompressedSize) / CompressedData.size();
575 double DecompressionSpeedMBs =
576 (UncompressedSize / (1024.0 * 1024.0)) / DecompressionTimeSeconds;
577
578 *VerboseStream << "Compressed bundle format version: " << ThisVersion
579 << "\n";
580 if (ThisVersion >= 2)
581 *VerboseStream << "Total file size (from header): "
582 << formatWithCommas(Value: TotalFileSize) << " bytes\n";
583 *VerboseStream
584 << "Decompression method: "
585 << (CompressionFormat == compression::Format::Zlib ? "zlib" : "zstd")
586 << "\n"
587 << "Size before decompression: "
588 << formatWithCommas(Value: CompressedData.size()) << " bytes\n"
589 << "Size after decompression: " << formatWithCommas(Value: UncompressedSize)
590 << " bytes\n"
591 << "Compression rate: " << format(Fmt: "%.2lf", Vals: CompressionRate) << "\n"
592 << "Compression ratio: " << format(Fmt: "%.2lf%%", Vals: 100.0 / CompressionRate)
593 << "\n"
594 << "Decompression speed: "
595 << format(Fmt: "%.2lf MB/s", Vals: DecompressionSpeedMBs) << "\n"
596 << "Stored hash: " << format_hex(N: StoredHash, Width: 16) << "\n"
597 << "Recalculated hash: " << format_hex(N: RecalculatedHash, Width: 16) << "\n"
598 << "Hashes match: " << (HashMatch ? "Yes" : "No") << "\n";
599 }
600
601 return MemoryBuffer::getMemBufferCopy(InputData: toStringRef(Input: DecompressedData));
602}
603