| #region Copyright notice and license |
| // Copyright 2015 gRPC authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| #endregion |
| |
| using System; |
| using System.Collections; |
| using System.Collections.Generic; |
| using System.Text; |
| using System.Text.RegularExpressions; |
| |
| using Grpc.Core.Internal; |
| using Grpc.Core.Utils; |
| |
| namespace Grpc.Core |
| { |
| /// <summary> |
| /// A collection of metadata entries that can be exchanged during a call. |
| /// gRPC supports these types of metadata: |
| /// <list type="bullet"> |
| /// <item><term>Request headers</term><description>are sent by the client at the beginning of a remote call before any request messages are sent.</description></item> |
| /// <item><term>Response headers</term><description>are sent by the server at the beginning of a remote call handler before any response messages are sent.</description></item> |
| /// <item><term>Response trailers</term><description>are sent by the server at the end of a remote call along with resulting call status.</description></item> |
| /// </list> |
| /// </summary> |
| public sealed class Metadata : IList<Metadata.Entry> |
| { |
| /// <summary> |
| /// All binary headers should have this suffix. |
| /// </summary> |
| public const string BinaryHeaderSuffix = "-bin"; |
| |
| /// <summary> |
| /// An read-only instance of metadata containing no entries. |
| /// </summary> |
| public static readonly Metadata Empty = new Metadata().Freeze(); |
| |
| /// <summary> |
| /// To be used in initial metadata to request specific compression algorithm |
| /// for given call. Direct selection of compression algorithms is an internal |
| /// feature and is not part of public API. |
| /// </summary> |
| internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request"; |
| |
| readonly List<Entry> entries; |
| bool readOnly; |
| |
| /// <summary> |
| /// Initializes a new instance of <c>Metadata</c>. |
| /// </summary> |
| public Metadata() |
| { |
| this.entries = new List<Entry>(); |
| } |
| |
| /// <summary> |
| /// Makes this object read-only. |
| /// </summary> |
| /// <returns>this object</returns> |
| internal Metadata Freeze() |
| { |
| this.readOnly = true; |
| return this; |
| } |
| |
| // TODO: add support for access by key |
| |
| #region IList members |
| |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public int IndexOf(Metadata.Entry item) |
| { |
| return entries.IndexOf(item); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void Insert(int index, Metadata.Entry item) |
| { |
| GrpcPreconditions.CheckNotNull(item); |
| CheckWriteable(); |
| entries.Insert(index, item); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void RemoveAt(int index) |
| { |
| CheckWriteable(); |
| entries.RemoveAt(index); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public Metadata.Entry this[int index] |
| { |
| get |
| { |
| return entries[index]; |
| } |
| |
| set |
| { |
| GrpcPreconditions.CheckNotNull(value); |
| CheckWriteable(); |
| entries[index] = value; |
| } |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void Add(Metadata.Entry item) |
| { |
| GrpcPreconditions.CheckNotNull(item); |
| CheckWriteable(); |
| entries.Add(item); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void Add(string key, string value) |
| { |
| Add(new Entry(key, value)); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void Add(string key, byte[] valueBytes) |
| { |
| Add(new Entry(key, valueBytes)); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void Clear() |
| { |
| CheckWriteable(); |
| entries.Clear(); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public bool Contains(Metadata.Entry item) |
| { |
| return entries.Contains(item); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public void CopyTo(Metadata.Entry[] array, int arrayIndex) |
| { |
| entries.CopyTo(array, arrayIndex); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public int Count |
| { |
| get { return entries.Count; } |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public bool IsReadOnly |
| { |
| get { return readOnly; } |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public bool Remove(Metadata.Entry item) |
| { |
| CheckWriteable(); |
| return entries.Remove(item); |
| } |
| |
| /// <summary> |
| /// <see cref="T:IList`1"/> |
| /// </summary> |
| public IEnumerator<Metadata.Entry> GetEnumerator() |
| { |
| return entries.GetEnumerator(); |
| } |
| |
| IEnumerator System.Collections.IEnumerable.GetEnumerator() |
| { |
| return entries.GetEnumerator(); |
| } |
| |
| private void CheckWriteable() |
| { |
| GrpcPreconditions.CheckState(!readOnly, "Object is read only"); |
| } |
| |
| #endregion |
| |
| /// <summary> |
| /// Metadata entry |
| /// </summary> |
| public class Entry |
| { |
| private static readonly Regex ValidKeyRegex = new Regex("^[.a-z0-9_-]+$"); |
| |
| readonly string key; |
| readonly string value; |
| readonly byte[] valueBytes; |
| |
| private Entry(string key, string value, byte[] valueBytes) |
| { |
| this.key = key; |
| this.value = value; |
| this.valueBytes = valueBytes; |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with a binary value. |
| /// </summary> |
| /// <param name="key">Metadata key, needs to have suffix indicating a binary valued metadata entry.</param> |
| /// <param name="valueBytes">Value bytes.</param> |
| public Entry(string key, byte[] valueBytes) |
| { |
| this.key = NormalizeKey(key); |
| GrpcPreconditions.CheckArgument(HasBinaryHeaderSuffix(this.key), |
| "Key for binary valued metadata entry needs to have suffix indicating binary value."); |
| this.value = null; |
| GrpcPreconditions.CheckNotNull(valueBytes, "valueBytes"); |
| this.valueBytes = new byte[valueBytes.Length]; |
| Buffer.BlockCopy(valueBytes, 0, this.valueBytes, 0, valueBytes.Length); // defensive copy to guarantee immutability |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct holding an ASCII value. |
| /// </summary> |
| /// <param name="key">Metadata key, must not use suffix indicating a binary valued metadata entry.</param> |
| /// <param name="value">Value string. Only ASCII characters are allowed.</param> |
| public Entry(string key, string value) |
| { |
| this.key = NormalizeKey(key); |
| GrpcPreconditions.CheckArgument(!HasBinaryHeaderSuffix(this.key), |
| "Key for ASCII valued metadata entry cannot have suffix indicating binary value."); |
| this.value = GrpcPreconditions.CheckNotNull(value, "value"); |
| this.valueBytes = null; |
| } |
| |
| /// <summary> |
| /// Gets the metadata entry key. |
| /// </summary> |
| public string Key |
| { |
| get |
| { |
| return this.key; |
| } |
| } |
| |
| /// <summary> |
| /// Gets the binary value of this metadata entry. |
| /// </summary> |
| public byte[] ValueBytes |
| { |
| get |
| { |
| if (valueBytes == null) |
| { |
| return MarshalUtils.GetBytesASCII(value); |
| } |
| |
| // defensive copy to guarantee immutability |
| var bytes = new byte[valueBytes.Length]; |
| Buffer.BlockCopy(valueBytes, 0, bytes, 0, valueBytes.Length); |
| return bytes; |
| } |
| } |
| |
| /// <summary> |
| /// Gets the string value of this metadata entry. |
| /// </summary> |
| public string Value |
| { |
| get |
| { |
| GrpcPreconditions.CheckState(!IsBinary, "Cannot access string value of a binary metadata entry"); |
| return value ?? MarshalUtils.GetStringASCII(valueBytes); |
| } |
| } |
| |
| /// <summary> |
| /// Returns <c>true</c> if this entry is a binary-value entry. |
| /// </summary> |
| public bool IsBinary |
| { |
| get |
| { |
| return value == null; |
| } |
| } |
| |
| /// <summary> |
| /// Returns a <see cref="System.String"/> that represents the current <see cref="Grpc.Core.Metadata.Entry"/>. |
| /// </summary> |
| public override string ToString() |
| { |
| if (IsBinary) |
| { |
| return string.Format("[Entry: key={0}, valueBytes={1}]", key, valueBytes); |
| } |
| |
| return string.Format("[Entry: key={0}, value={1}]", key, value); |
| } |
| |
| /// <summary> |
| /// Gets the serialized value for this entry. For binary metadata entries, this leaks |
| /// the internal <c>valueBytes</c> byte array and caller must not change contents of it. |
| /// </summary> |
| internal byte[] GetSerializedValueUnsafe() |
| { |
| return valueBytes ?? MarshalUtils.GetBytesASCII(value); |
| } |
| |
| /// <summary> |
| /// Creates a binary value or ascii value metadata entry from data received from the native layer. |
| /// We trust C core to give us well-formed data, so we don't perform any checks or defensive copying. |
| /// </summary> |
| internal static Entry CreateUnsafe(string key, byte[] valueBytes) |
| { |
| if (HasBinaryHeaderSuffix(key)) |
| { |
| return new Entry(key, null, valueBytes); |
| } |
| return new Entry(key, MarshalUtils.GetStringASCII(valueBytes), null); |
| } |
| |
| private static string NormalizeKey(string key) |
| { |
| var normalized = GrpcPreconditions.CheckNotNull(key, "key").ToLowerInvariant(); |
| GrpcPreconditions.CheckArgument(ValidKeyRegex.IsMatch(normalized), |
| "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores, hyphens and dots."); |
| return normalized; |
| } |
| |
| /// <summary> |
| /// Returns <c>true</c> if the key has "-bin" binary header suffix. |
| /// </summary> |
| private static bool HasBinaryHeaderSuffix(string key) |
| { |
| // We don't use just string.EndsWith because its implementation is extremely slow |
| // on CoreCLR and we've seen significant differences in gRPC benchmarks caused by it. |
| // See https://github.com/dotnet/coreclr/issues/5612 |
| |
| int len = key.Length; |
| if (len >= 4 && |
| key[len - 4] == '-' && |
| key[len - 3] == 'b' && |
| key[len - 2] == 'i' && |
| key[len - 1] == 'n') |
| { |
| return true; |
| } |
| return false; |
| } |
| } |
| } |
| } |