PackageTool and package library for assets
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user