Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
reader.cpp
Go to the documentation of this file.
1#include <bucket/bucket.hpp>
2#include <haio_util.hpp>
3
4#include <zlib.h>
5
6#include <cstring>
7#include <string>
8
9namespace {
10
11using Haio::Bytes;
12
13constexpr uint32_t endOfDirectory = 0x06054b50;
14constexpr uint32_t directoryEntry = 0x02014b50;
15constexpr uint32_t localHeader = 0x04034b50;
16
17constexpr uint16_t methodStored = 0;
18constexpr uint16_t methodDeflate = 8;
19
20/**
21 * a zip whose entries expand far more than this is not a picture archive, it is an
22 * attempt to spend the server's memory. real image data barely compresses at all, so
23 * even a generous ratio is orders of magnitude away from anything legitimate.
24 */
25constexpr uint64_t maxExpansionRatio = 200;
26
27uint16_t readU16LE(Bytes data, size_t off) {
28 return static_cast<uint16_t>(data[off] | (data[off + 1] << 8));
29}
30
31/** the record sits at the end, behind a comment of up to 64k that has to be walked back over */
32std::optional<size_t> findEndOfDirectory(Bytes data) {
33 if (data.size() < 22) return std::nullopt;
34
35 const size_t furthest = std::min<size_t>(data.size(), 22 + 0xFFFF);
36 for (size_t back = 22; back <= furthest; back++) {
37 const auto at = data.size() - back;
38 if (Haio::Util::readU32LE(data, at) == endOfDirectory) return at;
39 }
40 return std::nullopt;
41}
42
43}
44
45namespace Haio::Cdn::Bucket {
46
47/**
48 * enough of the zip format to read one file out of an archive already in memory.
49 *
50 * the central directory is the index the format itself keeps, so nothing has to be
51 * guessed by scanning for local headers; a file that disagrees with its own directory
52 * is refused rather than read around.
53 *
54 * @todo zip64 is not handled, so an archive over four gigabytes, or with more than
55 * 65535 entries, is refused rather than misread.
56 */
58 const auto end = findEndOfDirectory(data);
59 if (!end) return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "not a zip, or its directory is missing"});
60
61 const auto count = readU16LE(data, *end + 10);
62 const auto directorySize = Util::readU32LE(data, *end + 12);
63 const auto directoryAt = Util::readU32LE(data, *end + 16);
64
65 if (directoryAt == 0xFFFFFFFF || count == 0xFFFF) {
66 return std::unexpected(Haio::Error{Haio::ErrorCode::UnsupportedFormat, "zip64 archives are not supported"});
67 }
68 if (static_cast<uint64_t>(directoryAt) + directorySize > data.size()) {
69 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "the zip directory points outside the file"});
70 }
71
72 ZipIndex index;
73 size_t at = directoryAt;
74
75 for (uint16_t entry = 0; entry < count; entry++) {
76 if (at + 46 > data.size() || Util::readU32LE(data, at) != directoryEntry) {
77 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "the zip directory is truncated"});
78 }
79
80 ZipEntry found;
81 found.method = readU16LE(data, at + 10);
82 found.compressed = Util::readU32LE(data, at + 20);
83 found.uncompressed = Util::readU32LE(data, at + 24);
84 found.at = Util::readU32LE(data, at + 42);
85
86 const auto nameSize = readU16LE(data, at + 28);
87 const auto extraSize = readU16LE(data, at + 30);
88 const auto commentSize = readU16LE(data, at + 32);
89 if (at + 46 + nameSize > data.size()) {
90 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "a zip entry name runs past the end"});
91 }
92
93 std::string name(reinterpret_cast<const char*>(data.data()) + at + 46, nameSize);
94 at += 46 + nameSize + extraSize + commentSize;
95
96 // a directory entry has nothing to read, and a name that climbs is not read at all
97 if (name.empty() || name.back() == '/') continue;
98 if (name.find("..") != std::string::npos) continue;
99
100 index.entries.emplace(std::move(name), found);
101 }
102 return index;
103}
104
105/** inflates one entry, refusing anything that expands further than it should */
106Result<std::vector<uint8_t>> readZipEntry(Bytes data, const ZipEntry& entry, size_t maxSize) {
107 if (entry.uncompressed > maxSize) {
108 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput,
109 "the file inside the zip is larger than max_unzip allows"});
110 }
111 if (entry.compressed > 0 && entry.uncompressed / entry.compressed > maxExpansionRatio) {
112 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "the file inside the zip expands too far"});
113 }
114
115 if (entry.at + 30 > data.size() || Util::readU32LE(data, entry.at) != localHeader) {
116 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "a zip entry has no header where the directory says"});
117 }
118
119 // the local header repeats the name and may carry different extra data, so its
120 // own lengths are the ones that count rather than the directory's
121 const auto nameSize = readU16LE(data, entry.at + 26);
122 const auto extraSize = readU16LE(data, entry.at + 28);
123 const auto from = static_cast<size_t>(entry.at) + 30 + nameSize + extraSize;
124 if (from + entry.compressed > data.size()) {
125 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "a zip entry runs past the end of the file"});
126 }
127
128 if (entry.method == methodStored) {
129 if (entry.compressed != entry.uncompressed) {
130 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "a stored zip entry disagrees with its own size"});
131 }
132 return std::vector<uint8_t>(data.begin() + from, data.begin() + from + entry.compressed);
133 }
134 if (entry.method != methodDeflate) {
135 return std::unexpected(Haio::Error{Haio::ErrorCode::UnsupportedFormat, "the zip entry uses a compression haio cannot read"});
136 }
137
138 std::vector<uint8_t> out(entry.uncompressed);
139
140 z_stream stream{};
141 // a negative window means raw deflate, which is what a zip stores: no zlib header
142 if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
143 return std::unexpected(Haio::Error{Haio::ErrorCode::Internal, "cannot start inflate"});
144 }
145
146 stream.next_in = const_cast<Bytef*>(data.data() + from);
147 stream.avail_in = static_cast<uInt>(entry.compressed);
148 stream.next_out = out.data();
149 stream.avail_out = static_cast<uInt>(out.size());
150
151 const auto status = inflate(&stream, Z_FINISH);
152 const auto written = stream.total_out;
153 inflateEnd(&stream);
154
155 if (status != Z_STREAM_END || written != entry.uncompressed) {
156 return std::unexpected(Haio::Error{Haio::ErrorCode::InvalidInput, "the file inside the zip could not be inflated"});
157 }
158 return out;
159}
160
161}
Result< std::vector< uint8_t > > readZipEntry(Bytes data, const ZipEntry &entry, size_t maxSize)
Definition reader.cpp:106
Result< ZipIndex > readZipIndex(Bytes data)
Definition reader.cpp:57
std::map< std::string, ZipEntry > entries
Definition bucket.hpp:71
uint32_t readU32LE(Bytes data, size_t off)
Definition bytes.cpp:21
std::expected< T, Error > Result
std::span< const uint8_t > Bytes