diff --git a/include/cereal/archives/xml.hpp b/include/cereal/archives/xml.hpp index 3b1440c1..8f8bfb8c 100644 --- a/include/cereal/archives/xml.hpp +++ b/include/cereal/archives/xml.hpp @@ -226,8 +226,16 @@ namespace cereal itsOS.clear(); itsOS.seekp( 0, std::ios::beg ); itsOS << value << std::ends; + const auto strValue = itsOS.str(); + // if there is the first or the last character in string is whitespace then add xml:space attribute + // the last character has index length-2 because there is \0 character at end added with std::ends + if( !strValue.empty() && ( isWhitespace( strValue[0] ) || isWhitespace( strValue[strValue.length() - 2] ) ) ) + { + itsNodes.top().node->append_attribute( itsXML.allocate_attribute( "xml:space", "preserve" ) ); + } + // allocate strings for all of the data in the XML object - auto dataPtr = itsXML.allocate_string( itsOS.str().c_str() ); + auto dataPtr = itsXML.allocate_string( strValue.c_str() ); // insert into the XML itsNodes.top().node->append_node( itsXML.allocate_node( rapidxml::node_data, nullptr, dataPtr ) ); @@ -301,6 +309,11 @@ namespace cereal } }; // NodeInfo + bool isWhitespace( char c ) + { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' ; + } + //! @} private: @@ -367,7 +380,7 @@ namespace cereal try { itsData.push_back('\0'); // rapidxml will do terrible things without the data being null terminated - itsXML.parse( reinterpret_cast( itsData.data() ) ); + itsXML.parse( reinterpret_cast( itsData.data() ) ); } catch( rapidxml::parse_error const & ) { diff --git a/include/cereal/external/rapidxml/rapidxml.hpp b/include/cereal/external/rapidxml/rapidxml.hpp index 26bf065c..0cb03988 100644 --- a/include/cereal/external/rapidxml/rapidxml.hpp +++ b/include/cereal/external/rapidxml/rapidxml.hpp @@ -334,6 +334,14 @@ namespace rapidxml } return true; } + + template + inline bool preserve_space(xml_node* node) + { + const Ch preserve_value[] = { Ch('p'), Ch('r'), Ch('e'), Ch('s'), Ch('e'), Ch('r'), Ch('v'), Ch('e') }; + const xml_attribute* space = node->first_attribute("xml:space"); + return space && internal::compare(space->value(), space->value_size(), preserve_value, sizeof(preserve_value) / sizeof(Ch), true); + } } //! \endcond @@ -1566,7 +1574,7 @@ namespace rapidxml // - replacing XML character entity references with proper characters (' & " < > &#...;) // - condensing whitespace sequences to single space character template - static Ch *skip_and_expand_character_refs(Ch *&text) + static Ch *skip_and_expand_character_refs(Ch *&text, bool preserve_space) { // If entity translation, whitespace condense and whitespace trimming is disabled, use plain skip if (Flags & parse_no_entity_translation && @@ -1691,7 +1699,7 @@ namespace rapidxml } // If whitespace condensing is enabled - if (Flags & parse_normalize_whitespace) + if ((Flags & parse_normalize_whitespace) && !preserve_space) { // Test if condensing is needed if (whitespace_pred::test(*src)) @@ -1942,15 +1950,17 @@ namespace rapidxml if (!(Flags & parse_trim_whitespace)) text = contents_start; + const bool preserve_space = internal::preserve_space(node); + // Skip until end of data Ch *value_ = text, *end; - if (Flags & parse_normalize_whitespace) - end = skip_and_expand_character_refs(text); + if ((Flags & parse_normalize_whitespace) && !preserve_space) + end = skip_and_expand_character_refs(text, false); else - end = skip_and_expand_character_refs(text); + end = skip_and_expand_character_refs(text, preserve_space); // Trim trailing whitespace if flag is set; leading was already trimmed by whitespace skip after > - if (Flags & parse_trim_whitespace) + if ((Flags & parse_trim_whitespace) && !preserve_space) { if (Flags & parse_normalize_whitespace) { @@ -2187,6 +2197,12 @@ namespace rapidxml case Ch('<'): if (text[1] == Ch('/')) { + Ch *contents_end = 0; + if (internal::preserve_space(node)) + { + contents_end = text; + } + // Node closing text += 2; // Skip '')) RAPIDXML_PARSE_ERROR("expected >", text); ++text; // Skip '>' + + if (contents_end && contents_end != contents_start) + { + node->value(contents_start, contents_end - contents_start); + node->value()[node->value_size()] = Ch('\0'); + } return; // Node closed, finished parsing contents } else @@ -2275,9 +2297,9 @@ namespace rapidxml Ch *value_ = text, *end; const int AttFlags = Flags & ~parse_normalize_whitespace; // No whitespace normalization in attributes if (quote == Ch('\'')) - end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); + end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text, false); else - end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); + end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text, false); // Set attribute value attribute->value(value_, end - value_); diff --git a/unittests/basic_string.cpp b/unittests/basic_string.cpp index 20ca9fab..d9a9d454 100644 --- a/unittests/basic_string.cpp +++ b/unittests/basic_string.cpp @@ -26,6 +26,7 @@ */ #include "common.hpp" #include +#include template void test_string_basic() @@ -125,28 +126,37 @@ BOOST_AUTO_TEST_CASE( json_string_basic ) test_string_basic(); } -template -void test_string_for_array(T (&strings)[N][M]) +template +void test_ws_in_out(Out const & o_value_with_ws) { - for(size_t i=0; i o_string = strings[i]; + OArchive oar(os); + oar(o_value_with_ws); + } - std::ostringstream os; - { - OArchive oar(os); - oar(o_string); - } + In i_value_with_ws; - std::basic_string i_string; + std::istringstream is(os.str()); + { + IArchive iar(is); + iar(i_value_with_ws); + } - std::istringstream is(os.str()); - { - IArchive iar(is); - iar(i_string); - } + BOOST_CHECK_EQUAL(i_value_with_ws, o_value_with_ws); +} - BOOST_CHECK_EQUAL(i_string, o_string); +namespace boost +{ + + void save( cereal::XMLOutputArchive & ar, boost::string_ref const & str ) + { + ar.saveValue( str ); + } + + bool operator==( std::string const & s1, boost::string_ref const & s2 ) + { + return s1 == std::string(s2.data(), s2.length()); } } @@ -154,6 +164,8 @@ BOOST_AUTO_TEST_CASE( xml_string_issue109 ) { char strings[][20] = { "some text", + "some text ", + " some text", " some text ", " ", " text ", @@ -163,5 +175,50 @@ BOOST_AUTO_TEST_CASE( xml_string_issue109 ) " & & " }; - test_string_for_array(strings); + for( size_t i=0; i<( sizeof( strings ) / sizeof( strings[0] ) ); ++i ) + { + std::basic_string o_string = strings[i]; + + test_ws_in_out( o_string ); + } + + for( size_t i=0; i<( sizeof( strings ) / sizeof( strings[0] ) ); ++i ) + { + boost::string_ref o_string = strings[i]; + + test_ws_in_out( o_string ); + } +} + +BOOST_AUTO_TEST_CASE( xml_char_issue109 ) +{ + uint8_t chars[] = { + ' ', + '\t', + '\n', + '\r', + '&', + '>', + '<', + '\'', + '"', + '!', + '|' + }; + + for( size_t i=0; i<( sizeof( chars ) / sizeof( chars[0] ) ); ++i ) + { + test_ws_in_out( chars[i] ); + } + + for( size_t i=0; i<( sizeof( chars ) / sizeof( chars[0] ) ); ++i ) + { + test_ws_in_out( int8_t( chars[i] ) ); + } + + // TODO: Uncomment these lines once support for char in XML archive is fixed. + //for( size_t i=0; i<( sizeof( chars ) / sizeof( chars[0] ) ); ++i ) + //{ + // test_ws_in_out( char( chars[i] ) ); + //} }