// String preparation, as specified in RFC4013 /// <summary> /// This method will iterate all charactesr in given <see cref="String"/> and return index of first escapable character, as per <see href="https://tools.ietf.org/html/rfc4013">RFC-4013</see> spec. /// Will return <c>-1</c> if all characters are valid. /// </summary> /// <param name="str">The string to process.</param> /// <param name="normalizer">The custom normalizer callback</param> /// <returns>Index of first escapable or transformable character.</returns> /// <exception cref="ArgumentException">If <paramref name="str"/> contains any invalid character, as per <see href="https://tools.ietf.org/html/rfc3454">RFC-3454</see> spec.</exception> public static Int32 CheckString( String str, CustomStringNormalizer normalizer = null ) { var retVal = -1; for (var i = 0; i < str.Length && retVal == -1; ++i) { var c = str[i]; if (c == 0) { throw new ArgumentException("Null characters are not allowed.", nameof(str)); } else if (Char.IsControl(c)) { // Control characters not allowed throw new ArgumentException("Control characters are not allowed.", nameof(str)); } else if (c >= MIN_SPECIAL_CHAR) { // It is possible that we need to replace character if (IsNonAsciiSpace(c)) { // Mapping special space into "normal" space (0x20) retVal = i; } else if (IsCommonlyMappedToNothing(c)) { // Create builder but don't append anything, as this character is 'nothing' retVal = i; } else if (IsProhibited(str, i)) { // Illegal character throw new ArgumentException($"Prohibited character at index {i}", nameof(str)); } else if (normalizer?.Invoke(str, i) != null) { retVal = i; } } else if (normalizer?.Invoke(str, i) != null) { retVal = i; } } return(retVal); }
/// <summary> /// This method will write SASL string to given byte array using given <see cref="IEncodingInfo"/>, while escaping and transforming string as needed. /// See <see cref="CheckString"/> for more information about escaping and transforming. /// </summary> /// <param name="str">The string to write.</param> /// <param name="encodingInfo">The <see cref="IEncodingInfo"/> to use.</param> /// <param name="array">The byte array where to write the string.</param> /// <param name="offset">The offset in <paramref name="array"/> where to start writing.</param> /// <param name="normalizer">The optional custom normalizer callback to use.</param> /// <param name="checkResult">The result of <see cref="CheckString"/>, if it was used before calling this method.</param> /// <seealso cref="CheckString"/> /// <remarks> /// This method will not allocate any strings. /// </remarks> public static void WriteString( String str, IEncodingInfo encodingInfo, ResizableArray <Byte> array, ref Int32 offset, CustomStringNormalizer normalizer = null, Int32?checkResult = null ) { var checkResultValue = checkResult ?? CheckString(str, normalizer); var encoding = encodingInfo.Encoding; if (checkResultValue == -1) { // String may be written as-is array.CurrentMaxCapacity = offset + encoding.GetByteCount(str); encodingInfo.WriteString(array.Array, ref offset, str); } else { var maxCharByteCountDiff = encodingInfo.MaxCharByteCount - 1; String tmpStr; // Calculate byte size (actually, we are calculating the upper bound for byte size, but that's ok) var byteSize = encoding.GetByteCount(str); for (var i = checkResultValue; i < str.Length; ++i) { var c = str[i]; if (c >= MIN_SPECIAL_CHAR) { // It is possible that we need to replace character if (IsNonAsciiSpace(c)) { // Mapping special space into "normal" space (0x20) // Byte size doesn't grow at least. } else if (IsCommonlyMappedToNothing(c)) { --byteSize; } else if ((tmpStr = normalizer?.Invoke(str, i)) != null) { byteSize += encoding.GetByteCount(tmpStr) - 1; } } } // Now we know that the string will take max 'byteSize' amount of bytes array.CurrentMaxCapacity = offset + byteSize; // We can get the actual array now, since we won't be resizing the ResizableArray anymore var buffer = array.Array; // Helper method to write chunks of "normal" characters var prev = 0; void WritePreviousChunk(Int32 charIdx, ref Int32 writeIdx) { writeIdx += encoding.GetBytes(str, prev, charIdx - prev, buffer, writeIdx); prev = charIdx + 1; } for (var i = checkResultValue; i < str.Length; ++i) { var c = str[i]; if (c >= MIN_SPECIAL_CHAR) { // It is possible that we need to replace character if (IsNonAsciiSpace(c)) { // Mapping special space into "normal" space (0x20) WritePreviousChunk(i, ref offset); encodingInfo.WriteASCIIByte(buffer, ref offset, 0x20); } else if (IsCommonlyMappedToNothing(c)) { // Nothing additional to write WritePreviousChunk(i, ref offset); } else if ((tmpStr = normalizer?.Invoke(str, i)) != null) { WritePreviousChunk(i, ref offset); if (tmpStr.Length > 0) { encodingInfo.WriteString(buffer, ref offset, tmpStr); } } } } if (prev < str.Length) { // Write final chunk WritePreviousChunk(str.Length - 1, ref offset); } } }