// Copyright 2019 Google LLC
//
// 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
//
//     https://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 "sandboxed_api/util/fileops.h"

#include <sys/stat.h>
#include <unistd.h>

#include <algorithm>
#include <cerrno>
#include <climits>
#include <string>
#include <vector>

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/strings/str_cat.h"
#include "sandboxed_api/testing.h"
#include "sandboxed_api/util/file_helpers.h"
#include "sandboxed_api/util/status_matchers.h"

namespace sapi::file_util {

// Forward declare functions that are only used in fileops.cc.
namespace fileops {
bool GetCWD(std::string* result);
bool RemoveLastPathComponent(const std::string& file, std::string* output);
}  // namespace fileops

namespace {

using ::sapi::IsOk;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::IsFalse;
using ::testing::IsTrue;
using ::testing::Ne;
using ::testing::SizeIs;
using ::testing::StrEq;

class FileOpsTest : public testing::Test {
 protected:
  static void SetUpTestSuite() {
    ASSERT_THAT(chdir(GetTestTempPath().c_str()), Eq(0));
  }
};

TEST_F(FileOpsTest, GetCWDTest) {
  std::string result;
  ASSERT_THAT(fileops::GetCWD(&result), IsTrue());
  EXPECT_THAT(result, StrEq(GetTestTempPath()));
}

TEST_F(FileOpsTest, MakeAbsoluteTest) {
  const auto tmp_dir = GetTestTempPath();
  ASSERT_THAT(chdir(tmp_dir.c_str()), Eq(0));
  EXPECT_THAT(fileops::MakeAbsolute("", ""), StrEq(""));
  EXPECT_THAT(fileops::MakeAbsolute(".", ""), StrEq(tmp_dir));
  EXPECT_THAT(fileops::MakeAbsolute(".", tmp_dir), StrEq(tmp_dir));
  EXPECT_THAT(fileops::MakeAbsolute(".", "/"), StrEq("/"));
  EXPECT_THAT(fileops::MakeAbsolute("/", tmp_dir), StrEq("/"));
  EXPECT_THAT(fileops::MakeAbsolute("/", "/"), StrEq("/"));
  EXPECT_THAT(fileops::MakeAbsolute("/", ""), StrEq("/"));
  EXPECT_THAT(fileops::MakeAbsolute("/foo/bar", ""), StrEq("/foo/bar"));
  EXPECT_THAT(fileops::MakeAbsolute("foo/bar", ""),
              StrEq(absl::StrCat(tmp_dir, "/foo/bar")));
  EXPECT_THAT(fileops::MakeAbsolute("foo/bar", tmp_dir),
              StrEq(absl::StrCat(tmp_dir, "/foo/bar")));
  EXPECT_THAT(fileops::MakeAbsolute("foo/bar", tmp_dir + "/"),
              StrEq(absl::StrCat(tmp_dir, "/foo/bar")));
}

TEST_F(FileOpsTest, ExistsTest) {
  ASSERT_THAT(file::SetContents("exists_test", "", file::Defaults()), IsOk());
  EXPECT_THAT(fileops::Exists("exists_test", false), IsTrue());
  EXPECT_THAT(fileops::Exists("exists_test", true), IsTrue());

  ASSERT_THAT(symlink("exists_test", "exists_test_link"), Eq(0));
  EXPECT_THAT(fileops::Exists("exists_test_link", false), IsTrue());
  EXPECT_THAT(fileops::Exists("exists_test_link", true), IsTrue());

  ASSERT_THAT(unlink("exists_test"), Eq(0));
  EXPECT_THAT(fileops::Exists("exists_test_link", false), IsTrue());
  EXPECT_THAT(fileops::Exists("exists_test_link", true), IsFalse());

  ASSERT_THAT(unlink("exists_test_link"), Eq(0));
  EXPECT_THAT(fileops::Exists("exists_test_link", false), IsFalse());
  EXPECT_THAT(fileops::Exists("exists_test_link", true), IsFalse());
}

TEST_F(FileOpsTest, ReadLinkTest) {
  EXPECT_THAT(fileops::ReadLink("readlink_not_there"), StrEq(""));
  EXPECT_THAT(errno, Eq(ENOENT));

  ASSERT_THAT(file::SetContents("readlink_file", "", file::Defaults()), IsOk());
  EXPECT_THAT(fileops::ReadLink("readlink_file"), StrEq(""));
  unlink("readlink_file");

  ASSERT_THAT(symlink("..", "readlink_dotdot"), Eq(0));
  EXPECT_THAT(fileops::ReadLink("readlink_dotdot"), StrEq(".."));
  unlink("readlink_dotdot");

  ASSERT_THAT(symlink("../", "readlink_dotdotslash"), 0);
  EXPECT_THAT(fileops::ReadLink("readlink_dotdotslash"), "../");
  unlink("readlink_dotdotslash");

  ASSERT_THAT(symlink("/", "readlink_slash"), 0);
  EXPECT_THAT(fileops::ReadLink("readlink_slash"), "/");
  unlink("readlink_slash");

  const std::string very_long_name(PATH_MAX - 1, 'f');
  ASSERT_THAT(symlink(very_long_name.c_str(), "readlink_long"), Eq(0));
  EXPECT_THAT(fileops::ReadLink("readlink_long"), StrEq(very_long_name));
  unlink("readlink_long");
}

TEST_F(FileOpsTest, ListDirectoryEntriesFailTest) {
  std::vector<std::string> files;
  std::string error;

  EXPECT_THAT(fileops::ListDirectoryEntries("new_dir", &files, &error),
              IsFalse());
  EXPECT_THAT(files, IsEmpty());
  EXPECT_THAT(error, StrEq("opendir(new_dir): No such file or directory"));
}

TEST_F(FileOpsTest, ListDirectoryEntriesEmptyTest) {
  std::vector<std::string> files;
  std::string error;

  ASSERT_THAT(mkdir("new_dir", 0700), Eq(0));

  EXPECT_THAT(fileops::ListDirectoryEntries("new_dir", &files, &error),
              IsTrue());
  EXPECT_THAT(files, IsEmpty());

  rmdir("new_dir");
}

TEST_F(FileOpsTest, ListDirectoryEntriesOneFileTest) {
  ASSERT_THAT(mkdir("new_dir", 0700), Eq(0));
  ASSERT_THAT(file::SetContents("new_dir/first", "", file::Defaults()), IsOk());

  std::vector<std::string> files;
  std::string error;
  EXPECT_THAT(fileops::ListDirectoryEntries("new_dir", &files, &error),
              IsTrue());

  unlink("new_dir/first");
  rmdir("new_dir");

  ASSERT_THAT(files, SizeIs(1));
  EXPECT_THAT(files[0], "first");
}

TEST_F(FileOpsTest, ListDirectoryEntriesTest) {
  ASSERT_THAT(mkdir("new_dir", 0700), Eq(0));
  constexpr int kNumFiles = 10;
  for (int i = 0; i < kNumFiles; ++i) {
    ASSERT_THAT(file::SetContents(absl::StrCat("new_dir/file", i), "",
                                  file::Defaults()),
                IsOk());
  }

  std::vector<std::string> files;
  std::string error;
  EXPECT_THAT(fileops::ListDirectoryEntries("new_dir", &files, &error),
              IsTrue());

  fileops::DeleteRecursively("new_dir");

  ASSERT_THAT(files, SizeIs(kNumFiles));
  std::sort(files.begin(), files.end());
  for (int i = 0; i < kNumFiles; ++i) {
    EXPECT_THAT(files[i], StrEq(absl::StrCat("file", i)));
  }
}

TEST_F(FileOpsTest, RemoveLastPathComponentTest) {
  std::string result;

  EXPECT_THAT(fileops::RemoveLastPathComponent("/", &result), IsFalse());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("///", &result), IsFalse());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home", &result), IsTrue());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home/", &result), IsTrue());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home///", &result), IsTrue());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home///", &result), IsTrue());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("///home///", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home/someone", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home///someone", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home///someone/", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home///someone//", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home/someone/file", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home/someone"));

  EXPECT_THAT(
      fileops::RemoveLastPathComponent("/home/someone////file", &result),
      IsTrue());
  EXPECT_THAT(result, StrEq("/home/someone"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home///someone/file", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home///someone"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/home/someone/file", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("/home/someone"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("no_root", &result), IsTrue());
  EXPECT_THAT(result, StrEq(""));

  EXPECT_THAT(fileops::RemoveLastPathComponent("no_root/", &result), IsTrue());
  EXPECT_THAT(result, StrEq(""));

  EXPECT_THAT(fileops::RemoveLastPathComponent("no_root///", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq(""));

  EXPECT_THAT(fileops::RemoveLastPathComponent("/file", &result), IsTrue());
  EXPECT_THAT(result, "/");

  EXPECT_THAT(fileops::RemoveLastPathComponent("no_root/file", &result),
              IsTrue());
  EXPECT_THAT(result, StrEq("no_root"));

  result = "no_root";
  EXPECT_THAT(fileops::RemoveLastPathComponent(result, &result), IsTrue());
  EXPECT_THAT(result, StrEq(""));

  result = "no_root/";
  EXPECT_THAT(fileops::RemoveLastPathComponent(result, &result), IsTrue());
  EXPECT_THAT(result, StrEq(""));

  result = "no_root///";
  EXPECT_THAT(fileops::RemoveLastPathComponent(result, &result), IsTrue());
  EXPECT_THAT(result, StrEq(""));

  result = "/file";
  EXPECT_THAT(fileops::RemoveLastPathComponent(result, &result), IsTrue());
  EXPECT_THAT(result, StrEq("/"));

  result = "no_root/file";
  EXPECT_THAT(fileops::RemoveLastPathComponent(result, &result), IsTrue());
  EXPECT_THAT(result, StrEq("no_root"));

  EXPECT_THAT(fileops::RemoveLastPathComponent("", &result), IsFalse());
  EXPECT_THAT(result, StrEq(""));
}

TEST_F(FileOpsTest, TestBasename) {
  EXPECT_THAT(fileops::Basename(""), StrEq(""));
  EXPECT_THAT(fileops::Basename("/"), StrEq(""));
  EXPECT_THAT(fileops::Basename("//"), StrEq(""));
  EXPECT_THAT(fileops::Basename("/hello/"), StrEq(""));
  EXPECT_THAT(fileops::Basename("//hello"), StrEq("hello"));
  EXPECT_THAT(fileops::Basename("/hello/world"), StrEq("world"));
  EXPECT_THAT(fileops::Basename("/hello, world"), StrEq("hello, world"));
}

TEST_F(FileOpsTest, TestStripBasename) {
  EXPECT_THAT(fileops::StripBasename(""), StrEq(""));
  EXPECT_THAT(fileops::StripBasename("/"), StrEq("/"));
  EXPECT_THAT(fileops::StripBasename("//"), StrEq("/"));
  EXPECT_THAT(fileops::StripBasename("/hello"), StrEq("/"));
  EXPECT_THAT(fileops::StripBasename("//hello"), StrEq("/"));
  EXPECT_THAT(fileops::StripBasename("/hello/"), StrEq("/hello"));
  EXPECT_THAT(fileops::StripBasename("/hello//"), StrEq("/hello/"));
  EXPECT_THAT(fileops::StripBasename("/hello/world"), StrEq("/hello"));
  EXPECT_THAT(fileops::StripBasename("/hello, world"), StrEq("/"));
}

void SetupDirectory() {
  ASSERT_THAT(mkdir("foo", 0755), Eq(0));
  ASSERT_THAT(mkdir("foo/bar", 0755), Eq(0));
  ASSERT_THAT(mkdir("foo/baz", 0755), Eq(0));
  ASSERT_THAT(file::SetContents("foo/quux", "", file::Defaults()), IsOk());
  ASSERT_THAT(chmod("foo/quux", 0644), Eq(0));

  ASSERT_THAT(file::SetContents("foo/bar/foo", "", file::Defaults()), IsOk());
  ASSERT_THAT(chmod("foo/bar/foo", 0644), Eq(0));
  ASSERT_THAT(file::SetContents("foo/bar/bar", "", file::Defaults()), IsOk());
  ASSERT_THAT(chmod("foo/bar/bar", 0644), Eq(0));

  ASSERT_THAT(mkdir("foo/bar/baz", 0755), Eq(0));
  ASSERT_THAT(file::SetContents("foo/bar/baz/foo", "", file::Defaults()),
              IsOk());
  ASSERT_THAT(chmod("foo/bar/baz/foo", 0644), Eq(0));
}

TEST_F(FileOpsTest, CreateDirectoryRecursivelyTest) {
  constexpr char kTestDir[] = "a/b/c";
  EXPECT_THAT(fileops::CreateDirectoryRecursively(kTestDir, 0700), IsTrue());
  EXPECT_THAT(fileops::CreateDirectoryRecursively(kTestDir, 0700), IsTrue());
}

TEST_F(FileOpsTest, DeleteRecursivelyTest) {
  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsTrue());
  EXPECT_THAT(fileops::DeleteRecursively("/not_there"), IsTrue());

  // Can't stat file
  SetupDirectory();
  ASSERT_THAT(chmod("foo/bar/baz", 0000), Eq(0));
  EXPECT_THAT(fileops::DeleteRecursively("foo/bar/baz/quux"), IsFalse());
  EXPECT_THAT(errno, Eq(EACCES));
  ASSERT_THAT(chmod("foo/bar/baz", 0755), Eq(0));

  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsTrue());
  struct stat64 st;
  EXPECT_THAT(lstat64("foo", &st), Ne(0));

  // Can't list subdirectory
  SetupDirectory();
  ASSERT_THAT(chmod("foo/bar/baz", 0000), Eq(0));
  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsFalse());
  EXPECT_THAT(errno, Eq(EACCES));
  ASSERT_THAT(chmod("foo/bar/baz", 0755), Eq(0));

  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsTrue());

  // Can't delete file
  SetupDirectory();
  ASSERT_THAT(chmod("foo/bar/baz", 0500), Eq(0));
  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsFalse());
  EXPECT_THAT(errno, Eq(EACCES));
  ASSERT_THAT(chmod("foo/bar/baz", 0755), Eq(0));

  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsTrue());

  // Can't delete directory
  SetupDirectory();
  ASSERT_THAT(fileops::DeleteRecursively("foo/bar/baz/foo"), IsTrue());
  ASSERT_THAT(chmod("foo/bar", 0500), Eq(0));
  EXPECT_THAT(fileops::DeleteRecursively("foo/bar/baz"), IsFalse());
  EXPECT_THAT(errno, Eq(EACCES));
  ASSERT_THAT(chmod("foo/bar", 0755), Eq(0));

  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsTrue());
}

