98inline Result create(
const fs::path& source,
const fs::path& output) {
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"};
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);
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);
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};
117 const bool directory = fs::is_directory(status);
118 const auto size = directory ? 0 : fs::file_size(it->path(), ec);
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"};
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; });
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"};
143 auto fail = [&](
const std::string& message) {
145 fs::remove(temporary, ec);
146 return Result{
false, message};
148 std::array<unsigned char, 512> block{};
149 std::array<char, 64 * 1024> buffer{};
150 for (
const auto& entry : entries) {
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)) ||
157 !
put_octal(block.data() + 124, 12, entry.size) || !
put_octal(block.data() + 136, 12, 0)) {
158 return fail(
"package metadata is too large");
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");
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);
180 const auto padding =
static_cast<std::size_t
>((512 - (entry.size % 512)) % 512);
182 if (padding && !
write_gzip(out, block.data(), padding))
return fail(
"failed to write package padding");
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"};
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"};
197 fs::rename(temporary, absolute_output, ec);
198 if (ec) { fs::remove(temporary, ec);
return {
false,
"cannot activate output package"}; }
203inline Result extract(
const fs::path& archive,
const fs::path& destination) {
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"}; }
212 auto fail = [&](
const std::string& message) {
214 fs::remove_all(root, ec);
215 return Result{
false, message};
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;
223 if (!
read_gzip(input, header.data(), header.size()))
return fail(
"truncated package header");
225 for (
const auto byte : header) zero = zero &&
byte == 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");
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");
239 if (++entries >
max_entries)
return fail(
"package contains too many entries");
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");
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);
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);
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");
272 const unsigned char type = header[156];
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);
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");
292 return fail(
"links and special files are not supported in packages");
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");
300 if (gzclose(input) != Z_OK) { fs::remove_all(root, ec);
return {
false,
"invalid gzip stream"}; }