NAH 3.0.0
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_archive.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <cerrno>
7#include <cstdio>
8#include <cstring>
9#include <filesystem>
10#include <fstream>
11#include <limits>
12#include <optional>
13#include <random>
14#include <string>
15#include <unordered_set>
16#include <vector>
17
18#include <zlib.h>
19
20#ifdef _WIN32
21#include <windows.h>
22#endif
23
24namespace nah::archive {
25
26namespace fs = std::filesystem;
27
28inline constexpr std::uintmax_t max_entry_size = 512ULL * 1024 * 1024;
29inline constexpr std::uintmax_t max_archive_size = 1024ULL * 1024 * 1024;
30inline constexpr std::size_t max_entries = 100000;
31
32struct Result {
33 bool ok;
34 std::string error;
35};
36
37inline bool write_gzip(gzFile out, const void* data, std::size_t size) {
38 const auto* bytes = static_cast<const unsigned char*>(data);
39 while (size > 0) {
40 const auto chunk = static_cast<unsigned int>(
41 (std::min<std::size_t>)(size, (std::numeric_limits<unsigned int>::max)()));
42 if (gzwrite(out, bytes, chunk) != static_cast<int>(chunk)) return false;
43 bytes += chunk;
44 size -= chunk;
45 }
46 return true;
47}
48
49inline bool read_gzip(gzFile in, void* data, std::size_t size) {
50 auto* bytes = static_cast<unsigned char*>(data);
51 while (size > 0) {
52 const auto chunk = static_cast<unsigned int>(
53 (std::min<std::size_t>)(size, (std::numeric_limits<unsigned int>::max)()));
54 const int count = gzread(in, bytes, chunk);
55 if (count <= 0) return false;
56 bytes += count;
57 size -= static_cast<std::size_t>(count);
58 }
59 return true;
60}
61
62inline std::optional<std::uint64_t> parse_octal(const unsigned char* value, std::size_t length) {
63 std::uint64_t result = 0;
64 std::size_t i = 0;
65 while (i < length && (value[i] == ' ' || value[i] == '\0')) ++i;
66 for (; i < length && value[i] != '\0' && value[i] != ' '; ++i) {
67 if (value[i] < '0' || value[i] > '7') return std::nullopt;
68 if (result > ((std::numeric_limits<std::uint64_t>::max)() >> 3)) return std::nullopt;
69 result = (result << 3) + static_cast<std::uint64_t>(value[i] - '0');
70 }
71 return result;
72}
73
74inline bool put_octal(unsigned char* field, std::size_t length, std::uint64_t value) {
75 if (length < 2) return false;
76 std::string digits;
77 do {
78 digits.push_back(static_cast<char>('0' + (value & 7)));
79 value >>= 3;
80 } while (value != 0);
81 if (digits.size() + 1 > length) return false;
82 std::memset(field, '0', length);
83 field[length - 1] = '\0';
84 for (std::size_t i = 0; i < digits.size(); ++i) field[length - 2 - i] = static_cast<unsigned char>(digits[i]);
85 return true;
86}
87
88inline std::optional<std::pair<std::string, std::string>> split_ustar_path(const std::string& path) {
89 if (path.size() <= 100) return std::pair<std::string, std::string>{"", path};
90 for (std::size_t pos = path.rfind('/'); pos != std::string::npos; pos = pos == 0 ? std::string::npos : path.rfind('/', pos - 1)) {
91 if (pos <= 155 && path.size() - pos - 1 <= 100) {
92 return std::pair<std::string, std::string>{path.substr(0, pos), path.substr(pos + 1)};
93 }
94 }
95 return std::nullopt;
96}
97
98inline Result create(const fs::path& source, const fs::path& output) {
99 std::error_code ec;
100 const auto source_root = fs::weakly_canonical(source, ec);
101 if (ec || !fs::is_directory(source_root, ec)) return {false, "source is not a readable directory"};
102
103 struct Entry { fs::path disk_path; std::string archive_path; bool directory; std::uintmax_t size; unsigned int mode; };
104 std::vector<Entry> entries;
105 std::uintmax_t total = 0;
106 for (fs::recursive_directory_iterator it(source_root, fs::directory_options::none, ec), end; !ec && it != end; it.increment(ec)) {
107 const auto status = it->symlink_status(ec);
108 if (ec) break;
109 if (fs::is_symlink(status)) return {false, "symbolic links are not supported: " + it->path().string()};
110 if (!fs::is_directory(status) && !fs::is_regular_file(status)) return {false, "unsupported file type: " + it->path().string()};
111 auto relative = fs::relative(it->path(), source_root, ec);
112 if (ec) break;
113 std::string archive_path = relative.generic_string();
114 if (archive_path.empty() || archive_path.find('\\') != std::string::npos || !split_ustar_path(archive_path)) {
115 return {false, "path cannot be represented safely in a package: " + archive_path};
116 }
117 const bool directory = fs::is_directory(status);
118 const auto size = directory ? 0 : fs::file_size(it->path(), ec);
119 if (ec) break;
120 if (size > max_entry_size || total > max_archive_size - size) return {false, "package exceeds the size limit"};
121 total += size;
122 const auto perms = static_cast<unsigned int>(status.permissions()) & 0777U;
123 entries.push_back({it->path(), archive_path, directory, size, perms});
124 if (entries.size() > max_entries) return {false, "package contains too many entries"};
125 }
126 if (ec) return {false, "failed to enumerate source: " + ec.message()};
127 std::sort(entries.begin(), entries.end(), [](const Entry& a, const Entry& b) { return a.archive_path < b.archive_path; });
128
129 const auto absolute_output = fs::absolute(output, ec).lexically_normal();
130 if (ec) return {false, "invalid output path"};
131 auto source_it = source_root.begin();
132 auto output_it = absolute_output.begin();
133 for (; source_it != source_root.end() && output_it != absolute_output.end() && *source_it == *output_it;
134 ++source_it, ++output_it) {}
135 if (source_it == source_root.end()) return {false, "output package must be outside the source directory"};
136 fs::create_directories(absolute_output.parent_path(), ec);
137 if (ec) return {false, "cannot create output directory: " + ec.message()};
138 std::random_device random;
139 const auto temporary = absolute_output.string() + ".tmp." + std::to_string(random()) + std::to_string(random());
140 gzFile out = gzopen(temporary.c_str(), "wb9");
141 if (!out) return {false, "cannot create package"};
142
143 auto fail = [&](const std::string& message) {
144 gzclose(out);
145 fs::remove(temporary, ec);
146 return Result{false, message};
147 };
148 std::array<unsigned char, 512> block{};
149 std::array<char, 64 * 1024> buffer{};
150 for (const auto& entry : entries) {
151 block.fill(0);
152 const auto split = split_ustar_path(entry.archive_path);
153 std::memcpy(block.data(), split->second.data(), split->second.size());
154 std::memcpy(block.data() + 345, split->first.data(), split->first.size());
155 if (!put_octal(block.data() + 100, 8, entry.mode ? entry.mode : (entry.directory ? 0755 : 0644)) ||
156 !put_octal(block.data() + 108, 8, 0) || !put_octal(block.data() + 116, 8, 0) ||
157 !put_octal(block.data() + 124, 12, entry.size) || !put_octal(block.data() + 136, 12, 0)) {
158 return fail("package metadata is too large");
159 }
160 std::memset(block.data() + 148, ' ', 8);
161 block[156] = entry.directory ? '5' : '0';
162 std::memcpy(block.data() + 257, "ustar", 5);
163 std::memcpy(block.data() + 263, "00", 2);
164 std::uint64_t checksum = 0;
165 for (const auto byte : block) checksum += byte;
166 if (!put_octal(block.data() + 148, 7, checksum)) return fail("package checksum overflow");
167 block[155] = ' ';
168 if (!write_gzip(out, block.data(), block.size())) return fail("failed to write package");
169 if (!entry.directory) {
170 std::ifstream input(entry.disk_path, std::ios::binary);
171 if (!input) return fail("cannot read: " + entry.disk_path.string());
172 std::uintmax_t remaining = entry.size;
173 while (remaining > 0) {
174 const auto count = static_cast<std::streamsize>(
175 (std::min<std::uintmax_t>)(remaining, buffer.size()));
176 input.read(buffer.data(), count);
177 if (input.gcount() != count || !write_gzip(out, buffer.data(), static_cast<std::size_t>(count))) return fail("failed to package file");
178 remaining -= static_cast<std::uintmax_t>(count);
179 }
180 const auto padding = static_cast<std::size_t>((512 - (entry.size % 512)) % 512);
181 block.fill(0);
182 if (padding && !write_gzip(out, block.data(), padding)) return fail("failed to write package padding");
183 }
184 }
185 block.fill(0);
186 if (!write_gzip(out, block.data(), block.size()) || !write_gzip(out, block.data(), block.size()) || gzclose(out) != Z_OK) {
187 fs::remove(temporary, ec);
188 return {false, "failed to finalize package"};
189 }
190#ifdef _WIN32
191 if (!MoveFileExW(fs::path(temporary).wstring().c_str(), absolute_output.wstring().c_str(),
192 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
193 fs::remove(temporary, ec);
194 return {false, "cannot activate output package"};
195 }
196#else
197 fs::rename(temporary, absolute_output, ec);
198 if (ec) { fs::remove(temporary, ec); return {false, "cannot activate output package"}; }
199#endif
200 return {true, {}};
201}
202
203inline Result extract(const fs::path& archive, const fs::path& destination) {
204 std::error_code ec;
205 const auto root = fs::absolute(destination, ec).lexically_normal();
206 if (ec || fs::exists(root, ec)) return {false, "extraction directory must not already exist"};
207 fs::create_directories(root, ec);
208 if (ec) return {false, "cannot create extraction directory"};
209 gzFile input = gzopen(archive.string().c_str(), "rb");
210 if (!input) { fs::remove_all(root, ec); return {false, "cannot open package"}; }
211
212 auto fail = [&](const std::string& message) {
213 gzclose(input);
214 fs::remove_all(root, ec);
215 return Result{false, message};
216 };
217 std::array<unsigned char, 512> header{};
218 std::array<char, 64 * 1024> buffer{};
219 std::uintmax_t total = 0;
220 std::size_t entries = 0;
221 std::unordered_set<std::string> seen_paths;
222 while (true) {
223 if (!read_gzip(input, header.data(), header.size())) return fail("truncated package header");
224 bool zero = true;
225 for (const auto byte : header) zero = zero && byte == 0;
226 if (zero) {
227 int count = 0;
228 std::size_t trailing = 0;
229 while ((count = gzread(input, buffer.data(), static_cast<unsigned int>(buffer.size()))) > 0) {
230 trailing += static_cast<std::size_t>(count);
231 if (trailing > 1024 * 1024) return fail("package terminator is oversized");
232 for (int i = 0; i < count; ++i) if (buffer[static_cast<std::size_t>(i)] != 0) return fail("data follows the package terminator");
233 }
234 int gzip_error = Z_OK;
235 gzerror(input, &gzip_error);
236 if (count < 0 || (gzip_error != Z_OK && gzip_error != Z_STREAM_END)) return fail("invalid gzip stream");
237 break;
238 }
239 if (++entries > max_entries) return fail("package contains too many entries");
240
241 if (std::memcmp(header.data() + 257, "ustar", 5) != 0 ||
242 std::memcmp(header.data() + 263, "00", 2) != 0) {
243 return fail("package entry is not USTAR");
244 }
245
246 const auto stored_checksum = parse_octal(header.data() + 148, 8);
247 if (!stored_checksum) return fail("invalid package checksum");
248 std::uint64_t checksum = 0;
249 for (std::size_t i = 0; i < header.size(); ++i) checksum += (i >= 148 && i < 156) ? static_cast<unsigned char>(' ') : header[i];
250 if (checksum != *stored_checksum) return fail("package checksum mismatch");
251 const auto size = parse_octal(header.data() + 124, 12);
252 const auto mode = parse_octal(header.data() + 100, 8);
253 if (!size || !mode || *size > max_entry_size || total > max_archive_size - *size) return fail("invalid or oversized package entry");
254 total += *size;
255
256 const auto field = [](const unsigned char* data, std::size_t length) {
257 const auto* end = static_cast<const unsigned char*>(std::memchr(data, '\0', length));
258 return std::string(reinterpret_cast<const char*>(data), end ? static_cast<std::size_t>(end - data) : length);
259 };
260 const auto name = field(header.data(), 100);
261 const auto prefix = field(header.data() + 345, 155);
262 const auto archive_path = prefix.empty() ? name : prefix + "/" + name;
263 fs::path relative(archive_path);
264 if (archive_path.empty() || archive_path.find('\\') != std::string::npos || relative.is_absolute() || relative.has_root_name()) return fail("unsafe package path");
265 relative = relative.lexically_normal();
266 for (const auto& part : relative) if (part == "..") return fail("package path escapes the destination");
267 if (!seen_paths.insert(relative.generic_string()).second) return fail("duplicate package path");
268 const auto target = (root / relative).lexically_normal();
269 auto mismatch = std::mismatch(root.begin(), root.end(), target.begin(), target.end());
270 if (mismatch.first != root.end() || target == root) return fail("package path escapes the destination");
271
272 const unsigned char type = header[156];
273 if (type == '5') {
274 if (*size != 0) return fail("invalid directory entry");
275 fs::create_directories(target, ec);
276 } else if (type == '0' || type == '\0') {
277 fs::create_directories(target.parent_path(), ec);
278 if (!ec) {
279 std::ofstream output(target, std::ios::binary | std::ios::trunc);
280 if (!output) return fail("cannot create extracted file");
281 std::uint64_t remaining = *size;
282 while (remaining > 0) {
283 const auto count = static_cast<std::size_t>(
284 (std::min<std::uint64_t>)(remaining, buffer.size()));
285 if (!read_gzip(input, buffer.data(), count)) return fail("truncated package entry");
286 output.write(buffer.data(), static_cast<std::streamsize>(count));
287 if (!output) return fail("cannot write extracted file");
288 remaining -= count;
289 }
290 }
291 } else {
292 return fail("links and special files are not supported in packages");
293 }
294 if (ec) return fail("cannot create extracted path: " + ec.message());
295 fs::permissions(target, static_cast<fs::perms>(static_cast<unsigned int>(*mode) & 0777U), fs::perm_options::replace, ec);
296 if (ec) return fail("cannot set extracted permissions");
297 const auto padding = static_cast<std::size_t>((512 - (*size % 512)) % 512);
298 if (padding && !read_gzip(input, buffer.data(), padding)) return fail("truncated package padding");
299 }
300 if (gzclose(input) != Z_OK) { fs::remove_all(root, ec); return {false, "invalid gzip stream"}; }
301 return {true, {}};
302}
303
304} // namespace nah::archive
Result create(const fs::path &source, const fs::path &output)
Definition nah_archive.h:98
std::optional< std::pair< std::string, std::string > > split_ustar_path(const std::string &path)
Definition nah_archive.h:88
bool write_gzip(gzFile out, const void *data, std::size_t size)
Definition nah_archive.h:37
constexpr std::size_t max_entries
Definition nah_archive.h:30
std::optional< std::uint64_t > parse_octal(const unsigned char *value, std::size_t length)
Definition nah_archive.h:62
constexpr std::uintmax_t max_archive_size
Definition nah_archive.h:29
constexpr std::uintmax_t max_entry_size
Definition nah_archive.h:28
bool put_octal(unsigned char *field, std::size_t length, std::uint64_t value)
Definition nah_archive.h:74
Result extract(const fs::path &archive, const fs::path &destination)
bool read_gzip(gzFile in, void *data, std::size_t size)
Definition nah_archive.h:49