//
// Copyright (C) 2017 The Android Open Source Project
//
// 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 "update_engine/payload_generator/squashfs_filesystem.h"

#include <unistd.h>

#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <vector>

#include <base/format_macros.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <gtest/gtest.h>

#include "update_engine/common/test_utils.h"
#include "update_engine/common/utils.h"
#include "update_engine/payload_generator/extent_utils.h"

namespace chromeos_update_engine {

using std::map;
using std::set;
using std::string;
using std::unique_ptr;
using std::vector;

using test_utils::GetBuildArtifactsPath;

namespace {

constexpr uint64_t kTestBlockSize = 4096;
constexpr uint64_t kTestSqfsBlockSize = 1 << 15;

// Checks that all the blocks in |extents| are in the range [0, total_blocks).
void ExpectBlocksInRange(const vector<Extent>& extents, uint64_t total_blocks) {
  for (const Extent& extent : extents) {
    EXPECT_LE(0U, extent.start_block());
    EXPECT_LE(extent.start_block() + extent.num_blocks(), total_blocks);
  }
}

SquashfsFilesystem::SquashfsHeader GetSimpleHeader() {
  // These properties are enough for now. Add more as needed.
  return {
      .magic = 0x73717368,
      .block_size = kTestSqfsBlockSize,
      .compression_type = 1,  // For gzip.
      .major_version = 4,
  };
}

}  // namespace

class SquashfsFilesystemTest : public ::testing::Test {
 public:
  void CheckSquashfs(const unique_ptr<SquashfsFilesystem>& fs) {
    ASSERT_TRUE(fs);
    EXPECT_EQ(kTestBlockSize, fs->GetBlockSize());

    vector<FilesystemInterface::File> files;
    ASSERT_TRUE(fs->GetFiles(&files));

    map<string, FilesystemInterface::File> map_files;
    for (const auto& file : files) {
      EXPECT_EQ(map_files.end(), map_files.find(file.name))
          << "File " << file.name << " repeated in the list.";
      map_files[file.name] = file;
      ExpectBlocksInRange(file.extents, fs->GetBlockCount());
    }

    // Checking the sortness.
    EXPECT_TRUE(std::is_sorted(files.begin(),
                               files.end(),
                               [](const FilesystemInterface::File& a,
                                  const FilesystemInterface::File& b) {
                                 return a.extents[0].start_block() <
                                        b.extents[0].start_block();
                               }));

    auto overlap_check = [](const FilesystemInterface::File& a,
                            const FilesystemInterface::File& b) {
      // Return true if overlapping.
      return a.extents[0].start_block() + a.extents[0].num_blocks() >
             b.extents[0].start_block();
    };
    // Check files are not overlapping.
    EXPECT_EQ(std::adjacent_find(files.begin(), files.end(), overlap_check),
              files.end());
  }
};

// CreateFromFile() depends on unsquashfs -m, which only exists in Chrome OS.
#ifdef __CHROMEOS__
TEST_F(SquashfsFilesystemTest, EmptyFilesystemTest) {
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFile(
      GetBuildArtifactsPath("gen/disk_sqfs_empty.img"), true, false);
  CheckSquashfs(fs);

  // Even an empty squashfs filesystem is rounded up to 4K.
  EXPECT_EQ(4096 / kTestBlockSize, fs->GetBlockCount());

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  ASSERT_EQ(files.size(), 1u);

  FilesystemInterface::File file;
  file.name = "<metadata-0>";
  file.extents.emplace_back();
  file.extents[0].set_start_block(0);
  file.extents[0].set_num_blocks(1);
  EXPECT_EQ(files[0].name, file.name);
  EXPECT_EQ(files[0].extents, file.extents);
}

TEST_F(SquashfsFilesystemTest, DefaultFilesystemTest) {
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFile(
      GetBuildArtifactsPath("gen/disk_sqfs_default.img"), true, false);
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  ASSERT_EQ(files.size(), 1u);

  FilesystemInterface::File file;
  file.name = "<fragment-0>";
  file.extents.emplace_back();
  file.extents[0].set_start_block(0);
  file.extents[0].set_num_blocks(1);
  EXPECT_EQ(files[0].name, file.name);
  EXPECT_EQ(files[0].extents, file.extents);
}

TEST_F(SquashfsFilesystemTest, UpdateEngineConfigTest) {
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFile(
      GetBuildArtifactsPath("gen/disk_sqfs_unittest.img"), true, true);
  CheckSquashfs(fs);

  brillo::KeyValueStore kvs;
  EXPECT_TRUE(fs->LoadSettings(&kvs));
  string minor_version;
  EXPECT_TRUE(kvs.GetString("PAYLOAD_MINOR_VERSION", &minor_version));
  EXPECT_EQ(minor_version, "1234");
}
#endif  // __CHROMEOS__

TEST_F(SquashfsFilesystemTest, SimpleFileMapTest) {
  string filemap = R"(dir1/file1 96 4000
                      dir1/file2 4096 100)";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize * 2, GetSimpleHeader());
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  EXPECT_EQ(files.size(), 2u);
}

TEST_F(SquashfsFilesystemTest, FileMapZeroSizeFileTest) {
  // The second file's size is zero.
  string filemap = R"(dir1/file1 96 4000
                      dir1/file2 4096
                      dir1/file3 4096 100)";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize * 2, GetSimpleHeader());
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  // The second and third files are removed. The file with size zero is removed.
  EXPECT_EQ(files.size(), 2u);
}

