Added a stout utility header which interfaces with libarchive. This archiver utility can be invoked to extract an archive with several supported compression formats, including `.zip`, `.gz`, `.bz2`, and `.xz` formats.
Review: https://reviews.apache.org/r/67065/ Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/894d06e8 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/894d06e8 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/894d06e8 Branch: refs/heads/master Commit: 894d06e8c50d5080ea0a0e43400adc34b06717ad Parents: 106d6e8 Author: John Kordich <[email protected]> Authored: Mon Jun 4 13:44:20 2018 -0700 Committer: Joseph Wu <[email protected]> Committed: Mon Jun 4 15:50:56 2018 -0700 ---------------------------------------------------------------------- 3rdparty/stout/CMakeLists.txt | 1 + 3rdparty/stout/Makefile.am | 11 + 3rdparty/stout/include/Makefile.am | 1 + 3rdparty/stout/include/stout/archiver.hpp | 172 +++++++++ 3rdparty/stout/tests/CMakeLists.txt | 1 + 3rdparty/stout/tests/archiver_tests.cpp | 493 +++++++++++++++++++++++++ 6 files changed, 679 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/894d06e8/3rdparty/stout/CMakeLists.txt ---------------------------------------------------------------------- diff --git a/3rdparty/stout/CMakeLists.txt b/3rdparty/stout/CMakeLists.txt index 24a1f0a..9cbb6f2 100644 --- a/3rdparty/stout/CMakeLists.txt +++ b/3rdparty/stout/CMakeLists.txt @@ -29,6 +29,7 @@ target_link_libraries( picojson protobuf Threads::Threads + libarchive zlib $<$<PLATFORM_ID:Linux>:rt dl svn> $<$<PLATFORM_ID:Darwin>:svn> http://git-wip-us.apache.org/repos/asf/mesos/blob/894d06e8/3rdparty/stout/Makefile.am ---------------------------------------------------------------------- diff --git a/3rdparty/stout/Makefile.am b/3rdparty/stout/Makefile.am index ef22a02..5b922af 100644 --- a/3rdparty/stout/Makefile.am +++ b/3rdparty/stout/Makefile.am @@ -53,6 +53,7 @@ GLOG = $(BUNDLED_DIR)/glog-$(GLOG_VERSION) GOOGLETEST = $(BUNDLED_DIR)/googletest-release-$(GOOGLETEST_VERSION) GMOCK = $(GOOGLETEST)/googlemock GTEST = $(GOOGLETEST)/googletest +LIBARCHIVE = $(BUNDLED_DIR)/libarchive-$(LIBARCHIVE_VERSION) PROTOBUF = $(BUNDLED_DIR)/protobuf-$(PROTOBUF_VERSION) PICOJSON = $(BUNDLED_DIR)/picojson-$(PICOJSON_VERSION) @@ -97,6 +98,13 @@ PICOJSON_INCLUDE_FLAGS = \ -DPICOJSON_USE_INT64 \ -D__STDC_FORMAT_MACROS +if WITH_BUNDLED_LIBARCHIVE +LIBARCHIVE_INCLUDE_FLAGS = -I$(LIBARCHIVE)/libarchive +LIB_LIBARCHIVE = $(LIBARCHIVE)/.libs/libarchive.la +else +LIB_LIBARCHIVE = -larchive +endif + if WITH_BUNDLED_PICOJSON PICOJSON_INCLUDE_FLAGS += -I$(PICOJSON) BUNDLED_DEPS += $(PICOJSON)-stamp @@ -124,6 +132,7 @@ check_PROGRAMS = stout-tests stout_tests_SOURCES = \ tests/adaptor_tests.cpp \ + tests/archiver_tests.cpp \ tests/base64_tests.cpp \ tests/bits_tests.cpp \ tests/boundedhashmap_tests.cpp \ @@ -189,6 +198,7 @@ stout_tests_CPPFLAGS = \ $(GLOG_INCLUDE_FLAGS) \ $(GMOCK_INCLUDE_FLAGS) \ $(GTEST_INCLUDE_FLAGS) \ + $(LIBARCHIVE_INCLUDE_FLAGS) \ $(PICOJSON_INCLUDE_FLAGS) \ $(PROTOBUF_INCLUDE_FLAGS) \ $(AM_CPPFLAGS) @@ -199,6 +209,7 @@ stout_tests_CPPFLAGS = \ stout_tests_LDADD = \ $(LIB_GMOCK) \ $(LIB_GLOG) \ + $(LIB_LIBARCHIVE) \ $(LIB_PROTOBUF) \ -lsvn_subr-1 \ -lsvn_delta-1 \ http://git-wip-us.apache.org/repos/asf/mesos/blob/894d06e8/3rdparty/stout/include/Makefile.am ---------------------------------------------------------------------- diff --git a/3rdparty/stout/include/Makefile.am b/3rdparty/stout/include/Makefile.am index e0097c4..0a4ea7b 100644 --- a/3rdparty/stout/include/Makefile.am +++ b/3rdparty/stout/include/Makefile.am @@ -14,6 +14,7 @@ nobase_include_HEADERS = \ stout/abort.hpp \ stout/adaptor.hpp \ + stout/archiver.hpp \ stout/assert.hpp \ stout/attributes.hpp \ stout/base64.hpp \ http://git-wip-us.apache.org/repos/asf/mesos/blob/894d06e8/3rdparty/stout/include/stout/archiver.hpp ---------------------------------------------------------------------- diff --git a/3rdparty/stout/include/stout/archiver.hpp b/3rdparty/stout/include/stout/archiver.hpp new file mode 100644 index 0000000..f66da36 --- /dev/null +++ b/3rdparty/stout/include/stout/archiver.hpp @@ -0,0 +1,172 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __STOUT_ARCHIVER_HPP__ +#define __STOUT_ARCHIVER_HPP__ + +#include <archive.h> +#include <archive_entry.h> + +#include <stout/nothing.hpp> +#include <stout/path.hpp> +#include <stout/try.hpp> + +#include <stout/os/close.hpp> +#include <stout/os/int_fd.hpp> +#include <stout/os/open.hpp> + +namespace archiver { + +// Extracts the archive in source to the destination folder (if specified). +// If destination is not specified, it will use the working directory. +// Flags can be any of (or together): +// ARCHIVE_EXTRACT_ACL +// ARCHIVE_EXTRACT_FFLAGS +// ARCHIVE_EXTRACT_PERM +// ARCHIVE_EXTRACT_TIME +inline Try<Nothing> extract( + const std::string& source, + const std::string& destination, + const int flags = ARCHIVE_EXTRACT_TIME) +{ + // Get references to libarchive for reading/handling a compressed file. + std::unique_ptr<struct archive, std::function<void(struct archive*)>> reader( + archive_read_new(), + [](struct archive* p) { + archive_read_close(p); + archive_read_free(p); + }); + + // Enable auto-detection of the archive type/format. + archive_read_support_format_all(reader.get()); + archive_read_support_filter_all(reader.get()); + + std::unique_ptr<struct archive, std::function<void(struct archive*)>> writer( + archive_write_disk_new(), + [](struct archive* p) { + archive_write_close(p); + archive_write_free(p); + }); + + archive_write_disk_set_options(writer.get(), flags); + archive_write_disk_set_standard_lookup(writer.get()); + + // Open the compressed file for decompression. + // + // We do not use libarchive to open the file to ensure we have proper + // file descriptor and long path handling on both Posix and Windows. + Try<int_fd> fd = os::open(source, O_RDONLY | O_CLOEXEC); + if (fd.isError()) { + return Error(fd.error()); + } + +#ifdef __WINDOWS__ + int fd_real = fd->crt(); +#else + int fd_real = fd.get(); +#endif + + // Ensure the CRT file descriptor is closed when leaving scope. + // NOTE: On Windows, we need to explicitly allocate a CRT file descriptor + // because libarchive requires it. Once the CRT fd is allocated, it must + // be closed with _close instead of os::close. + struct Closer + { + int fd_value; +#ifdef __WINDOWS__ + ~Closer() { ::_close(fd_value); } +#else + ~Closer() { os::close(fd_value); } +#endif + } closer = {fd_real}; + + const size_t archive_block_size = 10240; + int result = archive_read_open_fd(reader.get(), fd_real, archive_block_size); + if (result != ARCHIVE_OK) { + return Error(archive_error_string(reader.get())); + } + + // Loop through file headers in the archive stream. + while (true) { + // Read the next header from the input stream. + struct archive_entry* entry; + result = archive_read_next_header(reader.get(), &entry); + + if (result == ARCHIVE_EOF) { + break; + } else if (result <= ARCHIVE_WARN) { + return Error( + std::string("Failed to read archive header: ") + + archive_error_string(reader.get())); + } + + // If a destination path is specified, update the entry to reflect it. + // We assume the destination directory already exists. + if (!destination.empty()) { + std::string path = path::join(destination, archive_entry_pathname(entry)); + archive_entry_update_pathname_utf8(entry, path.c_str()); + } + + result = archive_write_header(writer.get(), entry); + if (result <= ARCHIVE_WARN) { + return Error( + std::string("Failed to write archive header: ") + + archive_error_string(writer.get())); + } + + if (archive_entry_size(entry) > 0) { + const void* buff; + size_t size; +#if ARCHIVE_VERSION_NUMBER >= 3000000 + int64_t offset; +#else + off_t offset; +#endif + + // Loop through file data blocks until end of file. + while (true) { + result = archive_read_data_block(reader.get(), &buff, &size, &offset); + if (result == ARCHIVE_EOF) { + break; + } else if (result <= ARCHIVE_WARN) { + return Error( + std::string("Failed to read archive data block: ") + + archive_error_string(reader.get())); + } + + result = archive_write_data_block(writer.get(), buff, size, offset); + if (result <= ARCHIVE_WARN) { + return Error( + std::string("Failed to write archive data block: ") + + archive_error_string(writer.get())); + } + } + } + + result = archive_write_finish_entry(writer.get()); + if (result <= ARCHIVE_WARN) { + return Error( + std::string("Failed to write archive finish entry: ") + + archive_error_string(writer.get())); + } + } + + return Nothing(); +} + +} // namespace archiver { + +#endif // __STOUT_ARCHIVER_HPP__ http://git-wip-us.apache.org/repos/asf/mesos/blob/894d06e8/3rdparty/stout/tests/CMakeLists.txt ---------------------------------------------------------------------- diff --git a/3rdparty/stout/tests/CMakeLists.txt b/3rdparty/stout/tests/CMakeLists.txt index 86111a8..7c644b7 100644 --- a/3rdparty/stout/tests/CMakeLists.txt +++ b/3rdparty/stout/tests/CMakeLists.txt @@ -17,6 +17,7 @@ # STOUT TESTS. ############## set(STOUT_ROOT_TESTS_SRC + archiver_tests.cpp base64_tests.cpp bits_tests.cpp bytes_tests.cpp http://git-wip-us.apache.org/repos/asf/mesos/blob/894d06e8/3rdparty/stout/tests/archiver_tests.cpp ---------------------------------------------------------------------- diff --git a/3rdparty/stout/tests/archiver_tests.cpp b/3rdparty/stout/tests/archiver_tests.cpp new file mode 100644 index 0000000..fc8db56 --- /dev/null +++ b/3rdparty/stout/tests/archiver_tests.cpp @@ -0,0 +1,493 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +#include <string> + +#ifdef __WINDOWS__ +#include <stout/windows.hpp> +#endif + +#include <stout/archiver.hpp> +#include <stout/base64.hpp> +#include <stout/gtest.hpp> +#include <stout/os.hpp> + +#include <stout/os/write.hpp> + +#include <stout/tests/utils.hpp> + +using std::string; + + +class ArchiverTest : public TemporaryDirectoryTest {}; + +// No input file should return some error, not read from stdin. +TEST_F(ArchiverTest, ExtractEmptyInputFile) +{ + EXPECT_ERROR(archiver::extract("", "")); +} + +// File that does not exist should return some error. +TEST_F(ArchiverTest, ExtractInputFileNotFound) +{ + // Construct a temporary file path that is guarenteed unique. + Try<string> dir = os::mkdtemp(path::join(sandbox.get(), "XXXXXX")); + ASSERT_SOME(dir); + + string path = path::join(dir.get(), "ThisFileDoesNotExist"); + + EXPECT_ERROR(archiver::extract(path, "")); +} + +TEST_F(ArchiverTest, ExtractTarGzFile) +{ + // Construct a hello.tar.gz file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 22 22 2018-02-21 10:06 hello Howdy there, partner!\n + // -------- ------- ------- ------ + // 22 22 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "H4sICE61jVoAA2hlbGxvLnRhcgDtzjEOwjAQRNGtOcXSU9hx7FyBa0RgK0IR" + "RsYIcfsEaGhQqggh/VfsFLPFDHEcs6zLzEJon2k7bz7zrQliXdM6Nx/vxVgb" + "uiBqVt71crvWvqjKKaZ0yOnr31L/p/b5fnxoHWKJO730pZ5j2W5+vQoAAAAA" + "AAAAAAAAAAAAsGQC2DPIjgAoAAA=").get())); + + // Note: The file does NOT have a .tar.gz extension. We could rename + // it, but libarchive doesn't care about extensions. It determines + // the format from the contents of the file. So this is tested here + // as well. + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Howdy there, partner!\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractTarFile) +{ + // Construct a hello.tar file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // The .tar file, since not compressed, is long. So go through some + // pains to construct the contents to the file programmatically. + // + // We could skip the .tar file test, but it's worth having it. + // + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 10240 10240 2018-02-21 10:06 hello Howdy there, partner (.tar)!\n + // -------- ------- ------- ------ + // 10240 10240 1 file + + string tarContents = + "aGVsbG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAADAwMDA2NjQAMDAwMTc1MAAwMDAxNzUwADAwMDAwMDAwMDM1" + "ADEzMjQ1MTA2NTE1ADAxMTY3NAAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhciAgAGplZmZj" + "b2YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAamVmZmNvZgAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAABIb3dkeSB0aGVyZSwgcGFydG5lciEgKC50YXIp" + "CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + + for (int i = 0; i < 214; i++) + { + tarContents += + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + } + + tarContents += "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; + + // Now write out the .tar file, extract contents, and verify results + ASSERT_SOME(os::write(path.get(), base64::decode(tarContents).get())); + + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Howdy there, partner! (.tar)\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractZipFile) +{ + // Construct a hello.zip file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 189 189 2018-02-26 15:06 hello Howdy there, partner! (.zip)\n + // -------- ------- ------- ------ + // 189 189 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "UEsDBAoAAAAAAMZ4WkxFOXeVHQAAAB0AAAAFABwAaGVsbG9VVAkAA+SSlFrk" + "kpRadXgLAAEE6AMAAAToAwAASG93ZHkgdGhlcmUsIHBhcnRuZXIhICguemlw" + "KQpQSwECHgMKAAAAAADGeFpMRTl3lR0AAAAdAAAABQAYAAAAAAABAAAAtIEA" + "AAAAaGVsbG9VVAUAA+SSlFp1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBL" + "AAAAXAAAAAAA").get())); + + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Howdy there, partner! (.zip)\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractInvalidZipFile) +{ + // Construct a hello.zip file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Write broken zip to file [bad CRC 440a6aa5 (should be af083b2d)]. + // + // Length Date Time CRC expected CRC actual Name Content + // -------- ---------- ----- ------------ ---------- ---- ------ + // 12 2016-03-19 10:08 af083b2d 440a6aa5 world hello hello\n + // -------- ------- ------ + // 12 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "UEsDBAoAAAAAABBRc0gtOwivDAAAAAwAAAAFABwAd29ybG9VVAkAAxAX7VYQ" + "F+1WdXgLAAEE6AMAAARkAAAAaGVsbG8gaGVsbG8KUEsBAh4DCgAAAAAAEFFz" + "SC07CK8MAAAADAAAAAUAGAAAAAAAAQAAAKSBAAAAAHdvcmxkVVQFAAMQF+1W" + "dXgLAAEE6AMAAARkAAAAUEsFBgAAAAABAAEASwAAAEsAAAAAAA==").get())); + + EXPECT_ERROR(archiver::extract(path.get(), "")); +} + + +TEST_F(ArchiverTest, ExtractZipFileWithDuplicatedEntries) +{ + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Create zip file with duplicates. + // + // Length Method Size Cmpr Date Time CRC-32 Name Content + // -------- ------ ------- ---- ---------- ----- -------- ---- ------- + // 1 Stored 1 0% 2016-03-18 22:49 83dcefb7 A 1 + // 1 Stored 1 0% 2016-03-18 22:49 1ad5be0d A 2 + // -------- ------- --- ------- ------- + // 2 2 0% 2 files + + ASSERT_SOME(os::write(path.get(), base64::decode( + "UEsDBBQAAAAAADC2cki379yDAQAAAAEAAAABAAAAQTFQSwMEFAAAAAAAMrZy" + "SA2+1RoBAAAAAQAAAAEAAABBMlBLAQIUAxQAAAAAADC2cki379yDAQAAAAEA" + "AAABAAAAAAAAAAAAAACAAQAAAABBUEsBAhQDFAAAAAAAMrZySA2+1RoBAAAA" + "AQAAAAEAAAAAAAAAAAAAAIABIAAAAEFQSwUGAAAAAAIAAgBeAAAAQAAAAAAA").get())); + + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "A"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("2", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractTarXZFile) +{ + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Create a tar.xz compressed file. + // + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 192 192 2018-02-27 15:34 hello Hello world (xz)\n + // -------- ------- ------- ------ + // 192 192 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4Cf/AH5dADQZSe6N1/i4P8k3jGgA" + "rB4mJjQrf8ka7ajHWIxeYZoS+eGuA0Br4ooXZVdW4dnh8GpgDlbdfMQrOOPA" + "aJE3B9L56mP0ThtjwNuMhhc8/xiXsFOVeUf/xbgcqognut0NZNetr0p+FA/O" + "K6NqFHAjzSaANcbNj+iFfqY3sC/mAAAAADpda78LIiMIAAGaAYBQAADDUC3D" + "scRn+wIAAAAABFla").get())); + + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Hello world (xz)\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractTarBZ2File) +{ + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Create an tar.bz2 compressed file. + // + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 148 148 2018-02-27 15:34 hello Hello world (bzip2)\n + // -------- ------- ------- ------ + // 148 148 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "QlpoOTFBWSZTWZo+haYAAH//hMIRAgBAYH+AAEAACH903pAABAAIIAB0EpEa" + "IeiMJtAIeRP1BlNCA00AAAA+x2lRZBAgaACRM0TvUjA5RJAR6BfGS3MjVUIh" + "IUI0Yww9tmran651Du0Hk5ZN4pbSxgs5xlAlIjtgOImyv+auHhIXnipV/xXy" + "iIHQu5IpwoSE0fQtMA==").get())); + + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Hello world (bzip2)\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractTarBz2GzFile) +{ + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Create an tar.bz2.gz compressed file. + // + // Verify that archives compressed twice (in this case, .bzip2.gz) + // work. Libarchive will keep processing until fully extracted. + // + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 185 185 2018-02-27 15:46 hello Hello world (bzip2)\n + // -------- ------- ------- ------ + // 185 185 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "H4sICOPtlVoAA2hlbGxvLnRhci5iejIAAZQAa/9CWmg5MUFZJlNZmj6FpgAA" + "f/+EwhECAEBgf4AAQAAIf3TekAAEAAggAHQSkRoh6Iwm0Ah5E/UGU0IDTQAA" + "AD7HaVFkECBoAJEzRO9SMDlEkBHoF8ZLcyNVQiEhQjRjDD22atqfrnUO7QeT" + "lk3iltLGCznGUCUiO2A4ibK/5q4eEheeKlX/FfKIgdC7kinChITR9C0wSQeY" + "TJQAAAA=").get())); + + EXPECT_SOME(archiver::extract(path.get(), "")); + + string extractedFile = path::join(sandbox.get(), "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Hello world (bzip2)\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractBz2FileFails) +{ + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Create an .bz2 compressed file. + // + // Libarchive does not appear to work without some sort of container + // (tar or zip or whatever). Verify that a regular file, compressed, + // will fail. + // + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 63 63 2018-02-27 17:00 hello Hello world (bzip2)\n + // -------- ------- ------- ------ + // 63 63 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "QlpoOTFBWSZTWTMaBKkAAANdgAAQQGAQAABAFiTQkCAAIhGCD1HoUwAE0auv" + "Imhs/86EgGxdyRThQkDMaBKk").get())); + + EXPECT_ERROR(archiver::extract(path.get(), "")); +} + + +TEST_F(ArchiverTest, ExtractGzFileFails) +{ + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> path = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(path); + + // Create an .gz compressed file. + // + // Libarchive does not appear to work without some sort of container + // (tar or zip or whatever). Verify that a regular file, compressed, + // will fail. + // + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 43 43 2018-03-21 16:59 hello Hello world (gz)\n + // -------- ------- ------- ------ + // 43 43 1 file + + ASSERT_SOME(os::write(path.get(), base64::decode( + "H4sICNjxsloAA2hlbGxvAPNIzcnJVyjPL8pJUdBIr9LkAgAwtvTdEQAAAA==").get())); + + EXPECT_ERROR(archiver::extract(path.get(), "")); +} + + +TEST_F(ArchiverTest, ExtractTarGzFileWithDestinationDir) +{ + // Construct a hello.tar.gz file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> sourcePath = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(sourcePath); + + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 22 22 2018-02-21 10:06 hello Howdy there, partner!\n + // -------- ------- ------- ------ + // 22 22 1 file + + ASSERT_SOME(os::write(sourcePath.get(), base64::decode( + "H4sICE61jVoAA2hlbGxvLnRhcgDtzjEOwjAQRNGtOcXSU9hx7FyBa0RgK0IR" + "RsYIcfsEaGhQqggh/VfsFLPFDHEcs6zLzEJon2k7bz7zrQliXdM6Nx/vxVgb" + "uiBqVt71crvWvqjKKaZ0yOnr31L/p/b5fnxoHWKJO730pZ5j2W5+vQoAAAAA" + "AAAAAAAAAAAAsGQC2DPIjgAoAAA=").get())); + + // Make a destination directory to extract the archive to. + string destDir = path::join(dir, "somedestination"); + ASSERT_SOME(os::mkdir(destDir)); + + // Note: The file does NOT have a .tar.gz extension. We could rename + // it, but libarchive doesn't care about extensions. It determines + // the format from the contents of the file. So this is tested here + // as well. + // + // Note: In this test, we extract the file to a destination directory + // and expect to find it there. + EXPECT_SOME(archiver::extract(sourcePath.get(), destDir)); + + string extractedFile = path::join(destDir, "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Howdy there, partner!\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractZipFileWithDestinationDir) +{ + // Construct a hello.zip file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> sourcePath = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(sourcePath); + + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 189 189 2018-02-26 15:06 hello Howdy there, partner! (.zip)\n + // -------- ------- ------- ------ + // 189 189 1 file + + ASSERT_SOME(os::write(sourcePath.get(), base64::decode( + "UEsDBAoAAAAAAMZ4WkxFOXeVHQAAAB0AAAAFABwAaGVsbG9VVAkAA+SSlFrk" + "kpRadXgLAAEE6AMAAAToAwAASG93ZHkgdGhlcmUsIHBhcnRuZXIhICguemlw" + "KQpQSwECHgMKAAAAAADGeFpMRTl3lR0AAAAdAAAABQAYAAAAAAABAAAAtIEA" + "AAAAaGVsbG9VVAUAA+SSlFp1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBL" + "AAAAXAAAAAAA").get())); + + // Make a destination directory to extract the archive to. + string destDir = path::join(dir, "somedestination"); + ASSERT_SOME(os::mkdir(destDir)); + + EXPECT_SOME(archiver::extract(sourcePath.get(), destDir)); + + string extractedFile = path::join(destDir, "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Howdy there, partner! (.zip)\n", os::read(extractedFile)); +} + + +TEST_F(ArchiverTest, ExtractZipFileWithLongDestinationDir) +{ + // Construct a hello.zip file that can be extracted. + string dir = path::join(sandbox.get(), "somedir"); + ASSERT_SOME(os::mkdir(dir)); + + Try<string> sourcePath = os::mktemp(path::join(dir, "XXXXXX")); + ASSERT_SOME(sourcePath); + + // Length Size Date Time Name Content + // -------- ------- ---------- ----- ---- ------ + // 189 189 2018-02-26 15:06 hello Howdy there, partner! (.zip)\n + // -------- ------- ------- ------ + // 189 189 1 file + + ASSERT_SOME(os::write(sourcePath.get(), base64::decode( + "UEsDBAoAAAAAAMZ4WkxFOXeVHQAAAB0AAAAFABwAaGVsbG9VVAkAA+SSlFrk" + "kpRadXgLAAEE6AMAAAToAwAASG93ZHkgdGhlcmUsIHBhcnRuZXIhICguemlw" + "KQpQSwECHgMKAAAAAADGeFpMRTl3lR0AAAAdAAAABQAYAAAAAAABAAAAtIEA" + "AAAAaGVsbG9VVAUAA+SSlFp1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBL" + "AAAAXAAAAAAA").get())); + + // Make a destination directory to extract the archive to. + const size_t max_path_length = 248; + string destDir = path::join(dir, string(max_path_length + 1, 'b')); + + ASSERT_SOME(os::mkdir(destDir)); + + EXPECT_SOME(archiver::extract(sourcePath.get(), destDir)); + + string extractedFile = path::join(destDir, "hello"); + ASSERT_TRUE(os::exists(extractedFile)); + + ASSERT_SOME_EQ("Howdy there, partner! (.zip)\n", os::read(extractedFile)); +}
