#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; } } } }