// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace EpicGames.Core { /// /// Specifies how to format JSON output /// public enum JsonWriterStyle { /// /// Omit spaces between elements /// Compact, /// /// Put each value on a newline, and indent output /// Readable } /// /// Writer for JSON data, which indents the output text appropriately, and adds commas and newlines between fields /// public sealed class JsonWriter : IDisposable { TextWriter _writer; readonly bool _leaveOpen; readonly JsonWriterStyle _style; bool _bRequiresComma; string _indent; /// /// Constructor /// /// File to write to /// Should use packed JSON or not public JsonWriter(string fileName, JsonWriterStyle style = JsonWriterStyle.Readable) : this(new StreamWriter(fileName)) { _style = style; } /// /// Constructor /// /// File to write to /// Should use packed JSON or not public JsonWriter(FileReference fileName, JsonWriterStyle style = JsonWriterStyle.Readable) : this(new StreamWriter(fileName.FullName)) { _style = style; } /// /// Constructor /// /// The text writer to output to /// Whether to leave the writer open when the object is disposed /// The output style public JsonWriter(TextWriter writer, bool leaveOpen = false, JsonWriterStyle style = JsonWriterStyle.Readable) { _writer = writer; _leaveOpen = leaveOpen; _style = style; _indent = ""; } /// /// Dispose of any managed resources /// public void Dispose() { if(!_leaveOpen && _writer != null) { _writer.Dispose(); _writer = null!; } } private void IncreaseIndent() { if (_style == JsonWriterStyle.Readable) { _indent += "\t"; } } private void DecreaseIndent() { if (_style == JsonWriterStyle.Readable) { _indent = _indent.Substring(0, _indent.Length - 1); } } /// /// Write the opening brace for an object /// public void WriteObjectStart() { WriteCommaNewline(); _writer.Write(_indent); _writer.Write("{"); IncreaseIndent(); _bRequiresComma = false; } /// /// Write the name and opening brace for an object /// /// Name of the field public void WriteObjectStart(string objectName) { WriteCommaNewline(); WriteName(objectName); _bRequiresComma = false; WriteObjectStart(); } /// /// Write the closing brace for an object /// public void WriteObjectEnd() { DecreaseIndent(); WriteLine(); _writer.Write(_indent); _writer.Write("}"); _bRequiresComma = true; } /// /// Write the opening bracket for an unnamed array /// public void WriteArrayStart() { WriteCommaNewline(); _writer.Write("{0}[", _indent); IncreaseIndent(); _bRequiresComma = false; } /// /// Write the name and opening bracket for an array /// /// Name of the field public void WriteArrayStart(string arrayName) { WriteCommaNewline(); WriteName(arrayName); _writer.Write('['); IncreaseIndent(); _bRequiresComma = false; } /// /// Write the closing bracket for an array /// public void WriteArrayEnd() { DecreaseIndent(); WriteLine(); _writer.Write("{0}]", _indent); _bRequiresComma = true; } private void WriteLine() { if (_style == JsonWriterStyle.Readable) { _writer.WriteLine(); } } private void WriteLine(string line) { if (_style == JsonWriterStyle.Readable) { _writer.WriteLine(line); } else { _writer.Write(line); } } /// /// Write an array of strings /// /// Name of the field /// Values for the field public void WriteStringArrayField(string name, IEnumerable values) { WriteArrayStart(name); foreach(string value in values) { WriteValue(value); } WriteArrayEnd(); } /// /// Write an array of enum values /// /// Name of the field /// Values for the field public void WriteEnumArrayField(string name, IEnumerable values) where T : struct { WriteStringArrayField(name, values.Select(x => x.ToString()!)); } /// /// Write a value with no field name, for the contents of an array /// /// Value to write public void WriteValue(int value) { WriteCommaNewline(); _writer.Write(_indent); _writer.Write(value); _bRequiresComma = true; } /// /// Write a value with no field name, for the contents of an array /// /// Value to write public void WriteValue(string value) { WriteCommaNewline(); _writer.Write(_indent); WriteEscapedString(value); _bRequiresComma = true; } /// /// Write a field name and string value /// /// Name of the field /// Value for the field public void WriteValue(string name, string? value) { WriteCommaNewline(); WriteName(name); WriteEscapedString(value); _bRequiresComma = true; } /// /// Write a field name and integer value /// /// Name of the field /// Value for the field public void WriteValue(string name, int value) { WriteValueInternal(name, value.ToString()); } /// /// Write a field name and unsigned integer value /// /// Name of the field /// Value for the field public void WriteValue(string name, uint value) { WriteValueInternal(name, value.ToString()); } /// /// Write a field name and double value /// /// Name of the field /// Value for the field public void WriteValue(string name, double value) { WriteValueInternal(name, value.ToString()); } /// /// Write a field name and bool value /// /// Name of the field /// Value for the field public void WriteValue(string name, bool value) { WriteValueInternal(name, value ? "true" : "false"); } /// /// Write a field name and enum value /// /// The enum type /// Name of the field /// Value for the field public void WriteEnumValue(string name, T value) where T : struct { WriteValue(name, value.ToString()!); } void WriteCommaNewline() { if (_bRequiresComma) { WriteLine(","); } else if (_indent.Length > 0) { WriteLine(); } } void WriteName(string name) { string space = (_style == JsonWriterStyle.Readable) ? " " : ""; _writer.Write(_indent); WriteEscapedString(name); _writer.Write(":{0}", space); } void WriteValueInternal(string name, string value) { WriteCommaNewline(); WriteName(name); _writer.Write(value); _bRequiresComma = true; } void WriteEscapedString(string? value) { // Escape any characters which may not appear in a JSON string (see http://www.json.org). _writer.Write("\""); if (value != null) { _writer.Write(EscapeString(value)); } _writer.Write("\""); } /// /// Escapes a string for serializing to JSON /// /// The string to escape /// The escaped string public static string EscapeString(string value) { // Prescan the string looking for things to escape. If not found, we don't need to // create the string builder int idx = 0; for (; idx < value.Length; idx++) { char c = value[idx]; if (c == '\"' || c == '\\' || Char.IsControl(c)) { break; } } if (idx == value.Length) { return value; } // Otherwise, create the string builder, append the known portion that doesn't have an escape // and continue processing the string starting at the first character needing to be escaped. StringBuilder result = new StringBuilder(); result.Append(value.AsSpan(0, idx)); for (; idx < value.Length; idx++) { switch (value[idx]) { case '\"': result.Append("\\\""); break; case '\\': result.Append("\\\\"); break; case '\b': result.Append("\\b"); break; case '\f': result.Append("\\f"); break; case '\n': result.Append("\\n"); break; case '\r': result.Append("\\r"); break; case '\t': result.Append("\\t"); break; default: if (Char.IsControl(value[idx])) { result.AppendFormat("\\u{0:X4}", (int)value[idx]); } else { result.Append(value[idx]); } break; } } return result.ToString(); } } }