// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.


#include "content/browser/download/save_package.h"

#include <stddef.h>
#include <stdint.h>

#include <array>
#include <string>

#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/download/save_file_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/fake_local_frame.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "net/test/url_request/url_request_mock_http_job.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace content {

#define FPL FILE_PATH_LITERAL
#define HTML_EXTENSION ".html"
#if BUILDFLAG(IS_WIN)
#define FPL_HTML_EXTENSION L".html"
#elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
#define FPL_HTML_EXTENSION ".html"
#endif

namespace {

// This constant copied from save_package.cc.
#if BUILDFLAG(IS_WIN)
const uint32_t kMaxFilePathLength = MAX_PATH - 1;
#elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
const uint32_t kMaxFilePathLength = PATH_MAX - 1;
#endif

bool HasOrdinalNumber(const base::FilePath::StringType& filename) {
  base::FilePath::StringType::size_type r_paren_index =
      filename.rfind(FPL(')'));
  base::FilePath::StringType::size_type l_paren_index =
      filename.rfind(FPL('('));
  if (l_paren_index >= r_paren_index)
    return false;

  for (base::FilePath::StringType::size_type i = l_paren_index + 1;
       i != r_paren_index; ++i) {
    if (!base::IsAsciiDigit(filename[i]))
      return false;
  }

  return true;
}

}  // namespace

class SavePackageTest : public RenderViewHostImplTestHarness {
 public:
  bool GetGeneratedFilename(bool need_success_generate_filename,
                            const std::string& disposition,
                            const std::string& url,
                            bool need_htm_ext,
                            base::FilePath::StringType* generated_name) {
    SavePackage* save_package;
    if (need_success_generate_filename)
      save_package = save_package_success_.get();
    else
      save_package = save_package_fail_.get();
    return save_package->GenerateFileName(disposition, GURL(url), need_htm_ext,
                                          generated_name);
  }

  GURL GetUrlToBeSaved() {
    return SavePackage::GetUrlToBeSaved(contents()->GetPrimaryMainFrame());
  }

 protected:
  void SetUp() override {
    RenderViewHostImplTestHarness::SetUp();

    // Do the initialization in SetUp so contents() is initialized by
    // RenderViewHostImplTestHarness::SetUp.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    save_package_success_ = new SavePackage(
        contents()->GetPrimaryPage(), SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
        temp_dir_.GetPath().AppendASCII("testfile" HTML_EXTENSION),
        temp_dir_.GetPath().AppendASCII("testfile_files"));

    base::FilePath::StringType long_file_name = GetLongFileName();
    save_package_fail_ = new SavePackage(
        contents()->GetPrimaryPage(), SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
        temp_dir_.GetPath().Append(long_file_name + FPL_HTML_EXTENSION),
        temp_dir_.GetPath().Append(long_file_name + FPL("_files")));
  }

  std::unique_ptr<BrowserContext> CreateBrowserContext() override {
    // Initialize the SaveFileManager instance which we will use for the tests.
    save_file_manager_ = new SaveFileManager();
    return RenderViewHostImplTestHarness::CreateBrowserContext();
  }

  void TearDown() override {
    DeleteContents();
    base::RunLoop().RunUntilIdle();

    save_package_success_ = nullptr;
    save_package_fail_ = nullptr;

    RenderViewHostImplTestHarness::TearDown();
  }

  // Returns a path that is *almost* kMaxFilePathLength long
  base::FilePath::StringType GetLongFileName() const {
    size_t target_length =
        kMaxFilePathLength - 9 - temp_dir_.GetPath().value().length();
    return base::FilePath::StringType(target_length, FPL('a'));
  }

 private:
  // SavePackage for successfully generating file name.
  scoped_refptr<SavePackage> save_package_success_;
  // SavePackage for failed generating file name.
  scoped_refptr<SavePackage> save_package_fail_;
  base::ScopedTempDir temp_dir_;

  scoped_refptr<SaveFileManager> save_file_manager_;
};

