@@ -19,6 +19,8 @@ class KV3TokenReader : KVTokenReader
1919 // Dota 2 binary from 2017 used "+" as a terminate (for flagged values), but then they changed it to "|"
2020 static readonly SearchValues < char > TokenTerminators = SearchValues . Create ( "{}[]=, \t \n \r '\" :|;" ) ;
2121
22+ readonly StringBuilder sb = new ( ) ;
23+
2224 public KV3TokenReader ( TextReader textReader ) : base ( textReader )
2325 {
2426 }
@@ -112,8 +114,6 @@ KVToken ReadBinaryBlob()
112114 ReadChar ( BinaryBlobMarker ) ;
113115 ReadChar ( ArrayStart ) ; // TODO: Strictly speaking Valve allows bare # without [ to be read as literal value (but what would that be?)
114116
115- var sb = new StringBuilder ( ) ;
116-
117117 while ( true )
118118 {
119119 var next = Next ( ) ;
@@ -131,7 +131,9 @@ KVToken ReadBinaryBlob()
131131 sb . Append ( next ) ;
132132 }
133133
134- return new KVToken ( KVTokenType . BinaryBlob , sb . ToString ( ) ) ;
134+ var result = sb . ToString ( ) ;
135+ sb . Clear ( ) ;
136+ return new KVToken ( KVTokenType . BinaryBlob , result ) ;
135137 }
136138
137139 public KVHeader ReadHeader ( )
@@ -307,8 +309,6 @@ string ReadToken()
307309 return ReadQuotedStringRaw ( ( char ) next ) ;
308310 }
309311
310- var sb = new StringBuilder ( ) ;
311-
312312 while ( true )
313313 {
314314 next = Peek ( ) ;
@@ -321,7 +321,9 @@ string ReadToken()
321321 sb . Append ( Next ( ) ) ;
322322 }
323323
324- return sb . ToString ( ) ;
324+ var result = sb . ToString ( ) ;
325+ sb . Clear ( ) ;
326+ return result ;
325327 }
326328
327329 string ReadQuotedStringRaw ( char quotationMark )
@@ -330,8 +332,6 @@ string ReadQuotedStringRaw(char quotationMark)
330332
331333 var isMultiline = false ;
332334
333- var sb = new StringBuilder ( ) ;
334-
335335 if ( quotationMark == '"' && Peek ( ) == '"' )
336336 {
337337 Next ( ) ;
@@ -356,13 +356,22 @@ string ReadQuotedStringRaw(char quotationMark)
356356 }
357357 }
358358
359+ var escaped = false ;
360+
359361 if ( isMultiline )
360362 {
361363 while ( true )
362364 {
363365 var next = Next ( ) ;
364366
365- if ( next == '"' && ! IsEscaped ( sb ) )
367+ if ( next == '\\ ' )
368+ {
369+ escaped = ! escaped ;
370+ sb . Append ( next ) ;
371+ continue ;
372+ }
373+
374+ if ( next == '"' && ! escaped )
366375 {
367376 // Check if this is the start of """
368377 if ( Peek ( ) == '"' )
@@ -382,96 +391,97 @@ string ReadQuotedStringRaw(char quotationMark)
382391 }
383392 }
384393
394+ escaped = false ;
385395 sb . Append ( next ) ;
386396 }
387397
388398 // Strip trailing newline (\n or \r\n)
389399 if ( sb . Length > 0 && sb [ ^ 1 ] == '\n ' )
390400 {
391- sb . Remove ( sb . Length - 1 , 1 ) ;
401+ sb . Length -- ;
392402
393403 if ( sb . Length > 0 && sb [ ^ 1 ] == '\r ' )
394404 {
395- sb . Remove ( sb . Length - 1 , 1 ) ;
405+ sb . Length -- ;
396406 }
397407 }
408+
409+ var result = sb . ToString ( ) ;
410+ sb . Clear ( ) ;
411+ return result ;
398412 }
399413 else
400414 {
415+ var hasEscapes = false ;
416+
401417 while ( true )
402418 {
403419 var next = Next ( ) ;
404420
405- if ( next == quotationMark && ! IsEscaped ( sb ) )
421+ if ( next == '\\ ' )
422+ {
423+ escaped = ! escaped ;
424+ hasEscapes = true ;
425+ sb . Append ( next ) ;
426+ continue ;
427+ }
428+
429+ if ( next == quotationMark && ! escaped )
406430 {
407431 break ;
408432 }
409433
434+ escaped = false ;
410435 sb . Append ( next ) ;
411436 }
412437
413- return UnescapeString ( sb ) ;
414- }
415-
416- return sb . ToString ( ) ;
417- }
418-
419- static bool IsEscaped ( StringBuilder sb )
420- {
421- var count = 0 ;
438+ if ( ! hasEscapes )
439+ {
440+ var result = sb . ToString ( ) ;
441+ sb . Clear ( ) ;
442+ return result ;
443+ }
422444
423- for ( var i = sb . Length - 1 ; i >= 0 && sb [ i ] == '\\ ' ; i -- )
424- {
425- count ++ ;
445+ return UnescapeString ( ) ;
426446 }
427-
428- return count % 2 == 1 ;
429447 }
430448
431- static string UnescapeString ( StringBuilder input )
449+ string UnescapeString ( )
432450 {
433- if ( input . Length == 0 )
451+ var length = sb . Length ;
452+
453+ if ( length == 0 )
434454 {
435455 return string . Empty ;
436456 }
437457
438- var result = new StringBuilder ( input . Length ) ;
439- var isEscaped = false ;
458+ // Unescape in-place by reading ahead and writing back
459+ var write = 0 ;
440460
441- for ( var i = 0 ; i < input . Length ; i ++ )
461+ for ( var read = 0 ; read < length ; read ++ )
442462 {
443- var c = input [ i ] ;
463+ var c = sb [ read ] ;
444464
445- if ( c == '\\ ' && ! isEscaped )
465+ if ( c == '\\ ' && read + 1 < length )
446466 {
447- isEscaped = true ;
448- continue ;
449- }
450-
451- if ( isEscaped )
452- {
453- switch ( c )
467+ read ++ ;
468+ sb [ write ++ ] = sb [ read ] switch
454469 {
455- case 'n' :
456- result . Append ( '\n ' ) ;
457- break ;
458- case 't' :
459- result . Append ( '\t ' ) ;
460- break ;
461- default :
462- result . Append ( c ) ;
463- break ;
464- }
465-
466- isEscaped = false ;
470+ 'n' => '\n ' ,
471+ 't' => '\t ' ,
472+ var x => x ,
473+ } ;
467474 }
468475 else
469476 {
470- result . Append ( c ) ;
477+ sb [ write ++ ] = c ;
471478 }
472479 }
473480
474- return result . ToString ( ) ;
481+ sb . Length = write ;
482+ var result = sb . ToString ( ) ;
483+ sb . Clear ( ) ;
484+ return result ;
475485 }
476486 }
477487}
0 commit comments