|
| 1 | +#include "bundle_locator.hpp" |
| 2 | +#include "selfpath.hpp" |
| 3 | + |
| 4 | +#include <algorithm> |
| 5 | +#include <cstdint> |
| 6 | +#include <fstream> |
| 7 | +#include <system_error> |
| 8 | +#include <vector> |
| 9 | + |
| 10 | +namespace flapi { |
| 11 | + |
| 12 | +namespace { |
| 13 | + |
| 14 | +constexpr std::size_t kEocdRecordSize = 22; |
| 15 | +constexpr std::size_t kMaxCommentLen = 0xffffu; |
| 16 | + |
| 17 | +// We accept padding well in excess of the 10 KiB spike default; the |
| 18 | +// total tail buffer is EOCD + max comment + 64 KiB pad budget. |
| 19 | +constexpr std::size_t kPaddingBudget = 65536; |
| 20 | +constexpr std::size_t kScanBudget = kEocdRecordSize + kMaxCommentLen + kPaddingBudget; |
| 21 | + |
| 22 | +std::uint16_t ReadU16(const std::uint8_t* p) { |
| 23 | + return static_cast<std::uint16_t>( |
| 24 | + static_cast<std::uint16_t>(p[0]) | |
| 25 | + (static_cast<std::uint16_t>(p[1]) << 8)); |
| 26 | +} |
| 27 | + |
| 28 | +std::uint32_t ReadU32(const std::uint8_t* p) { |
| 29 | + return static_cast<std::uint32_t>(p[0]) |
| 30 | + | (static_cast<std::uint32_t>(p[1]) << 8) |
| 31 | + | (static_cast<std::uint32_t>(p[2]) << 16) |
| 32 | + | (static_cast<std::uint32_t>(p[3]) << 24); |
| 33 | +} |
| 34 | + |
| 35 | +} // namespace |
| 36 | + |
| 37 | +std::optional<BundleLocation> LocateBundle(const std::filesystem::path& path) { |
| 38 | + std::error_code ec; |
| 39 | + const auto file_size = std::filesystem::file_size(path, ec); |
| 40 | + if (ec || file_size < kEocdRecordSize) { |
| 41 | + return std::nullopt; |
| 42 | + } |
| 43 | + |
| 44 | + std::ifstream in(path, std::ios::binary); |
| 45 | + if (!in.is_open()) { |
| 46 | + return std::nullopt; |
| 47 | + } |
| 48 | + |
| 49 | + const std::size_t tail_bytes = |
| 50 | + static_cast<std::size_t>(std::min<std::uint64_t>(file_size, kScanBudget)); |
| 51 | + const std::uint64_t tail_start = file_size - tail_bytes; |
| 52 | + |
| 53 | + std::vector<std::uint8_t> tail(tail_bytes); |
| 54 | + in.seekg(static_cast<std::streamoff>(tail_start), std::ios::beg); |
| 55 | + in.read(reinterpret_cast<char*>(tail.data()), |
| 56 | + static_cast<std::streamsize>(tail_bytes)); |
| 57 | + if (!in) { |
| 58 | + return std::nullopt; |
| 59 | + } |
| 60 | + if (tail.size() < kEocdRecordSize) { |
| 61 | + return std::nullopt; |
| 62 | + } |
| 63 | + |
| 64 | + // Reverse-scan from the latest valid signature position. The latest |
| 65 | + // (largest-offset) EOCD wins, since any earlier signature byte |
| 66 | + // sequence in random leading data is a false positive. |
| 67 | + const std::size_t max_start = tail.size() - kEocdRecordSize; |
| 68 | + for (std::size_t i = max_start + 1; i-- > 0; ) { |
| 69 | + if (tail[i] != 0x50 || |
| 70 | + tail[i + 1] != 0x4b || |
| 71 | + tail[i + 2] != 0x05 || |
| 72 | + tail[i + 3] != 0x06) { |
| 73 | + continue; |
| 74 | + } |
| 75 | + |
| 76 | + const std::uint8_t* p = tail.data() + i; |
| 77 | + const std::uint16_t this_disk = ReadU16(p + 4); |
| 78 | + const std::uint16_t cd_start_disk = ReadU16(p + 6); |
| 79 | + const std::uint16_t entries_this = ReadU16(p + 8); |
| 80 | + const std::uint16_t entries_total = ReadU16(p + 10); |
| 81 | + const std::uint32_t cd_size = ReadU32(p + 12); |
| 82 | + const std::uint32_t cd_offset_arch = ReadU32(p + 16); |
| 83 | + const std::uint16_t comment_len = ReadU16(p + 20); |
| 84 | + |
| 85 | + // Multi-disk archives are not supported. |
| 86 | + if (this_disk != 0 || cd_start_disk != 0) { |
| 87 | + continue; |
| 88 | + } |
| 89 | + if (entries_this != entries_total) { |
| 90 | + continue; |
| 91 | + } |
| 92 | + |
| 93 | + // The comment must fit in the tail. |
| 94 | + const std::size_t comment_end = i + kEocdRecordSize + comment_len; |
| 95 | + if (comment_end > tail.size()) { |
| 96 | + continue; |
| 97 | + } |
| 98 | + |
| 99 | + // Anything after the comment up to file-EOF must be zero |
| 100 | + // padding -- the libarchive tar-block rounding tolerance. |
| 101 | + bool padding_ok = true; |
| 102 | + for (std::size_t j = comment_end; j < tail.size(); ++j) { |
| 103 | + if (tail[j] != 0) { |
| 104 | + padding_ok = false; |
| 105 | + break; |
| 106 | + } |
| 107 | + } |
| 108 | + if (!padding_ok) { |
| 109 | + continue; |
| 110 | + } |
| 111 | + |
| 112 | + const std::uint64_t eocd_file_offset = tail_start + i; |
| 113 | + |
| 114 | + // The central directory sits immediately before the EOCD. |
| 115 | + if (cd_size > eocd_file_offset) { |
| 116 | + continue; |
| 117 | + } |
| 118 | + const std::uint64_t cd_file_offset = eocd_file_offset - cd_size; |
| 119 | + |
| 120 | + if (cd_offset_arch > cd_file_offset) { |
| 121 | + continue; |
| 122 | + } |
| 123 | + const std::uint64_t bundle_start = cd_file_offset - cd_offset_arch; |
| 124 | + |
| 125 | + const std::uint64_t bundle_end = eocd_file_offset + kEocdRecordSize + comment_len; |
| 126 | + if (bundle_end < bundle_start) { |
| 127 | + continue; // overflow paranoia |
| 128 | + } |
| 129 | + |
| 130 | + BundleLocation loc; |
| 131 | + loc.offset = bundle_start; |
| 132 | + loc.size = bundle_end - bundle_start; |
| 133 | + return loc; |
| 134 | + } |
| 135 | + |
| 136 | + return std::nullopt; |
| 137 | +} |
| 138 | + |
| 139 | +std::optional<BundleLocation> LocateBundleInSelf() { |
| 140 | + try { |
| 141 | + return LocateBundle(GetSelfPath()); |
| 142 | + } catch (...) { |
| 143 | + return std::nullopt; |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +} // namespace flapi |
0 commit comments