From bba526981caaa36f76cb91b332cdc2985af5bc5a Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 10 Aug 2020 13:56:01 -0500 Subject: [PATCH 01/14] wip --- sdk/core/Azure.Core/src/MultipartContent.cs | 374 ++++++++++++++++++ .../src/MultipartFormDataContent.cs | 297 +------------- sdk/core/Azure.Core/src/RequestContent.cs | 6 + .../Azure.Core/src/RequestContentContent.cs | 202 ++++++++++ .../tests/HttpPipelineFunctionalTests.cs | 47 ++- 5 files changed, 634 insertions(+), 292 deletions(-) create mode 100644 sdk/core/Azure.Core/src/MultipartContent.cs create mode 100644 sdk/core/Azure.Core/src/RequestContentContent.cs diff --git a/sdk/core/Azure.Core/src/MultipartContent.cs b/sdk/core/Azure.Core/src/MultipartContent.cs new file mode 100644 index 0000000000000..ddd8bd5edba74 --- /dev/null +++ b/sdk/core/Azure.Core/src/MultipartContent.cs @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Core +{ + /// + /// Provides a container for content encoded using multipart/form-data MIME type. + /// + internal class MultipartContent : RequestContent + { + #region Fields + + private const string CrLf = "\r\n"; + private const string ColonSP = ": "; + + private static readonly int s_crlfLength = GetEncodedLength(CrLf); + private static readonly int s_dashDashLength = GetEncodedLength("--"); + private static readonly int s_colonSpaceLength = GetEncodedLength(ColonSP); + + private readonly List _nestedContent; + private readonly string _subtype; + private readonly string _boundary; + + #endregion Fields + + #region Construction + + public MultipartContent() + : this("mixed", GetDefaultBoundary()) + { } + + public MultipartContent(string subtype) + : this(subtype, GetDefaultBoundary()) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The multipart sub type. + /// The boundary string for the multipart form data content. + public MultipartContent(string subtype, string boundary) + { + ValidateBoundary(boundary); + _subtype = subtype; + _boundary = boundary; + Headers = new Dictionary + { + [HttpHeader.Names.ContentType] = $"multipart/{_subtype};boundary=\"{_boundary}\"" + }; + + _nestedContent = new List(); + } + + private static void ValidateBoundary(string boundary) + { + // NameValueHeaderValue is too restrictive for boundary. + // Instead validate it ourselves and then quote it. + Argument.AssertNotNullOrWhiteSpace(boundary, nameof(boundary)); + + // RFC 2046 Section 5.1.1 + // boundary := 0*69 bcharsnospace + // bchars := bcharsnospace / " " + // bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" / "+" / "_" / "," / "-" / "." / "/" / ":" / "=" / "?" + if (boundary.Length > 70) + { + throw new ArgumentOutOfRangeException(nameof(boundary), boundary, $"The field cannot be longer than {70} characters."); + } + // Cannot end with space. + if (boundary.EndsWith(" ", StringComparison.InvariantCultureIgnoreCase)) + { + throw new ArgumentException($"The format of value '{boundary}' is invalid.", nameof(boundary)); + } + + const string AllowedMarks = @"'()+_,-./:=? "; + + foreach (char ch in boundary) + { + if (('0' <= ch && ch <= '9') || // Digit. + ('a' <= ch && ch <= 'z') || // alpha. + ('A' <= ch && ch <= 'Z') || // ALPHA. + AllowedMarks.Contains(char.ToString(ch))) // Marks. + { + // Valid. + } + else + { + throw new ArgumentException($"The format of value '{boundary}' is invalid.", nameof(boundary)); + } + } + } + + private static string GetDefaultBoundary() + { + return Guid.NewGuid().ToString(); + } + + /// + /// Add content type header to the request. + /// + /// The request. + public void ApplyToRequest(Request request) + { + request.Headers.Add(HttpHeader.Names.ContentType, $"multipart/{_subtype};boundary=\"{_boundary}\""); + } + + /// + /// Add HTTP content to a collection of RequestContent objects that + /// get serialized to multipart/form-data MIME type. + /// + /// The Request content to add to the collection. + public virtual void Add(RequestContent content) + { + Argument.AssertNotNull(content, nameof(content)); + AddInternal(content, null); + } + + /// + /// Add HTTP content to a collection of RequestContent objects that + /// get serialized to multipart/form-data MIME type. + /// + /// The Request content to add to the collection. + /// The headers to add to the collection. + public virtual void Add(RequestContent content, Dictionary headers) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(headers, nameof(headers)); + + AddInternal(content, headers); + } + + private void AddInternal(RequestContent content, Dictionary? headers) + { + if (headers == null) + { + headers = new Dictionary(); + } + foreach (var key in content.Headers.Keys) + { + if (!headers.ContainsKey(key)) + { + headers[key] = content.Headers[key]; + } + } + _nestedContent.Add(new MultipartRequestContent(content, headers)); + } + + #endregion Construction + + #region Dispose + + /// + /// Frees resources held by the object. + /// + public override void Dispose() + { + foreach (MultipartRequestContent content in _nestedContent) + { + content.RequestContent.Dispose(); + } + _nestedContent.Clear(); + } + + #endregion Dispose + + #region Serialization + + // for-each content + // write "--" + boundary + // for-each content header + // write header: header-value + // write content.WriteTo[Async] + // write "--" + boundary + "--" + // Can't be canceled directly by the user. If the overall request is canceled + // then the stream will be closed an exception thrown. + /// + /// + /// + /// + /// + /// + public override void WriteTo(Stream stream, CancellationToken cancellationToken) + { + Argument.AssertNotNull(stream, nameof(stream)); + + try + { + // Write start boundary. + EncodeStringToStream(stream, "--" + _boundary + CrLf); + + // Write each nested content. + var output = new StringBuilder(); + for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) + { + // Write divider, headers, and content. + RequestContent content = _nestedContent[contentIndex].RequestContent; + Dictionary headers = _nestedContent[contentIndex].Headers; + EncodeStringToStream(stream, SerializeHeadersToString(output, contentIndex, headers)); + content.WriteTo(stream, cancellationToken); + } + + // Write footer boundary. + EncodeStringToStream(stream, CrLf + "--" + _boundary + "--" + CrLf); + } + catch (Exception) + { + throw; + } + } + + // for-each content + // write "--" + boundary + // for-each content header + // write header: header-value + // write content.WriteTo[Async] + // write "--" + boundary + "--" + // Can't be canceled directly by the user. If the overall request is canceled + // then the stream will be closed an exception thrown. + /// + /// + /// + /// + /// + /// + public override Task WriteToAsync(Stream stream, CancellationToken cancellation) => + SerializeToStreamAsync(stream, cancellation); + + private async Task SerializeToStreamAsync(Stream stream, CancellationToken cancellationToken) + { + Argument.AssertNotNull(stream, nameof(stream)); + try + { + // Write start boundary. + await EncodeStringToStreamAsync(stream, "--" + _boundary + CrLf, cancellationToken).ConfigureAwait(false); + + // Write each nested content. + var output = new StringBuilder(); + for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) + { + // Write divider, headers, and content. + RequestContent content = _nestedContent[contentIndex].RequestContent; + Dictionary headers = _nestedContent[contentIndex].Headers; + await EncodeStringToStreamAsync(stream, SerializeHeadersToString(output, contentIndex, headers), cancellationToken).ConfigureAwait(false); + await content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); + } + + // Write footer boundary. + await EncodeStringToStreamAsync(stream, CrLf + "--" + _boundary + "--" + CrLf, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + throw; + } + } + + private string SerializeHeadersToString(StringBuilder scratch, int contentIndex, Dictionary headers) + { + scratch.Clear(); + + // Add divider. + if (contentIndex != 0) // Write divider for all but the first content. + { + scratch.Append(CrLf + "--"); // const strings + scratch.Append(_boundary); + scratch.Append(CrLf); + } + + // Add headers. + foreach (KeyValuePair header in headers) + { + scratch.Append(header.Key); + scratch.Append(": "); + scratch.Append(header.Value); + scratch.Append(CrLf); + } + + // Extra CRLF to end headers (even if there are no headers). + scratch.Append(CrLf); + + return scratch.ToString(); + } + + private static void EncodeStringToStream(Stream stream, string input) + { + byte[] buffer = Encoding.Default.GetBytes(input); + stream.Write(buffer, 0, buffer.Length); + } + + private static Task EncodeStringToStreamAsync(Stream stream, string input, CancellationToken cancellationToken) + { + byte[] buffer = Encoding.Default.GetBytes(input); + return stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + + /// + /// Attempts to compute the length of the underlying content, if available. + /// + /// The length of the underlying data. + public override bool TryComputeLength(out long length) + { + int boundaryLength = GetEncodedLength(_boundary); + + long currentLength = 0; + long internalBoundaryLength = s_crlfLength + s_dashDashLength + boundaryLength + s_crlfLength; + + // Start Boundary. + currentLength += s_dashDashLength + boundaryLength + s_crlfLength; + + bool first = true; + foreach (MultipartRequestContent content in _nestedContent) + { + if (first) + { + first = false; // First boundary already written. + } + else + { + // Internal Boundary. + currentLength += internalBoundaryLength; + } + + // Headers. + foreach (KeyValuePair headerPair in content.Headers) + { + currentLength += GetEncodedLength(headerPair.Key) + s_colonSpaceLength; + currentLength += GetEncodedLength(headerPair.Value); + currentLength += s_crlfLength; + } + + currentLength += s_crlfLength; + + // Content. + if (!content.RequestContent.TryComputeLength(out long tempContentLength)) + { + length = 0; + return false; + } + currentLength += tempContentLength; + } + + // Terminating boundary. + currentLength += s_crlfLength + s_dashDashLength + boundaryLength + s_dashDashLength + s_crlfLength; + + length = currentLength; + return true; + } + + private static int GetEncodedLength(string input) + { + return Encoding.Default.GetByteCount(input); + } + + #endregion Serialization + + private class MultipartRequestContent + { + public readonly RequestContent RequestContent; + public Dictionary Headers; + + public MultipartRequestContent(RequestContent content, Dictionary headers) + { + RequestContent = content; + Headers = headers; + } + } + } +} diff --git a/sdk/core/Azure.Core/src/MultipartFormDataContent.cs b/sdk/core/Azure.Core/src/MultipartFormDataContent.cs index e83bfc2d567bb..c89008d8d7732 100644 --- a/sdk/core/Azure.Core/src/MultipartFormDataContent.cs +++ b/sdk/core/Azure.Core/src/MultipartFormDataContent.cs @@ -13,20 +13,12 @@ namespace Azure.Core /// /// Provides a container for content encoded using multipart/form-data MIME type. /// - internal class MultipartFormDataContent : RequestContent + internal class MultipartFormDataContent : MultipartContent { #region Fields - private const string CrLf = "\r\n"; private const string FormData = "form-data"; - private static readonly int s_crlfLength = GetEncodedLength(CrLf); - private static readonly int s_dashDashLength = GetEncodedLength("--"); - private static readonly int s_colonSpaceLength = GetEncodedLength(": "); - - private readonly List _nestedContent; - private readonly string _boundary; - #endregion Fields #region Construction @@ -34,78 +26,24 @@ internal class MultipartFormDataContent : RequestContent /// /// Initializes a new instance of the class. /// - public MultipartFormDataContent() : this(GetDefaultBoundary()) + public MultipartFormDataContent() : base(FormData) { } /// /// Initializes a new instance of the class. /// /// The boundary string for the multipart form data content. - public MultipartFormDataContent(string boundary) - { - ValidateBoundary(boundary); - _boundary = boundary; - _nestedContent = new List(); - } - - private static void ValidateBoundary(string boundary) - { - // NameValueHeaderValue is too restrictive for boundary. - // Instead validate it ourselves and then quote it. - Argument.AssertNotNullOrWhiteSpace(boundary, nameof(boundary)); - - // RFC 2046 Section 5.1.1 - // boundary := 0*69 bcharsnospace - // bchars := bcharsnospace / " " - // bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" / "+" / "_" / "," / "-" / "." / "/" / ":" / "=" / "?" - if (boundary.Length > 70) - { - throw new ArgumentOutOfRangeException(nameof(boundary), boundary, $"The field cannot be longer than {70} characters."); - } - // Cannot end with space. - if (boundary.EndsWith(" ", StringComparison.InvariantCultureIgnoreCase)) - { - throw new ArgumentException($"The format of value '{boundary}' is invalid.", nameof(boundary)); - } - - const string AllowedMarks = @"'()+_,-./:=? "; - - foreach (char ch in boundary) - { - if (('0' <= ch && ch <= '9') || // Digit. - ('a' <= ch && ch <= 'z') || // alpha. - ('A' <= ch && ch <= 'Z') || // ALPHA. - AllowedMarks.Contains(char.ToString(ch))) // Marks. - { - // Valid. - } - else - { - throw new ArgumentException($"The format of value '{boundary}' is invalid.", nameof(boundary)); - } - } - } - - private static string GetDefaultBoundary() - { - return Guid.NewGuid().ToString(); - } + public MultipartFormDataContent(string boundary) : base(FormData, boundary) + { } - /// - /// Add content type header to the request. - /// - /// The request. - public void ApplyToRequest(Request request) - { - request.Headers.Add("Content-Type", $"multipart/form-data;boundary=\"{_boundary}\""); - } + #endregion Construction /// /// Add HTTP content to a collection of RequestContent objects that /// get serialized to multipart/form-data MIME type. /// /// The Request content to add to the collection. - public void Add(RequestContent content) + public override void Add(RequestContent content) { Argument.AssertNotNull(content, nameof(content)); AddInternal(content, null, null, null); @@ -117,7 +55,7 @@ public void Add(RequestContent content) /// /// The Request content to add to the collection. /// The headers to add to the collection. - public void Add(RequestContent content, Dictionary headers) + public override void Add(RequestContent content, Dictionary headers) { Argument.AssertNotNull(content, nameof(content)); Argument.AssertNotNull(headers, nameof(headers)); @@ -180,227 +118,8 @@ private void AddInternal(RequestContent content, Dictionary? hea headers.Add("Content-Disposition", value); } - _nestedContent.Add(new MultipartRequestContent(content, headers)); - } - - #endregion Construction - - #region Dispose - - /// - /// Frees resources held by the object. - /// - public override void Dispose() - { - foreach (MultipartRequestContent content in _nestedContent) - { - content.RequestContent.Dispose(); - } - _nestedContent.Clear(); - - } - - #endregion Dispose - - #region Serialization - - // for-each content - // write "--" + boundary - // for-each content header - // write header: header-value - // write content.WriteTo[Async] - // write "--" + boundary + "--" - // Can't be canceled directly by the user. If the overall request is canceled - // then the stream will be closed an exception thrown. - /// - /// - /// - /// - /// - /// - public override void WriteTo(Stream stream, CancellationToken cancellationToken) - { - Argument.AssertNotNull(stream, nameof(stream)); - - try - { - // Write start boundary. - EncodeStringToStream(stream, "--" + _boundary + CrLf); - - // Write each nested content. - var output = new StringBuilder(); - for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) - { - // Write divider, headers, and content. - RequestContent content = _nestedContent[contentIndex].RequestContent; - Dictionary headers = _nestedContent[contentIndex].Headers; - EncodeStringToStream(stream, SerializeHeadersToString(output, contentIndex, headers)); - content.WriteTo(stream, cancellationToken); - } - - // Write footer boundary. - EncodeStringToStream(stream, CrLf + "--" + _boundary + "--" + CrLf); - } - catch (Exception) - { - throw; - } + base.Add(content, headers); } - // for-each content - // write "--" + boundary - // for-each content header - // write header: header-value - // write content.WriteTo[Async] - // write "--" + boundary + "--" - // Can't be canceled directly by the user. If the overall request is canceled - // then the stream will be closed an exception thrown. - /// - /// - /// - /// - /// - /// - public override Task WriteToAsync(Stream stream, CancellationToken cancellation) => - SerializeToStreamAsync(stream, cancellation); - - private async Task SerializeToStreamAsync(Stream stream, CancellationToken cancellationToken) - { - Argument.AssertNotNull(stream, nameof(stream)); - try - { - // Write start boundary. - await EncodeStringToStreamAsync(stream, "--" + _boundary + CrLf, cancellationToken).ConfigureAwait(false); - - // Write each nested content. - var output = new StringBuilder(); - for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) - { - // Write divider, headers, and content. - RequestContent content = _nestedContent[contentIndex].RequestContent; - Dictionary headers = _nestedContent[contentIndex].Headers; - await EncodeStringToStreamAsync(stream, SerializeHeadersToString(output, contentIndex, headers), cancellationToken).ConfigureAwait(false); - await content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); - } - - // Write footer boundary. - await EncodeStringToStreamAsync(stream, CrLf + "--" + _boundary + "--" + CrLf, cancellationToken).ConfigureAwait(false); - } - catch (Exception) - { - throw; - } - } - - private string SerializeHeadersToString(StringBuilder scratch, int contentIndex, Dictionary headers) - { - scratch.Clear(); - - // Add divider. - if (contentIndex != 0) // Write divider for all but the first content. - { - scratch.Append(CrLf + "--"); // const strings - scratch.Append(_boundary); - scratch.Append(CrLf); - } - - // Add headers. - foreach (KeyValuePair header in headers) - { - scratch.Append(header.Key); - scratch.Append(": "); - scratch.Append(header.Value); - scratch.Append(CrLf); - } - - // Extra CRLF to end headers (even if there are no headers). - scratch.Append(CrLf); - - return scratch.ToString(); - } - - private static void EncodeStringToStream(Stream stream, string input) - { - byte[] buffer = Encoding.Default.GetBytes(input); - stream.Write(buffer, 0, buffer.Length); - } - - private static Task EncodeStringToStreamAsync(Stream stream, string input, CancellationToken cancellationToken) - { - byte[] buffer = Encoding.Default.GetBytes(input); - return stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); - } - - /// - /// Attempts to compute the length of the underlying content, if available. - /// - /// The length of the underlying data. - public override bool TryComputeLength(out long length) - { - int boundaryLength = GetEncodedLength(_boundary); - - long currentLength = 0; - long internalBoundaryLength = s_crlfLength + s_dashDashLength + boundaryLength + s_crlfLength; - - // Start Boundary. - currentLength += s_dashDashLength + boundaryLength + s_crlfLength; - - bool first = true; - foreach (MultipartRequestContent content in _nestedContent) - { - if (first) - { - first = false; // First boundary already written. - } - else - { - // Internal Boundary. - currentLength += internalBoundaryLength; - } - - // Headers. - foreach (KeyValuePair headerPair in content.Headers) - { - currentLength += GetEncodedLength(headerPair.Key) + s_colonSpaceLength; - currentLength += GetEncodedLength(headerPair.Value); - currentLength += s_crlfLength; - } - - currentLength += s_crlfLength; - - // Content. - if (!content.RequestContent.TryComputeLength(out long tempContentLength)) - { - length = 0; - return false; - } - currentLength += tempContentLength; - } - - // Terminating boundary. - currentLength += s_crlfLength + s_dashDashLength + boundaryLength + s_dashDashLength + s_crlfLength; - - length = currentLength; - return true; - } - - private static int GetEncodedLength(string input) - { - return Encoding.Default.GetByteCount(input); - } - - #endregion Serialization - - private class MultipartRequestContent - { - public readonly RequestContent RequestContent; - public Dictionary Headers; - - public MultipartRequestContent(RequestContent content, Dictionary headers) - { - RequestContent = content; - Headers = headers; - } - } } } diff --git a/sdk/core/Azure.Core/src/RequestContent.cs b/sdk/core/Azure.Core/src/RequestContent.cs index d30d4091defaf..a4d81f87d6065 100644 --- a/sdk/core/Azure.Core/src/RequestContent.cs +++ b/sdk/core/Azure.Core/src/RequestContent.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Threading; using System.Buffers; +using System.Collections.Generic; namespace Azure.Core { @@ -77,6 +78,11 @@ public abstract class RequestContent : IDisposable /// public abstract void Dispose(); + /// + /// A collection of header values associated with this request content. + /// + internal virtual Dictionary? Headers { get; set; } + private sealed class StreamContent : RequestContent { private const int CopyToBufferSize = 81920; diff --git a/sdk/core/Azure.Core/src/RequestContentContent.cs b/sdk/core/Azure.Core/src/RequestContentContent.cs new file mode 100644 index 0000000000000..b6ba94d28cdab --- /dev/null +++ b/sdk/core/Azure.Core/src/RequestContentContent.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Core +{ + /// + /// Provides a container for content encoded using multipart/form-data MIME type. + /// + internal class RequestContentContent : RequestContent + { + #region Fields + + private const string SP = " "; + private const string ColonSP = ": "; + private const string CRLF = "\r\n"; + private const int DefaultHeaderAllocation = 2 * 1024; + private const string DefaultMediaType = "application/http"; + + private readonly Request request; + + #endregion Fields + + #region Construction + + /// + /// Initializes a new instance of the class. + /// + /// The instance to encapsulate. + public RequestContentContent(Request request) : this(request, default) + { } + + + /// + /// Initializes a new instance of the class. + /// + /// The instance to encapsulate. + /// Additional headers to apply to the request content. + public RequestContentContent(Request request, Dictionary? headers) + { + Argument.AssertNotNull(request, nameof(request)); + + Headers = new Dictionary + { + ["Content-Type"] = DefaultMediaType + }; + + if (headers != null) + { + foreach (var key in headers.Keys) + { + if (!Headers.ContainsKey(key)) + { + Headers[key] = headers[key]; + } + } + } + + this.request = request; + } + + #endregion Construction + + #region Dispose + + /// + /// Frees resources held by the object. + /// + public override void Dispose() + { + request.Dispose(); + } + + #endregion Dispose + + #region Serialization + + public override async Task WriteToAsync(Stream stream, CancellationToken cancellationToken) + { + Argument.AssertNotNull(stream, nameof(stream)); + + byte[] header = SerializeHeader(); + await stream.WriteAsync(header, 0, header.Length); + + if (request.Content != null) + { + await request.Content.WriteToAsync(stream, cancellationToken); + } + } + + public override void WriteTo(Stream stream, CancellationToken cancellationToken) + { + Argument.AssertNotNull(stream, nameof(stream)); + + byte[] header = SerializeHeader(); + stream.Write(header, 0, header.Length); + + if (request.Content != null) + { + request.Content.WriteTo(stream, cancellationToken); + } + } + + /// + /// Computes the length of the stream if possible. + /// + /// The computed length of the stream. + /// true if the length has been computed; otherwise false. + public override bool TryComputeLength(out long length) + { + // We have four states we could be in: + // 1. We have content, but the task is still running or finished without success + // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable + // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length + // 4. We don't have content (streamTask.Value == null) + // + // For #1 and #2, we return false. + // For #3, we return true & the size of our headers + the content length + // For #4, we return true & the size of our headers + + bool hasContent = request.Content != null; + length = 0; + + // Cases #1, #2, #3 + if (hasContent) + { + if (!request!.Content!.TryComputeLength(out length)) + { + length = 0; + return false; + } + } + + // We serialize header to a StringBuilder so that we can determine the length + // following the pattern for HttpContent to try and determine the message length. + // The perf overhead is no larger than for the other HttpContent implementations. + byte[] header = SerializeHeader(); + length += header.Length; + return true; + } + + private byte[] SerializeHeader() + { + StringBuilder message = new StringBuilder(DefaultHeaderAllocation); + RequestHeaders headers; + RequestContent? content; + + SerializeRequestLine(message, request); + headers = request.Headers; + content = request.Content; + + SerializeHeaderFields(message, headers); + + message.Append(CRLF); + return Encoding.UTF8.GetBytes(message.ToString()); + } + + /// + /// Serializes the HTTP request line. + /// + /// Where to write the request line. + /// The HTTP request. + private static void SerializeRequestLine(StringBuilder message, Request request) + { + Argument.AssertNotNull(message, nameof(message)); + message.Append(request.Method + SP); + message.Append(request.Uri.ToString() + SP); + message.Append("HTTP/1.1" + CRLF); + + // Only insert host header if not already present. + if (!request.Headers.TryGetValue(HttpHeader.Names.Host, out _)) + { + message.Append(HttpHeader.Names.Host + ColonSP + request.Uri.Host + CRLF); + } + } + + /// + /// Serializes the header fields. + /// + /// Where to write the status line. + /// The headers to write. + private static void SerializeHeaderFields(StringBuilder message, RequestHeaders headers) + { + Argument.AssertNotNull(message, nameof(message)); + foreach (HttpHeader header in headers) + { + message.Append(header.Name + ColonSP + header.Value + CRLF); + } + } + + #endregion Serialization + } +} diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index b185e54ae7f60..efbb944971021 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,11 +21,11 @@ namespace Azure.Core.Tests [TestFixture(typeof(HttpWebRequestTransport), true)] [TestFixture(typeof(HttpWebRequestTransport), false)] #endif - public class HttpPipelineFunctionalTests: PipelineTestBase + public class HttpPipelineFunctionalTests : PipelineTestBase { private readonly Type _transportType; - public HttpPipelineFunctionalTests(Type transportType, bool isAsync): base(isAsync) + public HttpPipelineFunctionalTests(Type transportType, bool isAsync) : base(isAsync) { _transportType = transportType; } @@ -32,7 +33,7 @@ public HttpPipelineFunctionalTests(Type transportType, bool isAsync): base(isAsy private TestOptions GetOptions() { var options = new TestOptions(); - options.Transport = (HttpPipelineTransport) Activator.CreateInstance(_transportType); + options.Transport = (HttpPipelineTransport)Activator.CreateInstance(_transportType); return options; } @@ -395,6 +396,46 @@ public async Task SendMultipartformData() Assert.AreEqual(formData.Current.ContentDisposition, "form-data; name=LastName; filename=file_name.txt"); } + [Test] + public async Task SendMultipartBatch() + { + string requestBody = null; + + HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions()); + using TestServer testServer = new TestServer( + context => + { + using var sr = new StreamReader(context.Request.Body, Encoding.UTF8); + requestBody = sr.ReadToEnd(); + return Task.CompletedTask; + }); + + using Request request = httpPipeline.CreateRequest(); + request.Method = RequestMethod.Put; + request.Uri.Reset(testServer.Address); + + Guid batchGuid = Guid.NewGuid(); + var content = new MultipartContent("mixed", $"batch_{batchGuid}"); + content.ApplyToRequest(request); + Guid changesetGuid = Guid.NewGuid(); + var changeset = new MultipartContent("mixed", $"changeset_{changesetGuid}"); + content.Add(changeset, new Dictionary { { HttpHeader.Names.ContentType, $"multipart/mixed;boundary=\"changeset_{changesetGuid}\"" } }); + + var message = httpPipeline.CreateMessage(); + var req = message.Request; + req.Method = RequestMethod.Post; + req.Uri.Reset(new Uri("https://myaccount.table.core.windows.net/Blogs")); + req.Headers.Add("x-ms-version", "2019-02-02"); + req.Headers.Add("DataServiceVersion", "3.0"); + req.Headers.Add("Accept", "application/json"); + req.Headers.Add("Content-Type", "application/json;odata=nometadata"); + req.Content = RequestContent.Create(Encoding.UTF8.GetBytes("{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}")); + changeset.Add(new RequestContentContent(req, new Dictionary { { "Content-Transfer-Encoding", "binary" } })); + changeset.Add(new RequestContentContent(req, new Dictionary { { "Content-Transfer-Encoding", "binary" } })); + request.Content = content; + using Response response = await ExecuteRequest(request, httpPipeline); + } + private class TestOptions : ClientOptions { } From d5ca3423017d200051e052858172fdedd4d5a20c Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 13 Aug 2020 09:21:11 -0500 Subject: [PATCH 02/14] wip --- sdk/core/Azure.Core/src/MultipartContent.cs | 17 +-- .../Azure.Core/src/RequestContentContent.cs | 26 ++--- .../tests/HttpPipelineFunctionalTests.cs | 100 +++++++++++++++--- 3 files changed, 103 insertions(+), 40 deletions(-) diff --git a/sdk/core/Azure.Core/src/MultipartContent.cs b/sdk/core/Azure.Core/src/MultipartContent.cs index ddd8bd5edba74..1b4f9e3903ae9 100644 --- a/sdk/core/Azure.Core/src/MultipartContent.cs +++ b/sdk/core/Azure.Core/src/MultipartContent.cs @@ -50,10 +50,12 @@ public MultipartContent(string subtype, string boundary) { ValidateBoundary(boundary); _subtype = subtype; - _boundary = boundary; + + // see https://www.ietf.org/rfc/rfc1521.txt page 29. + _boundary = boundary.Contains(":") ? $"\"{boundary}\"" : boundary; Headers = new Dictionary { - [HttpHeader.Names.ContentType] = $"multipart/{_subtype};boundary=\"{_boundary}\"" + [HttpHeader.Names.ContentType] = $"multipart/{_subtype}; boundary={_boundary}" }; _nestedContent = new List(); @@ -108,7 +110,7 @@ private static string GetDefaultBoundary() /// The request. public void ApplyToRequest(Request request) { - request.Headers.Add(HttpHeader.Names.ContentType, $"multipart/{_subtype};boundary=\"{_boundary}\""); + request.Headers.Add(HttpHeader.Names.ContentType, $"multipart/{_subtype}; boundary={_boundary}"); } /// @@ -142,11 +144,14 @@ private void AddInternal(RequestContent content, Dictionary? hea { headers = new Dictionary(); } - foreach (var key in content.Headers.Keys) + if (content.Headers != null) { - if (!headers.ContainsKey(key)) + foreach (var key in content.Headers.Keys) { - headers[key] = content.Headers[key]; + if (!headers.ContainsKey(key)) + { + headers[key] = content.Headers[key]; + } } } _nestedContent.Add(new MultipartRequestContent(content, headers)); diff --git a/sdk/core/Azure.Core/src/RequestContentContent.cs b/sdk/core/Azure.Core/src/RequestContentContent.cs index b6ba94d28cdab..fc4df2a37cee1 100644 --- a/sdk/core/Azure.Core/src/RequestContentContent.cs +++ b/sdk/core/Azure.Core/src/RequestContentContent.cs @@ -58,10 +58,7 @@ public RequestContentContent(Request request, Dictionary? header { foreach (var key in headers.Keys) { - if (!Headers.ContainsKey(key)) - { - Headers[key] = headers[key]; - } + Headers[key] = headers[key]; } } @@ -118,14 +115,13 @@ public override void WriteTo(Stream stream, CancellationToken cancellationToken) public override bool TryComputeLength(out long length) { // We have four states we could be in: - // 1. We have content, but the task is still running or finished without success - // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable - // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length - // 4. We don't have content (streamTask.Value == null) + // 1. We have content and the stream came back as a null or non-seekable + // 2. We have content and the stream is seekable, so we know its length + // 3. We don't have content // - // For #1 and #2, we return false. - // For #3, we return true & the size of our headers + the content length - // For #4, we return true & the size of our headers + // For #1, we return false. + // For #2, we return true & the size of our headers + the content length + // For #3, we return true & the size of our headers bool hasContent = request.Content != null; length = 0; @@ -151,15 +147,9 @@ public override bool TryComputeLength(out long length) private byte[] SerializeHeader() { StringBuilder message = new StringBuilder(DefaultHeaderAllocation); - RequestHeaders headers; - RequestContent? content; SerializeRequestLine(message, request); - headers = request.Headers; - content = request.Content; - - SerializeHeaderFields(message, headers); - + SerializeHeaderFields(message, request.Headers); message.Append(CRLF); return Encoding.UTF8.GetBytes(message.ToString()); } diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index efbb944971021..78240d61e9a51 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -397,8 +397,16 @@ public async Task SendMultipartformData() } [Test] - public async Task SendMultipartBatch() + public async Task SendMultipartData() { + const string ApplicationJson = "application/json"; + const string cteHeaderName = "Content-Transfer-Encoding"; + const string Binary = "binary"; + const string Mixed = "mixed"; + const string ApplicationJsonOdata = "application/json;odata=nometadata"; + const string DataServiceVersion = "DataServiceVersion"; + const string Three0 = "3.0"; + string requestBody = null; HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions()); @@ -415,25 +423,85 @@ public async Task SendMultipartBatch() request.Uri.Reset(testServer.Address); Guid batchGuid = Guid.NewGuid(); - var content = new MultipartContent("mixed", $"batch_{batchGuid}"); + var content = new MultipartContent(Mixed, $"batch_{batchGuid}"); content.ApplyToRequest(request); + Guid changesetGuid = Guid.NewGuid(); - var changeset = new MultipartContent("mixed", $"changeset_{changesetGuid}"); - content.Add(changeset, new Dictionary { { HttpHeader.Names.ContentType, $"multipart/mixed;boundary=\"changeset_{changesetGuid}\"" } }); - - var message = httpPipeline.CreateMessage(); - var req = message.Request; - req.Method = RequestMethod.Post; - req.Uri.Reset(new Uri("https://myaccount.table.core.windows.net/Blogs")); - req.Headers.Add("x-ms-version", "2019-02-02"); - req.Headers.Add("DataServiceVersion", "3.0"); - req.Headers.Add("Accept", "application/json"); - req.Headers.Add("Content-Type", "application/json;odata=nometadata"); - req.Content = RequestContent.Create(Encoding.UTF8.GetBytes("{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}")); - changeset.Add(new RequestContentContent(req, new Dictionary { { "Content-Transfer-Encoding", "binary" } })); - changeset.Add(new RequestContentContent(req, new Dictionary { { "Content-Transfer-Encoding", "binary" } })); + var changeset = new MultipartContent(Mixed, $"changeset_{changesetGuid}"); + content.Add(changeset); + + var postReq1 = httpPipeline.CreateMessage().Request; + postReq1.Method = RequestMethod.Post; + const string postUri = "https://myaccount.table.core.windows.net/Blogs"; + postReq1.Uri.Reset(new Uri(postUri)); + postReq1.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + postReq1.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + postReq1.Headers.Add(DataServiceVersion, Three0); + const string post1Body = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"1\", \"Rating\":9, \"Text\":\"Azure...\"}"; + postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post1Body)); + changeset.Add(new RequestContentContent(postReq1, new Dictionary { { cteHeaderName, Binary } })); + + var postReq2 = httpPipeline.CreateMessage().Request; + postReq1.Method = RequestMethod.Post; + postReq1.Uri.Reset(new Uri(postUri)); + postReq1.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + postReq1.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + postReq1.Headers.Add(DataServiceVersion, Three0); + const string post2Body = "{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}"; + postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); + changeset.Add(new RequestContentContent(postReq2, new Dictionary { { cteHeaderName, Binary } })); + + var patchReq = httpPipeline.CreateMessage().Request; + patchReq.Method = RequestMethod.Patch; + const string mergeUri = "https://myaccount.table.core.windows.net/Blogs(PartitionKey='Channel_17', RowKey='3')"; + patchReq.Uri.Reset(new Uri(mergeUri)); + patchReq.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + patchReq.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + patchReq.Headers.Add(DataServiceVersion, Three0); + const string patchBody = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"3\", \"Rating\":9, \"Text\":\"Azure Tables...\"}"; + patchReq.Content = RequestContent.Create(Encoding.UTF8.GetBytes(patchBody)); + changeset.Add(new RequestContentContent(postReq2, new Dictionary { { cteHeaderName, Binary } })); + request.Content = content; using Response response = await ExecuteRequest(request, httpPipeline); + Console.WriteLine(requestBody); + + Assert.That(requestBody, Is.EqualTo(@$"--batch_{batchGuid} +{HttpHeader.Names.ContentType}: multipart/mixed; boundary=changeset_{changesetGuid} + +--changeset_{changesetGuid} +{HttpHeader.Names.ContentType}: application/http +{cteHeaderName}: {Binary} + +POST {postUri} HTTP/1.1 +{HttpHeader.Names.ContentType}: {ApplicationJson} +{HttpHeader.Names.Accept}: {ApplicationJsonOdata} +{DataServiceVersion}: {Three0}; + +{post1Body} +--changeset_{changesetGuid} +{HttpHeader.Names.ContentType}: application/http +{cteHeaderName}: {Binary} + +POST {postUri} HTTP/1.1 +{HttpHeader.Names.ContentType}: {ApplicationJson} +{HttpHeader.Names.Accept}: {ApplicationJsonOdata} +{DataServiceVersion}: {Three0}; + +{post2Body} +--changeset_{changesetGuid} +{HttpHeader.Names.ContentType}: application/http +{cteHeaderName}: {Binary} + +MERGE {mergeUri} HTTP/1.1 +{HttpHeader.Names.ContentType}: {ApplicationJson} +{HttpHeader.Names.Accept}: {ApplicationJsonOdata} +{DataServiceVersion}: {Three0}; + +{patchBody} + +--changeset_{changesetGuid}-- +--batch_{batchGuid}")); } private class TestOptions : ClientOptions From 9f1f8123ecb4727b9146748f7132a144f28d11a7 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 14:10:50 -0500 Subject: [PATCH 03/14] support batch multiPart requests and responses --- sdk/core/Azure.Core/src/HttpHeader.cs | 14 +- sdk/core/Azure.Core/src/RequestContent.cs | 5 +- .../Azure.Core/src/Shared/BatchConstants.cs | 29 + sdk/core/Azure.Core/src/Shared/BatchErrors.cs | 27 + .../src/Shared/BufferedReadStream.cs | 440 +++++++ .../Azure.Core/src/Shared/HashCodeCombiner.cs | 86 ++ .../src/Shared/KeyValueAccumulator.cs | 91 ++ .../Azure.Core/src/Shared/MemoryResponse.cs | 149 +++ sdk/core/Azure.Core/src/Shared/Multipart.cs | 210 ++++ .../src/Shared/MultipartBoundary.cs | 74 ++ .../src/{ => Shared}/MultipartContent.cs | 5 +- .../{ => Shared}/MultipartFormDataContent.cs | 13 +- .../Azure.Core/src/Shared/MultipartReader.cs | 122 ++ .../src/Shared/MultipartReaderStream.cs | 348 ++++++ .../Azure.Core/src/Shared/MultipartSection.cs | 50 + .../src/{ => Shared}/RequestContentContent.cs | 12 +- .../src/Shared/StreamHelperExtensions.cs | 55 + .../Azure.Core/src/Shared/StringSegment.cs | 770 ++++++++++++ .../Azure.Core/src/Shared/StringTokenizer.cs | 129 ++ .../Azure.Core/src/Shared/StringValues.cs | 826 +++++++++++++ sdk/core/Azure.Core/src/Shared/ThrowHelper.cs | 112 ++ .../src/Shared/ValueStringBuilder.cs | 314 +++++ .../Azure.Core/tests/Azure.Core.Tests.csproj | 20 +- .../Azure.Core/tests/HashCodeCombinerTest.cs | 39 + .../tests/HttpPipelineFunctionalTests.cs | 51 +- .../Azure.Core/tests/MultipartReaderTests.cs | 385 ++++++ .../Azure.Core/tests/StringSegmentTest.cs | 1092 +++++++++++++++++ .../Azure.Core/tests/StringTokenizerTest.cs | 78 ++ .../Azure.Core/tests/StringValuesTests.cs | 604 +++++++++ .../tests/TransportFunctionalTests.cs | 10 +- .../src/Azure.Data.Tables.csproj | 31 +- .../src/MultipartContentExtensions.cs | 20 + .../Azure.Data.Tables/src/TableClient.cs | 70 ++ .../Azure.Data.Tables/src/TableRestClient.cs | 118 ++ .../tests/TableClientLiveTests.cs | 26 +- 35 files changed, 6371 insertions(+), 54 deletions(-) create mode 100644 sdk/core/Azure.Core/src/Shared/BatchConstants.cs create mode 100644 sdk/core/Azure.Core/src/Shared/BatchErrors.cs create mode 100644 sdk/core/Azure.Core/src/Shared/BufferedReadStream.cs create mode 100644 sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs create mode 100644 sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs create mode 100644 sdk/core/Azure.Core/src/Shared/MemoryResponse.cs create mode 100644 sdk/core/Azure.Core/src/Shared/Multipart.cs create mode 100644 sdk/core/Azure.Core/src/Shared/MultipartBoundary.cs rename sdk/core/Azure.Core/src/{ => Shared}/MultipartContent.cs (99%) rename sdk/core/Azure.Core/src/{ => Shared}/MultipartFormDataContent.cs (94%) create mode 100644 sdk/core/Azure.Core/src/Shared/MultipartReader.cs create mode 100644 sdk/core/Azure.Core/src/Shared/MultipartReaderStream.cs create mode 100644 sdk/core/Azure.Core/src/Shared/MultipartSection.cs rename sdk/core/Azure.Core/src/{ => Shared}/RequestContentContent.cs (97%) create mode 100644 sdk/core/Azure.Core/src/Shared/StreamHelperExtensions.cs create mode 100644 sdk/core/Azure.Core/src/Shared/StringSegment.cs create mode 100644 sdk/core/Azure.Core/src/Shared/StringTokenizer.cs create mode 100644 sdk/core/Azure.Core/src/Shared/StringValues.cs create mode 100644 sdk/core/Azure.Core/src/Shared/ThrowHelper.cs create mode 100644 sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs create mode 100644 sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs create mode 100644 sdk/core/Azure.Core/tests/MultipartReaderTests.cs create mode 100644 sdk/core/Azure.Core/tests/StringSegmentTest.cs create mode 100644 sdk/core/Azure.Core/tests/StringTokenizerTest.cs create mode 100644 sdk/core/Azure.Core/tests/StringValuesTests.cs create mode 100644 sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs create mode 100644 sdk/tables/Azure.Data.Tables/src/TableRestClient.cs diff --git a/sdk/core/Azure.Core/src/HttpHeader.cs b/sdk/core/Azure.Core/src/HttpHeader.cs index 5f075a98a0d4a..cf748bfb3f457 100644 --- a/sdk/core/Azure.Core/src/HttpHeader.cs +++ b/sdk/core/Azure.Core/src/HttpHeader.cs @@ -131,9 +131,19 @@ public static class Names /// Returns. "If-Unmodified-Since" /// public static string IfUnmodifiedSince => "If-Unmodified-Since"; + /// + /// Returns. "Referer" + /// + public static string Referer => "Referer"; + /// + /// Returns. "Host" + /// + public static string Host => "Host"; - internal static string Referer => "Referer"; - internal static string Host => "Host"; + /// + /// Returns "Content-Disposition" + /// + public static string ContentDisposition => "Content-Disposition"; } #pragma warning disable CA1034 // Nested types should not be visible diff --git a/sdk/core/Azure.Core/src/RequestContent.cs b/sdk/core/Azure.Core/src/RequestContent.cs index a4d81f87d6065..0e7d348679d05 100644 --- a/sdk/core/Azure.Core/src/RequestContent.cs +++ b/sdk/core/Azure.Core/src/RequestContent.cs @@ -81,7 +81,7 @@ public abstract class RequestContent : IDisposable /// /// A collection of header values associated with this request content. /// - internal virtual Dictionary? Headers { get; set; } + public IDictionary? Headers { get; set;} = null; private sealed class StreamContent : RequestContent { @@ -111,7 +111,8 @@ public override void WriteTo(Stream stream, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var read = _stream.Read(buffer, 0, buffer.Length); - if (read == 0) { break; } + if (read == 0) + { break; } cancellationToken.ThrowIfCancellationRequested(); stream.Write(buffer, 0, read); } diff --git a/sdk/core/Azure.Core/src/Shared/BatchConstants.cs b/sdk/core/Azure.Core/src/Shared/BatchConstants.cs new file mode 100644 index 0000000000000..b04a1c09cbdbd --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/BatchConstants.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core +{ + /// + /// Constants used by the batching APIs. + /// + internal static class BatchConstants + { + public const int KB = 1024; + public const int NoStatusCode = 0; + public const int RequestBufferSize = KB; + public const int ResponseLineSize = 4 * KB; + public const int ResponseBufferSize = 16 * KB; + + public const string XmsVersionName = "x-ms-version"; + public const string XmsClientRequestIdName = "x-ms-client-request-id"; + public const string XmsReturnClientRequestIdName = "x-ms-return-client-request-id"; + public const string ContentIdName = "Content-ID"; + public const string ContentLengthName = "Content-Length"; + + public const string MultipartContentTypePrefix = "multipart/mixed; boundary="; + public const string RequestContentType = "Content-Type: application/http"; + public const string RequestContentTransferEncoding = "Content-Transfer-Encoding: binary"; + public const string BatchSeparator = "--"; + public const string HttpVersion = "HTTP/1.1"; + } +} diff --git a/sdk/core/Azure.Core/src/Shared/BatchErrors.cs b/sdk/core/Azure.Core/src/Shared/BatchErrors.cs new file mode 100644 index 0000000000000..36aefa3cd5f63 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/BatchErrors.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ + /// + /// Errors raised by the batching APIs. + /// + internal static class BatchErrors + { + public static InvalidOperationException InvalidBatchContentType(string contentType) => + new InvalidOperationException($"Expected {HttpHeader.Names.ContentType} to start with {BatchConstants.MultipartContentTypePrefix} but received {contentType}"); + + public static InvalidOperationException InvalidHttpStatusLine(string statusLine) => + new InvalidOperationException($"Expected an HTTP status line, not {statusLine}"); + + public static InvalidOperationException InvalidHttpHeaderLine(string headerLine) => + new InvalidOperationException($"Expected an HTTP header line, not {headerLine}"); + + public static RequestFailedException InvalidResponse(ClientDiagnostics clientDiagnostics, Response response, Exception innerException) => + clientDiagnostics.CreateRequestFailedExceptionWithContent(response, message: "Invalid response", innerException: innerException); + + } +} diff --git a/sdk/core/Azure.Core/src/Shared/BufferedReadStream.cs b/sdk/core/Azure.Core/src/Shared/BufferedReadStream.cs new file mode 100644 index 0000000000000..e2f455765a021 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/BufferedReadStream.cs @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CA1305 // ToString Locale +#pragma warning disable CA1822 // Make member static +#pragma warning disable IDE0016 // Simplify null check +#pragma warning disable IDE0036 // Modifiers not ordered +#pragma warning disable IDE0054 // Use compound assignment +#pragma warning disable IDE0059 // Unnecessary assignment + +namespace Azure.Core +{ + /// + /// A Stream that wraps another stream and allows reading lines. + /// The data is buffered in memory. + /// + internal class BufferedReadStream : Stream + { + private const byte CR = (byte)'\r'; + private const byte LF = (byte)'\n'; + + private readonly Stream _inner; + private readonly byte[] _buffer; + private readonly ArrayPool _bytePool; + private int _bufferOffset = 0; + private int _bufferCount = 0; + private bool _disposed; + + /// + /// Creates a new stream. + /// + /// The stream to wrap. + /// Size of buffer in bytes. + public BufferedReadStream(Stream inner, int bufferSize) + : this(inner, bufferSize, ArrayPool.Shared) + { + } + + /// + /// Creates a new stream. + /// + /// The stream to wrap. + /// Size of buffer in bytes. + /// ArrayPool for the buffer. + public BufferedReadStream(Stream inner, int bufferSize, ArrayPool bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _inner = inner; + _bytePool = bytePool; + _buffer = bytePool.Rent(bufferSize); + } + + /// + /// The currently buffered data. + /// + public ArraySegment BufferedData + { + get { return new ArraySegment(_buffer, _bufferOffset, _bufferCount); } + } + + /// + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + /// + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + /// + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + /// + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + /// + public override long Length + { + get { return _inner.Length; } + } + + /// + public override long Position + { + get { return _inner.Position - _bufferCount; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Position must be positive."); + } + if (value == Position) + { + return; + } + + // Backwards? + if (value <= _inner.Position) + { + // Forward within the buffer? + var innerOffset = (int)(_inner.Position - value); + if (innerOffset <= _bufferCount) + { + // Yes, just skip some of the buffered data + _bufferOffset += innerOffset; + _bufferCount -= innerOffset; + } + else + { + // No, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + else + { + // Forward, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + /// + public override void SetLength(long value) + { + _inner.SetLength(value); + } + + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + _bytePool.Return(_buffer); + + if (disposing) + { + _inner.Dispose(); + } + } + } + + /// + public override void Flush() + { + _inner.Flush(); + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return _inner.Read(buffer, offset, count); + } + + /// + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return await _inner.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + /// + /// Ensures that the buffer is not empty. + /// + /// Returns true if the buffer is not empty; false otherwise. + public bool EnsureBuffered() + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = _inner.Read(_buffer, 0, _buffer.Length); + return _bufferCount > 0; + } + + /// + /// Ensures that the buffer is not empty. + /// + /// Cancellation token. + /// Returns true if the buffer is not empty; false otherwise. + public async Task EnsureBufferedAsync(CancellationToken cancellationToken) + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken).ConfigureAwait(false); + return _bufferCount > 0; + } + + /// + /// Ensures that a minimum amount of buffered data is available. + /// + /// Minimum amount of buffered data. + /// Returns true if the minimum amount of buffered data is available; false otherwise. + public bool EnsureBuffered(int minCount) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = _inner.Read(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + /// + /// Ensures that a minimum amount of buffered data is available. + /// + /// Minimum amount of buffered data. + /// Cancellation token. + /// Returns true if the minimum amount of buffered data is available; false otherwise. + public async Task EnsureBufferedAsync(int minCount, CancellationToken cancellationToken) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length.ToString()); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = await _inner.ReadAsync(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset, cancellationToken).ConfigureAwait(false); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + /// + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// + /// Maximum allowed line length. + /// A line. + public string ReadLine(int lengthLimit) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) + { + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && EnsureBuffered()) + { + if (builder.Length > lengthLimit) + { + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); + } + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + return DecodeLine(builder, foundCRLF); + } + } + + /// + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// + /// Maximum allowed line length. + /// Cancellation token. + /// A line. + public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) + { + bool foundCR = false, foundCRLF = false; + + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken).ConfigureAwait(false)) + { + if (builder.Length > lengthLimit) + { + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); + } + + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + return DecodeLine(builder, foundCRLF); + } + } + + private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) + { + var b = _buffer[_bufferOffset]; + builder.WriteByte(b); + _bufferOffset++; + _bufferCount--; + if (b == LF && foundCR) + { + foundCRLF = true; + return; + } + foundCR = b == CR; + } + + private string DecodeLine(MemoryStream builder, bool foundCRLF) + { + // Drop the final CRLF, if any + var length = foundCRLF ? builder.Length - 2 : builder.Length; + return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length); + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BufferedReadStream)); + } + } + + private void ValidateBuffer(byte[] buffer, int offset, int count) + { + // Delegate most of our validation. + var ignored = new ArraySegment(buffer, offset, count); + if (count == 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero."); + } + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs b/sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs new file mode 100644 index 0000000000000..ae89907b8ba06 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/dotnet/aspnetcore/tree/master/src/Shared/HashCodeCombiner + +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Azure.Core +{ + internal struct HashCodeCombiner + { + private long _combinedHash64; + + public int CombinedHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { return _combinedHash64.GetHashCode(); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private HashCodeCombiner(long seed) + { + _combinedHash64 = seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(IEnumerable e) + { + if (e == null) + { + Add(0); + } + else + { + var count = 0; + foreach (object o in e) + { + Add(o); + count++; + } + Add(count); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator int(HashCodeCombiner self) + { + return self.CombinedHash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(string s) + { + var hashCode = (s != null) ? s.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(object o) + { + var hashCode = (o != null) ? o.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TValue value, IEqualityComparer comparer) + { + var hashCode = value != null ? comparer.GetHashCode(value) : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(0x1505L); + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs b/sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs new file mode 100644 index 0000000000000..c9f2c3d1693a2 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System; +using System.Collections.Generic; + +#pragma warning disable IDE0008 // Use explicit type +#pragma warning disable IDE0018 // Inline declaration +#pragma warning disable IDE0034 // default can be simplified + +namespace Azure.Core +{ + internal struct KeyValueAccumulator + { + private Dictionary _accumulator; + private Dictionary> _expandingAccumulator; + + public void Append(string key, string value) + { + if (_accumulator == null) + { + _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + StringValues values; + if (_accumulator.TryGetValue(key, out values)) + { + if (values.Count == 0) + { + // Marker entry for this key to indicate entry already in expanding list dictionary + _expandingAccumulator[key].Add(value); + } + else if (values.Count == 1) + { + // Second value for this key + _accumulator[key] = new string[] { values[0], value }; + } + else + { + // Third value for this key + // Add zero count entry and move to data to expanding list dictionary + _accumulator[key] = default(StringValues); + + if (_expandingAccumulator == null) + { + _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more + var list = new List(8); + var array = values.ToArray(); + + list.Add(array[0]); + list.Add(array[1]); + list.Add(value); + + _expandingAccumulator[key] = list; + } + } + else + { + // First value for this key + _accumulator[key] = new StringValues(value); + } + + ValueCount++; + } + + public bool HasValues => ValueCount > 0; + + public int KeyCount => _accumulator?.Count ?? 0; + + public int ValueCount { get; private set; } + + public Dictionary GetResults() + { + if (_expandingAccumulator != null) + { + // Coalesce count 3+ multi-value entries into _accumulator dictionary + foreach (var entry in _expandingAccumulator) + { + _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); + } + } + + return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/MemoryResponse.cs b/sdk/core/Azure.Core/src/Shared/MemoryResponse.cs new file mode 100644 index 0000000000000..e924fce0d672d --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/MemoryResponse.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +#nullable disable + +namespace Azure.Core +{ + /// + /// A Response that can be constructed in memory without being tied to a + /// live request. + /// + internal class MemoryResponse : Response + { + /// + /// The Response . + /// + private int _status = BatchConstants.NoStatusCode; + + /// + /// The Response . + /// + private string _reasonPhrase = null; + + /// + /// The . + /// + private readonly IDictionary> _headers = + new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + public override int Status => _status; + + /// + public override string ReasonPhrase => _reasonPhrase; + + /// + public override Stream ContentStream { get; set; } + + /// + public override string ClientRequestId + { + get => TryGetHeader(BatchConstants.XmsClientRequestIdName, out string id) ? id : null; + set => SetHeader(BatchConstants.XmsClientRequestIdName, value); + } + + /// + /// Set the Response . + /// + /// The Response status. + public void SetStatus(int status) => _status = status; + + /// + /// Set the Response . + /// + /// The Response ReasonPhrase. + public void SetReasonPhrase(string reasonPhrase) => _reasonPhrase = reasonPhrase; + + /// + /// Set the Response . + /// + /// The response content. + public void SetContent(byte[] content) => ContentStream = new MemoryStream(content); + + /// + /// Set the Response . + /// + /// The response content. + public void SetContent(string content) => SetContent(Encoding.UTF8.GetBytes(content)); + + /// + /// Dispose the Response. + /// + public override void Dispose() => ContentStream?.Dispose(); + + /// + /// Set the value of a response header (and overwrite any existing + /// values). + /// + /// The name of the response header. + /// The response header value. + public void SetHeader(string name, string value) => + SetHeader(name, new List { value }); + + /// + /// Set the values of a response header (and overwrite any existing + /// values). + /// + /// The name of the response header. + /// The response header values. + public void SetHeader(string name, IEnumerable values) => + _headers[name] = values.ToList(); + + /// + /// Add a response header value. + /// + /// The name of the response header. + /// The response header value. + public void AddHeader(string name, string value) + { + if (!_headers.TryGetValue(name, out List values)) + { + _headers[name] = values = new List(); + } + values.Add(value); + } + + /// + protected override bool ContainsHeader(string name) => + _headers.ContainsKey(name); + + /// + protected override IEnumerable EnumerateHeaders() => + _headers.Select(header => new HttpHeader(header.Key, JoinHeaderValues(header.Value))); + + /// + protected override bool TryGetHeader(string name, out string value) + { + if (_headers.TryGetValue(name, out List headers)) + { + value = JoinHeaderValues(headers); + return true; + } + value = null; + return false; + } + + /// + protected override bool TryGetHeaderValues(string name, out IEnumerable values) + { + bool found = _headers.TryGetValue(name, out List headers); + values = headers; + return found; + } + + /// + /// Join multiple header values together with a comma. + /// + /// The header values. + /// A single joined value. + private static string JoinHeaderValues(IEnumerable values) => + string.Join(",", values); + } +} diff --git a/sdk/core/Azure.Core/src/Shared/Multipart.cs b/sdk/core/Azure.Core/src/Shared/Multipart.cs new file mode 100644 index 0000000000000..32c265ca78d1e --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/Multipart.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#nullable disable + +namespace Azure.Core +{ + /// + /// Provides support for creating and parsing multipart/mixed content. + /// This is implementing a couple of layered standards as mentioned at + /// https://docs.microsoft.com/en-us/rest/api/storageservices/blob-batch and + /// https://docs.microsoft.com/en-us/rest/api/storageservices/performing-entity-group-transactions + /// including https://www.odata.org/documentation/odata-version-3-0/batch-processing/ + /// and https://www.ietf.org/rfc/rfc2046.txt. + /// + internal static class Multipart + { + /// + /// Parse a multipart/mixed response body into several responses. + /// + /// The response content. + /// The response content type. + /// + /// Whether to invoke the operation asynchronously. + /// + /// + /// Optional to propagate notifications + /// that the operation should be cancelled. + /// + /// The parsed s. + internal static async Task ParseAsync( + Stream batchContent, + string batchContentType, + bool async, + CancellationToken cancellationToken) + { + // Get the batch boundary + if (!GetBoundary(batchContentType, out string batchBoundary)) + { + throw BatchErrors.InvalidBatchContentType(batchContentType); + } + + // Collect the responses in a dictionary (in case the Content-ID + // values come back out of order) + Dictionary responses = new Dictionary(); + + // Collect responses without a Content-ID in a List + List responsesWithoutId = new List(); + + // Read through the batch body one section at a time until the + // reader returns null + MultipartReader reader = new MultipartReader(batchBoundary, batchContent); + for (MultipartSection section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false); + section != null; + section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false)) + { + bool contentIdFound = true; + if (section.Headers.TryGetValue(HttpHeader.Names.ContentType, out StringValues contentTypeValues) && + contentTypeValues.Count == 1 && + GetBoundary(contentTypeValues[0], out string subBoundary)) + { + reader = new MultipartReader(subBoundary, section.Body); + continue; + } + // Get the Content-ID header + if (!section.Headers.TryGetValue(BatchConstants.ContentIdName, out StringValues contentIdValues) || + contentIdValues.Count != 1 || + !int.TryParse(contentIdValues[0], out int contentId)) + { + // If the header wasn't found, this is either: + // - a failed request with the details being sent as the first sub-operation + // - a tables batch request, which does not utilize Content-ID headers + // so we default the Content-ID to 0 and track that no Content-ID was found. + contentId = 0; + contentIdFound = false; + } + + // Build a response + MemoryResponse response = new MemoryResponse(); + if (contentIdFound) + { + // track responses by Content-ID + responses[contentId] = response; + } + else + { + // track responses without a Content-ID + responsesWithoutId.Add(response); + } + + // We're going to read the section's response body line by line + using var body = new BufferedReadStream(section.Body, BatchConstants.ResponseLineSize); + + // The first line is the status like "HTTP/1.1 202 Accepted" + string line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); + string[] status = line.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); + if (status.Length != 3) + { + throw BatchErrors.InvalidHttpStatusLine(line); + } + response.SetStatus(int.Parse(status[1], CultureInfo.InvariantCulture)); + response.SetReasonPhrase(status[2]); + + // Continue reading headers until we reach a blank line + line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); + while (!string.IsNullOrEmpty(line)) + { + // Split the header into the name and value + int splitIndex = line.IndexOf(':'); + if (splitIndex <= 0) + { + throw BatchErrors.InvalidHttpHeaderLine(line); + } + var name = line.Substring(0, splitIndex); + var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); + response.AddHeader(name, value); + + line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); + } + + // Copy the rest of the body as the response content + var responseContent = new MemoryStream(); + if (async) + { + await body.CopyToAsync(responseContent).ConfigureAwait(false); + } + else + { + body.CopyTo(responseContent); + } + responseContent.Seek(0, SeekOrigin.Begin); + response.ContentStream = responseContent; + } + + // Collect the responses and order by Content-ID, when available. + // Otherwise collect them as we received them. + Response[] ordered = new Response[responses.Count + responsesWithoutId.Count]; + for (int i = 0; i < responses.Count; i++) + { + ordered[i] = responses[i]; + } + for (int i = responses.Count; i < ordered.Length; i++) + { + ordered[i] = responsesWithoutId[i - responses.Count]; + } + return ordered; + } + + + /// + /// Read the next line of text. + /// + /// The stream to read from. + /// + /// Whether to invoke the operation asynchronously. + /// + /// + /// Optional to propagate notifications + /// that the operation should be cancelled. + /// + /// The next line of text. + internal static async Task ReadLineAsync( + this BufferedReadStream stream, + bool async, + CancellationToken cancellationToken) => + async ? + await stream.ReadLineAsync(BatchConstants.ResponseLineSize, cancellationToken).ConfigureAwait(false) : + stream.ReadLine(BatchConstants.ResponseLineSize); + + /// + /// Read the next multipart section. + /// + /// The reader to parse with. + /// + /// Whether to invoke the operation asynchronously. + /// + /// + /// Optional to propagate notifications + /// that the operation should be cancelled. + /// + /// The next multipart section. + internal static async Task GetNextSectionAsync( + this MultipartReader reader, + bool async, + CancellationToken cancellationToken) => + async ? + await reader.ReadNextSectionAsync(cancellationToken).ConfigureAwait(false) : +#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. + reader.ReadNextSectionAsync(cancellationToken).GetAwaiter().GetResult(); // #7972: Decide if we need a proper sync API here +#pragma warning restore AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. + + private static bool GetBoundary(string contentType, out string batchBoundary) + { + if (contentType == null || !contentType.StartsWith(BatchConstants.MultipartContentTypePrefix, StringComparison.Ordinal)) + { + batchBoundary = null; + return false; + } + batchBoundary = contentType.Substring(BatchConstants.MultipartContentTypePrefix.Length); + return true; + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/MultipartBoundary.cs b/sdk/core/Azure.Core/src/Shared/MultipartBoundary.cs new file mode 100644 index 0000000000000..bba72d47719b1 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/MultipartBoundary.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System; +using System.Text; + +namespace Azure.Core +{ + internal class MultipartBoundary + { + private readonly int[] _skipTable = new int[256]; + private readonly string _boundary; + private bool _expectLeadingCrlf; + + public MultipartBoundary(string boundary, bool expectLeadingCrlf = true) + { + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + _boundary = boundary; + _expectLeadingCrlf = expectLeadingCrlf; + Initialize(_boundary, _expectLeadingCrlf); + } + + private void Initialize(string boundary, bool expectLeadingCrlf) + { + if (expectLeadingCrlf) + { + BoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary); + } + else + { + BoundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); + } + FinalBoundaryLength = BoundaryBytes.Length + 2; // Include the final '--' terminator. + + var length = BoundaryBytes.Length; + for (var i = 0; i < _skipTable.Length; ++i) + { + _skipTable[i] = length; + } + for (var i = 0; i < length; ++i) + { + _skipTable[BoundaryBytes[i]] = Math.Max(1, length - 1 - i); + } + } + + public int GetSkipValue(byte input) + { + return _skipTable[input]; + } + + public bool ExpectLeadingCrlf + { + get { return _expectLeadingCrlf; } + set + { + if (value != _expectLeadingCrlf) + { + _expectLeadingCrlf = value; + Initialize(_boundary, _expectLeadingCrlf); + } + } + } + + public byte[] BoundaryBytes { get; private set; } = default!; // This gets initialized as part of Initialize called from in the ctor. + + public int FinalBoundaryLength { get; private set; } + } +} diff --git a/sdk/core/Azure.Core/src/MultipartContent.cs b/sdk/core/Azure.Core/src/Shared/MultipartContent.cs similarity index 99% rename from sdk/core/Azure.Core/src/MultipartContent.cs rename to sdk/core/Azure.Core/src/Shared/MultipartContent.cs index 1b4f9e3903ae9..c3d7f216f1cc8 100644 --- a/sdk/core/Azure.Core/src/MultipartContent.cs +++ b/sdk/core/Azure.Core/src/Shared/MultipartContent.cs @@ -3,12 +3,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; +#nullable disable + namespace Azure.Core { /// @@ -138,7 +139,7 @@ public virtual void Add(RequestContent content, Dictionary heade AddInternal(content, headers); } - private void AddInternal(RequestContent content, Dictionary? headers) + private void AddInternal(RequestContent content, Dictionary headers) { if (headers == null) { diff --git a/sdk/core/Azure.Core/src/MultipartFormDataContent.cs b/sdk/core/Azure.Core/src/Shared/MultipartFormDataContent.cs similarity index 94% rename from sdk/core/Azure.Core/src/MultipartFormDataContent.cs rename to sdk/core/Azure.Core/src/Shared/MultipartFormDataContent.cs index c89008d8d7732..9f77db775cad0 100644 --- a/sdk/core/Azure.Core/src/MultipartFormDataContent.cs +++ b/sdk/core/Azure.Core/src/Shared/MultipartFormDataContent.cs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; + +#nullable disable namespace Azure.Core { @@ -70,7 +67,7 @@ public override void Add(RequestContent content, Dictionary head /// The Request content to add to the collection. /// The name for the request content to add. /// The headers to add to the collection. - public void Add(RequestContent content, string name, Dictionary? headers) + public void Add(RequestContent content, string name, Dictionary headers) { Argument.AssertNotNull(content, nameof(content)); Argument.AssertNotNullOrWhiteSpace(name, nameof(name)); @@ -86,7 +83,7 @@ public void Add(RequestContent content, string name, Dictionary? /// The name for the request content to add. /// The file name for the reuest content to add to the collection. /// The headers to add to the collection. - public void Add(RequestContent content, string name, string fileName, Dictionary? headers) + public void Add(RequestContent content, string name, string fileName, Dictionary headers) { Argument.AssertNotNull(content, nameof(content)); Argument.AssertNotNullOrWhiteSpace(name, nameof(name)); @@ -95,7 +92,7 @@ public void Add(RequestContent content, string name, string fileName, Dictionary AddInternal(content, headers, name, fileName); } - private void AddInternal(RequestContent content, Dictionary? headers, string? name, string? fileName) + private void AddInternal(RequestContent content, Dictionary headers, string name, string fileName) { if (headers == null) { diff --git a/sdk/core/Azure.Core/src/Shared/MultipartReader.cs b/sdk/core/Azure.Core/src/Shared/MultipartReader.cs new file mode 100644 index 0000000000000..9804885823f68 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/MultipartReader.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CA1001 // disposable fields +#pragma warning disable IDE0008 // Use explicit type +#nullable disable + +namespace Azure.Core +{ + // https://www.ietf.org/rfc/rfc2046.txt + internal class MultipartReader + { + public const int DefaultHeadersCountLimit = 16; + public const int DefaultHeadersLengthLimit = 1024 * 16; + private const int DefaultBufferSize = 1024 * 4; + + private readonly BufferedReadStream _stream; + private readonly MultipartBoundary _boundary; + private MultipartReaderStream _currentStream; + + public MultipartReader(string boundary, Stream stream) + : this(boundary, stream, DefaultBufferSize) + { + } + + public MultipartReader(string boundary, Stream stream, int bufferSize) + { + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (bufferSize < boundary.Length + 8) // Size of the boundary + leading and trailing CRLF + leading and trailing '--' markers. + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "Insufficient buffer space, the buffer must be larger than the boundary: " + boundary); + } + _stream = new BufferedReadStream(stream, bufferSize); + _boundary = new MultipartBoundary(boundary, false); + // This stream will drain any preamble data and remove the first boundary marker. + // TODO: HeadersLengthLimit can't be modified until after the constructor. + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit }; + } + + /// + /// The limit for the number of headers to read. + /// + public int HeadersCountLimit { get; set; } = DefaultHeadersCountLimit; + + /// + /// The combined size limit for headers per multipart section. + /// + public int HeadersLengthLimit { get; set; } = DefaultHeadersLengthLimit; + + /// + /// The optional limit for the total response body length. + /// + public long? BodyLengthLimit { get; set; } + + public async Task ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // Drain the prior section. + await _currentStream.DrainAsync(cancellationToken).ConfigureAwait(false); + // If we're at the end return null + if (_currentStream.FinalBoundaryFound) + { + // There may be trailer data after the last boundary. + await _stream.DrainAsync(HeadersLengthLimit, cancellationToken).ConfigureAwait(false); + return null; + } + var headers = await ReadHeadersAsync(cancellationToken).ConfigureAwait(false); + _boundary.ExpectLeadingCrlf = true; + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit }; + long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null; + return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; + } + + private async Task> ReadHeadersAsync(CancellationToken cancellationToken) + { + int totalSize = 0; + var accumulator = new KeyValueAccumulator(); + var line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken).ConfigureAwait(false); + while (!string.IsNullOrEmpty(line)) + { + if (HeadersLengthLimit - totalSize < line.Length) + { + throw new InvalidDataException($"Multipart headers length limit {HeadersLengthLimit} exceeded."); + } + totalSize += line.Length; + int splitIndex = line.IndexOf(':'); + if (splitIndex <= 0) + { + throw new InvalidDataException($"Invalid header line: {line}"); + } + + var name = line.Substring(0, splitIndex); + var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); + accumulator.Append(name, value); + if (accumulator.KeyCount > HeadersCountLimit) + { + throw new InvalidDataException($"Multipart headers count limit {HeadersCountLimit} exceeded."); + } + + line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken).ConfigureAwait(false); + } + + return accumulator.GetResults(); + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/MultipartReaderStream.cs b/sdk/core/Azure.Core/src/Shared/MultipartReaderStream.cs new file mode 100644 index 0000000000000..157200bc2ed76 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/MultipartReaderStream.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable IDE0008 // Use explicit type +#pragma warning disable IDE0016 // Simplify null check +#pragma warning disable IDE0018 // Inline declaration +#pragma warning disable IDE0054 // Use compound assignment +#pragma warning disable IDE0059 // Unnecessary assignment + +namespace Azure.Core +{ + internal sealed class MultipartReaderStream : Stream + { + private readonly MultipartBoundary _boundary; + private readonly BufferedReadStream _innerStream; + private readonly ArrayPool _bytePool; + + private readonly long _innerOffset; + private long _position; + private long _observedLength; + private bool _finished; + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// The . + /// The boundary pattern to use. + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary) + : this(stream, boundary, ArrayPool.Shared) + { + } + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// The . + /// The boundary pattern to use. + /// The ArrayPool pool to use for temporary byte arrays. + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary, ArrayPool bytePool) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (boundary == null) + { + throw new ArgumentNullException(nameof(boundary)); + } + + _bytePool = bytePool; + _innerStream = stream; + _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; + _boundary = boundary; + } + + public bool FinalBoundaryFound { get; private set; } + + public long? LengthLimit { get; set; } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _observedLength; } + } + + public override long Position + { + get { return _position; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be positive."); + } + if (value > _observedLength) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be less than length."); + } + _position = value; + if (_position < _observedLength) + { + _finished = false; + } + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + private void PositionInnerStream() + { + if (_innerStream.CanSeek && _innerStream.Position != (_innerOffset + _position)) + { + _innerStream.Position = _innerOffset + _position; + } + } + + private int UpdatePosition(int read) + { + _position += read; + if (_observedLength < _position) + { + _observedLength = _position; + if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()) + { + throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded."); + } + } + return read; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!_innerStream.EnsureBuffered(_boundary.FinalBoundaryLength)) + { + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + + var length = _boundary.BoundaryBytes.Length; + Debug.Assert(matchCount == length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered + + var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } +#pragma warning disable CA1820 // Test for empty strings using string length + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); +#pragma warning restore CA1820 // Test for empty strings using string length + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!await _innerStream.EnsureBufferedAsync(_boundary.FinalBoundaryLength, cancellationToken).ConfigureAwait(false)) + { + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int matchOffset; + int matchCount; + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out matchOffset, out matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + // Sync, it's already buffered + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + + var length = _boundary.BoundaryBytes!.Length; + Debug.Assert(matchCount == length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered + + var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken).ConfigureAwait(false); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } +#pragma warning disable CA1820 // Test for empty strings using string length + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); +#pragma warning restore CA1820 // Test for empty strings using string length + + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + // Does segment1 contain all of matchBytes, or does it end with the start of matchBytes? + // 1: AAAAABBBBBCCCCC + // 2: BBBBB + // Or: + // 1: AAAAABBB + // 2: BBBBB + private bool SubMatch(ArraySegment segment1, byte[] matchBytes, out int matchOffset, out int matchCount) + { + // clear matchCount to zero + matchCount = 0; + + // case 1: does segment1 fully contain matchBytes? + { + var matchBytesLengthMinusOne = matchBytes.Length - 1; + var matchBytesLastByte = matchBytes[matchBytesLengthMinusOne]; + var segmentEndMinusMatchBytesLength = segment1.Offset + segment1.Count - matchBytes.Length; + + matchOffset = segment1.Offset; + while (matchOffset < segmentEndMinusMatchBytesLength) + { + var lookaheadTailChar = segment1.Array![matchOffset + matchBytesLengthMinusOne]; + if (lookaheadTailChar == matchBytesLastByte && + CompareBuffers(segment1.Array, matchOffset, matchBytes, 0, matchBytesLengthMinusOne) == 0) + { + matchCount = matchBytes.Length; + return true; + } + matchOffset += _boundary.GetSkipValue(lookaheadTailChar); + } + } + + // case 2: does segment1 end with the start of matchBytes? + var segmentEnd = segment1.Offset + segment1.Count; + + matchCount = 0; + for (; matchOffset < segmentEnd; matchOffset++) + { + var countLimit = segmentEnd - matchOffset; + for (matchCount = 0; matchCount < matchBytes.Length && matchCount < countLimit; matchCount++) + { + if (matchBytes[matchCount] != segment1.Array![matchOffset + matchCount]) + { + matchCount = 0; + break; + } + } + if (matchCount > 0) + { + break; + } + } + return matchCount > 0; + } + + private static int CompareBuffers(byte[] buffer1, int offset1, byte[] buffer2, int offset2, int count) + { + for (; count-- > 0; offset1++, offset2++) + { + if (buffer1[offset1] != buffer2[offset2]) + { + return buffer1[offset1] - buffer2[offset2]; + } + } + return 0; + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/MultipartSection.cs b/sdk/core/Azure.Core/src/Shared/MultipartSection.cs new file mode 100644 index 0000000000000..cc51a8b777437 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/MultipartSection.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System.Collections.Generic; +using System.IO; + +namespace Azure.Core +{ + internal class MultipartSection + { + public string ContentType + { + get + { + if (Headers != null && Headers.TryGetValue(HttpHeader.Names.ContentType, out StringValues values)) + { + return values; + } + return null; + } + } + + public string ContentDisposition + { + get + { + if (Headers != null && Headers.TryGetValue(HttpHeader.Names.ContentDisposition, out StringValues values)) + { + return values; + } + return null; + } + } + + public Dictionary Headers { get; set; } + + /// + /// Gets or sets the body. + /// + public Stream Body { get; set; } = default!; + + /// + /// The position where the body starts in the total multipart body. + /// This may not be available if the total multipart body is not seekable. + /// + public long? BaseStreamOffset { get; set; } + } +} diff --git a/sdk/core/Azure.Core/src/RequestContentContent.cs b/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs similarity index 97% rename from sdk/core/Azure.Core/src/RequestContentContent.cs rename to sdk/core/Azure.Core/src/Shared/RequestContentContent.cs index fc4df2a37cee1..9306b9bff00fe 100644 --- a/sdk/core/Azure.Core/src/RequestContentContent.cs +++ b/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs @@ -1,16 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; +#nullable disable + namespace Azure.Core { /// @@ -45,7 +43,7 @@ public RequestContentContent(Request request) : this(request, default) /// /// The instance to encapsulate. /// Additional headers to apply to the request content. - public RequestContentContent(Request request, Dictionary? headers) + public RequestContentContent(Request request, Dictionary headers) { Argument.AssertNotNull(request, nameof(request)); @@ -86,11 +84,11 @@ public override async Task WriteToAsync(Stream stream, CancellationToken cancell Argument.AssertNotNull(stream, nameof(stream)); byte[] header = SerializeHeader(); - await stream.WriteAsync(header, 0, header.Length); + await stream.WriteAsync(header, 0, header.Length).ConfigureAwait(false); if (request.Content != null) { - await request.Content.WriteToAsync(stream, cancellationToken); + await request.Content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); } } diff --git a/sdk/core/Azure.Core/src/Shared/StreamHelperExtensions.cs b/sdk/core/Azure.Core/src/Shared/StreamHelperExtensions.cs new file mode 100644 index 0000000000000..6be8b9cf138c5 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/StreamHelperExtensions.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable IDE1006 // Prefix _ unexpected + +namespace Azure.Core +{ + internal static class StreamHelperExtensions + { + private const int _maxReadBufferSize = 1024 * 4; + + public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool.Shared, null, cancellationToken); + } + + public static Task DrainAsync(this Stream stream, long? limit, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool.Shared, limit, cancellationToken); + } + + public static async Task DrainAsync(this Stream stream, ArrayPool bytePool, long? limit, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var buffer = bytePool.Rent(_maxReadBufferSize); + long total = 0; + try + { + var read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + while (read > 0) + { + // Not all streams support cancellation directly. + cancellationToken.ThrowIfCancellationRequested(); + if (limit.HasValue && limit.GetValueOrDefault() - total < read) + { + throw new InvalidDataException($"The stream exceeded the data limit {limit.GetValueOrDefault()}."); + } + total += read; + read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + } + } + finally + { + bytePool.Return(buffer); + } + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/StringSegment.cs b/sdk/core/Azure.Core/src/Shared/StringSegment.cs new file mode 100644 index 0000000000000..8c521b5d4f6d4 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/StringSegment.cs @@ -0,0 +1,770 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Primitives/src + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +#pragma warning disable CA1307 // Equals Locale +#pragma warning disable IDE0041 // Null check can be simplified +#nullable disable + +namespace Azure.Core +{ + /// + /// An optimized representation of a substring. + /// + internal readonly struct StringSegment : IEquatable, IEquatable + { + /// + /// A for . + /// + public static readonly StringSegment Empty = string.Empty; + + /// + /// Initializes an instance of the struct. + /// + /// + /// The original . The includes the whole . + /// + public StringSegment(string buffer) + { + Buffer = buffer; + Offset = 0; + Length = buffer?.Length ?? 0; + } + + /// + /// Initializes an instance of the struct. + /// + /// The original used as buffer. + /// The offset of the segment within the . + /// The length of the segment. + /// + /// is . + /// + /// + /// or is less than zero, or + + /// is greater than the number of characters in . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public StringSegment(string buffer, int offset, int length) + { + // Validate arguments, check is minimal instructions with reduced branching for inlinable fast-path + // Negative values discovered though conversion to high values when converted to unsigned + // Failure should be rare and location determination and message is delegated to failure functions + if (buffer == null || (uint)offset > (uint)buffer.Length || (uint)length > (uint)(buffer.Length - offset)) + { + ThrowInvalidArguments(buffer, offset, length); + } + + Buffer = buffer; + Offset = offset; + Length = length; + } + + /// + /// Gets the buffer for this . + /// + public string Buffer { get; } + + /// + /// Gets the offset within the buffer for this . + /// + public int Offset { get; } + + /// + /// Gets the length of this . + /// + public int Length { get; } + + /// + /// Gets the value of this segment as a . + /// + public string Value + { + get + { + if (HasValue) + { + return Buffer.Substring(Offset, Length); + } + else + { + return null; + } + } + } + + /// + /// Gets whether this contains a valid value. + /// + public bool HasValue + { + get { return Buffer != null; } + } + + /// + /// Gets the at a specified position in the current . + /// + /// The offset into the + /// The at a specified position. + /// + /// is greater than or equal to or less than zero. + /// + public char this[int index] + { + get + { + if ((uint)index >= (uint)Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index); + } + + return Buffer[Offset + index]; + } + } + + /// + /// Gets a from the current . + /// + /// The from this . + public ReadOnlySpan AsSpan() => Buffer.AsSpan(Offset, Length); + + /// + /// Gets a from the current . + /// + /// The from this . + public ReadOnlyMemory AsMemory() => Buffer.AsMemory(Offset, Length); + + /// + /// Compares substrings of two specified objects using the specified rules, + /// and returns an integer that indicates their relative position in the sort order. + /// + /// The first to compare. + /// The second to compare. + /// One of the enumeration values that specifies the rules for the comparison. + /// + /// A 32-bit signed integer indicating the lexical relationship between the two comparands. + /// The value is negative if is less than , 0 if the two comparands are equal, + /// and positive if is greater than . + /// + public static int Compare(StringSegment a, StringSegment b, StringComparison comparisonType) + { + int minLength = Math.Min(a.Length, b.Length); + int diff = string.Compare(a.Buffer, a.Offset, b.Buffer, b.Offset, minLength, comparisonType); + if (diff == 0) + { + diff = a.Length - b.Length; + } + + return diff; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is StringSegment segment && Equals(segment); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// if the current object is equal to the other parameter; otherwise, . + public bool Equals(StringSegment other) => Equals(other, StringComparison.Ordinal); + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if the current object is equal to the other parameter; otherwise, . + public bool Equals(StringSegment other, StringComparison comparisonType) + { + if (Length != other.Length) + { + return false; + } + + return string.Compare(Buffer, Offset, other.Buffer, other.Offset, other.Length, comparisonType) == 0; + } + + // This handles StringSegment.Equals(string, StringSegment, StringComparison) and StringSegment.Equals(StringSegment, string, StringComparison) + // via the implicit type converter + /// + /// Determines whether two specified objects have the same value. A parameter specifies the culture, case, and + /// sort rules used in the comparison. + /// + /// The first to compare. + /// The second to compare. + /// One of the enumeration values that specifies the rules for the comparison. + /// if the objects are equal; otherwise, . + public static bool Equals(StringSegment a, StringSegment b, StringComparison comparisonType) + { + return a.Equals(b, comparisonType); + } + + /// + /// Checks if the specified is equal to the current . + /// + /// The to compare with the current . + /// if the specified is equal to the current ; otherwise, . + public bool Equals(string text) + { + return Equals(text, StringComparison.Ordinal); + } + + /// + /// Checks if the specified is equal to the current . + /// + /// The to compare with the current . + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if the specified is equal to the current ; otherwise, . + /// + /// is . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(string text, StringComparison comparisonType) + { + if (text == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); + } + + int textLength = text.Length; + if (!HasValue || Length != textLength) + { + return false; + } + + return string.Compare(Buffer, Offset, text, 0, textLength, comparisonType) == 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { +#if (NETCOREAPP2_1 || NETSTANDARD2_0 || NETFRAMEWORK) + // This GetHashCode is expensive since it allocates on every call. + // However this is required to ensure we retain any behavior (such as hash code randomization) that + // string.GetHashCode has. + return Value?.GetHashCode() ?? 0; +#elif NETCOREAPP || NETSTANDARD2_1 + return string.GetHashCode(AsSpan()); +#else +#error Target frameworks need to be updated. +#endif + + } + + /// + /// Checks if two specified have the same value. + /// + /// The first to compare, or . + /// The second to compare, or . + /// if the value of is the same as the value of ; otherwise, . + public static bool operator ==(StringSegment left, StringSegment right) => left.Equals(right); + + /// + /// Checks if two specified have different values. + /// + /// The first to compare, or . + /// The second to compare, or . + /// if the value of is different from the value of ; otherwise, . + public static bool operator !=(StringSegment left, StringSegment right) => !left.Equals(right); + + // PERF: Do NOT add a implicit converter from StringSegment to String. That would negate most of the perf safety. + /// + /// Creates a new from the given . + /// + /// The to convert to a + public static implicit operator StringSegment(string value) => new StringSegment(value); + + /// + /// Creates a see from the given . + /// + /// The to convert to a . + public static implicit operator ReadOnlySpan(StringSegment segment) => segment.AsSpan(); + + /// + /// Creates a see from the given . + /// + /// The to convert to a . + public static implicit operator ReadOnlyMemory(StringSegment segment) => segment.AsMemory(); + + /// + /// Checks if the beginning of this matches the specified when compared using the specified . + /// + /// The to compare. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if matches the beginning of this ; otherwise, . + /// + /// is . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool StartsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); + } + + bool result = false; + int textLength = text.Length; + + if (HasValue && Length >= textLength) + { + result = string.Compare(Buffer, Offset, text, 0, textLength, comparisonType) == 0; + } + + return result; + } + + /// + /// Checks if the end of this matches the specified when compared using the specified . + /// + /// The to compare. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if matches the end of this ; otherwise, . + /// + /// is . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool EndsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); + } + + bool result = false; + int textLength = text.Length; + int comparisonLength = Offset + Length - textLength; + + if (HasValue && comparisonLength > 0) + { + result = string.Compare(Buffer, comparisonLength, text, 0, textLength, comparisonType) == 0; + } + + return result; + } + + /// + /// Retrieves a substring from this . + /// The substring starts at the position specified by and has the remaining length. + /// + /// The zero-based starting character position of a substring in this . + /// A that is equivalent to the substring of remaining length that begins at + /// in this + /// + /// is greater than or equal to or less than zero. + /// + public string Substring(int offset) => Substring(offset, Length - offset); + + /// + /// Retrieves a substring from this . + /// The substring starts at the position specified by and has the specified . + /// + /// The zero-based starting character position of a substring in this . + /// The number of characters in the substring. + /// A that is equivalent to the substring of length that begins at + /// in this + /// + /// or is less than zero, or + is + /// greater than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int offset, int length) + { + if (!HasValue || offset < 0 || length < 0 || (uint)(offset + length) > (uint)Length) + { + ThrowInvalidArguments(offset, length); + } + + return Buffer.Substring(Offset + offset, length); + } + + /// + /// Retrieves a that represents a substring from this . + /// The starts at the position specified by . + /// + /// The zero-based starting character position of a substring in this . + /// A that begins at in this + /// whose length is the remainder. + /// + /// is greater than or equal to or less than zero. + /// + public StringSegment Subsegment(int offset) => Subsegment(offset, Length - offset); + + /// + /// Retrieves a that represents a substring from this . + /// The starts at the position specified by and has the specified . + /// + /// The zero-based starting character position of a substring in this . + /// The number of characters in the substring. + /// A that is equivalent to the substring of length that begins at in this + /// + /// or is less than zero, or + is + /// greater than . + /// + public StringSegment Subsegment(int offset, int length) + { + if (!HasValue || offset < 0 || length < 0 || (uint)(offset + length) > (uint)Length) + { + ThrowInvalidArguments(offset, length); + } + + return new StringSegment(Buffer, Offset + offset, length); + } + + /// + /// Gets the zero-based index of the first occurrence of the character in this . + /// The search starts at and examines a specified number of character positions. + /// + /// The Unicode character to seek. + /// The zero-based index position at which the search starts. + /// The number of characters to examine. + /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. + /// + /// or is less than zero, or + is + /// greater than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOf(char c, int start, int count) + { + int offset = Offset + start; + + if (!HasValue || start < 0 || (uint)offset > (uint)Buffer.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); + } + + int index = Buffer.IndexOf(c, offset, count); + if (index != -1) + { + index -= Offset; + } + + return index; + } + + /// + /// Gets the zero-based index of the first occurrence of the character in this . + /// The search starts at . + /// + /// The Unicode character to seek. + /// The zero-based index position at which the search starts. + /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. + /// + /// is greater than or equal to or less than zero. + /// + public int IndexOf(char c, int start) => IndexOf(c, start, Length - start); + + /// + /// Gets the zero-based index of the first occurrence of the character in this . + /// + /// The Unicode character to seek. + /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. + public int IndexOf(char c) => IndexOf(c, 0, Length); + + /// + /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array + /// of Unicode characters. The search starts at a specified character position and examines a specified number + /// of character positions. + /// + /// A Unicode character array containing one or more characters to seek. + /// The search starting position. + /// The number of character positions to examine. + /// The zero-based index position of the first occurrence in this instance where any character in + /// was found; -1 if no character in was found. + /// + /// is . + /// + /// + /// or is less than zero, or + is + /// greater than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOfAny(char[] anyOf, int startIndex, int count) + { + int index = -1; + + if (HasValue) + { + if (startIndex < 0 || Offset + startIndex > Buffer.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); + } + + if (count < 0 || Offset + startIndex + count > Buffer.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); + } + + index = Buffer.IndexOfAny(anyOf, Offset + startIndex, count); + if (index != -1) + { + index -= Offset; + } + } + + return index; + } + + /// + /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array + /// of Unicode characters. The search starts at a specified character position. + /// + /// A Unicode character array containing one or more characters to seek. + /// The search starting position. + /// The zero-based index position of the first occurrence in this instance where any character in + /// was found; -1 if no character in was found. + /// + /// is greater than or equal to or less than zero. + /// + public int IndexOfAny(char[] anyOf, int startIndex) + { + return IndexOfAny(anyOf, startIndex, Length - startIndex); + } + + /// + /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array + /// of Unicode characters. + /// + /// A Unicode character array containing one or more characters to seek. + /// The zero-based index position of the first occurrence in this instance where any character in + /// was found; -1 if no character in was found. + public int IndexOfAny(char[] anyOf) + { + return IndexOfAny(anyOf, 0, Length); + } + + /// + /// Reports the zero-based index position of the last occurrence of a specified Unicode character within this instance. + /// + /// The Unicode character to seek. + /// The zero-based index position of value if that character is found, or -1 if it is not. + public int LastIndexOf(char value) + { + int index = -1; + + if (HasValue) + { + index = Buffer.LastIndexOf(value, Offset + Length - 1, Length); + if (index != -1) + { + index -= Offset; + } + } + + return index; + } + + /// + /// Removes all leading and trailing whitespaces. + /// + /// The trimmed . + public StringSegment Trim() => TrimStart().TrimEnd(); + + /// + /// Removes all leading whitespaces. + /// + /// The trimmed . + public unsafe StringSegment TrimStart() + { + int trimmedStart = Offset; + int length = Offset + Length; + + fixed (char* p = Buffer) + { + while (trimmedStart < length) + { + char c = p[trimmedStart]; + + if (!char.IsWhiteSpace(c)) + { + break; + } + + trimmedStart++; + } + } + + return new StringSegment(Buffer, trimmedStart, length - trimmedStart); + } + + /// + /// Removes all trailing whitespaces. + /// + /// The trimmed . + public unsafe StringSegment TrimEnd() + { + int offset = Offset; + int trimmedEnd = offset + Length - 1; + + fixed (char* p = Buffer) + { + while (trimmedEnd >= offset) + { + char c = p[trimmedEnd]; + + if (!char.IsWhiteSpace(c)) + { + break; + } + + trimmedEnd--; + } + } + + return new StringSegment(Buffer, offset, trimmedEnd - offset + 1); + } + + /// + /// Splits a string into s that are based on the characters in an array. + /// + /// A character array that delimits the substrings in this string, an empty array that + /// contains no delimiters, or null. + /// An whose elements contain the s from this instance + /// that are delimited by one or more characters in . + public StringTokenizer Split(char[] chars) + { + return new StringTokenizer(this, chars); + } + + /// + /// Indicates whether the specified is null or an Empty string. + /// + /// The to test. + /// + public static bool IsNullOrEmpty(StringSegment value) + { + bool res = false; + + if (!value.HasValue || value.Length == 0) + { + res = true; + } + + return res; + } + + /// + /// Returns the represented by this or if the does not contain a value. + /// + /// The represented by this or if the does not contain a value. + public override string ToString() + { + return Value ?? string.Empty; + } + + // Methods that do no return (i.e. throw) are not inlined + // https://github.com/dotnet/coreclr/pull/6103 + private static void ThrowInvalidArguments(string buffer, int offset, int length) + { + // Only have single throw in method so is marked as "does not return" and isn't inlined to caller + throw GetInvalidArgumentsException(); + + Exception GetInvalidArgumentsException() + { + if (buffer == null) + { + return ThrowHelper.GetArgumentNullException(ExceptionArgument.buffer); + } + + if (offset < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (length < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.length); + } + + return ThrowHelper.GetArgumentException(ExceptionResource.Argument_InvalidOffsetLength); + } + } + + private void ThrowInvalidArguments(int offset, int length) + { + throw GetInvalidArgumentsException(HasValue); + + Exception GetInvalidArgumentsException(bool hasValue) + { + if (!hasValue) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (offset < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (length < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.length); + } + + return ThrowHelper.GetArgumentException(ExceptionResource.Argument_InvalidOffsetLengthStringSegment); + } + } + } + + internal class StringSegmentComparer : IComparer, IEqualityComparer + { + public static StringSegmentComparer Ordinal { get; } + = new StringSegmentComparer(StringComparison.Ordinal, StringComparer.Ordinal); + + public static StringSegmentComparer OrdinalIgnoreCase { get; } + = new StringSegmentComparer(StringComparison.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase); + + private StringSegmentComparer(StringComparison comparison, StringComparer comparer) + { + Comparison = comparison; + Comparer = comparer; + } + + private StringComparison Comparison { get; } + private StringComparer Comparer { get; } + + public int Compare(StringSegment x, StringSegment y) + { + return StringSegment.Compare(x, y, Comparison); + } + + public bool Equals(StringSegment x, StringSegment y) + { + return StringSegment.Equals(x, y, Comparison); + } + + public int GetHashCode(StringSegment obj) + { +#if (NETCOREAPP2_1 || NETSTANDARD2_0 || NETFRAMEWORK) + if (!obj.HasValue) + { + return 0; + } + + // .NET Core strings use randomized hash codes for security reasons. Consequently we must materialize the StringSegment as a string + return Comparer.GetHashCode(obj.Value); +#elif NETCOREAPP || NETSTANDARD2_1 + return string.GetHashCode(obj.AsSpan(), Comparison); +#endif + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/StringTokenizer.cs b/sdk/core/Azure.Core/src/Shared/StringTokenizer.cs new file mode 100644 index 0000000000000..c793c3d147b7e --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/StringTokenizer.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Primitives/src + +using System; +using System.Collections; +using System.Collections.Generic; + +#pragma warning disable IDE0034 // Default expression can be simplified +#nullable disable + +namespace Azure.Core +{ + /// + /// Tokenizes a into s. + /// + internal readonly struct StringTokenizer : IEnumerable + { + private readonly StringSegment _value; + private readonly char[] _separators; + + /// + /// Initializes a new instance of . + /// + /// The to tokenize. + /// The characters to tokenize by. + public StringTokenizer(string value, char[] separators) + { + if (value == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); + } + + if (separators == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.separators); + } + + _value = value; + _separators = separators; + } + + /// + /// Initializes a new instance of . + /// + /// The to tokenize. + /// The characters to tokenize by. + public StringTokenizer(StringSegment value, char[] separators) + { + if (!value.HasValue) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); + } + + if (separators == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.separators); + } + + _value = value; + _separators = separators; + } + + public Enumerator GetEnumerator() => new Enumerator(in _value, _separators); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly StringSegment _value; + private readonly char[] _separators; + private int _index; + + internal Enumerator(in StringSegment value, char[] separators) + { + _value = value; + _separators = separators; + Current = default; + _index = 0; + } + + public Enumerator(ref StringTokenizer tokenizer) + { + _value = tokenizer._value; + _separators = tokenizer._separators; + Current = default(StringSegment); + _index = 0; + } + + public StringSegment Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (!_value.HasValue || _index > _value.Length) + { + Current = default(StringSegment); + return false; + } + + int next = _value.IndexOfAny(_separators, _index); + if (next == -1) + { + // No separator found. Consume the remainder of the string. + next = _value.Length; + } + + Current = _value.Subsegment(_index, next - _index); + _index = next + 1; + + return true; + } + + public void Reset() + { + Current = default(StringSegment); + _index = 0; + } + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/StringValues.cs b/sdk/core/Azure.Core/src/Shared/StringValues.cs new file mode 100644 index 0000000000000..7de47a52d790d --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/StringValues.cs @@ -0,0 +1,826 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Primitives/src + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0032 // Use auto property +#pragma warning disable IDE0066 // Use switch expression + +namespace Azure.Core +{ + /// + /// Represents zero/null, one, or many strings in an efficient way. + /// + internal readonly struct StringValues : IList, IReadOnlyList, IEquatable, IEquatable, IEquatable + { + /// + /// A readonly instance of the struct whose value is an empty string array. + /// + /// + /// In application code, this field is most commonly used to safely represent a that has null string values. + /// + public static readonly StringValues Empty = new StringValues(Array.Empty()); + + private readonly object _values; + + /// + /// Initializes a new instance of the structure using the specified string. + /// + /// A string value or null. + public StringValues(string value) + { + _values = value; + } + + /// + /// Initializes a new instance of the structure using the specified array of strings. + /// + /// A string array. + public StringValues(string[] values) + { + _values = values; + } + + /// + /// Defines an implicit conversion of a given string to a . + /// + /// A string to implicitly convert. + public static implicit operator StringValues(string value) + { + return new StringValues(value); + } + + /// + /// Defines an implicit conversion of a given string array to a . + /// + /// A string array to implicitly convert. + public static implicit operator StringValues(string[] values) + { + return new StringValues(values); + } + + /// + /// Defines an implicit conversion of a given to a string, with multiple values joined as a comma separated string. + /// + /// + /// Returns null where has been initialized from an empty string array or is . + /// + /// A to implicitly convert. + public static implicit operator string (StringValues values) + { + return values.GetStringValue(); + } + + /// + /// Defines an implicit conversion of a given to a string array. + /// + /// A to implicitly convert. + public static implicit operator string[] (StringValues value) + { + return value.GetArrayValue(); + } + + /// + /// Gets the number of elements contained in this . + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + object value = _values; + if (value is string) + { + return 1; + } + if (value is null) + { + return 0; + } + else + { + // Not string, not null, can only be string[] + return Unsafe.As(value).Length; + } + } + } + + bool ICollection.IsReadOnly + { + get { return true; } + } + + /// + /// Gets the at index. + /// + /// The string at the specified index. + /// The zero-based index of the element to get. + /// Set operations are not supported on readonly . + string IList.this[int index] + { + get { return this[index]; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets the at index. + /// + /// The string at the specified index. + /// The zero-based index of the element to get. + public string this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + object value = _values; + if (value is string str) + { + if (index == 0) + { + return str; + } + } + else if (value != null) + { + // Not string, not null, can only be string[] + return Unsafe.As(value)[index]; // may throw + } + + return OutOfBounds(); // throws + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static string OutOfBounds() + { + return Array.Empty()[0]; // throws + } + + /// + /// Converts the value of the current object to its equivalent string representation, with multiple values joined as a comma separated string. + /// + /// A string representation of the value of the current object. + public override string ToString() + { + return GetStringValue() ?? string.Empty; + } + + private string GetStringValue() + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + object value = _values; + if (value is string s) + { + return s; + } + else + { + return GetStringValueFromArray(value); + } + + static string GetStringValueFromArray(object value) + { + if (value is null) + { + return null; + } + + Debug.Assert(value is string[]); + // value is not null or string, array, can only be string[] + string[] values = Unsafe.As(value); + switch (values.Length) + { + case 0: return null; + case 1: return values[0]; + default: return GetJoinedStringValueFromArray(values); + } + } + + static string GetJoinedStringValueFromArray(string[] values) + { + // Calculate final length + int length = 0; + for (int i = 0; i < values.Length; i++) + { + string value = values[i]; + // Skip null and empty values + if (value != null && value.Length > 0) + { + if (length > 0) + { + // Add seperator + length++; + } + + length += value.Length; + } + } +#if NETCOREAPP + // Create the new string + return string.Create(length, values, (span, strings) => { + int offset = 0; + // Skip null and empty values + for (int i = 0; i < strings.Length; i++) + { + string value = strings[i]; + if (value != null && value.Length > 0) + { + if (offset > 0) + { + // Add seperator + span[offset] = ','; + offset++; + } + + value.AsSpan().CopyTo(span.Slice(offset)); + offset += value.Length; + } + } + }); +#else + var sb = new ValueStringBuilder(length); + bool hasAdded = false; + // Skip null and empty values + for (int i = 0; i < values.Length; i++) + { + string value = values[i]; + if (value != null && value.Length > 0) + { + if (hasAdded) + { + // Add seperator + sb.Append(','); + } + + sb.Append(value); + hasAdded = true; + } + } + + return sb.ToString(); +#endif + } + } + + /// + /// Creates a string array from the current object. + /// + /// A string array represented by this instance. + /// + /// If the contains a single string internally, it is copied to a new array. + /// If the contains an array internally it returns that array instance. + /// + public string[] ToArray() + { + return GetArrayValue() ?? Array.Empty(); + } + + private string[] GetArrayValue() + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + object value = _values; + if (value is string[] values) + { + return values; + } + else if (value != null) + { + // value not array, can only be string + return new[] { Unsafe.As(value) }; + } + else + { + return null; + } + } + + /// + /// Returns the zero-based index of the first occurrence of an item in the . + /// + /// The string to locate in the . + /// the zero-based index of the first occurrence of within the , if found; otherwise, -1. + int IList.IndexOf(string item) + { + return IndexOf(item); + } + + private int IndexOf(string item) + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + object value = _values; + if (value is string[] values) + { + for (int i = 0; i < values.Length; i++) + { + if (string.Equals(values[i], item, StringComparison.Ordinal)) + { + return i; + } + } + return -1; + } + + if (value != null) + { + // value not array, can only be string + return string.Equals(Unsafe.As(value), item, StringComparison.Ordinal) ? 0 : -1; + } + + return -1; + } + + /// Determines whether a string is in the . + /// The to locate in the . + /// true if item is found in the ; otherwise, false. + bool ICollection.Contains(string item) + { + return IndexOf(item) >= 0; + } + + /// + /// Copies the entire to a string array, starting at the specified index of the target array. + /// + /// The one-dimensional that is the destination of the elements copied from. The must have zero-based indexing. + /// The zero-based index in the destination array at which copying begins. + /// array is null. + /// arrayIndex is less than 0. + /// The number of elements in the source is greater than the available space from arrayIndex to the end of the destination array. + void ICollection.CopyTo(string[] array, int arrayIndex) + { + CopyTo(array, arrayIndex); + } + + private void CopyTo(string[] array, int arrayIndex) + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + object value = _values; + if (value is string[] values) + { + Array.Copy(values, 0, array, arrayIndex, values.Length); + return; + } + + if (value != null) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + if (array.Length - arrayIndex < 1) + { + throw new ArgumentException( + $"'{nameof(array)}' is not long enough to copy all the items in the collection. Check '{nameof(arrayIndex)}' and '{nameof(array)}' length."); + } + + // value not array, can only be string + array[arrayIndex] = Unsafe.As(value); + } + } + + void ICollection.Add(string item) => throw new NotSupportedException(); + + void IList.Insert(int index, string item) => throw new NotSupportedException(); + + bool ICollection.Remove(string item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + /// Retrieves an object that can iterate through the individual strings in this . + /// An enumerator that can be used to iterate through the . + public Enumerator GetEnumerator() + { + return new Enumerator(_values); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Indicates whether the specified contains no string values. + /// + /// The to test. + /// true if value contains a single null string or empty array; otherwise, false. + public static bool IsNullOrEmpty(StringValues value) + { + object data = value._values; + if (data is null) + { + return true; + } + if (data is string[] values) + { + switch (values.Length) + { + case 0: return true; + case 1: return string.IsNullOrEmpty(values[0]); + default: return false; + } + } + else + { + // Not array, can only be string + return string.IsNullOrEmpty(Unsafe.As(data)); + } + } + + /// + /// Concatenates two specified instances of . + /// + /// The first to concatenate. + /// The second to concatenate. + /// The concatenation of and . + public static StringValues Concat(StringValues values1, StringValues values2) + { + int count1 = values1.Count; + int count2 = values2.Count; + + if (count1 == 0) + { + return values2; + } + + if (count2 == 0) + { + return values1; + } + + var combined = new string[count1 + count2]; + values1.CopyTo(combined, 0); + values2.CopyTo(combined, count1); + return new StringValues(combined); + } + + /// + /// Concatenates specified instance of with specified . + /// + /// The to concatenate. + /// The to concatenate. + /// The concatenation of and . + public static StringValues Concat(in StringValues values, string value) + { + if (value == null) + { + return values; + } + + int count = values.Count; + if (count == 0) + { + return new StringValues(value); + } + + var combined = new string[count + 1]; + values.CopyTo(combined, 0); + combined[count] = value; + return new StringValues(combined); + } + + /// + /// Concatenates specified instance of with specified . + /// + /// The to concatenate. + /// The to concatenate. + /// The concatenation of and . + public static StringValues Concat(string value, in StringValues values) + { + if (value == null) + { + return values; + } + + int count = values.Count; + if (count == 0) + { + return new StringValues(value); + } + + var combined = new string[count + 1]; + combined[0] = value; + values.CopyTo(combined, 1); + return new StringValues(combined); + } + + /// + /// Determines whether two specified objects have the same values in the same order. + /// + /// The first to compare. + /// The second to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool Equals(StringValues left, StringValues right) + { + int count = left.Count; + + if (count != right.Count) + { + return false; + } + + for (int i = 0; i < count; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } + + /// + /// Determines whether two specified have the same values. + /// + /// The first to compare. + /// The second to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool operator ==(StringValues left, StringValues right) + { + return Equals(left, right); + } + + /// + /// Determines whether two specified have different values. + /// + /// The first to compare. + /// The second to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(StringValues left, StringValues right) + { + return !Equals(left, right); + } + + /// + /// Determines whether this instance and another specified object have the same values. + /// + /// The string to compare to this instance. + /// true if the value of is the same as the value of this instance; otherwise, false. + public bool Equals(StringValues other) => Equals(this, other); + + /// + /// Determines whether the specified and objects have the same values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is the same as the value of ; otherwise, false. If is null, the method returns false. + public static bool Equals(string left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and objects have the same values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is the same as the value of ; otherwise, false. If is null, the method returns false. + public static bool Equals(StringValues left, string right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether this instance and a specified , have the same value. + /// + /// The to compare to this instance. + /// true if the value of is the same as this instance; otherwise, false. If is null, returns false. + public bool Equals(string other) => Equals(this, new StringValues(other)); + + /// + /// Determines whether the specified string array and objects have the same values. + /// + /// The string array to compare. + /// The to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool Equals(string[] left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and string array objects have the same values. + /// + /// The to compare. + /// The string array to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool Equals(StringValues left, string[] right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether this instance and a specified string array have the same values. + /// + /// The string array to compare to this instance. + /// true if the value of is the same as this instance; otherwise, false. + public bool Equals(string[] other) => Equals(this, new StringValues(other)); + + /// + public static bool operator ==(StringValues left, string right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether the specified and objects have different values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(StringValues left, string right) => !Equals(left, new StringValues(right)); + + /// + public static bool operator ==(string left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and objects have different values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(string left, StringValues right) => !Equals(new StringValues(left), right); + + /// + public static bool operator ==(StringValues left, string[] right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether the specified and string array have different values. + /// + /// The to compare. + /// The string array to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(StringValues left, string[] right) => !Equals(left, new StringValues(right)); + + /// + public static bool operator ==(string[] left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified string array and have different values. + /// + /// The string array to compare. + /// The to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(string[] left, StringValues right) => !Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and , which must be a + /// , , or array of , have the same value. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator ==(StringValues left, object right) => left.Equals(right); + + /// + /// Determines whether the specified and , which must be a + /// , , or array of , have different values. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator !=(StringValues left, object right) => !left.Equals(right); + + /// + /// Determines whether the specified , which must be a + /// , , or array of , and specified , have the same value. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator ==(object left, StringValues right) => right.Equals(left); + + /// + /// Determines whether the specified and object have the same values. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator !=(object left, StringValues right) => !right.Equals(left); + + /// + /// Determines whether this instance and a specified object have the same value. + /// + /// An object to compare with this object. + /// true if the current object is equal to ; otherwise, false. + public override bool Equals(object obj) + { + if (obj == null) + { + return Equals(this, StringValues.Empty); + } + + if (obj is string stringValue) + { + return Equals(this, stringValue); + } + + if (obj is string[] stringArray) + { + return Equals(this, stringArray); + } + + if (obj is StringValues values) + { + return Equals(this, values); + } + + return false; + } + + /// + public override int GetHashCode() + { + object value = _values; + if (value is string[] values) + { + if (Count == 1) + { + return Unsafe.As(this[0])?.GetHashCode() ?? Count.GetHashCode(); + } + var hcc = default(HashCodeCombiner); + for (int i = 0; i < values.Length; i++) + { + hcc.Add(values[i]); + } + return hcc.CombinedHash; + } + else + { + return Unsafe.As(value)?.GetHashCode() ?? Count.GetHashCode(); + } + } + + /// + /// Enumerates the string values of a . + /// + public struct Enumerator : IEnumerator + { + private readonly string[] _values; + private string _current; + private int _index; + + internal Enumerator(object value) + { + if (value is string str) + { + _values = null; + _current = str; + } + else + { + _current = null; + _values = Unsafe.As(value); + } + _index = 0; + } + + public Enumerator(ref StringValues values) : this(values._values) + { } + + public bool MoveNext() + { + int index = _index; + if (index < 0) + { + return false; + } + + string[] values = _values; + if (values != null) + { + if ((uint)index < (uint)values.Length) + { + _index = index + 1; + _current = values[index]; + return true; + } + + _index = -1; + return false; + } + + _index = -1; // sentinel value + return _current != null; + } + + public string Current => _current; + + object IEnumerator.Current => _current; + + void IEnumerator.Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/ThrowHelper.cs b/sdk/core/Azure.Core/src/Shared/ThrowHelper.cs new file mode 100644 index 0000000000000..1ff6d014759ea --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/ThrowHelper.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Primitives/src + +using System; +using System.Diagnostics; + +#pragma warning disable CA1305 // ToString Locale +#pragma warning disable IDE0051 // Private member not used + +namespace Azure.Core +{ + internal static class ThrowHelper + { + internal static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw new ArgumentNullException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentException(ExceptionResource resource) + { + throw new ArgumentException(GetResourceText(resource)); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw new InvalidOperationException(GetResourceText(resource)); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource, params object[] args) + { + var message = string.Format(GetResourceText(resource), args); + + throw new InvalidOperationException(message); + } + + internal static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + internal static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static ArgumentException GetArgumentException(ExceptionResource resource) + { + return new ArgumentException(GetResourceText(resource)); + } + + private static string GetResourceText(ExceptionResource resource) + { + // return Resources.ResourceManager.GetString(GetResourceName(resource), Resources.Culture); + // Hack to avoid including the resx: + return resource switch + { + ExceptionResource.Argument_InvalidOffsetLength => "Offset and length are out of bounds for the string or length is greater than the number of characters from index to the end of the string.", + ExceptionResource.Argument_InvalidOffsetLengthStringSegment => "Offset and length are out of bounds for this StringSegment or length is greater than the number of characters to the end of this StringSegment.", + ExceptionResource.Capacity_CannotChangeAfterWriteStarted => "Cannot change capacity after write started.", + ExceptionResource.Capacity_NotEnough => "Not enough capacity to write '{0}' characters, only '{1}' left.", + ExceptionResource.Capacity_NotUsedEntirely => "Entire reserved capacity was not used. Capacity: '{0}', written '{1}'.", + _ => throw new ArgumentOutOfRangeException(nameof(resource)) + }; + } + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + private static string GetResourceName(ExceptionResource resource) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + "The enum value is not defined, please check the ExceptionResource Enum."); + + return resource.ToString(); + } + } + + internal enum ExceptionArgument + { + buffer, + offset, + length, + text, + start, + count, + index, + value, + capacity, + separators + } + + internal enum ExceptionResource + { + Argument_InvalidOffsetLength, + Argument_InvalidOffsetLengthStringSegment, + Capacity_CannotChangeAfterWriteStarted, + Capacity_NotEnough, + Capacity_NotUsedEntirely + } +} diff --git a/sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs b/sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs new file mode 100644 index 0000000000000..9136a8b57626a --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Common/src/System/Text + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable IDE0032 // Use auto property +#nullable enable + +namespace Azure.Core +{ + internal ref partial struct ValueStringBuilder + { + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public void EnsureCapacity(int capacity) + { + if (capacity > _chars.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)". + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after . + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public override string ToString() + { + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string? s) + { + if (s == null) + { + return; + } + + int count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s.AsSpan().CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + if ((uint)pos < (uint)_chars.Length) + { + _chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s == null) + { + return; + } + + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s.AsSpan().CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public unsafe void Append(char* value, int length) + { + int pos = _pos; + if (pos > _chars.Length - length) + { + Grow(length); + } + + Span dst = _chars.Slice(_pos, length); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = *value++; + } + _pos += length; + } + + public void Append(ReadOnlySpan value) + { + int pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + char[] poolArray = ArrayPool.Shared.Rent(Math.Max(_pos + additionalCapacityBeyondPos, _chars.Length * 2)); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[]? toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } +} diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index a7a10b64fb891..5c8270ab15f90 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -23,13 +23,29 @@ + + - + + - + + + + + + + + + + + + + + diff --git a/sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs b/sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs new file mode 100644 index 0000000000000..c66c6e9934f67 --- /dev/null +++ b/sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class HashCodeCombinerTest + { + [Test] + public void GivenTheSameInputs_ItProducesTheSameOutput() + { + var hashCode1 = new HashCodeCombiner(); + var hashCode2 = new HashCodeCombiner(); + + hashCode1.Add(42); + hashCode1.Add("foo"); + hashCode2.Add(42); + hashCode2.Add("foo"); + + Assert.That(hashCode1.CombinedHash, Is.EqualTo(hashCode2.CombinedHash)); + } + + [Test] + public void HashCode_Is_OrderSensitive() + { + var hashCode1 = HashCodeCombiner.Start(); + var hashCode2 = HashCodeCombiner.Start(); + + hashCode1.Add(42); + hashCode1.Add("foo"); + + hashCode2.Add("foo"); + hashCode2.Add(42); + + Assert.That(hashCode1.CombinedHash, Is.Not.EqualTo(hashCode2.CombinedHash)); + } + } +} diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index fe67dcef36342..3d0d38acb6519 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -17,7 +17,7 @@ namespace Azure.Core.Tests [TestFixture(typeof(HttpClientTransport), true)] [TestFixture(typeof(HttpClientTransport), false)] -// TODO: Uncomment after release + // TODO: Uncomment after release #if false && NETFRAMEWORK [TestFixture(typeof(HttpWebRequestTransport), true)] [TestFixture(typeof(HttpWebRequestTransport), false)] @@ -445,9 +445,10 @@ public async Task SendMultipartData() const string cteHeaderName = "Content-Transfer-Encoding"; const string Binary = "binary"; const string Mixed = "mixed"; - const string ApplicationJsonOdata = "application/json;odata=nometadata"; + const string ApplicationJsonOdata = "application/json; odata=nometadata"; const string DataServiceVersion = "DataServiceVersion"; const string Three0 = "3.0"; + const string Host = "myaccount.table.core.windows.net"; string requestBody = null; @@ -474,7 +475,7 @@ public async Task SendMultipartData() var postReq1 = httpPipeline.CreateMessage().Request; postReq1.Method = RequestMethod.Post; - const string postUri = "https://myaccount.table.core.windows.net/Blogs"; + string postUri = $"https://{Host}/Blogs"; postReq1.Uri.Reset(new Uri(postUri)); postReq1.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); postReq1.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); @@ -484,25 +485,25 @@ public async Task SendMultipartData() changeset.Add(new RequestContentContent(postReq1, new Dictionary { { cteHeaderName, Binary } })); var postReq2 = httpPipeline.CreateMessage().Request; - postReq1.Method = RequestMethod.Post; - postReq1.Uri.Reset(new Uri(postUri)); - postReq1.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); - postReq1.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); - postReq1.Headers.Add(DataServiceVersion, Three0); + postReq2.Method = RequestMethod.Post; + postReq2.Uri.Reset(new Uri(postUri)); + postReq2.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + postReq2.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + postReq2.Headers.Add(DataServiceVersion, Three0); const string post2Body = "{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}"; - postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); + postReq2.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); changeset.Add(new RequestContentContent(postReq2, new Dictionary { { cteHeaderName, Binary } })); var patchReq = httpPipeline.CreateMessage().Request; patchReq.Method = RequestMethod.Patch; - const string mergeUri = "https://myaccount.table.core.windows.net/Blogs(PartitionKey='Channel_17', RowKey='3')"; + string mergeUri = $"https://{Host}/Blogs(PartitionKey='Channel_17',%20RowKey='3')"; patchReq.Uri.Reset(new Uri(mergeUri)); patchReq.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); patchReq.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); patchReq.Headers.Add(DataServiceVersion, Three0); const string patchBody = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"3\", \"Rating\":9, \"Text\":\"Azure Tables...\"}"; patchReq.Content = RequestContent.Create(Encoding.UTF8.GetBytes(patchBody)); - changeset.Add(new RequestContentContent(postReq2, new Dictionary { { cteHeaderName, Binary } })); + changeset.Add(new RequestContentContent(patchReq, new Dictionary { { cteHeaderName, Binary } })); request.Content = content; using Response response = await ExecuteRequest(request, httpPipeline); @@ -516,9 +517,10 @@ public async Task SendMultipartData() {cteHeaderName}: {Binary} POST {postUri} HTTP/1.1 -{HttpHeader.Names.ContentType}: {ApplicationJson} -{HttpHeader.Names.Accept}: {ApplicationJsonOdata} -{DataServiceVersion}: {Three0}; +{HttpHeader.Names.Host}: {Host} +{HttpHeader.Names.Accept}: {ApplicationJson} +{DataServiceVersion}: {Three0} +{HttpHeader.Names.ContentType}: {ApplicationJsonOdata} {post1Body} --changeset_{changesetGuid} @@ -526,24 +528,27 @@ public async Task SendMultipartData() {cteHeaderName}: {Binary} POST {postUri} HTTP/1.1 -{HttpHeader.Names.ContentType}: {ApplicationJson} -{HttpHeader.Names.Accept}: {ApplicationJsonOdata} -{DataServiceVersion}: {Three0}; +{HttpHeader.Names.Host}: {Host} +{HttpHeader.Names.Accept}: {ApplicationJson} +{DataServiceVersion}: {Three0} +{HttpHeader.Names.ContentType}: {ApplicationJsonOdata} {post2Body} --changeset_{changesetGuid} {HttpHeader.Names.ContentType}: application/http {cteHeaderName}: {Binary} -MERGE {mergeUri} HTTP/1.1 -{HttpHeader.Names.ContentType}: {ApplicationJson} -{HttpHeader.Names.Accept}: {ApplicationJsonOdata} -{DataServiceVersion}: {Three0}; +PATCH {mergeUri} HTTP/1.1 +{HttpHeader.Names.Host}: {Host} +{HttpHeader.Names.Accept}: {ApplicationJson} +{DataServiceVersion}: {Three0} +{HttpHeader.Names.ContentType}: {ApplicationJsonOdata} {patchBody} - --changeset_{changesetGuid}-- ---batch_{batchGuid}")); + +--batch_{batchGuid}-- +")); } private class TestOptions : ClientOptions diff --git a/sdk/core/Azure.Core/tests/MultipartReaderTests.cs b/sdk/core/Azure.Core/tests/MultipartReaderTests.cs new file mode 100644 index 0000000000000..a5386f68e3484 --- /dev/null +++ b/sdk/core/Azure.Core/tests/MultipartReaderTests.cs @@ -0,0 +1,385 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable warnings + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class MultipartReaderTests + { + private const string Boundary = "9051914041544843365972754266"; + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string OnePartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string OnePartBodyTwoHeaders = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"Custom-header: custom-value\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string OnePartBodyWithTrailingWhitespace = +"--9051914041544843365972754266 \r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + // It's non-compliant but common to leave off the last CRLF. + private const string OnePartBodyWithoutFinalCRLF = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--"; + private const string TwoPartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string TwoPartBodyWithUnicodeFileName = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a色.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + private const string ThreePartBody = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\r\n" + +"Content-Type: text/html\r\n" + +"\r\n" + +"Content of a.html.\r\n" + +"\r\n" + +"--9051914041544843365972754266--\r\n"; + + private const string TwoPartBodyIncompleteBuffer = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" + +"Content-Type: text/plain\r\n" + +"\r\n" + +"Content of a.txt.\r\n" + +"\r\n" + +"--9051914041544843365"; + + private static MemoryStream MakeStream(string text) + { + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } + + private static string GetString(byte[] buffer, int count) + { + return Encoding.ASCII.GetString(buffer, 0, count); + } + + [Test] + public async Task MultipartReader_ReadSinglePartBody_Success() + { + var stream = MakeStream(OnePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public void MultipartReader_HeaderCountExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) + { + HeadersCountLimit = 1, + }; + + var exception = Assert.ThrowsAsync(async () => await reader.ReadNextSectionAsync()); + Assert.That(exception.Message, Is.EqualTo("Multipart headers count limit 1 exceeded.")); + } + + [Test] + public void MultipartReader_HeadersLengthExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) + { + HeadersLengthLimit = 60, + }; + + var exception = Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); + Assert.That(exception.Message, Is.EqualTo("Line length limit 17 exceeded.")); + } + + [Test] + public async Task MultipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() + { + var stream = MakeStream(OnePartBodyWithTrailingWhitespace); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public async Task MultipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() + { + var stream = MakeStream(OnePartBodyWithoutFinalCRLF); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public async Task MultipartReader_ReadTwoPartBody_Success() + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(2)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"file1\"; filename=\"a.txt\"")); + Assert.That(section.Headers["Content-Type"][0], Is.EqualTo("text/plain")); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("Content of a.txt.\r\n")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public async Task MultipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + { + var stream = MakeStream(TwoPartBodyWithUnicodeFileName); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(2)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"file1\"; filename=\"a色.txt\"")); + Assert.That(section.Headers["Content-Type"][0], Is.EqualTo("text/plain")); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("Content of a.txt.\r\n")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public async Task MultipartReader_ThreePartBody_Success() + { + var stream = MakeStream(ThreePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(2)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"file1\"; filename=\"a.txt\"")); + Assert.That(section.Headers["Content-Type"][0], Is.EqualTo("text/plain")); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("Content of a.txt.\r\n")); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(2)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"file2\"; filename=\"a.html\"")); + Assert.That(section.Headers["Content-Type"][0], Is.EqualTo("text/html")); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("Content of a.html.\r\n")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public void MultipartReader_BufferSizeMustBeLargerThanBoundary_Throws() + { + var stream = MakeStream(ThreePartBody); + Assert.Throws(() => + { + var reader = new MultipartReader(Boundary, stream, 5); + }); + } + + [Test] + public async Task MultipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() + { + var stream = MakeStream(TwoPartBodyIncompleteBuffer); + var reader = new MultipartReader(Boundary, stream); + var buffer = new byte[128]; + + //first section can be read successfully + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\"")); + var read = section.Body.Read(buffer, 0, buffer.Length); + Assert.That(GetString(buffer, read), Is.EqualTo("text default")); + + //second section can be read successfully (even though the bottom boundary is truncated) + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(2)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"file1\"; filename=\"a.txt\"")); + Assert.That(section.Headers["Content-Type"][0], Is.EqualTo("text/plain")); + read = section.Body.Read(buffer, 0, buffer.Length); + Assert.That(GetString(buffer, read), Is.EqualTo("Content of a.txt.\r\n")); + + Assert.ThrowsAsync(async () => + { + // we'll be unable to ensure enough bytes are buffered to even contain a final boundary + section = await reader.ReadNextSectionAsync(); + }); + } + + [Test] + public async Task MultipartReader_ReadInvalidUtf8Header_ReplacementCharacters() + { + var body1 = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\" filename=\"a"; + + var body2 = +".txt\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Test] + public async Task MultipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() + { + var body1 = +"--9051914041544843365972754266\r\n" + +"Content-Disposition: form-data; name=\"text\" filename=\"a"; + + var body2 = +".txt\"\r\n" + +"\r\n" + +"text default\r\n" + +"--9051914041544843365972754266--\r\n"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.That(section.Headers.Count, Is.EqualTo(1)); + Assert.That(section.Headers["Content-Disposition"][0], Is.EqualTo("form-data; name=\"text\" filename=\"a\uFFFDU.txt\"")); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.That(Encoding.ASCII.GetString(buffer.ToArray()), Is.EqualTo("text default")); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + } +} diff --git a/sdk/core/Azure.Core/tests/StringSegmentTest.cs b/sdk/core/Azure.Core/tests/StringSegmentTest.cs new file mode 100644 index 0000000000000..335bebb9ca1ac --- /dev/null +++ b/sdk/core/Azure.Core/tests/StringSegmentTest.cs @@ -0,0 +1,1092 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class StringSegmentTest + { + [Test] + public void StringSegment_Empty() + { + // Arrange & Act + var segment = StringSegment.Empty; + + // Assert + Assert.True(segment.HasValue); + Assert.AreSame(string.Empty, segment.Value); + Assert.AreEqual(0, segment.Offset); + Assert.AreEqual(0, segment.Length); + } + + [Test] + public void StringSegment_ImplicitConvertFromString() + { + StringSegment segment = "Hello"; + + Assert.True(segment.HasValue); + Assert.AreEqual(0, segment.Offset); + Assert.AreEqual(5, segment.Length); + Assert.AreEqual("Hello", segment.Value); + } + + [Test] + public void StringSegment_AsSpan() + { + var segment = new StringSegment("Hello"); + + var span = segment.AsSpan(); + + Assert.AreEqual(5, span.Length); + } + + [Test] + public void StringSegment_ImplicitConvertToSpan() + { + ReadOnlySpan span = new StringSegment("Hello"); + + Assert.AreEqual(5, span.Length); + } + + [Test] + public void StringSegment_AsMemory() + { + var segment = new StringSegment("Hello"); + + var memory = segment.AsMemory(); + + Assert.AreEqual(5, memory.Length); + } + + [Test] + public void StringSegment_ImplicitConvertToMemory() + { + ReadOnlyMemory memory = new StringSegment("Hello"); + + Assert.AreEqual(5, memory.Length); + } + + [Test] + public void StringSegment_StringCtor_AllowsNullBuffers() + { + // Arrange & Act + var segment = new StringSegment(null); + + // Assert + Assert.False(segment.HasValue); + Assert.AreEqual(0, segment.Offset); + Assert.AreEqual(0, segment.Length); + } + + [Test] + public void StringSegmentConstructor_NullBuffer_Throws() + { + // Arrange, Act and Assert + var exception = Assert.Throws(() => new StringSegment(null, 0, 0)); + Assert.That(exception.Message.Contains("buffer")); + } + + [Test] + public void StringSegmentConstructor_NegativeOffset_Throws() + { + // Arrange, Act and Assert + var exception = Assert.Throws(() => new StringSegment("", -1, 0)); + Assert.That(exception.Message.Contains("offset")); + } + + [Test] + public void StringSegmentConstructor_NegativeLength_Throws() + { + // Arrange, Act and Assert + var exception = Assert.Throws(() => new StringSegment("", 0, -1)); + Assert.That(exception.Message.Contains("length")); + } + + [Test] + [TestCase(0, 10)] + [TestCase(10, 0)] + [TestCase(5, 5)] + [TestCase(int.MaxValue, int.MaxValue)] + public void StringSegmentConstructor_OffsetOrLengthOutOfBounds_Throws(int offset, int length) + { + // Arrange, Act and Assert + Assert.Throws(() => new StringSegment("lengthof9", offset, length)); + } + + [Test] + [TestCase("", 0, 0)] + [TestCase("abc", 2, 0)] + public void StringSegmentConstructor_AllowsEmptyBuffers(string text, int offset, int length) + { + // Arrange & Act + var segment = new StringSegment(text, offset, length); + + // Assert + Assert.True(segment.HasValue); + Assert.AreEqual(offset, segment.Offset); + Assert.AreEqual(length, segment.Length); + } + + [Test] + public void StringSegment_StringCtor_InitializesValuesCorrectly() + { + // Arrange + var buffer = "Hello world!"; + + // Act + var segment = new StringSegment(buffer); + + // Assert + Assert.True(segment.HasValue); + Assert.AreEqual(0, segment.Offset); + Assert.AreEqual(buffer.Length, segment.Length); + } + + [Test] + public void StringSegment_Value_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var value = segment.Value; + + // Assert + Assert.AreEqual("ello", value); + } + + [Test] + public void StringSegment_Value_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var value = segment.Value; + + // Assert + Assert.Null(value); + } + + [Test] + public void StringSegment_HasValue_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var hasValue = segment.HasValue; + + // Assert + Assert.True(hasValue); + } + + [Test] + public void StringSegment_HasValue_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var hasValue = segment.HasValue; + + // Assert + Assert.False(hasValue); + } + + [Test] + [TestCase("a", 0, 1, 0, 'a')] + [TestCase("abc", 1, 1, 0, 'b')] + [TestCase("abcdef", 1, 4, 0, 'b')] + [TestCase("abcdef", 1, 4, 1, 'c')] + [TestCase("abcdef", 1, 4, 2, 'd')] + [TestCase("abcdef", 1, 4, 3, 'e')] + public void StringSegment_Indexer_InRange(string value, int offset, int length, int index, char expected) + { + var segment = new StringSegment(value, offset, length); + + var result = segment[index]; + + Assert.AreEqual(expected, result); + } + + [Test] + [TestCase("", 0, 0, 0)] + [TestCase("a", 0, 1, -1)] + [TestCase("a", 0, 1, 1)] + public void StringSegment_Indexer_OutOfRangeThrows(string value, int offset, int length, int index) + { + var segment = new StringSegment(value, offset, length); + Assert.Throws(() => { var x = segment[index]; }); + } + + public static IEnumerable EndsWithData = new List + { + new object[] { "Hello", StringComparison.Ordinal, false }, + new object[] { "ello ", StringComparison.Ordinal, false }, + new object[] { "ll", StringComparison.Ordinal, false }, + new object[] { "ello", StringComparison.Ordinal, true }, + new object[] { "llo", StringComparison.Ordinal, true }, + new object[] { "lo", StringComparison.Ordinal, true }, + new object[] { "o", StringComparison.Ordinal, true }, + new object[] { string.Empty, StringComparison.Ordinal, true }, + new object[] { "eLLo", StringComparison.Ordinal, false }, + new object[] { "eLLo", StringComparison.OrdinalIgnoreCase, true }, + }; + + [Test] + [TestCaseSource(nameof(EndsWithData))] + public void StringSegment_EndsWith_Valid(string candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.EndsWith(candidate, comparison); + + // Assert + Assert.AreEqual(expectedResult, result); + } + + [Test] + public void StringSegment_EndsWith_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var result = segment.EndsWith(string.Empty, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static IEnumerable StartsWithData = new List + { + new object[] { "Hello", StringComparison.Ordinal, false }, + new object[] { "ello ", StringComparison.Ordinal, false }, + new object[] { "ll", StringComparison.Ordinal, false }, + new object[] { "ello", StringComparison.Ordinal, true }, + new object[] { "ell", StringComparison.Ordinal, true }, + new object[] { "el", StringComparison.Ordinal, true }, + new object[] { "e", StringComparison.Ordinal, true }, + new object[] { string.Empty, StringComparison.Ordinal, true }, + new object[] { "eLLo", StringComparison.Ordinal, false }, + new object[] { "eLLo", StringComparison.OrdinalIgnoreCase, true }, + }; + + [Test] + [TestCaseSource(nameof(StartsWithData))] + public void StringSegment_StartsWith_Valid(string candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.StartsWith(candidate, comparison); + + // Assert + Assert.AreEqual(expectedResult, result); + } + + [Test] + public void StringSegment_StartsWith_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var result = segment.StartsWith(string.Empty, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static IEnumerable EqualsStringData = new List + { + new object[] { "eLLo", StringComparison.OrdinalIgnoreCase, true }, + new object[] { "eLLo", StringComparison.Ordinal, false }, + }; + + [Test] + [TestCaseSource(nameof(EqualsStringData))] + public void StringSegment_Equals_String_Valid(string candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Equals(candidate, comparison); + + // Assert + Assert.AreEqual(expectedResult, result); + } + + [Test] + public void StringSegment_EqualsObject_Valid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + var segment2 = new StringSegment("Your Carport is blue", 5, 3); + + Assert.True(segment1.Equals((object)segment2)); + } + + [Test] + public void StringSegment_EqualsNull_Invalid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + + Assert.False(segment1.Equals(null as object)); + } + + [Test] + public void StringSegment_StaticEquals_Valid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + var segment2 = new StringSegment("Your Carport is blue", 5, 3); + + Assert.True(StringSegment.Equals(segment1, segment2)); + } + + [Test] + public void StringSegment_StaticEquals_Invalid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 4); + var segment2 = new StringSegment("Your Carport is blue", 5, 4); + + Assert.False(StringSegment.Equals(segment1, segment2)); + } + + [Test] + public void StringSegment_IsNullOrEmpty_Valid() + { + Assert.True(StringSegment.IsNullOrEmpty(null)); + Assert.True(StringSegment.IsNullOrEmpty(string.Empty)); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(null))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty))); + Assert.True(StringSegment.IsNullOrEmpty(StringSegment.Empty)); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty, 0, 0))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 0, 0))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 3, 0))); + } + + [Test] + public void StringSegment_IsNullOrEmpty_Invalid() + { + Assert.False(StringSegment.IsNullOrEmpty("A")); + Assert.False(StringSegment.IsNullOrEmpty("ABCDefg")); + Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("A", 0, 1))); + Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("ABCDefg", 3, 2))); + } + + public static IEnumerable GetHashCode_ReturnsSameValueForEqualSubstringsData = new List + { + new object[] { default(StringSegment), default(StringSegment) }, + new object[] { default(StringSegment), new StringSegment() }, + new object[] { new StringSegment("Test123", 0, 0), new StringSegment(string.Empty) }, + new object[] { new StringSegment("C`est si bon", 2, 3), new StringSegment("Yesterday", 1, 3) }, + new object[] { new StringSegment("Hello", 1, 4), new StringSegment("Hello world", 1, 4) }, + new object[] { new StringSegment("Hello"), new StringSegment("Hello", 0, 5) }, + }; + + [Test] + [TestCaseSource(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] + public void GetHashCode_ReturnsSameValueForEqualSubstrings(object segment1, object segment2) + { + // Act + var hashCode1 = segment1.GetHashCode(); + var hashCode2 = segment2.GetHashCode(); + + // Assert + Assert.AreEqual(hashCode1, hashCode2); + } + + public static string testString = "Test123"; + + public static IEnumerable GetHashCode_ReturnsDifferentValuesForInequalSubstringsData = new List + { + new object[] { new StringSegment(testString, 0, 1), new StringSegment(string.Empty) }, + new object[] { new StringSegment(testString, 0, 1), new StringSegment(testString, 1, 1) }, + new object[] { new StringSegment(testString, 1, 2), new StringSegment(testString, 1, 3) }, + new object[] { new StringSegment(testString, 0, 4), new StringSegment("TEST123", 0, 4) }, + }; + + [Test] + [TestCaseSource(nameof(GetHashCode_ReturnsDifferentValuesForInequalSubstringsData))] + public void GetHashCode_ReturnsDifferentValuesForInequalSubstrings( + object segment1, + object segment2) + { + // Act + var hashCode1 = segment1.GetHashCode(); + var hashCode2 = segment2.GetHashCode(); + + // Assert + Assert.AreNotEqual(hashCode1, hashCode2); + } + + [Test] + public void StringSegment_EqualsString_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var result = segment.Equals(string.Empty, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static IEnumerable DefaultStringSegmentEqualsStringSegmentData = new List + { + new object[] { default(StringSegment) }, + new object[] { new StringSegment() }, + }; + + [Test] + [TestCaseSource(nameof(DefaultStringSegmentEqualsStringSegmentData))] + public void DefaultStringSegment_EqualsStringSegment(object candidate) + { + // Arrange + var segment = default(StringSegment); + + // Act + var result = segment.Equals((StringSegment)candidate, StringComparison.Ordinal); + + // Assert + Assert.True(result); + } + + public static IEnumerable DefaultStringSegmentDoesNotEqualStringSegmentData = new List + { + new object[] { new StringSegment("Hello, World!", 1, 4) }, + new object[] { new StringSegment("Hello", 1, 0) }, + new object[] { new StringSegment(string.Empty) }, + }; + + [Test] + [TestCaseSource(nameof(DefaultStringSegmentDoesNotEqualStringSegmentData))] + public void DefaultStringSegment_DoesNotEqualStringSegment(object candidate) + { + // Arrange + var segment = default(StringSegment); + + // Act + var result = segment.Equals((StringSegment)candidate, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static IEnumerable DefaultStringSegmentDoesNotEqualStringData = new List + { + new object[] { string.Empty }, + new object[] { "Hello, World!" }, + }; + + [Test] + [TestCaseSource(nameof(DefaultStringSegmentDoesNotEqualStringData))] + public void DefaultStringSegment_DoesNotEqualString(string candidate) + { + // Arrange + var segment = default(StringSegment); + + // Act + var result = segment.Equals(candidate, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static IEnumerable EqualsStringSegmentData = new List + { + new object[] { new StringSegment("Hello, World!", 1, 4), StringComparison.Ordinal, true }, + new object[] { new StringSegment("HELlo, World!", 1, 4), StringComparison.Ordinal, false }, + new object[] { new StringSegment("HELlo, World!", 1, 4), StringComparison.OrdinalIgnoreCase, true }, + new object[] { new StringSegment("ello, World!", 0, 4), StringComparison.Ordinal, true }, + new object[] { new StringSegment("ello, World!", 0, 3), StringComparison.Ordinal, false }, + new object[] { new StringSegment("ello, World!", 1, 3), StringComparison.Ordinal, false }, + }; + + [Test] + [TestCaseSource(nameof(EqualsStringSegmentData))] + public void StringSegment_Equals_StringSegment_Valid(object candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Equals((StringSegment)candidate, comparison); + + // Assert + Assert.AreEqual(expectedResult, result); + } + + [Test] + public void StringSegment_EqualsStringSegment_Invalid() + { + // Arrange + var segment = new StringSegment(); + var candidate = new StringSegment("Hello, World!", 3, 2); + + // Act + var result = segment.Equals(candidate, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + [Test] + public void StringSegment_SubstringOffset_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Substring(offset: 1); + + // Assert + Assert.AreEqual("llo", result); + } + + [Test] + public void StringSegment_Substring_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Substring(offset: 1, length: 2); + + // Assert + Assert.AreEqual("ll", result); + } + + [Test] + public void StringSegment_Substring_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act & Assert + Assert.Throws(() => segment.Substring(0, 0)); + } + + [Test] + public void StringSegment_Substring_InvalidOffset() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(-1, 1)); + Assert.AreEqual("offset", exception.ParamName); + } + + [Test] + public void StringSegment_Substring_InvalidLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(0, -1)); + Assert.AreEqual("length", exception.ParamName); + } + + [Test] + public void StringSegment_Substring_InvalidOffsetAndLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(2, 3)); + Assert.That(exception.Message.Contains("bounds")); + } + + [Test] + public void StringSegment_Substring_OffsetAndLengthOverflows() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(1, int.MaxValue)); + Assert.That(exception.Message.Contains("bounds")); + } + + [Test] + public void StringSegment_SubsegmentOffset_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Subsegment(offset: 1); + + // Assert + Assert.AreEqual(new StringSegment("Hello, World!", 2, 3), result); + Assert.AreEqual("llo", result.Value); + } + + [Test] + public void StringSegment_Subsegment_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Subsegment(offset: 1, length: 2); + + // Assert + Assert.AreEqual(new StringSegment("Hello, World!", 2, 2), result); + Assert.AreEqual("ll", result.Value); + } + + [Test] + public void StringSegment_Subsegment_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act & Assert + Assert.Throws(() => segment.Subsegment(0, 0)); + } + + [Test] + public void StringSegment_Subsegment_InvalidOffset() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(-1, 1)); + Assert.AreEqual("offset", exception.ParamName); + } + + [Test] + public void StringSegment_Subsegment_InvalidLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(0, -1)); + Assert.AreEqual("length", exception.ParamName); + } + + [Test] + public void StringSegment_Subsegment_InvalidOffsetAndLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(2, 3)); + Assert.That(exception.Message.Contains("bounds")); + } + + [Test] + public void StringSegment_Subsegment_OffsetAndLengthOverflows() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(1, int.MaxValue)); + Assert.That(exception.Message.Contains("bounds")); + } + + public static IEnumerable CompareLesserData = new List + { + new object[] { new StringSegment("abcdef", 1, 4), StringSegmentComparer.Ordinal }, + new object[] { new StringSegment("abcdef", 1, 5), StringSegmentComparer.OrdinalIgnoreCase }, + new object[] { new StringSegment("ABCDEF", 2, 2), StringSegmentComparer.OrdinalIgnoreCase }, + }; + + [Test] + [TestCaseSource(nameof(CompareLesserData))] + public void StringSegment_Compare_Lesser(object candidate, object comparer) + { + // Arrange + var segment = new StringSegment("ABCDEF", 1, 4); + + // Act + var result = ((StringSegmentComparer)comparer).Compare(segment, (StringSegment)candidate); + + // Assert + Assert.True(result < 0, $"{segment} should be less than {candidate}"); + } + + public static IEnumerable CompareEqualData = new List + { + new object[] { new StringSegment("abcdef", 1, 4), StringSegmentComparer.Ordinal }, + new object[] { new StringSegment("ABCDEF", 1, 4), StringSegmentComparer.OrdinalIgnoreCase }, + new object[] { new StringSegment("bcde", 0, 4), StringSegmentComparer.Ordinal }, + new object[] { new StringSegment("BcDeF", 0, 4), StringSegmentComparer.OrdinalIgnoreCase }, + }; + + [Test] + [TestCaseSource(nameof(CompareEqualData))] + public void StringSegment_Compare_Equal(object candidate, object comparer) + { + // Arrange + var segment = new StringSegment("abcdef", 1, 4); + + // Act + var result = ((StringSegmentComparer)comparer).Compare(segment, (StringSegment)candidate); + + // Assert + Assert.True(result == 0, $"{segment} should equal {candidate}"); + } + + public static IEnumerable CompareGreaterData = new List + { + new object[] { new StringSegment("ABCDEF", 1, 4), StringSegmentComparer.Ordinal }, + new object[] { new StringSegment("ABCDEF", 0, 6), StringSegmentComparer.OrdinalIgnoreCase }, + new object[] { new StringSegment("abcdef", 0, 3), StringSegmentComparer.Ordinal }, + }; + + [Test] + [TestCaseSource(nameof(CompareGreaterData))] + public void StringSegment_Compare_Greater(object candidate, object comparer) + { + // Arrange + var segment = new StringSegment("abcdef", 1, 4); + + // Act + var result = ((StringSegmentComparer)comparer).Compare(segment, (StringSegment)candidate); + + // Assert + Assert.True(result > 0, $"{segment} should be greater than {candidate}"); + } + + [Test] + [TestCaseSource(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] + public void StringSegmentComparerOrdinal_GetHashCode_ReturnsSameValueForEqualSubstrings(object segment1, object segment2) + { + // Arrange + var comparer = StringSegmentComparer.Ordinal; + + // Act + var hashCode1 = comparer.GetHashCode((StringSegment)segment1); + var hashCode2 = comparer.GetHashCode((StringSegment)segment2); + + // Assert + Assert.AreEqual(hashCode1, hashCode2); + } + + [Test] + [TestCaseSource(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] + public void StringSegmentComparerOrdinalIgnoreCase_GetHashCode_ReturnsSameValueForEqualSubstrings(object segment1, object segment2) + { + // Arrange + var comparer = StringSegmentComparer.OrdinalIgnoreCase; + + // Act + var hashCode1 = comparer.GetHashCode((StringSegment)segment1); + var hashCode2 = comparer.GetHashCode((StringSegment)segment2); + + // Assert + Assert.AreEqual(hashCode1, hashCode2); + } + + [Test] + public void StringSegmentComparerOrdinalIgnoreCase_GetHashCode_ReturnsSameValueForDifferentlyCasedStrings() + { + // Arrange + var segment1 = new StringSegment("abc"); + var segment2 = new StringSegment("Abcd", 0, 3); + var comparer = StringSegmentComparer.OrdinalIgnoreCase; + + // Act + var hashCode1 = comparer.GetHashCode(segment1); + var hashCode2 = comparer.GetHashCode(segment2); + + // Assert + Assert.AreEqual(hashCode1, hashCode2); + } + + [Test] + [TestCaseSource(nameof(GetHashCode_ReturnsDifferentValuesForInequalSubstringsData))] + public void StringSegmentComparerOrdinal_GetHashCode_ReturnsDifferentValuesForInequalSubstrings(object segment1, object segment2) + { + // Arrange + var comparer = StringSegmentComparer.Ordinal; + + // Act + var hashCode1 = comparer.GetHashCode((StringSegment)segment1); + var hashCode2 = comparer.GetHashCode((StringSegment)segment2); + + // Assert + Assert.AreNotEqual(hashCode1, hashCode2); + } + + [Test] + public void IndexOf_ComputesIndex_RelativeToTheCurrentSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 10); + + // Act + var result = segment.IndexOf(','); + + // Assert + Assert.AreEqual(4, result); + } + + [Test] + public void IndexOf_ReturnsMinusOne_IfElementNotInSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act + var result = segment.IndexOf(','); + + // Assert + Assert.AreEqual(-1, result); + } + + [Test] + public void IndexOf_SkipsANumberOfCaracters_IfStartIsProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOf('!', 15); + + // Assert + Assert.AreEqual(buffer.Length - 4, result); + } + + [Test] + public void IndexOf_SearchOnlyInsideTheRange_IfStartAndCountAreProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOf('!', 15, 5); + + // Assert + Assert.AreEqual(-1, result); + } + + [Test] + public void IndexOf_NegativeStart_OutOfRangeThrows() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act & Assert + Assert.Throws(() => segment.IndexOf('!', -1, 3)); + } + + [Test] + public void IndexOf_StartOverflowsWithOffset_OutOfRangeThrows() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.IndexOf('!', int.MaxValue, 3)); + Assert.AreEqual("start", exception.ParamName); + } + + [Test] + public void IndexOfAny_ComputesIndex_RelativeToTheCurrentSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 10); + + // Act + var result = segment.IndexOfAny(new[] { ',' }); + + // Assert + Assert.AreEqual(4, result); + } + + [Test] + public void IndexOfAny_ReturnsMinusOne_IfElementNotInSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act + var result = segment.IndexOfAny(new[] { ',' }); + + // Assert + Assert.AreEqual(-1, result); + } + + [Test] + public void IndexOfAny_SkipsANumberOfCaracters_IfStartIsProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOfAny(new[] { '!' }, 15); + + // Assert + Assert.AreEqual(buffer.Length - 4, result); + } + + [Test] + public void IndexOfAny_SearchOnlyInsideTheRange_IfStartAndCountAreProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOfAny(new[] { '!' }, 15, 5); + + // Assert + Assert.AreEqual(-1, result); + } + + [Test] + public void LastIndexOf_ComputesIndex_RelativeToTheCurrentSegment() + { + // Arrange + var segment = new StringSegment("Hello, World, how, are, you!", 1, 14); + + // Act + var result = segment.LastIndexOf(','); + + // Assert + Assert.AreEqual(11, result); + } + + [Test] + public void LastIndexOf_ReturnsMinusOne_IfElementNotInSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act + var result = segment.LastIndexOf(','); + + // Assert + Assert.AreEqual(-1, result); + } + + [Test] + public void Value_DoesNotAllocateANewString_IfTheSegmentContainsTheWholeBuffer() + { + // Arrange + const string buffer = "Hello, World!"; + var segment = new StringSegment(buffer); + + // Act + var result = segment.Value; + + // Assert + Assert.AreSame(buffer, result); + } + + [Test] + public void StringSegment_CreateEmptySegment() + { + // Arrange + var segment = new StringSegment("//", 1, 0); + + // Assert + Assert.True(segment.HasValue); + } + + [Test] + [TestCase(" value", 0, 8, "value")] + [TestCase("value ", 0, 8, "value")] + [TestCase("\t\tvalue", 0, 7, "value")] + [TestCase("value\t\t", 0, 7, "value")] + [TestCase("\t\tvalue \t a", 1, 8, "value")] + [TestCase(" a ", 0, 9, "a")] + [TestCase("value\t value value ", 2, 13, "lue\t value v")] + [TestCase("\x0009value \x0085", 0, 8, "value")] + [TestCase(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "Hello")] + [TestCase(" ", 1, 2, "")] + [TestCase("\t\t\t", 0, 3, "")] + [TestCase("\n\n\t\t \t", 2, 3, "")] + [TestCase(" ", 1, 0, "")] + [TestCase("", 0, 0, "")] + public void Trim_RemovesLeadingAndTrailingWhitespaces(string value, int start, int length, string expected) + { + // Arrange + var segment = new StringSegment(value, start, length); + + // Act + var actual = segment.Trim(); + + // Assert + Assert.AreEqual(expected, actual.Value); + } + + [Test] + [TestCase(" value", 0, 8, "value")] + [TestCase("value ", 0, 8, "value ")] + [TestCase("\t\tvalue", 0, 7, "value")] + [TestCase("value\t\t", 0, 7, "value\t\t")] + [TestCase("\t\tvalue \t a", 1, 8, "value \t")] + [TestCase(" a ", 0, 9, "a ")] + [TestCase("value\t value value ", 2, 13, "lue\t value v")] + [TestCase("\x0009value \x0085", 0, 8, "value \x0085")] + [TestCase(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "Hello \u2029\n\t")] + [TestCase(" ", 1, 2, "")] + [TestCase("\t\t\t", 0, 3, "")] + [TestCase("\n\n\t\t \t", 2, 3, "")] + [TestCase(" ", 1, 0, "")] + [TestCase("", 0, 0, "")] + public void TrimStart_RemovesLeadingWhitespaces(string value, int start, int length, string expected) + { + // Arrange + var segment = new StringSegment(value, start, length); + + // Act + var actual = segment.TrimStart(); + + // Assert + Assert.AreEqual(expected, actual.Value); + } + + [Test] + [TestCase(" value", 0, 8, " value")] + [TestCase("value ", 0, 8, "value")] + [TestCase("\t\tvalue", 0, 7, "\t\tvalue")] + [TestCase("value\t\t", 0, 7, "value")] + [TestCase("\t\tvalue \t a", 1, 8, "\tvalue")] + [TestCase(" a ", 0, 9, " a")] + [TestCase("value\t value value ", 2, 13, "lue\t value v")] + [TestCase("\x0009value \x0085", 0, 8, "\x0009value")] + [TestCase(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "\f\t\u000B\u2028Hello")] + [TestCase(" ", 1, 2, "")] + [TestCase("\t\t\t", 0, 3, "")] + [TestCase("\n\n\t\t \t", 2, 3, "")] + [TestCase(" ", 1, 0, "")] + [TestCase("", 0, 0, "")] + public void TrimEnd_RemovesTrailingWhitespaces(string value, int start, int length, string expected) + { + // Arrange + var segment = new StringSegment(value, start, length); + + // Act + var actual = segment.TrimEnd(); + + // Assert + Assert.AreEqual(expected, actual.Value); + } + } +} diff --git a/sdk/core/Azure.Core/tests/StringTokenizerTest.cs b/sdk/core/Azure.Core/tests/StringTokenizerTest.cs new file mode 100644 index 0000000000000..707ab6fadab64 --- /dev/null +++ b/sdk/core/Azure.Core/tests/StringTokenizerTest.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using NUnit.Framework; + +namespace Microsoft.Extensions.Primitives +{ + public class StringTokenizerTest + { + [Test] + public void TokenizerReturnsEmptySequenceForNullValues() + { + // Arrange + var stringTokenizer = new StringTokenizer(); + var enumerator = stringTokenizer.GetEnumerator(); + + // Act + var next = enumerator.MoveNext(); + + // Assert + Assert.False(next); + } + + [Test] + [TestCase("", new[] { "" })] + [TestCase("a", new[] { "a" })] + [TestCase("abc", new[] { "abc" })] + [TestCase("a,b", new[] { "a", "b" })] + [TestCase("a,,b", new[] { "a", "", "b" })] + [TestCase(",a,b", new[] { "", "a", "b" })] + [TestCase(",,a,b", new[] { "", "", "a", "b" })] + [TestCase("a,b,", new[] { "a", "b", "" })] + [TestCase("a,b,,", new[] { "a", "b", "", "" })] + [TestCase("ab,cde,efgh", new[] { "ab", "cde", "efgh" })] + public void Tokenizer_ReturnsSequenceOfValues(string value, string[] expected) + { + // Arrange + var tokenizer = new StringTokenizer(value, new[] { ',' }); + + // Act + var result = tokenizer.Select(t => t.Value).ToArray(); + + // Assert + Assert.AreEqual(expected, result); + } + + [Test] + [TestCase("", new[] { "" })] + [TestCase("a", new[] { "a" })] + [TestCase("abc", new[] { "abc" })] + [TestCase("a.b", new[] { "a", "b" })] + [TestCase("a,b", new[] { "a", "b" })] + [TestCase("a.b,c", new[] { "a", "b", "c" })] + [TestCase("a,b.c", new[] { "a", "b", "c" })] + [TestCase("ab.cd,ef", new[] { "ab", "cd", "ef" })] + [TestCase("ab,cd.ef", new[] { "ab", "cd", "ef" })] + [TestCase(",a.b", new[] { "", "a", "b" })] + [TestCase(".a,b", new[] { "", "a", "b" })] + [TestCase(".,a.b", new[] { "", "", "a", "b" })] + [TestCase(",.a,b", new[] { "", "", "a", "b" })] + [TestCase("a.b,", new[] { "a", "b", "" })] + [TestCase("a,b.", new[] { "a", "b", "" })] + [TestCase("a.b,.", new[] { "a", "b", "", "" })] + [TestCase("a,b.,", new[] { "a", "b", "", "" })] + public void Tokenizer_SupportsMultipleSeparators(string value, string[] expected) + { + // Arrange + var tokenizer = new StringTokenizer(value, new[] { '.', ',' }); + + // Act + var result = tokenizer.Select(t => t.Value).ToArray(); + + // Assert + Assert.AreEqual(expected, result); + } + } +} diff --git a/sdk/core/Azure.Core/tests/StringValuesTests.cs b/sdk/core/Azure.Core/tests/StringValuesTests.cs new file mode 100644 index 0000000000000..0a6547a5cc875 --- /dev/null +++ b/sdk/core/Azure.Core/tests/StringValuesTests.cs @@ -0,0 +1,604 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class StringValuesTests + { + public static IEnumerable DefaultOrNullStringValues = new List + { + new object[] { new StringValues() }, + new object[] { new StringValues((string)null) }, + new object[] { new StringValues((string[])null) }, + new object[] { (string)null }, + new object[] { (string[])null } + }; + + public static IEnumerable EmptyStringValues = new List + { + new object[]{ StringValues.Empty}, + new object[]{ new StringValues(new string[0])}, + new object[]{ new string[0]} + }; + + public static IEnumerable FilledStringValues = new List + { + new StringValues("abc"), + new StringValues(new[] { "abc" }), + new StringValues(new[] { "abc", "bcd" }), + new StringValues(new[] { "abc", "bcd", "foo" }), + "abc", + new[] { "abc" }, + new[] { "abc", "bcd" }, + new[] { "abc", "bcd", "foo" } + }; + + public static IEnumerable FilledStringValuesWithExpectedStrings = new List + { + new object[] { default(StringValues), (string)null }, + new object[] { StringValues.Empty, (string)null }, + new object[] { new StringValues(new string[] { }), (string)null }, + new object[] { new StringValues(string.Empty), string.Empty }, + new object[] { new StringValues(new string[] { string.Empty }), string.Empty }, + new object[] { new StringValues("abc"), "abc" } + }; + + public static IEnumerable FilledStringValuesWithExpectedObjects = new List + { + new object[] { default(StringValues), (object)null }, + new object[] { StringValues.Empty, (object)null }, + new object[] { new StringValues(new string[] { }), (object)null }, + new object[] { new StringValues("abc"), (object)"abc" }, + new object[] { new StringValues("abc"), (object)new[] { "abc" } }, + new object[] { new StringValues(new[] { "abc" }), (object)new[] { "abc" } }, + new object[] { new StringValues(new[] { "abc", "bcd" }), (object)new[] { "abc", "bcd" } } + }; + + public static IEnumerable FilledStringValuesWithExpected = new List + { + new object[] { default(StringValues), new string[0] }, + new object[] { StringValues.Empty, new string[0] }, + new object[] { new StringValues(string.Empty), new[] { string.Empty } }, + new object[] { new StringValues("abc"), new[] { "abc" } }, + new object[] { new StringValues(new[] { "abc" }), new[] { "abc" } }, + new object[] { new StringValues(new[] { "abc", "bcd" }), new[] { "abc", "bcd" } }, + new object[] { new StringValues(new[] { "abc", "bcd", "foo" }), new[] { "abc", "bcd", "foo" } }, + new object[] { string.Empty, new[] { string.Empty } }, + new object[] { "abc", new[] { "abc" } }, + new object[] { new[] { "abc" }, new[] { "abc" } }, + new object[] { new[] { "abc", "bcd" }, new[] { "abc", "bcd" } }, + new object[] { new[] { "abc", "bcd", "foo" }, new[] { "abc", "bcd", "foo" } }, + new object[] { new[] { null, "abc", "bcd", "foo" }, new[] { null, "abc", "bcd", "foo" } }, + new object[] { new[] { "abc", null, "bcd", "foo" }, new[] { "abc", null, "bcd", "foo" } }, + new object[] { new[] { "abc", "bcd", "foo", null }, new[] { "abc", "bcd", "foo", null } }, + new object[] { new[] { string.Empty, "abc", "bcd", "foo" }, new[] { string.Empty, "abc", "bcd", "foo" } }, + new object[] { new[] { "abc", string.Empty, "bcd", "foo" }, new[] { "abc", string.Empty, "bcd", "foo" } }, + new object[] { new[] { "abc", "bcd", "foo", string.Empty }, new[] { "abc", "bcd", "foo", string.Empty } } + }; + + public static IEnumerable FilledStringValuesToStringToExpected = new List + { + new object[] { default(StringValues), string.Empty }, + new object[] { StringValues.Empty, string.Empty }, + new object[] { new StringValues(string.Empty), string.Empty }, + new object[] { new StringValues("abc"), "abc" }, + new object[] { new StringValues(new[] { "abc" }), "abc" }, + new object[] { new StringValues(new[] { "abc", "bcd" }), "abc,bcd" }, + new object[] { new StringValues(new[] { "abc", "bcd", "foo" }), "abc,bcd,foo" }, + new object[] { string.Empty, string.Empty }, + new object[] { (string)null, string.Empty }, + new object[] { "abc","abc" }, + new object[] { new[] { "abc" }, "abc" }, + new object[] { new[] { "abc", "bcd" }, "abc,bcd" }, + new object[] { new[] { "abc", null, "bcd" }, "abc,bcd" }, + new object[] { new[] { "abc", string.Empty, "bcd" }, "abc,bcd" }, + new object[] { new[] { "abc", "bcd", "foo" }, "abc,bcd,foo" }, + new object[] { new[] { null, "abc", "bcd", "foo" }, "abc,bcd,foo" }, + new object[] { new[] { "abc", null, "bcd", "foo" }, "abc,bcd,foo" }, + new object[] { new[] { "abc", "bcd", "foo", null }, "abc,bcd,foo" }, + new object[] { new[] { string.Empty, "abc", "bcd", "foo" }, "abc,bcd,foo" }, + new object[] { new[] { "abc", string.Empty, "bcd", "foo" }, "abc,bcd,foo" }, + new object[] { new[] { "abc", "bcd", "foo", string.Empty }, "abc,bcd,foo" }, + new object[] { new[] { "abc", "bcd", "foo", string.Empty, null }, "abc,bcd,foo" } + }; + + [Theory] + [TestCaseSource(nameof(DefaultOrNullStringValues))] + [TestCaseSource(nameof(EmptyStringValues))] + [TestCaseSource(nameof(FilledStringValues))] + public void IsReadOnly_True(object stringValuesObj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + Assert.True(((IList)stringValues).IsReadOnly); + Assert.Throws(() => ((IList)stringValues)[0] = string.Empty); + Assert.Throws(() => ((ICollection)stringValues).Add(string.Empty)); + Assert.Throws(() => ((IList)stringValues).Insert(0, string.Empty)); + Assert.Throws(() => ((IList)stringValues).RemoveAt(0)); + Assert.Throws(() => ((ICollection)stringValues).Clear()); + } + + [Theory] + [TestCaseSource(nameof(DefaultOrNullStringValues))] + public void DefaultOrNull_ExpectedValues(object stringValuesObj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + Assert.Null((string[])stringValues); + } + + [Theory] + [TestCaseSource(nameof(DefaultOrNullStringValues))] + [TestCaseSource(nameof(EmptyStringValues))] + public void DefaultNullOrEmpty_ExpectedValues(object stringValuesObj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + Assert.That(stringValues, Is.EqualTo(StringValues.Empty)); + Assert.AreEqual(string.Empty, stringValues.ToString()); + Assert.AreEqual(new string[0], stringValues.ToArray()); + + Assert.True(StringValues.IsNullOrEmpty(stringValues)); + Assert.Throws(() => { var x = stringValues[0]; }); + Assert.Throws(() => { var x = ((IList)stringValues)[0]; }); + Assert.AreEqual(string.Empty, stringValues.ToString()); + Assert.AreEqual(-1, ((IList)stringValues).IndexOf(null)); + Assert.AreEqual(-1, ((IList)stringValues).IndexOf(string.Empty)); + Assert.AreEqual(-1, ((IList)stringValues).IndexOf("not there")); + Assert.False(((ICollection)stringValues).Contains(null)); + Assert.False(((ICollection)stringValues).Contains(string.Empty)); + Assert.False(((ICollection)stringValues).Contains("not there")); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesToStringToExpected))] + public void ToString_ExpectedValues(object stringValuesObj, string expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + Assert.AreEqual(stringValues.ToString(), expected); + } + + [Test] + public void ImplicitStringConverter_Works() + { + string nullString = null; + StringValues stringValues = nullString; + Assert.That(stringValues, Is.EqualTo(StringValues.Empty)); + Assert.Null((string)stringValues); + Assert.Null((string[])stringValues); + + string aString = "abc"; + stringValues = aString; + Assert.AreEqual(1, stringValues.Count); + Assert.AreEqual(aString, stringValues); + Assert.AreEqual(aString, stringValues[0]); + Assert.AreEqual(aString, ((IList)stringValues)[0]); + Assert.AreEqual(new string[] { aString }, stringValues); + } + + [Test] + public void GetHashCode_SingleValueVsArrayWithOneItem_SameHashCode() + { + var sv1 = new StringValues("value"); + var sv2 = new StringValues(new[] { "value" }); + Assert.AreEqual(sv1, sv2); + Assert.AreEqual(sv1.GetHashCode(), sv2.GetHashCode()); + } + + [Test] + public void GetHashCode_NullCases_DifferentHashCodes() + { + var sv1 = new StringValues((string)null); + var sv2 = new StringValues(new[] { (string)null }); + Assert.AreNotEqual(sv1, sv2); + Assert.AreNotEqual(sv1.GetHashCode(), sv2.GetHashCode()); + + var sv3 = new StringValues((string[])null); + Assert.AreEqual(sv1, sv3); + Assert.AreEqual(sv1.GetHashCode(), sv3.GetHashCode()); + } + + [Test] + public void GetHashCode_SingleValueVsArrayWithTwoItems_DifferentHashCodes() + { + var sv1 = new StringValues("value"); + var sv2 = new StringValues(new[] { "value", "value" }); + Assert.AreNotEqual(sv1, sv2); + Assert.AreNotEqual(sv1.GetHashCode(), sv2.GetHashCode()); + } + + [Test] + public void ImplicitStringArrayConverter_Works() + { + string[] nullStringArray = null; + StringValues stringValues = nullStringArray; + Assert.That(stringValues, Is.EqualTo(StringValues.Empty)); + Assert.Null((string)stringValues); + Assert.Null((string[])stringValues); + + string aString = "abc"; + string[] aStringArray = new[] { aString }; + stringValues = aStringArray; + Assert.AreEqual(1, stringValues.Count); + Assert.AreEqual(aString, stringValues); + Assert.AreEqual(aString, stringValues[0]); + Assert.AreEqual(aString, ((IList)stringValues)[0]); + Assert.AreEqual(aStringArray, stringValues); + + aString = "abc"; + string bString = "bcd"; + aStringArray = new[] { aString, bString }; + stringValues = aStringArray; + Assert.AreEqual(2, stringValues.Count); + Assert.AreEqual("abc,bcd", stringValues.ToString()); + Assert.AreEqual(aStringArray, stringValues); + } + + [Theory] + [TestCaseSource(nameof(DefaultOrNullStringValues))] + [TestCaseSource(nameof(EmptyStringValues))] + public void DefaultNullOrEmpty_Enumerator(object stringValuesObj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + var e = stringValues.GetEnumerator(); + Assert.Null(e.Current); + Assert.False(e.MoveNext()); + Assert.Null(e.Current); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + + var e1 = ((IEnumerable)stringValues).GetEnumerator(); + Assert.Null(e1.Current); + Assert.False(e1.MoveNext()); + Assert.Null(e1.Current); + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + + var e2 = ((IEnumerable)stringValues).GetEnumerator(); + Assert.Null(e2.Current); + Assert.False(e2.MoveNext()); + Assert.Null(e2.Current); + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpected))] + public void Enumerator(object stringValuesObj, string[] expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + var e = stringValues.GetEnumerator(); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(e.MoveNext()); + Assert.AreEqual(expected[i], e.Current); + } + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + + var e1 = ((IEnumerable)stringValues).GetEnumerator(); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(e1.MoveNext()); + Assert.AreEqual(expected[i], e1.Current); + } + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + + var e2 = ((IEnumerable)stringValues).GetEnumerator(); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(e2.MoveNext()); + Assert.AreEqual(expected[i], e2.Current); + } + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + } + + [Test] + public void Indexer() + { + StringValues sv; + + // Default empty + sv = default; + Assert.Throws(() => { var x = sv[0]; }); + Assert.Throws(() => { var x = sv[-1]; }); + + // Empty with null string ctor + sv = new StringValues((string)null); + Assert.Throws(() => { var x = sv[0]; }); + Assert.Throws(() => { var x = sv[-1]; }); + // Empty with null string[] ctor + sv = new StringValues((string[])null); + Assert.Throws(() => { var x = sv[0]; }); + Assert.Throws(() => { var x = sv[-1]; }); + + // Empty with array + sv = Array.Empty(); + Assert.Throws(() => { var x = sv[0]; }); + Assert.Throws(() => { var x = sv[-1]; }); + + // One element with string + sv = "hello"; + Assert.AreEqual("hello", sv[0]); + Assert.Throws(() => { var x = sv[1]; }); + Assert.Throws(() => { var x = sv[-1]; }); + + // One element with string[] + sv = new string[] { "hello" }; + Assert.AreEqual("hello", sv[0]); + Assert.Throws(() => { var x = sv[1]; }); + Assert.Throws(() => { var x = sv[-1]; }); + + // One element with string[] containing null + sv = new string[] { null }; + Assert.Null(sv[0]); + Assert.Throws(() => { var x = sv[1]; }); + Assert.Throws(() => { var x = sv[-1]; }); + + // Two elements with string[] + sv = new string[] { "hello", "world" }; + Assert.AreEqual("hello", sv[0]); + Assert.AreEqual("world", sv[1]); + Assert.Throws(() => { var x = sv[2]; }); + Assert.Throws(() => { var x = sv[-1]; }); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpected))] + public void IndexOf(object stringValuesObj, string[] expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + IList list = stringValues; + Assert.AreEqual(-1, list.IndexOf("not there")); + for (int i = 0; i < expected.Length; i++) + { + Assert.AreEqual(i, list.IndexOf(expected[i])); + } + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpected))] + public void Contains(object stringValuesObj, string[] expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + + ICollection collection = stringValues; + Assert.False(collection.Contains("not there")); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(collection.Contains(expected[i])); + } + + } + + private static StringValues ImplicitlyCastStringValues(object stringValuesObj) + { + StringValues stringValues = (string)null; + if (stringValuesObj is StringValues strVal) + { + stringValues = strVal; + } + if (stringValuesObj is string[] strArr) + { + stringValues = strArr; + } + if (stringValuesObj is string str) + { + stringValues = str; + } + + return stringValues; + } + + [Theory] + [TestCaseSource(nameof(DefaultOrNullStringValues))] + [TestCaseSource(nameof(EmptyStringValues))] + [TestCaseSource(nameof(FilledStringValues))] + public void CopyTo_TooSmall(object stringValuesObj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + ICollection collection = stringValues; + string[] tooSmall = new string[0]; + + if (collection.Count > 0) + { + Assert.Throws(() => collection.CopyTo(tooSmall, 0)); + } + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpected))] + public void CopyTo_CorrectSize(object stringValuesObj, string[] expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + ICollection collection = stringValues; + string[] actual = new string[expected.Length]; + + if (collection.Count > 0) + { + Assert.Throws(() => collection.CopyTo(actual, -1)); + Assert.Throws(() => collection.CopyTo(actual, actual.Length + 1)); + } + collection.CopyTo(actual, 0); + Assert.AreEqual(expected, actual); + } + + [Theory] + [TestCaseSource(nameof(DefaultOrNullStringValues))] + [TestCaseSource(nameof(EmptyStringValues))] + public void DefaultNullOrEmpty_Concat(object stringValuesObj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + string[] expected = new[] { "abc", "bcd", "foo" }; + StringValues expectedStringValues = new StringValues(expected); + Assert.AreEqual(expected, StringValues.Concat(stringValues, expectedStringValues)); + Assert.AreEqual(expected, StringValues.Concat(expectedStringValues, stringValues)); + Assert.AreEqual(expected, StringValues.Concat((string)null, in expectedStringValues)); + Assert.AreEqual(expected, StringValues.Concat(in expectedStringValues, (string)null)); + + string[] empty = new string[0]; + StringValues emptyStringValues = new StringValues(empty); + Assert.AreEqual(empty, StringValues.Concat(stringValues, StringValues.Empty)); + Assert.AreEqual(empty, StringValues.Concat(StringValues.Empty, stringValues)); + Assert.AreEqual(empty, StringValues.Concat(stringValues, new StringValues())); + Assert.AreEqual(empty, StringValues.Concat(new StringValues(), stringValues)); + Assert.AreEqual(empty, StringValues.Concat((string)null, in emptyStringValues)); + Assert.AreEqual(empty, StringValues.Concat(in emptyStringValues, (string)null)); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpected))] + public void Concat(object stringValuesObj, string[] array) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + string[] filled = new[] { "abc", "bcd", "foo" }; + + string[] expectedPrepended = array.Concat(filled).ToArray(); + Assert.AreEqual(expectedPrepended, StringValues.Concat(stringValues, new StringValues(filled))); + + string[] expectedAppended = filled.Concat(array).ToArray(); + Assert.AreEqual(expectedAppended, StringValues.Concat(new StringValues(filled), stringValues)); + + StringValues values = stringValues; + foreach (string s in filled) + { + values = StringValues.Concat(in values, s); + } + Assert.AreEqual(expectedPrepended, values); + + values = stringValues; + foreach (string s in filled.Reverse()) + { + values = StringValues.Concat(s, in values); + } + Assert.AreEqual(expectedAppended, values); + } + + [Test] + public void Equals_OperatorEqual() + { + var equalString = "abc"; + + var equalStringArray = new string[] { equalString }; + var equalStringValues = new StringValues(equalString); + var otherStringValues = new StringValues(equalString); + var stringArray = new string[] { equalString, equalString }; + var stringValuesArray = new StringValues(stringArray); + + Assert.True(equalStringValues == otherStringValues); + + Assert.True(equalStringValues == equalString); + Assert.True(equalString == equalStringValues); + + Assert.True(equalStringValues == equalStringArray); + Assert.True(equalStringArray == equalStringValues); + + Assert.True(stringArray == stringValuesArray); + Assert.True(stringValuesArray == stringArray); + + Assert.False(stringValuesArray == equalString); + Assert.False(stringValuesArray == equalStringArray); + Assert.False(stringValuesArray == equalStringValues); + } + + [Test] + public void Equals_OperatorNotEqual() + { + var equalString = "abc"; + + var equalStringArray = new string[] { equalString }; + var equalStringValues = new StringValues(equalString); + var otherStringValues = new StringValues(equalString); + var stringArray = new string[] { equalString, equalString }; + var stringValuesArray = new StringValues(stringArray); + + Assert.False(equalStringValues != otherStringValues); + + Assert.False(equalStringValues != equalString); + Assert.False(equalString != equalStringValues); + + Assert.False(equalStringValues != equalStringArray); + Assert.False(equalStringArray != equalStringValues); + + Assert.False(stringArray != stringValuesArray); + Assert.False(stringValuesArray != stringArray); + + Assert.True(stringValuesArray != equalString); + Assert.True(stringValuesArray != equalStringArray); + Assert.True(stringValuesArray != equalStringValues); + } + + [Test] + public void Equals_Instance() + { + var equalString = "abc"; + + var equalStringArray = new string[] { equalString }; + var equalStringValues = new StringValues(equalString); + var stringArray = new string[] { equalString, equalString }; + var stringValuesArray = new StringValues(stringArray); + + Assert.True(equalStringValues.Equals(equalStringValues)); + Assert.True(equalStringValues.Equals(equalString)); + Assert.True(equalStringValues.Equals(equalStringArray)); + Assert.True(stringValuesArray.Equals(stringArray)); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpectedObjects))] + public void Equals_ObjectEquals(object stringValuesObj, object obj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + Assert.True(stringValues == obj); + Assert.True(obj == stringValues); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpectedObjects))] + public void Equals_ObjectNotEquals(object stringValuesObj, object obj) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + Assert.False(stringValues != obj); + Assert.False(obj != stringValues); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpectedStrings))] + public void Equals_String(object stringValuesObj, string expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + var notEqual = new StringValues("bcd"); + + Assert.True(StringValues.Equals(stringValues, expected)); + Assert.False(StringValues.Equals(stringValues, notEqual)); + + Assert.True(StringValues.Equals(stringValues, new StringValues(expected))); + Assert.AreEqual(stringValues.GetHashCode(), new StringValues(expected).GetHashCode()); + } + + [Theory] + [TestCaseSource(nameof(FilledStringValuesWithExpected))] + public void Equals_StringArray(object stringValuesObj, string[] expected) + { + StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); + var notEqual = new StringValues(new[] { "bcd", "abc" }); + + Assert.True(StringValues.Equals(stringValues, expected)); + Assert.False(StringValues.Equals(stringValues, notEqual)); + + Assert.True(StringValues.Equals(stringValues, new StringValues(expected))); + Assert.AreEqual(stringValues.GetHashCode(), new StringValues(expected).GetHashCode()); + } + } +} diff --git a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs index 81d011d065291..5a987f200e7a0 100644 --- a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs @@ -239,7 +239,7 @@ public async Task CanGetAndSetMethod(RequestMethod method, string expectedMethod [TestCaseSource(nameof(AllHeadersWithValuesAndType))] public async Task CanGetAndAddRequestHeaders(string headerName, string headerValue, bool contentHeader) { - StringValues httpHeaderValues; + Microsoft.Extensions.Primitives.StringValues httpHeaderValues; using TestServer testServer = new TestServer( context => @@ -298,7 +298,7 @@ public async Task CanAddMultipleValuesToRequestHeader(string headerName, string var anotherHeaderValue = headerValue + "1"; var joinedHeaderValues = headerValue + "," + anotherHeaderValue; - StringValues httpHeaderValues; + Microsoft.Extensions.Primitives.StringValues httpHeaderValues; using TestServer testServer = new TestServer( context => @@ -366,7 +366,7 @@ public async Task CanGetAndSetMultiValueResponseHeaders(string headerName, strin context => { context.Response.Headers.Add(headerName, - new StringValues(new[] + new Microsoft.Extensions.Primitives.StringValues(new[] { headerValue, anotherHeaderValue @@ -416,7 +416,7 @@ public async Task CanRemoveHeaders(string headerName, string headerValue, bool c public async Task CanSetRequestHeaders(string headerName, string headerValue, bool contentHeader) { - StringValues httpHeaderValues; + Microsoft.Extensions.Primitives.StringValues httpHeaderValues; using TestServer testServer = new TestServer( context => @@ -511,7 +511,7 @@ public async Task CanGetAndSetContentStream() [Test] public async Task ContentLength0WhenNoContent() { - StringValues contentLengthHeader = default; + Microsoft.Extensions.Primitives.StringValues contentLengthHeader = default; using TestServer testServer = new TestServer( context => { diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index e9e6e12240890..21d59ad14d175 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -8,10 +8,13 @@ $(RequiredTargetFrameworks) $(NoWarn);CA1812;CS1591 + true + + @@ -21,15 +24,39 @@ + + + + + + + + + + + + + + + + + + + - - + + + + + diff --git a/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs b/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs new file mode 100644 index 0000000000000..cd87b7a025b27 --- /dev/null +++ b/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +using System; +using Azure.Core; + +namespace Azure.Data.Tables +{ + internal static class MultipartContentExtensions + { + internal static MultipartContent AddChangeset(this MultipartContent batch) + { + var changeset = new MultipartContent("mixed", $"changeset_{Guid.NewGuid()}"); + batch.Add(changeset); + return changeset; + } + } +} diff --git a/sdk/tables/Azure.Data.Tables/src/TableClient.cs b/sdk/tables/Azure.Data.Tables/src/TableClient.cs index 49bb12b12ad74..64ce36d44641c 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableClient.cs @@ -903,6 +903,76 @@ public virtual Response SetAccessPolicy(IEnumerable tableAcl, } } + /// + /// Placeholder for batch operations. This is just being used for testing. + /// + /// + /// + /// + /// + internal virtual async Task>> BatchTestAsync(IEnumerable entities, CancellationToken cancellationToken = default) where T : class, ITableEntity, new() + { + using DiagnosticScope scope = _diagnostics.CreateScope($"{nameof(TableClient)}.{nameof(BatchTest)}"); + scope.Start(); + try + { + var batch = TableRestClient.CreateBatchContent(); + var changeset = batch.AddChangeset(); + foreach (var entity in entities) + { + _tableOperations.AddInsertEntityRequest( + changeset, + _table, + null, + null, + null, + tableEntityProperties: entity.ToOdataAnnotatedDictionary(), + queryOptions: new QueryOptions() { Format = _format }); + } + return await _tableOperations.SendBatchRequestAsync(_tableOperations.CreateBatchRequest(batch, null, null), cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + scope.Failed(ex); + throw; + } + } + + /// + /// Placeholder for batch operations. This is just being used for testing. + /// + /// + /// + /// + /// + internal virtual Response> BatchTest(IEnumerable entities, CancellationToken cancellationToken = default) where T : class, ITableEntity, new() + { + using DiagnosticScope scope = _diagnostics.CreateScope($"{nameof(TableClient)}.{nameof(BatchTest)}"); + scope.Start(); + try + { + var batch = TableRestClient.CreateBatchContent(); + var changeset = batch.AddChangeset(); + foreach (var entity in entities) + { + _tableOperations.AddInsertEntityRequest( + changeset, + _table, + null, + null, + null, + tableEntityProperties: entity.ToOdataAnnotatedDictionary(), + queryOptions: new QueryOptions() { Format = _format }); + } + return _tableOperations.SendBatchRequest(_tableOperations.CreateBatchRequest(batch, null, null), cancellationToken); + } + catch (Exception ex) + { + scope.Failed(ex); + throw; + } + } + /// /// Creates an Odata filter query string from the provided expression. /// diff --git a/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs b/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs new file mode 100644 index 0000000000000..052c07c803da8 --- /dev/null +++ b/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.Data.Tables.Models; + +namespace Azure.Data.Tables +{ + internal partial class TableRestClient + { + private const string CteHeaderName = "Content-Transfer-Encoding"; + private const string Binary = "binary"; + + internal HttpMessage CreateBatchRequest(MultipartContent content, string requestId, ResponseFormat? responsePreference) + { + var message = _pipeline.CreateMessage(); + var request = message.Request; + request.Method = RequestMethod.Post; + var uri = new RawRequestUriBuilder(); + uri.AppendRaw(url, false); + uri.AppendPath("/$batch", false); + request.Uri = uri; + request.Headers.Add("x-ms-version", version); + if (requestId != null) + { + request.Headers.Add("x-ms-client-request-id", requestId); + } + request.Headers.Add("DataServiceVersion", "3.0"); + if (responsePreference != null) + { + request.Headers.Add("Prefer", responsePreference.Value.ToString()); + } + //request.Headers.Add("Accept", "application/json"); + request.Content = content; + content.ApplyToRequest(request); + return message; + } + + internal static MultipartContent CreateBatchContent() + { + return new MultipartContent("mixed", $"batch_{Guid.NewGuid()}"); + } + + internal void AddInsertEntityRequest(MultipartContent changeset, string table, int? timeout, string requestId, ResponseFormat? responsePreference, IDictionary tableEntityProperties, QueryOptions queryOptions) + { + var message = CreateInsertEntityRequest(table, timeout, requestId, responsePreference, tableEntityProperties, queryOptions); + changeset.Add(new RequestContentContent(message.Request, new Dictionary { { CteHeaderName, Binary } })); + } + + /// Insert entity in a table. + /// + /// The cancellation token to use. + /// is null. + public async Task>> SendBatchRequestAsync(HttpMessage message, CancellationToken cancellationToken = default) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); + switch (message.Response.Status) + { + case 202: + { + var responses = await Multipart.ParseAsync( + message.Response.ContentStream, + message.Response.Headers.ContentType, + true, + cancellationToken).ConfigureAwait(false); + + return Response.FromValue(responses.ToList(), message.Response); + } + default: + throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); + } + } + + /// Insert entity in a table. + /// + /// The cancellation token to use. + /// is null. + public Response> SendBatchRequest(HttpMessage message, CancellationToken cancellationToken = default) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + _pipeline.Send(message, cancellationToken); + switch (message.Response.Status) + { + case 202: + { + var responses = Multipart.ParseAsync( + message.Response.ContentStream, + message.Response.Headers.ContentType, + false, + cancellationToken).EnsureCompleted(); + + return Response.FromValue(responses.ToList(), message.Response); + } + default: + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } + } + } +} diff --git a/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs b/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs index ff94e1137b5c0..37882986cfc0a 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs @@ -870,7 +870,6 @@ public async Task GetAccessPoliciesReturnsPolicies() await client.SetAccessPolicyAsync(tableAcl: policyToCreate); - // Get the created policy. var policies = await client.GetAccessPolicyAsync(); @@ -901,5 +900,30 @@ public async Task GetEntityReturnsSingleEntity() Assert.That(entityResults, Is.Not.Null, "The entity should not be null."); } + + /// + /// Validates the functionality of the TableClient. + /// + [Test] + [LiveOnly] + public async Task BatchInsert() + { + if (_endpointType == TableEndpointType.CosmosTable) + { + Assert.Ignore("https://github.com/Azure/azure-sdk-for-net/issues/14272"); + } + var entitiesToCreate = CreateCustomTableEntities(PartitionKeyValue, 20); + + // Create the new entities. + + var responses = await client.BatchTestAsync(entitiesToCreate).ConfigureAwait(false); + + foreach (var response in responses.Value) + { + Assert.That(response.Status, Is.EqualTo((int)HttpStatusCode.Created)); + } + Assert.That(responses.Value.Count, Is.EqualTo(entitiesToCreate.Count)); + } } + } From e8ff4c7cc62e209caa8e366b6053cc47c4ed57fa Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 16:00:54 -0500 Subject: [PATCH 04/14] remove RequestContent.Headers --- sdk/core/Azure.Core/src/RequestContent.cs | 5 -- .../Azure.Core/src/Shared/MultipartContent.cs | 13 +---- .../src/Shared/RequestContentContent.cs | 48 +++++-------------- .../tests/HttpPipelineFunctionalTests.cs | 8 ++-- .../src/MultipartContentExtensions.cs | 2 +- .../Azure.Data.Tables/src/TableRestClient.cs | 5 +- .../tests/TableClientLiveTests.cs | 4 +- 7 files changed, 23 insertions(+), 62 deletions(-) diff --git a/sdk/core/Azure.Core/src/RequestContent.cs b/sdk/core/Azure.Core/src/RequestContent.cs index 0e7d348679d05..100c5551560bd 100644 --- a/sdk/core/Azure.Core/src/RequestContent.cs +++ b/sdk/core/Azure.Core/src/RequestContent.cs @@ -78,11 +78,6 @@ public abstract class RequestContent : IDisposable /// public abstract void Dispose(); - /// - /// A collection of header values associated with this request content. - /// - public IDictionary? Headers { get; set;} = null; - private sealed class StreamContent : RequestContent { private const int CopyToBufferSize = 81920; diff --git a/sdk/core/Azure.Core/src/Shared/MultipartContent.cs b/sdk/core/Azure.Core/src/Shared/MultipartContent.cs index c3d7f216f1cc8..3604f62cb2a6f 100644 --- a/sdk/core/Azure.Core/src/Shared/MultipartContent.cs +++ b/sdk/core/Azure.Core/src/Shared/MultipartContent.cs @@ -29,6 +29,7 @@ internal class MultipartContent : RequestContent private readonly List _nestedContent; private readonly string _subtype; private readonly string _boundary; + internal readonly Dictionary _headers; #endregion Fields @@ -54,7 +55,7 @@ public MultipartContent(string subtype, string boundary) // see https://www.ietf.org/rfc/rfc1521.txt page 29. _boundary = boundary.Contains(":") ? $"\"{boundary}\"" : boundary; - Headers = new Dictionary + _headers = new Dictionary { [HttpHeader.Names.ContentType] = $"multipart/{_subtype}; boundary={_boundary}" }; @@ -145,16 +146,6 @@ private void AddInternal(RequestContent content, Dictionary head { headers = new Dictionary(); } - if (content.Headers != null) - { - foreach (var key in content.Headers.Keys) - { - if (!headers.ContainsKey(key)) - { - headers[key] = content.Headers[key]; - } - } - } _nestedContent.Add(new MultipartRequestContent(content, headers)); } diff --git a/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs b/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs index 9306b9bff00fe..d34c5d1eab63b 100644 --- a/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs +++ b/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs @@ -22,9 +22,8 @@ internal class RequestContentContent : RequestContent private const string ColonSP = ": "; private const string CRLF = "\r\n"; private const int DefaultHeaderAllocation = 2 * 1024; - private const string DefaultMediaType = "application/http"; - private readonly Request request; + private readonly Request _request; #endregion Fields @@ -34,33 +33,10 @@ internal class RequestContentContent : RequestContent /// Initializes a new instance of the class. /// /// The instance to encapsulate. - public RequestContentContent(Request request) : this(request, default) - { } - - - /// - /// Initializes a new instance of the class. - /// - /// The instance to encapsulate. - /// Additional headers to apply to the request content. - public RequestContentContent(Request request, Dictionary headers) + public RequestContentContent(Request request) { Argument.AssertNotNull(request, nameof(request)); - - Headers = new Dictionary - { - ["Content-Type"] = DefaultMediaType - }; - - if (headers != null) - { - foreach (var key in headers.Keys) - { - Headers[key] = headers[key]; - } - } - - this.request = request; + this._request = request; } #endregion Construction @@ -72,7 +48,7 @@ public RequestContentContent(Request request, Dictionary headers /// public override void Dispose() { - request.Dispose(); + _request.Dispose(); } #endregion Dispose @@ -86,9 +62,9 @@ public override async Task WriteToAsync(Stream stream, CancellationToken cancell byte[] header = SerializeHeader(); await stream.WriteAsync(header, 0, header.Length).ConfigureAwait(false); - if (request.Content != null) + if (_request.Content != null) { - await request.Content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); + await _request.Content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); } } @@ -99,9 +75,9 @@ public override void WriteTo(Stream stream, CancellationToken cancellationToken) byte[] header = SerializeHeader(); stream.Write(header, 0, header.Length); - if (request.Content != null) + if (_request.Content != null) { - request.Content.WriteTo(stream, cancellationToken); + _request.Content.WriteTo(stream, cancellationToken); } } @@ -121,13 +97,13 @@ public override bool TryComputeLength(out long length) // For #2, we return true & the size of our headers + the content length // For #3, we return true & the size of our headers - bool hasContent = request.Content != null; + bool hasContent = _request.Content != null; length = 0; // Cases #1, #2, #3 if (hasContent) { - if (!request!.Content!.TryComputeLength(out length)) + if (!_request!.Content!.TryComputeLength(out length)) { length = 0; return false; @@ -146,8 +122,8 @@ private byte[] SerializeHeader() { StringBuilder message = new StringBuilder(DefaultHeaderAllocation); - SerializeRequestLine(message, request); - SerializeHeaderFields(message, request.Headers); + SerializeRequestLine(message, _request); + SerializeHeaderFields(message, _request.Headers); message.Append(CRLF); return Encoding.UTF8.GetBytes(message.ToString()); } diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index 3d0d38acb6519..58108a7a86c8e 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -471,7 +471,7 @@ public async Task SendMultipartData() Guid changesetGuid = Guid.NewGuid(); var changeset = new MultipartContent(Mixed, $"changeset_{changesetGuid}"); - content.Add(changeset); + content.Add(changeset, changeset._headers); var postReq1 = httpPipeline.CreateMessage().Request; postReq1.Method = RequestMethod.Post; @@ -482,7 +482,7 @@ public async Task SendMultipartData() postReq1.Headers.Add(DataServiceVersion, Three0); const string post1Body = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"1\", \"Rating\":9, \"Text\":\"Azure...\"}"; postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post1Body)); - changeset.Add(new RequestContentContent(postReq1, new Dictionary { { cteHeaderName, Binary } })); + changeset.Add(new RequestContentContent(postReq1), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); var postReq2 = httpPipeline.CreateMessage().Request; postReq2.Method = RequestMethod.Post; @@ -492,7 +492,7 @@ public async Task SendMultipartData() postReq2.Headers.Add(DataServiceVersion, Three0); const string post2Body = "{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}"; postReq2.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); - changeset.Add(new RequestContentContent(postReq2, new Dictionary { { cteHeaderName, Binary } })); + changeset.Add(new RequestContentContent(postReq2), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); var patchReq = httpPipeline.CreateMessage().Request; patchReq.Method = RequestMethod.Patch; @@ -503,7 +503,7 @@ public async Task SendMultipartData() patchReq.Headers.Add(DataServiceVersion, Three0); const string patchBody = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"3\", \"Rating\":9, \"Text\":\"Azure Tables...\"}"; patchReq.Content = RequestContent.Create(Encoding.UTF8.GetBytes(patchBody)); - changeset.Add(new RequestContentContent(patchReq, new Dictionary { { cteHeaderName, Binary } })); + changeset.Add(new RequestContentContent(patchReq), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); request.Content = content; using Response response = await ExecuteRequest(request, httpPipeline); diff --git a/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs b/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs index cd87b7a025b27..7db3698c60cd0 100644 --- a/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs +++ b/sdk/tables/Azure.Data.Tables/src/MultipartContentExtensions.cs @@ -13,7 +13,7 @@ internal static class MultipartContentExtensions internal static MultipartContent AddChangeset(this MultipartContent batch) { var changeset = new MultipartContent("mixed", $"changeset_{Guid.NewGuid()}"); - batch.Add(changeset); + batch.Add(changeset, changeset._headers); return changeset; } } diff --git a/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs b/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs index 052c07c803da8..e431d6b162e70 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs @@ -6,10 +6,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; using Azure.Core; using Azure.Core.Pipeline; using Azure.Data.Tables.Models; @@ -20,6 +18,7 @@ internal partial class TableRestClient { private const string CteHeaderName = "Content-Transfer-Encoding"; private const string Binary = "binary"; + private const string ApplicationHttp = "application/http"; internal HttpMessage CreateBatchRequest(MultipartContent content, string requestId, ResponseFormat? responsePreference) { @@ -54,7 +53,7 @@ internal static MultipartContent CreateBatchContent() internal void AddInsertEntityRequest(MultipartContent changeset, string table, int? timeout, string requestId, ResponseFormat? responsePreference, IDictionary tableEntityProperties, QueryOptions queryOptions) { var message = CreateInsertEntityRequest(table, timeout, requestId, responsePreference, tableEntityProperties, queryOptions); - changeset.Add(new RequestContentContent(message.Request, new Dictionary { { CteHeaderName, Binary } })); + changeset.Add(new RequestContentContent(message.Request), new Dictionary { { HttpHeader.Names.ContentType, ApplicationHttp }, { CteHeaderName, Binary } }); } /// Insert entity in a table. diff --git a/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs b/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs index 37882986cfc0a..1b737bd5ecf9e 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs @@ -23,7 +23,7 @@ namespace Azure.Data.Tables.Tests public class TableClientLiveTests : TableServiceLiveTestsBase { - public TableClientLiveTests(bool isAsync, TableEndpointType endpointType) : base(isAsync, endpointType /* To record tests, add this argument, RecordedTestMode.Record */) + public TableClientLiveTests(bool isAsync, TableEndpointType endpointType) : base(isAsync, endpointType, RecordedTestMode.Live /* To record tests, add this argument, RecordedTestMode.Record */) { } /// @@ -905,7 +905,7 @@ public async Task GetEntityReturnsSingleEntity() /// Validates the functionality of the TableClient. /// [Test] - [LiveOnly] + //[LiveOnly] public async Task BatchInsert() { if (_endpointType == TableEndpointType.CosmosTable) From e5499451879d4d63f66da32dff8786898b713ea9 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 16:04:00 -0500 Subject: [PATCH 05/14] remove test mode --- sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs b/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs index 1b737bd5ecf9e..37882986cfc0a 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TableClientLiveTests.cs @@ -23,7 +23,7 @@ namespace Azure.Data.Tables.Tests public class TableClientLiveTests : TableServiceLiveTestsBase { - public TableClientLiveTests(bool isAsync, TableEndpointType endpointType) : base(isAsync, endpointType, RecordedTestMode.Live /* To record tests, add this argument, RecordedTestMode.Record */) + public TableClientLiveTests(bool isAsync, TableEndpointType endpointType) : base(isAsync, endpointType /* To record tests, add this argument, RecordedTestMode.Record */) { } /// @@ -905,7 +905,7 @@ public async Task GetEntityReturnsSingleEntity() /// Validates the functionality of the TableClient. /// [Test] - //[LiveOnly] + [LiveOnly] public async Task BatchInsert() { if (_endpointType == TableEndpointType.CosmosTable) From d3db09d85a93d392d168b3468adb954664fc8faf Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 16:22:29 -0500 Subject: [PATCH 06/14] remove HashCodeCombiner --- .../api/Azure.Core.netstandard2.0.cs | 3 + .../Azure.Core/src/Shared/HashCodeCombiner.cs | 86 ------------------- ...entContent.cs => RequestRequestContent.cs} | 6 +- .../Azure.Core/src/Shared/StringValues.cs | 4 +- .../Azure.Core/tests/Azure.Core.Tests.csproj | 3 +- .../Azure.Core/tests/HashCodeCombinerTest.cs | 39 --------- .../tests/HttpPipelineFunctionalTests.cs | 6 +- .../src/Azure.Data.Tables.csproj | 4 +- .../Azure.Data.Tables/src/TableRestClient.cs | 2 +- 9 files changed, 15 insertions(+), 138 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs rename sdk/core/Azure.Core/src/Shared/{RequestContentContent.cs => RequestRequestContent.cs} (97%) delete mode 100644 sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index c0b60287caf04..9fe50a17e2e9b 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -216,15 +216,18 @@ public static partial class Names { public static string Accept { get { throw null; } } public static string Authorization { get { throw null; } } + public static string ContentDisposition { get { throw null; } } public static string ContentLength { get { throw null; } } public static string ContentType { get { throw null; } } public static string Date { get { throw null; } } public static string ETag { get { throw null; } } + public static string Host { get { throw null; } } public static string IfMatch { get { throw null; } } public static string IfModifiedSince { get { throw null; } } public static string IfNoneMatch { get { throw null; } } public static string IfUnmodifiedSince { get { throw null; } } public static string Range { get { throw null; } } + public static string Referer { get { throw null; } } public static string UserAgent { get { throw null; } } public static string XMsDate { get { throw null; } } public static string XMsRange { get { throw null; } } diff --git a/sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs b/sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs deleted file mode 100644 index ae89907b8ba06..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/HashCodeCombiner.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Copied from https://github.com/dotnet/aspnetcore/tree/master/src/Shared/HashCodeCombiner - -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace Azure.Core -{ - internal struct HashCodeCombiner - { - private long _combinedHash64; - - public int CombinedHash - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { return _combinedHash64.GetHashCode(); } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private HashCodeCombiner(long seed) - { - _combinedHash64 = seed; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(IEnumerable e) - { - if (e == null) - { - Add(0); - } - else - { - var count = 0; - foreach (object o in e) - { - Add(o); - count++; - } - Add(count); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator int(HashCodeCombiner self) - { - return self.CombinedHash; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(int i) - { - _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(string s) - { - var hashCode = (s != null) ? s.GetHashCode() : 0; - Add(hashCode); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(object o) - { - var hashCode = (o != null) ? o.GetHashCode() : 0; - Add(hashCode); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(TValue value, IEqualityComparer comparer) - { - var hashCode = value != null ? comparer.GetHashCode(value) : 0; - Add(hashCode); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HashCodeCombiner Start() - { - return new HashCodeCombiner(0x1505L); - } - } -} diff --git a/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs b/sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs similarity index 97% rename from sdk/core/Azure.Core/src/Shared/RequestContentContent.cs rename to sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs index d34c5d1eab63b..bcb935205c977 100644 --- a/sdk/core/Azure.Core/src/Shared/RequestContentContent.cs +++ b/sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs @@ -14,7 +14,7 @@ namespace Azure.Core /// /// Provides a container for content encoded using multipart/form-data MIME type. /// - internal class RequestContentContent : RequestContent + internal class RequestRequestContent : RequestContent { #region Fields @@ -30,10 +30,10 @@ internal class RequestContentContent : RequestContent #region Construction /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The instance to encapsulate. - public RequestContentContent(Request request) + public RequestRequestContent(Request request) { Argument.AssertNotNull(request, nameof(request)); this._request = request; diff --git a/sdk/core/Azure.Core/src/Shared/StringValues.cs b/sdk/core/Azure.Core/src/Shared/StringValues.cs index 7de47a52d790d..eb8aeaea4ace0 100644 --- a/sdk/core/Azure.Core/src/Shared/StringValues.cs +++ b/sdk/core/Azure.Core/src/Shared/StringValues.cs @@ -743,12 +743,12 @@ public override int GetHashCode() { return Unsafe.As(this[0])?.GetHashCode() ?? Count.GetHashCode(); } - var hcc = default(HashCodeCombiner); + var hcc = default(HashCodeBuilder); for (int i = 0; i < values.Length; i++) { hcc.Add(values[i]); } - return hcc.CombinedHash; + return hcc.ToHashCode(); } else { diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 5c8270ab15f90..4b191ef8c35af 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -27,7 +27,6 @@ - @@ -38,7 +37,7 @@ - + diff --git a/sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs b/sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs deleted file mode 100644 index c66c6e9934f67..0000000000000 --- a/sdk/core/Azure.Core/tests/HashCodeCombinerTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using NUnit.Framework; - -namespace Azure.Core.Tests -{ - public class HashCodeCombinerTest - { - [Test] - public void GivenTheSameInputs_ItProducesTheSameOutput() - { - var hashCode1 = new HashCodeCombiner(); - var hashCode2 = new HashCodeCombiner(); - - hashCode1.Add(42); - hashCode1.Add("foo"); - hashCode2.Add(42); - hashCode2.Add("foo"); - - Assert.That(hashCode1.CombinedHash, Is.EqualTo(hashCode2.CombinedHash)); - } - - [Test] - public void HashCode_Is_OrderSensitive() - { - var hashCode1 = HashCodeCombiner.Start(); - var hashCode2 = HashCodeCombiner.Start(); - - hashCode1.Add(42); - hashCode1.Add("foo"); - - hashCode2.Add("foo"); - hashCode2.Add(42); - - Assert.That(hashCode1.CombinedHash, Is.Not.EqualTo(hashCode2.CombinedHash)); - } - } -} diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index 58108a7a86c8e..88e52556d897b 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -482,7 +482,7 @@ public async Task SendMultipartData() postReq1.Headers.Add(DataServiceVersion, Three0); const string post1Body = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"1\", \"Rating\":9, \"Text\":\"Azure...\"}"; postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post1Body)); - changeset.Add(new RequestContentContent(postReq1), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); + changeset.Add(new RequestRequestContent(postReq1), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); var postReq2 = httpPipeline.CreateMessage().Request; postReq2.Method = RequestMethod.Post; @@ -492,7 +492,7 @@ public async Task SendMultipartData() postReq2.Headers.Add(DataServiceVersion, Three0); const string post2Body = "{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}"; postReq2.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); - changeset.Add(new RequestContentContent(postReq2), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); + changeset.Add(new RequestRequestContent(postReq2), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); var patchReq = httpPipeline.CreateMessage().Request; patchReq.Method = RequestMethod.Patch; @@ -503,7 +503,7 @@ public async Task SendMultipartData() patchReq.Headers.Add(DataServiceVersion, Three0); const string patchBody = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"3\", \"Rating\":9, \"Text\":\"Azure Tables...\"}"; patchReq.Content = RequestContent.Create(Encoding.UTF8.GetBytes(patchBody)); - changeset.Add(new RequestContentContent(patchReq), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); + changeset.Add(new RequestRequestContent(patchReq), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); request.Content = content; using Response response = await ExecuteRequest(request, httpPipeline); diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 21d59ad14d175..03580c8c1a36e 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -31,7 +31,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs b/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs index e431d6b162e70..a484f1abcc470 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableRestClient.cs @@ -53,7 +53,7 @@ internal static MultipartContent CreateBatchContent() internal void AddInsertEntityRequest(MultipartContent changeset, string table, int? timeout, string requestId, ResponseFormat? responsePreference, IDictionary tableEntityProperties, QueryOptions queryOptions) { var message = CreateInsertEntityRequest(table, timeout, requestId, responsePreference, tableEntityProperties, queryOptions); - changeset.Add(new RequestContentContent(message.Request), new Dictionary { { HttpHeader.Names.ContentType, ApplicationHttp }, { CteHeaderName, Binary } }); + changeset.Add(new RequestRequestContent(message.Request), new Dictionary { { HttpHeader.Names.ContentType, ApplicationHttp }, { CteHeaderName, Binary } }); } /// Insert entity in a table. From cc35ee6bf5fe3fd79865a0482c71fcfa8bce1d2a Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 16:44:46 -0500 Subject: [PATCH 07/14] remove StringSegment and StringTokenizer --- .../Azure.Core/src/Shared/StringSegment.cs | 770 ------------ .../Azure.Core/src/Shared/StringTokenizer.cs | 129 -- .../Azure.Core/tests/Azure.Core.Tests.csproj | 2 - .../Azure.Core/tests/StringSegmentTest.cs | 1092 ----------------- .../Azure.Core/tests/StringTokenizerTest.cs | 78 -- .../src/Azure.Data.Tables.csproj | 2 - 6 files changed, 2073 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Shared/StringSegment.cs delete mode 100644 sdk/core/Azure.Core/src/Shared/StringTokenizer.cs delete mode 100644 sdk/core/Azure.Core/tests/StringSegmentTest.cs delete mode 100644 sdk/core/Azure.Core/tests/StringTokenizerTest.cs diff --git a/sdk/core/Azure.Core/src/Shared/StringSegment.cs b/sdk/core/Azure.Core/src/Shared/StringSegment.cs deleted file mode 100644 index 8c521b5d4f6d4..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/StringSegment.cs +++ /dev/null @@ -1,770 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Primitives/src - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -#pragma warning disable CA1307 // Equals Locale -#pragma warning disable IDE0041 // Null check can be simplified -#nullable disable - -namespace Azure.Core -{ - /// - /// An optimized representation of a substring. - /// - internal readonly struct StringSegment : IEquatable, IEquatable - { - /// - /// A for . - /// - public static readonly StringSegment Empty = string.Empty; - - /// - /// Initializes an instance of the struct. - /// - /// - /// The original . The includes the whole . - /// - public StringSegment(string buffer) - { - Buffer = buffer; - Offset = 0; - Length = buffer?.Length ?? 0; - } - - /// - /// Initializes an instance of the struct. - /// - /// The original used as buffer. - /// The offset of the segment within the . - /// The length of the segment. - /// - /// is . - /// - /// - /// or is less than zero, or + - /// is greater than the number of characters in . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public StringSegment(string buffer, int offset, int length) - { - // Validate arguments, check is minimal instructions with reduced branching for inlinable fast-path - // Negative values discovered though conversion to high values when converted to unsigned - // Failure should be rare and location determination and message is delegated to failure functions - if (buffer == null || (uint)offset > (uint)buffer.Length || (uint)length > (uint)(buffer.Length - offset)) - { - ThrowInvalidArguments(buffer, offset, length); - } - - Buffer = buffer; - Offset = offset; - Length = length; - } - - /// - /// Gets the buffer for this . - /// - public string Buffer { get; } - - /// - /// Gets the offset within the buffer for this . - /// - public int Offset { get; } - - /// - /// Gets the length of this . - /// - public int Length { get; } - - /// - /// Gets the value of this segment as a . - /// - public string Value - { - get - { - if (HasValue) - { - return Buffer.Substring(Offset, Length); - } - else - { - return null; - } - } - } - - /// - /// Gets whether this contains a valid value. - /// - public bool HasValue - { - get { return Buffer != null; } - } - - /// - /// Gets the at a specified position in the current . - /// - /// The offset into the - /// The at a specified position. - /// - /// is greater than or equal to or less than zero. - /// - public char this[int index] - { - get - { - if ((uint)index >= (uint)Length) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index); - } - - return Buffer[Offset + index]; - } - } - - /// - /// Gets a from the current . - /// - /// The from this . - public ReadOnlySpan AsSpan() => Buffer.AsSpan(Offset, Length); - - /// - /// Gets a from the current . - /// - /// The from this . - public ReadOnlyMemory AsMemory() => Buffer.AsMemory(Offset, Length); - - /// - /// Compares substrings of two specified objects using the specified rules, - /// and returns an integer that indicates their relative position in the sort order. - /// - /// The first to compare. - /// The second to compare. - /// One of the enumeration values that specifies the rules for the comparison. - /// - /// A 32-bit signed integer indicating the lexical relationship between the two comparands. - /// The value is negative if is less than , 0 if the two comparands are equal, - /// and positive if is greater than . - /// - public static int Compare(StringSegment a, StringSegment b, StringComparison comparisonType) - { - int minLength = Math.Min(a.Length, b.Length); - int diff = string.Compare(a.Buffer, a.Offset, b.Buffer, b.Offset, minLength, comparisonType); - if (diff == 0) - { - diff = a.Length - b.Length; - } - - return diff; - } - - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is StringSegment segment && Equals(segment); - } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// if the current object is equal to the other parameter; otherwise, . - public bool Equals(StringSegment other) => Equals(other, StringComparison.Ordinal); - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// One of the enumeration values that specifies the rules to use in the comparison. - /// if the current object is equal to the other parameter; otherwise, . - public bool Equals(StringSegment other, StringComparison comparisonType) - { - if (Length != other.Length) - { - return false; - } - - return string.Compare(Buffer, Offset, other.Buffer, other.Offset, other.Length, comparisonType) == 0; - } - - // This handles StringSegment.Equals(string, StringSegment, StringComparison) and StringSegment.Equals(StringSegment, string, StringComparison) - // via the implicit type converter - /// - /// Determines whether two specified objects have the same value. A parameter specifies the culture, case, and - /// sort rules used in the comparison. - /// - /// The first to compare. - /// The second to compare. - /// One of the enumeration values that specifies the rules for the comparison. - /// if the objects are equal; otherwise, . - public static bool Equals(StringSegment a, StringSegment b, StringComparison comparisonType) - { - return a.Equals(b, comparisonType); - } - - /// - /// Checks if the specified is equal to the current . - /// - /// The to compare with the current . - /// if the specified is equal to the current ; otherwise, . - public bool Equals(string text) - { - return Equals(text, StringComparison.Ordinal); - } - - /// - /// Checks if the specified is equal to the current . - /// - /// The to compare with the current . - /// One of the enumeration values that specifies the rules to use in the comparison. - /// if the specified is equal to the current ; otherwise, . - /// - /// is . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(string text, StringComparison comparisonType) - { - if (text == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); - } - - int textLength = text.Length; - if (!HasValue || Length != textLength) - { - return false; - } - - return string.Compare(Buffer, Offset, text, 0, textLength, comparisonType) == 0; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() - { -#if (NETCOREAPP2_1 || NETSTANDARD2_0 || NETFRAMEWORK) - // This GetHashCode is expensive since it allocates on every call. - // However this is required to ensure we retain any behavior (such as hash code randomization) that - // string.GetHashCode has. - return Value?.GetHashCode() ?? 0; -#elif NETCOREAPP || NETSTANDARD2_1 - return string.GetHashCode(AsSpan()); -#else -#error Target frameworks need to be updated. -#endif - - } - - /// - /// Checks if two specified have the same value. - /// - /// The first to compare, or . - /// The second to compare, or . - /// if the value of is the same as the value of ; otherwise, . - public static bool operator ==(StringSegment left, StringSegment right) => left.Equals(right); - - /// - /// Checks if two specified have different values. - /// - /// The first to compare, or . - /// The second to compare, or . - /// if the value of is different from the value of ; otherwise, . - public static bool operator !=(StringSegment left, StringSegment right) => !left.Equals(right); - - // PERF: Do NOT add a implicit converter from StringSegment to String. That would negate most of the perf safety. - /// - /// Creates a new from the given . - /// - /// The to convert to a - public static implicit operator StringSegment(string value) => new StringSegment(value); - - /// - /// Creates a see from the given . - /// - /// The to convert to a . - public static implicit operator ReadOnlySpan(StringSegment segment) => segment.AsSpan(); - - /// - /// Creates a see from the given . - /// - /// The to convert to a . - public static implicit operator ReadOnlyMemory(StringSegment segment) => segment.AsMemory(); - - /// - /// Checks if the beginning of this matches the specified when compared using the specified . - /// - /// The to compare. - /// One of the enumeration values that specifies the rules to use in the comparison. - /// if matches the beginning of this ; otherwise, . - /// - /// is . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool StartsWith(string text, StringComparison comparisonType) - { - if (text == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); - } - - bool result = false; - int textLength = text.Length; - - if (HasValue && Length >= textLength) - { - result = string.Compare(Buffer, Offset, text, 0, textLength, comparisonType) == 0; - } - - return result; - } - - /// - /// Checks if the end of this matches the specified when compared using the specified . - /// - /// The to compare. - /// One of the enumeration values that specifies the rules to use in the comparison. - /// if matches the end of this ; otherwise, . - /// - /// is . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool EndsWith(string text, StringComparison comparisonType) - { - if (text == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); - } - - bool result = false; - int textLength = text.Length; - int comparisonLength = Offset + Length - textLength; - - if (HasValue && comparisonLength > 0) - { - result = string.Compare(Buffer, comparisonLength, text, 0, textLength, comparisonType) == 0; - } - - return result; - } - - /// - /// Retrieves a substring from this . - /// The substring starts at the position specified by and has the remaining length. - /// - /// The zero-based starting character position of a substring in this . - /// A that is equivalent to the substring of remaining length that begins at - /// in this - /// - /// is greater than or equal to or less than zero. - /// - public string Substring(int offset) => Substring(offset, Length - offset); - - /// - /// Retrieves a substring from this . - /// The substring starts at the position specified by and has the specified . - /// - /// The zero-based starting character position of a substring in this . - /// The number of characters in the substring. - /// A that is equivalent to the substring of length that begins at - /// in this - /// - /// or is less than zero, or + is - /// greater than . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string Substring(int offset, int length) - { - if (!HasValue || offset < 0 || length < 0 || (uint)(offset + length) > (uint)Length) - { - ThrowInvalidArguments(offset, length); - } - - return Buffer.Substring(Offset + offset, length); - } - - /// - /// Retrieves a that represents a substring from this . - /// The starts at the position specified by . - /// - /// The zero-based starting character position of a substring in this . - /// A that begins at in this - /// whose length is the remainder. - /// - /// is greater than or equal to or less than zero. - /// - public StringSegment Subsegment(int offset) => Subsegment(offset, Length - offset); - - /// - /// Retrieves a that represents a substring from this . - /// The starts at the position specified by and has the specified . - /// - /// The zero-based starting character position of a substring in this . - /// The number of characters in the substring. - /// A that is equivalent to the substring of length that begins at in this - /// - /// or is less than zero, or + is - /// greater than . - /// - public StringSegment Subsegment(int offset, int length) - { - if (!HasValue || offset < 0 || length < 0 || (uint)(offset + length) > (uint)Length) - { - ThrowInvalidArguments(offset, length); - } - - return new StringSegment(Buffer, Offset + offset, length); - } - - /// - /// Gets the zero-based index of the first occurrence of the character in this . - /// The search starts at and examines a specified number of character positions. - /// - /// The Unicode character to seek. - /// The zero-based index position at which the search starts. - /// The number of characters to examine. - /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. - /// - /// or is less than zero, or + is - /// greater than . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int IndexOf(char c, int start, int count) - { - int offset = Offset + start; - - if (!HasValue || start < 0 || (uint)offset > (uint)Buffer.Length) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); - } - - if (count < 0) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); - } - - int index = Buffer.IndexOf(c, offset, count); - if (index != -1) - { - index -= Offset; - } - - return index; - } - - /// - /// Gets the zero-based index of the first occurrence of the character in this . - /// The search starts at . - /// - /// The Unicode character to seek. - /// The zero-based index position at which the search starts. - /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. - /// - /// is greater than or equal to or less than zero. - /// - public int IndexOf(char c, int start) => IndexOf(c, start, Length - start); - - /// - /// Gets the zero-based index of the first occurrence of the character in this . - /// - /// The Unicode character to seek. - /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. - public int IndexOf(char c) => IndexOf(c, 0, Length); - - /// - /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array - /// of Unicode characters. The search starts at a specified character position and examines a specified number - /// of character positions. - /// - /// A Unicode character array containing one or more characters to seek. - /// The search starting position. - /// The number of character positions to examine. - /// The zero-based index position of the first occurrence in this instance where any character in - /// was found; -1 if no character in was found. - /// - /// is . - /// - /// - /// or is less than zero, or + is - /// greater than . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int IndexOfAny(char[] anyOf, int startIndex, int count) - { - int index = -1; - - if (HasValue) - { - if (startIndex < 0 || Offset + startIndex > Buffer.Length) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); - } - - if (count < 0 || Offset + startIndex + count > Buffer.Length) - { - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); - } - - index = Buffer.IndexOfAny(anyOf, Offset + startIndex, count); - if (index != -1) - { - index -= Offset; - } - } - - return index; - } - - /// - /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array - /// of Unicode characters. The search starts at a specified character position. - /// - /// A Unicode character array containing one or more characters to seek. - /// The search starting position. - /// The zero-based index position of the first occurrence in this instance where any character in - /// was found; -1 if no character in was found. - /// - /// is greater than or equal to or less than zero. - /// - public int IndexOfAny(char[] anyOf, int startIndex) - { - return IndexOfAny(anyOf, startIndex, Length - startIndex); - } - - /// - /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array - /// of Unicode characters. - /// - /// A Unicode character array containing one or more characters to seek. - /// The zero-based index position of the first occurrence in this instance where any character in - /// was found; -1 if no character in was found. - public int IndexOfAny(char[] anyOf) - { - return IndexOfAny(anyOf, 0, Length); - } - - /// - /// Reports the zero-based index position of the last occurrence of a specified Unicode character within this instance. - /// - /// The Unicode character to seek. - /// The zero-based index position of value if that character is found, or -1 if it is not. - public int LastIndexOf(char value) - { - int index = -1; - - if (HasValue) - { - index = Buffer.LastIndexOf(value, Offset + Length - 1, Length); - if (index != -1) - { - index -= Offset; - } - } - - return index; - } - - /// - /// Removes all leading and trailing whitespaces. - /// - /// The trimmed . - public StringSegment Trim() => TrimStart().TrimEnd(); - - /// - /// Removes all leading whitespaces. - /// - /// The trimmed . - public unsafe StringSegment TrimStart() - { - int trimmedStart = Offset; - int length = Offset + Length; - - fixed (char* p = Buffer) - { - while (trimmedStart < length) - { - char c = p[trimmedStart]; - - if (!char.IsWhiteSpace(c)) - { - break; - } - - trimmedStart++; - } - } - - return new StringSegment(Buffer, trimmedStart, length - trimmedStart); - } - - /// - /// Removes all trailing whitespaces. - /// - /// The trimmed . - public unsafe StringSegment TrimEnd() - { - int offset = Offset; - int trimmedEnd = offset + Length - 1; - - fixed (char* p = Buffer) - { - while (trimmedEnd >= offset) - { - char c = p[trimmedEnd]; - - if (!char.IsWhiteSpace(c)) - { - break; - } - - trimmedEnd--; - } - } - - return new StringSegment(Buffer, offset, trimmedEnd - offset + 1); - } - - /// - /// Splits a string into s that are based on the characters in an array. - /// - /// A character array that delimits the substrings in this string, an empty array that - /// contains no delimiters, or null. - /// An whose elements contain the s from this instance - /// that are delimited by one or more characters in . - public StringTokenizer Split(char[] chars) - { - return new StringTokenizer(this, chars); - } - - /// - /// Indicates whether the specified is null or an Empty string. - /// - /// The to test. - /// - public static bool IsNullOrEmpty(StringSegment value) - { - bool res = false; - - if (!value.HasValue || value.Length == 0) - { - res = true; - } - - return res; - } - - /// - /// Returns the represented by this or if the does not contain a value. - /// - /// The represented by this or if the does not contain a value. - public override string ToString() - { - return Value ?? string.Empty; - } - - // Methods that do no return (i.e. throw) are not inlined - // https://github.com/dotnet/coreclr/pull/6103 - private static void ThrowInvalidArguments(string buffer, int offset, int length) - { - // Only have single throw in method so is marked as "does not return" and isn't inlined to caller - throw GetInvalidArgumentsException(); - - Exception GetInvalidArgumentsException() - { - if (buffer == null) - { - return ThrowHelper.GetArgumentNullException(ExceptionArgument.buffer); - } - - if (offset < 0) - { - return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); - } - - if (length < 0) - { - return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.length); - } - - return ThrowHelper.GetArgumentException(ExceptionResource.Argument_InvalidOffsetLength); - } - } - - private void ThrowInvalidArguments(int offset, int length) - { - throw GetInvalidArgumentsException(HasValue); - - Exception GetInvalidArgumentsException(bool hasValue) - { - if (!hasValue) - { - return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); - } - - if (offset < 0) - { - return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); - } - - if (length < 0) - { - return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.length); - } - - return ThrowHelper.GetArgumentException(ExceptionResource.Argument_InvalidOffsetLengthStringSegment); - } - } - } - - internal class StringSegmentComparer : IComparer, IEqualityComparer - { - public static StringSegmentComparer Ordinal { get; } - = new StringSegmentComparer(StringComparison.Ordinal, StringComparer.Ordinal); - - public static StringSegmentComparer OrdinalIgnoreCase { get; } - = new StringSegmentComparer(StringComparison.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase); - - private StringSegmentComparer(StringComparison comparison, StringComparer comparer) - { - Comparison = comparison; - Comparer = comparer; - } - - private StringComparison Comparison { get; } - private StringComparer Comparer { get; } - - public int Compare(StringSegment x, StringSegment y) - { - return StringSegment.Compare(x, y, Comparison); - } - - public bool Equals(StringSegment x, StringSegment y) - { - return StringSegment.Equals(x, y, Comparison); - } - - public int GetHashCode(StringSegment obj) - { -#if (NETCOREAPP2_1 || NETSTANDARD2_0 || NETFRAMEWORK) - if (!obj.HasValue) - { - return 0; - } - - // .NET Core strings use randomized hash codes for security reasons. Consequently we must materialize the StringSegment as a string - return Comparer.GetHashCode(obj.Value); -#elif NETCOREAPP || NETSTANDARD2_1 - return string.GetHashCode(obj.AsSpan(), Comparison); -#endif - } - } -} diff --git a/sdk/core/Azure.Core/src/Shared/StringTokenizer.cs b/sdk/core/Azure.Core/src/Shared/StringTokenizer.cs deleted file mode 100644 index c793c3d147b7e..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/StringTokenizer.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Primitives/src - -using System; -using System.Collections; -using System.Collections.Generic; - -#pragma warning disable IDE0034 // Default expression can be simplified -#nullable disable - -namespace Azure.Core -{ - /// - /// Tokenizes a into s. - /// - internal readonly struct StringTokenizer : IEnumerable - { - private readonly StringSegment _value; - private readonly char[] _separators; - - /// - /// Initializes a new instance of . - /// - /// The to tokenize. - /// The characters to tokenize by. - public StringTokenizer(string value, char[] separators) - { - if (value == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); - } - - if (separators == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.separators); - } - - _value = value; - _separators = separators; - } - - /// - /// Initializes a new instance of . - /// - /// The to tokenize. - /// The characters to tokenize by. - public StringTokenizer(StringSegment value, char[] separators) - { - if (!value.HasValue) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); - } - - if (separators == null) - { - ThrowHelper.ThrowArgumentNullException(ExceptionArgument.separators); - } - - _value = value; - _separators = separators; - } - - public Enumerator GetEnumerator() => new Enumerator(in _value, _separators); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public struct Enumerator : IEnumerator - { - private readonly StringSegment _value; - private readonly char[] _separators; - private int _index; - - internal Enumerator(in StringSegment value, char[] separators) - { - _value = value; - _separators = separators; - Current = default; - _index = 0; - } - - public Enumerator(ref StringTokenizer tokenizer) - { - _value = tokenizer._value; - _separators = tokenizer._separators; - Current = default(StringSegment); - _index = 0; - } - - public StringSegment Current { get; private set; } - - object IEnumerator.Current => Current; - - public void Dispose() - { - } - - public bool MoveNext() - { - if (!_value.HasValue || _index > _value.Length) - { - Current = default(StringSegment); - return false; - } - - int next = _value.IndexOfAny(_separators, _index); - if (next == -1) - { - // No separator found. Consume the remainder of the string. - next = _value.Length; - } - - Current = _value.Subsegment(_index, next - _index); - _index = next + 1; - - return true; - } - - public void Reset() - { - Current = default(StringSegment); - _index = 0; - } - } - } -} diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 4b191ef8c35af..be360a68b9271 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -39,8 +39,6 @@ - - diff --git a/sdk/core/Azure.Core/tests/StringSegmentTest.cs b/sdk/core/Azure.Core/tests/StringSegmentTest.cs deleted file mode 100644 index 335bebb9ca1ac..0000000000000 --- a/sdk/core/Azure.Core/tests/StringSegmentTest.cs +++ /dev/null @@ -1,1092 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using NUnit.Framework; - -namespace Azure.Core.Tests -{ - public class StringSegmentTest - { - [Test] - public void StringSegment_Empty() - { - // Arrange & Act - var segment = StringSegment.Empty; - - // Assert - Assert.True(segment.HasValue); - Assert.AreSame(string.Empty, segment.Value); - Assert.AreEqual(0, segment.Offset); - Assert.AreEqual(0, segment.Length); - } - - [Test] - public void StringSegment_ImplicitConvertFromString() - { - StringSegment segment = "Hello"; - - Assert.True(segment.HasValue); - Assert.AreEqual(0, segment.Offset); - Assert.AreEqual(5, segment.Length); - Assert.AreEqual("Hello", segment.Value); - } - - [Test] - public void StringSegment_AsSpan() - { - var segment = new StringSegment("Hello"); - - var span = segment.AsSpan(); - - Assert.AreEqual(5, span.Length); - } - - [Test] - public void StringSegment_ImplicitConvertToSpan() - { - ReadOnlySpan span = new StringSegment("Hello"); - - Assert.AreEqual(5, span.Length); - } - - [Test] - public void StringSegment_AsMemory() - { - var segment = new StringSegment("Hello"); - - var memory = segment.AsMemory(); - - Assert.AreEqual(5, memory.Length); - } - - [Test] - public void StringSegment_ImplicitConvertToMemory() - { - ReadOnlyMemory memory = new StringSegment("Hello"); - - Assert.AreEqual(5, memory.Length); - } - - [Test] - public void StringSegment_StringCtor_AllowsNullBuffers() - { - // Arrange & Act - var segment = new StringSegment(null); - - // Assert - Assert.False(segment.HasValue); - Assert.AreEqual(0, segment.Offset); - Assert.AreEqual(0, segment.Length); - } - - [Test] - public void StringSegmentConstructor_NullBuffer_Throws() - { - // Arrange, Act and Assert - var exception = Assert.Throws(() => new StringSegment(null, 0, 0)); - Assert.That(exception.Message.Contains("buffer")); - } - - [Test] - public void StringSegmentConstructor_NegativeOffset_Throws() - { - // Arrange, Act and Assert - var exception = Assert.Throws(() => new StringSegment("", -1, 0)); - Assert.That(exception.Message.Contains("offset")); - } - - [Test] - public void StringSegmentConstructor_NegativeLength_Throws() - { - // Arrange, Act and Assert - var exception = Assert.Throws(() => new StringSegment("", 0, -1)); - Assert.That(exception.Message.Contains("length")); - } - - [Test] - [TestCase(0, 10)] - [TestCase(10, 0)] - [TestCase(5, 5)] - [TestCase(int.MaxValue, int.MaxValue)] - public void StringSegmentConstructor_OffsetOrLengthOutOfBounds_Throws(int offset, int length) - { - // Arrange, Act and Assert - Assert.Throws(() => new StringSegment("lengthof9", offset, length)); - } - - [Test] - [TestCase("", 0, 0)] - [TestCase("abc", 2, 0)] - public void StringSegmentConstructor_AllowsEmptyBuffers(string text, int offset, int length) - { - // Arrange & Act - var segment = new StringSegment(text, offset, length); - - // Assert - Assert.True(segment.HasValue); - Assert.AreEqual(offset, segment.Offset); - Assert.AreEqual(length, segment.Length); - } - - [Test] - public void StringSegment_StringCtor_InitializesValuesCorrectly() - { - // Arrange - var buffer = "Hello world!"; - - // Act - var segment = new StringSegment(buffer); - - // Assert - Assert.True(segment.HasValue); - Assert.AreEqual(0, segment.Offset); - Assert.AreEqual(buffer.Length, segment.Length); - } - - [Test] - public void StringSegment_Value_Valid() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var value = segment.Value; - - // Assert - Assert.AreEqual("ello", value); - } - - [Test] - public void StringSegment_Value_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act - var value = segment.Value; - - // Assert - Assert.Null(value); - } - - [Test] - public void StringSegment_HasValue_Valid() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var hasValue = segment.HasValue; - - // Assert - Assert.True(hasValue); - } - - [Test] - public void StringSegment_HasValue_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act - var hasValue = segment.HasValue; - - // Assert - Assert.False(hasValue); - } - - [Test] - [TestCase("a", 0, 1, 0, 'a')] - [TestCase("abc", 1, 1, 0, 'b')] - [TestCase("abcdef", 1, 4, 0, 'b')] - [TestCase("abcdef", 1, 4, 1, 'c')] - [TestCase("abcdef", 1, 4, 2, 'd')] - [TestCase("abcdef", 1, 4, 3, 'e')] - public void StringSegment_Indexer_InRange(string value, int offset, int length, int index, char expected) - { - var segment = new StringSegment(value, offset, length); - - var result = segment[index]; - - Assert.AreEqual(expected, result); - } - - [Test] - [TestCase("", 0, 0, 0)] - [TestCase("a", 0, 1, -1)] - [TestCase("a", 0, 1, 1)] - public void StringSegment_Indexer_OutOfRangeThrows(string value, int offset, int length, int index) - { - var segment = new StringSegment(value, offset, length); - Assert.Throws(() => { var x = segment[index]; }); - } - - public static IEnumerable EndsWithData = new List - { - new object[] { "Hello", StringComparison.Ordinal, false }, - new object[] { "ello ", StringComparison.Ordinal, false }, - new object[] { "ll", StringComparison.Ordinal, false }, - new object[] { "ello", StringComparison.Ordinal, true }, - new object[] { "llo", StringComparison.Ordinal, true }, - new object[] { "lo", StringComparison.Ordinal, true }, - new object[] { "o", StringComparison.Ordinal, true }, - new object[] { string.Empty, StringComparison.Ordinal, true }, - new object[] { "eLLo", StringComparison.Ordinal, false }, - new object[] { "eLLo", StringComparison.OrdinalIgnoreCase, true }, - }; - - [Test] - [TestCaseSource(nameof(EndsWithData))] - public void StringSegment_EndsWith_Valid(string candidate, StringComparison comparison, bool expectedResult) - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.EndsWith(candidate, comparison); - - // Assert - Assert.AreEqual(expectedResult, result); - } - - [Test] - public void StringSegment_EndsWith_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act - var result = segment.EndsWith(string.Empty, StringComparison.Ordinal); - - // Assert - Assert.False(result); - } - - public static IEnumerable StartsWithData = new List - { - new object[] { "Hello", StringComparison.Ordinal, false }, - new object[] { "ello ", StringComparison.Ordinal, false }, - new object[] { "ll", StringComparison.Ordinal, false }, - new object[] { "ello", StringComparison.Ordinal, true }, - new object[] { "ell", StringComparison.Ordinal, true }, - new object[] { "el", StringComparison.Ordinal, true }, - new object[] { "e", StringComparison.Ordinal, true }, - new object[] { string.Empty, StringComparison.Ordinal, true }, - new object[] { "eLLo", StringComparison.Ordinal, false }, - new object[] { "eLLo", StringComparison.OrdinalIgnoreCase, true }, - }; - - [Test] - [TestCaseSource(nameof(StartsWithData))] - public void StringSegment_StartsWith_Valid(string candidate, StringComparison comparison, bool expectedResult) - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.StartsWith(candidate, comparison); - - // Assert - Assert.AreEqual(expectedResult, result); - } - - [Test] - public void StringSegment_StartsWith_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act - var result = segment.StartsWith(string.Empty, StringComparison.Ordinal); - - // Assert - Assert.False(result); - } - - public static IEnumerable EqualsStringData = new List - { - new object[] { "eLLo", StringComparison.OrdinalIgnoreCase, true }, - new object[] { "eLLo", StringComparison.Ordinal, false }, - }; - - [Test] - [TestCaseSource(nameof(EqualsStringData))] - public void StringSegment_Equals_String_Valid(string candidate, StringComparison comparison, bool expectedResult) - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.Equals(candidate, comparison); - - // Assert - Assert.AreEqual(expectedResult, result); - } - - [Test] - public void StringSegment_EqualsObject_Valid() - { - var segment1 = new StringSegment("My Car Is Cool", 3, 3); - var segment2 = new StringSegment("Your Carport is blue", 5, 3); - - Assert.True(segment1.Equals((object)segment2)); - } - - [Test] - public void StringSegment_EqualsNull_Invalid() - { - var segment1 = new StringSegment("My Car Is Cool", 3, 3); - - Assert.False(segment1.Equals(null as object)); - } - - [Test] - public void StringSegment_StaticEquals_Valid() - { - var segment1 = new StringSegment("My Car Is Cool", 3, 3); - var segment2 = new StringSegment("Your Carport is blue", 5, 3); - - Assert.True(StringSegment.Equals(segment1, segment2)); - } - - [Test] - public void StringSegment_StaticEquals_Invalid() - { - var segment1 = new StringSegment("My Car Is Cool", 3, 4); - var segment2 = new StringSegment("Your Carport is blue", 5, 4); - - Assert.False(StringSegment.Equals(segment1, segment2)); - } - - [Test] - public void StringSegment_IsNullOrEmpty_Valid() - { - Assert.True(StringSegment.IsNullOrEmpty(null)); - Assert.True(StringSegment.IsNullOrEmpty(string.Empty)); - Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(null))); - Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty))); - Assert.True(StringSegment.IsNullOrEmpty(StringSegment.Empty)); - Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty, 0, 0))); - Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 0, 0))); - Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 3, 0))); - } - - [Test] - public void StringSegment_IsNullOrEmpty_Invalid() - { - Assert.False(StringSegment.IsNullOrEmpty("A")); - Assert.False(StringSegment.IsNullOrEmpty("ABCDefg")); - Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("A", 0, 1))); - Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("ABCDefg", 3, 2))); - } - - public static IEnumerable GetHashCode_ReturnsSameValueForEqualSubstringsData = new List - { - new object[] { default(StringSegment), default(StringSegment) }, - new object[] { default(StringSegment), new StringSegment() }, - new object[] { new StringSegment("Test123", 0, 0), new StringSegment(string.Empty) }, - new object[] { new StringSegment("C`est si bon", 2, 3), new StringSegment("Yesterday", 1, 3) }, - new object[] { new StringSegment("Hello", 1, 4), new StringSegment("Hello world", 1, 4) }, - new object[] { new StringSegment("Hello"), new StringSegment("Hello", 0, 5) }, - }; - - [Test] - [TestCaseSource(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] - public void GetHashCode_ReturnsSameValueForEqualSubstrings(object segment1, object segment2) - { - // Act - var hashCode1 = segment1.GetHashCode(); - var hashCode2 = segment2.GetHashCode(); - - // Assert - Assert.AreEqual(hashCode1, hashCode2); - } - - public static string testString = "Test123"; - - public static IEnumerable GetHashCode_ReturnsDifferentValuesForInequalSubstringsData = new List - { - new object[] { new StringSegment(testString, 0, 1), new StringSegment(string.Empty) }, - new object[] { new StringSegment(testString, 0, 1), new StringSegment(testString, 1, 1) }, - new object[] { new StringSegment(testString, 1, 2), new StringSegment(testString, 1, 3) }, - new object[] { new StringSegment(testString, 0, 4), new StringSegment("TEST123", 0, 4) }, - }; - - [Test] - [TestCaseSource(nameof(GetHashCode_ReturnsDifferentValuesForInequalSubstringsData))] - public void GetHashCode_ReturnsDifferentValuesForInequalSubstrings( - object segment1, - object segment2) - { - // Act - var hashCode1 = segment1.GetHashCode(); - var hashCode2 = segment2.GetHashCode(); - - // Assert - Assert.AreNotEqual(hashCode1, hashCode2); - } - - [Test] - public void StringSegment_EqualsString_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act - var result = segment.Equals(string.Empty, StringComparison.Ordinal); - - // Assert - Assert.False(result); - } - - public static IEnumerable DefaultStringSegmentEqualsStringSegmentData = new List - { - new object[] { default(StringSegment) }, - new object[] { new StringSegment() }, - }; - - [Test] - [TestCaseSource(nameof(DefaultStringSegmentEqualsStringSegmentData))] - public void DefaultStringSegment_EqualsStringSegment(object candidate) - { - // Arrange - var segment = default(StringSegment); - - // Act - var result = segment.Equals((StringSegment)candidate, StringComparison.Ordinal); - - // Assert - Assert.True(result); - } - - public static IEnumerable DefaultStringSegmentDoesNotEqualStringSegmentData = new List - { - new object[] { new StringSegment("Hello, World!", 1, 4) }, - new object[] { new StringSegment("Hello", 1, 0) }, - new object[] { new StringSegment(string.Empty) }, - }; - - [Test] - [TestCaseSource(nameof(DefaultStringSegmentDoesNotEqualStringSegmentData))] - public void DefaultStringSegment_DoesNotEqualStringSegment(object candidate) - { - // Arrange - var segment = default(StringSegment); - - // Act - var result = segment.Equals((StringSegment)candidate, StringComparison.Ordinal); - - // Assert - Assert.False(result); - } - - public static IEnumerable DefaultStringSegmentDoesNotEqualStringData = new List - { - new object[] { string.Empty }, - new object[] { "Hello, World!" }, - }; - - [Test] - [TestCaseSource(nameof(DefaultStringSegmentDoesNotEqualStringData))] - public void DefaultStringSegment_DoesNotEqualString(string candidate) - { - // Arrange - var segment = default(StringSegment); - - // Act - var result = segment.Equals(candidate, StringComparison.Ordinal); - - // Assert - Assert.False(result); - } - - public static IEnumerable EqualsStringSegmentData = new List - { - new object[] { new StringSegment("Hello, World!", 1, 4), StringComparison.Ordinal, true }, - new object[] { new StringSegment("HELlo, World!", 1, 4), StringComparison.Ordinal, false }, - new object[] { new StringSegment("HELlo, World!", 1, 4), StringComparison.OrdinalIgnoreCase, true }, - new object[] { new StringSegment("ello, World!", 0, 4), StringComparison.Ordinal, true }, - new object[] { new StringSegment("ello, World!", 0, 3), StringComparison.Ordinal, false }, - new object[] { new StringSegment("ello, World!", 1, 3), StringComparison.Ordinal, false }, - }; - - [Test] - [TestCaseSource(nameof(EqualsStringSegmentData))] - public void StringSegment_Equals_StringSegment_Valid(object candidate, StringComparison comparison, bool expectedResult) - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.Equals((StringSegment)candidate, comparison); - - // Assert - Assert.AreEqual(expectedResult, result); - } - - [Test] - public void StringSegment_EqualsStringSegment_Invalid() - { - // Arrange - var segment = new StringSegment(); - var candidate = new StringSegment("Hello, World!", 3, 2); - - // Act - var result = segment.Equals(candidate, StringComparison.Ordinal); - - // Assert - Assert.False(result); - } - - [Test] - public void StringSegment_SubstringOffset_Valid() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.Substring(offset: 1); - - // Assert - Assert.AreEqual("llo", result); - } - - [Test] - public void StringSegment_Substring_Valid() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.Substring(offset: 1, length: 2); - - // Assert - Assert.AreEqual("ll", result); - } - - [Test] - public void StringSegment_Substring_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act & Assert - Assert.Throws(() => segment.Substring(0, 0)); - } - - [Test] - public void StringSegment_Substring_InvalidOffset() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Substring(-1, 1)); - Assert.AreEqual("offset", exception.ParamName); - } - - [Test] - public void StringSegment_Substring_InvalidLength() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Substring(0, -1)); - Assert.AreEqual("length", exception.ParamName); - } - - [Test] - public void StringSegment_Substring_InvalidOffsetAndLength() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Substring(2, 3)); - Assert.That(exception.Message.Contains("bounds")); - } - - [Test] - public void StringSegment_Substring_OffsetAndLengthOverflows() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Substring(1, int.MaxValue)); - Assert.That(exception.Message.Contains("bounds")); - } - - [Test] - public void StringSegment_SubsegmentOffset_Valid() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.Subsegment(offset: 1); - - // Assert - Assert.AreEqual(new StringSegment("Hello, World!", 2, 3), result); - Assert.AreEqual("llo", result.Value); - } - - [Test] - public void StringSegment_Subsegment_Valid() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 4); - - // Act - var result = segment.Subsegment(offset: 1, length: 2); - - // Assert - Assert.AreEqual(new StringSegment("Hello, World!", 2, 2), result); - Assert.AreEqual("ll", result.Value); - } - - [Test] - public void StringSegment_Subsegment_Invalid() - { - // Arrange - var segment = new StringSegment(); - - // Act & Assert - Assert.Throws(() => segment.Subsegment(0, 0)); - } - - [Test] - public void StringSegment_Subsegment_InvalidOffset() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Subsegment(-1, 1)); - Assert.AreEqual("offset", exception.ParamName); - } - - [Test] - public void StringSegment_Subsegment_InvalidLength() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Subsegment(0, -1)); - Assert.AreEqual("length", exception.ParamName); - } - - [Test] - public void StringSegment_Subsegment_InvalidOffsetAndLength() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Subsegment(2, 3)); - Assert.That(exception.Message.Contains("bounds")); - } - - [Test] - public void StringSegment_Subsegment_OffsetAndLengthOverflows() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.Subsegment(1, int.MaxValue)); - Assert.That(exception.Message.Contains("bounds")); - } - - public static IEnumerable CompareLesserData = new List - { - new object[] { new StringSegment("abcdef", 1, 4), StringSegmentComparer.Ordinal }, - new object[] { new StringSegment("abcdef", 1, 5), StringSegmentComparer.OrdinalIgnoreCase }, - new object[] { new StringSegment("ABCDEF", 2, 2), StringSegmentComparer.OrdinalIgnoreCase }, - }; - - [Test] - [TestCaseSource(nameof(CompareLesserData))] - public void StringSegment_Compare_Lesser(object candidate, object comparer) - { - // Arrange - var segment = new StringSegment("ABCDEF", 1, 4); - - // Act - var result = ((StringSegmentComparer)comparer).Compare(segment, (StringSegment)candidate); - - // Assert - Assert.True(result < 0, $"{segment} should be less than {candidate}"); - } - - public static IEnumerable CompareEqualData = new List - { - new object[] { new StringSegment("abcdef", 1, 4), StringSegmentComparer.Ordinal }, - new object[] { new StringSegment("ABCDEF", 1, 4), StringSegmentComparer.OrdinalIgnoreCase }, - new object[] { new StringSegment("bcde", 0, 4), StringSegmentComparer.Ordinal }, - new object[] { new StringSegment("BcDeF", 0, 4), StringSegmentComparer.OrdinalIgnoreCase }, - }; - - [Test] - [TestCaseSource(nameof(CompareEqualData))] - public void StringSegment_Compare_Equal(object candidate, object comparer) - { - // Arrange - var segment = new StringSegment("abcdef", 1, 4); - - // Act - var result = ((StringSegmentComparer)comparer).Compare(segment, (StringSegment)candidate); - - // Assert - Assert.True(result == 0, $"{segment} should equal {candidate}"); - } - - public static IEnumerable CompareGreaterData = new List - { - new object[] { new StringSegment("ABCDEF", 1, 4), StringSegmentComparer.Ordinal }, - new object[] { new StringSegment("ABCDEF", 0, 6), StringSegmentComparer.OrdinalIgnoreCase }, - new object[] { new StringSegment("abcdef", 0, 3), StringSegmentComparer.Ordinal }, - }; - - [Test] - [TestCaseSource(nameof(CompareGreaterData))] - public void StringSegment_Compare_Greater(object candidate, object comparer) - { - // Arrange - var segment = new StringSegment("abcdef", 1, 4); - - // Act - var result = ((StringSegmentComparer)comparer).Compare(segment, (StringSegment)candidate); - - // Assert - Assert.True(result > 0, $"{segment} should be greater than {candidate}"); - } - - [Test] - [TestCaseSource(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] - public void StringSegmentComparerOrdinal_GetHashCode_ReturnsSameValueForEqualSubstrings(object segment1, object segment2) - { - // Arrange - var comparer = StringSegmentComparer.Ordinal; - - // Act - var hashCode1 = comparer.GetHashCode((StringSegment)segment1); - var hashCode2 = comparer.GetHashCode((StringSegment)segment2); - - // Assert - Assert.AreEqual(hashCode1, hashCode2); - } - - [Test] - [TestCaseSource(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] - public void StringSegmentComparerOrdinalIgnoreCase_GetHashCode_ReturnsSameValueForEqualSubstrings(object segment1, object segment2) - { - // Arrange - var comparer = StringSegmentComparer.OrdinalIgnoreCase; - - // Act - var hashCode1 = comparer.GetHashCode((StringSegment)segment1); - var hashCode2 = comparer.GetHashCode((StringSegment)segment2); - - // Assert - Assert.AreEqual(hashCode1, hashCode2); - } - - [Test] - public void StringSegmentComparerOrdinalIgnoreCase_GetHashCode_ReturnsSameValueForDifferentlyCasedStrings() - { - // Arrange - var segment1 = new StringSegment("abc"); - var segment2 = new StringSegment("Abcd", 0, 3); - var comparer = StringSegmentComparer.OrdinalIgnoreCase; - - // Act - var hashCode1 = comparer.GetHashCode(segment1); - var hashCode2 = comparer.GetHashCode(segment2); - - // Assert - Assert.AreEqual(hashCode1, hashCode2); - } - - [Test] - [TestCaseSource(nameof(GetHashCode_ReturnsDifferentValuesForInequalSubstringsData))] - public void StringSegmentComparerOrdinal_GetHashCode_ReturnsDifferentValuesForInequalSubstrings(object segment1, object segment2) - { - // Arrange - var comparer = StringSegmentComparer.Ordinal; - - // Act - var hashCode1 = comparer.GetHashCode((StringSegment)segment1); - var hashCode2 = comparer.GetHashCode((StringSegment)segment2); - - // Assert - Assert.AreNotEqual(hashCode1, hashCode2); - } - - [Test] - public void IndexOf_ComputesIndex_RelativeToTheCurrentSegment() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 10); - - // Act - var result = segment.IndexOf(','); - - // Assert - Assert.AreEqual(4, result); - } - - [Test] - public void IndexOf_ReturnsMinusOne_IfElementNotInSegment() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act - var result = segment.IndexOf(','); - - // Assert - Assert.AreEqual(-1, result); - } - - [Test] - public void IndexOf_SkipsANumberOfCaracters_IfStartIsProvided() - { - // Arrange - const string buffer = "Hello, World!, Hello people!"; - var segment = new StringSegment(buffer, 3, buffer.Length - 3); - - // Act - var result = segment.IndexOf('!', 15); - - // Assert - Assert.AreEqual(buffer.Length - 4, result); - } - - [Test] - public void IndexOf_SearchOnlyInsideTheRange_IfStartAndCountAreProvided() - { - // Arrange - const string buffer = "Hello, World!, Hello people!"; - var segment = new StringSegment(buffer, 3, buffer.Length - 3); - - // Act - var result = segment.IndexOf('!', 15, 5); - - // Assert - Assert.AreEqual(-1, result); - } - - [Test] - public void IndexOf_NegativeStart_OutOfRangeThrows() - { - // Arrange - const string buffer = "Hello, World!, Hello people!"; - var segment = new StringSegment(buffer, 3, buffer.Length - 3); - - // Act & Assert - Assert.Throws(() => segment.IndexOf('!', -1, 3)); - } - - [Test] - public void IndexOf_StartOverflowsWithOffset_OutOfRangeThrows() - { - // Arrange - const string buffer = "Hello, World!, Hello people!"; - var segment = new StringSegment(buffer, 3, buffer.Length - 3); - - // Act & Assert - var exception = Assert.Throws(() => segment.IndexOf('!', int.MaxValue, 3)); - Assert.AreEqual("start", exception.ParamName); - } - - [Test] - public void IndexOfAny_ComputesIndex_RelativeToTheCurrentSegment() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 10); - - // Act - var result = segment.IndexOfAny(new[] { ',' }); - - // Assert - Assert.AreEqual(4, result); - } - - [Test] - public void IndexOfAny_ReturnsMinusOne_IfElementNotInSegment() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act - var result = segment.IndexOfAny(new[] { ',' }); - - // Assert - Assert.AreEqual(-1, result); - } - - [Test] - public void IndexOfAny_SkipsANumberOfCaracters_IfStartIsProvided() - { - // Arrange - const string buffer = "Hello, World!, Hello people!"; - var segment = new StringSegment(buffer, 3, buffer.Length - 3); - - // Act - var result = segment.IndexOfAny(new[] { '!' }, 15); - - // Assert - Assert.AreEqual(buffer.Length - 4, result); - } - - [Test] - public void IndexOfAny_SearchOnlyInsideTheRange_IfStartAndCountAreProvided() - { - // Arrange - const string buffer = "Hello, World!, Hello people!"; - var segment = new StringSegment(buffer, 3, buffer.Length - 3); - - // Act - var result = segment.IndexOfAny(new[] { '!' }, 15, 5); - - // Assert - Assert.AreEqual(-1, result); - } - - [Test] - public void LastIndexOf_ComputesIndex_RelativeToTheCurrentSegment() - { - // Arrange - var segment = new StringSegment("Hello, World, how, are, you!", 1, 14); - - // Act - var result = segment.LastIndexOf(','); - - // Assert - Assert.AreEqual(11, result); - } - - [Test] - public void LastIndexOf_ReturnsMinusOne_IfElementNotInSegment() - { - // Arrange - var segment = new StringSegment("Hello, World!", 1, 3); - - // Act - var result = segment.LastIndexOf(','); - - // Assert - Assert.AreEqual(-1, result); - } - - [Test] - public void Value_DoesNotAllocateANewString_IfTheSegmentContainsTheWholeBuffer() - { - // Arrange - const string buffer = "Hello, World!"; - var segment = new StringSegment(buffer); - - // Act - var result = segment.Value; - - // Assert - Assert.AreSame(buffer, result); - } - - [Test] - public void StringSegment_CreateEmptySegment() - { - // Arrange - var segment = new StringSegment("//", 1, 0); - - // Assert - Assert.True(segment.HasValue); - } - - [Test] - [TestCase(" value", 0, 8, "value")] - [TestCase("value ", 0, 8, "value")] - [TestCase("\t\tvalue", 0, 7, "value")] - [TestCase("value\t\t", 0, 7, "value")] - [TestCase("\t\tvalue \t a", 1, 8, "value")] - [TestCase(" a ", 0, 9, "a")] - [TestCase("value\t value value ", 2, 13, "lue\t value v")] - [TestCase("\x0009value \x0085", 0, 8, "value")] - [TestCase(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "Hello")] - [TestCase(" ", 1, 2, "")] - [TestCase("\t\t\t", 0, 3, "")] - [TestCase("\n\n\t\t \t", 2, 3, "")] - [TestCase(" ", 1, 0, "")] - [TestCase("", 0, 0, "")] - public void Trim_RemovesLeadingAndTrailingWhitespaces(string value, int start, int length, string expected) - { - // Arrange - var segment = new StringSegment(value, start, length); - - // Act - var actual = segment.Trim(); - - // Assert - Assert.AreEqual(expected, actual.Value); - } - - [Test] - [TestCase(" value", 0, 8, "value")] - [TestCase("value ", 0, 8, "value ")] - [TestCase("\t\tvalue", 0, 7, "value")] - [TestCase("value\t\t", 0, 7, "value\t\t")] - [TestCase("\t\tvalue \t a", 1, 8, "value \t")] - [TestCase(" a ", 0, 9, "a ")] - [TestCase("value\t value value ", 2, 13, "lue\t value v")] - [TestCase("\x0009value \x0085", 0, 8, "value \x0085")] - [TestCase(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "Hello \u2029\n\t")] - [TestCase(" ", 1, 2, "")] - [TestCase("\t\t\t", 0, 3, "")] - [TestCase("\n\n\t\t \t", 2, 3, "")] - [TestCase(" ", 1, 0, "")] - [TestCase("", 0, 0, "")] - public void TrimStart_RemovesLeadingWhitespaces(string value, int start, int length, string expected) - { - // Arrange - var segment = new StringSegment(value, start, length); - - // Act - var actual = segment.TrimStart(); - - // Assert - Assert.AreEqual(expected, actual.Value); - } - - [Test] - [TestCase(" value", 0, 8, " value")] - [TestCase("value ", 0, 8, "value")] - [TestCase("\t\tvalue", 0, 7, "\t\tvalue")] - [TestCase("value\t\t", 0, 7, "value")] - [TestCase("\t\tvalue \t a", 1, 8, "\tvalue")] - [TestCase(" a ", 0, 9, " a")] - [TestCase("value\t value value ", 2, 13, "lue\t value v")] - [TestCase("\x0009value \x0085", 0, 8, "\x0009value")] - [TestCase(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "\f\t\u000B\u2028Hello")] - [TestCase(" ", 1, 2, "")] - [TestCase("\t\t\t", 0, 3, "")] - [TestCase("\n\n\t\t \t", 2, 3, "")] - [TestCase(" ", 1, 0, "")] - [TestCase("", 0, 0, "")] - public void TrimEnd_RemovesTrailingWhitespaces(string value, int start, int length, string expected) - { - // Arrange - var segment = new StringSegment(value, start, length); - - // Act - var actual = segment.TrimEnd(); - - // Assert - Assert.AreEqual(expected, actual.Value); - } - } -} diff --git a/sdk/core/Azure.Core/tests/StringTokenizerTest.cs b/sdk/core/Azure.Core/tests/StringTokenizerTest.cs deleted file mode 100644 index 707ab6fadab64..0000000000000 --- a/sdk/core/Azure.Core/tests/StringTokenizerTest.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Linq; -using NUnit.Framework; - -namespace Microsoft.Extensions.Primitives -{ - public class StringTokenizerTest - { - [Test] - public void TokenizerReturnsEmptySequenceForNullValues() - { - // Arrange - var stringTokenizer = new StringTokenizer(); - var enumerator = stringTokenizer.GetEnumerator(); - - // Act - var next = enumerator.MoveNext(); - - // Assert - Assert.False(next); - } - - [Test] - [TestCase("", new[] { "" })] - [TestCase("a", new[] { "a" })] - [TestCase("abc", new[] { "abc" })] - [TestCase("a,b", new[] { "a", "b" })] - [TestCase("a,,b", new[] { "a", "", "b" })] - [TestCase(",a,b", new[] { "", "a", "b" })] - [TestCase(",,a,b", new[] { "", "", "a", "b" })] - [TestCase("a,b,", new[] { "a", "b", "" })] - [TestCase("a,b,,", new[] { "a", "b", "", "" })] - [TestCase("ab,cde,efgh", new[] { "ab", "cde", "efgh" })] - public void Tokenizer_ReturnsSequenceOfValues(string value, string[] expected) - { - // Arrange - var tokenizer = new StringTokenizer(value, new[] { ',' }); - - // Act - var result = tokenizer.Select(t => t.Value).ToArray(); - - // Assert - Assert.AreEqual(expected, result); - } - - [Test] - [TestCase("", new[] { "" })] - [TestCase("a", new[] { "a" })] - [TestCase("abc", new[] { "abc" })] - [TestCase("a.b", new[] { "a", "b" })] - [TestCase("a,b", new[] { "a", "b" })] - [TestCase("a.b,c", new[] { "a", "b", "c" })] - [TestCase("a,b.c", new[] { "a", "b", "c" })] - [TestCase("ab.cd,ef", new[] { "ab", "cd", "ef" })] - [TestCase("ab,cd.ef", new[] { "ab", "cd", "ef" })] - [TestCase(",a.b", new[] { "", "a", "b" })] - [TestCase(".a,b", new[] { "", "a", "b" })] - [TestCase(".,a.b", new[] { "", "", "a", "b" })] - [TestCase(",.a,b", new[] { "", "", "a", "b" })] - [TestCase("a.b,", new[] { "a", "b", "" })] - [TestCase("a,b.", new[] { "a", "b", "" })] - [TestCase("a.b,.", new[] { "a", "b", "", "" })] - [TestCase("a,b.,", new[] { "a", "b", "", "" })] - public void Tokenizer_SupportsMultipleSeparators(string value, string[] expected) - { - // Arrange - var tokenizer = new StringTokenizer(value, new[] { '.', ',' }); - - // Act - var result = tokenizer.Select(t => t.Value).ToArray(); - - // Assert - Assert.AreEqual(expected, result); - } - } -} diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 03580c8c1a36e..304b71e3c7d02 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -45,8 +45,6 @@ - - From 72ca094c5fea82d594cd580662e312c2d313a0da Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 17:54:59 -0500 Subject: [PATCH 08/14] Multipart subdir in shared, remove more types --- .../Shared/{ => Multipart}/BatchConstants.cs | 0 .../src/Shared/{ => Multipart}/BatchErrors.cs | 0 .../{ => Multipart}/BufferedReadStream.cs | 0 .../{ => Multipart}/KeyValueAccumulator.cs | 25 +- .../Shared/{ => Multipart}/MemoryResponse.cs | 0 .../src/Shared/{ => Multipart}/Multipart.cs | 8 +- .../{ => Multipart}/MultipartBoundary.cs | 0 .../{ => Multipart}/MultipartContent.cs | 0 .../MultipartFormDataContent.cs | 2 +- .../Shared/{ => Multipart}/MultipartReader.cs | 2 +- .../{ => Multipart}/MultipartReaderStream.cs | 0 .../src/Shared/Multipart/MultipartSection.cs | 27 + .../Azure.Core/src/Shared/MultipartSection.cs | 50 -- .../Azure.Core/src/Shared/StringValues.cs | 826 ------------------ .../src/Shared/ValueStringBuilder.cs | 314 ------- .../Azure.Core/tests/Azure.Core.Tests.csproj | 13 +- .../Azure.Core/tests/StringValuesTests.cs | 604 ------------- .../src/Azure.Data.Tables.csproj | 14 +- 18 files changed, 49 insertions(+), 1836 deletions(-) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/BatchConstants.cs (100%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/BatchErrors.cs (100%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/BufferedReadStream.cs (100%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/KeyValueAccumulator.cs (75%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/MemoryResponse.cs (100%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/Multipart.cs (97%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/MultipartBoundary.cs (100%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/MultipartContent.cs (100%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/MultipartFormDataContent.cs (97%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/MultipartReader.cs (97%) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/MultipartReaderStream.cs (100%) create mode 100644 sdk/core/Azure.Core/src/Shared/Multipart/MultipartSection.cs delete mode 100644 sdk/core/Azure.Core/src/Shared/MultipartSection.cs delete mode 100644 sdk/core/Azure.Core/src/Shared/StringValues.cs delete mode 100644 sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs delete mode 100644 sdk/core/Azure.Core/tests/StringValuesTests.cs diff --git a/sdk/core/Azure.Core/src/Shared/BatchConstants.cs b/sdk/core/Azure.Core/src/Shared/Multipart/BatchConstants.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/BatchConstants.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/BatchConstants.cs diff --git a/sdk/core/Azure.Core/src/Shared/BatchErrors.cs b/sdk/core/Azure.Core/src/Shared/Multipart/BatchErrors.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/BatchErrors.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/BatchErrors.cs diff --git a/sdk/core/Azure.Core/src/Shared/BufferedReadStream.cs b/sdk/core/Azure.Core/src/Shared/Multipart/BufferedReadStream.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/BufferedReadStream.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/BufferedReadStream.cs diff --git a/sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs b/sdk/core/Azure.Core/src/Shared/Multipart/KeyValueAccumulator.cs similarity index 75% rename from sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/KeyValueAccumulator.cs index c9f2c3d1693a2..2db059026d771 100644 --- a/sdk/core/Azure.Core/src/Shared/KeyValueAccumulator.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/KeyValueAccumulator.cs @@ -14,25 +14,25 @@ namespace Azure.Core { internal struct KeyValueAccumulator { - private Dictionary _accumulator; + private Dictionary _accumulator; private Dictionary> _expandingAccumulator; public void Append(string key, string value) { if (_accumulator == null) { - _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); + _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); } - StringValues values; + string[] values; if (_accumulator.TryGetValue(key, out values)) { - if (values.Count == 0) + if (values.Length == 0) { // Marker entry for this key to indicate entry already in expanding list dictionary _expandingAccumulator[key].Add(value); } - else if (values.Count == 1) + else if (values.Length == 1) { // Second value for this key _accumulator[key] = new string[] { values[0], value }; @@ -41,7 +41,7 @@ public void Append(string key, string value) { // Third value for this key // Add zero count entry and move to data to expanding list dictionary - _accumulator[key] = default(StringValues); + _accumulator[key] = null; if (_expandingAccumulator == null) { @@ -50,10 +50,9 @@ public void Append(string key, string value) // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more var list = new List(8); - var array = values.ToArray(); - list.Add(array[0]); - list.Add(array[1]); + list.Add(values[0]); + list.Add(values[1]); list.Add(value); _expandingAccumulator[key] = list; @@ -62,7 +61,7 @@ public void Append(string key, string value) else { // First value for this key - _accumulator[key] = new StringValues(value); + _accumulator[key] = new[] { value }; } ValueCount++; @@ -74,18 +73,18 @@ public void Append(string key, string value) public int ValueCount { get; private set; } - public Dictionary GetResults() + public Dictionary GetResults() { if (_expandingAccumulator != null) { // Coalesce count 3+ multi-value entries into _accumulator dictionary foreach (var entry in _expandingAccumulator) { - _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); + _accumulator[entry.Key] = entry.Value.ToArray(); } } - return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); + return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); } } } diff --git a/sdk/core/Azure.Core/src/Shared/MemoryResponse.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/MemoryResponse.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs diff --git a/sdk/core/Azure.Core/src/Shared/Multipart.cs b/sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs similarity index 97% rename from sdk/core/Azure.Core/src/Shared/Multipart.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs index 32c265ca78d1e..bcaa2c8b927a9 100644 --- a/sdk/core/Azure.Core/src/Shared/Multipart.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs @@ -62,16 +62,16 @@ internal static async Task ParseAsync( section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false)) { bool contentIdFound = true; - if (section.Headers.TryGetValue(HttpHeader.Names.ContentType, out StringValues contentTypeValues) && - contentTypeValues.Count == 1 && + if (section.Headers.TryGetValue(HttpHeader.Names.ContentType, out string [] contentTypeValues) && + contentTypeValues.Length == 1 && GetBoundary(contentTypeValues[0], out string subBoundary)) { reader = new MultipartReader(subBoundary, section.Body); continue; } // Get the Content-ID header - if (!section.Headers.TryGetValue(BatchConstants.ContentIdName, out StringValues contentIdValues) || - contentIdValues.Count != 1 || + if (!section.Headers.TryGetValue(BatchConstants.ContentIdName, out string [] contentIdValues) || + contentIdValues.Length != 1 || !int.TryParse(contentIdValues[0], out int contentId)) { // If the header wasn't found, this is either: diff --git a/sdk/core/Azure.Core/src/Shared/MultipartBoundary.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartBoundary.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/MultipartBoundary.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/MultipartBoundary.cs diff --git a/sdk/core/Azure.Core/src/Shared/MultipartContent.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartContent.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/MultipartContent.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/MultipartContent.cs diff --git a/sdk/core/Azure.Core/src/Shared/MultipartFormDataContent.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartFormDataContent.cs similarity index 97% rename from sdk/core/Azure.Core/src/Shared/MultipartFormDataContent.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/MultipartFormDataContent.cs index 9f77db775cad0..b99ee6ef83a66 100644 --- a/sdk/core/Azure.Core/src/Shared/MultipartFormDataContent.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartFormDataContent.cs @@ -81,7 +81,7 @@ public void Add(RequestContent content, string name, Dictionary /// /// The Request content to add to the collection. /// The name for the request content to add. - /// The file name for the reuest content to add to the collection. + /// The file name for the request content to add to the collection. /// The headers to add to the collection. public void Add(RequestContent content, string name, string fileName, Dictionary headers) { diff --git a/sdk/core/Azure.Core/src/Shared/MultipartReader.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartReader.cs similarity index 97% rename from sdk/core/Azure.Core/src/Shared/MultipartReader.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/MultipartReader.cs index 9804885823f68..6ac52f6aec032 100644 --- a/sdk/core/Azure.Core/src/Shared/MultipartReader.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartReader.cs @@ -87,7 +87,7 @@ public MultipartReader(string boundary, Stream stream, int bufferSize) return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; } - private async Task> ReadHeadersAsync(CancellationToken cancellationToken) + private async Task> ReadHeadersAsync(CancellationToken cancellationToken) { int totalSize = 0; var accumulator = new KeyValueAccumulator(); diff --git a/sdk/core/Azure.Core/src/Shared/MultipartReaderStream.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartReaderStream.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/MultipartReaderStream.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/MultipartReaderStream.cs diff --git a/sdk/core/Azure.Core/src/Shared/Multipart/MultipartSection.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartSection.cs new file mode 100644 index 0000000000000..5849be42c0180 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/Multipart/MultipartSection.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Azure.Core +{ + internal class MultipartSection + { + public Dictionary Headers { get; set; } + + /// + /// Gets or sets the body. + /// + public Stream Body { get; set; } = default!; + + /// + /// The position where the body starts in the total multipart body. + /// This may not be available if the total multipart body is not seekable. + /// + public long? BaseStreamOffset { get; set; } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/MultipartSection.cs b/sdk/core/Azure.Core/src/Shared/MultipartSection.cs deleted file mode 100644 index cc51a8b777437..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/MultipartSection.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/WebUtilities/src - -using System.Collections.Generic; -using System.IO; - -namespace Azure.Core -{ - internal class MultipartSection - { - public string ContentType - { - get - { - if (Headers != null && Headers.TryGetValue(HttpHeader.Names.ContentType, out StringValues values)) - { - return values; - } - return null; - } - } - - public string ContentDisposition - { - get - { - if (Headers != null && Headers.TryGetValue(HttpHeader.Names.ContentDisposition, out StringValues values)) - { - return values; - } - return null; - } - } - - public Dictionary Headers { get; set; } - - /// - /// Gets or sets the body. - /// - public Stream Body { get; set; } = default!; - - /// - /// The position where the body starts in the total multipart body. - /// This may not be available if the total multipart body is not seekable. - /// - public long? BaseStreamOffset { get; set; } - } -} diff --git a/sdk/core/Azure.Core/src/Shared/StringValues.cs b/sdk/core/Azure.Core/src/Shared/StringValues.cs deleted file mode 100644 index eb8aeaea4ace0..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/StringValues.cs +++ /dev/null @@ -1,826 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Primitives/src - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -#pragma warning disable IDE0032 // Use auto property -#pragma warning disable IDE0066 // Use switch expression - -namespace Azure.Core -{ - /// - /// Represents zero/null, one, or many strings in an efficient way. - /// - internal readonly struct StringValues : IList, IReadOnlyList, IEquatable, IEquatable, IEquatable - { - /// - /// A readonly instance of the struct whose value is an empty string array. - /// - /// - /// In application code, this field is most commonly used to safely represent a that has null string values. - /// - public static readonly StringValues Empty = new StringValues(Array.Empty()); - - private readonly object _values; - - /// - /// Initializes a new instance of the structure using the specified string. - /// - /// A string value or null. - public StringValues(string value) - { - _values = value; - } - - /// - /// Initializes a new instance of the structure using the specified array of strings. - /// - /// A string array. - public StringValues(string[] values) - { - _values = values; - } - - /// - /// Defines an implicit conversion of a given string to a . - /// - /// A string to implicitly convert. - public static implicit operator StringValues(string value) - { - return new StringValues(value); - } - - /// - /// Defines an implicit conversion of a given string array to a . - /// - /// A string array to implicitly convert. - public static implicit operator StringValues(string[] values) - { - return new StringValues(values); - } - - /// - /// Defines an implicit conversion of a given to a string, with multiple values joined as a comma separated string. - /// - /// - /// Returns null where has been initialized from an empty string array or is . - /// - /// A to implicitly convert. - public static implicit operator string (StringValues values) - { - return values.GetStringValue(); - } - - /// - /// Defines an implicit conversion of a given to a string array. - /// - /// A to implicitly convert. - public static implicit operator string[] (StringValues value) - { - return value.GetArrayValue(); - } - - /// - /// Gets the number of elements contained in this . - /// - public int Count - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory - object value = _values; - if (value is string) - { - return 1; - } - if (value is null) - { - return 0; - } - else - { - // Not string, not null, can only be string[] - return Unsafe.As(value).Length; - } - } - } - - bool ICollection.IsReadOnly - { - get { return true; } - } - - /// - /// Gets the at index. - /// - /// The string at the specified index. - /// The zero-based index of the element to get. - /// Set operations are not supported on readonly . - string IList.this[int index] - { - get { return this[index]; } - set { throw new NotSupportedException(); } - } - - /// - /// Gets the at index. - /// - /// The string at the specified index. - /// The zero-based index of the element to get. - public string this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory - object value = _values; - if (value is string str) - { - if (index == 0) - { - return str; - } - } - else if (value != null) - { - // Not string, not null, can only be string[] - return Unsafe.As(value)[index]; // may throw - } - - return OutOfBounds(); // throws - } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static string OutOfBounds() - { - return Array.Empty()[0]; // throws - } - - /// - /// Converts the value of the current object to its equivalent string representation, with multiple values joined as a comma separated string. - /// - /// A string representation of the value of the current object. - public override string ToString() - { - return GetStringValue() ?? string.Empty; - } - - private string GetStringValue() - { - // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory - object value = _values; - if (value is string s) - { - return s; - } - else - { - return GetStringValueFromArray(value); - } - - static string GetStringValueFromArray(object value) - { - if (value is null) - { - return null; - } - - Debug.Assert(value is string[]); - // value is not null or string, array, can only be string[] - string[] values = Unsafe.As(value); - switch (values.Length) - { - case 0: return null; - case 1: return values[0]; - default: return GetJoinedStringValueFromArray(values); - } - } - - static string GetJoinedStringValueFromArray(string[] values) - { - // Calculate final length - int length = 0; - for (int i = 0; i < values.Length; i++) - { - string value = values[i]; - // Skip null and empty values - if (value != null && value.Length > 0) - { - if (length > 0) - { - // Add seperator - length++; - } - - length += value.Length; - } - } -#if NETCOREAPP - // Create the new string - return string.Create(length, values, (span, strings) => { - int offset = 0; - // Skip null and empty values - for (int i = 0; i < strings.Length; i++) - { - string value = strings[i]; - if (value != null && value.Length > 0) - { - if (offset > 0) - { - // Add seperator - span[offset] = ','; - offset++; - } - - value.AsSpan().CopyTo(span.Slice(offset)); - offset += value.Length; - } - } - }); -#else - var sb = new ValueStringBuilder(length); - bool hasAdded = false; - // Skip null and empty values - for (int i = 0; i < values.Length; i++) - { - string value = values[i]; - if (value != null && value.Length > 0) - { - if (hasAdded) - { - // Add seperator - sb.Append(','); - } - - sb.Append(value); - hasAdded = true; - } - } - - return sb.ToString(); -#endif - } - } - - /// - /// Creates a string array from the current object. - /// - /// A string array represented by this instance. - /// - /// If the contains a single string internally, it is copied to a new array. - /// If the contains an array internally it returns that array instance. - /// - public string[] ToArray() - { - return GetArrayValue() ?? Array.Empty(); - } - - private string[] GetArrayValue() - { - // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory - object value = _values; - if (value is string[] values) - { - return values; - } - else if (value != null) - { - // value not array, can only be string - return new[] { Unsafe.As(value) }; - } - else - { - return null; - } - } - - /// - /// Returns the zero-based index of the first occurrence of an item in the . - /// - /// The string to locate in the . - /// the zero-based index of the first occurrence of within the , if found; otherwise, -1. - int IList.IndexOf(string item) - { - return IndexOf(item); - } - - private int IndexOf(string item) - { - // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory - object value = _values; - if (value is string[] values) - { - for (int i = 0; i < values.Length; i++) - { - if (string.Equals(values[i], item, StringComparison.Ordinal)) - { - return i; - } - } - return -1; - } - - if (value != null) - { - // value not array, can only be string - return string.Equals(Unsafe.As(value), item, StringComparison.Ordinal) ? 0 : -1; - } - - return -1; - } - - /// Determines whether a string is in the . - /// The to locate in the . - /// true if item is found in the ; otherwise, false. - bool ICollection.Contains(string item) - { - return IndexOf(item) >= 0; - } - - /// - /// Copies the entire to a string array, starting at the specified index of the target array. - /// - /// The one-dimensional that is the destination of the elements copied from. The must have zero-based indexing. - /// The zero-based index in the destination array at which copying begins. - /// array is null. - /// arrayIndex is less than 0. - /// The number of elements in the source is greater than the available space from arrayIndex to the end of the destination array. - void ICollection.CopyTo(string[] array, int arrayIndex) - { - CopyTo(array, arrayIndex); - } - - private void CopyTo(string[] array, int arrayIndex) - { - // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory - object value = _values; - if (value is string[] values) - { - Array.Copy(values, 0, array, arrayIndex, values.Length); - return; - } - - if (value != null) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - if (arrayIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - } - if (array.Length - arrayIndex < 1) - { - throw new ArgumentException( - $"'{nameof(array)}' is not long enough to copy all the items in the collection. Check '{nameof(arrayIndex)}' and '{nameof(array)}' length."); - } - - // value not array, can only be string - array[arrayIndex] = Unsafe.As(value); - } - } - - void ICollection.Add(string item) => throw new NotSupportedException(); - - void IList.Insert(int index, string item) => throw new NotSupportedException(); - - bool ICollection.Remove(string item) => throw new NotSupportedException(); - - void IList.RemoveAt(int index) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - /// Retrieves an object that can iterate through the individual strings in this . - /// An enumerator that can be used to iterate through the . - public Enumerator GetEnumerator() - { - return new Enumerator(_values); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Indicates whether the specified contains no string values. - /// - /// The to test. - /// true if value contains a single null string or empty array; otherwise, false. - public static bool IsNullOrEmpty(StringValues value) - { - object data = value._values; - if (data is null) - { - return true; - } - if (data is string[] values) - { - switch (values.Length) - { - case 0: return true; - case 1: return string.IsNullOrEmpty(values[0]); - default: return false; - } - } - else - { - // Not array, can only be string - return string.IsNullOrEmpty(Unsafe.As(data)); - } - } - - /// - /// Concatenates two specified instances of . - /// - /// The first to concatenate. - /// The second to concatenate. - /// The concatenation of and . - public static StringValues Concat(StringValues values1, StringValues values2) - { - int count1 = values1.Count; - int count2 = values2.Count; - - if (count1 == 0) - { - return values2; - } - - if (count2 == 0) - { - return values1; - } - - var combined = new string[count1 + count2]; - values1.CopyTo(combined, 0); - values2.CopyTo(combined, count1); - return new StringValues(combined); - } - - /// - /// Concatenates specified instance of with specified . - /// - /// The to concatenate. - /// The to concatenate. - /// The concatenation of and . - public static StringValues Concat(in StringValues values, string value) - { - if (value == null) - { - return values; - } - - int count = values.Count; - if (count == 0) - { - return new StringValues(value); - } - - var combined = new string[count + 1]; - values.CopyTo(combined, 0); - combined[count] = value; - return new StringValues(combined); - } - - /// - /// Concatenates specified instance of with specified . - /// - /// The to concatenate. - /// The to concatenate. - /// The concatenation of and . - public static StringValues Concat(string value, in StringValues values) - { - if (value == null) - { - return values; - } - - int count = values.Count; - if (count == 0) - { - return new StringValues(value); - } - - var combined = new string[count + 1]; - combined[0] = value; - values.CopyTo(combined, 1); - return new StringValues(combined); - } - - /// - /// Determines whether two specified objects have the same values in the same order. - /// - /// The first to compare. - /// The second to compare. - /// true if the value of is the same as the value of ; otherwise, false. - public static bool Equals(StringValues left, StringValues right) - { - int count = left.Count; - - if (count != right.Count) - { - return false; - } - - for (int i = 0; i < count; i++) - { - if (left[i] != right[i]) - { - return false; - } - } - - return true; - } - - /// - /// Determines whether two specified have the same values. - /// - /// The first to compare. - /// The second to compare. - /// true if the value of is the same as the value of ; otherwise, false. - public static bool operator ==(StringValues left, StringValues right) - { - return Equals(left, right); - } - - /// - /// Determines whether two specified have different values. - /// - /// The first to compare. - /// The second to compare. - /// true if the value of is different to the value of ; otherwise, false. - public static bool operator !=(StringValues left, StringValues right) - { - return !Equals(left, right); - } - - /// - /// Determines whether this instance and another specified object have the same values. - /// - /// The string to compare to this instance. - /// true if the value of is the same as the value of this instance; otherwise, false. - public bool Equals(StringValues other) => Equals(this, other); - - /// - /// Determines whether the specified and objects have the same values. - /// - /// The to compare. - /// The to compare. - /// true if the value of is the same as the value of ; otherwise, false. If is null, the method returns false. - public static bool Equals(string left, StringValues right) => Equals(new StringValues(left), right); - - /// - /// Determines whether the specified and objects have the same values. - /// - /// The to compare. - /// The to compare. - /// true if the value of is the same as the value of ; otherwise, false. If is null, the method returns false. - public static bool Equals(StringValues left, string right) => Equals(left, new StringValues(right)); - - /// - /// Determines whether this instance and a specified , have the same value. - /// - /// The to compare to this instance. - /// true if the value of is the same as this instance; otherwise, false. If is null, returns false. - public bool Equals(string other) => Equals(this, new StringValues(other)); - - /// - /// Determines whether the specified string array and objects have the same values. - /// - /// The string array to compare. - /// The to compare. - /// true if the value of is the same as the value of ; otherwise, false. - public static bool Equals(string[] left, StringValues right) => Equals(new StringValues(left), right); - - /// - /// Determines whether the specified and string array objects have the same values. - /// - /// The to compare. - /// The string array to compare. - /// true if the value of is the same as the value of ; otherwise, false. - public static bool Equals(StringValues left, string[] right) => Equals(left, new StringValues(right)); - - /// - /// Determines whether this instance and a specified string array have the same values. - /// - /// The string array to compare to this instance. - /// true if the value of is the same as this instance; otherwise, false. - public bool Equals(string[] other) => Equals(this, new StringValues(other)); - - /// - public static bool operator ==(StringValues left, string right) => Equals(left, new StringValues(right)); - - /// - /// Determines whether the specified and objects have different values. - /// - /// The to compare. - /// The to compare. - /// true if the value of is different to the value of ; otherwise, false. - public static bool operator !=(StringValues left, string right) => !Equals(left, new StringValues(right)); - - /// - public static bool operator ==(string left, StringValues right) => Equals(new StringValues(left), right); - - /// - /// Determines whether the specified and objects have different values. - /// - /// The to compare. - /// The to compare. - /// true if the value of is different to the value of ; otherwise, false. - public static bool operator !=(string left, StringValues right) => !Equals(new StringValues(left), right); - - /// - public static bool operator ==(StringValues left, string[] right) => Equals(left, new StringValues(right)); - - /// - /// Determines whether the specified and string array have different values. - /// - /// The to compare. - /// The string array to compare. - /// true if the value of is different to the value of ; otherwise, false. - public static bool operator !=(StringValues left, string[] right) => !Equals(left, new StringValues(right)); - - /// - public static bool operator ==(string[] left, StringValues right) => Equals(new StringValues(left), right); - - /// - /// Determines whether the specified string array and have different values. - /// - /// The string array to compare. - /// The to compare. - /// true if the value of is different to the value of ; otherwise, false. - public static bool operator !=(string[] left, StringValues right) => !Equals(new StringValues(left), right); - - /// - /// Determines whether the specified and , which must be a - /// , , or array of , have the same value. - /// - /// The to compare. - /// The to compare. - /// true if the object is equal to the ; otherwise, false. - public static bool operator ==(StringValues left, object right) => left.Equals(right); - - /// - /// Determines whether the specified and , which must be a - /// , , or array of , have different values. - /// - /// The to compare. - /// The to compare. - /// true if the object is equal to the ; otherwise, false. - public static bool operator !=(StringValues left, object right) => !left.Equals(right); - - /// - /// Determines whether the specified , which must be a - /// , , or array of , and specified , have the same value. - /// - /// The to compare. - /// The to compare. - /// true if the object is equal to the ; otherwise, false. - public static bool operator ==(object left, StringValues right) => right.Equals(left); - - /// - /// Determines whether the specified and object have the same values. - /// - /// The to compare. - /// The to compare. - /// true if the object is equal to the ; otherwise, false. - public static bool operator !=(object left, StringValues right) => !right.Equals(left); - - /// - /// Determines whether this instance and a specified object have the same value. - /// - /// An object to compare with this object. - /// true if the current object is equal to ; otherwise, false. - public override bool Equals(object obj) - { - if (obj == null) - { - return Equals(this, StringValues.Empty); - } - - if (obj is string stringValue) - { - return Equals(this, stringValue); - } - - if (obj is string[] stringArray) - { - return Equals(this, stringArray); - } - - if (obj is StringValues values) - { - return Equals(this, values); - } - - return false; - } - - /// - public override int GetHashCode() - { - object value = _values; - if (value is string[] values) - { - if (Count == 1) - { - return Unsafe.As(this[0])?.GetHashCode() ?? Count.GetHashCode(); - } - var hcc = default(HashCodeBuilder); - for (int i = 0; i < values.Length; i++) - { - hcc.Add(values[i]); - } - return hcc.ToHashCode(); - } - else - { - return Unsafe.As(value)?.GetHashCode() ?? Count.GetHashCode(); - } - } - - /// - /// Enumerates the string values of a . - /// - public struct Enumerator : IEnumerator - { - private readonly string[] _values; - private string _current; - private int _index; - - internal Enumerator(object value) - { - if (value is string str) - { - _values = null; - _current = str; - } - else - { - _current = null; - _values = Unsafe.As(value); - } - _index = 0; - } - - public Enumerator(ref StringValues values) : this(values._values) - { } - - public bool MoveNext() - { - int index = _index; - if (index < 0) - { - return false; - } - - string[] values = _values; - if (values != null) - { - if ((uint)index < (uint)values.Length) - { - _index = index + 1; - _current = values[index]; - return true; - } - - _index = -1; - return false; - } - - _index = -1; // sentinel value - return _current != null; - } - - public string Current => _current; - - object IEnumerator.Current => _current; - - void IEnumerator.Reset() - { - throw new NotSupportedException(); - } - - public void Dispose() - { - } - } - } -} diff --git a/sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs b/sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs deleted file mode 100644 index 9136a8b57626a..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/ValueStringBuilder.cs +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// copied from https://github.com/dotnet/runtime/tree/master/src/libraries/Common/src/System/Text - -using System; -using System.Buffers; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -#pragma warning disable IDE0032 // Use auto property -#nullable enable - -namespace Azure.Core -{ - internal ref partial struct ValueStringBuilder - { - private char[]? _arrayToReturnToPool; - private Span _chars; - private int _pos; - - public ValueStringBuilder(Span initialBuffer) - { - _arrayToReturnToPool = null; - _chars = initialBuffer; - _pos = 0; - } - - public ValueStringBuilder(int initialCapacity) - { - _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); - _chars = _arrayToReturnToPool; - _pos = 0; - } - - public int Length - { - get => _pos; - set - { - Debug.Assert(value >= 0); - Debug.Assert(value <= _chars.Length); - _pos = value; - } - } - - public int Capacity => _chars.Length; - - public void EnsureCapacity(int capacity) - { - if (capacity > _chars.Length) - Grow(capacity - _pos); - } - - /// - /// Get a pinnable reference to the builder. - /// Does not ensure there is a null char after - /// This overload is pattern matched in the C# 7.3+ compiler so you can omit - /// the explicit method call, and write eg "fixed (char* c = builder)". - /// - public ref char GetPinnableReference() - { - return ref MemoryMarshal.GetReference(_chars); - } - - /// - /// Get a pinnable reference to the builder. - /// - /// Ensures that the builder has a null char after . - public ref char GetPinnableReference(bool terminate) - { - if (terminate) - { - EnsureCapacity(Length + 1); - _chars[Length] = '\0'; - } - return ref MemoryMarshal.GetReference(_chars); - } - - public ref char this[int index] - { - get - { - Debug.Assert(index < _pos); - return ref _chars[index]; - } - } - - public override string ToString() - { - string s = _chars.Slice(0, _pos).ToString(); - Dispose(); - return s; - } - - /// Returns the underlying storage of the builder. - public Span RawChars => _chars; - - /// - /// Returns a span around the contents of the builder. - /// - /// Ensures that the builder has a null char after - public ReadOnlySpan AsSpan(bool terminate) - { - if (terminate) - { - EnsureCapacity(Length + 1); - _chars[Length] = '\0'; - } - return _chars.Slice(0, _pos); - } - - public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); - public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); - public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); - - public bool TryCopyTo(Span destination, out int charsWritten) - { - if (_chars.Slice(0, _pos).TryCopyTo(destination)) - { - charsWritten = _pos; - Dispose(); - return true; - } - else - { - charsWritten = 0; - Dispose(); - return false; - } - } - - public void Insert(int index, char value, int count) - { - if (_pos > _chars.Length - count) - { - Grow(count); - } - - int remaining = _pos - index; - _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); - _chars.Slice(index, count).Fill(value); - _pos += count; - } - - public void Insert(int index, string? s) - { - if (s == null) - { - return; - } - - int count = s.Length; - - if (_pos > (_chars.Length - count)) - { - Grow(count); - } - - int remaining = _pos - index; - _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); - s.AsSpan().CopyTo(_chars.Slice(index)); - _pos += count; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(char c) - { - int pos = _pos; - if ((uint)pos < (uint)_chars.Length) - { - _chars[pos] = c; - _pos = pos + 1; - } - else - { - GrowAndAppend(c); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(string? s) - { - if (s == null) - { - return; - } - - int pos = _pos; - if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. - { - _chars[pos] = s[0]; - _pos = pos + 1; - } - else - { - AppendSlow(s); - } - } - - private void AppendSlow(string s) - { - int pos = _pos; - if (pos > _chars.Length - s.Length) - { - Grow(s.Length); - } - - s.AsSpan().CopyTo(_chars.Slice(pos)); - _pos += s.Length; - } - - public void Append(char c, int count) - { - if (_pos > _chars.Length - count) - { - Grow(count); - } - - Span dst = _chars.Slice(_pos, count); - for (int i = 0; i < dst.Length; i++) - { - dst[i] = c; - } - _pos += count; - } - - public unsafe void Append(char* value, int length) - { - int pos = _pos; - if (pos > _chars.Length - length) - { - Grow(length); - } - - Span dst = _chars.Slice(_pos, length); - for (int i = 0; i < dst.Length; i++) - { - dst[i] = *value++; - } - _pos += length; - } - - public void Append(ReadOnlySpan value) - { - int pos = _pos; - if (pos > _chars.Length - value.Length) - { - Grow(value.Length); - } - - value.CopyTo(_chars.Slice(_pos)); - _pos += value.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AppendSpan(int length) - { - int origPos = _pos; - if (origPos > _chars.Length - length) - { - Grow(length); - } - - _pos = origPos + length; - return _chars.Slice(origPos, length); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void GrowAndAppend(char c) - { - Grow(1); - Append(c); - } - - /// - /// Resize the internal buffer either by doubling current buffer size or - /// by adding to - /// whichever is greater. - /// - /// - /// Number of chars requested beyond current position. - /// - [MethodImpl(MethodImplOptions.NoInlining)] - private void Grow(int additionalCapacityBeyondPos) - { - Debug.Assert(additionalCapacityBeyondPos > 0); - Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); - - char[] poolArray = ArrayPool.Shared.Rent(Math.Max(_pos + additionalCapacityBeyondPos, _chars.Length * 2)); - - _chars.Slice(0, _pos).CopyTo(poolArray); - - char[]? toReturn = _arrayToReturnToPool; - _chars = _arrayToReturnToPool = poolArray; - if (toReturn != null) - { - ArrayPool.Shared.Return(toReturn); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() - { - char[]? toReturn = _arrayToReturnToPool; - this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again - if (toReturn != null) - { - ArrayPool.Shared.Return(toReturn); - } - } - } -} diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index be360a68b9271..0820ac8fcc5ef 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -22,27 +22,20 @@ + + + - - - - - - - - - - diff --git a/sdk/core/Azure.Core/tests/StringValuesTests.cs b/sdk/core/Azure.Core/tests/StringValuesTests.cs deleted file mode 100644 index 0a6547a5cc875..0000000000000 --- a/sdk/core/Azure.Core/tests/StringValuesTests.cs +++ /dev/null @@ -1,604 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; - -namespace Azure.Core.Tests -{ - public class StringValuesTests - { - public static IEnumerable DefaultOrNullStringValues = new List - { - new object[] { new StringValues() }, - new object[] { new StringValues((string)null) }, - new object[] { new StringValues((string[])null) }, - new object[] { (string)null }, - new object[] { (string[])null } - }; - - public static IEnumerable EmptyStringValues = new List - { - new object[]{ StringValues.Empty}, - new object[]{ new StringValues(new string[0])}, - new object[]{ new string[0]} - }; - - public static IEnumerable FilledStringValues = new List - { - new StringValues("abc"), - new StringValues(new[] { "abc" }), - new StringValues(new[] { "abc", "bcd" }), - new StringValues(new[] { "abc", "bcd", "foo" }), - "abc", - new[] { "abc" }, - new[] { "abc", "bcd" }, - new[] { "abc", "bcd", "foo" } - }; - - public static IEnumerable FilledStringValuesWithExpectedStrings = new List - { - new object[] { default(StringValues), (string)null }, - new object[] { StringValues.Empty, (string)null }, - new object[] { new StringValues(new string[] { }), (string)null }, - new object[] { new StringValues(string.Empty), string.Empty }, - new object[] { new StringValues(new string[] { string.Empty }), string.Empty }, - new object[] { new StringValues("abc"), "abc" } - }; - - public static IEnumerable FilledStringValuesWithExpectedObjects = new List - { - new object[] { default(StringValues), (object)null }, - new object[] { StringValues.Empty, (object)null }, - new object[] { new StringValues(new string[] { }), (object)null }, - new object[] { new StringValues("abc"), (object)"abc" }, - new object[] { new StringValues("abc"), (object)new[] { "abc" } }, - new object[] { new StringValues(new[] { "abc" }), (object)new[] { "abc" } }, - new object[] { new StringValues(new[] { "abc", "bcd" }), (object)new[] { "abc", "bcd" } } - }; - - public static IEnumerable FilledStringValuesWithExpected = new List - { - new object[] { default(StringValues), new string[0] }, - new object[] { StringValues.Empty, new string[0] }, - new object[] { new StringValues(string.Empty), new[] { string.Empty } }, - new object[] { new StringValues("abc"), new[] { "abc" } }, - new object[] { new StringValues(new[] { "abc" }), new[] { "abc" } }, - new object[] { new StringValues(new[] { "abc", "bcd" }), new[] { "abc", "bcd" } }, - new object[] { new StringValues(new[] { "abc", "bcd", "foo" }), new[] { "abc", "bcd", "foo" } }, - new object[] { string.Empty, new[] { string.Empty } }, - new object[] { "abc", new[] { "abc" } }, - new object[] { new[] { "abc" }, new[] { "abc" } }, - new object[] { new[] { "abc", "bcd" }, new[] { "abc", "bcd" } }, - new object[] { new[] { "abc", "bcd", "foo" }, new[] { "abc", "bcd", "foo" } }, - new object[] { new[] { null, "abc", "bcd", "foo" }, new[] { null, "abc", "bcd", "foo" } }, - new object[] { new[] { "abc", null, "bcd", "foo" }, new[] { "abc", null, "bcd", "foo" } }, - new object[] { new[] { "abc", "bcd", "foo", null }, new[] { "abc", "bcd", "foo", null } }, - new object[] { new[] { string.Empty, "abc", "bcd", "foo" }, new[] { string.Empty, "abc", "bcd", "foo" } }, - new object[] { new[] { "abc", string.Empty, "bcd", "foo" }, new[] { "abc", string.Empty, "bcd", "foo" } }, - new object[] { new[] { "abc", "bcd", "foo", string.Empty }, new[] { "abc", "bcd", "foo", string.Empty } } - }; - - public static IEnumerable FilledStringValuesToStringToExpected = new List - { - new object[] { default(StringValues), string.Empty }, - new object[] { StringValues.Empty, string.Empty }, - new object[] { new StringValues(string.Empty), string.Empty }, - new object[] { new StringValues("abc"), "abc" }, - new object[] { new StringValues(new[] { "abc" }), "abc" }, - new object[] { new StringValues(new[] { "abc", "bcd" }), "abc,bcd" }, - new object[] { new StringValues(new[] { "abc", "bcd", "foo" }), "abc,bcd,foo" }, - new object[] { string.Empty, string.Empty }, - new object[] { (string)null, string.Empty }, - new object[] { "abc","abc" }, - new object[] { new[] { "abc" }, "abc" }, - new object[] { new[] { "abc", "bcd" }, "abc,bcd" }, - new object[] { new[] { "abc", null, "bcd" }, "abc,bcd" }, - new object[] { new[] { "abc", string.Empty, "bcd" }, "abc,bcd" }, - new object[] { new[] { "abc", "bcd", "foo" }, "abc,bcd,foo" }, - new object[] { new[] { null, "abc", "bcd", "foo" }, "abc,bcd,foo" }, - new object[] { new[] { "abc", null, "bcd", "foo" }, "abc,bcd,foo" }, - new object[] { new[] { "abc", "bcd", "foo", null }, "abc,bcd,foo" }, - new object[] { new[] { string.Empty, "abc", "bcd", "foo" }, "abc,bcd,foo" }, - new object[] { new[] { "abc", string.Empty, "bcd", "foo" }, "abc,bcd,foo" }, - new object[] { new[] { "abc", "bcd", "foo", string.Empty }, "abc,bcd,foo" }, - new object[] { new[] { "abc", "bcd", "foo", string.Empty, null }, "abc,bcd,foo" } - }; - - [Theory] - [TestCaseSource(nameof(DefaultOrNullStringValues))] - [TestCaseSource(nameof(EmptyStringValues))] - [TestCaseSource(nameof(FilledStringValues))] - public void IsReadOnly_True(object stringValuesObj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - Assert.True(((IList)stringValues).IsReadOnly); - Assert.Throws(() => ((IList)stringValues)[0] = string.Empty); - Assert.Throws(() => ((ICollection)stringValues).Add(string.Empty)); - Assert.Throws(() => ((IList)stringValues).Insert(0, string.Empty)); - Assert.Throws(() => ((IList)stringValues).RemoveAt(0)); - Assert.Throws(() => ((ICollection)stringValues).Clear()); - } - - [Theory] - [TestCaseSource(nameof(DefaultOrNullStringValues))] - public void DefaultOrNull_ExpectedValues(object stringValuesObj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - Assert.Null((string[])stringValues); - } - - [Theory] - [TestCaseSource(nameof(DefaultOrNullStringValues))] - [TestCaseSource(nameof(EmptyStringValues))] - public void DefaultNullOrEmpty_ExpectedValues(object stringValuesObj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - Assert.That(stringValues, Is.EqualTo(StringValues.Empty)); - Assert.AreEqual(string.Empty, stringValues.ToString()); - Assert.AreEqual(new string[0], stringValues.ToArray()); - - Assert.True(StringValues.IsNullOrEmpty(stringValues)); - Assert.Throws(() => { var x = stringValues[0]; }); - Assert.Throws(() => { var x = ((IList)stringValues)[0]; }); - Assert.AreEqual(string.Empty, stringValues.ToString()); - Assert.AreEqual(-1, ((IList)stringValues).IndexOf(null)); - Assert.AreEqual(-1, ((IList)stringValues).IndexOf(string.Empty)); - Assert.AreEqual(-1, ((IList)stringValues).IndexOf("not there")); - Assert.False(((ICollection)stringValues).Contains(null)); - Assert.False(((ICollection)stringValues).Contains(string.Empty)); - Assert.False(((ICollection)stringValues).Contains("not there")); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesToStringToExpected))] - public void ToString_ExpectedValues(object stringValuesObj, string expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - Assert.AreEqual(stringValues.ToString(), expected); - } - - [Test] - public void ImplicitStringConverter_Works() - { - string nullString = null; - StringValues stringValues = nullString; - Assert.That(stringValues, Is.EqualTo(StringValues.Empty)); - Assert.Null((string)stringValues); - Assert.Null((string[])stringValues); - - string aString = "abc"; - stringValues = aString; - Assert.AreEqual(1, stringValues.Count); - Assert.AreEqual(aString, stringValues); - Assert.AreEqual(aString, stringValues[0]); - Assert.AreEqual(aString, ((IList)stringValues)[0]); - Assert.AreEqual(new string[] { aString }, stringValues); - } - - [Test] - public void GetHashCode_SingleValueVsArrayWithOneItem_SameHashCode() - { - var sv1 = new StringValues("value"); - var sv2 = new StringValues(new[] { "value" }); - Assert.AreEqual(sv1, sv2); - Assert.AreEqual(sv1.GetHashCode(), sv2.GetHashCode()); - } - - [Test] - public void GetHashCode_NullCases_DifferentHashCodes() - { - var sv1 = new StringValues((string)null); - var sv2 = new StringValues(new[] { (string)null }); - Assert.AreNotEqual(sv1, sv2); - Assert.AreNotEqual(sv1.GetHashCode(), sv2.GetHashCode()); - - var sv3 = new StringValues((string[])null); - Assert.AreEqual(sv1, sv3); - Assert.AreEqual(sv1.GetHashCode(), sv3.GetHashCode()); - } - - [Test] - public void GetHashCode_SingleValueVsArrayWithTwoItems_DifferentHashCodes() - { - var sv1 = new StringValues("value"); - var sv2 = new StringValues(new[] { "value", "value" }); - Assert.AreNotEqual(sv1, sv2); - Assert.AreNotEqual(sv1.GetHashCode(), sv2.GetHashCode()); - } - - [Test] - public void ImplicitStringArrayConverter_Works() - { - string[] nullStringArray = null; - StringValues stringValues = nullStringArray; - Assert.That(stringValues, Is.EqualTo(StringValues.Empty)); - Assert.Null((string)stringValues); - Assert.Null((string[])stringValues); - - string aString = "abc"; - string[] aStringArray = new[] { aString }; - stringValues = aStringArray; - Assert.AreEqual(1, stringValues.Count); - Assert.AreEqual(aString, stringValues); - Assert.AreEqual(aString, stringValues[0]); - Assert.AreEqual(aString, ((IList)stringValues)[0]); - Assert.AreEqual(aStringArray, stringValues); - - aString = "abc"; - string bString = "bcd"; - aStringArray = new[] { aString, bString }; - stringValues = aStringArray; - Assert.AreEqual(2, stringValues.Count); - Assert.AreEqual("abc,bcd", stringValues.ToString()); - Assert.AreEqual(aStringArray, stringValues); - } - - [Theory] - [TestCaseSource(nameof(DefaultOrNullStringValues))] - [TestCaseSource(nameof(EmptyStringValues))] - public void DefaultNullOrEmpty_Enumerator(object stringValuesObj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - var e = stringValues.GetEnumerator(); - Assert.Null(e.Current); - Assert.False(e.MoveNext()); - Assert.Null(e.Current); - Assert.False(e.MoveNext()); - Assert.False(e.MoveNext()); - Assert.False(e.MoveNext()); - - var e1 = ((IEnumerable)stringValues).GetEnumerator(); - Assert.Null(e1.Current); - Assert.False(e1.MoveNext()); - Assert.Null(e1.Current); - Assert.False(e1.MoveNext()); - Assert.False(e1.MoveNext()); - Assert.False(e1.MoveNext()); - - var e2 = ((IEnumerable)stringValues).GetEnumerator(); - Assert.Null(e2.Current); - Assert.False(e2.MoveNext()); - Assert.Null(e2.Current); - Assert.False(e2.MoveNext()); - Assert.False(e2.MoveNext()); - Assert.False(e2.MoveNext()); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpected))] - public void Enumerator(object stringValuesObj, string[] expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - var e = stringValues.GetEnumerator(); - for (int i = 0; i < expected.Length; i++) - { - Assert.True(e.MoveNext()); - Assert.AreEqual(expected[i], e.Current); - } - Assert.False(e.MoveNext()); - Assert.False(e.MoveNext()); - Assert.False(e.MoveNext()); - - var e1 = ((IEnumerable)stringValues).GetEnumerator(); - for (int i = 0; i < expected.Length; i++) - { - Assert.True(e1.MoveNext()); - Assert.AreEqual(expected[i], e1.Current); - } - Assert.False(e1.MoveNext()); - Assert.False(e1.MoveNext()); - Assert.False(e1.MoveNext()); - - var e2 = ((IEnumerable)stringValues).GetEnumerator(); - for (int i = 0; i < expected.Length; i++) - { - Assert.True(e2.MoveNext()); - Assert.AreEqual(expected[i], e2.Current); - } - Assert.False(e2.MoveNext()); - Assert.False(e2.MoveNext()); - Assert.False(e2.MoveNext()); - } - - [Test] - public void Indexer() - { - StringValues sv; - - // Default empty - sv = default; - Assert.Throws(() => { var x = sv[0]; }); - Assert.Throws(() => { var x = sv[-1]; }); - - // Empty with null string ctor - sv = new StringValues((string)null); - Assert.Throws(() => { var x = sv[0]; }); - Assert.Throws(() => { var x = sv[-1]; }); - // Empty with null string[] ctor - sv = new StringValues((string[])null); - Assert.Throws(() => { var x = sv[0]; }); - Assert.Throws(() => { var x = sv[-1]; }); - - // Empty with array - sv = Array.Empty(); - Assert.Throws(() => { var x = sv[0]; }); - Assert.Throws(() => { var x = sv[-1]; }); - - // One element with string - sv = "hello"; - Assert.AreEqual("hello", sv[0]); - Assert.Throws(() => { var x = sv[1]; }); - Assert.Throws(() => { var x = sv[-1]; }); - - // One element with string[] - sv = new string[] { "hello" }; - Assert.AreEqual("hello", sv[0]); - Assert.Throws(() => { var x = sv[1]; }); - Assert.Throws(() => { var x = sv[-1]; }); - - // One element with string[] containing null - sv = new string[] { null }; - Assert.Null(sv[0]); - Assert.Throws(() => { var x = sv[1]; }); - Assert.Throws(() => { var x = sv[-1]; }); - - // Two elements with string[] - sv = new string[] { "hello", "world" }; - Assert.AreEqual("hello", sv[0]); - Assert.AreEqual("world", sv[1]); - Assert.Throws(() => { var x = sv[2]; }); - Assert.Throws(() => { var x = sv[-1]; }); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpected))] - public void IndexOf(object stringValuesObj, string[] expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - IList list = stringValues; - Assert.AreEqual(-1, list.IndexOf("not there")); - for (int i = 0; i < expected.Length; i++) - { - Assert.AreEqual(i, list.IndexOf(expected[i])); - } - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpected))] - public void Contains(object stringValuesObj, string[] expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - - ICollection collection = stringValues; - Assert.False(collection.Contains("not there")); - for (int i = 0; i < expected.Length; i++) - { - Assert.True(collection.Contains(expected[i])); - } - - } - - private static StringValues ImplicitlyCastStringValues(object stringValuesObj) - { - StringValues stringValues = (string)null; - if (stringValuesObj is StringValues strVal) - { - stringValues = strVal; - } - if (stringValuesObj is string[] strArr) - { - stringValues = strArr; - } - if (stringValuesObj is string str) - { - stringValues = str; - } - - return stringValues; - } - - [Theory] - [TestCaseSource(nameof(DefaultOrNullStringValues))] - [TestCaseSource(nameof(EmptyStringValues))] - [TestCaseSource(nameof(FilledStringValues))] - public void CopyTo_TooSmall(object stringValuesObj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - ICollection collection = stringValues; - string[] tooSmall = new string[0]; - - if (collection.Count > 0) - { - Assert.Throws(() => collection.CopyTo(tooSmall, 0)); - } - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpected))] - public void CopyTo_CorrectSize(object stringValuesObj, string[] expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - ICollection collection = stringValues; - string[] actual = new string[expected.Length]; - - if (collection.Count > 0) - { - Assert.Throws(() => collection.CopyTo(actual, -1)); - Assert.Throws(() => collection.CopyTo(actual, actual.Length + 1)); - } - collection.CopyTo(actual, 0); - Assert.AreEqual(expected, actual); - } - - [Theory] - [TestCaseSource(nameof(DefaultOrNullStringValues))] - [TestCaseSource(nameof(EmptyStringValues))] - public void DefaultNullOrEmpty_Concat(object stringValuesObj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - string[] expected = new[] { "abc", "bcd", "foo" }; - StringValues expectedStringValues = new StringValues(expected); - Assert.AreEqual(expected, StringValues.Concat(stringValues, expectedStringValues)); - Assert.AreEqual(expected, StringValues.Concat(expectedStringValues, stringValues)); - Assert.AreEqual(expected, StringValues.Concat((string)null, in expectedStringValues)); - Assert.AreEqual(expected, StringValues.Concat(in expectedStringValues, (string)null)); - - string[] empty = new string[0]; - StringValues emptyStringValues = new StringValues(empty); - Assert.AreEqual(empty, StringValues.Concat(stringValues, StringValues.Empty)); - Assert.AreEqual(empty, StringValues.Concat(StringValues.Empty, stringValues)); - Assert.AreEqual(empty, StringValues.Concat(stringValues, new StringValues())); - Assert.AreEqual(empty, StringValues.Concat(new StringValues(), stringValues)); - Assert.AreEqual(empty, StringValues.Concat((string)null, in emptyStringValues)); - Assert.AreEqual(empty, StringValues.Concat(in emptyStringValues, (string)null)); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpected))] - public void Concat(object stringValuesObj, string[] array) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - string[] filled = new[] { "abc", "bcd", "foo" }; - - string[] expectedPrepended = array.Concat(filled).ToArray(); - Assert.AreEqual(expectedPrepended, StringValues.Concat(stringValues, new StringValues(filled))); - - string[] expectedAppended = filled.Concat(array).ToArray(); - Assert.AreEqual(expectedAppended, StringValues.Concat(new StringValues(filled), stringValues)); - - StringValues values = stringValues; - foreach (string s in filled) - { - values = StringValues.Concat(in values, s); - } - Assert.AreEqual(expectedPrepended, values); - - values = stringValues; - foreach (string s in filled.Reverse()) - { - values = StringValues.Concat(s, in values); - } - Assert.AreEqual(expectedAppended, values); - } - - [Test] - public void Equals_OperatorEqual() - { - var equalString = "abc"; - - var equalStringArray = new string[] { equalString }; - var equalStringValues = new StringValues(equalString); - var otherStringValues = new StringValues(equalString); - var stringArray = new string[] { equalString, equalString }; - var stringValuesArray = new StringValues(stringArray); - - Assert.True(equalStringValues == otherStringValues); - - Assert.True(equalStringValues == equalString); - Assert.True(equalString == equalStringValues); - - Assert.True(equalStringValues == equalStringArray); - Assert.True(equalStringArray == equalStringValues); - - Assert.True(stringArray == stringValuesArray); - Assert.True(stringValuesArray == stringArray); - - Assert.False(stringValuesArray == equalString); - Assert.False(stringValuesArray == equalStringArray); - Assert.False(stringValuesArray == equalStringValues); - } - - [Test] - public void Equals_OperatorNotEqual() - { - var equalString = "abc"; - - var equalStringArray = new string[] { equalString }; - var equalStringValues = new StringValues(equalString); - var otherStringValues = new StringValues(equalString); - var stringArray = new string[] { equalString, equalString }; - var stringValuesArray = new StringValues(stringArray); - - Assert.False(equalStringValues != otherStringValues); - - Assert.False(equalStringValues != equalString); - Assert.False(equalString != equalStringValues); - - Assert.False(equalStringValues != equalStringArray); - Assert.False(equalStringArray != equalStringValues); - - Assert.False(stringArray != stringValuesArray); - Assert.False(stringValuesArray != stringArray); - - Assert.True(stringValuesArray != equalString); - Assert.True(stringValuesArray != equalStringArray); - Assert.True(stringValuesArray != equalStringValues); - } - - [Test] - public void Equals_Instance() - { - var equalString = "abc"; - - var equalStringArray = new string[] { equalString }; - var equalStringValues = new StringValues(equalString); - var stringArray = new string[] { equalString, equalString }; - var stringValuesArray = new StringValues(stringArray); - - Assert.True(equalStringValues.Equals(equalStringValues)); - Assert.True(equalStringValues.Equals(equalString)); - Assert.True(equalStringValues.Equals(equalStringArray)); - Assert.True(stringValuesArray.Equals(stringArray)); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpectedObjects))] - public void Equals_ObjectEquals(object stringValuesObj, object obj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - Assert.True(stringValues == obj); - Assert.True(obj == stringValues); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpectedObjects))] - public void Equals_ObjectNotEquals(object stringValuesObj, object obj) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - Assert.False(stringValues != obj); - Assert.False(obj != stringValues); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpectedStrings))] - public void Equals_String(object stringValuesObj, string expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - var notEqual = new StringValues("bcd"); - - Assert.True(StringValues.Equals(stringValues, expected)); - Assert.False(StringValues.Equals(stringValues, notEqual)); - - Assert.True(StringValues.Equals(stringValues, new StringValues(expected))); - Assert.AreEqual(stringValues.GetHashCode(), new StringValues(expected).GetHashCode()); - } - - [Theory] - [TestCaseSource(nameof(FilledStringValuesWithExpected))] - public void Equals_StringArray(object stringValuesObj, string[] expected) - { - StringValues stringValues = ImplicitlyCastStringValues(stringValuesObj); - var notEqual = new StringValues(new[] { "bcd", "abc" }); - - Assert.True(StringValues.Equals(stringValues, expected)); - Assert.False(StringValues.Equals(stringValues, notEqual)); - - Assert.True(StringValues.Equals(stringValues, new StringValues(expected))); - Assert.AreEqual(stringValues.GetHashCode(), new StringValues(expected).GetHashCode()); - } - } -} diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 304b71e3c7d02..e259e7ad7a2d7 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -20,34 +20,22 @@ + - - - - - - - - - - - - - From 8e0baf983a99afb7a3ac8f7e5b4183057a6a3e2f Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 17 Aug 2020 18:15:22 -0500 Subject: [PATCH 09/14] remove consts and error classes --- .../src/Shared/Multipart/BatchConstants.cs | 29 ----------------- .../src/Shared/Multipart/BatchErrors.cs | 27 ---------------- .../src/Shared/Multipart/MemoryResponse.cs | 9 ++++-- .../src/Shared/Multipart/Multipart.cs | 31 +++++++++++++------ .../src/Azure.Data.Tables.csproj | 2 ++ 5 files changed, 30 insertions(+), 68 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Shared/Multipart/BatchConstants.cs delete mode 100644 sdk/core/Azure.Core/src/Shared/Multipart/BatchErrors.cs diff --git a/sdk/core/Azure.Core/src/Shared/Multipart/BatchConstants.cs b/sdk/core/Azure.Core/src/Shared/Multipart/BatchConstants.cs deleted file mode 100644 index b04a1c09cbdbd..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/Multipart/BatchConstants.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Azure.Core -{ - /// - /// Constants used by the batching APIs. - /// - internal static class BatchConstants - { - public const int KB = 1024; - public const int NoStatusCode = 0; - public const int RequestBufferSize = KB; - public const int ResponseLineSize = 4 * KB; - public const int ResponseBufferSize = 16 * KB; - - public const string XmsVersionName = "x-ms-version"; - public const string XmsClientRequestIdName = "x-ms-client-request-id"; - public const string XmsReturnClientRequestIdName = "x-ms-return-client-request-id"; - public const string ContentIdName = "Content-ID"; - public const string ContentLengthName = "Content-Length"; - - public const string MultipartContentTypePrefix = "multipart/mixed; boundary="; - public const string RequestContentType = "Content-Type: application/http"; - public const string RequestContentTransferEncoding = "Content-Transfer-Encoding: binary"; - public const string BatchSeparator = "--"; - public const string HttpVersion = "HTTP/1.1"; - } -} diff --git a/sdk/core/Azure.Core/src/Shared/Multipart/BatchErrors.cs b/sdk/core/Azure.Core/src/Shared/Multipart/BatchErrors.cs deleted file mode 100644 index 36aefa3cd5f63..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/Multipart/BatchErrors.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Azure.Core.Pipeline; - -namespace Azure.Core -{ - /// - /// Errors raised by the batching APIs. - /// - internal static class BatchErrors - { - public static InvalidOperationException InvalidBatchContentType(string contentType) => - new InvalidOperationException($"Expected {HttpHeader.Names.ContentType} to start with {BatchConstants.MultipartContentTypePrefix} but received {contentType}"); - - public static InvalidOperationException InvalidHttpStatusLine(string statusLine) => - new InvalidOperationException($"Expected an HTTP status line, not {statusLine}"); - - public static InvalidOperationException InvalidHttpHeaderLine(string headerLine) => - new InvalidOperationException($"Expected an HTTP header line, not {headerLine}"); - - public static RequestFailedException InvalidResponse(ClientDiagnostics clientDiagnostics, Response response, Exception innerException) => - clientDiagnostics.CreateRequestFailedExceptionWithContent(response, message: "Invalid response", innerException: innerException); - - } -} diff --git a/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs index e924fce0d672d..b2ef9adb95502 100644 --- a/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs @@ -17,10 +17,13 @@ namespace Azure.Core /// internal class MemoryResponse : Response { + private const int NoStatusCode = 0; + private const string XmsClientRequestIdName = "x-ms-client-request-id"; + /// /// The Response . /// - private int _status = BatchConstants.NoStatusCode; + private int _status = NoStatusCode; /// /// The Response . @@ -45,8 +48,8 @@ internal class MemoryResponse : Response /// public override string ClientRequestId { - get => TryGetHeader(BatchConstants.XmsClientRequestIdName, out string id) ? id : null; - set => SetHeader(BatchConstants.XmsClientRequestIdName, value); + get => TryGetHeader(XmsClientRequestIdName, out string id) ? id : null; + set => SetHeader(XmsClientRequestIdName, value); } /// diff --git a/sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs b/sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs index bcaa2c8b927a9..6047d8bb7f139 100644 --- a/sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/Multipart.cs @@ -22,6 +22,19 @@ namespace Azure.Core /// internal static class Multipart { + private const int KB = 1024; + private const int ResponseLineSize = 4 * KB; + private const string MultipartContentTypePrefix = "multipart/mixed; boundary="; + private const string ContentIdName = "Content-ID"; + internal static InvalidOperationException InvalidBatchContentType(string contentType) => + new InvalidOperationException($"Expected {HttpHeader.Names.ContentType} to start with {MultipartContentTypePrefix} but received {contentType}"); + + internal static InvalidOperationException InvalidHttpStatusLine(string statusLine) => + new InvalidOperationException($"Expected an HTTP status line, not {statusLine}"); + + internal static InvalidOperationException InvalidHttpHeaderLine(string headerLine) => + new InvalidOperationException($"Expected an HTTP header line, not {headerLine}"); + /// /// Parse a multipart/mixed response body into several responses. /// @@ -44,7 +57,7 @@ internal static async Task ParseAsync( // Get the batch boundary if (!GetBoundary(batchContentType, out string batchBoundary)) { - throw BatchErrors.InvalidBatchContentType(batchContentType); + throw InvalidBatchContentType(batchContentType); } // Collect the responses in a dictionary (in case the Content-ID @@ -70,7 +83,7 @@ internal static async Task ParseAsync( continue; } // Get the Content-ID header - if (!section.Headers.TryGetValue(BatchConstants.ContentIdName, out string [] contentIdValues) || + if (!section.Headers.TryGetValue(ContentIdName, out string [] contentIdValues) || contentIdValues.Length != 1 || !int.TryParse(contentIdValues[0], out int contentId)) { @@ -96,14 +109,14 @@ internal static async Task ParseAsync( } // We're going to read the section's response body line by line - using var body = new BufferedReadStream(section.Body, BatchConstants.ResponseLineSize); + using var body = new BufferedReadStream(section.Body, ResponseLineSize); // The first line is the status like "HTTP/1.1 202 Accepted" string line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); string[] status = line.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); if (status.Length != 3) { - throw BatchErrors.InvalidHttpStatusLine(line); + throw InvalidHttpStatusLine(line); } response.SetStatus(int.Parse(status[1], CultureInfo.InvariantCulture)); response.SetReasonPhrase(status[2]); @@ -116,7 +129,7 @@ internal static async Task ParseAsync( int splitIndex = line.IndexOf(':'); if (splitIndex <= 0) { - throw BatchErrors.InvalidHttpHeaderLine(line); + throw InvalidHttpHeaderLine(line); } var name = line.Substring(0, splitIndex); var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); @@ -171,8 +184,8 @@ internal static async Task ReadLineAsync( bool async, CancellationToken cancellationToken) => async ? - await stream.ReadLineAsync(BatchConstants.ResponseLineSize, cancellationToken).ConfigureAwait(false) : - stream.ReadLine(BatchConstants.ResponseLineSize); + await stream.ReadLineAsync(ResponseLineSize, cancellationToken).ConfigureAwait(false) : + stream.ReadLine(ResponseLineSize); /// /// Read the next multipart section. @@ -198,12 +211,12 @@ await reader.ReadNextSectionAsync(cancellationToken).ConfigureAwait(false) : private static bool GetBoundary(string contentType, out string batchBoundary) { - if (contentType == null || !contentType.StartsWith(BatchConstants.MultipartContentTypePrefix, StringComparison.Ordinal)) + if (contentType == null || !contentType.StartsWith(MultipartContentTypePrefix, StringComparison.Ordinal)) { batchBoundary = null; return false; } - batchBoundary = contentType.Substring(BatchConstants.MultipartContentTypePrefix.Length); + batchBoundary = contentType.Substring(MultipartContentTypePrefix.Length); return true; } } diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index e259e7ad7a2d7..7cf2b0a3ee9d6 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -21,6 +21,8 @@ + + From 673a3844417c072782d6f5840385255ef3ebb7c7 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Tue, 18 Aug 2020 07:59:22 -0500 Subject: [PATCH 10/14] explicit line breaks in SendMultipartData test --- .../tests/HttpPipelineFunctionalTests.cs | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index 88e52556d897b..2995d4e81cdc2 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -509,46 +509,46 @@ public async Task SendMultipartData() using Response response = await ExecuteRequest(request, httpPipeline); Console.WriteLine(requestBody); - Assert.That(requestBody, Is.EqualTo(@$"--batch_{batchGuid} -{HttpHeader.Names.ContentType}: multipart/mixed; boundary=changeset_{changesetGuid} - ---changeset_{changesetGuid} -{HttpHeader.Names.ContentType}: application/http -{cteHeaderName}: {Binary} - -POST {postUri} HTTP/1.1 -{HttpHeader.Names.Host}: {Host} -{HttpHeader.Names.Accept}: {ApplicationJson} -{DataServiceVersion}: {Three0} -{HttpHeader.Names.ContentType}: {ApplicationJsonOdata} - -{post1Body} ---changeset_{changesetGuid} -{HttpHeader.Names.ContentType}: application/http -{cteHeaderName}: {Binary} - -POST {postUri} HTTP/1.1 -{HttpHeader.Names.Host}: {Host} -{HttpHeader.Names.Accept}: {ApplicationJson} -{DataServiceVersion}: {Three0} -{HttpHeader.Names.ContentType}: {ApplicationJsonOdata} - -{post2Body} ---changeset_{changesetGuid} -{HttpHeader.Names.ContentType}: application/http -{cteHeaderName}: {Binary} - -PATCH {mergeUri} HTTP/1.1 -{HttpHeader.Names.Host}: {Host} -{HttpHeader.Names.Accept}: {ApplicationJson} -{DataServiceVersion}: {Three0} -{HttpHeader.Names.ContentType}: {ApplicationJsonOdata} - -{patchBody} ---changeset_{changesetGuid}-- - ---batch_{batchGuid}-- -")); + Assert.That(requestBody, Is.EqualTo($"--batch_{batchGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: multipart/mixed; boundary=changeset_{changesetGuid}\r\n" + + $"\r\n" + + $"--changeset_{changesetGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: application/http\r\n" + + $"{cteHeaderName}: {Binary}\r\n" + + $"\r\n" + + $"POST {postUri} HTTP/1.1\r\n" + + $"{HttpHeader.Names.Host}: {Host}\r\n" + + $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + + $"{DataServiceVersion}: {Three0}\r\n" + + $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + + $"\r\n" + + $"{post1Body}\r\n" + + $"--changeset_{changesetGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: application/http\r\n" + + $"{cteHeaderName}: {Binary}\r\n" + + $"\r\n" + + $"POST {postUri} HTTP/1.1\r\n" + + $"{HttpHeader.Names.Host}: {Host}\r\n" + + $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + + $"{DataServiceVersion}: {Three0}\r\n" + + $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + + $"\r\n" + + $"{post2Body}\r\n" + + $"--changeset_{changesetGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: application/http\r\n" + + $"{cteHeaderName}: {Binary}\r\n" + + $"\r\n" + + $"PATCH {mergeUri} HTTP/1.1\r\n" + + $"{HttpHeader.Names.Host}: {Host}\r\n" + + $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + + $"{DataServiceVersion}: {Three0}\r\n" + + $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + + $"\r\n" + + $"{patchBody}\r\n" + + $"--changeset_{changesetGuid}--\r\n" + + $"\r\n" + + $"--batch_{batchGuid}--\r\n" + + $"")); } private class TestOptions : ClientOptions From 459678f4357ad838a3f7c4ebc7a5e66fdc886576 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 19 Aug 2020 17:09:34 -0500 Subject: [PATCH 11/14] pr comments --- sdk/core/Azure.Core/src/RequestContent.cs | 3 +- .../src/Shared/RequestRequestContent.cs | 4 +- sdk/core/Azure.Core/src/Shared/ThrowHelper.cs | 112 ------------------ .../Azure.Core/tests/Azure.Core.Tests.csproj | 1 - .../tests/TransportFunctionalTests.cs | 10 +- .../src/Azure.Data.Tables.csproj | 11 +- 6 files changed, 9 insertions(+), 132 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Shared/ThrowHelper.cs diff --git a/sdk/core/Azure.Core/src/RequestContent.cs b/sdk/core/Azure.Core/src/RequestContent.cs index 100c5551560bd..b5537c134d258 100644 --- a/sdk/core/Azure.Core/src/RequestContent.cs +++ b/sdk/core/Azure.Core/src/RequestContent.cs @@ -106,8 +106,7 @@ public override void WriteTo(Stream stream, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var read = _stream.Read(buffer, 0, buffer.Length); - if (read == 0) - { break; } + if (read == 0) { break; } cancellationToken.ThrowIfCancellationRequested(); stream.Write(buffer, 0, read); } diff --git a/sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs b/sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs index bcb935205c977..5ab190bd68468 100644 --- a/sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs +++ b/sdk/core/Azure.Core/src/Shared/RequestRequestContent.cs @@ -141,9 +141,9 @@ private static void SerializeRequestLine(StringBuilder message, Request request) message.Append("HTTP/1.1" + CRLF); // Only insert host header if not already present. - if (!request.Headers.TryGetValue(HttpHeader.Names.Host, out _)) + if (!request.Headers.TryGetValue("Host", out _)) { - message.Append(HttpHeader.Names.Host + ColonSP + request.Uri.Host + CRLF); + message.Append("Host" + ColonSP + request.Uri.Host + CRLF); } } diff --git a/sdk/core/Azure.Core/src/Shared/ThrowHelper.cs b/sdk/core/Azure.Core/src/Shared/ThrowHelper.cs deleted file mode 100644 index 1ff6d014759ea..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/ThrowHelper.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Copied from https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Primitives/src - -using System; -using System.Diagnostics; - -#pragma warning disable CA1305 // ToString Locale -#pragma warning disable IDE0051 // Private member not used - -namespace Azure.Core -{ - internal static class ThrowHelper - { - internal static void ThrowArgumentNullException(ExceptionArgument argument) - { - throw new ArgumentNullException(GetArgumentName(argument)); - } - - internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) - { - throw new ArgumentOutOfRangeException(GetArgumentName(argument)); - } - - internal static void ThrowArgumentException(ExceptionResource resource) - { - throw new ArgumentException(GetResourceText(resource)); - } - - internal static void ThrowInvalidOperationException(ExceptionResource resource) - { - throw new InvalidOperationException(GetResourceText(resource)); - } - - internal static void ThrowInvalidOperationException(ExceptionResource resource, params object[] args) - { - var message = string.Format(GetResourceText(resource), args); - - throw new InvalidOperationException(message); - } - - internal static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) - { - return new ArgumentNullException(GetArgumentName(argument)); - } - - internal static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument) - { - return new ArgumentOutOfRangeException(GetArgumentName(argument)); - } - - internal static ArgumentException GetArgumentException(ExceptionResource resource) - { - return new ArgumentException(GetResourceText(resource)); - } - - private static string GetResourceText(ExceptionResource resource) - { - // return Resources.ResourceManager.GetString(GetResourceName(resource), Resources.Culture); - // Hack to avoid including the resx: - return resource switch - { - ExceptionResource.Argument_InvalidOffsetLength => "Offset and length are out of bounds for the string or length is greater than the number of characters from index to the end of the string.", - ExceptionResource.Argument_InvalidOffsetLengthStringSegment => "Offset and length are out of bounds for this StringSegment or length is greater than the number of characters to the end of this StringSegment.", - ExceptionResource.Capacity_CannotChangeAfterWriteStarted => "Cannot change capacity after write started.", - ExceptionResource.Capacity_NotEnough => "Not enough capacity to write '{0}' characters, only '{1}' left.", - ExceptionResource.Capacity_NotUsedEntirely => "Entire reserved capacity was not used. Capacity: '{0}', written '{1}'.", - _ => throw new ArgumentOutOfRangeException(nameof(resource)) - }; - } - - private static string GetArgumentName(ExceptionArgument argument) - { - Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), - "The enum value is not defined, please check the ExceptionArgument Enum."); - - return argument.ToString(); - } - - private static string GetResourceName(ExceptionResource resource) - { - Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), - "The enum value is not defined, please check the ExceptionResource Enum."); - - return resource.ToString(); - } - } - - internal enum ExceptionArgument - { - buffer, - offset, - length, - text, - start, - count, - index, - value, - capacity, - separators - } - - internal enum ExceptionResource - { - Argument_InvalidOffsetLength, - Argument_InvalidOffsetLengthStringSegment, - Capacity_CannotChangeAfterWriteStarted, - Capacity_NotEnough, - Capacity_NotUsedEntirely - } -} diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 0820ac8fcc5ef..8bec8e06c09e9 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -34,7 +34,6 @@ - diff --git a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs index 5a987f200e7a0..0e508fc9d9226 100644 --- a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs @@ -239,7 +239,7 @@ public async Task CanGetAndSetMethod(RequestMethod method, string expectedMethod [TestCaseSource(nameof(AllHeadersWithValuesAndType))] public async Task CanGetAndAddRequestHeaders(string headerName, string headerValue, bool contentHeader) { - Microsoft.Extensions.Primitives.StringValues httpHeaderValues; + StringValues httpHeaderValues; using TestServer testServer = new TestServer( context => @@ -298,7 +298,7 @@ public async Task CanAddMultipleValuesToRequestHeader(string headerName, string var anotherHeaderValue = headerValue + "1"; var joinedHeaderValues = headerValue + "," + anotherHeaderValue; - Microsoft.Extensions.Primitives.StringValues httpHeaderValues; + StringValues httpHeaderValues; using TestServer testServer = new TestServer( context => @@ -366,7 +366,7 @@ public async Task CanGetAndSetMultiValueResponseHeaders(string headerName, strin context => { context.Response.Headers.Add(headerName, - new Microsoft.Extensions.Primitives.StringValues(new[] + new StringValues(new[] { headerValue, anotherHeaderValue @@ -416,7 +416,7 @@ public async Task CanRemoveHeaders(string headerName, string headerValue, bool c public async Task CanSetRequestHeaders(string headerName, string headerValue, bool contentHeader) { - Microsoft.Extensions.Primitives.StringValues httpHeaderValues; + StringValues httpHeaderValues; using TestServer testServer = new TestServer( context => @@ -511,7 +511,7 @@ public async Task CanGetAndSetContentStream() [Test] public async Task ContentLength0WhenNoContent() { - Microsoft.Extensions.Primitives.StringValues contentLengthHeader = default; + StringValues contentLengthHeader = default; using TestServer testServer = new TestServer( context => { diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 7cf2b0a3ee9d6..985b1e7c571ac 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -21,8 +21,6 @@ - - @@ -37,14 +35,7 @@ - - - - - - + From b9cab1876c442f255e6cdff2a70c052a9764419a Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 20 Aug 2020 08:43:33 -0500 Subject: [PATCH 12/14] multipart tests --- sdk/core/Azure.Core/src/RequestContent.cs | 1 - .../src/Shared/Multipart/MemoryResponse.cs | 12 ++ .../{ => Multipart}/StreamHelperExtensions.cs | 0 .../Azure.Core/tests/Azure.Core.Tests.csproj | 6 +- sdk/core/Azure.Core/tests/MultipartTests.cs | 174 ++++++++++++++++++ .../src/Azure.Data.Tables.csproj | 1 - 6 files changed, 189 insertions(+), 5 deletions(-) rename sdk/core/Azure.Core/src/Shared/{ => Multipart}/StreamHelperExtensions.cs (100%) create mode 100644 sdk/core/Azure.Core/tests/MultipartTests.cs diff --git a/sdk/core/Azure.Core/src/RequestContent.cs b/sdk/core/Azure.Core/src/RequestContent.cs index b5537c134d258..d30d4091defaf 100644 --- a/sdk/core/Azure.Core/src/RequestContent.cs +++ b/sdk/core/Azure.Core/src/RequestContent.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using System.Threading; using System.Buffers; -using System.Collections.Generic; namespace Azure.Core { diff --git a/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs b/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs index b2ef9adb95502..b63f440ece2e8 100644 --- a/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs +++ b/sdk/core/Azure.Core/src/Shared/Multipart/MemoryResponse.cs @@ -114,14 +114,23 @@ public void AddHeader(string name, string value) } /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif protected override bool ContainsHeader(string name) => _headers.ContainsKey(name); /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif protected override IEnumerable EnumerateHeaders() => _headers.Select(header => new HttpHeader(header.Key, JoinHeaderValues(header.Value))); /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif protected override bool TryGetHeader(string name, out string value) { if (_headers.TryGetValue(name, out List headers)) @@ -134,6 +143,9 @@ protected override bool TryGetHeader(string name, out string value) } /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif protected override bool TryGetHeaderValues(string name, out IEnumerable values) { bool found = _headers.TryGetValue(name, out List headers); diff --git a/sdk/core/Azure.Core/src/Shared/StreamHelperExtensions.cs b/sdk/core/Azure.Core/src/Shared/Multipart/StreamHelperExtensions.cs similarity index 100% rename from sdk/core/Azure.Core/src/Shared/StreamHelperExtensions.cs rename to sdk/core/Azure.Core/src/Shared/Multipart/StreamHelperExtensions.cs diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 8bec8e06c09e9..7a6449b58fb6e 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -2,6 +2,7 @@ {84491222-6C36-4FA7-BBAE-1FA804129151} $(RequiredTargetFrameworks) + $(DefineConstants);HAS_INTERNALS_VISIBLE_CORE true @@ -23,8 +24,8 @@ - - + @@ -33,7 +34,6 @@ - diff --git a/sdk/core/Azure.Core/tests/MultipartTests.cs b/sdk/core/Azure.Core/tests/MultipartTests.cs new file mode 100644 index 0000000000000..674c2aebcab25 --- /dev/null +++ b/sdk/core/Azure.Core/tests/MultipartTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable warnings + +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class MultipartTests + { + private const string Boundary = "batchresponse_6040fee7-a2b8-4e78-a674-02086369606a"; + private const string ContentType = "multipart/mixed; boundary=" + Boundary; + private const string Body = "{}"; + + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string TablesOdataBatchResponse = +"--batchresponse_6040fee7-a2b8-4e78-a674-02086369606a\r\n" + +"Content-Type: multipart/mixed; boundary=changesetresponse_e52cbca8-7e7f-4d91-a719-f99c69686a92\r\n" + +"\r\n" + +"--changesetresponse_e52cbca8-7e7f-4d91-a719-f99c69686a92\r\n" + +"Content-Type: application/http\r\n" + +"Content-Transfer-Encoding: binary\r\n" + +"\r\n" + +"HTTP/1.1 201 Created\r\n" + +"DataServiceVersion: 3.0;\r\n" + +"Content-Type: application/json;odata=fullmetadata;streaming=true;charset=utf-8\r\n" + +"X-Content-Type-Options: nosniff\r\n" + +"Cache-Control: no-cache\r\n" + +"Location: https://mytable.table.core.windows.net/tablename(PartitionKey='somPartition',RowKey='01')\r\n" + +"ETag: W/\"datetime'2020-08-14T22%3A58%3A57.8328323Z'\"\r\n" + +"\r\n" + +"{}\r\n" + +"--changesetresponse_e52cbca8-7e7f-4d91-a719-f99c69686a92\r\n" + +"Content-Type: application/http\r\n" + +"Content-Transfer-Encoding: binary\r\n" + +"\r\n" + +"HTTP/1.1 201 Created\r\n" + +"DataServiceVersion: 3.0;\r\n" + +"Content-Type: application/json;odata=fullmetadata;streaming=true;charset=utf-8\r\n" + +"X-Content-Type-Options: nosniff\r\n" + +"Cache-Control: no-cache\r\n" + +"Location: https://mytable.table.core.windows.net/tablename(PartitionKey='somPartition',RowKey='02')\r\n" + +"ETag: W/\"datetime'2020-08-14T22%3A58%3A57.8328323Z'\"\r\n" + +"\r\n" + +"{}\r\n" + +"--changesetresponse_e52cbca8-7e7f-4d91-a719-f99c69686a92\r\n" + +"Content-Type: application/http\r\n" + +"Content-Transfer-Encoding: binary\r\n" + +"\r\n" + +"HTTP/1.1 201 Created\r\n" + +"DataServiceVersion: 3.0;\r\n" + +"Content-Type: application/json;odata=fullmetadata;streaming=true;charset=utf-8\r\n" + +"X-Content-Type-Options: nosniff\r\n" + +"Cache-Control: no-cache\r\n" + +"Location: https://mytable.table.core.windows.net/tablename(PartitionKey='somPartition',RowKey='03')\r\n" + +"ETag: W/\"datetime'2020-08-14T22%3A58%3A57.8328323Z'\"\r\n" + +"\r\n" + +"{}\r\n" + +"--changesetresponse_e52cbca8-7e7f-4d91-a719-f99c69686a92--\r\n" + +"--batchresponse_6040fee7-a2b8-4e78-a674-02086369606a--\r\n" + +""; + + private const string BlobBatchResponse = +"--batchresponse_6040fee7-a2b8-4e78-a674-02086369606a \r\n" + +"Content-Type: application/http \r\n" + +"Content-ID: 0 \r\n" + +"\r\n" + +"HTTP/1.1 202 Accepted \r\n" + +"x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f \r\n" + +"x-ms-version: 2018-11-09 \r\n" + +"\r\n" + +"--batchresponse_6040fee7-a2b8-4e78-a674-02086369606a \r\n" + +"Content-Type: application/http \r\n" + +"Content-ID: 1 \r\n" + +"\r\n" + +"HTTP/1.1 202 Accepted \r\n" + +"x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e2851 \r\n" + +"x-ms-version: 2018-11-09 \r\n" + +"\r\n" + +"--batchresponse_6040fee7-a2b8-4e78-a674-02086369606a \r\n" + +"Content-Type: application/http \r\n" + +"Content-ID: 2 \r\n" + +"\r\n" + +"HTTP/1.1 404 The specified blob does not exist. \r\n" + +"x-ms-error-code: BlobNotFound \r\n" + +"x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e2852 \r\n" + +"x-ms-version: 2018-11-09 \r\n" + +"Content-Length: 216 \r\n" + +"Content-Type: application/xml \r\n" + +"\r\n" + +" \r\n" + +"BlobNotFoundThe specified blob does not exist. \r\n" + +"RequestId:778fdc83-801e-0000-62ff-0334671e2852 \r\n" + +"Time:2018-06-14T16:46:54.6040685Z \r\n" + +"--batchresponse_6040fee7-a2b8-4e78-a674-02086369606a-- \r\n" + +"0"; + + + [Test] + public async Task ParseBatchChangesetResponse() + { + var stream = MakeStream(TablesOdataBatchResponse); + var responses = await Multipart.ParseAsync(stream, ContentType, true, default); + + Assert.That(responses, Is.Not.Null); + Assert.That(responses.Length, Is.EqualTo(3)); + Assert.That(responses.All(r => r.Status == (int)HttpStatusCode.Created)); + + foreach (var response in responses) + { + Assert.That(response.TryGetHeader("DataServiceVersion", out var version)); + Assert.That(version, Is.EqualTo("3.0;")); + + Assert.That(response.TryGetHeader("Content-Type", out var contentType)); + Assert.That(contentType, Is.EqualTo("application/json;odata=fullmetadata;streaming=true;charset=utf-8")); + + var bytes = new byte[response.ContentStream.Length]; + await response.ContentStream.ReadAsync(bytes, 0, bytes.Length); + var content = GetString(bytes, bytes.Length); + + Assert.That(content, Is.EqualTo(Body)); + + } + } + + [Test] + public async Task ParseBatchResponse() + { + var stream = MakeStream(BlobBatchResponse); + var responses = await Multipart.ParseAsync(stream, ContentType, true, default); + + Assert.That(responses, Is.Not.Null); + Assert.That(responses.Length, Is.EqualTo(3)); + + var response = responses[0]; + Assert.That(response.Status, Is.EqualTo( (int)HttpStatusCode.Accepted)); + Assert.That(response.TryGetHeader("x-ms-version", out var version)); + Assert.That(version, Is.EqualTo("2018-11-09")); + Assert.That(response.TryGetHeader("x-ms-request-id", out _)); + + response = responses[1]; + Assert.That(response.Status, Is.EqualTo((int)HttpStatusCode.Accepted)); + Assert.That(response.TryGetHeader("x-ms-version", out version)); + Assert.That(version, Is.EqualTo("2018-11-09")); + Assert.That(response.TryGetHeader("x-ms-request-id", out _)); + + response = responses[2]; + Assert.That(response.Status, Is.EqualTo((int)HttpStatusCode.NotFound)); + Assert.That(response.TryGetHeader("x-ms-version", out version)); + Assert.That(version, Is.EqualTo("2018-11-09")); + Assert.That(response.TryGetHeader("x-ms-request-id", out _)); + var bytes = new byte[response.ContentStream.Length]; + await response.ContentStream.ReadAsync(bytes, 0, bytes.Length); + var content = GetString(bytes, bytes.Length); + Assert.That(content.Contains("BlobNotFoundThe specified blob does not exist.")); + } + + private static MemoryStream MakeStream(string text) + { + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } + + private static string GetString(byte[] buffer, int count) + { + return Encoding.ASCII.GetString(buffer, 0, count); + } + } +} diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 985b1e7c571ac..44dbd456d22ab 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -33,7 +33,6 @@ - From 6ed40a1d5c4630d69d62c14d55a0ed3d13d23681 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 20 Aug 2020 13:34:39 -0500 Subject: [PATCH 13/14] refactor tests --- .../tests/HttpPipelineFunctionalTests.cs | 161 ------------------ sdk/core/Azure.Core/tests/MultipartTests.cs | 121 +++++++++++++ 2 files changed, 121 insertions(+), 161 deletions(-) diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index a4a6af71e5189..a80324172f01e 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -437,166 +436,6 @@ public async Task SendMultipartformData() Assert.AreEqual(formData.Current.ContentDisposition, "form-data; name=LastName; filename=file_name.txt"); } - [Test] - public async Task SendMultipartData() - { - const string ApplicationJson = "application/json"; - const string cteHeaderName = "Content-Transfer-Encoding"; - const string Binary = "binary"; - const string Mixed = "mixed"; - const string ApplicationJsonOdata = "application/json; odata=nometadata"; - const string DataServiceVersion = "DataServiceVersion"; - const string Three0 = "3.0"; - const string Host = "myaccount.table.core.windows.net"; - - string requestBody = null; - - HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions()); - using TestServer testServer = new TestServer( - context => - { - using var sr = new StreamReader(context.Request.Body, Encoding.UTF8); - requestBody = sr.ReadToEnd(); - return Task.CompletedTask; - }); - - using Request request = httpPipeline.CreateRequest(); - request.Method = RequestMethod.Put; - request.Uri.Reset(testServer.Address); - - Guid batchGuid = Guid.NewGuid(); - var content = new MultipartContent(Mixed, $"batch_{batchGuid}"); - content.ApplyToRequest(request); - - Guid changesetGuid = Guid.NewGuid(); - var changeset = new MultipartContent(Mixed, $"changeset_{changesetGuid}"); - content.Add(changeset, changeset._headers); - - var postReq1 = httpPipeline.CreateMessage().Request; - postReq1.Method = RequestMethod.Post; - string postUri = $"https://{Host}/Blogs"; - postReq1.Uri.Reset(new Uri(postUri)); - postReq1.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); - postReq1.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); - postReq1.Headers.Add(DataServiceVersion, Three0); - const string post1Body = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"1\", \"Rating\":9, \"Text\":\"Azure...\"}"; - postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post1Body)); - changeset.Add(new RequestRequestContent(postReq1), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); - - var postReq2 = httpPipeline.CreateMessage().Request; - postReq2.Method = RequestMethod.Post; - postReq2.Uri.Reset(new Uri(postUri)); - postReq2.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); - postReq2.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); - postReq2.Headers.Add(DataServiceVersion, Three0); - const string post2Body = "{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}"; - postReq2.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); - changeset.Add(new RequestRequestContent(postReq2), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); - - var patchReq = httpPipeline.CreateMessage().Request; - patchReq.Method = RequestMethod.Patch; - string mergeUri = $"https://{Host}/Blogs(PartitionKey='Channel_17',%20RowKey='3')"; - patchReq.Uri.Reset(new Uri(mergeUri)); - patchReq.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); - patchReq.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); - patchReq.Headers.Add(DataServiceVersion, Three0); - const string patchBody = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"3\", \"Rating\":9, \"Text\":\"Azure Tables...\"}"; - patchReq.Content = RequestContent.Create(Encoding.UTF8.GetBytes(patchBody)); - changeset.Add(new RequestRequestContent(patchReq), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); - - request.Content = content; - using Response response = await ExecuteRequest(request, httpPipeline); - Console.WriteLine(requestBody); - - if (_transportType == typeof(HttpClientTransport)) - { - Assert.That(requestBody, Is.EqualTo($"--batch_{batchGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: multipart/mixed; boundary=changeset_{changesetGuid}\r\n" + - $"\r\n" + - $"--changeset_{changesetGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: application/http\r\n" + - $"{cteHeaderName}: {Binary}\r\n" + - $"\r\n" + - $"POST {postUri} HTTP/1.1\r\n" + - $"{HttpHeader.Names.Host}: {Host}\r\n" + - $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + - $"{DataServiceVersion}: {Three0}\r\n" + - $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + - $"\r\n" + - $"{post1Body}\r\n" + - $"--changeset_{changesetGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: application/http\r\n" + - $"{cteHeaderName}: {Binary}\r\n" + - $"\r\n" + - $"POST {postUri} HTTP/1.1\r\n" + - $"{HttpHeader.Names.Host}: {Host}\r\n" + - $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + - $"{DataServiceVersion}: {Three0}\r\n" + - $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + - $"\r\n" + - $"{post2Body}\r\n" + - $"--changeset_{changesetGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: application/http\r\n" + - $"{cteHeaderName}: {Binary}\r\n" + - $"\r\n" + - $"PATCH {mergeUri} HTTP/1.1\r\n" + - $"{HttpHeader.Names.Host}: {Host}\r\n" + - $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + - $"{DataServiceVersion}: {Three0}\r\n" + - $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + - $"\r\n" + - $"{patchBody}\r\n" + - $"--changeset_{changesetGuid}--\r\n" + - $"\r\n" + - $"--batch_{batchGuid}--\r\n" + - $"")); - } - else //header ordering is different for HttpWebRequestTransport - { - Assert.That(requestBody, Is.EqualTo($"--batch_{batchGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: multipart/mixed; boundary=changeset_{changesetGuid}\r\n" + - $"\r\n" + - $"--changeset_{changesetGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: application/http\r\n" + - $"{cteHeaderName}: {Binary}\r\n" + - $"\r\n" + - $"POST {postUri} HTTP/1.1\r\n" + - $"{HttpHeader.Names.Host}: {Host}\r\n" + - $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + - $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + - $"{DataServiceVersion}: {Three0}\r\n" + - $"\r\n" + - $"{post1Body}\r\n" + - $"--changeset_{changesetGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: application/http\r\n" + - $"{cteHeaderName}: {Binary}\r\n" + - $"\r\n" + - $"POST {postUri} HTTP/1.1\r\n" + - $"{HttpHeader.Names.Host}: {Host}\r\n" + - $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + - $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + - $"{DataServiceVersion}: {Three0}\r\n" + - $"\r\n" + - $"{post2Body}\r\n" + - $"--changeset_{changesetGuid}\r\n" + - $"{HttpHeader.Names.ContentType}: application/http\r\n" + - $"{cteHeaderName}: {Binary}\r\n" + - $"\r\n" + - $"PATCH {mergeUri} HTTP/1.1\r\n" + - $"{HttpHeader.Names.Host}: {Host}\r\n" + - $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + - $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + - $"{DataServiceVersion}: {Three0}\r\n" + - $"\r\n" + - $"{patchBody}\r\n" + - $"--changeset_{changesetGuid}--\r\n" + - $"\r\n" + - $"--batch_{batchGuid}--\r\n" + - $"")); - } - - } - private class TestOptions : ClientOptions { } diff --git a/sdk/core/Azure.Core/tests/MultipartTests.cs b/sdk/core/Azure.Core/tests/MultipartTests.cs index 674c2aebcab25..a283bec4469c7 100644 --- a/sdk/core/Azure.Core/tests/MultipartTests.cs +++ b/sdk/core/Azure.Core/tests/MultipartTests.cs @@ -3,11 +3,14 @@ #nullable disable warnings +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; +using Azure.Core.TestFramework; using NUnit.Framework; namespace Azure.Core.Tests @@ -161,6 +164,124 @@ public async Task ParseBatchResponse() Assert.That(content.Contains("BlobNotFoundThe specified blob does not exist.")); } + [Test] + public async Task SendMultipartData() + { + const string ApplicationJson = "application/json"; + const string cteHeaderName = "Content-Transfer-Encoding"; + const string Binary = "binary"; + const string Mixed = "mixed"; + const string ApplicationJsonOdata = "application/json; odata=nometadata"; + const string DataServiceVersion = "DataServiceVersion"; + const string Three0 = "3.0"; + const string Host = "myaccount.table.core.windows.net"; + + using Request request = new MockRequest + { + Method = RequestMethod.Put + }; + request.Uri.Reset(new Uri("https://foo")); + + Guid batchGuid = Guid.NewGuid(); + var content = new MultipartContent(Mixed, $"batch_{batchGuid}"); + content.ApplyToRequest(request); + + Guid changesetGuid = Guid.NewGuid(); + var changeset = new MultipartContent(Mixed, $"changeset_{changesetGuid}"); + content.Add(changeset, changeset._headers); + + var postReq1 = new MockRequest + { + Method = RequestMethod.Post + }; + string postUri = $"https://{Host}/Blogs"; + postReq1.Uri.Reset(new Uri(postUri)); + postReq1.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + postReq1.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + postReq1.Headers.Add(DataServiceVersion, Three0); + const string post1Body = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"1\", \"Rating\":9, \"Text\":\"Azure...\"}"; + postReq1.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post1Body)); + changeset.Add(new RequestRequestContent(postReq1), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); + + var postReq2 = new MockRequest + { + Method = RequestMethod.Post + }; + postReq2.Uri.Reset(new Uri(postUri)); + postReq2.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + postReq2.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + postReq2.Headers.Add(DataServiceVersion, Three0); + const string post2Body = "{ \"PartitionKey\":\"Channel_17\", \"RowKey\":\"2\", \"Rating\":9, \"Text\":\"Azure...\"}"; + postReq2.Content = RequestContent.Create(Encoding.UTF8.GetBytes(post2Body)); + changeset.Add(new RequestRequestContent(postReq2), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); + + var patchReq = new MockRequest + { + Method = RequestMethod.Patch + }; + string mergeUri = $"https://{Host}/Blogs(PartitionKey='Channel_17',%20RowKey='3')"; + patchReq.Uri.Reset(new Uri(mergeUri)); + patchReq.Headers.Add(HttpHeader.Names.ContentType, ApplicationJsonOdata); + patchReq.Headers.Add(HttpHeader.Names.Accept, ApplicationJson); + patchReq.Headers.Add(DataServiceVersion, Three0); + const string patchBody = "{ \"PartitionKey\":\"Channel_19\", \"RowKey\":\"3\", \"Rating\":9, \"Text\":\"Azure Tables...\"}"; + patchReq.Content = RequestContent.Create(Encoding.UTF8.GetBytes(patchBody)); + changeset.Add(new RequestRequestContent(patchReq), new Dictionary { { HttpHeader.Names.ContentType, "application/http" }, { cteHeaderName, Binary } }); + + request.Content = content; + var memStream = new MemoryStream(); + await content.WriteToAsync(memStream, default); + memStream.Position = 0; + using var sr = new StreamReader(memStream, Encoding.UTF8); + string requestBody = sr.ReadToEnd(); + Console.WriteLine(requestBody); + + + Assert.That(requestBody, Is.EqualTo($"--batch_{batchGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: multipart/mixed; boundary=changeset_{changesetGuid}\r\n" + + $"\r\n" + + $"--changeset_{changesetGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: application/http\r\n" + + $"{cteHeaderName}: {Binary}\r\n" + + $"\r\n" + + $"POST {postUri} HTTP/1.1\r\n" + + $"{HttpHeader.Names.Host}: {Host}\r\n" + + $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + + $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + + $"{DataServiceVersion}: {Three0}\r\n" + + $"{HttpHeader.Names.ContentLength}: 75\r\n" + + $"\r\n" + + $"{post1Body}\r\n" + + $"--changeset_{changesetGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: application/http\r\n" + + $"{cteHeaderName}: {Binary}\r\n" + + $"\r\n" + + $"POST {postUri} HTTP/1.1\r\n" + + $"{HttpHeader.Names.Host}: {Host}\r\n" + + $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + + $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + + $"{DataServiceVersion}: {Three0}\r\n" + + $"{HttpHeader.Names.ContentLength}: 75\r\n" + + $"\r\n" + + $"{post2Body}\r\n" + + $"--changeset_{changesetGuid}\r\n" + + $"{HttpHeader.Names.ContentType}: application/http\r\n" + + $"{cteHeaderName}: {Binary}\r\n" + + $"\r\n" + + $"PATCH {mergeUri} HTTP/1.1\r\n" + + $"{HttpHeader.Names.Host}: {Host}\r\n" + + $"{HttpHeader.Names.ContentType}: {ApplicationJsonOdata}\r\n" + + $"{HttpHeader.Names.Accept}: {ApplicationJson}\r\n" + + $"{DataServiceVersion}: {Three0}\r\n" + + $"{HttpHeader.Names.ContentLength}: 82\r\n" + + $"\r\n" + + $"{patchBody}\r\n" + + $"--changeset_{changesetGuid}--\r\n" + + $"\r\n" + + $"--batch_{batchGuid}--\r\n" + + $"")); + } + private static MemoryStream MakeStream(string text) { return new MemoryStream(Encoding.UTF8.GetBytes(text)); From ee3fe61378faac3e9698feed47263527b623b1c6 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Fri, 21 Aug 2020 08:50:16 -0500 Subject: [PATCH 14/14] export core --- sdk/core/Azure.Core/api/Azure.Core.net461.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index c0b60287caf04..9fe50a17e2e9b 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -216,15 +216,18 @@ public static partial class Names { public static string Accept { get { throw null; } } public static string Authorization { get { throw null; } } + public static string ContentDisposition { get { throw null; } } public static string ContentLength { get { throw null; } } public static string ContentType { get { throw null; } } public static string Date { get { throw null; } } public static string ETag { get { throw null; } } + public static string Host { get { throw null; } } public static string IfMatch { get { throw null; } } public static string IfModifiedSince { get { throw null; } } public static string IfNoneMatch { get { throw null; } } public static string IfUnmodifiedSince { get { throw null; } } public static string Range { get { throw null; } } + public static string Referer { get { throw null; } } public static string UserAgent { get { throw null; } } public static string XMsDate { get { throw null; } } public static string XMsRange { get { throw null; } }