Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KeyValue: Handle (ignore) conditionals #684

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 59 additions & 43 deletions SteamKit2/SteamKit2/Types/KeyValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ public KVTextReader( KeyValue kv, Stream input )
: base( input )
{
bool wasQuoted;
bool wasConditional;
string condition;

KeyValue currentKey = kv;

do
{
// bool bAccepted = true;

string s = ReadToken( out wasQuoted, out wasConditional );
string s = ReadToken( out wasQuoted, out condition );

if ( string.IsNullOrEmpty( s ) )
break;
Expand All @@ -47,14 +47,14 @@ public KVTextReader( KeyValue kv, Stream input )
currentKey.Name = s;
}

s = ReadToken( out wasQuoted, out wasConditional );
s = ReadToken( out wasQuoted, out condition);

if ( wasConditional )
if ( condition != null )
{
// bAccepted = ( s == "[$WIN32]" );

// Now get the '{'
s = ReadToken( out wasQuoted, out wasConditional );
s = ReadToken( out wasQuoted, out condition);
}

if ( s.StartsWith( "{" ) && !wasQuoted )
Expand All @@ -76,7 +76,7 @@ private void EatWhiteSpace()
{
while ( !EndOfStream )
{
if ( !Char.IsWhiteSpace( ( char )Peek() ) )
if ( !char.IsWhiteSpace( ( char )Peek() ) )
{
break;
}
Expand All @@ -92,14 +92,14 @@ private bool EatCPPComment()
char next = ( char )Peek();
if ( next == '/' )
{
ReadLine();
return true;
/*
* As came up in parsing the Dota 2 units.txt file, the reference (Valve) implementation
* of the KV format considers a single forward slash to be sufficient to comment out the
* entirety of a line. While they still _tend_ to use two, it's not required, and likely
* is just done out of habit.
*/
ReadLine();
return true;
/*
* As came up in parsing the Dota 2 units.txt file, the reference (Valve) implementation
* of the KV format considers a single forward slash to be sufficient to comment out the
* entirety of a line. While they still _tend_ to use two, it's not required, and likely
* is just done out of habit.
*/
}

return false;
Expand All @@ -108,10 +108,10 @@ private bool EatCPPComment()
return false;
}

public string ReadToken( out bool wasQuoted, out bool wasConditional )
public string ReadToken( out bool wasQuoted, out string condition )
{
wasQuoted = false;
wasConditional = false;
condition = null;

while ( true )
{
Expand All @@ -130,6 +130,8 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional )

if ( EndOfStream )
return null;

var sb = new StringBuilder();

char next = ( char )Peek();
if ( next == '"' )
Expand All @@ -138,8 +140,6 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional )

// "
Read();

var sb = new StringBuilder();
while ( !EndOfStream )
{
if ( Peek() == '\\' )
Expand All @@ -165,8 +165,6 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional )

// "
Read();

return sb.ToString();
}

if ( next == '{' || next == '}' )
Expand All @@ -175,7 +173,6 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional )
return next.ToString();
}

bool bConditionalStart = false;
int count = 0;
var ret = new StringBuilder();
while ( !EndOfStream )
Expand All @@ -185,14 +182,17 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional )
if ( next == '"' || next == '{' || next == '}' )
break;

if ( next == '[' )
bConditionalStart = true;

if ( next == ']' && bConditionalStart )
wasConditional = true;
if ( char.IsWhiteSpace( next ) )
{
EatWhiteSpace();
continue;
}

if ( Char.IsWhiteSpace( next ) )
break;
if ( next == '/' )
{
EatCPPComment();
continue;
}

if ( count < 1023 )
{
Expand All @@ -206,7 +206,17 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional )
Read();
}

return ret.ToString();
if ( ret.Length > 0 )
{
condition = ret.ToString();

if ( condition[ 0 ] != '[' || condition[ condition.Length - 1 ] != ']' )
{
throw new InvalidDataException("Improperly formatted conditional.");
}
}

return sb.ToString();
}
}

Expand Down Expand Up @@ -559,7 +569,7 @@ public bool ReadAsText( Stream input )
/// <returns><c>true</c> if the read was successful; otherwise, <c>false</c>.</returns>
public bool ReadFileAsText( string filename )
{
using ( FileStream fs = new FileStream( filename, FileMode.Open ) )
using ( FileStream fs = new FileStream( filename, FileMode.Open, FileAccess.Read, FileShare.Read ) )
{
return ReadAsText( fs );
}
Expand All @@ -568,20 +578,25 @@ public bool ReadFileAsText( string filename )
internal void RecursiveLoadFromBuffer( KVTextReader kvr )
{
bool wasQuoted;
bool wasConditional;
string condition;

while ( true )
{
// bool bAccepted = true;

// get the key name
string name = kvr.ReadToken( out wasQuoted, out wasConditional );

string name = kvr.ReadToken( out wasQuoted, out condition );
if ( string.IsNullOrEmpty( name ) )
{
throw new Exception( "RecursiveLoadFromBuffer: got EOF or empty keyname" );
}

if (condition != null)
{
throw new Exception("RecursiveLoadFromBuffer: got conditional between key and value");
}

if ( name.StartsWith( "}" ) && !wasQuoted ) // top level closed, stop reading
break;

Expand All @@ -590,12 +605,12 @@ internal void RecursiveLoadFromBuffer( KVTextReader kvr )
this.Children.Add( dat );

// get the value
string value = kvr.ReadToken( out wasQuoted, out wasConditional );
string value = kvr.ReadToken( out wasQuoted, out condition);

if ( wasConditional && value != null )
if ( condition != null && value != null )
{
// bAccepted = ( value == "[$WIN32]" );
value = kvr.ReadToken( out wasQuoted, out wasConditional );
// value = kvr.ReadToken( out wasQuoted, out condition);
}

if ( value == null )
Expand All @@ -610,13 +625,7 @@ internal void RecursiveLoadFromBuffer( KVTextReader kvr )
}
else
{
if ( wasConditional )
{
throw new Exception( "RecursiveLoadFromBuffer: got conditional between key and value" );
}

dat.Value = value;
// blahconditionalsdontcare
}
}
}
Expand Down Expand Up @@ -701,7 +710,7 @@ private void RecursiveSaveTextToFile( Stream stream, int indentLevel = 0 )
WriteIndents( stream, indentLevel + 1 );
WriteString( stream, child.Name, true );
WriteString( stream, "\t\t" );
WriteString( stream, child.AsString(), true );
WriteString( stream, FormatValueForWriting( child ), true );
WriteString( stream, "\n" );
}
}
Expand All @@ -710,6 +719,13 @@ private void RecursiveSaveTextToFile( Stream stream, int indentLevel = 0 )
WriteString( stream, "}\n" );
}

