#include "global.h" #include "XmlFileUtil.h" #include "XmlFile.h" #include "RageFile.h" #include "RageFileDriverMemory.h" #include "RageUtil.h" #include "RageLog.h" #include "arch/Dialog/Dialog.h" #include "Foreach.h" bool XmlFileUtil::LoadFromFileShowErrors( XNode &xml, RageFileBasic &f ) { RString sError; RString s; if( f.Read( s ) == -1 ) sError = f.GetError(); else Load( &xml, s, sError ); if( sError.empty() ) return true; RString sWarning = ssprintf( "XML: LoadFromFile failed: %s", sError.c_str() ); LOG->Warn( sWarning ); Dialog::OK( sWarning, "XML_PARSE_ERROR" ); return false; } bool XmlFileUtil::LoadFromFileShowErrors( XNode &xml, const RString &sFile ) { RageFile f; if( !f.Open(sFile, RageFile::READ) ) { LOG->Warn("Couldn't open %s for reading: %s", sFile.c_str(), f.GetError().c_str() ); return false; } bool bSuccess = LoadFromFileShowErrors( xml, f ); if( !bSuccess ) { RString sWarning = ssprintf( "XML: LoadFromFile failed for file: %s", sFile.c_str() ); LOG->Warn( sWarning ); Dialog::OK( sWarning, "XML_PARSE_ERROR" ); } return bSuccess; } static const char chXMLTagOpen = '<'; static const char chXMLTagClose = '>'; static const char chXMLQuestion = '?'; // used in checking for meta tags: "" static const char chXMLTagPre = '/'; static const char chXMLExclamation = '!'; static const char chXMLDash = '-'; static map g_mapEntitiesToChars; static map g_mapCharsToEntities; static void InitEntities() { if( !g_mapEntitiesToChars.empty() ) return; static struct Entity { char c; const char *pEntity; } const EntityTable[] = { { '&', "amp", }, { '\"', "quot", }, { '\'', "apos", }, { '<', "lt", }, { '>', "gt", } }; for( unsigned i = 0; i < ARRAYLEN(EntityTable); ++i ) { const Entity &ent = EntityTable[i]; g_mapEntitiesToChars[ent.pEntity] = RString(1, ent.c); g_mapCharsToEntities[ent.c] = ent.pEntity; } } // skip spaces static void tcsskip( const RString &s, unsigned &i ) { i = s.find_first_not_of( " \t\r\n", i ); } static bool XIsEmptyString( const RString &s ) { return s.find_first_not_of( "\r\n\t " ) == s.npos; } // put string of (psz~end) on ps string static void SetString( const RString &s, int iStart, int iEnd, RString* ps, bool trim = false ) { if( trim ) { while( iStart < iEnd && s[iStart] > 0 && isspace(s[iStart]) ) iStart++; while( iEnd-1 >= iStart && s[iEnd-1] > 0 && isspace(s[iEnd-1]) ) iEnd--; } int len = iEnd - iStart; if( len <= 0 ) return; ps->assign( s, iStart, len ); } // attr1="value1" attr2='value2' attr3=value3 /> // ^- return pointer // Desc : loading attribute plain xml text // Param : pszAttrs - xml of attributes // pi = parser information // Return : advanced string pointer. (error return npos) namespace { unsigned LoadAttributes( XNode *pNode, const RString &xml, RString &sErrorOut, unsigned iOffset ) { while( iOffset < xml.size() ) { tcsskip( xml, iOffset ); if( iOffset >= xml.size() ) continue; // close tag if( iOffset < xml.size() && (xml[iOffset] == chXMLTagClose || xml[iOffset] == chXMLTagPre || xml[iOffset] == chXMLQuestion || xml[iOffset] == chXMLDash) ) return iOffset; // well-formed tag // XML Attr Name unsigned iEnd = xml.find_first_of( " =", iOffset ); if( iEnd == xml.npos ) { // error if( sErrorOut.empty() ) sErrorOut = ssprintf( "<%s> attribute has error ", pNode->GetName().c_str() ); return string::npos; } // XML Attr Name RString sName; SetString( xml, iOffset, iEnd, &sName ); // add new attribute DEBUG_ASSERT( sName.size() ); XNodeValue *pAttr = pNode->AppendAttr( sName ); iOffset = iEnd; // XML Attr Value tcsskip( xml, iOffset ); if( iOffset < xml.size() && xml[iOffset] == '=' ) { ++iOffset; tcsskip( xml, iOffset ); if( iOffset >= xml.size() ) continue; // if " or ' // or none quote char quote = xml[iOffset]; if( quote == '"' || quote == '\'' ) { ++iOffset; iEnd = xml.find( quote, iOffset ); } else { //attr= value> // none quote mode iEnd = xml.find_first_of( " >", iOffset ); } if( iEnd == xml.npos ) { // error if( sErrorOut.empty() ) sErrorOut = ssprintf( "<%s> attribute text: couldn't find matching quote", sName.c_str() ); return string::npos; } RString sValue; SetString( xml, iOffset, iEnd, &sValue, true ); pAttr->SetValue( sValue ); iOffset = iEnd; ReplaceEntityText( sValue, g_mapEntitiesToChars ); if( quote == '"' || quote == '\'' ) ++iOffset; } } // not well-formed tag return string::npos; } // // // or // // ^- return pointer // Desc : load xml plain text // Param : pszXml - plain xml text // pi = parser information // Return : advanced string pointer (error return npos) unsigned LoadInternal( XNode *pNode, const RString &xml, RString &sErrorOut, unsigned iOffset ) { pNode->Clear(); // < iOffset = xml.find( chXMLTagOpen, iOffset ); if( iOffset == string::npos ) return string::npos; // ", iOffset ); if( iEnd == string::npos ) { if( sErrorOut.empty() ) sErrorOut = "Unterminated comment"; return string::npos; } // Skip -->. iOffset = iEnd + 3; return LoadInternal( pNode, xml, sErrorOut, iOffset ); } // XML Node Tag Name Open iOffset++; unsigned iTagEnd = xml.find_first_of( " \t\r\n/>", iOffset ); SetString( xml, iOffset, iTagEnd, &pNode->m_sName ); iOffset = iTagEnd; // Generate XML Attributte List iOffset = LoadAttributes( pNode, xml, sErrorOut, iOffset ); if( iOffset == string::npos ) return string::npos; // alone tag or or // current pointer: ^ ^ ^ if( iOffset < xml.size() && (xml[iOffset] == chXMLTagPre || xml[iOffset] == chXMLQuestion || xml[iOffset] == chXMLDash) ) { iOffset++; // skip over 2nd dash if( iOffset < xml.size() && xml[iOffset] == chXMLDash ) iOffset++; if( iOffset == xml.size() || xml[iOffset] != chXMLTagClose ) { // error: if( sErrorOut.empty() ) sErrorOut = "Element must be closed."; // ill-formed tag return string::npos; } // well-formed tag ++iOffset; // UGLY: We want to ignore all XML meta tags. So, since the Node we // just loaded is a meta tag, then Load ourself again using the rest // of the file until we reach a non-meta tag. if( !pNode->GetName().empty() && (pNode->GetName()[0] == chXMLQuestion || pNode->GetName()[0] == chXMLExclamation) ) iOffset = LoadInternal( pNode, xml, sErrorOut, iOffset ); return iOffset; } // open/close tag ... // ^- current pointer if( XIsEmptyString(pNode->m_pValue->GetValue()) ) { // Text Value ++iOffset; unsigned iEnd = xml.find( chXMLTagOpen, iOffset ); if( iEnd == string::npos ) { if( sErrorOut.empty() ) sErrorOut = ssprintf( "%s must be closed with ", pNode->GetName().c_str(), pNode->GetName().c_str() ); // error cos not exist CloseTag return string::npos; } RString sValue; SetString( xml, iOffset, iEnd, &sValue, true ); iOffset = iEnd; ReplaceEntityText( sValue, g_mapEntitiesToChars ); pNode->m_pValue->SetValue( sValue ); } // generate child nodes while( iOffset < xml.size() ) { XNode *node = new XNode; iOffset = LoadInternal( node, xml, sErrorOut, iOffset ); if( iOffset == string::npos ) { delete node; return iOffset; } if( !node->GetName().empty() ) { DEBUG_ASSERT( node->GetName().size() ); pNode->m_childs.insert( make_pair(node->GetName(), node) ); } else { delete node; } // open/close tag ... // ^- current pointer // CloseTag case if( iOffset+1 < xml.size() && xml[iOffset] == chXMLTagOpen && xml[iOffset+1] == chXMLTagPre ) { // iOffset += 2; // C tcsskip( xml, iOffset ); if( iOffset >= xml.size() ) continue; unsigned iEnd = xml.find_first_of( " >", iOffset ); if( iEnd == string::npos ) { if( sErrorOut.empty() ) sErrorOut = ssprintf( "it must be closed with ", pNode->GetName().c_str() ); // error return string::npos; } RString closename; SetString( xml, iOffset, iEnd, &closename ); iOffset = iEnd+1; if( closename == pNode->GetName() ) { // wel-formed open/close // return '>' or ' ' after pointer return iOffset; } else { // not welformed open/close if( sErrorOut.empty() ) sErrorOut = ssprintf( "'<%s> ... ' is not well-formed.", pNode->GetName().c_str(), closename.c_str() ); return string::npos; } } else // Alone child Tag Loaded { if( XIsEmptyString(pNode->m_pValue->GetValue()) && iOffset < xml.size() && xml[iOffset] != chXMLTagOpen ) { // Text Value unsigned iEnd = xml.find( chXMLTagOpen, iOffset ); if( iEnd == string::npos ) { // error cos not exist CloseTag if( sErrorOut.empty() ) sErrorOut = ssprintf( "it must be closed with ", pNode->GetName().c_str() ); return string::npos; } RString sValue; SetString( xml, iOffset, iEnd, &sValue, true ); iOffset = iEnd; ReplaceEntityText( sValue, g_mapEntitiesToChars ); pNode->m_pValue->SetValue( sValue ); } } } return iOffset; } bool GetAttrXML( RageFileBasic &f, const RString &sName, const RString &sValue ) { RString s(sValue); ReplaceEntityText( s, g_mapCharsToEntities ); return f.Write(sName + "='" + s + "' ") != -1; } bool GetXMLInternal( const XNode *pNode, RageFileBasic &f, bool bWriteTabs, int &iTabBase ) { // tab if( f.Write("\r\n") == -1 ) return false; if( bWriteTabs ) for( int i = 0 ; i < iTabBase ; i++) if( f.Write("\t") == -1 ) return false; // GetName()) == -1 ) return false; // m_attrs.empty() ) if( f.Write(" ") == -1 ) return false; FOREACH_CONST_Attr( pNode, p ) if( !GetAttrXML(f, p->first, p->second->GetValue()) ) return false; if( pNode->m_childs.empty() && pNode->m_pValue->GetValue().empty() ) { // alone tag if( f.Write("/>") == -1 ) return false; } else { // and get child if( f.Write(">") == -1 ) return false; if( !pNode->m_childs.empty() ) iTabBase++; FOREACH_CONST_Child( pNode, p ) if( !GetXMLInternal( p, f, bWriteTabs, iTabBase ) ) return false; // Text Value if( !pNode->m_pValue->GetValue().empty() ) { if( !pNode->m_childs.empty() ) { if( f.Write("\r\n") == -1 ) return false; if( bWriteTabs ) for( int i = 0 ; i < iTabBase ; i++) if( f.Write("\t") == -1 ) return false; } RString s; pNode->m_pValue->GetValue( s ); ReplaceEntityText( s, g_mapCharsToEntities ); if( f.Write(s) == -1 ) return false; } // CloseTag if( !pNode->m_childs.empty() ) { if( f.Write("\r\n") == -1 ) return false; if( bWriteTabs ) for( int i = 0 ; i < iTabBase-1 ; i++) if( f.Write("\t") == -1 ) return false; } if( f.Write("GetName() + ">") == -1 ) return false; if( !pNode->m_childs.empty() ) iTabBase--; } return true; } } void XmlFileUtil::Load( XNode *pNode, const RString &sXml, RString &sErrorOut ) { InitEntities(); LoadInternal( pNode, sXml, sErrorOut, 0 ); } bool XmlFileUtil::GetXML( const XNode *pNode, RageFileBasic &f, bool bWriteTabs ) { int iTabBase = 0; InitEntities(); return GetXMLInternal( pNode, f, bWriteTabs, iTabBase ); } RString XmlFileUtil::GetXML( const XNode *pNode ) { RageFileObjMem f; int iTabBase = 0; InitEntities(); GetXMLInternal( pNode, f, true, iTabBase ); return f.GetString(); } bool XmlFileUtil::SaveToFile( const XNode *pNode, RageFileBasic &f, const RString &sStylesheet, bool bWriteTabs ) { f.PutLine( "" ); if( !sStylesheet.empty() ) f.PutLine( "" ); int iTabBase = 0; InitEntities(); if( !GetXMLInternal(pNode, f, bWriteTabs, iTabBase) ) return false; f.PutLine( "" ); if( f.Flush() == -1 ) return false; return true; } bool XmlFileUtil::SaveToFile( const XNode *pNode, const RString &sFile, const RString &sStylesheet, bool bWriteTabs ) { RageFile f; if( !f.Open(sFile, RageFile::WRITE) ) { LOG->Warn( "Couldn't open %s for writing: %s", sFile.c_str(), f.GetError().c_str() ); return false; } return SaveToFile( pNode, f, sStylesheet, bWriteTabs ); } #include "LuaManager.h" #include "LuaReference.h" class XNodeLuaValue: public XNodeValue { public: LuaReference m_Value; XNodeValue *Copy() const { return new XNodeLuaValue( *this ); } template T GetValue() const { T val; GetValue(val); return val; } void GetValue( RString &out ) const; void GetValue( int &out ) const; void GetValue( float &out ) const; void GetValue( bool &out ) const; void GetValue( unsigned &out ) const; void PushValue( lua_State *L ) const; void SetValue( const RString &v ); void SetValue( int v ); void SetValue( float v ); void SetValue( unsigned v ); void SetValueFromStack( lua_State *L ); }; void XNodeLuaValue::PushValue( lua_State *L ) const { m_Value.PushSelf( L ); } void XNodeLuaValue::GetValue( RString &out ) const { Lua *L = LUA->Get(); PushValue( L ); LuaHelpers::Pop( L, out ); LUA->Release( L ); } void XNodeLuaValue::GetValue( int &out ) const { Lua *L = LUA->Get(); PushValue( L ); LuaHelpers::Pop( L, out ); LUA->Release( L ); } void XNodeLuaValue::GetValue( float &out ) const { Lua *L = LUA->Get(); PushValue( L ); LuaHelpers::Pop( L, out ); LUA->Release( L ); } void XNodeLuaValue::GetValue( bool &out ) const { Lua *L = LUA->Get(); PushValue( L ); LuaHelpers::Pop( L, out ); LUA->Release( L ); } void XNodeLuaValue::GetValue( unsigned &out ) const { Lua *L = LUA->Get(); PushValue( L ); float fVal; LuaHelpers::Pop( L, fVal ); out = unsigned(fVal); LUA->Release( L ); } void XNodeLuaValue::SetValueFromStack( lua_State *L ) { m_Value.SetFromStack( L ); } void XNodeLuaValue::SetValue( const RString &v ) { Lua *L = LUA->Get(); LuaHelpers::Push( L, v ); SetValueFromStack( L ); LUA->Release( L ); } void XNodeLuaValue::SetValue( int v ) { Lua *L = LUA->Get(); LuaHelpers::Push( L, v ); SetValueFromStack( L ); LUA->Release( L ); } void XNodeLuaValue::SetValue( float v ) { Lua *L = LUA->Get(); LuaHelpers::Push( L, v ); SetValueFromStack( L ); LUA->Release( L ); } void XNodeLuaValue::SetValue( unsigned v ) { Lua *L = LUA->Get(); LuaHelpers::Push( L, (float) v ); SetValueFromStack( L ); LUA->Release( L ); } namespace { void CompileXMLNodeValue( Lua *L, const RString &sName, XNodeValue *&pValue, const RString &sFile ) { RString sExpression; pValue->GetValue( sExpression ); if( EndsWith(sName, "Command") && sExpression.size() > 0 && sExpression[0] == '%' ) { sExpression.erase( 0, 1 ); LuaHelpers::RunExpression( L, sExpression, sFile ); } else if( EndsWith(sName, "Command") && (sExpression.size() == 0 || sExpression[0] != '%') ) { LuaHelpers::ParseCommandList( L, sExpression, sFile ); } else if( sExpression.size() > 0 && sExpression[0] == '@' ) { /* This is a raw string. */ sExpression.erase( 0, 1 ); LuaHelpers::Push( L, sExpression ); } else { LuaHelpers::RunExpression( L, sExpression, sFile ); } delete pValue; pValue = new XNodeLuaValue; pValue->SetValueFromStack( L ); } } void XmlFileUtil::CompileXNodeTree( XNode *pNode, const RString &sFile ) { vector aToCompile; aToCompile.push_back( pNode ); Lua *L = LUA->Get(); while( aToCompile.size() ) { pNode = aToCompile.back(); aToCompile.pop_back(); FOREACH_Child( pNode, pChild ) aToCompile.push_back( pChild ); CompileXMLNodeValue( L, pNode->GetName(), pNode->m_pValue, sFile ); FOREACH_Attr( pNode, pAttr ) CompileXMLNodeValue( L, pAttr->first, pAttr->second, sFile ); } LUA->Release( L ); } namespace { XNode *XNodeFromTableRecursive( lua_State *L, const RString &sName, LuaReference &ProcessedTables ) { XNode *pNode = new XNode( sName ); /* Set the value of the node to the table. */ { XNodeLuaValue *pValue = new XNodeLuaValue; lua_pushvalue( L, -1 ); pValue->SetValueFromStack( L ); pNode->SetValueFrom( pValue ); } /* Iterate over the table, pulling out attributes and tables to process. */ map NodesToAdd; FOREACH_LUATABLE( L, -1 ) { RString sName; LuaHelpers::Pop( L, sName ); /* If this entry is a table, add it recursively. */ if( lua_istable(L, -1) ) { NodesToAdd[sName].SetFromStack( L ); continue; } /* Otherwise, add an attribute. */ XNodeLuaValue *pValue = new XNodeLuaValue; pValue->SetValueFromStack( L ); pNode->AppendAttrFrom( sName, pValue ); } lua_pop( L, 1 ); /* Recursively process tables. */ FOREACHM( RString, LuaReference, NodesToAdd, t ) { const RString &sNodeName = t->first; LuaReference &NodeToAdd = t->second; /* Check if the table is on the stack. */ ProcessedTables.PushSelf( L ); NodeToAdd.PushSelf( L ); // push table lua_gettable( L, -2 ); bool bSawThisTableAlready = !lua_isnil(L, -1); lua_pop( L, 2 ); // pop lua_gettable result, ProcessedTables if( bSawThisTableAlready ) continue; /* Add the table to the stack. */ ProcessedTables.PushSelf( L ); NodeToAdd.PushSelf( L ); lua_pushboolean( L, true ); lua_settable( L, -3 ); lua_pop( L, 1 ); // pop ProcessedTables NodeToAdd.PushSelf( L ); XNode *pNewNode = XNodeFromTableRecursive( L, sNodeName, ProcessedTables ); if( pNewNode ) pNode->AppendChild( pNewNode ); /* Remove the table from the stack. */ ProcessedTables.PushSelf( L ); NodeToAdd.PushSelf( L ); lua_pushnil( L ); lua_settable( L, -3 ); lua_pop( L, 1 ); // pop ProcessedTables } return pNode; } } /* * Pop a table off of the stack, and return an XNode tree referring recursively * to entries in the table. * * The table may not contain table cycles; if a cycle is detected, only the first * table seen will have a corresponding XNode. * * Users of the resulting XNode may access the original table via PushValue. */ XNode *XmlFileUtil::XNodeFromTable( lua_State *L ) { /* Maintain a set of references that we've created. Tables may loop; XNode trees may * not. If we encounter a cycle, skip creating an XNode for that node. */ LuaReference ProcessedTables; lua_newtable( L ); ProcessedTables.SetFromStack( L ); return XNodeFromTableRecursive( L, "Layer", ProcessedTables ); } /* Move nodes from pFrom into pTo which don't already exist in pTo. For * efficiency, nodes will be moved, not copied, so pFrom will be modified. * On return, the contents of pFrom will be undefined and should be deleted. */ void XmlFileUtil::MergeIniUnder( XNode *pFrom, XNode *pTo ) { /* Iterate over each section in pFrom. */ XNodes::iterator it = pFrom->m_childs.begin(); while( it != pFrom->m_childs.end() ) { XNodes::iterator next = it; ++next; /* If this node doesn't exist in pTo, just move the whole node. */ XNode *pChildNode = pTo->GetChild( it->first ); XNode *pSectionNode = it->second; if( pChildNode == NULL ) { pFrom->RemoveChild( pSectionNode, false ); // don't delete pTo->AppendChild( pSectionNode ); } else { FOREACHM( RString, XNodeValue *, pSectionNode->m_attrs, it2 ) { /* Don't overwrite existing nodes. */ pChildNode->AppendAttrFrom( it2->first, it2->second->Copy(), false ); } } it = next; } } /* * (c) 2001-2006 Chris Danford, Glenn Maynard * All rights reserved. * * 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, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * 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 OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */