#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( "%s", sWarning.c_str() ); 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( "%s", sWarning.c_str() ); 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, RString::size_type &i ) { i = s.find_first_not_of( " \t\r\n", i ); } // 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 { RString::size_type LoadAttributes( XNode *pNode, const RString &xml, RString &sErrorOut, RString::size_type 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 RString::size_type 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 { // XXX: This is invalid XML, should we be accepting it? -- Steve //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 ); ReplaceEntityText( sValue, g_mapEntitiesToChars ); pAttr->SetValue( sValue ); iOffset = iEnd; 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) RString::size_type LoadInternal( XNode *pNode, const RString &xml, RString &sErrorOut, RString::size_type 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++; RString::size_type iTagEnd = xml.find_first_of( " \t\r\n/>", iOffset ); RString sName; SetString( xml, iOffset, iTagEnd, &sName ); pNode->SetName( sName ); iOffset = iTagEnd; // Generate XML Attribute 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( pNode->GetAttr(XNode::TEXT_ATTRIBUTE) == NULL ) { // Text Value ++iOffset; RString::size_type 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->AppendAttr(XNode::TEXT_ATTRIBUTE, 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.push_back( 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; RString::size_type 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( pNode->GetAttr(XNode::TEXT_ATTRIBUTE) == NULL && iOffset < xml.size() && xml[iOffset] != chXMLTagOpen ) { // Text Value RString::size_type 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->AppendAttr(XNode::TEXT_ATTRIBUTE, sValue ); } } } return iOffset; } bool GetXMLInternal( const XNode *pNode, RageFileBasic &f, bool bWriteTabs, int &iTabBase ) { #define WRITE(x) if( f.Write(x) == -1 ) return false // tab WRITE( "\r\n" ); if( bWriteTabs ) for( int i = 0 ; i < iTabBase ; i++) WRITE( "\t" ); // GetName() ); // first == XNode::TEXT_ATTRIBUTE ) continue; RString attr( p->second->GetValue() ); ReplaceEntityText( attr, g_mapCharsToEntities ); WRITE( " " ); WRITE( p->first ); WRITE( "='" ); WRITE( attr ); WRITE( "'" ); } if( pNode->m_childs.empty() && pNode->GetAttr(XNode::TEXT_ATTRIBUTE) == NULL ) { // alone tag WRITE( "/>" ); } else { // and get child WRITE( ">" ); if( !pNode->m_childs.empty() ) iTabBase++; FOREACH_CONST_Child( pNode, p ) if( !GetXMLInternal( p, f, bWriteTabs, iTabBase ) ) return false; // Text Value const XNodeValue *pText = pNode->GetAttr( XNode::TEXT_ATTRIBUTE ); if( pText != NULL ) { if( !pNode->m_childs.empty() ) { WRITE( "\r\n" ); if( bWriteTabs ) for( int i = 0 ; i < iTabBase ; i++) WRITE( "\t" ); } RString s; pText->GetValue( s ); ReplaceEntityText( s, g_mapCharsToEntities ); WRITE( s ); } // CloseTag if( !pNode->m_childs.empty() ) { WRITE( "\r\n" ); if( bWriteTabs ) for( int i = 0 ; i < iTabBase-1 ; i++) WRITE( "\t" ); } WRITE( "GetName() ); WRITE( ">" ); if( !pNode->m_childs.empty() ) iTabBase--; } return true; #undef WRITE } } 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 { XNodeValue *CompileXMLNodeValue( Lua *L, const RString &sName, const XNodeValue *pValue, const RString &sFile ) { RString sExpression; pValue->GetValue( sExpression ); if( EndsWith(sName, "Command") ) { 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 ); } XNodeLuaValue *pRet = new XNodeLuaValue; pRet->SetValueFromStack( L ); return pRet; } } 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 ); FOREACH_Attr( pNode, pAttr ) { XNodeValue *pValue = CompileXMLNodeValue( L, pAttr->first, pAttr->second, sFile ); delete pAttr->second; pAttr->second = pValue; } } 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->AppendAttrFrom( XNode::TEXT_ATTRIBUTE, pValue ); } /* Iterate over the table, pulling out attributes and tables to process. */ vector NodeNamesToAdd; vector NodesToAdd; /* Add array elements first, in array order, so iterating over the XNode keeps the * array in order. */ FOREACH_LUATABLEI( L, -1, i ) { if( !lua_istable(L, -1) ) continue; NodeNamesToAdd.push_back( ssprintf("%i", i) ); NodesToAdd.push_back( LuaReference() ); NodesToAdd.back().SetFromStack( L ); } int iLen = NodeNamesToAdd.size(); FOREACH_LUATABLE( L, -1 ) { /* If this entry is a table, add it recursively. */ if( lua_istable(L, -2) ) { if( lua_isnumber(L, -1) ) { /* If this number is an integer, and between [1,iLen], then we added * this one already above. */ lua_Number f = lua_tonumber( L, -1 ); int i; lua_number2int(i, f); if( i >= 1 && i <= iLen && float(i) == f ) continue; } RString sName; LuaHelpers::Pop( L, sName ); NodeNamesToAdd.push_back( sName ); NodesToAdd.push_back( LuaReference() ); NodesToAdd.back().SetFromStack( L ); continue; } RString sName; LuaHelpers::Pop( L, sName ); /* Otherwise, add an attribute. */ XNodeLuaValue *pValue = new XNodeLuaValue; pValue->SetValueFromStack( L ); pNode->AppendAttrFrom( sName, pValue ); } lua_pop( L, 1 ); /* Recursively process tables. */ for( size_t i = 0; i < NodesToAdd.size(); ++i ) { const RString &sNodeName = NodeNamesToAdd[i]; LuaReference &NodeToAdd = NodesToAdd[i]; /* 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 ) { /* Batch up nodes to move, and do them all at once, to deal sanely * with the possibility of duplicate child names. */ vector aToMove; /* 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 *pSectionNode = *it; XNode *pChildNode = pTo->GetChild( pSectionNode->GetName() ); if( pChildNode == NULL ) { aToMove.push_back( it ); } else { FOREACH_Attr( pSectionNode, it2 ) { /* Don't overwrite existing nodes. */ pChildNode->AppendAttrFrom( it2->first, it2->second->Copy(), false ); } } it = next; } /* Iterate in reverse, since erasing iterators will invalidate the * iterators after it. */ for( int i = aToMove.size()-1; i >= 0; --i ) { XNode *pNode = *aToMove[i]; pFrom->m_childs.erase( aToMove[i] ); pTo->AppendChild( pNode ); } } /* * (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. */