TEST_F(FileOpsTest, ReadLinkAbsoluteTest) {
  const auto tmp_dir = GetTestTempPath();
  ASSERT_THAT(chdir(tmp_dir.c_str()), Eq(0));

  EXPECT_THAT(fileops::DeleteRecursively("foo"), IsTrue());
  ASSERT_THAT(symlink("rel/path", "foo"), Eq(0));

  const std::string expected_path = absl::StrCat(tmp_dir, "/rel/path");
  const std::string expected_path2 = absl::StrCat(tmp_dir, "/./rel/path");
  std::string result;
  EXPECT_THAT(fileops::ReadLinkAbsolute("foo", &result), IsTrue());
  EXPECT_THAT(result, StrEq(expected_path));
  EXPECT_THAT(fileops::ReadLinkAbsolute("./foo", &result), IsTrue());
  EXPECT_THAT(result, StrEq(expected_path2));
  EXPECT_THAT(fileops::ReadLinkAbsolute(absl::StrCat(tmp_dir, "/foo"), &result),
              IsTrue());
  EXPECT_THAT(result, StrEq(expected_path));

  result.clear();
  EXPECT_THAT(fileops::ReadLinkAbsolute("/not_there", &result), IsFalse());
  EXPECT_THAT(result, IsEmpty());
}

TEST_F(FileOpsTest, CopyFileTest) {
  const auto tmp_dir = GetTestTempPath();
  // Non-existent source
  EXPECT_THAT(
      fileops::CopyFile("/not/there", absl::StrCat(tmp_dir, "/out"), 0777),
      IsFalse());

  // Unwritable target
  EXPECT_THAT(fileops::CopyFile("/proc/self/exe", tmp_dir, 0777), IsFalse());

  EXPECT_THAT(file::SetContents(absl::StrCat(tmp_dir, "/test"), "test\n",
                                file::Defaults()),
              IsOk());
  EXPECT_THAT(fileops::CopyFile(absl::StrCat(tmp_dir, "/test"),
                                absl::StrCat(tmp_dir, "/test2"), 0666),
              IsTrue());

  std::string text;
  EXPECT_THAT(file::GetContents(absl::StrCat(tmp_dir, "/test2"), &text,
                                file::Defaults()),
              IsOk());

  EXPECT_THAT(text, StrEq("test\n"));

  unlink((absl::StrCat(tmp_dir, "/test")).c_str());
  unlink((absl::StrCat(tmp_dir, "/test2")).c_str());
}

}  // namespace
}  // namespace sapi::file_util
