You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Tools/RC/ResourceCompilerXML/XMLConverter.cpp

705 lines
20 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
#include "ResourceCompilerXML_precompiled.h"
#include "XMLConverter.h"
#include "IRCLog.h"
#include "IXMLSerializer.h"
#include "XMLBinaryHeaders.h"
#include <CrySystem/XML/XMLBinaryReader.h>
#include <CrySystem/XML/XMLBinaryWriter.h>
#include "IConfig.h"
#include "IResCompiler.h"
#include "FileUtil.h"
#include "StringHelpers.h"
#include "UpToDateFileHelpers.h"
#include <QDir>
XMLCompiler::XMLCompiler(
ICryXML* pCryXML,
const std::vector<XMLFilterElement>& filter,
const std::vector<string>& tableFilemasks,
const NameConvertor& nameConverter)
: m_refCount(1)
, m_pCryXML(pCryXML)
, m_pFilter(&filter)
, m_pTableFilemasks(&tableFilemasks)
, m_pNameConverter(&nameConverter)
{
}
void XMLCompiler::Release()
{
if (--m_refCount <= 0)
{
delete this;
}
}
XMLConverter::XMLConverter(ICryXML* pCryXML)
: m_pCryXML(pCryXML)
, m_refCount(1)
{
m_pCryXML->AddRef();
}
XMLConverter::~XMLConverter()
{
m_pCryXML->Release();
}
void XMLConverter::Release()
{
if (--m_refCount <= 0)
{
delete this;
}
}
void XMLConverter::Init(const ConvertorInitContext& context)
{
m_filter.clear();
m_tableFilemasks.clear();
if (!m_nameConvertor.SetRules(context.config->GetAsString("targetnameformat", "", "")))
{
return;
}
const string xmlFilterFile = context.config->GetAsString("xmlFilterFile", "", "");
if (xmlFilterFile.empty())
{
return;
}
FILE* f = nullptr;
azfopen(&f, xmlFilterFile.c_str(), "rt");
if (!f)
{
RCLogError("XML: Failed to open XML filter file \"%s\"", xmlFilterFile.c_str());
return;
}
char line[1024];
while (fgets(line, sizeof(line), f) != NULL)
{
if (line[0] == 0)
{
continue;
}
string sLine = line;
sLine.Trim();
if (sLine.empty())
{
continue;
}
if (::tolower(sLine[0]) == 'f')
{
sLine = sLine.substr(1, sLine.npos);
sLine.TrimLeft();
const string sKeywordTable = "table";
if (StringHelpers::StartsWithIgnoreCase(sLine, sKeywordTable))
{
sLine.erase(0, sKeywordTable.length());
sLine.TrimLeft();
if (!sLine.empty())
{
m_tableFilemasks.push_back(PathHelpers::ToDosPath(sLine));
}
}
continue;
}
if ((::tolower(sLine[0]) == 'a' || ::tolower(sLine[0]) == 'e'))
{
XMLFilterElement fe;
fe.type = (::tolower(sLine[0]) == 'a')
? XMLBinary::IFilter::eType_AttributeName
: XMLBinary::IFilter::eType_ElementName;
sLine = sLine.substr(1, sLine.npos);
sLine.TrimLeft();
if (!sLine.empty() && (sLine[0] == '+' || sLine[0] == '-'))
{
fe.bAccept = (sLine[0] == '+');
sLine = sLine.substr(1, sLine.npos);
sLine.TrimLeft();
if (!sLine.empty())
{
fe.wildcards = sLine;
m_filter.push_back(fe);
}
}
}
}
fclose(f);
}
//////////////////////////////////////////////////////////////////////////
class CXmlBinaryDataWriterFile
: public XMLBinary::IDataWriter
{
public:
CXmlBinaryDataWriterFile(const char* file)
{
m_file = nullptr;
azfopen(&m_file, file, "wb");
}
~CXmlBinaryDataWriterFile()
{
if (m_file)
{
fclose(m_file);
}
}
virtual bool IsOk()
{
return m_file != 0;
}
virtual void Write(const void* pData, size_t size)
{
if (m_file)
{
fwrite(pData, size, 1, m_file);
}
}
private:
FILE* m_file;
};
//////////////////////////////////////////////////////////////////////////
class CXmlFilter
: public XMLBinary::IFilter
{
public:
CXmlFilter(const std::vector<XMLFilterElement>* pFilterArray)
: m_pFilterArray((pFilterArray && !pFilterArray->empty()) ? pFilterArray : 0)
{
}
virtual bool IsAccepted(EType type, const char* pName) const
{
if (!m_pFilterArray)
{
return true;
}
const string name(pName);
for (size_t i = 0; i < m_pFilterArray->size(); ++i)
{
if (((*m_pFilterArray)[i].type == type) &&
StringHelpers::MatchesWildcardsIgnoreCase(name, (*m_pFilterArray)[i].wildcards))
{
return (*m_pFilterArray)[i].bAccept;
}
}
return true;
}
private:
const std::vector<XMLFilterElement>* m_pFilterArray;
};
//////////////////////////////////////////////////////////////////////////
static bool xmlsAreEqual(XmlNodeRef node0, XmlNodeRef node1, XMLBinary::IFilter* pFilter, const char*& mismatchInfo)
{
mismatchInfo = 0;
{
const char* const tag0 = node0->getTag();
const char* const tag1 = node1->getTag();
if (strcmp(tag0, tag1) != 0)
{
mismatchInfo = "tags";
return false;
}
}
{
const char* const content0 = node0->getContent();
const char* const content1 = node1->getContent();
if (strcmp(content0, content1) != 0)
{
mismatchInfo = "content";
return false;
}
}
{
const int count0 = node0->getNumAttributes();
const int count1 = node1->getNumAttributes();
int index1 = 0;
int filteredCount1 = 0;
for (int index0 = 0; index0 < count0; ++index0)
{
const char* key0;
const char* value0;
node0->getAttributeByIndex(index0, &key0, &value0);
const char* key1;
const char* value1;
for (;; )
{
if (index1 >= count1)
{
mismatchInfo = "attribute count";
return false;
}
node1->getAttributeByIndex(index1++, &key1, &value1);
if (!pFilter || pFilter->IsAccepted(XMLBinary::IFilter::eType_AttributeName, key1))
{
++filteredCount1;
break;
}
}
if (strcmp(key0, key1) != 0)
{
mismatchInfo = "attribute name";
return false;
}
if (strcmp(value0, value1) != 0)
{
mismatchInfo = "attribute value";
return false;
}
}
if (count0 != filteredCount1)
{
mismatchInfo = "attribute count";
return false;
}
}
{
const int count0 = node0->getChildCount();
const int count1 = node1->getChildCount();
int index1 = 0;
int filteredCount1 = 0;
for (int index0 = 0; index0 < count0; ++index0)
{
XmlNodeRef child0 = node0->getChild(index0);
XmlNodeRef child1;
for (;; )
{
if (index1 >= count1)
{
mismatchInfo = "child count";
return false;
}
child1 = node1->getChild(index1++);
if (!pFilter || pFilter->IsAccepted(XMLBinary::IFilter::eType_ElementName, child1->getTag()))
{
++filteredCount1;
break;
}
}
const bool bEqual = xmlsAreEqual(child0, child1, pFilter, mismatchInfo);
if (!bEqual)
{
return false;
}
}
if (count0 != filteredCount1)
{
mismatchInfo = "child count";
return false;
}
}
return true;
}
static string GetNormalizedFullPath(const string& relativePath)
{
QFileInfo file(relativePath.c_str());
return file.absoluteFilePath().toStdString().c_str();
}
static XmlNodeRef ConvertFromExcelXmlToCryEngineTableXml(XmlNodeRef root, IXMLSerializer* pSerializer, const string& sInputFile)
{
XmlNodeRef nodeTable;
{
const char* err = 0;
XmlNodeRef nodeWorksheet = root->findChild("Worksheet");
if (!nodeWorksheet)
{
err = "element 'Worksheet' is missing";
}
else
{
nodeTable = nodeWorksheet->findChild("Table");
if (!nodeTable)
{
err = "element 'Table' is missing in 'Worksheet'";
}
}
if (err)
{
RCLogError(
"XML: File \"%s\" expected to be an Excel spreadsheet XML, but it's not: %s. Check your /xmlfilterfile's file.",
sInputFile.c_str(), err);
return XmlNodeRef(0);
}
}
XmlNodeRef outTable = pSerializer->CreateNode("Table");
const bool bKeepEmptyRows = false;
const string sNewLine("\n");
string sRow;
int rowIndex = -1;
for (int row = 0; row < nodeTable->getChildCount(); ++row)
{
XmlNodeRef nodeRow = nodeTable->getChild(row);
if (!nodeRow->isTag("Row"))
{
continue;
}
++rowIndex;
// Handling skipped rows
{
int index = 0;
if (nodeRow->getAttr("ss:Index", index))
{
--index; // one-based -> zero-based
if (index < rowIndex)
{
RCLogError("XML: \"%s\": ss:Index has unexpected value %d", sInputFile.c_str(), index + 1);
return XmlNodeRef(0);
}
if (bKeepEmptyRows)
{
while (rowIndex < index)
{
XmlNodeRef outRow = pSerializer->CreateNode("Row");
outTable->addChild(outRow);
++rowIndex;
}
}
else
{
rowIndex = index;
}
}
}
sRow.clear();
int cellIndex = -1;
for (int cell = 0; cell < nodeRow->getChildCount(); ++cell)
{
XmlNodeRef nodeCell = nodeRow->getChild(cell);
if (!nodeCell->isTag("Cell"))
{
continue;
}
++cellIndex;
// Handling skipped columns
{
int index = 0;
if (nodeCell->getAttr("ss:Index", index))
{
--index; // one-based -> zero-based
if (index < cellIndex)
{
RCLogError("XML: \"%s\": ss:Index has unexpected value %d", sInputFile.c_str(), index + 1);
return XmlNodeRef(0);
}
while (cellIndex < index)
{
sRow.append(sNewLine);
++cellIndex;
}
}
}
XmlNodeRef nodeCellData = nodeCell->findChild("Data");
if (nodeCellData)
{
const char* const pContent = nodeCellData->getContent();
if (pContent)
{
sRow.append(pContent);
}
}
sRow.append(sNewLine);
}
// Erase empty trailing cells in the row
while (StringHelpers::EndsWith(sRow, sNewLine))
{
sRow.erase(sRow.length() - sNewLine.length());
}
if (!sRow.empty() || bKeepEmptyRows)
{
XmlNodeRef outRow = pSerializer->CreateNode("Row");
outRow->setContent(sRow.c_str());
outTable->addChild(outRow);
}
}
XmlNodeRef outRoot = pSerializer->CreateNode("Tables");
outRoot->addChild(outTable);
if (false)
{
const string sFilename = sInputFile + ".debug.xml";
const bool ok = pSerializer->Write(outRoot, sFilename.c_str());
if (!ok)
{
RCLogError("XML: Failed to write XML file \"%s\".", sFilename.c_str());
return XmlNodeRef(0);
}
}
return outRoot;
}
//////////////////////////////////////////////////////////////////////////
bool XMLCompiler::Process()
{
const int verbosityLevel = m_CC.m_pRC->GetVerbosityLevel();
const bool bNeedSwapEndian = m_CC.m_pRC->GetPlatformInfo(m_CC.m_platform)->bBigEndian;
if (verbosityLevel >= 1 && bNeedSwapEndian)
{
RCLog("XML: Endian conversion specified");
}
// Get the files to process.
const string sInputFile = m_CC.GetSourcePath();
string sOutputFile = GetOutputPath();
if (m_pNameConverter->HasRules())
{
const string oldFilename = PathHelpers::GetFilename(sOutputFile);
const string newFilename = m_pNameConverter->GetConvertedName(oldFilename);
if (newFilename.empty())
{
return false;
}
if (!StringHelpers::EqualsIgnoreCase(oldFilename, newFilename))
{
if (verbosityLevel >= 2)
{
RCLog("Target file name changed: %s -> %s", oldFilename.c_str(), newFilename.c_str());
}
sOutputFile = PathHelpers::Join(PathHelpers::GetDirectory(sOutputFile), newFilename);
}
}
// Check that we will not overwrite source file
{
const string fname0 = GetNormalizedFullPath(sInputFile);
const string fname1 = GetNormalizedFullPath(sOutputFile);
if (fname0.empty() || fname1.empty())
{
return false;
}
if (StringHelpers::EqualsIgnoreCase(fname0, fname1))
{
RCLogError("XML: Source file cannot be same as target file. Use /targetroot=... option.");
return false;
}
}
if (!m_CC.m_bForceRecompiling && UpToDateFileHelpers::FileExistsAndUpToDate(sOutputFile, sInputFile))
{
// The file is up-to-date
m_CC.m_pRC->AddInputOutputFilePair(sInputFile, sOutputFile);
return true;
}
// Check that the input file exists.
if (!FileUtil::FileExists(sInputFile.c_str()))
{
RCLogError("XML: File \"%s\" does not exist", sInputFile.c_str());
return false;
}
// Get XML serializer.
IXMLSerializer* pSerializer = m_pCryXML->GetXMLSerializer();
// Ensure that the input file is not in binary XML format.
{
XMLBinary::XMLBinaryReader binReader;
XMLBinary::XMLBinaryReader::EResult binResult;
XmlNodeRef rootBin = binReader.LoadFromFile(sInputFile.c_str(), binResult);
if (binResult == XMLBinary::XMLBinaryReader::eResult_Success)
{
RCLogError("XML: Source file is binary XML \"%s\"", sInputFile.c_str());
return false;
}
if (binResult == XMLBinary::XMLBinaryReader::eResult_Error)
{
RCLogError("XML: Input XML file is either binary or damaged \"%s\": %s", sInputFile.c_str(), binReader.GetErrorDescription());
return false;
}
}
// Read input XML file.
XmlNodeRef root;
{
const bool bRemoveNonessentialSpacesFromContent = true;
char szErrorBuffer[1024];
szErrorBuffer[0] = 0;
root = pSerializer->Read(FileXmlBufferSource(sInputFile.c_str()), bRemoveNonessentialSpacesFromContent, sizeof(szErrorBuffer), szErrorBuffer);
if (!root)
{
const char* const pErrorStr =
(szErrorBuffer[0])
? &szErrorBuffer[0]
: "Probably this file has bad XML syntax or it's not XML file at all";
RCLogError("XML: Cannot read file \"%s\": %s", sInputFile.c_str(), pErrorStr);
return false;
}
}
// Convert Excel's XML format to CryEngine's table XML format, if requested.
{
bool bConvert = false;
for (size_t i = 0; i < m_pTableFilemasks->size(); ++i)
{
if (StringHelpers::MatchesWildcardsIgnoreCase(sInputFile, (*m_pTableFilemasks)[i]))
{
bConvert = true;
break;
}
}
if (bConvert)
{
XmlNodeRef rootConverted = ConvertFromExcelXmlToCryEngineTableXml(root, pSerializer, sInputFile);
if (!rootConverted)
{
return false;
}
root = rootConverted;
}
}
// Create filter to get rid of unneeded elements and attributes
CXmlFilter filter(m_pFilter);
// Write out the destination file.
{
#if defined(AZ_PLATFORM_WINDOWS)
SetFileAttributes(sOutputFile.c_str(), FILE_ATTRIBUTE_ARCHIVE);
#endif
FILE* pDestinationFile = nullptr;
azfopen(&pDestinationFile, sOutputFile.c_str(), "wb");
if (pDestinationFile == 0)
{
char error[1024];
azstrerror_s(error, AZ_ARRAY_SIZE(error), errno);
RCLogError("XML: Cannot write file \"%s\": %s", sInputFile.c_str(), error);
return false;
}
fclose(pDestinationFile);
CXmlBinaryDataWriterFile outputFile(sOutputFile.c_str());
XMLBinary::CXMLBinaryWriter xmlBinaryWriter;
string error;
const bool ok = xmlBinaryWriter.WriteNode(&outputFile, root, bNeedSwapEndian, &filter, error);
if (!ok)
{
remove(sOutputFile.c_str());
RCLogError("XML: Failed to write binary XML file \"%s\": %s", sOutputFile.c_str(), error.c_str());
return false;
}
}
// Verify that the output file was written
if (!FileUtil::FileExists(sOutputFile.c_str()))
{
RCLogError("XML: Failed to write file \"%s\"", sOutputFile.c_str());
return false;
}
// Check that the output binary XML file has same content as the input XML
if (!bNeedSwapEndian)
{
XMLBinary::XMLBinaryReader binReader;
XMLBinary::XMLBinaryReader::EResult binResult;
XmlNodeRef rootBin = binReader.LoadFromFile(sOutputFile.c_str(), binResult);
if (!rootBin)
{
RCLogError("XML: Cannot read binary XML file \"%s\". Contact RC programmers.", sOutputFile.c_str());
return false;
}
const char* mismatchInfo = 0;
const bool bEqual = xmlsAreEqual(rootBin, root, &filter, mismatchInfo);
if (!bEqual)
{
RCLogError(
"XML: Source XML file \"%s\" and result binary XML file \"%s\" are different: %s. Contact RC programmers.",
sInputFile.c_str(), sOutputFile.c_str(), mismatchInfo);
return false;
}
}
if (!UpToDateFileHelpers::SetMatchingFileTime(sOutputFile, sInputFile))
{
return false;
}
m_CC.m_pRC->AddInputOutputFilePair(sInputFile, sOutputFile);
return true;
}
string XMLCompiler::GetOutputFileNameOnly() const
{
return m_CC.m_config->GetAsString("overwritefilename", m_CC.m_sourceFileNameOnly.c_str(), m_CC.m_sourceFileNameOnly.c_str());
}
string XMLCompiler::GetOutputPath() const
{
return PathHelpers::Join(m_CC.GetOutputFolder(), GetOutputFileNameOnly());
}
const char* XMLConverter::GetExt(int index) const
{
return (index == 0) ? "xml" : 0;
}
ICompiler* XMLConverter::CreateCompiler()
{
return new XMLCompiler(m_pCryXML, m_filter, m_tableFilemasks, m_nameConvertor);
}