struct GeneratedFiles {
  const char* disposition;
  const char* url;
  const base::FilePath::CharType* expected_name;
  bool need_htm_ext;
};
constexpr auto kGeneratedFiles = std::to_array<GeneratedFiles>({
    // We mainly focus on testing duplicated names here, since retrieving file
    // name from disposition and url has been tested in DownloadManagerTest.

    // No useful information in disposition or URL, use default.
    {"1.html", "http://www.savepage.com/",
     FPL("saved_resource") FPL_HTML_EXTENSION, true},

    // No duplicate occurs.
    {"filename=1.css", "http://www.savepage.com", FPL("1.css"), false},

    // No duplicate occurs.
    {"filename=1.js", "http://www.savepage.com", FPL("1.js"), false},

    // Append numbers for duplicated names.
    {"filename=1.css", "http://www.savepage.com", FPL("1(1).css"), false},

    // No duplicate occurs.
    {"filename=1(1).js", "http://www.savepage.com", FPL("1(1).js"), false},

    // Append numbers for duplicated names.
    {"filename=1.css", "http://www.savepage.com", FPL("1(2).css"), false},

    // Change number for duplicated names.
    {"filename=1(1).css", "http://www.savepage.com", FPL("1(3).css"), false},

    // No duplicate occurs.
    {"filename=1(11).css", "http://www.savepage.com", FPL("1(11).css"), false},

    // Test for case-insensitive file names.
    {"filename=readme.txt", "http://www.savepage.com", FPL("readme.txt"),
     false},

    {"filename=readme.TXT", "http://www.savepage.com", FPL("readme(1).TXT"),
     false},

    {"filename=READme.txt", "http://www.savepage.com", FPL("readme(2).txt"),
     false},

    {"filename=Readme(1).txt", "http://www.savepage.com", FPL("readme(3).txt"),
     false},
});

TEST_F(SavePackageTest, TestSuccessfullyGenerateSavePackageFilename) {
  for (size_t i = 0; i < std::size(kGeneratedFiles); ++i) {
    base::FilePath::StringType file_name;
    bool ok = GetGeneratedFilename(true,
                                   kGeneratedFiles[i].disposition,
                                   kGeneratedFiles[i].url,
                                   kGeneratedFiles[i].need_htm_ext,
                                   &file_name);
    ASSERT_TRUE(ok);
    EXPECT_EQ(kGeneratedFiles[i].expected_name, file_name);
  }
}

TEST_F(SavePackageTest, TestUnSuccessfullyGenerateSavePackageFilename) {
  for (size_t i = 0; i < std::size(kGeneratedFiles); ++i) {
    base::FilePath::StringType file_name;
    bool ok = GetGeneratedFilename(false,
                                   kGeneratedFiles[i].disposition,
                                   kGeneratedFiles[i].url,
                                   kGeneratedFiles[i].need_htm_ext,
                                   &file_name);
    ASSERT_FALSE(ok);
  }
}

// Crashing on Windows, see http://crbug.com/79365
#if BUILDFLAG(IS_WIN)
#define MAYBE_TestLongSavePackageFilename DISABLED_TestLongSavePackageFilename
#else
#define MAYBE_TestLongSavePackageFilename TestLongSavePackageFilename
#endif
TEST_F(SavePackageTest, MAYBE_TestLongSavePackageFilename) {
  const std::string base_url("http://www.google.com/");
  const base::FilePath::StringType long_file_name =
      GetLongFileName() + FPL(".css");
  const std::string url =
      base_url + base::FilePath(long_file_name).AsUTF8Unsafe();

  base::FilePath::StringType filename;
  // Test that the filename is successfully shortened to fit.
  ASSERT_TRUE(GetGeneratedFilename(true, std::string(), url, false, &filename));
  EXPECT_TRUE(filename.length() < long_file_name.length());
  EXPECT_FALSE(HasOrdinalNumber(filename));

  // Test that the filename is successfully shortened to fit, and gets an
  // an ordinal appended.
  ASSERT_TRUE(GetGeneratedFilename(true, std::string(), url, false, &filename));
  EXPECT_TRUE(filename.length() < long_file_name.length());
  EXPECT_TRUE(HasOrdinalNumber(filename));

  // Test that the filename is successfully shortened to fit, and gets a
  // different ordinal appended.
  base::FilePath::StringType filename2;
  ASSERT_TRUE(
      GetGeneratedFilename(true, std::string(), url, false, &filename2));
  EXPECT_TRUE(filename2.length() < long_file_name.length());
  EXPECT_TRUE(HasOrdinalNumber(filename2));
  EXPECT_NE(filename, filename2);
}

