diff --git a/SteamKit2/SteamKit2/Types/KeyValue.cs b/SteamKit2/SteamKit2/Types/KeyValue.cs index fa4b69eab..c908ce736 100644 --- a/SteamKit2/SteamKit2/Types/KeyValue.cs +++ b/SteamKit2/SteamKit2/Types/KeyValue.cs @@ -25,7 +25,7 @@ public KVTextReader( KeyValue kv, Stream input ) : base( input ) { bool wasQuoted; - bool wasConditional; + string condition; KeyValue currentKey = kv; @@ -33,7 +33,7 @@ public KVTextReader( KeyValue kv, Stream input ) { // bool bAccepted = true; - string s = ReadToken( out wasQuoted, out wasConditional ); + string s = ReadToken( out wasQuoted, out condition ); if ( string.IsNullOrEmpty( s ) ) break; @@ -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 ) @@ -76,7 +76,7 @@ private void EatWhiteSpace() { while ( !EndOfStream ) { - if ( !Char.IsWhiteSpace( ( char )Peek() ) ) + if ( !char.IsWhiteSpace( ( char )Peek() ) ) { break; } @@ -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; @@ -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 ) { @@ -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 == '"' ) @@ -138,8 +140,6 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional ) // " Read(); - - var sb = new StringBuilder(); while ( !EndOfStream ) { if ( Peek() == '\\' ) @@ -165,8 +165,6 @@ public string ReadToken( out bool wasQuoted, out bool wasConditional ) // " Read(); - - return sb.ToString(); } if ( next == '{' || next == '}' ) @@ -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 ) @@ -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 ) { @@ -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(); } } @@ -559,7 +569,7 @@ public bool ReadAsText( Stream input ) /// true if the read was successful; otherwise, false. 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 ); } @@ -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; @@ -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 ) @@ -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 } } } @@ -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" ); } } @@ -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 ) ); diff --git a/SteamKit2/Tests/KeyValueFacts.cs b/SteamKit2/Tests/KeyValueFacts.cs index 144a41f99..24bcddc3b 100644 --- a/SteamKit2/Tests/KeyValueFacts.cs +++ b/SteamKit2/Tests/KeyValueFacts.cs @@ -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() ) { @@ -496,8 +602,7 @@ public void KeyValuesSavesTextToStream() text = reader.ReadToEnd(); } } - - Assert.Equal( expected, text ); + return text; } const string TestObjectHex = "00546573744F626A65637400016B65790076616C7565000808";