// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.Extensions.CommandLineUtils; using Microsoft.VisualStudio.LanguageServices.Razor; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Tools { internal class DiscoverCommand : CommandBase { public DiscoverCommand(Application parent) : base(parent, "discover") { Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true); TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue); ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue); ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue); } public CommandArgument Assemblies { get; } public CommandOption TagHelperManifest { get; } public CommandOption ProjectDirectory { get; } public CommandOption Version { get; } public CommandOption Configuration { get; } public CommandOption ExtensionNames { get; } public CommandOption ExtensionFilePaths { get; } protected override bool ValidateArguments() { if (string.IsNullOrEmpty(TagHelperManifest.Value())) { Error.WriteLine($"{TagHelperManifest.ValueName} must be specified."); return false; } if (Assemblies.Values.Count == 0) { Error.WriteLine($"{Assemblies.Name} must have at least one value."); return false; } if (string.IsNullOrEmpty(ProjectDirectory.Value())) { ProjectDirectory.Values.Add(Environment.CurrentDirectory); } if (string.IsNullOrEmpty(Version.Value())) { Error.WriteLine($"{Version.ValueName} must be specified."); return false; } else if (!RazorLanguageVersion.TryParse(Version.Value(), out _)) { Error.WriteLine($"{Version.ValueName} is not a valid language version."); return false; } if (string.IsNullOrEmpty(Configuration.Value())) { Error.WriteLine($"{Configuration.ValueName} must be specified."); return false; } if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count) { Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values."); } foreach (var filePath in ExtensionFilePaths.Values) { if (!Path.IsPathRooted(filePath)) { Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths."); return false; } } if (!Parent.Checker.Check(ExtensionFilePaths.Values)) { Error.WriteLine($"Extenions could not be loaded. See output for details."); return false; } return true; } protected override Task ExecuteCoreAsync() { // Loading all of the extensions should succeed as the dependency checker will have already // loaded them. var extensions = new RazorExtension[ExtensionNames.Values.Count]; for (var i = 0; i < ExtensionNames.Values.Count; i++) { extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i])); } var version = RazorLanguageVersion.Parse(Version.Value()); var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); var result = ExecuteCore( configuration: configuration, projectDirectory: ProjectDirectory.Value(), outputFilePath: TagHelperManifest.Value(), assemblies: Assemblies.Values.ToArray()); return Task.FromResult(result); } private int ExecuteCore(RazorConfiguration configuration, string projectDirectory, string outputFilePath, string[] assemblies) { outputFilePath = Path.Combine(projectDirectory, outputFilePath); var metadataReferences = new MetadataReference[assemblies.Length]; for (var i = 0; i < assemblies.Length; i++) { metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]); } var engine = RazorProjectEngine.Create(configuration, RazorProjectFileSystem.Empty, b => { b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences }); b.Features.Add(new CompilationTagHelperFeature()); b.Features.Add(new DefaultTagHelperDescriptorProvider()); }); var feature = engine.Engine.Features.OfType().Single(); var tagHelpers = feature.GetDescriptors(); using (var stream = new MemoryStream()) { Serialize(stream, tagHelpers); stream.Position = 0L; var newHash = Hash(stream); var existingHash = Hash(outputFilePath); if (!HashesEqual(newHash, existingHash)) { stream.Position = 0; using (var output = File.OpenWrite(outputFilePath)) { stream.CopyTo(output); } } } return 0; } private static byte[] Hash(string path) { if (!File.Exists(path)) { return Array.Empty(); } using (var stream = File.OpenRead(path)) { return Hash(stream); } } private static byte[] Hash(Stream stream) { using (var sha = SHA256.Create()) { sha.ComputeHash(stream); return sha.Hash; } } private bool HashesEqual(byte[] x, byte[] y) { if (x.Length != y.Length) { return false; } for (var i = 0; i < x.Length; i++) { if (x[i] != y[i]) { return false; } } return true; } private static void Serialize(Stream stream, IReadOnlyList tagHelpers) { using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true)) { var serializer = new JsonSerializer(); serializer.Converters.Add(new TagHelperDescriptorJsonConverter()); serializer.Converters.Add(new RazorDiagnosticJsonConverter()); serializer.Serialize(writer, tagHelpers); } } } }