2024-03-21 09:20:03 +00:00
|
|
|
/*
|
2026-02-13 03:37:55 +00:00
|
|
|
* Copyright (c) 2024 - 2026 Ember
|
2024-03-21 09:20:03 +00:00
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <mpq/MPQ.h>
|
2024-03-22 09:09:11 +00:00
|
|
|
#include <mpq/DynamicMemorySink.h>
|
2024-12-30 23:45:04 +00:00
|
|
|
#include <shared/utility/FileMD5.h>
|
2024-03-22 08:49:25 +00:00
|
|
|
#include <botan/bigint.h>
|
2024-03-21 09:20:03 +00:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
using namespace ember;
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_FileNotFound) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/no_such_file");
|
2026-02-19 01:25:02 +00:00
|
|
|
ASSERT_EQ(result.error(), mpq::ErrorCode::file_not_found);
|
2024-03-21 09:20:03 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_ArchiveNotFound) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/not_an_mpq");
|
2026-02-19 01:25:02 +00:00
|
|
|
ASSERT_EQ(result.error(), mpq::ErrorCode::no_archive_found);
|
2024-03-21 09:20:03 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_ArchiveFound) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_0.mpq");
|
|
|
|
|
ASSERT_EQ(*result, 0); // offset
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_ArchiveFoundOffset) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_1.mpq");
|
|
|
|
|
ASSERT_EQ(*result, 0x200); // offset
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_ArchiveNotFoundOffset) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_2.mpq");
|
|
|
|
|
ASSERT_TRUE(!result);
|
2026-02-19 01:25:02 +00:00
|
|
|
ASSERT_EQ(result.error(), mpq::ErrorCode::no_archive_found);
|
2024-03-21 09:20:03 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_BadFile_PartialMagic) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_3.mpq");
|
|
|
|
|
ASSERT_TRUE(!result);
|
2026-02-19 01:25:02 +00:00
|
|
|
ASSERT_EQ(result.error(), mpq::ErrorCode::no_archive_found);
|
2024-03-21 09:20:03 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_BadHeader_Size) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_5.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_5.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_BadHeader_Version) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_6.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_6.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-22 08:49:25 +00:00
|
|
|
TEST(MPQ, Locate_BadHeader_Truncated) {
|
2024-03-21 09:20:03 +00:00
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_7.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_7.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_HeaderTruncated) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_8.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_8.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_BadArchiveSize) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_9.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_9.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_Success) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_10.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_NO_THROW(mpq::open_archive("test_data/mpqs/v0_10.mpq", *result));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_BadHashTableOffset) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_11.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_11.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_BadBlockTableOffset) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_12.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_12.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_BadHashTableSize) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_13.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_13.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_BadBlockTableSize) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v0_14.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v0_14.mpq", *result), mpq::exception);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Open_BadExtendedBlockTableOffset) {
|
|
|
|
|
const auto result = mpq::locate_archive("test_data/mpqs/v1_15.mpq");
|
|
|
|
|
ASSERT_TRUE(result);
|
|
|
|
|
ASSERT_THROW(mpq::open_archive("test_data/mpqs/v1_15.mpq", *result), mpq::exception);
|
2024-03-22 08:49:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-06 05:27:22 +01:00
|
|
|
// ADPCM & Huffman compression, encrypted
|
2026-04-06 20:53:10 +01:00
|
|
|
TEST(MPQ, Extract_WAV) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("owl.wav", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("owl.wav", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0x3a66b4b718686cb4d5aa143895290d84");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No compression
|
|
|
|
|
TEST(MPQ, Extract_MP3) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("owl.mp3", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("owl.mp3", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0x2f7b030648e1f7ec77109c659bbea3ce");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PKWare compression
|
|
|
|
|
TEST(MPQ, Extract_Binary) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("elevated_1920_1080.ex_", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("elevated_1920_1080.ex_", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0xf3b82b404a36a9714bb266007cacd489");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Imploded
|
|
|
|
|
TEST(MPQ, Extract_JPG) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& index = archive->file_lookup("ember.jpg", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-08-20 08:35:06 +01:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("ember.jpg", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0x8a623acdf09f9388719010d76a5c7a52");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Zlib compression, encrypted, single unit, fix key
|
|
|
|
|
TEST(MPQ, Extract_PNG) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("ember.png", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("ember.png", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0xb29a1ad8ebeef28de68fa9bb0325b893");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Extract_Listfile) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("(listfile)", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("(listfile)", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0xc4185e8d87a01ac3057a6498deccab1e");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Extract_Attributes) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("(attributes)", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("(attributes)", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0x44fd6bad334ea24d8901e80ab20c07ba");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Extract_Text) {
|
2024-03-22 13:38:30 +00:00
|
|
|
auto archive = mpq::open_archive("test_data/mpqs/v1_16.mpq", 0);
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_TRUE(archive);
|
|
|
|
|
const auto index = archive->file_lookup("compressed.txt", 0);
|
|
|
|
|
ASSERT_NE(index, mpq::Archive::npos);
|
2024-03-23 06:13:56 +00:00
|
|
|
const auto& entry = archive->file_entry(index);
|
2024-03-22 09:09:11 +00:00
|
|
|
mpq::DynamicMemorySink sink;
|
2024-03-22 08:49:25 +00:00
|
|
|
ASSERT_NO_THROW(archive->extract_file("compressed.txt", sink));
|
2026-02-18 21:38:07 +00:00
|
|
|
const auto md5_buf = utility::generate_md5(sink.data());
|
2026-02-13 03:37:55 +00:00
|
|
|
const auto md5 = Botan::BigInt::from_bytes(md5_buf);
|
2024-03-22 08:49:25 +00:00
|
|
|
Botan::BigInt expected("0xf13fad545731a0b71b2cc17e1acd48f4");
|
|
|
|
|
ASSERT_EQ(md5, expected);
|
2024-03-23 15:44:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(MPQ, Locate_BadAlignment) {
|
2026-02-19 01:25:02 +00:00
|
|
|
alignas(mpq::v0::Header) std::byte data[mpq::header_size_v0]{};
|
2024-03-23 15:44:38 +00:00
|
|
|
std::span span(data + 1, sizeof(data) - 1); // force misaligned buffer
|
|
|
|
|
auto result = mpq::locate_archive(span);
|
2026-02-19 01:25:02 +00:00
|
|
|
ASSERT_EQ(result.error(), mpq::ErrorCode::bad_alignment);
|
2024-03-21 09:20:03 +00:00
|
|
|
}
|