// Crashing on Windows, see http://crbug.com/79365
#if BUILDFLAG(IS_WIN)
#define MAYBE_TestLongSafePureFilename DISABLED_TestLongSafePureFilename
#else
#define MAYBE_TestLongSafePureFilename TestLongSafePureFilename
#endif
TEST_F(SavePackageTest, MAYBE_TestLongSafePureFilename) {
  const base::FilePath save_dir(FPL("test_dir"));
  const base::FilePath::StringType ext(FPL_HTML_EXTENSION);
  base::FilePath::StringType filename = GetLongFileName();

  // Test filename truncation respects length constraints.
  uint32_t max_path = SavePackage::ComputeMaxPathLengthForDirectory(save_dir);
  ASSERT_TRUE(SavePackage::TruncateBaseNameToFitPathConstraints(
      save_dir, ext, max_path, &filename));
  // Verify truncation worked by checking the total path length fits
  // constraints.
  uint32_t total_path_length =
      save_dir.value().length() + 1 + filename.length() + ext.length();
  EXPECT_LE(total_path_length, max_path);
}

// GetUrlToBeSaved method should return correct url to be saved.
TEST_F(SavePackageTest, TestGetUrlToBeSaved) {
  GURL url = net::URLRequestMockHTTPJob::GetMockUrl("save_page/a.htm");
  NavigateAndCommit(url);
  EXPECT_EQ(url, GetUrlToBeSaved());
}

// GetUrlToBeSaved method sould return actual url to be saved,
// instead of the displayed url used to view source of a page.
// Ex:GetUrlToBeSaved method should return http://www.google.com
// when user types view-source:http://www.google.com
TEST_F(SavePackageTest, TestGetUrlToBeSavedViewSource) {
  GURL mock_url = net::URLRequestMockHTTPJob::GetMockUrl("save_page/a.htm");
  GURL view_source_url =
      GURL(kViewSourceScheme + std::string(":") + mock_url.spec());
  GURL actual_url = net::URLRequestMockHTTPJob::GetMockUrl("save_page/a.htm");
  NavigateAndCommit(view_source_url);
  EXPECT_EQ(actual_url, GetUrlToBeSaved());
  EXPECT_EQ(view_source_url, contents()->GetLastCommittedURL());
}

// Test ComputeMaxPathLengthForDirectory with various directory lengths.
TEST_F(SavePackageTest, TestComputeMaxPathLengthForDirectory) {
  // Test with short directory.
  base::FilePath short_dir(FPL("dir"));
  uint32_t max_path_short =
      SavePackage::ComputeMaxPathLengthForDirectory(short_dir);
  EXPECT_GT(max_path_short, short_dir.value().length());

  // Test with longer directory.
  base::FilePath long_dir(FPL("very/long/directory/path/structure"));
  uint32_t max_path_long =
      SavePackage::ComputeMaxPathLengthForDirectory(long_dir);
  EXPECT_GT(max_path_long, long_dir.value().length());

  // Verify that the function returns reasonable values.
  // The max path should account for directory length + separator + component
  // limit.
  EXPECT_LE(short_dir.value().length() + 1, max_path_short);
  EXPECT_LE(long_dir.value().length() + 1, max_path_long);

  // Both should be positive and reasonable.
  EXPECT_GT(max_path_short, 0u);
  EXPECT_GT(max_path_long, 0u);
}

