Skip to content

Commit db98dde

Browse files
committed
Reuse string builder
1 parent 73cfc03 commit db98dde

File tree

1 file changed

+64
-54
lines changed

1 file changed

+64
-54
lines changed

ValveKeyValue/ValveKeyValue/Deserialization/KeyValues3/KV3TokenReader.cs

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)