@@ -3820,20 +3820,31 @@ FROM INFORMATION_SCHEMA.COLUMNS
38203820 // The grinning-face emoji is outside the BMP (a UTF-16 surrogate pair, four UTF-8 bytes) and the euro sign
38213821 // is a single UTF-16 code unit but three UTF-8 bytes; both are represented differently in UTF-16 than in
38223822 // UTF-8 and are lost when an xml value is sent to the server as a non-Unicode string, which makes them good
3823- // probes for the SqlXml/ SqlDbType.Xml parameter path.
3823+ // probes for the SqlDbType.Xml parameter path.
38243824 private const string XmlEmoji = "\U0001F600 " ;
38253825 private const string XmlEuro = "\u20AC " ;
38263826
38273827 [ Theory ]
3828- [ InlineData ( "<root>" + XmlEmoji + XmlEuro + "</root>" , "<root>" + XmlEmoji + XmlEuro + "</root>" ) ]
3829- // An explicit non-UTF-16 prolog is accepted because the value is sent as 'xml', not 'nvarchar(max)'.
3830- [ InlineData ( "<?xml version=\" 1.0\" encoding=\" utf-8\" ?><root>" + XmlEmoji + "</root>" , "<root>" + XmlEmoji + "</root>" ) ]
3831- [ InlineData ( "<?xml version=\" 1.0\" encoding=\" utf-16\" ?><root>" + XmlEuro + "</root>" , "<root>" + XmlEuro + "</root>" ) ]
3828+ [ InlineData (
3829+ "<root>" + XmlEmoji + XmlEuro + "</root>" ,
3830+ "<root>" + XmlEmoji + XmlEuro + "</root>" ,
3831+ "<root>" + XmlEmoji + XmlEuro + "</root>" ) ]
3832+ // Only the XML declaration is removed; a following stylesheet PI and the rest of the value are sent verbatim.
3833+ [ InlineData (
3834+ "<?xml version=\" 1.0\" encoding=\" utf-8\" standalone='yes' ?> <?xml-stylesheet href=\" style.xsl\" type=\" text/xml\" ?> <root>" + XmlEmoji + "</root>" ,
3835+ " <?xml-stylesheet href=\" style.xsl\" type=\" text/xml\" ?> <root>" + XmlEmoji + "</root>" ,
3836+ "<?xml-stylesheet href=\" style.xsl\" type=\" text/xml\" ?><root>" + XmlEmoji + "</root>" ) ]
3837+ // The leading whitespace and the declaration are removed when the value is sent.
3838+ [ InlineData (
3839+ " <?xml version=\" 1.1\" encoding=\" utf-16\" ?> <root>" + XmlEuro + "</root>" ,
3840+ " <root>" + XmlEuro + "</root>" ,
3841+ "<root>" + XmlEuro + "</root>" ) ]
38323842 // Content forms that the 'xml' store type accepts beyond a single well-formed document.
3833- [ InlineData ( "" , "" ) ]
3834- [ InlineData ( "text fragment" , "text fragment" ) ]
3835- [ InlineData ( "<a/><b/>" , "<a /><b />" ) ]
3836- public async Task Xml_value_round_trips ( string value , string expected )
3843+ [ InlineData ( "" , "" , "" ) ]
3844+ [ InlineData ( "text fragment" , "text fragment" , "text fragment" ) ]
3845+ // The content is sent verbatim, but the server expands self-closing tags when the xml column is read back.
3846+ [ InlineData ( "<a/><b/>" , "<a/><b/>" , "<a /><b />" ) ]
3847+ public async Task Xml_value_round_trips ( string value , string expected , string roundTripped )
38373848 {
38383849 await using var context = CreateContext ( ) ;
38393850
@@ -3845,7 +3856,7 @@ public async Task Xml_value_round_trips(string value, string expected)
38453856 context . ChangeTracker . Clear ( ) ;
38463857
38473858 // xml columns cannot be compared directly in a WHERE clause, so the row is fetched by its key. Coalescing
3848- // the column with the original value sends that value as an 'xml' parameter, exercising the SqlXml
3859+ // the column with the original value sends that value as an 'xml' parameter, exercising the prolog-removal
38493860 // parameter path in a query in addition to the insert above.
38503861 var query = context . Set < XmlTestDocument > ( )
38513862 . Where ( d => d . Id == id )
@@ -3860,14 +3871,15 @@ SELECT COALESCE([x].[Content], @value)
38603871FROM [XmlTestDocument] AS [x]
38613872WHERE [x].[Id] = @id
38623873""" ,
3863- query . ToQueryString ( ) ) ;
3874+ query . ToQueryString ( ) ,
3875+ ignoreLineEndingDifferences : true ) ;
38643876
3865- var roundTripped = await query . SingleAsync ( ) ;
3866- Assert . Equal ( expected , roundTripped ) ;
3877+ var actual = await query . SingleAsync ( ) ;
3878+ Assert . Equal ( roundTripped , actual ) ;
38673879
38683880 AssertSql (
38693881 $ """
3870- @p0='{ expected } ' (DbType = Xml)
3882+ @p0='{ expected } ' (Size = -1) ( DbType = Xml)
38713883
38723884SET IMPLICIT_TRANSACTIONS OFF;
38733885SET NOCOUNT ON;
@@ -3877,7 +3889,7 @@ OUTPUT INSERTED.[Id]
38773889""" ,
38783890 //
38793891 $ """
3880- @value='{ expected } ' (DbType = Xml)
3892+ @value='{ expected } ' (Size = -1) ( DbType = Xml)
38813893@id='{ id } '
38823894
38833895SELECT TOP(2) COALESCE([x].[Content], @value)
@@ -3886,40 +3898,6 @@ FROM [XmlTestDocument] AS [x]
38863898""" ) ;
38873899 }
38883900
3889- [ Fact ]
3890- public async Task Xml_value_with_dtd_payload_is_rejected ( )
3891- {
3892- await using var context = CreateContext ( ) ;
3893-
3894- // A "billion laughs" entity-expansion payload: the reader must reject the DTD rather than expand it.
3895- const string maliciousXml =
3896- "<?xml version=\" 1.0\" ?>"
3897- + "<!DOCTYPE lolz [<!ENTITY lol \" lol\" >"
3898- + "<!ENTITY lol2 \" &lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\" >"
3899- + "<!ENTITY lol3 \" &lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\" >]>"
3900- + "<lolz>&lol3;</lolz>" ;
3901-
3902- context . Add ( new XmlTestDocument { Content = maliciousXml } ) ;
3903-
3904- var exception = await Assert . ThrowsAnyAsync < Exception > ( ( ) => context . SaveChangesAsync ( ) ) ;
3905- Assert . True (
3906- HasXmlException ( exception ) ,
3907- $ "Expected an { nameof ( XmlException ) } in the exception chain but found: { exception } ") ;
3908-
3909- static bool HasXmlException ( Exception exception )
3910- {
3911- for ( var current = exception ; current is not null ; current = current . InnerException )
3912- {
3913- if ( current is XmlException )
3914- {
3915- return true ;
3916- }
3917- }
3918-
3919- return false ;
3920- }
3921- }
3922-
39233901 private class XmlTestDocument
39243902 {
39253903 public int Id { get ; set ; }
0 commit comments