public static CBORObject ParseJSONNumber( string str, bool integersOnly, bool positiveOnly, bool preserveNegativeZero) { if (String.IsNullOrEmpty(str)) { return(null); } if (integersOnly) { for (var i = 0; i < str.Length; ++i) { if (str[i] >= '0' && str[i] <= '9' && (i > 0 || str[i] != '-')) { return(null); } } } JSONOptions jo = preserveNegativeZero ? PreserveNegZeroYes : PreserveNegZeroNo; return((positiveOnly && str[0] == '-') ? null : ParseJSONNumber(str, 0, str.Length, jo)); }
internal static CBORObject ParseSmallNumber(int digit, JSONOptions options) { #if DEBUG if (digit < 0) { throw new ArgumentException("digit (" + digit + ") is not greater" + "\u0020or equal to 0"); } #endif if (options != null && options.NumberConversion == JSONOptions.ConversionMode.Double) { return(CBORObject.FromObject((double)digit)); } else if (options != null && options.NumberConversion == JSONOptions.ConversionMode.Decimal128) { return(CBORObject.FromObject(EDecimal.FromInt32(digit))); } else { // NOTE: Assumes digit is nonnegative, so PreserveNegativeZeros is irrelevant return(CBORObject.FromObject(digit)); } }
/// <summary>Parses a number whose format follows the JSON /// specification (RFC 8259) and converts that number to a CBOR /// object.</summary> /// <param name='str'>A text string to parse as a JSON number.</param> /// <param name='offset'>An index, starting at 0, showing where the /// desired portion of <paramref name='str'/> begins.</param> /// <param name='count'>The length, in code units, of the desired /// portion of <paramref name='str'/> (but not more than <paramref /// name='str'/> 's length).</param> /// <param name='options'>An object containing options to control how /// JSON numbers are decoded to CBOR objects. Can be null, in which /// case a JSONOptions object with all default properties is used /// instead.</param> /// <returns>A CBOR object that represents the parsed number. Returns /// null if the parsing fails, including if the string is null or empty /// or <paramref name='count'/> is 0 or less.</returns> /// <exception cref='ArgumentNullException'>The parameter <paramref /// name='str'/> is null.</exception> /// <exception cref='ArgumentException'>Unsupported conversion /// kind.</exception> /// <remarks>Roughly speaking, a valid JSON number consists of an /// optional minus sign, one or more basic digits (starting with 1 to 9 /// unless there is only one digit and that digit is 0), an optional /// decimal point (".", full stop) with one or more basic digits, and /// an optional letter E or e with an optional plus or minus sign and /// one or more basic digits (the exponent). A string representing a /// valid JSON number is not allowed to contain white space characters, /// including spaces.</remarks> public static CBORObject ParseJSONNumber( string str, int offset, int count, JSONOptions options) { return(ParseJSONNumber(str, offset, count, options, null)); }
public CBORJson(CharacterInputWithCount reader, JSONOptions options) { this.reader = reader; this.sb = null; this.options = options; this.jsonSequenceMode = false; this.recordSeparatorSeen = false; }
internal static CBORObject ParseJSONValue( CharacterInputWithCount reader, JSONOptions options, int[] nextChar) { var cj = new CBORJson(reader, options); return(cj.ParseJSON(nextChar)); }
/// <summary>Parses a number whose format follows the JSON /// specification (RFC 8259) and converts that number to a CBOR /// object.</summary> /// <param name='str'>A text string to parse as a JSON number.</param> /// <param name='options'>An object containing options to control how /// JSON numbers are decoded to CBOR objects. Can be null, in which /// case a JSONOptions object with all default properties is used /// instead.</param> /// <returns>A CBOR object that represents the parsed number. Returns /// null if the parsing fails, including if the string is null or /// empty.</returns> /// <remarks>Roughly speaking, a valid JSON number consists of an /// optional minus sign, one or more basic digits (starting with 1 to 9 /// unless there is only one digit and that digit is 0), an optional /// decimal point (".", full stop) with one or more basic digits, and /// an optional letter E or e with an optional plus or minus sign and /// one or more basic digits (the exponent). A string representing a /// valid JSON number is not allowed to contain white space characters, /// including spaces.</remarks> public static CBORObject ParseJSONNumber( string str, JSONOptions options) { return(String.IsNullOrEmpty(str) ? null : ParseJSONNumber(str, 0, str.Length, options)); }
internal static void WriteJSONToInternal( CBORObject obj, StringOutput writer, JSONOptions options) { if (obj.Type == CBORType.Array || obj.Type == CBORType.Map) { var stack = new List <CBORObject>(); WriteJSONToInternal(obj, writer, options, stack); } else { WriteJSONToInternal(obj, writer, options, null); } }
internal static CBORObject ParseJSONValue( byte[] bytes, int index, int endPos, JSONOptions options) { var nextchar = new int[1]; var cj = new CBORJson2(bytes, index, endPos, options); CBORObject obj = cj.ParseJSON(nextchar); if (nextchar[0] != -1) { cj.RaiseError("End of bytes not reached"); } return(obj); }
internal static CBORObject ParseJSONValue( byte[] bytes, int index, int endPos, JSONOptions options, int[] nextchar) { #if DEBUG if (bytes == null) { throw new ArgumentNullException(nameof(bytes)); } if (index < 0) { throw new ArgumentException("index (" + index + ") is not greater or" + "\u0020equal to 0"); } if (index > bytes.Length) { throw new ArgumentException("index (" + index + ") is not less or" + "\u0020equal to " + bytes.Length); } if (endPos < 0) { throw new ArgumentException("endPos (" + endPos + ") is not greater" + "\u0020or equal to 0"); } if (endPos > bytes.Length) { throw new ArgumentException("endPos (" + endPos + ") is not less or" + "\u0020equal to " + bytes.Length); } if (endPos < index) { throw new ArgumentException("endPos (" + endPos + ") is not greater" + "\u0020or equal to " + index); } #endif var cj = new CBORJson2(bytes, index, endPos, options); return(cj.ParseJSON(nextchar)); }
public CBORJson2(byte[] bytes, int index, int endPos, JSONOptions options) { #if DEBUG if (bytes == null) { throw new ArgumentNullException(nameof(bytes)); } if (index < 0) { throw new ArgumentException("index (" + index + ") is not greater or" + "\u0020equal to 0"); } if (index > bytes.Length) { throw new ArgumentException("index (" + index + ") is not less or" + "\u0020equal to " + bytes.Length); } if (endPos < 0) { throw new ArgumentException("endPos (" + endPos + ") is not greater" + "\u0020or equal to 0"); } if (endPos > bytes.Length) { throw new ArgumentException("endPos (" + endPos + ") is not less or" + "\u0020equal to " + bytes.Length); } if (endPos < index) { throw new ArgumentException("endPos (" + endPos + ") is not greater" + "\u0020or equal to " + index); } #endif this.sb = null; this.bytes = bytes; this.index = index; this.endPos = endPos; this.options = options; }
public CBORJson3(string jstring, int index, int endPos, JSONOptions options) { #if DEBUG if (jstring == null) { throw new ArgumentNullException(nameof(jstring)); } if (index < 0) { throw new ArgumentException("index (" + index + ") is not greater or" + "\u0020equal to 0"); } if (index > jstring.Length) { throw new ArgumentException("index (" + index + ") is not less or" + "\u0020equal to " + jstring.Length); } if (endPos < 0) { throw new ArgumentException("endPos (" + endPos + ") is not greater" + "\u0020or equal to 0"); } if (endPos > jstring.Length) { throw new ArgumentException("endPos (" + endPos + ") is not less or" + "\u0020equal to " + jstring.Length); } if (endPos < index) { throw new ArgumentException("endPos (" + endPos + ") is not greater" + "\u0020or equal to " + index); } #endif this.sb = null; this.jstring = jstring; this.index = index; this.endPos = endPos; this.options = options; }
internal static CBORObject ParseJSONNumber( string str, int offset, int count, JSONOptions options, int[] endOfNumber) { if (String.IsNullOrEmpty(str) || count <= 0) { return(null); } if (offset < 0 || offset > str.Length) { return(null); } if (count > str.Length || str.Length - offset < count) { return(null); } JSONOptions opt = options ?? DefaultOptions; bool preserveNegativeZero = options.PreserveNegativeZero; JSONOptions.ConversionMode kind = options.NumberConversion; int endPos = offset + count; int initialOffset = offset; var negative = false; if (str[initialOffset] == '-') { ++offset; negative = true; } int numOffset = offset; var haveDecimalPoint = false; var haveDigits = false; var haveDigitsAfterDecimal = false; var haveExponent = false; int i = offset; var decimalPointPos = -1; // Check syntax int k = i; if (endPos - 1 > k && str[k] == '0' && str[k + 1] >= '0' && str[k + 1] <= '9') { if (endOfNumber != null) { endOfNumber[0] = k + 2; } return(null); } for (; k < endPos; ++k) { char c = str[k]; if (c >= '0' && c <= '9') { haveDigits = true; haveDigitsAfterDecimal |= haveDecimalPoint; } else if (c == '.') { if (!haveDigits || haveDecimalPoint) { // no digits before the decimal point, // or decimal point already seen if (endOfNumber != null) { endOfNumber[0] = k; } return(null); } haveDecimalPoint = true; decimalPointPos = k; } else if (c == 'E' || c == 'e') { ++k; haveExponent = true; break; } else { if (endOfNumber != null) { endOfNumber[0] = k; // Check if character can validly appear after a JSON number if (c != ',' && c != ']' && c != '}' && c != 0x20 && c != 0x0a && c != 0x0d && c != 0x09) { return(null); } else { endPos = k; break; } } return(null); } } if (!haveDigits || (haveDecimalPoint && !haveDigitsAfterDecimal)) { if (endOfNumber != null) { endOfNumber[0] = k; } return(null); } var exponentPos = -1; var negativeExp = false; if (haveExponent) { haveDigits = false; if (k == endPos) { if (endOfNumber != null) { endOfNumber[0] = k; } return(null); } char c = str[k]; if (c == '+') { ++k; } else if (c == '-') { negativeExp = true; ++k; } for (; k < endPos; ++k) { c = str[k]; if (c >= '0' && c <= '9') { if (exponentPos < 0 && c != '0') { exponentPos = k; } haveDigits = true; } else if (endOfNumber != null) { endOfNumber[0] = k; // Check if character can validly appear after a JSON number if (c != ',' && c != ']' && c != '}' && c != 0x20 && c != 0x0a && c != 0x0d && c != 0x09) { return(null); } else { endPos = k; break; } } else { return(null); } } if (!haveDigits) { if (endOfNumber != null) { endOfNumber[0] = k; } return(null); } } if (endOfNumber != null) { endOfNumber[0] = endPos; } if (exponentPos >= 0 && endPos - exponentPos > 20) { // Exponent too high for precision to overcome (which // has a length no bigger than Int32.MaxValue, which is 10 digits // long) if (negativeExp) { // underflow if (kind == JSONOptions.ConversionMode.Double || kind == JSONOptions.ConversionMode.IntOrFloat) { if (!negative) { return(CBORObject.FromObject((double)0.0)); } else { return(CBORObject.FromFloatingPointBits(0x8000, 2)); } } else if (kind == JSONOptions.ConversionMode.IntOrFloatFromDouble) { return(CBORObject.FromObject(0)); } } else { // overflow if (kind == JSONOptions.ConversionMode.Double || kind == JSONOptions.ConversionMode.IntOrFloatFromDouble || kind == JSONOptions.ConversionMode.IntOrFloat) { return(CBORObject.FromObject( negative ? Double.NegativeInfinity : Double.PositiveInfinity)); } else if (kind == JSONOptions.ConversionMode.Decimal128) { return(CBORObject.FromObject(negative ? EDecimal.NegativeInfinity : EDecimal.PositiveInfinity)); } } } if (!haveExponent && !haveDecimalPoint && (endPos - numOffset) <= 16) { // Very common case of all-digit JSON number strings // less than 2^53 (with or without number sign) long v = 0L; int vi = numOffset; for (; vi < endPos; ++vi) { v = (v * 10) + (int)(str[vi] - '0'); } if ((v != 0 || !negative) && v < (1L << 53) - 1) { if (negative) { v = -v; } if (kind == JSONOptions.ConversionMode.Double) { return(CBORObject.FromObject((double)v)); } else if (kind == JSONOptions.ConversionMode.Decimal128) { return(CBORObject.FromObject(EDecimal.FromInt64(v))); } else { return(CBORObject.FromObject(v)); } } } if (kind == JSONOptions.ConversionMode.Full) { if (!haveDecimalPoint && !haveExponent) { EInteger ei = EInteger.FromSubstring(str, initialOffset, endPos); if (preserveNegativeZero && ei.IsZero && negative) { // TODO: In next major version, change to EDecimal.NegativeZero return(CBORObject.FromFloatingPointBits(0x8000, 2)); } return(CBORObject.FromObject(ei)); } if (!haveExponent && haveDecimalPoint && (endPos - numOffset) <= 19) { // No more than 18 digits plus one decimal point (which // should fit a long) long lv = 0L; int expo = -(endPos - (decimalPointPos + 1)); int vi = numOffset; for (; vi < decimalPointPos; ++vi) { lv = checked ((lv * 10) + (int)(str[vi] - '0')); } for (vi = decimalPointPos + 1; vi < endPos; ++vi) { lv = checked ((lv * 10) + (int)(str[vi] - '0')); } if (negative) { lv = -lv; } if (!negative || lv != 0) { if (expo == 0) { return(CBORObject.FromObject(lv)); } else { CBORObject cbor = CBORObject.FromObject( new CBORObject[] { CBORObject.FromObject(expo), CBORObject.FromObject(lv), }); return(cbor.WithTag(4)); } } } EDecimal ed = EDecimal.FromString( str, initialOffset, endPos - initialOffset); if (ed.IsZero && negative) { if (ed.Exponent.IsZero) { // TODO: In next major version, use EDecimal // for preserveNegativeZero return(preserveNegativeZero ? CBORObject.FromFloatingPointBits(0x8000, 2) : CBORObject.FromObject(0)); } else if (!preserveNegativeZero) { return(CBORObject.FromObject(ed.Negate())); } else { return(CBORObject.FromObject(ed)); } } else { return(ed.Exponent.IsZero ? CBORObject.FromObject(ed.Mantissa) : CBORObject.FromObject(ed)); } } else if (kind == JSONOptions.ConversionMode.Double) { // TODO: Avoid converting to double double dbl = EFloat.FromString( str, initialOffset, endPos - initialOffset, EContext.Binary64).ToDouble(); if (!preserveNegativeZero && dbl == 0.0) { dbl = 0.0; } return(CBORObject.FromObject(dbl)); } else if (kind == JSONOptions.ConversionMode.Decimal128) { EDecimal ed = EDecimal.FromString( str, initialOffset, endPos - initialOffset, EContext.Decimal128); if (!preserveNegativeZero && ed.IsNegative && ed.IsZero) { ed = ed.Negate(); } return(CBORObject.FromObject(ed)); } else if (kind == JSONOptions.ConversionMode.IntOrFloatFromDouble) { // TODO: Avoid converting to double double dbl = EFloat.FromString( str, initialOffset, endPos - initialOffset, EContext.Binary64).ToDouble(); if (!Double.IsNaN(dbl) && dbl >= -9007199254740991.0 && dbl <= 9007199254740991.0 && Math.Floor(dbl) == dbl) { var idbl = (long)dbl; return(CBORObject.FromObject(idbl)); } return(CBORObject.FromObject(dbl)); } else if (kind == JSONOptions.ConversionMode.IntOrFloat) { EContext ctx = EContext.Binary64.WithBlankFlags(); // TODO: Avoid converting to double double dbl = EFloat.FromString( str, initialOffset, endPos - initialOffset, ctx).ToDouble(); if ((ctx.Flags & EContext.FlagInexact) != 0) { // Inexact conversion to double, meaning that the string doesn't // represent an integer in [-(2^53)+1, 2^53), which is representable // exactly as double, so treat as ConversionMode.Double if (!preserveNegativeZero && dbl == 0.0) { dbl = 0.0; } return(CBORObject.FromObject(dbl)); } else { // Exact conversion; treat as ConversionMode.IntToFloatFromDouble if (!Double.IsNaN(dbl) && dbl >= -9007199254740991.0 && dbl <= 9007199254740991.0 && Math.Floor(dbl) == dbl) { var idbl = (long)dbl; return(CBORObject.FromObject(idbl)); } return(CBORObject.FromObject(dbl)); } } else { throw new ArgumentException("Unsupported conversion kind."); } }
internal static CBORObject[] ParseJSONSequence( CharacterInputWithCount reader, JSONOptions options, int[] nextChar) { var cj = new CBORJson(reader, options); cj.SetJSONSequenceMode(); bool seenSeparator = cj.SkipRecordSeparators(nextChar, false); if (nextChar[0] >= 0 && !seenSeparator) { // Stream is not empty and did not begin with // record separator cj.RaiseError("Not a JSON text sequence"); } else if (nextChar[0] < 0 && !seenSeparator) { // Stream is empty return(new CBORObject[0]); } else if (nextChar[0] < 0) { // Stream had only record separators, so we found // a truncated JSON text return(new CBORObject[] { null }); } var list = new List <CBORObject>(); while (true) { CBORObject co; try { co = cj.ParseJSON(nextChar); } catch (CBORException) { cj.SkipToEnd(); co = null; } if (co != null && nextChar[0] >= 0) { // End of JSON text not reached cj.SkipToEnd(); co = null; } list.Add(co); if (!cj.recordSeparatorSeen) { // End of the stream was reached nextChar[0] = -1; break; } else { // A record separator was seen, so // another JSON text follows cj.ResetJSONSequenceMode(); cj.SkipRecordSeparators(nextChar, true); if (nextChar[0] < 0) { // Rest of stream had only record separators, so we found // a truncated JSON text list.Add(null); break; } } } return((CBORObject[])list.ToArray()); }
internal static void WriteJSONToInternal( CBORObject obj, StringOutput writer, JSONOptions options, IList <CBORObject> stack) { if (obj.IsNumber) { writer.WriteString(CBORNumber.FromCBORObject(obj).ToJSONString()); return; } switch (obj.Type) { case CBORType.Integer: case CBORType.FloatingPoint: { CBORObject untaggedObj = obj.Untag(); writer.WriteString( CBORNumber.FromCBORObject(untaggedObj).ToJSONString()); break; } case CBORType.Boolean: { if (obj.IsTrue) { writer.WriteString("true"); return; } if (obj.IsFalse) { writer.WriteString("false"); return; } return; } case CBORType.SimpleValue: { writer.WriteString("null"); return; } case CBORType.ByteString: { byte[] byteArray = obj.GetByteString(); if (byteArray.Length == 0) { writer.WriteString("\"\""); return; } writer.WriteCodePoint((int)'\"'); if (obj.HasTag(22)) { // Base64 with padding Base64.WriteBase64( writer, byteArray, 0, byteArray.Length, true); } else if (obj.HasTag(23)) { // Write as base16 for (int i = 0; i < byteArray.Length; ++i) { writer.WriteCodePoint((int)Hex16[(byteArray[i] >> 4) & 15]); writer.WriteCodePoint((int)Hex16[byteArray[i] & 15]); } } else { // Base64url no padding Base64.WriteBase64URL( writer, byteArray, 0, byteArray.Length, false); } writer.WriteCodePoint((int)'\"'); break; } case CBORType.TextString: { string thisString = obj.AsString(); if (thisString.Length == 0) { writer.WriteString("\"\""); return; } writer.WriteCodePoint((int)'\"'); WriteJSONStringUnquoted(thisString, writer, options); writer.WriteCodePoint((int)'\"'); break; } case CBORType.Array: { writer.WriteCodePoint((int)'['); for (var i = 0; i < obj.Count; ++i) { if (i > 0) { writer.WriteCodePoint((int)','); } bool pop = CheckCircularRef(stack, obj, obj[i]); WriteJSONToInternal(obj[i], writer, options, stack); PopRefIfNeeded(stack, pop); } writer.WriteCodePoint((int)']'); break; } case CBORType.Map: { var first = true; var hasNonStringKeys = false; ICollection <KeyValuePair <CBORObject, CBORObject> > entries = obj.Entries; foreach (KeyValuePair <CBORObject, CBORObject> entry in entries) { CBORObject key = entry.Key; if (key.Type != CBORType.TextString || key.IsTagged) { // treat a non-text-string item or a tagged item // as having non-string keys hasNonStringKeys = true; break; } } if (!hasNonStringKeys) { writer.WriteCodePoint((int)'{'); foreach (KeyValuePair <CBORObject, CBORObject> entry in entries) { CBORObject key = entry.Key; CBORObject value = entry.Value; if (!first) { writer.WriteCodePoint((int)','); } writer.WriteCodePoint((int)'\"'); WriteJSONStringUnquoted(key.AsString(), writer, options); writer.WriteCodePoint((int)'\"'); writer.WriteCodePoint((int)':'); bool pop = CheckCircularRef(stack, obj, value); WriteJSONToInternal(value, writer, options, stack); PopRefIfNeeded(stack, pop); first = false; } writer.WriteCodePoint((int)'}'); } else { // This map has non-string keys IDictionary <string, CBORObject> stringMap = new Dictionary <string, CBORObject>(); // Copy to a map with String keys, since // some keys could be duplicates // when serialized to strings foreach (KeyValuePair <CBORObject, CBORObject> entry in entries) { CBORObject key = entry.Key; CBORObject value = entry.Value; string str = null; switch (key.Type) { case CBORType.TextString: str = key.AsString(); break; case CBORType.Array: case CBORType.Map: { var sb = new StringBuilder(); var sw = new StringOutput(sb); bool pop = CheckCircularRef(stack, obj, key); WriteJSONToInternal(key, sw, options, stack); PopRefIfNeeded(stack, pop); str = sb.ToString(); break; } default: str = key.ToJSONString(options); break; } if (stringMap.ContainsKey(str)) { throw new CBORException( "Duplicate JSON string equivalents of map" + "\u0020keys"); } stringMap[str] = value; } first = true; writer.WriteCodePoint((int)'{'); foreach (KeyValuePair <string, CBORObject> entry in stringMap) { string key = entry.Key; CBORObject value = entry.Value; if (!first) { writer.WriteCodePoint((int)','); } writer.WriteCodePoint((int)'\"'); WriteJSONStringUnquoted((string)key, writer, options); writer.WriteCodePoint((int)'\"'); writer.WriteCodePoint((int)':'); bool pop = CheckCircularRef(stack, obj, value); WriteJSONToInternal(value, writer, options, stack); PopRefIfNeeded(stack, pop); first = false; } writer.WriteCodePoint((int)'}'); } break; } default: throw new InvalidOperationException("Unexpected item" + "\u0020type"); } }
internal static void WriteJSONStringUnquoted( string str, StringOutput sb, JSONOptions options) { var first = true; for (var i = 0; i < str.Length; ++i) { char c = str[i]; if (c == '\\' || c == '"') { if (first) { first = false; sb.WriteString(str, 0, i); } sb.WriteCodePoint((int)'\\'); sb.WriteCodePoint((int)c); } else if (c < 0x20 || (c >= 0x7f && (c == 0x2028 || c == 0x2029 || (c >= 0x7f && c <= 0xa0) || c == 0xfeff || c == 0xfffe || c == 0xffff))) { // Control characters, and also the line and paragraph separators // which apparently can't appear in JavaScript (as opposed to // JSON) strings if (first) { first = false; sb.WriteString(str, 0, i); } if (c == 0x0d) { sb.WriteString("\\r"); } else if (c == 0x0a) { sb.WriteString("\\n"); } else if (c == 0x08) { sb.WriteString("\\b"); } else if (c == 0x0c) { sb.WriteString("\\f"); } else if (c == 0x09) { sb.WriteString("\\t"); } else if (c == 0x85) { sb.WriteString("\\u0085"); } else if (c >= 0x100) { sb.WriteString("\\u"); sb.WriteCodePoint((int)Hex16[(int)((c >> 12) & 15)]); sb.WriteCodePoint((int)Hex16[(int)((c >> 8) & 15)]); sb.WriteCodePoint((int)Hex16[(int)((c >> 4) & 15)]); sb.WriteCodePoint((int)Hex16[(int)(c & 15)]); } else { sb.WriteString("\\u00"); sb.WriteCodePoint((int)Hex16[(int)(c >> 4)]); sb.WriteCodePoint((int)Hex16[(int)(c & 15)]); } } else { if ((c & 0xfc00) == 0xd800) { if (i >= str.Length - 1 || (str[i + 1] & 0xfc00) != 0xdc00) { // NOTE: RFC 8259 doesn't prohibit any particular // error-handling behavior when a writer of JSON // receives a string with an unpaired surrogate. if (options.ReplaceSurrogates) { if (first) { first = false; sb.WriteString(str, 0, i); } // Replace unpaired surrogate with U+FFFD c = (char)0xfffd; } else { throw new CBORException("Unpaired surrogate in string"); } } } if (!first) { if ((c & 0xfc00) == 0xd800) { sb.WriteString(str, i, 2); ++i; } else { sb.WriteCodePoint((int)c); } } } } if (first) { sb.WriteString(str); } }