PackageTool and package library for assets

This commit is contained in:
2026-05-16 20:50:26 +03:00
parent 8630bfcf18
commit 3f40d84847
6 changed files with 2257 additions and 0 deletions
+57
View File
@@ -567,6 +567,63 @@ target_include_directories(character_class_lua_test PRIVATE
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# ---------------------------------------------------------------------------
# Package Archive Library
# ---------------------------------------------------------------------------
# This implements an Ogre::Archive for .package files (uncompressed indexed
# file containers, similar to Unity packages). It can be used from resources.cfg
# with "Package=path/to/file.package".
add_library(PackageArchive STATIC
package/OgrePackageArchive.cpp
package/OgrePackageArchive.h
)
target_include_directories(PackageArchive PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(PackageArchive PUBLIC
OgreMain
)
# ---------------------------------------------------------------------------
# PackageTool - command-line tool for creating/managing .package archives
# ---------------------------------------------------------------------------
add_executable(PackageTool
package/PackageTool.cpp
package/OgrePackageArchive.cpp
package/OgrePackageArchive.h
)
target_link_libraries(PackageTool
PackageArchive
OgreMain
)
target_include_directories(PackageTool PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
# ---------------------------------------------------------------------------
# Package Archive Test
# ---------------------------------------------------------------------------
add_executable(PackageArchiveTest
tests/package_archive_test.cpp
package/OgrePackageArchive.cpp
package/OgrePackageArchive.h
)
target_link_libraries(PackageArchiveTest
PackageArchive
OgreMain
)
target_include_directories(PackageArchiveTest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/package
)
# Copy local resources (materials, etc.)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources"
@@ -0,0 +1,814 @@
/*
* -----------------------------------------------------------------------------
* This source file is part of OGRE
* (Object-oriented Graphics Rendering Engine)
* For the latest info, see http://www.ogre3d.org/
*
* Copyright (c) 2000-2014 Torus Knot Software Ltd
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* -----------------------------------------------------------------------------
*/
#include "OgrePackageArchive.h"
#include "OgreException.h"
#include "OgreLogManager.h"
#include "OgreString.h"
#include "OgreStringVector.h"
#include <algorithm>
#include <cstring>
#include <sys/stat.h>
namespace Ogre
{
const uint32_t PackageArchive::MAGIC = 0x4B434150; // "PACK" in little-endian
const uint32_t PackageArchive::FORMAT_VERSION = 1;
//-----------------------------------------------------------------------
PackageArchive::PackageArchive(const String &name, const String &archType)
: Archive(name, archType)
, mLoaded(false)
{
mFilePath = name;
}
//-----------------------------------------------------------------------
PackageArchive::~PackageArchive()
{
unload();
}
//-----------------------------------------------------------------------
void PackageArchive::load()
{
OGRE_LOCK_AUTO_MUTEX;
if (mLoaded)
return;
// Open the file for reading
mFileStream.open(mFilePath.c_str(),
std::ios::in | std::ios::out | std::ios::binary);
if (!mFileStream.is_open()) {
// Try opening as read-only if read-write failed
mFileStream.clear();
mFileStream.open(mFilePath.c_str(),
std::ios::in | std::ios::binary);
}
if (mFileStream.is_open()) {
// Read the index from the existing file
readIndex();
mLoaded = true;
// Check if we have write access
mFileStream.clear();
mFileStream.seekp(0, std::ios::end);
mReadOnly = !mFileStream.good();
} else {
// File doesn't exist yet - create it for writing
mFileStream.clear();
mFileStream.open(mFilePath.c_str(),
std::ios::in | std::ios::out |
std::ios::binary | std::ios::trunc);
if (mFileStream.is_open()) {
mLoaded = true;
mReadOnly = false;
} else {
OGRE_EXCEPT(Exception::ERR_FILE_NOT_FOUND,
"Cannot open package file: " + mFilePath,
"PackageArchive::load");
}
}
}
//-----------------------------------------------------------------------
void PackageArchive::unload()
{
OGRE_LOCK_AUTO_MUTEX;
if (mFileStream.is_open()) {
mFileStream.close();
}
mFileIndex.clear();
mLoaded = false;
}
//-----------------------------------------------------------------------
void PackageArchive::readIndex()
{
// Seek to end to read footer
mFileStream.seekg(0, std::ios::end);
std::streampos fileSize = mFileStream.tellg();
if (fileSize < static_cast<std::streamoff>(12)) {
// File too small to contain a valid footer
return;
}
// Read magic number and index offset from the end
// Footer layout (last 12 bytes):
// [index offset (uint64_t)] [magic (uint32_t)]
// The index offset is at offset -12 from end, magic is at offset -4
uint64_t indexOffset;
uint32_t magic;
mFileStream.seekg(-static_cast<std::streamoff>(12), std::ios::end);
mFileStream.read(reinterpret_cast<char *>(&indexOffset),
sizeof(indexOffset));
mFileStream.read(reinterpret_cast<char *>(&magic), sizeof(magic));
if (magic != MAGIC) {
// Not a valid package file
if (LogManager::getSingletonPtr())
LogManager::getSingleton().logMessage(
"PackageArchive: Invalid magic number in " +
mFilePath);
return;
}
// Seek to the index
mFileStream.seekg(static_cast<std::streamoff>(indexOffset),
std::ios::beg);
// Read number of files
uint32_t numFiles;
mFileStream.read(reinterpret_cast<char *>(&numFiles), sizeof(numFiles));
mFileIndex.clear();
for (uint32_t i = 0; i < numFiles; i++) {
// Read filename length and data
uint32_t nameLen;
mFileStream.read(reinterpret_cast<char *>(&nameLen),
sizeof(nameLen));
std::string filename(nameLen, '\0');
mFileStream.read(&filename[0], nameLen);
// Read entry data
FileEntry entry;
mFileStream.read(reinterpret_cast<char *>(&entry.offset),
sizeof(entry.offset));
mFileStream.read(reinterpret_cast<char *>(&entry.size),
sizeof(entry.size));
mFileStream.read(reinterpret_cast<char *>(&entry.timestamp),
sizeof(entry.timestamp));
mFileIndex[filename] = entry;
}
}
//-----------------------------------------------------------------------
void PackageArchive::writeIndex()
{
// Write the index and footer at the current stream position.
// The caller is responsible for positioning the write cursor
// at the correct offset (after all file data).
uint32_t numFiles = static_cast<uint32_t>(mFileIndex.size());
mFileStream.write(reinterpret_cast<const char *>(&numFiles),
sizeof(numFiles));
for (auto &[filename, entry] : mFileIndex) {
uint32_t nameLen = static_cast<uint32_t>(filename.length());
mFileStream.write(reinterpret_cast<const char *>(&nameLen),
sizeof(nameLen));
mFileStream.write(filename.c_str(), nameLen);
mFileStream.write(reinterpret_cast<const char *>(&entry.offset),
sizeof(entry.offset));
mFileStream.write(reinterpret_cast<const char *>(&entry.size),
sizeof(entry.size));
mFileStream.write(
reinterpret_cast<const char *>(&entry.timestamp),
sizeof(entry.timestamp));
}
// Write footer: index offset + magic
uint64_t indexOffset = static_cast<uint64_t>(mFileStream.tellp());
mFileStream.write(reinterpret_cast<const char *>(&indexOffset),
sizeof(indexOffset));
mFileStream.write(reinterpret_cast<const char *>(&MAGIC),
sizeof(MAGIC));
mFileStream.flush();
}
//-----------------------------------------------------------------------
void PackageArchive::rebuildPackage()
{
// Rebuild the entire package file from the in-memory index.
// Strategy: write to a temporary file, then swap.
String tmpPath = mFilePath + ".tmp";
std::ofstream tmpFile(tmpPath.c_str(),
std::ios::binary | std::ios::trunc);
if (!tmpFile.is_open()) {
OGRE_EXCEPT(Exception::ERR_CANNOT_WRITE_TO_FILE,
"Cannot create temporary file: " + tmpPath,
"PackageArchive::rebuildPackage");
}
// Write file data sequentially and build new offsets
std::map<String, FileEntry> newIndex;
uint64_t currentOffset = 0;
for (auto &[filename, entry] : mFileIndex) {
// Read the old data from the original file
std::vector<char> buffer(static_cast<size_t>(entry.size));
mFileStream.seekg(static_cast<std::streamoff>(entry.offset),
std::ios::beg);
mFileStream.read(buffer.data(), buffer.size());
// Write to temp file at new position
tmpFile.write(buffer.data(), buffer.size());
// Update entry
FileEntry newEntry;
newEntry.offset = currentOffset;
newEntry.size = entry.size;
newEntry.timestamp = entry.timestamp;
newIndex[filename] = newEntry;
currentOffset += entry.size;
}
// Write index and footer to temp file
uint32_t numFiles = static_cast<uint32_t>(newIndex.size());
tmpFile.write(reinterpret_cast<const char *>(&numFiles),
sizeof(numFiles));
for (auto &[filename, entry] : newIndex) {
uint32_t nameLen = static_cast<uint32_t>(filename.length());
tmpFile.write(reinterpret_cast<const char *>(&nameLen),
sizeof(nameLen));
tmpFile.write(filename.c_str(), nameLen);
tmpFile.write(reinterpret_cast<const char *>(&entry.offset),
sizeof(entry.offset));
tmpFile.write(reinterpret_cast<const char *>(&entry.size),
sizeof(entry.size));
tmpFile.write(reinterpret_cast<const char *>(&entry.timestamp),
sizeof(entry.timestamp));
}
uint64_t indexOffset = currentOffset;
tmpFile.write(reinterpret_cast<const char *>(&indexOffset),
sizeof(indexOffset));
tmpFile.write(reinterpret_cast<const char *>(&MAGIC), sizeof(MAGIC));
tmpFile.flush();
tmpFile.close();
// Close the original file
mFileStream.close();
// Replace the original with the temp file
if (std::rename(tmpPath.c_str(), mFilePath.c_str()) != 0) {
OGRE_EXCEPT(Exception::ERR_CANNOT_WRITE_TO_FILE,
"Cannot replace package file: " + mFilePath,
"PackageArchive::rebuildPackage");
}
// Re-open the file
mFileStream.open(mFilePath.c_str(),
std::ios::in | std::ios::out | std::ios::binary);
if (!mFileStream.is_open()) {
OGRE_EXCEPT(Exception::ERR_FILE_NOT_FOUND,
"Cannot re-open package file after rebuild: " +
mFilePath,
"PackageArchive::rebuildPackage");
}
// Update the in-memory index with new offsets
mFileIndex = newIndex;
}
//-----------------------------------------------------------------------
DataStreamPtr PackageArchive::open(const String &filename, bool readOnly) const
{
OGRE_LOCK_AUTO_MUTEX;
if (!mLoaded) {
OGRE_EXCEPT(Exception::ERR_INVALID_STATE, "Archive not loaded",
"PackageArchive::open");
}
String lookUp = filename;
// Try direct match first
auto it = mFileIndex.find(lookUp);
if (it == mFileIndex.end()) {
// Try with just the basename (non-strict mode)
String basename, path;
StringUtil::splitFilename(lookUp, basename, path);
// Search for matching basename
for (auto &[fname, entry] : mFileIndex) {
String fbasename, fpath;
StringUtil::splitFilename(fname, fbasename, fpath);
if (fbasename == basename) {
lookUp = fname;
it = mFileIndex.find(lookUp);
break;
}
}
}
if (it == mFileIndex.end()) {
OGRE_EXCEPT(Exception::ERR_FILE_NOT_FOUND,
"File not found in package: " + filename,
"PackageArchive::open");
}
const FileEntry &entry = it->second;
// Read the data from the file
auto buf = OGRE_ALLOC_T(char, entry.size, MEMCATEGORY_GENERAL);
mFileStream.seekg(static_cast<std::streamoff>(entry.offset),
std::ios::beg);
mFileStream.read(buf, entry.size);
auto ret = std::make_shared<MemoryDataStream>(
lookUp, buf, static_cast<size_t>(entry.size), true, true);
return ret;
}
//-----------------------------------------------------------------------
DataStreamPtr PackageArchive::create(const String &filename)
{
OGRE_EXCEPT(Exception::ERR_NOT_IMPLEMENTED,
"Use addFile() to add files to a package archive",
"PackageArchive::create");
}
//-----------------------------------------------------------------------
void PackageArchive::remove(const String &filename)
{
OGRE_LOCK_AUTO_MUTEX;
if (mReadOnly) {
OGRE_EXCEPT(Exception::ERR_NOT_IMPLEMENTED,
"Cannot remove file from read-only package",
"PackageArchive::remove");
}
auto it = mFileIndex.find(filename);
if (it == mFileIndex.end()) {
OGRE_EXCEPT(Exception::ERR_FILE_NOT_FOUND,
"File not found in package: " + filename,
"PackageArchive::remove");
}
mFileIndex.erase(it);
// Rebuild the entire package without the removed file
rebuildPackage();
}
//-----------------------------------------------------------------------
StringVectorPtr PackageArchive::list(bool recursive, bool dirs) const
{
OGRE_LOCK_AUTO_MUTEX;
auto ret = std::make_shared<StringVector>();
for (auto &[filename, entry] : mFileIndex) {
// In our flat archive, there are no directories
if (dirs)
continue;
if (recursive) {
ret->push_back(filename);
} else {
// Only top-level files (no '/' in path)
if (filename.find('/') == String::npos)
ret->push_back(filename);
}
}
return ret;
}
//-----------------------------------------------------------------------
FileInfoListPtr PackageArchive::listFileInfo(bool recursive, bool dirs) const
{
OGRE_LOCK_AUTO_MUTEX;
auto ret = std::make_shared<FileInfoList>();
for (auto &[filename, entry] : mFileIndex) {
if (dirs)
continue;
if (!recursive && filename.find('/') != String::npos)
continue;
FileInfo info;
info.archive = this;
info.filename = filename;
info.compressedSize = static_cast<size_t>(entry.size);
info.uncompressedSize = static_cast<size_t>(entry.size);
StringUtil::splitFilename(filename, info.basename, info.path);
ret->push_back(info);
}
return ret;
}
//-----------------------------------------------------------------------
StringVectorPtr PackageArchive::find(const String &pattern, bool recursive,
bool dirs) const
{
OGRE_LOCK_AUTO_MUTEX;
auto ret = std::make_shared<StringVector>();
bool full_match = (pattern.find('/') != String::npos) ||
(pattern.find('\\') != String::npos);
bool wildCard = pattern.find('*') != String::npos;
for (auto &[filename, entry] : mFileIndex) {
if (dirs)
continue;
if (!recursive && !full_match && !wildCard) {
// For non-recursive, non-wildcard searches,
// check exact match
String basename, path;
StringUtil::splitFilename(filename, basename, path);
if (filename == pattern || basename == pattern)
ret->push_back(filename);
continue;
}
String basename, path;
StringUtil::splitFilename(filename, basename, path);
// For non-recursive searches with wildcards, skip files in subdirectories
if (!recursive && !full_match && wildCard && !path.empty())
continue;
if (StringUtil::match(full_match ? filename : basename, pattern,
false)) {
ret->push_back(filename);
}
}
return ret;
}
//-----------------------------------------------------------------------
FileInfoListPtr PackageArchive::findFileInfo(const String &pattern,
bool recursive, bool dirs) const
{
OGRE_LOCK_AUTO_MUTEX;
auto ret = std::make_shared<FileInfoList>();
bool full_match = (pattern.find('/') != String::npos) ||
(pattern.find('\\') != String::npos);
bool wildCard = pattern.find('*') != String::npos;
for (auto &[filename, entry] : mFileIndex) {
if (dirs)
continue;
if (!recursive && !full_match && !wildCard) {
// For non-recursive, non-wildcard searches,
// check exact match
String basename, path;
StringUtil::splitFilename(filename, basename, path);
if (filename == pattern || basename == pattern) {
FileInfo info;
info.archive = this;
info.filename = filename;
info.compressedSize =
static_cast<size_t>(entry.size);
info.uncompressedSize =
static_cast<size_t>(entry.size);
info.basename = basename;
info.path = path;
ret->push_back(info);
}
continue;
}
String basename, path;
StringUtil::splitFilename(filename, basename, path);
// For non-recursive searches with wildcards, skip files in subdirectories
if (!recursive && !full_match && wildCard && !path.empty())
continue;
if (StringUtil::match(full_match ? filename : basename, pattern,
false)) {
FileInfo info;
info.archive = this;
info.filename = filename;
info.compressedSize = static_cast<size_t>(entry.size);
info.uncompressedSize = static_cast<size_t>(entry.size);
info.basename = basename;
info.path = path;
ret->push_back(info);
}
}
return ret;
}
//-----------------------------------------------------------------------
bool PackageArchive::exists(const String &filename) const
{
OGRE_LOCK_AUTO_MUTEX;
if (!mLoaded)
return false;
String lookUp = filename;
// Try direct match
if (mFileIndex.find(lookUp) != mFileIndex.end())
return true;
// Try basename match
String basename, path;
StringUtil::splitFilename(lookUp, basename, path);
for (auto &[fname, entry] : mFileIndex) {
String fbasename, fpath;
StringUtil::splitFilename(fname, fbasename, fpath);
if (fbasename == basename)
return true;
}
return false;
}
//-----------------------------------------------------------------------
time_t PackageArchive::getModifiedTime(const String &filename) const
{
OGRE_LOCK_AUTO_MUTEX;
auto it = mFileIndex.find(filename);
if (it == mFileIndex.end()) {
// Fall back to the package file's modification time
struct stat tagStat;
if (stat(mFilePath.c_str(), &tagStat) == 0)
return tagStat.st_mtime;
return 0;
}
return static_cast<time_t>(it->second.timestamp);
}
//-----------------------------------------------------------------------
void PackageArchive::addFile(const String &filename, const void *data,
size_t size, int64_t timestamp)
{
OGRE_LOCK_AUTO_MUTEX;
if (mReadOnly) {
OGRE_EXCEPT(Exception::ERR_NOT_IMPLEMENTED,
"Cannot add file to read-only package",
"PackageArchive::addFile");
}
if (!mLoaded) {
load();
// After loading a non-existent file, it's writable
mReadOnly = false;
}
// Create new entry
FileEntry entry;
entry.offset = 0; // Will be set during rebuild
entry.size = size;
entry.timestamp = (timestamp != 0) ? timestamp : time(nullptr);
// If the file already exists, we need to replace it
auto it = mFileIndex.find(filename);
if (it != mFileIndex.end()) {
// Replace existing entry
it->second = entry;
} else {
mFileIndex[filename] = entry;
}
// Rebuild the entire package with the new file data
// We need to write the new data into the rebuilt file.
// rebuildPackage reads from the old file, so we need to handle
// the new data separately. We'll do a custom rebuild.
String tmpPath = mFilePath + ".tmp";
std::ofstream tmpFile(tmpPath.c_str(),
std::ios::binary | std::ios::trunc);
if (!tmpFile.is_open()) {
OGRE_EXCEPT(Exception::ERR_CANNOT_WRITE_TO_FILE,
"Cannot create temporary file: " + tmpPath,
"PackageArchive::addFile");
}
// Write file data sequentially and build new offsets
std::map<String, FileEntry> newIndex;
uint64_t currentOffset = 0;
for (auto &[fname, fentry] : mFileIndex) {
if (fname == filename) {
// This is the new/updated file - write the provided
// data
tmpFile.write(static_cast<const char *>(data),
static_cast<std::streamsize>(size));
FileEntry newEntry;
newEntry.offset = currentOffset;
newEntry.size = size;
newEntry.timestamp = entry.timestamp;
newIndex[fname] = newEntry;
currentOffset += size;
} else {
// Read existing data from the original file
std::vector<char> buffer(
static_cast<size_t>(fentry.size));
mFileStream.seekg(
static_cast<std::streamoff>(fentry.offset),
std::ios::beg);
mFileStream.read(buffer.data(), buffer.size());
// Write to temp file
tmpFile.write(buffer.data(), buffer.size());
FileEntry newEntry;
newEntry.offset = currentOffset;
newEntry.size = fentry.size;
newEntry.timestamp = fentry.timestamp;
newIndex[fname] = newEntry;
currentOffset += fentry.size;
}
}
// Write index and footer to temp file
uint32_t numFiles = static_cast<uint32_t>(newIndex.size());
tmpFile.write(reinterpret_cast<const char *>(&numFiles),
sizeof(numFiles));
for (auto &[fname, fentry] : newIndex) {
uint32_t nameLen = static_cast<uint32_t>(fname.length());
tmpFile.write(reinterpret_cast<const char *>(&nameLen),
sizeof(nameLen));
tmpFile.write(fname.c_str(), nameLen);
tmpFile.write(reinterpret_cast<const char *>(&fentry.offset),
sizeof(fentry.offset));
tmpFile.write(reinterpret_cast<const char *>(&fentry.size),
sizeof(fentry.size));
tmpFile.write(reinterpret_cast<const char *>(&fentry.timestamp),
sizeof(fentry.timestamp));
}
uint64_t indexOffset = currentOffset;
tmpFile.write(reinterpret_cast<const char *>(&indexOffset),
sizeof(indexOffset));
tmpFile.write(reinterpret_cast<const char *>(&MAGIC), sizeof(MAGIC));
tmpFile.flush();
tmpFile.close();
// Close the original file
mFileStream.close();
// Replace the original with the temp file
if (std::rename(tmpPath.c_str(), mFilePath.c_str()) != 0) {
OGRE_EXCEPT(Exception::ERR_CANNOT_WRITE_TO_FILE,
"Cannot replace package file: " + mFilePath,
"PackageArchive::addFile");
}
// Re-open the file
mFileStream.open(mFilePath.c_str(),
std::ios::in | std::ios::out | std::ios::binary);
if (!mFileStream.is_open()) {
OGRE_EXCEPT(Exception::ERR_FILE_NOT_FOUND,
"Cannot re-open package file after add: " +
mFilePath,
"PackageArchive::addFile");
}
// Update the in-memory index with new offsets
mFileIndex = newIndex;
}
//-----------------------------------------------------------------------
void PackageArchive::addFileFromDisk(const String &destFilename,
const String &sourcePath)
{
std::ifstream source(sourcePath.c_str(),
std::ios::binary | std::ios::ate);
if (!source.is_open()) {
OGRE_EXCEPT(Exception::ERR_FILE_NOT_FOUND,
"Cannot open source file: " + sourcePath,
"PackageArchive::addFileFromDisk");
}
std::streamsize size = source.tellg();
source.seekg(0, std::ios::beg);
std::vector<char> buffer(static_cast<size_t>(size));
source.read(buffer.data(), size);
source.close();
// Get file modification time
struct stat tagStat;
int64_t timestamp = 0;
if (stat(sourcePath.c_str(), &tagStat) == 0) {
timestamp = static_cast<int64_t>(tagStat.st_mtime);
}
addFile(destFilename, buffer.data(), static_cast<size_t>(size),
timestamp);
}
//-----------------------------------------------------------------------
bool PackageArchive::extractFile(const String &filename,
const String &destPath) const
{
OGRE_LOCK_AUTO_MUTEX;
auto it = mFileIndex.find(filename);
if (it == mFileIndex.end())
return false;
const FileEntry &entry = it->second;
std::vector<char> buffer(static_cast<size_t>(entry.size));
mFileStream.seekg(static_cast<std::streamoff>(entry.offset),
std::ios::beg);
mFileStream.read(buffer.data(), buffer.size());
std::ofstream dest(destPath.c_str(), std::ios::binary);
if (!dest.is_open())
return false;
dest.write(buffer.data(), buffer.size());
dest.close();
return true;
}
//-----------------------------------------------------------------------
StringVector PackageArchive::listFiles() const
{
OGRE_LOCK_AUTO_MUTEX;
StringVector result;
for (auto &[filename, entry] : mFileIndex) {
result.push_back(filename);
}
return result;
}
//-----------------------------------------------------------------------
// PackageArchiveFactory
//-----------------------------------------------------------------------
Archive *PackageArchiveFactory::createInstance(const String &name,
bool readOnly)
{
// Package archives support both read-only and read-write access
return OGRE_NEW PackageArchive(name, getType());
}
//-----------------------------------------------------------------------
const String &PackageArchiveFactory::getType(void) const
{
static String name = "Package";
return name;
}
} // namespace Ogre
@@ -0,0 +1,223 @@
/*
* -----------------------------------------------------------------------------
* This source file is part of OGRE
* (Object-oriented Graphics Rendering Engine)
* For the latest info, see http://www.ogre3d.org/
*
* Copyright (c) 2000-2014 Torus Knot Software Ltd
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* -----------------------------------------------------------------------------
*/
#ifndef __OgrePackageArchive_H__
#define __OgrePackageArchive_H__
#include "OgrePrerequisites.h"
#include "OgreArchive.h"
#include "OgreArchiveFactory.h"
#include "OgreHeaderPrefix.h"
#include "Threading/OgreThreadHeaders.h"
#include <fstream>
#include <map>
#include <vector>
#include <cstdint>
namespace Ogre
{
/** \addtogroup Core
* @{
*/
/** \addtogroup Resources
* @{
*/
/**
* Package archive format - an uncompressed indexed file container.
*
* The .package format is a simple archive format similar to Unity's asset
* bundles. It stores files sequentially with a file index at the end,
* allowing fast random access without compression overhead.
*
* Format layout:
* [File 1 Data]
* [File 2 Data]
* ...
* [File N Data]
* [Index Block]
* - Number of files (uint32_t)
* - For each file:
* - Filename length (uint32_t)
* - Filename data
* - Offset (uint64_t)
* - Size (uint64_t)
* - Timestamp (int64_t)
* [Magic Footer]
* - Index offset (uint64_t)
* - Magic number "PACK" (uint32_t)
*/
class _OgreExport PackageArchive : public Archive {
public:
struct FileEntry {
uint64_t offset;
uint64_t size;
int64_t timestamp;
};
protected:
/// File path to the .package file on disk
String mFilePath;
/// File stream for reading/writing
mutable std::fstream mFileStream;
/// File index: filename -> FileEntry
typedef std::map<String, FileEntry> FileIndex;
FileIndex mFileIndex;
/// Whether the archive is loaded
bool mLoaded;
/// Magic number and format constants
static const uint32_t MAGIC;
static const uint32_t FORMAT_VERSION;
OGRE_AUTO_MUTEX;
/** Read the index from the package file. */
void readIndex();
/** Write the index to the package file.
* This rewrites the entire file: data blocks followed by index and footer.
*/
void writeIndex();
/** Rebuild the package file from the current in-memory index.
* This is used after add/remove operations to produce a clean file.
*/
void rebuildPackage();
public:
PackageArchive(const String &name, const String &archType);
~PackageArchive();
/// @copydoc Archive::isCaseSensitive
bool isCaseSensitive(void) const override
{
return false;
}
/// @copydoc Archive::load
void load() override;
/// @copydoc Archive::unload
void unload() override;
/// @copydoc Archive::isReadOnly
bool isReadOnly() const override
{
return mReadOnly;
}
/// @copydoc Archive::open
DataStreamPtr open(const String &filename,
bool readOnly = true) const override;
/// @copydoc Archive::create
DataStreamPtr create(const String &filename) override;
/// @copydoc Archive::remove
void remove(const String &filename) override;
/// @copydoc Archive::list
StringVectorPtr list(bool recursive = true,
bool dirs = false) const override;
/// @copydoc Archive::listFileInfo
FileInfoListPtr listFileInfo(bool recursive = true,
bool dirs = false) const override;
/// @copydoc Archive::find
StringVectorPtr find(const String &pattern, bool recursive = true,
bool dirs = false) const override;
/// @copydoc Archive::findFileInfo
FileInfoListPtr findFileInfo(const String &pattern,
bool recursive = true,
bool dirs = false) const override;
/// @copydoc Archive::exists
bool exists(const String &filename) const override;
/// @copydoc Archive::getModifiedTime
time_t getModifiedTime(const String &filename) const override;
/** Add a file from an external source into the package.
* @param filename The name to store the file as in the archive
* @param data Pointer to the file data
* @param size Size of the data in bytes
* @param timestamp Modification time (0 = use current time)
*/
void addFile(const String &filename, const void *data, size_t size,
int64_t timestamp = 0);
/** Add a file from the filesystem into the package.
* @param destFilename The name to store the file as in the archive
* @param sourcePath Path to the source file on disk
*/
void addFileFromDisk(const String &destFilename,
const String &sourcePath);
/** Extract a file from the package to the filesystem.
* @param filename The name of the file in the archive
* @param destPath Path to write the extracted file to
* @return true if successful
*/
bool extractFile(const String &filename, const String &destPath) const;
/** List all files in the package index. */
StringVector listFiles() const;
};
/** Factory for creating PackageArchive instances. */
class _OgreExport PackageArchiveFactory : public ArchiveFactory {
public:
virtual ~PackageArchiveFactory()
{
}
/// @copydoc FactoryObj::getType
const String &getType(void) const override;
//! @cond Doxygen_Suppress
using ArchiveFactory::createInstance;
//! @endcond
Archive *createInstance(const String &name, bool readOnly) override;
};
/** @} */
/** @} */
} // namespace Ogre
#include "OgreHeaderSuffix.h"
#endif // __OgrePackageArchive_H__
@@ -0,0 +1,374 @@
/*
* PackageTool - Command-line tool for creating and managing .package archives
*
* Usage:
* PackageTool create <package.package> <file1> [file2 ...]
* PackageTool add <package.package> <file1> [file2 ...]
* PackageTool remove <package.package> <file1> [file2 ...]
* PackageTool extract <package.package> <file1> [dest]
* PackageTool extract-all <package.package> [dest-dir]
* PackageTool list <package.package>
* PackageTool info <package.package>
*/
#include "OgrePackageArchive.h"
#include "OgreException.h"
#include <cstring>
#include <filesystem>
#include <iostream>
#include <vector>
namespace fs = std::filesystem;
static void printUsage(const char *prog)
{
std::cerr << "Usage:" << std::endl;
std::cerr << " " << prog
<< " create <package.package> <file1> [file2 ...]"
<< std::endl;
std::cerr << " " << prog
<< " add <package.package> <file1> [file2 ...]" << std::endl;
std::cerr << " " << prog
<< " remove <package.package> <file1> [file2 ...]"
<< std::endl;
std::cerr << " " << prog << " extract <package.package> <file1> [dest]"
<< std::endl;
std::cerr << " " << prog << " extract-all <package.package> [dest-dir]"
<< std::endl;
std::cerr << " " << prog << " list <package.package>" << std::endl;
std::cerr << " " << prog << " info <package.package>" << std::endl;
std::cerr << std::endl;
std::cerr << "Notes:" << std::endl;
std::cerr << " - For 'create' and 'add', if a source path is a "
<< "directory, its contents are added recursively."
<< std::endl;
std::cerr << " - Files are stored with their relative path when "
<< "added from subdirectories." << std::endl;
}
/**
* Add files from a source path to the archive.
* If the source is a directory, it is added recursively.
* The destName is the name to store the file as in the archive.
* For directories, destName is used as a prefix for contained files.
*/
static void addPathToArchive(Ogre::PackageArchive &archive,
const fs::path &sourcePath,
const std::string &destPrefix = "")
{
std::error_code ec;
if (fs::is_directory(sourcePath, ec)) {
// Recursively add directory contents
for (const auto &entry :
fs::recursive_directory_iterator(sourcePath, ec)) {
if (fs::is_regular_file(entry.path(), ec)) {
// Compute relative path from the source
// directory
fs::path relativePath = fs::relative(
entry.path(), sourcePath, ec);
std::string destName =
destPrefix.empty() ?
relativePath.string() :
destPrefix + "/" +
relativePath.string();
// Normalize path separators to forward slash
for (auto &c : destName) {
if (c == '\\')
c = '/';
}
std::cout << "Adding: " << entry.path().string()
<< " -> " << destName << std::endl;
archive.addFileFromDisk(destName,
entry.path().string());
}
}
} else if (fs::is_regular_file(sourcePath, ec)) {
// Single file
std::string destName = destPrefix.empty() ?
sourcePath.filename().string() :
destPrefix;
std::cout << "Adding: " << sourcePath.string() << " -> "
<< destName << std::endl;
archive.addFileFromDisk(destName, sourcePath.string());
} else {
std::cerr << "Warning: skipping '" << sourcePath.string()
<< "' - not a regular file or directory" << std::endl;
}
}
static int cmdCreate(const std::vector<std::string> &args)
{
if (args.size() < 2) {
std::cerr << "Error: create requires a package path and at "
"least one source file"
<< std::endl;
return 1;
}
const std::string &packagePath = args[0];
Ogre::PackageArchive archive(packagePath, "Package");
// Load will create the file if it doesn't exist
archive.load();
for (size_t i = 1; i < args.size(); i++) {
addPathToArchive(archive, args[i]);
}
std::cout << "Created package: " << packagePath << " with "
<< archive.listFiles().size() << " file(s)" << std::endl;
return 0;
}
static int cmdAdd(const std::vector<std::string> &args)
{
if (args.size() < 2) {
std::cerr << "Error: add requires a package path and at least "
"one source file"
<< std::endl;
return 1;
}
const std::string &packagePath = args[0];
Ogre::PackageArchive archive(packagePath, "Package");
archive.load();
for (size_t i = 1; i < args.size(); i++) {
addPathToArchive(archive, args[i]);
}
std::cout << "Added files to " << packagePath
<< " (total: " << archive.listFiles().size() << " file(s))"
<< std::endl;
return 0;
}
static int cmdRemove(const std::vector<std::string> &args)
{
if (args.size() < 2) {
std::cerr << "Error: remove requires a package path and at "
"least one filename"
<< std::endl;
return 1;
}
const std::string &packagePath = args[0];
Ogre::PackageArchive archive(packagePath, "Package");
archive.load();
for (size_t i = 1; i < args.size(); i++) {
const std::string &filename = args[i];
std::cout << "Removing: " << filename << std::endl;
archive.remove(filename);
}
std::cout << "Removed " << args.size() - 1 << " file(s) from "
<< packagePath << std::endl;
return 0;
}
static int cmdExtract(const std::vector<std::string> &args)
{
if (args.size() < 2) {
std::cerr << "Error: extract requires a package path and a "
"filename"
<< std::endl;
return 1;
}
const std::string &packagePath = args[0];
const std::string &filename = args[1];
std::string destPath;
if (args.size() >= 3) {
destPath = args[2];
} else {
destPath = filename;
}
Ogre::PackageArchive archive(packagePath, "Package");
archive.load();
if (archive.extractFile(filename, destPath)) {
std::cout << "Extracted: " << filename << " -> " << destPath
<< std::endl;
return 0;
} else {
std::cerr << "Error: could not extract " << filename
<< std::endl;
return 1;
}
}
static int cmdExtractAll(const std::vector<std::string> &args)
{
if (args.size() < 1) {
std::cerr << "Error: extract-all requires a package path"
<< std::endl;
return 1;
}
const std::string &packagePath = args[0];
std::string destDir;
if (args.size() >= 2) {
destDir = args[1];
} else {
destDir = ".";
}
Ogre::PackageArchive archive(packagePath, "Package");
archive.load();
Ogre::StringVector files = archive.listFiles();
if (files.empty()) {
std::cout << "Package is empty." << std::endl;
return 0;
}
// Create destination directory if needed
fs::create_directories(destDir);
for (const auto &filename : files) {
// Preserve directory structure in the extracted path
fs::path filePath(filename);
fs::path fullDestPath = fs::path(destDir) / filePath;
// Create parent directories as needed
fs::create_directories(fullDestPath.parent_path());
if (archive.extractFile(filename, fullDestPath.string())) {
std::cout << "Extracted: " << filename << " -> "
<< fullDestPath.string() << std::endl;
} else {
std::cerr << "Error: could not extract " << filename
<< std::endl;
}
}
return 0;
}
static int cmdList(const std::vector<std::string> &args)
{
if (args.size() < 1) {
std::cerr << "Error: list requires a package path" << std::endl;
return 1;
}
const std::string &packagePath = args[0];
Ogre::PackageArchive archive(packagePath, "Package");
archive.load();
Ogre::StringVector files = archive.listFiles();
std::cout << "Files in " << packagePath << ":" << std::endl;
if (files.empty()) {
std::cout << " (empty)" << std::endl;
} else {
for (const auto &filename : files) {
std::cout << " " << filename << std::endl;
}
}
std::cout << "Total: " << files.size() << " file(s)" << std::endl;
return 0;
}
static int cmdInfo(const std::vector<std::string> &args)
{
if (args.size() < 1) {
std::cerr << "Error: info requires a package path" << std::endl;
return 1;
}
const std::string &packagePath = args[0];
Ogre::PackageArchive archive(packagePath, "Package");
archive.load();
Ogre::StringVector files = archive.listFiles();
std::cout << "Package: " << packagePath << std::endl;
std::cout << "Type: Ogre Package Archive" << std::endl;
std::cout << "Files: " << files.size() << std::endl;
uint64_t totalSize = 0;
for (const auto &filename : files) {
// Get file info via listFileInfo
Ogre::FileInfoListPtr infoList =
archive.findFileInfo(filename, false, false);
if (infoList && !infoList->empty()) {
totalSize += infoList->at(0).uncompressedSize;
}
}
std::cout << "Total uncompressed size: " << totalSize << " bytes"
<< std::endl;
std::cout << std::endl;
std::cout << "Files:" << std::endl;
for (const auto &filename : files) {
Ogre::FileInfoListPtr infoList =
archive.findFileInfo(filename, false, false);
if (infoList && !infoList->empty()) {
const auto &info = infoList->at(0);
std::cout << " " << filename << " ("
<< info.uncompressedSize << " bytes)"
<< std::endl;
} else {
std::cout << " " << filename << std::endl;
}
}
return 0;
}
int main(int argc, char **argv)
{
if (argc < 2) {
printUsage(argv[0]);
return 1;
}
std::string command = argv[1];
std::vector<std::string> args;
for (int i = 2; i < argc; i++) {
args.push_back(argv[i]);
}
try {
if (command == "create") {
return cmdCreate(args);
} else if (command == "add") {
return cmdAdd(args);
} else if (command == "remove") {
return cmdRemove(args);
} else if (command == "extract") {
return cmdExtract(args);
} else if (command == "extract-all") {
return cmdExtractAll(args);
} else if (command == "list") {
return cmdList(args);
} else if (command == "info") {
return cmdInfo(args);
} else {
std::cerr << "Error: unknown command '" << command
<< "'" << std::endl;
printUsage(argv[0]);
return 1;
}
} catch (const Ogre::Exception &e) {
std::cerr << "Ogre Error: " << e.getFullDescription()
<< std::endl;
return 1;
} catch (const std::exception &e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}
+135
View File
@@ -0,0 +1,135 @@
# Ogre Package Archive (.package)
A simple uncompressed indexed file container format for OGRE3D, similar to Unity asset bundles.
## Format
The `.package` format stores files sequentially with a file index at the end:
```
[File 1 Data]
[File 2 Data]
...
[File N Data]
[Index Block]
- Number of files (uint32_t)
- For each file:
- Filename length (uint32_t)
- Filename data (UTF-8)
- Offset in file (uint64_t)
- Size in bytes (uint64_t)
- Timestamp (int64_t)
[Footer]
- Index offset (uint64_t) - points to start of Index Block
- Magic number "PACK" (uint32_t) = 0x4B434150
```
## Features
- **No compression** - Files are stored as-is for fast access
- **Indexed** - O(1) lookup by filename via in-memory index
- **Random access** - Files can be read individually without scanning
- **Persistent** - Index is stored in the file footer for fast loading
- **Mutable** - Supports add, remove, and replace operations
- **OGRE Archive interface** - Usable from `resources.cfg` like Zip archives
## Usage from resources.cfg
```
[General]
Package=path/to/resources.package
```
The archive type is registered as `"Package"`.
## PackageTool CLI
The `PackageTool` executable provides command-line management of `.package` files:
```
PackageTool create <package.package> <file1> [file2 ...]
Create a new package containing the specified files.
PackageTool add <package.package> <file1> [file2 ...]
Add files to an existing package.
PackageTool remove <package.package> <file1> [file2 ...]
Remove files from a package.
PackageTool extract <package.package> <file1> [dest]
Extract a single file from a package. If dest is omitted,
the file is extracted to the current directory with its
original filename.
PackageTool extract-all <package.package> [dest-dir]
Extract all files from a package. If dest-dir is omitted,
files are extracted to the current directory.
PackageTool list <package.package>
List all files in a package.
PackageTool info <package.package>
Show detailed information about a package.
```
### Notes
- For `create` and `add`, if a source path is a directory, its contents are added recursively.
- Files are stored with their relative path when added from subdirectories.
- If a file with the same name already exists in the package, it is replaced.
## C++ API
```cpp
#include "OgrePackageArchive.h"
// Create or open a package
Ogre::PackageArchive archive("my.package", "Package");
archive.load();
// Add a file from memory
const char *data = "Hello, World!";
archive.addFile("hello.txt", data, strlen(data));
// Add a file from disk
archive.addFileFromDisk("stored.txt", "/path/to/source.txt");
// Open and read a file
auto stream = archive.open("hello.txt");
std::vector<char> buf(stream->size());
stream->read(buf.data(), stream->size());
// List files
auto files = archive.list(true, false); // recursive, no dirs
for (const auto &f : *files) {
std::cout << f << std::endl;
}
// Find files by pattern
auto found = archive.find("*.txt", true, false);
// Check if file exists
bool exists = archive.exists("hello.txt");
// Remove a file
archive.remove("hello.txt");
// Extract to disk
archive.extractFile("hello.txt", "/tmp/hello.txt");
// Get modification time
time_t mtime = archive.getModifiedTime("hello.txt");
// Unload
archive.unload();
```
## Building
The library is built as a static library target `PackageArchive`:
```cmake
target_link_libraries(my_target PackageArchive OgreMain)
```
The `PackageTool` executable is built as a separate target.
@@ -0,0 +1,654 @@
/**
* @file package_archive_test.cpp
* @brief Tests for the Package Archive library.
*
* Tests the Ogre::PackageArchive implementation including:
* - Creating packages
* - Adding files
* - Listing files
* - Finding files
* - Opening/reading files
* - Removing files
* - Extracting files
* - Error handling
* - Edge cases (empty packages, duplicate files, etc.)
*/
#include "OgrePackageArchive.h"
#include "OgreException.h"
#include <cassert>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
// Test utilities
static int testsPassed = 0;
static int testsFailed = 0;
#define TEST(name) \
static void test_##name(); \
struct TestRegister_##name { \
TestRegister_##name() \
{ \
test_##name(); \
} \
} testReg_##name; \
static void test_##name()
#define ASSERT(cond) \
do { \
if (!(cond)) { \
std::cerr << "FAIL: " << #cond << " at " << __FILE__ \
<< ":" << __LINE__ << std::endl; \
testsFailed++; \
return; \
} \
} while (0)
#define ASSERT_EQ(a, b) \
do { \
if ((a) != (b)) { \
std::cerr << "FAIL: " << #a << " == " << #b << " at " \
<< __FILE__ << ":" << __LINE__ << " (" \
<< (a) << " != " << (b) << ")" << std::endl; \
testsFailed++; \
return; \
} \
} while (0)
#define ASSERT_THROW(expr, exc_code) \
do { \
bool caught = false; \
try { \
expr; \
} catch (const Ogre::Exception &) { \
caught = true; \
} catch (...) { \
} \
if (!caught) { \
std::cerr << "FAIL: expected Ogre::Exception at " \
<< __FILE__ << ":" << __LINE__ << std::endl; \
testsFailed++; \
return; \
} \
} while (0)
// Helper: create a test file on disk
static void createTestFile(const std::string &path, const std::string &content)
{
std::ofstream f(path.c_str(), std::ios::binary);
f.write(content.data(), content.size());
f.close();
}
// Helper: read a file from disk
static std::string readTestFile(const std::string &path)
{
std::ifstream f(path.c_str(), std::ios::binary | std::ios::ate);
if (!f.is_open())
return "";
std::streamsize size = f.tellg();
f.seekg(0, std::ios::beg);
std::string content(static_cast<size_t>(size), '\0');
f.read(&content[0], size);
return content;
}
// Helper: remove a file
static void removeTestFile(const std::string &path)
{
std::remove(path.c_str());
}
// -----------------------------------------------------------------------
// Test: Create an empty package
// -----------------------------------------------------------------------
TEST(createEmptyPackage)
{
const std::string pkgPath = "/tmp/test_empty.package";
// Create empty package via the factory
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
// Should be loaded and writable
ASSERT_EQ(archive.isReadOnly(), false);
// List should be empty
auto files = archive.list(true, false);
ASSERT(files);
ASSERT_EQ(files->size(), 0u);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: createEmptyPackage" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Create package with files
// -----------------------------------------------------------------------
TEST(createPackageWithFiles)
{
const std::string pkgPath = "/tmp/test_with_files.package";
const std::string data1 = "Hello World";
const std::string data2 = "Test file 2 content here";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("file1.txt", data1.data(), data1.size());
archive.addFile("subdir/file2.txt", data2.data(), data2.size());
// List all files (recursive)
auto files = archive.list(true, false);
ASSERT(files);
ASSERT_EQ(files->size(), 2u);
// List non-recursive should only show top-level
auto topFiles = archive.list(false, false);
ASSERT(topFiles);
ASSERT_EQ(topFiles->size(), 1u);
ASSERT_EQ((*topFiles)[0], "file1.txt");
// List file info
auto infoList = archive.listFileInfo(true, false);
ASSERT(infoList);
ASSERT_EQ(infoList->size(), 2u);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: createPackageWithFiles" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Open and read files from package
// -----------------------------------------------------------------------
TEST(openAndReadFiles)
{
const std::string pkgPath = "/tmp/test_read.package";
const std::string data1 = "Content of file 1";
const std::string data2 = "Content of file 2 with more data";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("readme.txt", data1.data(), data1.size());
archive.addFile("data.bin", data2.data(), data2.size());
// Open and read file 1
auto stream1 = archive.open("readme.txt");
ASSERT(stream1);
ASSERT_EQ(stream1->size(), data1.size());
std::string read1(static_cast<size_t>(stream1->size()), '\0');
stream1->read(&read1[0], static_cast<size_t>(stream1->size()));
ASSERT_EQ(read1, data1);
// Open and read file 2
auto stream2 = archive.open("data.bin");
ASSERT(stream2);
ASSERT_EQ(stream2->size(), data2.size());
std::string read2(static_cast<size_t>(stream2->size()), '\0');
stream2->read(&read2[0], static_cast<size_t>(stream2->size()));
ASSERT_EQ(read2, data2);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: openAndReadFiles" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Find files by pattern
// -----------------------------------------------------------------------
TEST(findFiles)
{
const std::string pkgPath = "/tmp/test_find.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("alpha.txt", "a", 1);
archive.addFile("beta.txt", "b", 1);
archive.addFile("gamma.dat", "c", 1);
archive.addFile("subdir/delta.txt", "d", 1);
// Find by exact name
auto found = archive.find("beta.txt", false, false);
ASSERT(found);
ASSERT_EQ(found->size(), 1u);
ASSERT_EQ((*found)[0], "beta.txt");
// Find by wildcard
auto wildFound = archive.find("*.txt", true, false);
ASSERT(wildFound);
ASSERT_EQ(wildFound->size(), 3u);
// Find by wildcard non-recursive
auto wildTop = archive.find("*.txt", false, false);
ASSERT(wildTop);
ASSERT_EQ(wildTop->size(), 2u);
// Find non-existent
auto notFound = archive.find("nonexistent.txt", false, false);
ASSERT(notFound);
ASSERT_EQ(notFound->size(), 0u);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: findFiles" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Exists
// -----------------------------------------------------------------------
TEST(exists)
{
const std::string pkgPath = "/tmp/test_exists.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("present.txt", "data", 4);
ASSERT(archive.exists("present.txt"));
ASSERT(!archive.exists("missing.txt"));
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: exists" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Remove files
// -----------------------------------------------------------------------
TEST(removeFiles)
{
const std::string pkgPath = "/tmp/test_remove.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("keep.txt", "keep", 4);
archive.addFile("remove.txt", "remove", 6);
ASSERT_EQ(archive.list(true, false)->size(), 2u);
archive.remove("remove.txt");
ASSERT_EQ(archive.list(true, false)->size(), 1u);
ASSERT(archive.exists("keep.txt"));
ASSERT(!archive.exists("remove.txt"));
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: removeFiles" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Extract files
// -----------------------------------------------------------------------
TEST(extractFiles)
{
const std::string pkgPath = "/tmp/test_extract.package";
const std::string data = "Extract this content!";
const std::string extractPath = "/tmp/extracted_test.txt";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("extract_me.txt", data.data(), data.size());
bool result = archive.extractFile("extract_me.txt", extractPath);
ASSERT(result);
std::string extracted = readTestFile(extractPath);
ASSERT_EQ(extracted, data);
removeTestFile(extractPath);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: extractFiles" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Add file from disk
// -----------------------------------------------------------------------
TEST(addFileFromDisk)
{
const std::string pkgPath = "/tmp/test_add_disk.package";
const std::string srcPath = "/tmp/source_file.txt";
const std::string srcData = "File from disk content";
createTestFile(srcPath, srcData);
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFileFromDisk("stored_file.txt", srcPath);
ASSERT(archive.exists("stored_file.txt"));
auto stream = archive.open("stored_file.txt");
ASSERT(stream);
ASSERT_EQ(stream->size(), srcData.size());
removeTestFile(srcPath);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: addFileFromDisk" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Persistence (save and reload)
// -----------------------------------------------------------------------
TEST(persistence)
{
const std::string pkgPath = "/tmp/test_persist.package";
const std::string data1 = "Persistent data 1";
const std::string data2 = "Persistent data 2";
// Create and add files
{
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("persist1.txt", data1.data(), data1.size());
archive.addFile("persist2.txt", data2.data(), data2.size());
archive.unload();
}
// Re-open and verify
{
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
ASSERT_EQ(archive.list(true, false)->size(), 2u);
ASSERT(archive.exists("persist1.txt"));
ASSERT(archive.exists("persist2.txt"));
auto stream1 = archive.open("persist1.txt");
ASSERT(stream1);
std::string read1(static_cast<size_t>(stream1->size()), '\0');
stream1->read(&read1[0], static_cast<size_t>(stream1->size()));
ASSERT_EQ(read1, data1);
auto stream2 = archive.open("persist2.txt");
ASSERT(stream2);
std::string read2(static_cast<size_t>(stream2->size()), '\0');
stream2->read(&read2[0], static_cast<size_t>(stream2->size()));
ASSERT_EQ(read2, data2);
archive.unload();
}
removeTestFile(pkgPath);
std::cout << " PASS: persistence" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Replace existing file
// -----------------------------------------------------------------------
TEST(replaceFile)
{
const std::string pkgPath = "/tmp/test_replace.package";
const std::string oldData = "Old content";
const std::string newData = "New content that is longer";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("replace.txt", oldData.data(), oldData.size());
archive.addFile("replace.txt", newData.data(), newData.size());
// Should only have one file with the new content
ASSERT_EQ(archive.list(true, false)->size(), 1u);
auto stream = archive.open("replace.txt");
ASSERT(stream);
ASSERT_EQ(stream->size(), newData.size());
std::string readData(static_cast<size_t>(stream->size()), '\0');
stream->read(&readData[0], static_cast<size_t>(stream->size()));
ASSERT_EQ(readData, newData);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: replaceFile" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Error handling - open non-existent file
// -----------------------------------------------------------------------
TEST(openNonExistent)
{
const std::string pkgPath = "/tmp/test_noexist.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
ASSERT_THROW(archive.open("nonexistent.txt"),
Ogre::Exception);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: openNonExistent" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Error handling - remove non-existent file
// -----------------------------------------------------------------------
TEST(removeNonExistent)
{
const std::string pkgPath = "/tmp/test_remove_noexist.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
ASSERT_THROW(archive.remove("nonexistent.txt"),
Ogre::Exception);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: removeNonExistent" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Binary data integrity
// -----------------------------------------------------------------------
TEST(binaryData)
{
const std::string pkgPath = "/tmp/test_binary.package";
// Create binary data with null bytes and all byte values
std::vector<char> binaryData(256);
for (int i = 0; i < 256; i++)
binaryData[i] = static_cast<char>(i);
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("binary.bin", binaryData.data(), binaryData.size());
auto stream = archive.open("binary.bin");
ASSERT(stream);
ASSERT_EQ(stream->size(), binaryData.size());
std::vector<char> readData(static_cast<size_t>(stream->size()));
stream->read(readData.data(), stream->size());
// Verify all bytes
for (size_t i = 0; i < binaryData.size(); i++) {
ASSERT_EQ(static_cast<unsigned char>(readData[i]),
static_cast<unsigned char>(binaryData[i]));
}
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: binaryData" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Multiple add/remove cycles
// -----------------------------------------------------------------------
TEST(multipleCycles)
{
const std::string pkgPath = "/tmp/test_cycles.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
// Add files
archive.addFile("a.txt", "a", 1);
archive.addFile("b.txt", "b", 1);
archive.addFile("c.txt", "c", 1);
ASSERT_EQ(archive.list(true, false)->size(), 3u);
// Remove one
archive.remove("b.txt");
ASSERT_EQ(archive.list(true, false)->size(), 2u);
// Add another
archive.addFile("d.txt", "d", 1);
ASSERT_EQ(archive.list(true, false)->size(), 3u);
// Remove another
archive.remove("a.txt");
ASSERT_EQ(archive.list(true, false)->size(), 2u);
// Verify remaining
ASSERT(archive.exists("c.txt"));
ASSERT(archive.exists("d.txt"));
ASSERT(!archive.exists("a.txt"));
ASSERT(!archive.exists("b.txt"));
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: multipleCycles" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Large file
// -----------------------------------------------------------------------
TEST(largeFile)
{
const std::string pkgPath = "/tmp/test_large.package";
// Create 1MB of data
const size_t largeSize = 1024 * 1024;
std::vector<char> largeData(largeSize);
for (size_t i = 0; i < largeSize; i++)
largeData[i] = static_cast<char>(i & 0xFF);
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("large.bin", largeData.data(), largeData.size());
auto stream = archive.open("large.bin");
ASSERT(stream);
ASSERT_EQ(stream->size(), largeSize);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: largeFile" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: getModifiedTime
// -----------------------------------------------------------------------
TEST(modifiedTime)
{
const std::string pkgPath = "/tmp/test_mtime.package";
Ogre::PackageArchive archive(pkgPath, "Package");
archive.load();
archive.addFile("timestamped.txt", "data", 4);
time_t mtime = archive.getModifiedTime("timestamped.txt");
ASSERT(mtime > 0);
// Non-existent file should return 0 or the package file's mtime
time_t missingMtime = archive.getModifiedTime("missing.txt");
ASSERT(missingMtime > 0);
archive.unload();
removeTestFile(pkgPath);
std::cout << " PASS: modifiedTime" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Test: Factory creation
// -----------------------------------------------------------------------
TEST(factory)
{
Ogre::PackageArchiveFactory factory;
const std::string &type = factory.getType();
ASSERT_EQ(type, "Package");
Ogre::Archive *archive =
factory.createInstance("/tmp/test_factory.package", false);
ASSERT(archive);
ASSERT_EQ(archive->getType(), "Package");
delete archive;
removeTestFile("/tmp/test_factory.package");
std::cout << " PASS: factory" << std::endl;
testsPassed++;
}
// -----------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------
int main()
{
std::cout << "Package Archive Tests" << std::endl;
std::cout << "=====================" << std::endl;
// Run all tests (registered via static constructors)
std::cout << std::endl;
std::cout << "Results: " << (testsPassed + testsFailed) << " total, "
<< testsPassed << " passed, " << testsFailed << " failed"
<< std::endl;
return testsFailed > 0 ? 1 : 0;
}