aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/csharp/Grpc.Tools/DepFileUtil.cs
blob: 440d3d535c84154b71672cadb25c99840318d1e8 (plain)
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#region Copyright notice and license

// Copyright 2018 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.Generic;
using System.IO;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Grpc.Tools
{
    internal static class DepFileUtil
    {
        /*
           Sample dependency files. Notable features we have to deal with:
            * Slash doubling, must normalize them.
            * Spaces in file names. Cannot just "unwrap" the line on backslash at eof;
              rather, treat every line as containing one file name except for one with
              the ':' separator, as containing exactly two.
            * Deal with ':' also being drive letter separator (second example).

        obj\Release\net45\/Foo.cs \
        obj\Release\net45\/FooGrpc.cs: C:/foo/include/google/protobuf/wrappers.proto\
         C:/projects/foo/src//foo.proto

        C:\projects\foo\src\./foo.grpc.pb.cc \
        C:\projects\foo\src\./foo.grpc.pb.h \
        C:\projects\foo\src\./foo.pb.cc \
        C:\projects\foo\src\./foo.pb.h: C:/foo/include/google/protobuf/wrappers.proto\
         C:/foo/include/google/protobuf/any.proto\
         C:/foo/include/google/protobuf/source_context.proto\
         C:/foo/include/google/protobuf/type.proto\
         foo.proto
        */

        /// <summary>
        /// Read file names from the dependency file to the right of ':'
        /// </summary>
        /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param>
        /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
        /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param>
        /// <returns>
        /// Array of the proto file <b>input</b> dependencies as written by protoc, or empty
        /// array if the dependency file does not exist or cannot be parsed.
        /// </returns>
        public static string[] ReadDependencyInputs(string protoDepDir, string proto,
                                                    TaskLoggingHelper log)
        {
            string depFilename = GetDepFilenameForProto(protoDepDir, proto);
            string[] lines = ReadDepFileLines(depFilename, false, log);
            if (lines.Length == 0)
            {
                return lines;
            }

            var result = new List<string>();
            bool skip = true;
            foreach (string line in lines)
            {
                // Start at the only line separating dependency outputs from inputs.
                int ix = skip ? FindLineSeparator(line) : -1;
                skip = skip && ix < 0;
                if (skip) { continue; }
                string file = ExtractFilenameFromLine(line, ix + 1, line.Length);
                if (file == "")
                {
                    log.LogMessage(MessageImportance.Low,
              $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'");
                    return new string[0];
                }

                // Do not bend over backwards trying not to include a proto into its
                // own list of dependencies. Since a file is not older than self,
                // it is safe to add; this is purely a memory optimization.
                if (file != proto)
                {
                    result.Add(file);
                }
            }
            return result.ToArray();
        }

        /// <summary>
        /// Read file names from the dependency file to the left of ':'
        /// </summary>
        /// <param name="depFilename">Path to dependency file written by protoc</param>
        /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param>
        /// <returns>
        /// Array of the protoc-generated outputs from the given dependency file
        /// written by protoc, or empty array if the file does not exist or cannot
        /// be parsed.
        /// </returns>
        /// <remarks>
        /// Since this is called after a protoc invocation, an unparsable or missing
        /// file causes an error-level message to be logged.
        /// </remarks>
        public static string[] ReadDependencyOutputs(string depFilename,
                                                    TaskLoggingHelper log)
        {
            string[] lines = ReadDepFileLines(depFilename, true, log);
            if (lines.Length == 0)
            {
                return lines;
            }

            var result = new List<string>();
            foreach (string line in lines)
            {
                int ix = FindLineSeparator(line);
                string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length);
                if (file == "")
                {
                    log.LogError("Unable to parse generated dependency file {0}.\n" +
                                 "Line with error: '{1}'", depFilename, line);
                    return new string[0];
                }
                result.Add(file);

                // If this is the line with the separator, do not read further.
                if (ix >= 0) { break; }
            }
            return result.ToArray();
        }

        /// <summary>
        /// Construct relative dependency file name from directory hash and file name
        /// </summary>
        /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param>
        /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
        /// <returns>
        /// Full relative path to the dependency file, e. g.
        /// "out/deadbeef12345678_file.protodep"
        /// </returns>
        /// <remarks>
        /// Since a project may contain proto files with the same filename but in different
        /// directories, a unique filename for the dependency file is constructed based on the
        /// proto file name both name and directory. The directory path can be arbitrary,
        /// for example, it can be outside of the project, or an absolute path including
        /// a drive letter, or a UNC network path. A name constructed from such a path by,
        /// for example, replacing disallowed name characters with an underscore, may well
        /// be over filesystem's allowed path length, since it will be located under the
        /// project and solution directories, which are also some level deep from the root.
        /// Instead of creating long and unwieldy names for these proto sources, we cache
        /// the full path of the name without the filename, and append the filename to it,
        /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678_file", where
        /// "deadbeef12345678" is a presumed hash value of the string "foo/". This allows
        /// the file names be short, unique (up to a hash collision), and still allowing
        /// the user to guess their provenance.
        /// </remarks>
        public static string GetDepFilenameForProto(string protoDepDir, string proto)
        {
            string dirname = Path.GetDirectoryName(proto);
            if (Platform.IsFsCaseInsensitive)
            {
                dirname = dirname.ToLowerInvariant();
            }
            string dirhash = HashString64Hex(dirname);
            string filename = Path.GetFileNameWithoutExtension(proto);
            return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep");
        }

        // Get a 64-bit hash for a directory string. We treat it as if it were
        // unique, since there are not so many distinct proto paths in a project.
        // We take the first 64 bit of the string SHA1.
        // Internal for tests access only.
        internal static string HashString64Hex(string str)
        {
            using (var sha1 = System.Security.Cryptography.SHA1.Create())
            {
                byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str));
                var hashstr = new StringBuilder(16);
                for (int i = 0; i < 8; i++)
                {
                    hashstr.Append(hash[i].ToString("x2"));
                }
                return hashstr.ToString();
            }
        }

        // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from
        // line 'line', skipping over trailing and leading whitespace, and, when
        // 'end' is immediately past end of line 'line', also final '\' (used
        // as a line continuation token in the dep file).
        // Returns an empty string if the filename cannot be extracted.
        static string ExtractFilenameFromLine(string line, int beg, int end)
        {
            while (beg < end && char.IsWhiteSpace(line[beg])) beg++;
            if (beg < end && end == line.Length && line[end - 1] == '\\') end--;
            while (beg < end && char.IsWhiteSpace(line[end - 1])) end--;
            if (beg == end) return "";

            string filename = line.Substring(beg, end - beg);
            try
            {
                // Normalize file name.
                return Path.Combine(Path.GetDirectoryName(filename), Path.GetFileName(filename));
            }
            catch (Exception ex) when (Exceptions.IsIoRelated(ex))
            {
                return "";
            }
        }

        // Finds the index of the ':' separating dependency clauses in the line,
        // not taking Windows drive spec into account. Returns the index of the
        // separating ':', or -1 if no separator found.
        static int FindLineSeparator(string line)
        {
            // Mind this case where the first ':' is not separator:
            // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\
            int ix = line.IndexOf(':');
            if (ix <= 0 || ix == line.Length - 1
                || (line[ix + 1] != '/' && line[ix + 1] != '\\')
                || !char.IsLetter(line[ix - 1]))
            {
                return ix;  // Not a windows drive: no letter before ':', or no '\' after.
            }
            for (int j = ix - 1; --j >= 0;)
            {
                if (!char.IsWhiteSpace(line[j]))
                {
                    return ix;  // Not space or BOL only before "X:/".
                }
            }
            return line.IndexOf(':', ix + 1);
        }

        // Read entire dependency file. The 'required' parameter controls error
        // logging behavior in case the file not found. We require this file when
        // compiling, but reading it is optional when computing dependencies.
        static string[] ReadDepFileLines(string filename, bool required,
                                         TaskLoggingHelper log)
        {
            try
            {
                var result = File.ReadAllLines(filename);
                if (!required)
                {
                    log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}");
                }
                return result;
            }
            catch (Exception ex) when (Exceptions.IsIoRelated(ex))
            {
                if (required)
                {
                    log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}");
                }
                else
                {
                    log.LogMessage(MessageImportance.Low, $"Skipping {filename}: {ex.Message}");
                }
                return new string[0];
            }
        }
    };
}