1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <set>
6#include <string>
7#include <vector>
8
9#include "base/files/file.h"
10#include "base/files/file_enumerator.h"
11#include "base/files/file_path.h"
12#include "base/files/file_util.h"
13#include "base/files/scoped_temp_dir.h"
14#include "base/path_service.h"
15#include "base/strings/string_util.h"
16#include "base/strings/stringprintf.h"
17#include "testing/gtest/include/gtest/gtest.h"
18#include "testing/platform_test.h"
19#include "third_party/zlib/google/zip.h"
20#include "third_party/zlib/google/zip_reader.h"
21
22namespace {
23
24// Make the test a PlatformTest to setup autorelease pools properly on Mac.
25class ZipTest : public PlatformTest {
26 protected:
27  enum ValidYearType {
28    VALID_YEAR,
29    INVALID_YEAR
30  };
31
32  virtual void SetUp() {
33    PlatformTest::SetUp();
34
35    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
36    test_dir_ = temp_dir_.path();
37
38    base::FilePath zip_path(test_dir_);
39    zip_contents_.insert(zip_path.AppendASCII("foo.txt"));
40    zip_path = zip_path.AppendASCII("foo");
41    zip_contents_.insert(zip_path);
42    zip_contents_.insert(zip_path.AppendASCII("bar.txt"));
43    zip_path = zip_path.AppendASCII("bar");
44    zip_contents_.insert(zip_path);
45    zip_contents_.insert(zip_path.AppendASCII("baz.txt"));
46    zip_contents_.insert(zip_path.AppendASCII("quux.txt"));
47    zip_contents_.insert(zip_path.AppendASCII(".hidden"));
48
49    // Include a subset of files in |zip_file_list_| to test ZipFiles().
50    zip_file_list_.push_back(base::FilePath(FILE_PATH_LITERAL("foo.txt")));
51    zip_file_list_.push_back(
52        base::FilePath(FILE_PATH_LITERAL("foo/bar/quux.txt")));
53    zip_file_list_.push_back(
54        base::FilePath(FILE_PATH_LITERAL("foo/bar/.hidden")));
55  }
56
57  virtual void TearDown() {
58    PlatformTest::TearDown();
59  }
60
61  bool GetTestDataDirectory(base::FilePath* path) {
62    bool success = PathService::Get(base::DIR_SOURCE_ROOT, path);
63    EXPECT_TRUE(success);
64    if (!success)
65      return false;
66    *path = path->AppendASCII("third_party");
67    *path = path->AppendASCII("zlib");
68    *path = path->AppendASCII("google");
69    *path = path->AppendASCII("test");
70    *path = path->AppendASCII("data");
71    return true;
72  }
73
74  void TestUnzipFile(const base::FilePath::StringType& filename,
75                     bool expect_hidden_files) {
76    base::FilePath test_dir;
77    ASSERT_TRUE(GetTestDataDirectory(&test_dir));
78    TestUnzipFile(test_dir.Append(filename), expect_hidden_files);
79  }
80
81  void TestUnzipFile(const base::FilePath& path, bool expect_hidden_files) {
82    ASSERT_TRUE(base::PathExists(path)) << "no file " << path.value();
83    ASSERT_TRUE(zip::Unzip(path, test_dir_));
84
85    base::FileEnumerator files(test_dir_, true,
86        base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
87    base::FilePath next_path = files.Next();
88    size_t count = 0;
89    while (!next_path.value().empty()) {
90      if (next_path.value().find(FILE_PATH_LITERAL(".svn")) ==
91          base::FilePath::StringType::npos) {
92        EXPECT_EQ(zip_contents_.count(next_path), 1U) <<
93            "Couldn't find " << next_path.value();
94        count++;
95      }
96      next_path = files.Next();
97    }
98
99    size_t expected_count = 0;
100    for (std::set<base::FilePath>::iterator iter = zip_contents_.begin();
101         iter != zip_contents_.end(); ++iter) {
102      if (expect_hidden_files || iter->BaseName().value()[0] != '.')
103        ++expected_count;
104    }
105
106    EXPECT_EQ(expected_count, count);
107  }
108
109  // This function does the following:
110  // 1) Creates a test.txt file with the given last modification timestamp
111  // 2) Zips test.txt and extracts it back into a different location.
112  // 3) Confirms that test.txt in the output directory has the specified
113  //    last modification timestamp if it is valid (|valid_year| is true).
114  //    If the timestamp is not supported by the zip format, the last
115  //    modification defaults to the current time.
116  void TestTimeStamp(const char* date_time, ValidYearType valid_year) {
117    SCOPED_TRACE(std::string("TestTimeStamp(") + date_time + ")");
118    base::ScopedTempDir temp_dir;
119    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
120
121    base::FilePath zip_file = temp_dir.path().AppendASCII("out.zip");
122    base::FilePath src_dir = temp_dir.path().AppendASCII("input");
123    base::FilePath out_dir = temp_dir.path().AppendASCII("output");
124
125    base::FilePath src_file = src_dir.AppendASCII("test.txt");
126    base::FilePath out_file = out_dir.AppendASCII("test.txt");
127
128    EXPECT_TRUE(base::CreateDirectory(src_dir));
129    EXPECT_TRUE(base::CreateDirectory(out_dir));
130
131    base::Time test_mtime;
132    ASSERT_TRUE(base::Time::FromString(date_time, &test_mtime));
133
134    // Adjusting the current timestamp to the resolution that the zip file
135    // supports, which is 2 seconds. Note that between this call to Time::Now()
136    // and zip::Zip() the clock can advance a bit, hence the use of EXPECT_GE.
137    base::Time::Exploded now_parts;
138    base::Time::Now().LocalExplode(&now_parts);
139    now_parts.second = now_parts.second & ~1;
140    now_parts.millisecond = 0;
141    base::Time now_time = base::Time::FromLocalExploded(now_parts);
142
143    EXPECT_EQ(1, base::WriteFile(src_file, "1", 1));
144    EXPECT_TRUE(base::TouchFile(src_file, base::Time::Now(), test_mtime));
145
146    EXPECT_TRUE(zip::Zip(src_dir, zip_file, true));
147    ASSERT_TRUE(zip::Unzip(zip_file, out_dir));
148
149    base::File::Info file_info;
150    EXPECT_TRUE(base::GetFileInfo(out_file, &file_info));
151    EXPECT_EQ(file_info.size, 1);
152
153    if (valid_year == VALID_YEAR) {
154      EXPECT_EQ(file_info.last_modified, test_mtime);
155    } else {
156      // Invalid date means the modification time will default to 'now'.
157      EXPECT_GE(file_info.last_modified, now_time);
158    }
159  }
160
161  // The path to temporary directory used to contain the test operations.
162  base::FilePath test_dir_;
163
164  base::ScopedTempDir temp_dir_;
165
166  // Hard-coded contents of a known zip file.
167  std::set<base::FilePath> zip_contents_;
168
169  // Hard-coded list of relative paths for a zip file created with ZipFiles.
170  std::vector<base::FilePath> zip_file_list_;
171};
172
173TEST_F(ZipTest, Unzip) {
174  TestUnzipFile(FILE_PATH_LITERAL("test.zip"), true);
175}
176
177TEST_F(ZipTest, UnzipUncompressed) {
178  TestUnzipFile(FILE_PATH_LITERAL("test_nocompress.zip"), true);
179}
180
181TEST_F(ZipTest, UnzipEvil) {
182  base::FilePath path;
183  ASSERT_TRUE(GetTestDataDirectory(&path));
184  path = path.AppendASCII("evil.zip");
185  // Unzip the zip file into a sub directory of test_dir_ so evil.zip
186  // won't create a persistent file outside test_dir_ in case of a
187  // failure.
188  base::FilePath output_dir = test_dir_.AppendASCII("out");
189  ASSERT_FALSE(zip::Unzip(path, output_dir));
190  base::FilePath evil_file = output_dir;
191  evil_file = evil_file.AppendASCII(
192      "../levilevilevilevilevilevilevilevilevilevilevilevil");
193  ASSERT_FALSE(base::PathExists(evil_file));
194}
195
196TEST_F(ZipTest, UnzipEvil2) {
197  base::FilePath path;
198  ASSERT_TRUE(GetTestDataDirectory(&path));
199  // The zip file contains an evil file with invalid UTF-8 in its file
200  // name.
201  path = path.AppendASCII("evil_via_invalid_utf8.zip");
202  // See the comment at UnzipEvil() for why we do this.
203  base::FilePath output_dir = test_dir_.AppendASCII("out");
204  // This should fail as it contains an evil file.
205  ASSERT_FALSE(zip::Unzip(path, output_dir));
206  base::FilePath evil_file = output_dir;
207  evil_file = evil_file.AppendASCII("../evil.txt");
208  ASSERT_FALSE(base::PathExists(evil_file));
209}
210
211TEST_F(ZipTest, Zip) {
212  base::FilePath src_dir;
213  ASSERT_TRUE(GetTestDataDirectory(&src_dir));
214  src_dir = src_dir.AppendASCII("test");
215
216  base::ScopedTempDir temp_dir;
217  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
218  base::FilePath zip_file = temp_dir.path().AppendASCII("out.zip");
219
220  EXPECT_TRUE(zip::Zip(src_dir, zip_file, true));
221  TestUnzipFile(zip_file, true);
222}
223
224TEST_F(ZipTest, ZipIgnoreHidden) {
225  base::FilePath src_dir;
226  ASSERT_TRUE(GetTestDataDirectory(&src_dir));
227  src_dir = src_dir.AppendASCII("test");
228
229  base::ScopedTempDir temp_dir;
230  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
231  base::FilePath zip_file = temp_dir.path().AppendASCII("out.zip");
232
233  EXPECT_TRUE(zip::Zip(src_dir, zip_file, false));
234  TestUnzipFile(zip_file, false);
235}
236
237TEST_F(ZipTest, ZipNonASCIIDir) {
238  base::FilePath src_dir;
239  ASSERT_TRUE(GetTestDataDirectory(&src_dir));
240  src_dir = src_dir.AppendASCII("test");
241
242  base::ScopedTempDir temp_dir;
243  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
244  // Append 'Тест' (in cyrillic).
245  base::FilePath src_dir_russian =
246      temp_dir.path().Append(base::FilePath::FromUTF8Unsafe(
247          "\xD0\xA2\xD0\xB5\xD1\x81\xD1\x82"));
248  base::CopyDirectory(src_dir, src_dir_russian, true);
249  base::FilePath zip_file = temp_dir.path().AppendASCII("out_russian.zip");
250
251  EXPECT_TRUE(zip::Zip(src_dir_russian, zip_file, true));
252  TestUnzipFile(zip_file, true);
253}
254
255TEST_F(ZipTest, ZipTimeStamp) {
256  // The dates tested are arbitrary, with some constraints. The zip format can
257  // only store years from 1980 to 2107 and an even number of seconds, due to it
258  // using the ms dos date format.
259
260  // Valid arbitrary date.
261  TestTimeStamp("23 Oct 1997 23:22:20", VALID_YEAR);
262
263  // Date before 1980, zip format limitation, must default to unix epoch.
264  TestTimeStamp("29 Dec 1979 21:00:10", INVALID_YEAR);
265
266  // Despite the minizip headers telling the maximum year should be 2044, it
267  // can actually go up to 2107. Beyond that, the dos date format cannot store
268  // the year (2107-1980=127). To test that limit, the input file needs to be
269  // touched, but the code that modifies the file access and modification times
270  // relies on time_t which is defined as long, therefore being in many
271  // platforms just a 4-byte integer, like 32-bit Mac OSX or linux. As such, it
272  // suffers from the year-2038 bug. Therefore 2038 is the highest we can test
273  // in all platforms reliably.
274  TestTimeStamp("02 Jan 2038 23:59:58", VALID_YEAR);
275}
276
277#if defined(OS_POSIX)
278TEST_F(ZipTest, ZipFiles) {
279  base::FilePath src_dir;
280  ASSERT_TRUE(GetTestDataDirectory(&src_dir));
281  src_dir = src_dir.AppendASCII("test");
282
283  base::ScopedTempDir temp_dir;
284  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
285  base::FilePath zip_name = temp_dir.path().AppendASCII("out.zip");
286
287  base::File zip_file(zip_name,
288                      base::File::FLAG_CREATE | base::File::FLAG_WRITE);
289  ASSERT_TRUE(zip_file.IsValid());
290  EXPECT_TRUE(zip::ZipFiles(src_dir, zip_file_list_,
291                            zip_file.GetPlatformFile()));
292  zip_file.Close();
293
294  zip::ZipReader reader;
295  EXPECT_TRUE(reader.Open(zip_name));
296  EXPECT_EQ(zip_file_list_.size(), static_cast<size_t>(reader.num_entries()));
297  for (size_t i = 0; i < zip_file_list_.size(); ++i) {
298    EXPECT_TRUE(reader.LocateAndOpenEntry(zip_file_list_[i]));
299    // Check the path in the entry just in case.
300    const zip::ZipReader::EntryInfo* entry_info = reader.current_entry_info();
301    EXPECT_EQ(entry_info->file_path(), zip_file_list_[i]);
302  }
303}
304#endif  // defined(OS_POSIX)
305
306TEST_F(ZipTest, UnzipFilesWithIncorrectSize) {
307  base::FilePath test_data_folder;
308  ASSERT_TRUE(GetTestDataDirectory(&test_data_folder));
309
310  // test_mismatch_size.zip contains files with names from 0.txt to 7.txt with
311  // sizes from 0 to 7 bytes respectively, but the metadata in the zip file says
312  // the uncompressed size is 3 bytes. The ZipReader and minizip code needs to
313  // be clever enough to get all the data out.
314  base::FilePath test_zip_file =
315      test_data_folder.AppendASCII("test_mismatch_size.zip");
316
317  base::ScopedTempDir scoped_temp_dir;
318  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
319  const base::FilePath& temp_dir = scoped_temp_dir.path();
320
321  ASSERT_TRUE(zip::Unzip(test_zip_file, temp_dir));
322  EXPECT_TRUE(base::DirectoryExists(temp_dir.AppendASCII("d")));
323
324  for (int i = 0; i < 8; i++) {
325    SCOPED_TRACE(base::StringPrintf("Processing %d.txt", i));
326    base::FilePath file_path = temp_dir.AppendASCII(
327        base::StringPrintf("%d.txt", i));
328    int64 file_size = -1;
329    EXPECT_TRUE(base::GetFileSize(file_path, &file_size));
330    EXPECT_EQ(static_cast<int64>(i), file_size);
331  }
332}
333
334}  // namespace
335