blob: 2ae1dac00037bbd2527f9355b3bd674b3851117e [file]
// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <gtest/gtest.h>
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "src/dawn/node/standalone/FileBundle.h"
#include "src/dawn/node/test/TestZip.h"
namespace dawn::node::standalone {
namespace {
using dawn::node::test::WriteZip;
using dawn::node::test::ZipEntry;
using dawn::node::test::ZipMethod;
using Kind = FileBundle::Kind;
using Names = std::vector<std::string>;
class FileBundleTest : public ::testing::Test {
protected:
void SetUp() override {
static std::atomic<uint32_t> counter{0};
const ::testing::TestInfo* info = ::testing::UnitTest::GetInstance()->current_test_info();
const std::filesystem::path base = std::filesystem::temp_directory_path();
while (true) {
const std::filesystem::path candidate =
base /
("dawn_file_bundle_" + std::string(info->name()) + "_" + std::to_string(counter++));
std::error_code ec;
if (std::filesystem::create_directory(candidate, ec)) {
dir_ = candidate;
return;
}
ASSERT_FALSE(ec) << "failed to create " << candidate << ": " << ec.message();
}
}
void TearDown() override {
if (!dir_.empty()) {
std::error_code ec;
std::filesystem::remove_all(dir_, ec);
}
}
// Writes a zip holding `entries` and opens it as `bundle_`.
void Open(const std::vector<ZipEntry>& entries, ZipMethod method = ZipMethod::kDeflated) {
const std::filesystem::path zip = dir_ / "bundle.zip";
ASSERT_TRUE(WriteZip(zip, entries, method));
bundle_ = OpenZip(zip);
ASSERT_NE(bundle_, nullptr);
}
// Opens `zip` as a bundle, or records a failure and returns null.
static std::unique_ptr<FileBundle> OpenZip(const std::filesystem::path& zip) {
dawn::Result<std::unique_ptr<FileBundle>, std::string> result =
FileBundle::OpenZip(zip.string());
if (result.IsError()) {
ADD_FAILURE() << zip << ": " << *result.AcquireError();
return nullptr;
}
return result.AcquireSuccess();
}
// Returns whether FileBundle::OpenZip() rejects `zip`.
static bool OpenZipFails(const std::filesystem::path& zip) {
dawn::Result<std::unique_ptr<FileBundle>, std::string> result =
FileBundle::OpenZip(zip.string());
if (result.IsError()) {
result.AcquireError();
return true;
}
result.AcquireSuccess();
return false;
}
std::string Read(const std::string& path) {
std::optional<std::string> contents = bundle_->ReadFile(path);
EXPECT_TRUE(contents.has_value()) << path;
return contents.value_or("");
}
std::filesystem::path dir_;
std::unique_ptr<FileBundle> bundle_;
};
TEST_F(FileBundleTest, OpenZipFailsForAMissingFile) {
EXPECT_TRUE(OpenZipFails(dir_ / "absent.zip"));
}
TEST_F(FileBundleTest, OpenZipFailsForAFileThatIsNotAZip) {
const std::filesystem::path text = dir_ / "text.zip";
std::ofstream(text) << "not a zip";
EXPECT_TRUE(OpenZipFails(text));
}
TEST_F(FileBundleTest, StatFindsFilesAndTheDirectoriesHoldingThem) {
Open({{"a/b/c.txt", "c"}});
EXPECT_EQ(bundle_->Stat("a/b/c.txt"), Kind::kFile);
EXPECT_EQ(bundle_->Stat("a/b"), Kind::kDirectory);
EXPECT_EQ(bundle_->Stat("a"), Kind::kDirectory);
EXPECT_EQ(bundle_->Stat(""), Kind::kDirectory);
EXPECT_EQ(bundle_->Stat("a/b/c"), Kind::kNotFound);
EXPECT_EQ(bundle_->Stat("b"), Kind::kNotFound);
}
// minizip decompresses the data. This covers the read loop around it, for both of the methods that
// zip tools write.
TEST_F(FileBundleTest, ReadFileReturnsStoredAndDeflatedContents) {
// Bytes that are not text, and a run long enough for deflate to shrink.
const std::string bytes("\x00\x01\xff\r\n", 5);
const std::string repeated(100000, 'x');
for (ZipMethod method : {ZipMethod::kStored, ZipMethod::kDeflated}) {
const std::filesystem::path zip = dir_ / "method.zip";
ASSERT_TRUE(
WriteZip(zip, {{"bytes.bin", bytes}, {"big.txt", repeated}, {"empty", ""}}, method));
std::unique_ptr<FileBundle> bundle = OpenZip(zip);
ASSERT_NE(bundle, nullptr);
EXPECT_EQ(bundle->ReadFile("bytes.bin"), bytes);
EXPECT_EQ(bundle->ReadFile("big.txt"), repeated);
EXPECT_EQ(bundle->ReadFile("empty"), "");
}
}
TEST_F(FileBundleTest, ReadFileFailsForDirectoriesAndMissingPaths) {
Open({{"dir/file.txt", "x"}});
EXPECT_EQ(bundle_->ReadFile("dir"), std::nullopt);
EXPECT_EQ(bundle_->ReadFile(""), std::nullopt);
EXPECT_EQ(bundle_->ReadFile("dir/absent.txt"), std::nullopt);
}
// minizip checks the CRC. This covers that ReadFile reports a mismatch instead of returning the
// damaged data.
TEST_F(FileBundleTest, ReadFileFailsWhenTheDataDoesNotMatchItsChecksum) {
const std::filesystem::path zip = dir_ / "corrupt.zip";
ASSERT_TRUE(WriteZip(zip, {{"file.txt", "PAYLOAD"}}, ZipMethod::kStored));
std::string data;
{
std::ifstream in(zip, std::ios::binary);
data.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
const size_t payload = data.find("PAYLOAD");
ASSERT_NE(payload, std::string::npos);
data[payload] = 'p';
std::ofstream(zip, std::ios::binary) << data;
std::unique_ptr<FileBundle> bundle = OpenZip(zip);
ASSERT_NE(bundle, nullptr);
EXPECT_EQ(bundle->ReadFile("file.txt"), std::nullopt);
}
TEST_F(FileBundleTest, ReadDirListsEachEntryOnceInOrder) {
// The first entries create several directories at once.
Open({{"a/sub/w.js", "w"},
{"b/c/d/e.txt", "e"},
{"z.txt", "z"},
{"a/y.js", "y"},
{"a/x.js", "x"}});
EXPECT_EQ(bundle_->ReadDir(""), (Names{"a", "b", "z.txt"}));
EXPECT_EQ(bundle_->ReadDir("a"), (Names{"sub", "x.js", "y.js"}));
EXPECT_EQ(bundle_->ReadDir("a/sub"), (Names{"w.js"}));
EXPECT_EQ(bundle_->ReadDir("b"), (Names{"c"}));
EXPECT_EQ(bundle_->ReadDir("b/c"), (Names{"d"}));
EXPECT_EQ(bundle_->ReadDir("b/c/d"), (Names{"e.txt"}));
EXPECT_EQ(bundle_->ReadDir("z.txt"), std::nullopt);
EXPECT_EQ(bundle_->ReadDir("q"), std::nullopt);
}
TEST_F(FileBundleTest, DirectoryEntriesAreDirectoriesEvenWhenEmpty) {
Open({{"empty/", ""}, {"full/", ""}, {"full/f.txt", "f"}});
EXPECT_EQ(bundle_->Stat("empty"), Kind::kDirectory);
EXPECT_EQ(bundle_->ReadDir("empty"), Names{});
EXPECT_EQ(bundle_->ReadDir("full"), Names{"f.txt"});
EXPECT_EQ(bundle_->ReadDir(""), (Names{"empty", "full"}));
}
TEST_F(FileBundleTest, EntryNamesAreNormalizedAndThoseEscapingTheRootSkipped) {
Open({{"./dot.txt", "dot"},
{"../up.txt", "up"},
{"a/../b.txt", "b"},
{"/abs.txt", "abs"},
{"", "a file with no name"}});
EXPECT_EQ(Read("dot.txt"), "dot");
EXPECT_EQ(bundle_->ReadDir(""), Names{"dot.txt"});
}
} // namespace
} // namespace dawn::node::standalone