#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 { /// /// A collection of metadata entries that can be exchanged during a call. /// gRPC supports these types of metadata: /// /// Request headersare sent by the client at the beginning of a remote call before any request messages are sent. /// Response headersare sent by the server at the beginning of a remote call handler before any response messages are sent. /// Response trailersare sent by the server at the end of a remote call along with resulting call status. /// /// public sealed class Metadata : IList { /// /// All binary headers should have this suffix. /// public const string BinaryHeaderSuffix = "-bin"; /// /// An read-only instance of metadata containing no entries. /// public static readonly Metadata Empty = new Metadata().Freeze(); /// /// 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. /// internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request"; readonly List entries; bool readOnly; /// /// Initializes a new instance of Metadata. /// public Metadata() { this.entries = new List(); } /// /// Makes this object read-only. /// /// this object internal Metadata Freeze() { this.readOnly = true; return this; } // TODO: add support for access by key #region IList members /// /// /// public int IndexOf(Metadata.Entry item) { return entries.IndexOf(item); } /// /// /// public void Insert(int index, Metadata.Entry item) { GrpcPreconditions.CheckNotNull(item); CheckWriteable(); entries.Insert(index, item); } /// /// /// public void RemoveAt(int index) { CheckWriteable(); entries.RemoveAt(index); } /// /// /// public Metadata.Entry this[int index] { get { return entries[index]; } set { GrpcPreconditions.CheckNotNull(value); CheckWriteable(); entries[index] = value; } } /// /// /// public void Add(Metadata.Entry item) { GrpcPreconditions.CheckNotNull(item); CheckWriteable(); entries.Add(item); } /// /// Adds a new ASCII-valued metadata entry. See Metadata.Entry constructor for params. /// public void Add(string key, string value) { Add(new Entry(key, value)); } /// /// Adds a new binary-valued metadata entry. See Metadata.Entry constructor for params. /// public void Add(string key, byte[] valueBytes) { Add(new Entry(key, valueBytes)); } /// /// /// public void Clear() { CheckWriteable(); entries.Clear(); } /// /// /// public bool Contains(Metadata.Entry item) { return entries.Contains(item); } /// /// /// public void CopyTo(Metadata.Entry[] array, int arrayIndex) { entries.CopyTo(array, arrayIndex); } /// /// /// public int Count { get { return entries.Count; } } /// /// /// public bool IsReadOnly { get { return readOnly; } } /// /// /// public bool Remove(Metadata.Entry item) { CheckWriteable(); return entries.Remove(item); } /// /// /// public IEnumerator GetEnumerator() { return entries.GetEnumerator(); } IEnumerator System.Collections.IEnumerable.GetEnumerator() { return entries.GetEnumerator(); } private void CheckWriteable() { GrpcPreconditions.CheckState(!readOnly, "Object is read only"); } #endregion /// /// Metadata entry /// public class Entry { 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; } /// /// Initializes a new instance of the struct with a binary value. /// /// Metadata key. Gets converted to lowercase. Needs to have suffix indicating a binary valued metadata entry. Can only contain lowercase alphanumeric characters, underscores, hyphens and dots. /// Value bytes. 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 } /// /// Initializes a new instance of the struct with an ASCII value. /// /// Metadata key. Gets converted to lowercase. Must not use suffix indicating a binary valued metadata entry. Can only contain lowercase alphanumeric characters, underscores, hyphens and dots. /// Value string. Only ASCII characters are allowed. 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; } /// /// Gets the metadata entry key. /// public string Key { get { return this.key; } } /// /// Gets the binary value of this metadata entry. /// 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; } } /// /// Gets the string value of this metadata entry. /// public string Value { get { GrpcPreconditions.CheckState(!IsBinary, "Cannot access string value of a binary metadata entry"); return value ?? MarshalUtils.GetStringASCII(valueBytes); } } /// /// Returns true if this entry is a binary-value entry. /// public bool IsBinary { get { return value == null; } } /// /// Returns a that represents the current . /// 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); } /// /// Gets the serialized value for this entry. For binary metadata entries, this leaks /// the internal valueBytes byte array and caller must not change contents of it. /// internal byte[] GetSerializedValueUnsafe() { return valueBytes ?? MarshalUtils.GetBytesASCII(value); } /// /// 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. /// 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) { GrpcPreconditions.CheckNotNull(key, "key"); GrpcPreconditions.CheckArgument(IsValidKey(key, out bool isLowercase), "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores, hyphens and dots."); if (isLowercase) { // save allocation of a new string if already lowercase return key; } return key.ToLowerInvariant(); } private static bool IsValidKey(string input, out bool isLowercase) { isLowercase = true; for (int i = 0; i < input.Length; i++) { char c = input[i]; if ('a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '.' || c == '_' || c == '-' ) continue; if ('A' <= c && c <= 'Z') { isLowercase = false; continue; } return false; } return true; } /// /// Returns true if the key has "-bin" binary header suffix. /// 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; } } } }