File size: 8,668 Bytes
7b715bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/*
Copyright 2019 Google Inc

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

    https://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.
*/

using Google.Apis.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;

namespace Google.Apis.Requests
{
    // Note: this code is copied from GAX:
    // https://github.com/googleapis/gax-dotnet/blob/main/Google.Api.Gax/VersionHeaderBuilder.cs
    // The duplication is annoying, but hard to avoid due to dependencies.
    // Any changes should be made in both places.

    /// <summary>
    /// Helps build version strings for the x-goog-api-client header.
    /// The value is a space-separated list of name/value pairs, where the value
    /// should be a semantic version string. Names must be unique.
    /// </summary>
    public sealed class VersionHeaderBuilder
    {
        private static readonly Lazy<string> s_environmentVersion = new Lazy<string>(GetEnvironmentVersion);

        /// <summary>
        /// The name of the header to set.
        /// </summary>
        public const string HeaderName = "x-goog-api-client";
        private readonly List<string> _names = new List<string>();
        private readonly List<string> _values = new List<string>();

        /// <summary>
        /// Appends the given name/version string to the list.
        /// </summary>
        /// <param name="name">The name. Must not be null or empty, or contain a space or a slash.</param>
        /// <param name="version">The version. Must not be null, or contain a space or a slash.</param>
        public VersionHeaderBuilder AppendVersion(string name, string version)
        {
            Utilities.ThrowIfNull(name, nameof(name));
            Utilities.ThrowIfNull(version, nameof(version));
            // Names can't be empty, but versions can. (We use the empty string to indicate an unknown version.)

            CheckArgument(name.Length > 0 && !name.Contains(" ") && !name.Contains("/"), nameof(name), $"Invalid name: {name}");
            CheckArgument(!version.Contains(" ") && !version.Contains("/"), nameof(version), $"Invalid version: {version}");
            CheckArgument(!_names.Contains(name), nameof(name), "Names in version headers must be unique");
            _names.Add(name);
            _values.Add(version);
            return this;
        }

        // This is in GaxPreconditions in the original code.
        private static void CheckArgument(bool condition, string paramName, string message)
        {
            if (!condition)
            {
                throw new ArgumentException(message, paramName);
            }
        }

        /// <summary>
        /// Appends a name/version string, taking the version from the version of the assembly
        /// containing the given type.
        /// </summary>
        public VersionHeaderBuilder AppendAssemblyVersion(string name, System.Type type)
            => AppendVersion(name, FormatAssemblyVersion(type));

        /// <summary>
        /// Appends the .NET environment information to the list.
        /// </summary>
        public VersionHeaderBuilder AppendDotNetEnvironment() => AppendVersion("gl-dotnet", s_environmentVersion.Value);

        /// <summary>
        /// Whether the name or value that are supposed to be included in a header are valid
        /// </summary>
        private static bool IsHeaderNameValueValid(string nameOrValue) =>
            !nameOrValue.Contains(" ") && !nameOrValue.Contains("/");

        private static string GetEnvironmentVersion()
        {
            // We can pick between the version reported by System.Environment.Version, or the version in the
            // entry assembly, if any. Neither gives us exactly what we might want, 
            string systemEnvironmentVersion = FormatVersion(Environment.Version);
            string entryAssemblyVersion = GetEntryAssemblyVersionOrNull();

            return entryAssemblyVersion ?? systemEnvironmentVersion ?? "";
        }

        private static string GetEntryAssemblyVersionOrNull()
        {
            try
            {
                // Assembly.GetEntryAssembly() isn't available in netstandard1.3. Attempt to fetch it with reflection, which is ugly but should work.
                // This is a slightly more robust version of the code we previously used in Microsoft.Extensions.PlatformAbstractions.
                var getEntryAssemblyMethod = typeof(Assembly)
                    .GetTypeInfo()
                    .DeclaredMethods
                    .Where(m => m.Name == "GetEntryAssembly" && m.IsStatic && m.GetParameters().Length == 0 && m.ReturnType == typeof(Assembly))
                    .FirstOrDefault();
                if (getEntryAssemblyMethod == null)
                {
                    return null;
                }
                Assembly entryAssembly = (Assembly) getEntryAssemblyMethod.Invoke(null, new object[0]);
                var frameworkName = entryAssembly?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName;
                return frameworkName == null ? null : FormatVersion(new FrameworkName(frameworkName).Version);
            }
            catch
            {
                // If we simply can't get the version for whatever reason, don't fail.
                return null;
            }
        }

        private static string FormatAssemblyVersion(System.Type type)
        {
            // Prefer AssemblyInformationalVersion, then AssemblyFileVersion,
            // then AssemblyVersion.

            var assembly = type.GetTypeInfo().Assembly;
            var info = assembly.GetCustomAttributes<AssemblyInformationalVersionAttribute>().FirstOrDefault()?.InformationalVersion;
            if (info != null && IsHeaderNameValueValid(info)) // Skip informational version if it's not a valid header value
            {
                return FormatInformationalVersion(info);
            }
            var file = assembly.GetCustomAttributes<AssemblyFileVersionAttribute>().FirstOrDefault()?.Version;
            if (file != null)
            {
                return string.Join(".", file.Split('.').Take(3));
            }
            return FormatVersion(assembly.GetName().Version);
        }

        // Visible for testing

        /// <summary>
        /// Formats an AssemblyInformationalVersionAttribute value to avoid losing useful information,
        /// but also avoid including unnecessary hex that is appended automatically.
        /// </summary>
        internal static string FormatInformationalVersion(string info)
        {
            // In some cases, the runtime includes 40 hex digits after a + or period in InformationalVersion.
            // In precisely those cases, we strip this.
            int signIndex = Math.Max(info.LastIndexOf('.'), info.LastIndexOf('+'));
            if (signIndex == -1 || signIndex != info.Length - 41)
            {
                return info;
            }
            for (int i = signIndex + 1; i < info.Length; i++)
            {
                char c = info[i];
                bool isHex = (c >= '0' && c <= '9')
                    || (c >= 'a' && c <= 'f')
                    || (c >= 'A' && c <= 'F');
                if (!isHex)
                {
                    return info;
                }
            }
            return info.Substring(0, signIndex);
        }

        private static string FormatVersion(Version version) =>
            version != null ?
            $"{version.Major}.{version.Minor}.{(version.Build != -1 ? version.Build : 0)}" :
            ""; // Empty string means "unknown"

        /// <inheritdoc />
        public override string ToString() => string.Join(" ", _names.Zip(_values, (name, value) => $"{name}/{value}"));

        /// <summary>
        /// Clones this VersionHeaderBuilder, creating an independent copy with the same names/values.
        /// </summary>
        /// <returns>A clone of this builder.</returns>
        public VersionHeaderBuilder Clone()
        {
            var ret = new VersionHeaderBuilder();
            ret._names.AddRange(_names);
            ret._values.AddRange(_values);
            return ret;
        }
    }
}