You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by jo...@apache.org on 2018/06/05 00:01:27 UTC

[4/4] mesos git commit: Added a stout utility header which interfaces with libarchive.

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 <jo...@microsoft.com>
Authored: Mon Jun 4 13:44:20 2018 -0700
Committer: Joseph Wu <jo...@apache.org>
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));
+}