1 //
2 // Copyright (C) 2020 The Android Open Source Project
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16
17 #include <arpa/inet.h>
18 #include <errno.h>
19 #include <string.h>
20 #include <sys/stat.h>
21 #include <sys/types.h>
22 #include <unistd.h>
23
24 #include <iostream>
25 #include <limits>
26 #include <string>
27 #include <unordered_set>
28
29 #include <android-base/file.h>
30 #include <android-base/logging.h>
31 #include <android-base/unique_fd.h>
32 #include <bsdiff/bspatch.h>
33 #include <bzlib.h>
34 #include <gflags/gflags.h>
35 #include <libsnapshot/cow_writer.h>
36 #include <puffin/puffpatch.h>
37 #include <sparse/sparse.h>
38 #include <update_engine/update_metadata.pb.h>
39 #include <xz.h>
40 #include <ziparchive/zip_archive.h>
41
42 namespace android {
43 namespace snapshot {
44
45 using android::base::borrowed_fd;
46 using android::base::unique_fd;
47 using chromeos_update_engine::DeltaArchiveManifest;
48 using chromeos_update_engine::Extent;
49 using chromeos_update_engine::InstallOperation;
50 using chromeos_update_engine::PartitionUpdate;
51
52 static constexpr uint64_t kBlockSize = 4096;
53
54 DEFINE_string(source_tf, "", "Source target files (dir or zip file) for incremental payloads");
55 DEFINE_string(compression, "gz", "Compression type to use (none or gz)");
56 DEFINE_uint32(cluster_ops, 0, "Number of Cow Ops per cluster (0 or >1)");
57
MyLogger(android::base::LogId,android::base::LogSeverity severity,const char *,const char *,unsigned int,const char * message)58 void MyLogger(android::base::LogId, android::base::LogSeverity severity, const char*, const char*,
59 unsigned int, const char* message) {
60 if (severity == android::base::ERROR) {
61 fprintf(stderr, "%s\n", message);
62 } else {
63 fprintf(stdout, "%s\n", message);
64 }
65 }
66
ToLittleEndian(uint64_t value)67 uint64_t ToLittleEndian(uint64_t value) {
68 union {
69 uint64_t u64;
70 char bytes[8];
71 } packed;
72 packed.u64 = value;
73 std::swap(packed.bytes[0], packed.bytes[7]);
74 std::swap(packed.bytes[1], packed.bytes[6]);
75 std::swap(packed.bytes[2], packed.bytes[5]);
76 std::swap(packed.bytes[3], packed.bytes[4]);
77 return packed.u64;
78 }
79
80 class PayloadConverter final {
81 public:
PayloadConverter(const std::string & in_file,const std::string & out_dir)82 PayloadConverter(const std::string& in_file, const std::string& out_dir)
83 : in_file_(in_file), out_dir_(out_dir), source_tf_zip_(nullptr, &CloseArchive) {}
84
85 bool Run();
86
87 private:
88 bool OpenPayload();
89 bool OpenSourceTargetFiles();
90 bool ProcessPartition(const PartitionUpdate& update);
91 bool ProcessOperation(const InstallOperation& op);
92 bool ProcessZero(const InstallOperation& op);
93 bool ProcessCopy(const InstallOperation& op);
94 bool ProcessReplace(const InstallOperation& op);
95 bool ProcessDiff(const InstallOperation& op);
96 borrowed_fd OpenSourceImage();
97
98 std::string in_file_;
99 std::string out_dir_;
100 unique_fd in_fd_;
101 uint64_t payload_offset_ = 0;
102 DeltaArchiveManifest manifest_;
103 std::unordered_set<std::string> dap_;
104 unique_fd source_tf_fd_;
105 std::unique_ptr<ZipArchive, decltype(&CloseArchive)> source_tf_zip_;
106
107 // Updated during ProcessPartition().
108 std::string partition_name_;
109 std::unique_ptr<CowWriter> writer_;
110 unique_fd source_image_;
111 };
112
Run()113 bool PayloadConverter::Run() {
114 if (!OpenPayload()) {
115 return false;
116 }
117
118 if (manifest_.has_dynamic_partition_metadata()) {
119 const auto& dpm = manifest_.dynamic_partition_metadata();
120 for (const auto& group : dpm.groups()) {
121 for (const auto& partition : group.partition_names()) {
122 dap_.emplace(partition);
123 }
124 }
125 }
126
127 if (dap_.empty()) {
128 LOG(ERROR) << "No dynamic partitions found.";
129 return false;
130 }
131
132 if (!OpenSourceTargetFiles()) {
133 return false;
134 }
135
136 for (const auto& update : manifest_.partitions()) {
137 if (!ProcessPartition(update)) {
138 return false;
139 }
140 writer_ = nullptr;
141 source_image_.reset();
142 }
143 return true;
144 }
145
OpenSourceTargetFiles()146 bool PayloadConverter::OpenSourceTargetFiles() {
147 if (FLAGS_source_tf.empty()) {
148 return true;
149 }
150
151 source_tf_fd_.reset(open(FLAGS_source_tf.c_str(), O_RDONLY));
152 if (source_tf_fd_ < 0) {
153 LOG(ERROR) << "open failed: " << FLAGS_source_tf;
154 return false;
155 }
156
157 struct stat s;
158 if (fstat(source_tf_fd_.get(), &s) < 0) {
159 LOG(ERROR) << "fstat failed: " << FLAGS_source_tf;
160 return false;
161 }
162 if (S_ISDIR(s.st_mode)) {
163 return true;
164 }
165
166 // Otherwise, assume it's a zip file.
167 ZipArchiveHandle handle;
168 if (OpenArchiveFd(source_tf_fd_.get(), FLAGS_source_tf.c_str(), &handle, false)) {
169 LOG(ERROR) << "Could not open " << FLAGS_source_tf << " as a zip archive.";
170 return false;
171 }
172 source_tf_zip_.reset(handle);
173 return true;
174 }
175
ProcessPartition(const PartitionUpdate & update)176 bool PayloadConverter::ProcessPartition(const PartitionUpdate& update) {
177 auto partition_name = update.partition_name();
178 if (dap_.find(partition_name) == dap_.end()) {
179 // Skip non-DAP partitions.
180 return true;
181 }
182
183 auto path = out_dir_ + "/" + partition_name + ".cow";
184 unique_fd fd(open(path.c_str(), O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0644));
185 if (fd < 0) {
186 PLOG(ERROR) << "open failed: " << path;
187 return false;
188 }
189
190 CowOptions options;
191 options.block_size = kBlockSize;
192 options.compression = FLAGS_compression;
193 options.cluster_ops = FLAGS_cluster_ops;
194
195 writer_ = std::make_unique<CowWriter>(options);
196 if (!writer_->Initialize(std::move(fd))) {
197 LOG(ERROR) << "Unable to initialize COW writer";
198 return false;
199 }
200
201 partition_name_ = partition_name;
202
203 for (const auto& op : update.operations()) {
204 if (!ProcessOperation(op)) {
205 return false;
206 }
207 }
208
209 if (!writer_->Finalize()) {
210 LOG(ERROR) << "Unable to finalize COW for " << partition_name;
211 return false;
212 }
213 return true;
214 }
215
ProcessOperation(const InstallOperation & op)216 bool PayloadConverter::ProcessOperation(const InstallOperation& op) {
217 switch (op.type()) {
218 case InstallOperation::SOURCE_COPY:
219 return ProcessCopy(op);
220 case InstallOperation::BROTLI_BSDIFF:
221 case InstallOperation::PUFFDIFF:
222 return ProcessDiff(op);
223 case InstallOperation::REPLACE:
224 case InstallOperation::REPLACE_XZ:
225 case InstallOperation::REPLACE_BZ:
226 return ProcessReplace(op);
227 case InstallOperation::ZERO:
228 return ProcessZero(op);
229 default:
230 LOG(ERROR) << "Unsupported op: " << (int)op.type();
231 return false;
232 }
233 return true;
234 }
235
ProcessZero(const InstallOperation & op)236 bool PayloadConverter::ProcessZero(const InstallOperation& op) {
237 for (const auto& extent : op.dst_extents()) {
238 if (!writer_->AddZeroBlocks(extent.start_block(), extent.num_blocks())) {
239 LOG(ERROR) << "Could not add zero operation";
240 return false;
241 }
242 }
243 return true;
244 }
245
246 template <typename T>
SizeOfAllExtents(const T & extents)247 static uint64_t SizeOfAllExtents(const T& extents) {
248 uint64_t total = 0;
249 for (const auto& extent : extents) {
250 total += extent.num_blocks() * kBlockSize;
251 }
252 return total;
253 }
254
255 class PuffInputStream final : public puffin::StreamInterface {
256 public:
PuffInputStream(uint8_t * buffer,size_t length)257 PuffInputStream(uint8_t* buffer, size_t length) : buffer_(buffer), length_(length), pos_(0) {}
258
GetSize(uint64_t * size) const259 bool GetSize(uint64_t* size) const override {
260 *size = length_;
261 return true;
262 }
GetOffset(uint64_t * offset) const263 bool GetOffset(uint64_t* offset) const override {
264 *offset = pos_;
265 return true;
266 }
Seek(uint64_t offset)267 bool Seek(uint64_t offset) override {
268 if (offset > length_) return false;
269 pos_ = offset;
270 return true;
271 }
Read(void * buffer,size_t length)272 bool Read(void* buffer, size_t length) override {
273 if (length_ - pos_ < length) return false;
274 memcpy(buffer, buffer_ + pos_, length);
275 pos_ += length;
276 return true;
277 }
Write(const void *,size_t)278 bool Write(const void*, size_t) override { return false; }
Close()279 bool Close() override { return true; }
280
281 private:
282 uint8_t* buffer_;
283 size_t length_;
284 size_t pos_;
285 };
286
287 class PuffOutputStream final : public puffin::StreamInterface {
288 public:
PuffOutputStream(std::vector<uint8_t> & stream)289 PuffOutputStream(std::vector<uint8_t>& stream) : stream_(stream), pos_(0) {}
290
GetSize(uint64_t * size) const291 bool GetSize(uint64_t* size) const override {
292 *size = stream_.size();
293 return true;
294 }
GetOffset(uint64_t * offset) const295 bool GetOffset(uint64_t* offset) const override {
296 *offset = pos_;
297 return true;
298 }
Seek(uint64_t offset)299 bool Seek(uint64_t offset) override {
300 if (offset > stream_.size()) {
301 return false;
302 }
303 pos_ = offset;
304 return true;
305 }
Read(void * buffer,size_t length)306 bool Read(void* buffer, size_t length) override {
307 if (stream_.size() - pos_ < length) {
308 return false;
309 }
310 memcpy(buffer, &stream_[0] + pos_, length);
311 pos_ += length;
312 return true;
313 }
Write(const void * buffer,size_t length)314 bool Write(const void* buffer, size_t length) override {
315 auto remaining = stream_.size() - pos_;
316 if (remaining < length) {
317 stream_.resize(stream_.size() + (length - remaining));
318 }
319 memcpy(&stream_[0] + pos_, buffer, length);
320 pos_ += length;
321 return true;
322 }
Close()323 bool Close() override { return true; }
324
325 private:
326 std::vector<uint8_t>& stream_;
327 size_t pos_;
328 };
329
ProcessDiff(const InstallOperation & op)330 bool PayloadConverter::ProcessDiff(const InstallOperation& op) {
331 auto source_image = OpenSourceImage();
332 if (source_image < 0) {
333 return false;
334 }
335
336 uint64_t src_length = SizeOfAllExtents(op.src_extents());
337 auto src = std::make_unique<uint8_t[]>(src_length);
338 size_t src_pos = 0;
339
340 // Read source bytes.
341 for (const auto& extent : op.src_extents()) {
342 uint64_t offset = extent.start_block() * kBlockSize;
343 if (lseek(source_image.get(), offset, SEEK_SET) < 0) {
344 PLOG(ERROR) << "lseek source image failed";
345 return false;
346 }
347
348 uint64_t size = extent.num_blocks() * kBlockSize;
349 CHECK(src_length - src_pos >= size);
350 if (!android::base::ReadFully(source_image, src.get() + src_pos, size)) {
351 PLOG(ERROR) << "read source image failed";
352 return false;
353 }
354 src_pos += size;
355 }
356 CHECK(src_pos == src_length);
357
358 // Read patch bytes.
359 auto patch = std::make_unique<uint8_t[]>(op.data_length());
360 if (lseek(in_fd_.get(), payload_offset_ + op.data_offset(), SEEK_SET) < 0) {
361 PLOG(ERROR) << "lseek payload failed";
362 return false;
363 }
364 if (!android::base::ReadFully(in_fd_, patch.get(), op.data_length())) {
365 PLOG(ERROR) << "read payload failed";
366 return false;
367 }
368
369 std::vector<uint8_t> dest(SizeOfAllExtents(op.dst_extents()));
370
371 // Apply the diff.
372 if (op.type() == InstallOperation::BROTLI_BSDIFF) {
373 size_t dest_pos = 0;
374 auto sink = [&](const uint8_t* data, size_t length) -> size_t {
375 CHECK(dest.size() - dest_pos >= length);
376 memcpy(&dest[dest_pos], data, length);
377 dest_pos += length;
378 return length;
379 };
380 if (int rv = bsdiff::bspatch(src.get(), src_pos, patch.get(), op.data_length(), sink)) {
381 LOG(ERROR) << "bspatch failed, error code " << rv;
382 return false;
383 }
384 } else if (op.type() == InstallOperation::PUFFDIFF) {
385 auto src_stream = std::make_unique<PuffInputStream>(src.get(), src_length);
386 auto dest_stream = std::make_unique<PuffOutputStream>(dest);
387 bool ok = PuffPatch(std::move(src_stream), std::move(dest_stream), patch.get(),
388 op.data_length());
389 if (!ok) {
390 LOG(ERROR) << "puffdiff operation failed to apply";
391 return false;
392 }
393 } else {
394 LOG(ERROR) << "unsupported diff operation: " << op.type();
395 return false;
396 }
397
398 // Write the final blocks to the COW.
399 size_t dest_pos = 0;
400 for (const auto& extent : op.dst_extents()) {
401 uint64_t size = extent.num_blocks() * kBlockSize;
402 CHECK(dest.size() - dest_pos >= size);
403
404 if (!writer_->AddRawBlocks(extent.start_block(), &dest[dest_pos], size)) {
405 return false;
406 }
407 dest_pos += size;
408 }
409 return true;
410 }
411
OpenSourceImage()412 borrowed_fd PayloadConverter::OpenSourceImage() {
413 if (source_image_ >= 0) {
414 return source_image_;
415 }
416
417 unique_fd unzip_fd;
418
419 auto local_path = "IMAGES/" + partition_name_ + ".img";
420 if (source_tf_zip_) {
421 {
422 TemporaryFile tmp;
423 if (tmp.fd < 0) {
424 PLOG(ERROR) << "mkstemp failed";
425 return -1;
426 }
427 unzip_fd.reset(tmp.release());
428 }
429
430 ZipEntry64 entry;
431 if (FindEntry(source_tf_zip_.get(), local_path, &entry)) {
432 LOG(ERROR) << "not found in archive: " << local_path;
433 return -1;
434 }
435 if (ExtractEntryToFile(source_tf_zip_.get(), &entry, unzip_fd.get())) {
436 LOG(ERROR) << "could not extract " << local_path;
437 return -1;
438 }
439 if (lseek(unzip_fd.get(), 0, SEEK_SET) < 0) {
440 PLOG(ERROR) << "lseek failed";
441 return -1;
442 }
443 } else if (source_tf_fd_ >= 0) {
444 unzip_fd.reset(openat(source_tf_fd_.get(), local_path.c_str(), O_RDONLY));
445 if (unzip_fd < 0) {
446 PLOG(ERROR) << "open failed: " << FLAGS_source_tf << "/" << local_path;
447 return -1;
448 }
449 } else {
450 LOG(ERROR) << "No source target files package was specified; need -source_tf";
451 return -1;
452 }
453
454 std::unique_ptr<struct sparse_file, decltype(&sparse_file_destroy)> s(
455 sparse_file_import(unzip_fd.get(), false, false), &sparse_file_destroy);
456 if (s) {
457 TemporaryFile tmp;
458 if (tmp.fd < 0) {
459 PLOG(ERROR) << "mkstemp failed";
460 return -1;
461 }
462 if (sparse_file_write(s.get(), tmp.fd, false, false, false) < 0) {
463 LOG(ERROR) << "sparse_file_write failed";
464 return -1;
465 }
466 source_image_.reset(tmp.release());
467 } else {
468 source_image_ = std::move(unzip_fd);
469 }
470 return source_image_;
471 }
472
473 template <typename ContainerType>
474 class ExtentIter final {
475 public:
ExtentIter(const ContainerType & container)476 ExtentIter(const ContainerType& container)
477 : iter_(container.cbegin()), end_(container.cend()), dst_index_(0) {}
478
GetNext(uint64_t * block)479 bool GetNext(uint64_t* block) {
480 while (iter_ != end_) {
481 if (dst_index_ < iter_->num_blocks()) {
482 break;
483 }
484 iter_++;
485 dst_index_ = 0;
486 }
487 if (iter_ == end_) {
488 return false;
489 }
490 *block = iter_->start_block() + dst_index_;
491 dst_index_++;
492 return true;
493 }
494
495 private:
496 typename ContainerType::const_iterator iter_;
497 typename ContainerType::const_iterator end_;
498 uint64_t dst_index_;
499 };
500
ProcessCopy(const InstallOperation & op)501 bool PayloadConverter::ProcessCopy(const InstallOperation& op) {
502 ExtentIter dst_blocks(op.dst_extents());
503
504 for (const auto& extent : op.src_extents()) {
505 for (uint64_t i = 0; i < extent.num_blocks(); i++) {
506 uint64_t src_block = extent.start_block() + i;
507 uint64_t dst_block;
508 if (!dst_blocks.GetNext(&dst_block)) {
509 LOG(ERROR) << "SOURCE_COPY contained mismatching extents";
510 return false;
511 }
512 if (src_block == dst_block) continue;
513 if (!writer_->AddCopy(dst_block, src_block)) {
514 LOG(ERROR) << "Could not add copy operation";
515 return false;
516 }
517 }
518 }
519 return true;
520 }
521
ProcessReplace(const InstallOperation & op)522 bool PayloadConverter::ProcessReplace(const InstallOperation& op) {
523 auto buffer_size = op.data_length();
524 auto buffer = std::make_unique<char[]>(buffer_size);
525 uint64_t offs = payload_offset_ + op.data_offset();
526 if (lseek(in_fd_.get(), offs, SEEK_SET) < 0) {
527 PLOG(ERROR) << "lseek " << offs << " failed";
528 return false;
529 }
530 if (!android::base::ReadFully(in_fd_, buffer.get(), buffer_size)) {
531 PLOG(ERROR) << "read " << buffer_size << " bytes from offset " << offs << "failed";
532 return false;
533 }
534
535 uint64_t dst_size = 0;
536 for (const auto& extent : op.dst_extents()) {
537 dst_size += extent.num_blocks() * kBlockSize;
538 }
539
540 if (op.type() == InstallOperation::REPLACE_BZ) {
541 auto tmp = std::make_unique<char[]>(dst_size);
542
543 uint32_t actual_size;
544 if (dst_size > std::numeric_limits<typeof(actual_size)>::max()) {
545 LOG(ERROR) << "too many bytes to decompress: " << dst_size;
546 return false;
547 }
548 actual_size = static_cast<uint32_t>(dst_size);
549
550 auto rv = BZ2_bzBuffToBuffDecompress(tmp.get(), &actual_size, buffer.get(), buffer_size, 0,
551 0);
552 if (rv) {
553 LOG(ERROR) << "bz2 decompress failed: " << rv;
554 return false;
555 }
556 if (actual_size != dst_size) {
557 LOG(ERROR) << "bz2 returned " << actual_size << " bytes, expected " << dst_size;
558 return false;
559 }
560 buffer = std::move(tmp);
561 buffer_size = dst_size;
562 } else if (op.type() == InstallOperation::REPLACE_XZ) {
563 constexpr uint32_t kXzMaxDictSize = 64 * 1024 * 1024;
564
565 if (dst_size > std::numeric_limits<size_t>::max()) {
566 LOG(ERROR) << "too many bytes to decompress: " << dst_size;
567 return false;
568 }
569
570 std::unique_ptr<struct xz_dec, decltype(&xz_dec_end)> s(
571 xz_dec_init(XZ_DYNALLOC, kXzMaxDictSize), xz_dec_end);
572 if (!s) {
573 LOG(ERROR) << "xz_dec_init failed";
574 return false;
575 }
576
577 auto tmp = std::make_unique<char[]>(dst_size);
578
579 struct xz_buf args;
580 args.in = reinterpret_cast<const uint8_t*>(buffer.get());
581 args.in_pos = 0;
582 args.in_size = buffer_size;
583 args.out = reinterpret_cast<uint8_t*>(tmp.get());
584 args.out_pos = 0;
585 args.out_size = dst_size;
586
587 auto rv = xz_dec_run(s.get(), &args);
588 if (rv != XZ_STREAM_END) {
589 LOG(ERROR) << "xz decompress failed: " << (int)rv;
590 return false;
591 }
592 buffer = std::move(tmp);
593 buffer_size = dst_size;
594 }
595
596 uint64_t buffer_pos = 0;
597 for (const auto& extent : op.dst_extents()) {
598 uint64_t extent_size = extent.num_blocks() * kBlockSize;
599 if (buffer_size - buffer_pos < extent_size) {
600 LOG(ERROR) << "replace op ran out of input buffer";
601 return false;
602 }
603 if (!writer_->AddRawBlocks(extent.start_block(), buffer.get() + buffer_pos, extent_size)) {
604 LOG(ERROR) << "failed to add raw blocks from replace op";
605 return false;
606 }
607 buffer_pos += extent_size;
608 }
609 return true;
610 }
611
OpenPayload()612 bool PayloadConverter::OpenPayload() {
613 in_fd_.reset(open(in_file_.c_str(), O_RDONLY));
614 if (in_fd_ < 0) {
615 PLOG(ERROR) << "open " << in_file_;
616 return false;
617 }
618
619 char magic[4];
620 if (!android::base::ReadFully(in_fd_, magic, sizeof(magic))) {
621 PLOG(ERROR) << "read magic";
622 return false;
623 }
624 if (std::string(magic, sizeof(magic)) != "CrAU") {
625 LOG(ERROR) << "Invalid magic in " << in_file_;
626 return false;
627 }
628
629 uint64_t version;
630 uint64_t manifest_size;
631 uint32_t manifest_signature_size = 0;
632 if (!android::base::ReadFully(in_fd_, &version, sizeof(version))) {
633 PLOG(ERROR) << "read version";
634 return false;
635 }
636 version = ToLittleEndian(version);
637 if (version < 2) {
638 LOG(ERROR) << "Only payload version 2 or higher is supported.";
639 return false;
640 }
641
642 if (!android::base::ReadFully(in_fd_, &manifest_size, sizeof(manifest_size))) {
643 PLOG(ERROR) << "read manifest_size";
644 return false;
645 }
646 manifest_size = ToLittleEndian(manifest_size);
647 if (!android::base::ReadFully(in_fd_, &manifest_signature_size,
648 sizeof(manifest_signature_size))) {
649 PLOG(ERROR) << "read manifest_signature_size";
650 return false;
651 }
652 manifest_signature_size = ntohl(manifest_signature_size);
653
654 auto manifest = std::make_unique<uint8_t[]>(manifest_size);
655 if (!android::base::ReadFully(in_fd_, manifest.get(), manifest_size)) {
656 PLOG(ERROR) << "read manifest";
657 return false;
658 }
659
660 // Skip past manifest signature.
661 auto offs = lseek(in_fd_, manifest_signature_size, SEEK_CUR);
662 if (offs < 0) {
663 PLOG(ERROR) << "lseek failed";
664 return false;
665 }
666 payload_offset_ = offs;
667
668 if (!manifest_.ParseFromArray(manifest.get(), manifest_size)) {
669 LOG(ERROR) << "could not parse manifest";
670 return false;
671 }
672 return true;
673 }
674
675 } // namespace snapshot
676 } // namespace android
677
main(int argc,char ** argv)678 int main(int argc, char** argv) {
679 android::base::InitLogging(argv, android::snapshot::MyLogger);
680 gflags::SetUsageMessage("Convert OTA payload to a Virtual A/B COW");
681 int arg_start = gflags::ParseCommandLineFlags(&argc, &argv, false);
682
683 xz_crc32_init();
684
685 if (argc - arg_start != 2) {
686 std::cerr << "Usage: [options] <payload.bin> <out-dir>\n";
687 return 1;
688 }
689
690 android::snapshot::PayloadConverter pc(argv[arg_start], argv[arg_start + 1]);
691 return pc.Run() ? 0 : 1;
692 }
693