static string FormatValueForWriting( KeyValue kv )
{
return kv.AsString()
.Replace("\r\n", @"\n")
.Replace("\n", @"\n");
}

void WriteIndents( Stream stream, int indentLevel )
{
WriteString( stream, new string( '\t', indentLevel ) );
Expand Down
109 changes: 107 additions & 2 deletions SteamKit2/Tests/KeyValueFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,112 @@ public void KeyValuesSavesTextToStream()
}
};

var text = SaveToText( kv );

Assert.Equal( expected, text );
}

[Fact]
public void CanLoadUnicodeTextDocument()
{
var expected = "\"RootNode\"\n{\n\t\"key1\"\t\t\"value1\"\n\t\"key2\"\n\t{\n\t\t\"ChildKey\"\t\t\"ChildValue\"\n\t}\n}\n";
var kv = new KeyValue();

var temporaryFile = Path.GetTempFileName();
try
{
File.WriteAllText( temporaryFile, expected, Encoding.Unicode );
kv.ReadFileAsText( temporaryFile );
}
finally
{
File.Delete( temporaryFile );
}

Assert.Equal( "RootNode", kv.Name );
Assert.Equal( 2, kv.Children.Count );
Assert.Equal( "key1", kv.Children[0].Name );
Assert.Equal( "value1", kv.Children[0].Value );
Assert.Equal( "key2", kv.Children[1].Name );
Assert.Equal( 1, kv.Children[1].Children.Count );
Assert.Equal( "ChildKey", kv.Children[1].Children[0].Name );
Assert.Equal( "ChildValue", kv.Children[1].Children[0].Value );
}

[Fact]
public void CanLoadUnicodeTextStream()
{
var expected = "\"RootNode\"\n{\n\t\"key1\"\t\t\"value1\"\n\t\"key2\"\n\t{\n\t\t\"ChildKey\"\t\t\"ChildValue\"\n\t}\n}\n";
var kv = new KeyValue();

var temporaryFile = Path.GetTempFileName();
try
{
File.WriteAllText( temporaryFile, expected, Encoding.Unicode );

using ( var fs = File.OpenRead( temporaryFile ) )
{
kv.ReadAsText( fs );
}
}
finally
{
File.Delete( temporaryFile );
}

Assert.Equal( "RootNode", kv.Name );
Assert.Equal( 2, kv.Children.Count );
Assert.Equal( "key1", kv.Children[0].Name );
Assert.Equal( "value1", kv.Children[0].Value );
Assert.Equal( "key2", kv.Children[1].Name );
Assert.Equal( 1, kv.Children[1].Children.Count );
Assert.Equal( "ChildKey", kv.Children[1].Children[0].Name );
Assert.Equal( "ChildValue", kv.Children[1].Children[0].Value );
}

[Fact]
public void CanReadAndIgnoreConditionals()
{
var text = @"
""Repro""
{
""Conditional"" ""You're not running Windows."" [$!WIN32] // DEPRECATED
""EmptyThing"" """"
}
".Trim();

var kv = new KeyValue();
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))
{
kv.ReadAsText(ms);
}

Assert.Equal( "Repro", kv.Name );
Assert.Equal( 2, kv.Children.Count );
Assert.Equal( "Conditional", kv.Children[0].Name );
Assert.Equal( "You're not running Windows.", kv.Children[0].Value );
Assert.Equal( "EmptyThing", kv.Children[1].Name );
Assert.Equal( "", kv.Children[1].Value );
}

[Fact]
public void WritesNewLineAsSlashN()
{
var kv = new KeyValue( "abc" );
kv.Children.Add( new KeyValue( "def", "ghi\njkl" ) );
var text = SaveToText( kv );
var expected = (@"
""abc""
{
" + "\t" + @"""def"" ""ghi\njkl""
}
").Trim().Replace("\r\n", "\n") + "\n";

Assert.Equal(expected, text);
}

static string SaveToText( KeyValue kv )
{
string text;
using ( var ms = new MemoryStream() )
{
Expand All @@ -496,8 +602,7 @@ public void KeyValuesSavesTextToStream()
text = reader.ReadToEnd();
}
}

Assert.Equal( expected, text );
return text;
}

const string TestObjectHex = "00546573744F626A65637400016B65790076616C7565000808";
Expand Down