// Testing the compressed bit.
TEST_F(SquashfsFilesystemTest, CompressedBitTest) {
  string filemap = "dir1/file1 0 " + std::to_string(4000 | (1 << 24)) + "\n";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize, GetSimpleHeader());
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  ASSERT_EQ(files.size(), 1u);
  EXPECT_EQ(files[0].extents[0].num_blocks(), 1u);
}

// Test overlap.
TEST_F(SquashfsFilesystemTest, OverlapingFiles1Test) {
  string filemap = R"(file1 0 6000
                      file2 5000 5000)";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize * 3, GetSimpleHeader());
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  ASSERT_EQ(files.size(), 2u);
  EXPECT_EQ(files[0].extents[0].num_blocks(), 1u);
  EXPECT_EQ(files[1].extents[0].num_blocks(), 2u);
}

// Test overlap, first inside second.
TEST_F(SquashfsFilesystemTest, OverlapingFiles2Test) {
  string filemap = R"(file1 0 4000
                      file2 0 6000)";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize * 2, GetSimpleHeader());
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  ASSERT_EQ(files.size(), 1u);
  EXPECT_EQ(files[0].name, "file2");
  EXPECT_EQ(files[0].extents[0].num_blocks(), 2u);
}

// Test overlap, second inside first.
TEST_F(SquashfsFilesystemTest, OverlapingFiles3Test) {
  string filemap = R"(file1 0 8000
                      file2 100 100)";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize * 2, GetSimpleHeader());
  CheckSquashfs(fs);

  vector<FilesystemInterface::File> files;
  ASSERT_TRUE(fs->GetFiles(&files));
  ASSERT_EQ(files.size(), 1u);
  EXPECT_EQ(files[0].name, "file1");
  EXPECT_EQ(files[0].extents[0].num_blocks(), 2u);
}

// Fail a line with only one argument.
TEST_F(SquashfsFilesystemTest, FailOnlyFileNameTest) {
  string filemap = "dir1/file1\n";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize, GetSimpleHeader());
  EXPECT_FALSE(fs);
}

// Fail a line with space separated filen name
TEST_F(SquashfsFilesystemTest, FailSpaceInFileNameTest) {
  string filemap = "dir1 file1 0 10\n";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize, GetSimpleHeader());
  EXPECT_FALSE(fs);
}

// Fail empty line
TEST_F(SquashfsFilesystemTest, FailEmptyLineTest) {
  // The second file's size is zero.
  string filemap = R"(
  /t
                      dir1/file3 4096 100)";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize * 2, GetSimpleHeader());
  EXPECT_FALSE(fs);
}

// Fail on bad magic or major
TEST_F(SquashfsFilesystemTest, FailBadMagicOrMajorTest) {
  string filemap = "dir1/file1 0 10\n";
  auto header = GetSimpleHeader();
  header.magic = 1;
  EXPECT_FALSE(
      SquashfsFilesystem::CreateFromFileMap(filemap, kTestBlockSize, header));

  header = GetSimpleHeader();
  header.major_version = 3;
  EXPECT_FALSE(
      SquashfsFilesystem::CreateFromFileMap(filemap, kTestBlockSize, header));
}

// Fail size with larger than block_size
TEST_F(SquashfsFilesystemTest, FailLargerThanBlockSizeTest) {
  string filemap = "file1 0 " + std::to_string(kTestSqfsBlockSize + 1) + "\n";
  unique_ptr<SquashfsFilesystem> fs = SquashfsFilesystem::CreateFromFileMap(
      filemap, kTestBlockSize, GetSimpleHeader());
  EXPECT_FALSE(fs);
}

// Test is squashfs image.
TEST_F(SquashfsFilesystemTest, IsSquashfsImageTest) {
  // Some sample from a recent squashfs file.
  brillo::Blob super_block = {
      0x68, 0x73, 0x71, 0x73, 0x59, 0x05, 0x00, 0x00, 0x09, 0x3a, 0x89, 0x58,
      0x00, 0x00, 0x02, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x11, 0x00,
      0xc0, 0x00, 0x06, 0x00, 0x04, 0x00, 0x00, 0x00, 0x89, 0x18, 0xf7, 0x7c,
      0x00, 0x00, 0x00, 0x00, 0x2e, 0x33, 0xcd, 0x16, 0x00, 0x00, 0x00, 0x00,
      0x3a, 0x30, 0xcd, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x33, 0xcd, 0x16,
      0x00, 0x00, 0x00, 0x00, 0x07, 0x62, 0xcc, 0x16, 0x00, 0x00, 0x00, 0x00,
      0x77, 0xe6, 0xcc, 0x16, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x25, 0xcd, 0x16,
      0x00, 0x00, 0x00, 0x00, 0x10, 0x30, 0xcd, 0x16, 0x00, 0x00, 0x00, 0x00};

  EXPECT_TRUE(SquashfsFilesystem::IsSquashfsImage(super_block));

  // Bad magic
  auto bad_super_block = super_block;
  bad_super_block[1] = 0x02;
  EXPECT_FALSE(SquashfsFilesystem::IsSquashfsImage(bad_super_block));

  // Bad major
  bad_super_block = super_block;
  bad_super_block[28] = 0x03;
  EXPECT_FALSE(SquashfsFilesystem::IsSquashfsImage(bad_super_block));

  // Small size;
  bad_super_block = super_block;
  bad_super_block.resize(10);
  EXPECT_FALSE(SquashfsFilesystem::IsSquashfsImage(bad_super_block));
}

}  // namespace chromeos_update_engine
