Avoid collisions in cs files generated by Grpc.Tools

pull/22869/head
Kraemer, Benjamin 5 years ago
parent 2e2d21d6e5
commit 47ec56beb4
  1. 28
      src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs
  2. 62
      src/csharp/Grpc.Tools/DepFileUtil.cs
  3. 11
      src/csharp/Grpc.Tools/GeneratorServices.cs
  4. 22
      src/csharp/Grpc.Tools/ProtoCompile.cs

@ -67,6 +67,34 @@ namespace Grpc.Tools.Tests
Assert.AreNotEqual(unsame1, unsame2);
}
[Test]
public void GetOutputDirWithHash_IsSane()
{
StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}$",
DepFileUtil.GetOutputDirWithHash("out", "foo.proto"));
StringAssert.IsMatch(@"^[a-f0-9]{16}$",
DepFileUtil.GetOutputDirWithHash("", "foo.proto"));
}
[Test]
public void GetOutputDirWithHash_HashesDir()
{
string PickHash(string fname) => DepFileUtil.GetOutputDirWithHash("", fname);
string same1 = PickHash("dir1/dir2/foo.proto");
string same2 = PickHash("dir1/dir2/proto.foo");
string same3 = PickHash("dir1/dir2/proto");
string same4 = PickHash("dir1/dir2/.proto");
string unsame1 = PickHash("dir2/foo.proto");
string unsame2 = PickHash("/dir2/foo.proto");
Assert.AreEqual(same1, same2);
Assert.AreEqual(same1, same3);
Assert.AreEqual(same1, same4);
Assert.AreNotEqual(same1, unsame1);
Assert.AreNotEqual(unsame1, unsame2);
}
//////////////////////////////////////////////////////////////////////////
// Full file reading tests

@ -138,6 +138,24 @@ namespace Grpc.Tools
return result.ToArray();
}
/// <summary>
/// Construct the directory hash from a relative file name
/// </summary>
/// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
/// <returns>
/// Directory hash based on the file name, e. g. "deadbeef12345678"
/// </returns>
private static string GetDirectoryHash(string proto)
{
string dirname = Path.GetDirectoryName(proto);
if (Platform.IsFsCaseInsensitive)
{
dirname = dirname.ToLowerInvariant();
}
return HashString64Hex(dirname);
}
/// <summary>
/// Construct relative dependency file name from directory hash and file name
/// </summary>
@ -145,7 +163,7 @@ namespace Grpc.Tools
/// <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"
/// "out/deadbeef12345678/file.protodep"
/// </returns>
/// <remarks>
/// Since a project may contain proto files with the same filename but in different
@ -158,21 +176,45 @@ namespace Grpc.Tools
/// 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
/// 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 outdir = GetOutputDirWithHash(protoDepDir, proto);
string filename = Path.GetFileNameWithoutExtension(proto);
return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep");
return Path.Combine(outdir, $"{filename}.protodep");
}
/// <summary>
/// Construct relative output directory with directory hash
/// </summary>
/// <param name="outputDir">Relative path to the output directory, 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 directory, e. g. "out/deadbeef12345678"
/// </returns>
/// <remarks>
/// Since a project may contain proto files with the same filename but in different
/// directories, a unique directory for the generated files is constructed based on the
/// proto file names 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, as in e. g. "foo/file.proto"
/// will yield the name "deadbeef12345678", where that is a presumed hash value
/// of the string "foo". This allows the path to be short, unique (up to a hash
/// collision), and still allowing the user to guess their provenance.
/// </remarks>
public static string GetOutputDirWithHash(string outputDir, string proto)
{
var dirhash = GetDirectoryHash(proto);
return Path.Combine(outputDir, dirhash);
}
// Get a 64-bit hash for a directory string. We treat it as if it were

@ -67,19 +67,20 @@ namespace Grpc.Tools
{
bool doGrpc = GrpcOutputPossible(protoItem);
var outputs = new string[doGrpc ? 2 : 1];
string basename = Path.GetFileNameWithoutExtension(protoItem.ItemSpec);
var itemSpec = protoItem.ItemSpec;
string basename = Path.GetFileNameWithoutExtension(itemSpec);
string outdir = protoItem.GetMetadata(Metadata.OutputDir);
string outdir = DepFileUtil.GetOutputDirWithHash(protoItem.GetMetadata(Metadata.OutputDir), itemSpec);
string filename = LowerUnderscoreToUpperCamelProtocWay(basename);
outputs[0] = Path.Combine(outdir, filename) + ".cs";
if (doGrpc)
{
// Override outdir if kGrpcOutputDir present, default to proto output.
// Override outdir if GrpcOutputDir present, default to proto output.
string grpcdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
grpcdir = grpcdir == "" ? outdir : DepFileUtil.GetOutputDirWithHash(grpcdir, itemSpec);
filename = LowerUnderscoreToUpperCamelGrpcWay(basename);
outputs[1] = Path.Combine(
grpcdir != "" ? grpcdir : outdir, filename) + "Grpc.cs";
outputs[1] = Path.Combine(grpcdir, filename) + "Grpc.cs";
}
return outputs;
}

@ -18,6 +18,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
@ -413,16 +414,21 @@ namespace Grpc.Tools
// Called by the base ToolTask to get response file contents.
protected override string GenerateResponseFileCommands()
{
var outDir = TrimEndSlash(MaybeEnhanceOutputDir(OutputDir, Protobuf));
var grpcOutDir = TrimEndSlash(MaybeEnhanceOutputDir(GrpcOutputDir, Protobuf));
var cmd = new ProtocResponseFileBuilder();
cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir));
cmd.AddSwitchMaybe(Generator + "_out", outDir);
cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions);
cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe);
cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir));
cmd.AddSwitchMaybe("grpc_out", grpcOutDir);
cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions);
if (ProtoPath != null)
{
foreach (string path in ProtoPath)
{
cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path));
}
}
cmd.AddSwitchMaybe("dependency_out", DependencyOut);
cmd.AddSwitchMaybe("error_format", "msvs");
@ -433,6 +439,18 @@ namespace Grpc.Tools
return cmd.ToString();
}
// If possible, disambiguate output dir by adding a hash of the proto file's path
static string MaybeEnhanceOutputDir(string outputDir, ITaskItem[] protobufs)
{
if (protobufs.Length != 1)
{
return outputDir;
}
var protoFile = protobufs[0].ItemSpec;
return DepFileUtil.GetOutputDirWithHash(outputDir, protoFile);
}
// Protoc cannot digest trailing slashes in directory names,
// curiously under Linux, but not in Windows.
static string TrimEndSlash(string dir)

Loading…
Cancel
Save