1
0
forked from 0ad/0ad
0ad/source/ps/XML/Xeromyces.cpp

333 lines
9.9 KiB
C++
Raw Normal View History

2004-07-08 17:22:09 +02:00
#include "precompiled.h"
#include <vector>
#include <set>
#include <map>
#include <stack>
#include <algorithm>
2004-07-08 17:22:09 +02:00
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "Xeromyces.h"
2004-07-08 17:22:09 +02:00
#include <libxml/parser.h>
#define LOG_CATEGORY "xml"
2004-07-08 17:22:09 +02:00
static bool g_XeromycesStarted = false;
void CXeromyces::Startup()
{
debug_assert(!g_XeromycesStarted);
xmlInitParser();
g_XeromycesStarted = true;
2004-07-08 17:22:09 +02:00
}
void CXeromyces::Terminate()
{
debug_assert(g_XeromycesStarted);
xmlCleanupParser();
g_XeromycesStarted = false;
2004-07-08 17:22:09 +02:00
}
// Find out write location of the XMB file corresponding to xmlFilename
void CXeromyces::GetXMBPath(const PIVFS& vfs, const VfsPath& xmlFilename, const VfsPath& xmbFilename, VfsPath& xmbActualPath)
{
// rationale:
// - it is necessary to write out XMB files into a subdirectory
// corresponding to the mod from which the XML file is taken.
// this avoids confusion when multiple mods are active -
// their XMB files' VFS filename would otherwise be indistinguishable.
// - we group files in the cache/ mount point first by mod, and only
// then XMB. this is so that all output files for a given mod can
// easily be deleted. the operation of deleting all old/unused
// XMB files requires a program anyway (to find out which are no
// longer needed), so it's not a problem that XMB files reside in
// a subdirectory (which would make manually deleting all harder).
// get real path of XML file (e.g. mods/official/entities/...)
Path P_XMBRealPath;
vfs->GetRealPath(xmlFilename, P_XMBRealPath);
// extract mod name from that
char modName[PATH_MAX];
// .. NOTE: can't use %s, of course (keeps going beyond '/')
int matches = sscanf(P_XMBRealPath.string().c_str(), "mods/%[^/]", modName);
debug_assert(matches == 1);
// build full name: cache, then mod name, XMB subdir, original XMB path
xmbActualPath = VfsPath("cache/mods") / modName / "xmb" / xmbFilename;
}
PSRETURN CXeromyces::Load(const VfsPath& filename)
2004-07-08 17:22:09 +02:00
{
debug_assert(g_XeromycesStarted);
// Make sure the .xml actually exists
if (! FileExists(filename))
{
LOG(CLogger::Error, LOG_CATEGORY, "CXeromyces: Failed to find XML file %s", filename.string().c_str());
return PSRETURN_Xeromyces_XMLOpenFailed;
}
2004-07-08 17:22:09 +02:00
// Get some data about the .xml file
FileInfo fileInfo;
if (g_VFS->GetFileInfo(filename, &fileInfo) < 0)
2004-07-08 17:22:09 +02:00
{
LOG(CLogger::Error, LOG_CATEGORY, "CXeromyces: Failed to stat XML file %s", filename.string().c_str());
return PSRETURN_Xeromyces_XMLOpenFailed;
2004-07-08 17:22:09 +02:00
}
/*
XMBs are stored with a unique name, where the name is generated from
characteristics of the XML file. If a file already exists with the
generated name, it is assumed that that file is a valid conversion of
the XML, and so it's loaded. Otherwise, the XMB is created with that
filename.
This means it's never necessary to overwrite existing XMB files; since
the XMBs are often in archives, it's not easy to rewrite those files,
and it's not possible to switch to using a loose file because the VFS
has already decided that file is inside an archive. So each XMB is given
a unique name, and old ones are somehow purged.
*/
2004-07-08 17:22:09 +02:00
// Generate the filename for the xmb:
// <xml filename>_<mtime><size><format version>.xmb
// with mtime/size as 8-digit hex, where mtime's lowest bit is
// zeroed because zip files only have 2 second resolution.
const int suffixLength = 22;
char suffix[suffixLength+1];
int printed = sprintf(suffix, "_%08x%08xB.xmb", (int)(fileInfo.MTime() & ~1), (int)fileInfo.Size());
debug_assert(printed == suffixLength);
VfsPath xmbFilename = change_extension(filename, suffix);
2004-07-08 17:22:09 +02:00
VfsPath xmbPath;
GetXMBPath(g_VFS, filename, xmbFilename, xmbPath);
// If the file exists, use it
if (FileExists(xmbPath))
{
if (ReadXMBFile(xmbPath))
return PSRETURN_OK;
// (no longer return PSRETURN_Xeromyces_XMLOpenFailed here because
// failure legitimately happens due to partially-written XMB files.)
}
2004-07-08 17:22:09 +02:00
// XMB isn't up to date with the XML, so rebuild it:
CVFSFile input;
if (input.Load(filename))
{
LOG(CLogger::Error, LOG_CATEGORY, "CXeromyces: Failed to open XML file %s", filename.string().c_str());
return PSRETURN_Xeromyces_XMLOpenFailed;
}
2004-07-08 17:22:09 +02:00
xmlDocPtr doc = xmlReadMemory((const char*)input.GetBuffer(), input.GetBufferSize(), "", NULL,
XML_PARSE_NONET|XML_PARSE_NOCDATA);
// TODO: handle parse errors
WriteBuffer writeBuffer;
CreateXMB(doc, writeBuffer);
xmlFreeDoc(doc);
// Save the file to disk, so it can be loaded quickly next time
g_VFS->CreateFile(xmbPath, writeBuffer.Data(), writeBuffer.Size());
m_XMBBuffer = writeBuffer.Data(); // add a reference
// Set up the XMBFile
const bool ok = Initialise((const char*)m_XMBBuffer.get());
debug_assert(ok);
return PSRETURN_OK;
}
bool CXeromyces::ReadXMBFile(const VfsPath& filename)
2004-07-08 17:22:09 +02:00
{
size_t size;
if(g_VFS->LoadFile(filename, m_XMBBuffer, size) < 0)
2004-07-08 17:22:09 +02:00
return false;
debug_assert(size >= 4); // make sure it's at least got the initial header
2004-07-08 17:22:09 +02:00
// Set up the XMBFile
if(!Initialise((const char*)m_XMBBuffer.get()))
return false;
2004-07-08 17:22:09 +02:00
return true;
}
static void FindNames(const xmlNodePtr node, std::set<std::string>& elementNames, std::set<std::string>& attributeNames)
2004-07-08 17:22:09 +02:00
{
elementNames.insert((const char*)node->name);
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next)
attributeNames.insert((const char*)attr->name);
for (xmlNodePtr child = node->children; child; child = child->next)
if (child->type == XML_ELEMENT_NODE)
FindNames(child, elementNames, attributeNames);
2004-07-08 17:22:09 +02:00
}
static void OutputElement(const xmlNodePtr node, WriteBuffer& writeBuffer,
std::map<std::string, u32>& elementIDs,
std::map<std::string, u32>& attributeIDs
)
2004-07-08 17:22:09 +02:00
{
// Filled in later with the length of the element
size_t posLength = writeBuffer.Size();
writeBuffer.Append("????", 4);
2004-07-08 17:22:09 +02:00
writeBuffer.Append(&elementIDs[(const char*)node->name], 4);
2004-07-08 17:22:09 +02:00
u32 attrCount = 0;
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next)
++attrCount;
writeBuffer.Append(&attrCount, 4);
2004-07-08 17:22:09 +02:00
u32 childCount = 0;
for (xmlNodePtr child = node->children; child; child = child->next)
if (child->type == XML_ELEMENT_NODE)
++childCount;
writeBuffer.Append(&childCount, 4);
2004-07-08 17:22:09 +02:00
// Filled in later with the offset to the list of child elements
size_t posChildrenOffset = writeBuffer.Size();
writeBuffer.Append("????", 4);
2004-07-08 17:22:09 +02:00
// Trim excess whitespace in the entity's text, while counting
// the number of newlines trimmed (so that JS error reporting
// can give the correct line number)
std::string whitespace = " \t\r\n";
std::string text;
for (xmlNodePtr child = node->children; child; child = child->next)
{
if (child->type == XML_TEXT_NODE)
{
xmlChar* content = xmlNodeGetContent(child);
text += std::string((const char*)content);
xmlFree(content);
}
}
u32 linenum = XML_GET_LINE(node);
// Find the start of the non-whitespace section
size_t first = text.find_first_not_of(whitespace);
if (first == text.npos)
// Entirely whitespace - easy to handle
text = "";
2004-07-08 17:22:09 +02:00
else
{
// Count the number of \n being cut off,
// and add them to the line number
std::string trimmed (text.begin(), text.begin()+first);
linenum += std::count(trimmed.begin(), trimmed.end(), '\n');
// Find the end of the non-whitespace section,
// and trim off everything else
size_t last = text.find_last_not_of(whitespace);
text = text.substr(first, 1+last-first);
2004-07-08 17:22:09 +02:00
}
2004-07-08 17:22:09 +02:00
// Output text, prefixed by length in bytes
if (text.length() == 0)
{
// No text; don't write much
writeBuffer.Append("\0\0\0\0", 4);
}
else
{
// Write length and line number and null-terminated text
utf16string textW = CStr8(text).FromUTF8().utf16();
u32 nodeLen = 4 + 2*(textW.length()+1);
writeBuffer.Append(&nodeLen, 4);
writeBuffer.Append(&linenum, 4);
writeBuffer.Append((void*)textW.c_str(), nodeLen-4);
}
// Output attributes
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next)
2004-07-08 17:22:09 +02:00
{
writeBuffer.Append(&attributeIDs[(const char*)attr->name], 4);
xmlChar* value = xmlNodeGetContent(attr->children);
utf16string textW = CStr8((const char*)value).FromUTF8().utf16();
xmlFree(value);
u32 attrLen = 2*(textW.length()+1);
writeBuffer.Append(&attrLen, 4);
writeBuffer.Append((void*)textW.c_str(), attrLen);
2004-07-08 17:22:09 +02:00
}
// Go back and fill in the child-element offset
u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4));
writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset);
2004-07-08 17:22:09 +02:00
// Output all child elements
for (xmlNodePtr child = node->children; child; child = child->next)
if (child->type == XML_ELEMENT_NODE)
OutputElement(child, writeBuffer, elementIDs, attributeIDs);
2004-07-08 17:22:09 +02:00
// Go back and fill in the length
u32 length = (u32)(writeBuffer.Size() - posLength);
writeBuffer.Overwrite(&length, 4, posLength);
}
PSRETURN CXeromyces::CreateXMB(const xmlDocPtr doc, WriteBuffer& writeBuffer)
{
// Header
writeBuffer.Append(UnfinishedHeaderMagicStr, 4);
std::set<std::string>::iterator it;
u32 i;
// Find the unique element/attribute names
std::set<std::string> elementNames;
std::set<std::string> attributeNames;
FindNames(xmlDocGetRootElement(doc), elementNames, attributeNames);
std::map<std::string, u32> elementIDs;
std::map<std::string, u32> attributeIDs;
// Output element names
i = 0;
u32 elementCount = (u32)elementNames.size();
writeBuffer.Append(&elementCount, 4);
for (it = elementNames.begin(); it != elementNames.end(); ++it)
{
u32 textLen = (u32)it->length()+1;
writeBuffer.Append(&textLen, 4);
writeBuffer.Append((void*)it->c_str(), textLen);
elementIDs[*it] = i++;
}
// Output attribute names
i = 0;
u32 attributeCount = (u32)attributeNames.size();
writeBuffer.Append(&attributeCount, 4);
for (it = attributeNames.begin(); it != attributeNames.end(); ++it)
{
u32 textLen = (u32)it->length()+1;
writeBuffer.Append(&textLen, 4);
writeBuffer.Append((void*)it->c_str(), textLen);
attributeIDs[*it] = i++;
}
OutputElement(xmlDocGetRootElement(doc), writeBuffer, elementIDs, attributeIDs);
// file is now valid, so insert correct magic string
writeBuffer.Overwrite(HeaderMagicStr, 4, 0);
return PSRETURN_OK;
}