71 const std::string project_path = *parser.
GetString(
"project");
72 const std::string out_path = *parser.
GetString(
"out");
73 const bool overwrite = parser.
HasFlag(
"overwrite");
76 std::error_code fserr;
77 fs::path bundle_dir(project_path);
78 if (bundle_dir.extension() !=
".yazeproj") {
80 formatter.
AddField(
"error", std::string(
"Input must be a .yazeproj directory"));
81 return absl::InvalidArgumentError(
82 "project-bundle-pack: input must be a .yazeproj directory");
84 if (!fs::is_directory(bundle_dir, fserr) || fserr) {
87 absl::StrFormat(
"Not a directory: %s", project_path));
88 return absl::NotFoundError(
89 absl::StrFormat(
"project-bundle-pack: not a directory: %s",
94 if (fs::exists(out_path, fserr) && !overwrite) {
97 std::string(
"Output file exists (use --overwrite)"));
98 return absl::AlreadyExistsError(
99 "project-bundle-pack: output file exists; use --overwrite");
103 std::vector<fs::path> entries;
104 for (
auto it = fs::recursive_directory_iterator(
105 bundle_dir, fs::directory_options::skip_permission_denied, fserr);
106 it != fs::recursive_directory_iterator(); ++it) {
107 if (it->is_regular_file(fserr) && !fserr) {
108 entries.push_back(it->path());
114 std::memset(&zip, 0,
sizeof(zip));
116 if (!mz_zip_writer_init_file(&zip, out_path.c_str(), 0)) {
119 absl::StrFormat(
"Cannot create zip: %s", out_path));
120 return absl::InternalError(
121 absl::StrFormat(
"project-bundle-pack: cannot create zip: %s",
126 std::string bundle_name = bundle_dir.filename().string();
129 for (
const auto& entry : entries) {
131 fs::path rel = fs::relative(entry, bundle_dir.parent_path(), fserr);
134 std::string archive_name = rel.generic_string();
137 std::ifstream infile(entry, std::ios::binary | std::ios::ate);
138 if (!infile.is_open())
continue;
139 auto file_size = infile.tellg();
140 infile.seekg(0, std::ios::beg);
141 std::vector<char> buffer(
static_cast<size_t>(file_size));
142 infile.read(buffer.data(), file_size);
144 if (!mz_zip_writer_add_mem(&zip, archive_name.c_str(), buffer.data(),
145 buffer.size(), MZ_DEFAULT_COMPRESSION)) {
146 mz_zip_writer_end(&zip);
147 fs::remove(out_path, fserr);
150 absl::StrFormat(
"Failed to add: %s", archive_name));
151 return absl::InternalError(
152 absl::StrFormat(
"project-bundle-pack: failed to add: %s",
158 if (!mz_zip_writer_finalize_archive(&zip)) {
159 mz_zip_writer_end(&zip);
160 fs::remove(out_path, fserr);
162 formatter.
AddField(
"error", std::string(
"Failed to finalize archive"));
163 return absl::InternalError(
164 "project-bundle-pack: failed to finalize archive");
166 mz_zip_writer_end(&zip);
168 auto archive_size = fs::file_size(out_path, fserr);
171 formatter.
AddField(
"status", std::string(
"pass"));
172 formatter.
AddField(
"archive_path", out_path);
173 formatter.
AddField(
"bundle_name", bundle_name);
174 formatter.
AddField(
"files_packed", files_added);
176 static_cast<int>(fserr ? 0 : archive_size));
178 return absl::OkStatus();
226 const std::string archive_path = *parser.
GetString(
"archive");
227 const std::string out_dir = *parser.
GetString(
"out");
228 const bool overwrite = parser.
HasFlag(
"overwrite");
229 const bool keep_partial = parser.
HasFlag(
"keep-partial-output");
230 const bool dry_run = parser.
HasFlag(
"dry-run");
232 std::error_code fserr;
233 auto add_cleanup_field = [&]() {
235 formatter.
AddField(
"cleanup", std::string(
"skipped (dry-run)"));
240 std::string(
"skipped (--keep-partial-output)"));
243 std::error_code cleanup_err;
244 if (!fs::exists(out_dir, cleanup_err) || cleanup_err) {
248 ? std::string(
"failed: " + cleanup_err.message())
249 : std::string(
"not-needed"));
252 fs::remove_all(out_dir, cleanup_err);
253 formatter.
AddField(
"cleanup", cleanup_err
254 ? std::string(
"failed: " +
255 cleanup_err.message())
256 : std::string(
"removed"));
260 if (!fs::exists(archive_path, fserr) || fserr) {
263 absl::StrFormat(
"Archive not found: %s", archive_path));
264 return absl::NotFoundError(
265 absl::StrFormat(
"project-bundle-unpack: archive not found: %s",
270 if (fs::exists(out_dir, fserr) && !overwrite) {
273 std::string(
"Output exists (use --overwrite)"));
274 return absl::AlreadyExistsError(
275 "project-bundle-unpack: output exists; use --overwrite");
280 if (overwrite && !dry_run && fs::exists(out_dir, fserr)) {
281 fs::remove_all(out_dir, fserr);
286 std::memset(&zip, 0,
sizeof(zip));
288 if (!mz_zip_reader_init_file(&zip, archive_path.c_str(), 0)) {
291 absl::StrFormat(
"Cannot open zip: %s", archive_path));
292 return absl::InternalError(
293 absl::StrFormat(
"project-bundle-unpack: cannot open zip: %s",
297 int num_files =
static_cast<int>(mz_zip_reader_get_num_files(&zip));
298 int files_counted = 0;
299 int files_extracted = 0;
300 std::string detected_bundle_name;
301 bool mixed_roots =
false;
302 bool has_project_yaze =
false;
304 for (
int idx = 0; idx < num_files; ++idx) {
305 mz_zip_archive_file_stat file_stat;
306 if (!mz_zip_reader_file_stat(&zip,
static_cast<mz_uint
>(idx),
311 std::string entry_name(file_stat.m_filename);
314 if (mz_zip_reader_is_file_a_directory(
315 &zip,
static_cast<mz_uint
>(idx))) {
321 if (HasTraversalPathComponent(fs::path(entry_name))) {
322 mz_zip_reader_end(&zip);
325 absl::StrFormat(
"Path traversal rejected: %s",
328 return absl::FailedPreconditionError(
330 "project-bundle-unpack: path traversal rejected: %s",
334 if (fs::path(entry_name).is_absolute()) {
335 mz_zip_reader_end(&zip);
338 absl::StrFormat(
"Absolute path rejected: %s",
341 return absl::FailedPreconditionError(
343 "project-bundle-unpack: absolute path rejected: %s",
349 fs::path entry_path(entry_name);
350 auto root_component = *entry_path.begin();
351 if (detected_bundle_name.empty()) {
352 detected_bundle_name = root_component.string();
353 }
else if (detected_bundle_name != root_component.string()) {
357 fs::path relative_to_root = entry_path.lexically_relative(root_component);
358 if (relative_to_root ==
"project.yaze") {
359 has_project_yaze =
true;
371 fs::path dest = fs::path(out_dir) / entry_name;
372 fs::create_directories(dest.parent_path(), fserr);
374 size_t uncomp_size = 0;
375 void* data = mz_zip_reader_extract_to_heap(
376 &zip,
static_cast<mz_uint
>(idx), &uncomp_size, 0);
378 mz_zip_reader_end(&zip);
381 absl::StrFormat(
"Failed to extract: %s", entry_name));
383 return absl::InternalError(
384 absl::StrFormat(
"project-bundle-unpack: failed to extract: %s",
388 std::ofstream outfile(dest, std::ios::binary | std::ios::trunc);
389 if (!outfile.is_open()) {
391 mz_zip_reader_end(&zip);
394 absl::StrFormat(
"Cannot write: %s", dest.string()));
396 return absl::InternalError(
397 absl::StrFormat(
"project-bundle-unpack: cannot write: %s",
400 outfile.write(
static_cast<const char*
>(data), uncomp_size);
406 mz_zip_reader_end(&zip);
409 std::string bundle_path;
410 if (!detected_bundle_name.empty()) {
411 bundle_path = (fs::path(out_dir) / detected_bundle_name).
string();
415 bool is_valid_bundle =
false;
416 if (!bundle_path.empty()) {
417 fs::path bp(bundle_path);
422 bp.extension() ==
".yazeproj" && has_project_yaze && !mixed_roots;
425 bp.extension() ==
".yazeproj" && fs::is_directory(bp, fserr) &&
426 has_project_yaze && !mixed_roots;
430 formatter.
AddField(
"ok", is_valid_bundle);
432 is_valid_bundle ? std::string(
"pass")
433 : std::string(
"fail"));
434 formatter.
AddField(
"archive_path", archive_path);
435 formatter.
AddField(
"output_directory", out_dir);
436 formatter.
AddField(
"bundle_path", bundle_path);
437 formatter.
AddField(
"bundle_name", detected_bundle_name);
438 formatter.
AddField(
"files_counted", files_counted);
439 formatter.
AddField(
"files_extracted", files_extracted);
440 formatter.
AddField(
"is_valid_bundle", is_valid_bundle);
441 formatter.
AddField(
"dry_run", dry_run);
443 if (!is_valid_bundle) {
446 std::string(
"Archive contains multiple root folders"));
447 }
else if (!has_project_yaze) {
450 std::string(
"Bundle is missing required root file: project.yaze"));
454 return absl::FailedPreconditionError(
455 "project-bundle-unpack: extracted archive is not a valid "
458 return absl::OkStatus();