// Test TruncateBaseNameToFitPathConstraints edge cases.
TEST_F(SavePackageTest, TestTruncateBaseNameEdgeCases) {
  const base::FilePath save_dir(FPL("test"));
  const base::FilePath::StringType ext(FPL(".html"));

  // Test with filename that doesn't need truncation.
  base::FilePath::StringType short_name(FPL("short"));
  uint32_t max_path = SavePackage::ComputeMaxPathLengthForDirectory(save_dir);
  ASSERT_TRUE(SavePackage::TruncateBaseNameToFitPathConstraints(
      save_dir, ext, max_path, &short_name));
  EXPECT_EQ(short_name, FPL("short"));

  // Test with very small max_path_len that forces base_name to be cleared.
  base::FilePath::StringType any_name(FPL("filename"));
  uint32_t tiny_max =
      save_dir.value().length() + ext.length();  // No room for base_name.
  EXPECT_FALSE(SavePackage::TruncateBaseNameToFitPathConstraints(
      save_dir, ext, tiny_max, &any_name));
  EXPECT_TRUE(any_name.empty());

  // Test with max_path_len exactly fitting directory + separator + extension.
  base::FilePath::StringType zero_name(FPL("test"));
  uint32_t exact_max = save_dir.value().length() + 1 + ext.length();
  EXPECT_FALSE(SavePackage::TruncateBaseNameToFitPathConstraints(
      save_dir, ext, exact_max, &zero_name));
}

// Test UTF-8 filename truncation on POSIX systems.
TEST_F(SavePackageTest, TestUTF8FilenameTruncation) {
  const base::FilePath save_dir(FPL("test"));
  const base::FilePath::StringType ext(FPL(".html"));

  // Create a filename with multibyte UTF-8 characters.
  base::FilePath::StringType utf8_name;
#if BUILDFLAG(IS_WIN)
  // On Windows, use UTF-16.
  utf8_name = L"файл测试テスト";
#else
  // On POSIX, use UTF-8.
  utf8_name = "файл测试テスト";
#endif

  // Make the name very long to force truncation.
  for (int i = 0; i < 20; ++i) {
    utf8_name += utf8_name;
  }

  uint32_t max_path = SavePackage::ComputeMaxPathLengthForDirectory(save_dir);
  base::FilePath::StringType original_name = utf8_name;

  ASSERT_TRUE(SavePackage::TruncateBaseNameToFitPathConstraints(
      save_dir, ext, max_path, &utf8_name));

  // Verify truncation occurred.
  EXPECT_LT(utf8_name.length(), original_name.length());

  // Verify total path fits constraints.
  uint32_t total_length =
      save_dir.value().length() + 1 + utf8_name.length() + ext.length();
  EXPECT_LE(total_length, max_path);

#if !BUILDFLAG(IS_WIN)
  // On POSIX, verify the truncated string is valid UTF-8.
  std::string utf8_result = base::FilePath(utf8_name).AsUTF8Unsafe();
  EXPECT_FALSE(utf8_result.empty());
#endif
}

// Test directory with trailing separator.
TEST_F(SavePackageTest, TestDirectoryWithTrailingSeparator) {
  // Create directory with trailing separator using platform-specific separator.
  base::FilePath::StringType dir_str = FPL("test");
  dir_str += base::FilePath::kSeparators[0];
  base::FilePath dir_with_sep(dir_str);
  ASSERT_TRUE(dir_with_sep.EndsWithSeparator());

  const base::FilePath::StringType ext(FPL(".html"));
  base::FilePath::StringType filename(FPL("testfile"));

  uint32_t max_path =
      SavePackage::ComputeMaxPathLengthForDirectory(dir_with_sep);

  ASSERT_TRUE(SavePackage::TruncateBaseNameToFitPathConstraints(
      dir_with_sep, ext, max_path, &filename));

  // Should handle trailing separator correctly - no extra separator needed.
  uint32_t total_length =
      dir_with_sep.value().length() + filename.length() + ext.length();
  EXPECT_LE(total_length, max_path);
}

// Test empty extension.
TEST_F(SavePackageTest, TestEmptyExtension) {
  const base::FilePath save_dir(FPL("test"));
  const base::FilePath::StringType empty_ext;
  base::FilePath::StringType filename = GetLongFileName();

  uint32_t max_path = SavePackage::ComputeMaxPathLengthForDirectory(save_dir);

  ASSERT_TRUE(SavePackage::TruncateBaseNameToFitPathConstraints(
      save_dir, empty_ext, max_path, &filename));

  uint32_t total_length = save_dir.value().length() + 1 + filename.length();
  EXPECT_LE(total_length, max_path);
}

// Test very long directory path.
TEST_F(SavePackageTest, TestVeryLongDirectory) {
  // Create a very long directory path.
  base::FilePath::StringType long_path_str;
  for (int i = 0; i < 50; ++i) {
    long_path_str += FPL("very_long_directory_name/");
  }
  base::FilePath long_dir(long_path_str);

  const base::FilePath::StringType ext(FPL(".html"));
  base::FilePath::StringType filename(FPL("test"));

  uint32_t max_path = SavePackage::ComputeMaxPathLengthForDirectory(long_dir);

  bool result = SavePackage::TruncateBaseNameToFitPathConstraints(
      long_dir, ext, max_path, &filename);

  if (result) {
    // If truncation succeeded, verify constraints are met.
    uint32_t total_length =
        long_dir.value().length() + 1 + filename.length() + ext.length();
    EXPECT_LE(total_length, max_path);
  } else {
    // If truncation failed, filename should be empty.
    EXPECT_TRUE(filename.empty());
  }
}

class SavePackageFencedFrameTest : public SavePackageTest {
 public:
  SavePackageFencedFrameTest() {
    scoped_feature_list_.InitAndEnableFeatureWithParameters(
        blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
  }
  ~SavePackageFencedFrameTest() override = default;

  RenderFrameHost* CreateFencedFrame(RenderFrameHost* parent) {
    RenderFrameHost* fenced_frame =
        RenderFrameHostTester::For(parent)->AppendFencedFrame();
    return fenced_frame;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// FakeLocalFrame implementation that records calls to
// GetSavableResourceLinks().
class FakeLocalFrameWithSavableResourceLinks : public FakeLocalFrame {
 public:
  explicit FakeLocalFrameWithSavableResourceLinks(RenderFrameHost* rfh) {
    auto* test_host = static_cast<TestRenderFrameHost*>(rfh);
    test_host->ResetLocalFrame();
    Init(test_host->GetRemoteAssociatedInterfaces());
  }

  bool is_called() const { return is_called_; }

  // FakeLocalFrame:
  void GetSavableResourceLinks(
      GetSavableResourceLinksCallback callback) override {
    is_called_ = true;
    std::move(callback).Run(nullptr);
  }

 private:
  bool is_called_ = false;
};

// Tests that SavePackage does not create an unnecessary task that gets the
// resources links from fenced frames.
// If fenced frames become savable, this test will need to be updated.
// See https://crbug.com/1321102
TEST_F(SavePackageFencedFrameTest,
       DontRequestSavableResourcesFromFencedFrames) {
  NavigateAndCommit(GURL("https://www.example.com"));

  // Create a fenced frame.
  RenderFrameHostTester::For(contents()->GetPrimaryMainFrame())
      ->InitializeRenderFrameIfNeeded();
  RenderFrameHost* fenced_frame_rfh =
      CreateFencedFrame(contents()->GetPrimaryMainFrame());

  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  scoped_refptr<SavePackage> save_package(new SavePackage(
      contents()->GetPrimaryPage(), SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
      temp_dir.GetPath().AppendASCII("testfile" HTML_EXTENSION),
      temp_dir.GetPath().AppendASCII("testfile_files")));

  FakeLocalFrameWithSavableResourceLinks local_frame_for_primary(
      contents()->GetPrimaryMainFrame());
  local_frame_for_primary.Init(
      contents()->GetPrimaryMainFrame()->GetRemoteAssociatedInterfaces());

  FakeLocalFrameWithSavableResourceLinks local_frame_for_fenced_frame(
      fenced_frame_rfh);
  local_frame_for_fenced_frame.Init(
      fenced_frame_rfh->GetRemoteAssociatedInterfaces());

  EXPECT_TRUE(save_package->Init(base::DoNothing()));

  ASSERT_TRUE(base::test::RunUntil([&local_frame_for_primary]() {
    return local_frame_for_primary.is_called();
  }));
  EXPECT_FALSE(local_frame_for_fenced_frame.is_called());
}

}  // namespace content
