diff --git a/src/Tests/ProjectionWriterTest/Helpers/TypeFilteringMetadata.cs b/src/Tests/ProjectionWriterTest/Helpers/TypeFilteringMetadata.cs new file mode 100644 index 0000000000..358306a05b --- /dev/null +++ b/src/Tests/ProjectionWriterTest/Helpers/TypeFilteringMetadata.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; + +namespace ProjectionWriterTest.Helpers; + +/// +/// Creates synthetic metadata whose excluded types collide with a selected runtime class's name. +/// +internal static class TypeFilteringMetadata +{ + public static string Create(string directory) + { + ModuleDefinition module = new("Contoso.winmd") + { + RuntimeVersion = "WindowsRuntime 1.4" + }; + + _ = new AssemblyDefinition("Contoso", new Version(255, 255, 255, 255)) + { + Modules = { module }, + Attributes = AssemblyAttributes.ContentWindowsRuntime + }; + + AssemblyReference corlib = (AssemblyReference)module.CorLibTypeFactory.CorLibScope; + corlib.Version = new Version(255, 255, 255, 255); + + AssemblyReference foundation = new("Windows.Foundation.FoundationContract", new Version(255, 255, 255, 255)) + { + Attributes = AssemblyAttributes.ContentWindowsRuntime + }; + + TypeSignature systemType = new TypeReference(module, corlib, "System", "Type").ToTypeSignature(false); + + CustomAttribute Attribute(string name, params CustomAttributeArgument[] arguments) + { + TypeReference attributeType = new(module, foundation, "Windows.Foundation.Metadata", name); + MemberReference constructor = new(attributeType, ".ctor", MethodSignature.CreateInstance( + module.CorLibTypeFactory.Void, arguments.Select(static argument => argument.ArgumentType).ToArray())); + + return new CustomAttribute(constructor, new CustomAttributeSignature(arguments)); + } + + void AddGuid(TypeDefinition type, Guid iid) + { + byte[] bytes = iid.ToByteArray(); + + type.CustomAttributes.Add(Attribute("GuidAttribute", + new(module.CorLibTypeFactory.UInt32, BitConverter.ToUInt32(bytes, 0)), + new(module.CorLibTypeFactory.UInt16, BitConverter.ToUInt16(bytes, 4)), + new(module.CorLibTypeFactory.UInt16, BitConverter.ToUInt16(bytes, 6)), + new(module.CorLibTypeFactory.Byte, bytes[8]), + new(module.CorLibTypeFactory.Byte, bytes[9]), + new(module.CorLibTypeFactory.Byte, bytes[10]), + new(module.CorLibTypeFactory.Byte, bytes[11]), + new(module.CorLibTypeFactory.Byte, bytes[12]), + new(module.CorLibTypeFactory.Byte, bytes[13]), + new(module.CorLibTypeFactory.Byte, bytes[14]), + new(module.CorLibTypeFactory.Byte, bytes[15]))); + } + + TypeDefinition AddInterface(string ns, string name, Guid iid) + { + TypeDefinition type = new(ns, name, + TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.WindowsRuntime); + + module.TopLevelTypes.Add(type); + AddGuid(type, iid); + + return type; + } + + void AddMethod(TypeDefinition type, string name, TypeSignature returnType, params TypeSignature[] parameters) + { + type.Methods.Add(new MethodDefinition(name, + MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.HideBySig, + MethodSignature.CreateInstance(returnType, parameters))); + } + + TypeDefinition AddClass(string ns, string name, Guid defaultIid, Guid staticIid) + { + TypeDefinition type = new(ns, name, + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.WindowsRuntime, + module.CorLibTypeFactory.Object.Type); + + module.TopLevelTypes.Add(type); + + TypeDefinition defaultInterface = AddInterface(ns, $"I{name}", defaultIid); + defaultInterface.CustomAttributes.Add(Attribute("ExclusiveToAttribute", new CustomAttributeArgument(systemType, type.ToTypeSignature()))); + AddMethod(defaultInterface, "GetValue", module.CorLibTypeFactory.Object); + + InterfaceImplementation implementation = new(defaultInterface); + implementation.CustomAttributes.Add(Attribute("DefaultAttribute")); + type.Interfaces.Add(implementation); + + TypeDefinition statics = AddInterface(ns, $"I{name}Statics", staticIid); + statics.CustomAttributes.Add(Attribute("ExclusiveToAttribute", new CustomAttributeArgument(systemType, type.ToTypeSignature()))); + AddMethod(statics, "GetStaticValue", module.CorLibTypeFactory.Int32); + type.CustomAttributes.Add(Attribute("StaticAttribute", + new(systemType, statics.ToTypeSignature()), + new(module.CorLibTypeFactory.UInt32, 1u))); + + return defaultInterface; + } + + _ = AddClass("Contoso", "User", + new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622501"), + new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622502")); + _ = AddClass("Contoso", "User2", + new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622503"), + new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622504")); + TypeDefinition omittedInterface = AddClass("Contoso.UserProfile", "UserSetupManager", + new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622505"), + new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622506")); + + // These signatures are intentionally unsupported. Excluding their declaring types from the + // reference projection must also exclude them when regenerating the implementation. + TypeDefinition propertyValue = AddInterface("Windows.Foundation", "IPropertyValue", + new Guid("4BD682DD-7554-40E9-9A9B-82654EDE7E62")); + TypeDefinition asyncOperation = AddInterface("Windows.Foundation", "IAsyncOperation`1", + new Guid("9FC2B0BB-E446-44E2-AA61-9CAB8F636AF2")); + asyncOperation.GenericParameters.Add(new GenericParameter("T")); + + AddMethod(omittedInterface, "SetProperty", module.CorLibTypeFactory.Void, propertyValue.ToTypeSignature()); + AddMethod(omittedInterface, "GetPropertyAsync", new GenericInstanceTypeSignature(asyncOperation, false, [propertyValue.ToTypeSignature()])); + + string path = Path.Combine(directory, "Contoso.winmd"); + module.Write(path); + + return path; + } +} diff --git a/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj b/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj index 303e803f02..157a8426fa 100644 --- a/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj +++ b/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj @@ -52,6 +52,8 @@ match the path published as assembly metadata below. --> + + + + + + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = [inputPath], + OutputFolder = outputFolder, + IncludeTypes = ["Contoso.User"], + ReferenceProjection = referenceProjection + }); + + AssertSelectedTypes(outputFolder); + }); + } + + [TestMethod] + public void ReferenceProjection_PrefixExclusionsOmitUnsupportedTypes() + { + WithMetadata((inputPath, outputFolder) => + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = [inputPath], + OutputFolder = outputFolder, + Include = ["Contoso"], + Exclude = ["Contoso.User2", "Contoso.IUser2", "Contoso.UserProfile"], + ReferenceProjection = true + }); + + AssertSelectedTypes(outputFolder); + }); + } + + [TestMethod] + public void ExactTypeIncludes_StillEmitExclusiveFactoryInfrastructure() + { + WithMetadata((inputPath, outputFolder) => + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = [inputPath], + OutputFolder = outputFolder, + IncludeTypes = ["Contoso.User"] + }); + + string source = File.ReadAllText(Path.Combine(outputFolder, "Contoso.cs")); + string iids = File.ReadAllText(Path.Combine(outputFolder, "GeneratedInterfaceIIDs.cs")); + + StringAssert.Contains(source, "GetValue"); + StringAssert.Contains(source, "GetStaticValue"); + StringAssert.Contains(source, "IUserStaticsMethods"); + StringAssert.Contains(iids, "IUserStatics"); + StringAssert.Contains(iids, "IUser"); + }); + } + + private static void AssertSelectedTypes(string outputFolder) + { + string source = File.ReadAllText(Path.Combine(outputFolder, "Contoso.cs")); + + StringAssert.Contains(source, "class User"); + Assert.IsFalse(source.Contains("User2", StringComparison.Ordinal)); + Assert.IsFalse(File.Exists(Path.Combine(outputFolder, "Contoso.UserProfile.cs"))); + + string allSources = string.Join(Environment.NewLine, Directory.GetFiles(outputFolder, "*.cs").Select(File.ReadAllText)); + + Assert.IsFalse(allSources.Contains("UserSetupManager", StringComparison.Ordinal)); + Assert.IsFalse(allSources.Contains("Windows.Foundation.IPropertyValue", StringComparison.Ordinal)); + } + + private static void WithMetadata(Action action) + { + string directory = Directory.CreateTempSubdirectory("ProjectionTypeFilteringTest_").FullName; + + try + { + action(TypeFilteringMetadata.Create(directory), Path.Combine(directory, "Generated")); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } +} diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs index e3bbc1437b..d453a80efd 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs @@ -106,6 +106,7 @@ private static void BuildWriterOptions( componentAssemblyNames = []; List includes = []; + List includeTypes = []; List excludes = []; List winmdInputs = []; @@ -181,7 +182,7 @@ private static void BuildWriterOptions( continue; } - includes.Add(type.FullName); + includeTypes.Add(type.FullName); hasTypesToProject = true; } } @@ -244,7 +245,8 @@ private static void BuildWriterOptions( foreach (TypeDefinition exportedType in moduleDefinition.TopLevelTypes) { - includes.Add(exportedType.FullName); + // Reference assemblies describe exact type identities, not namespace prefixes + includeTypes.Add(exportedType.FullName); hasTypesToProject = true; } } @@ -285,6 +287,7 @@ private static void BuildWriterOptions( InputPaths = winmdInputs, OutputFolder = outputFolder, Include = includes, + IncludeTypes = includeTypes, Exclude = excludes, Component = componentMode, ComponentImplementationAssemblyPaths = componentImplementationAssemblies, diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs index 71c1ecd44a..c29f7965a3 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs @@ -259,7 +259,7 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe // Phase 4: Custom additions to namespaces _token.ThrowIfCancellationRequested(); - if (_settings.AdditionFilter.Includes(ns)) + if (_settings.AdditionFilter.IncludesNamespace(ns)) { foreach (Addition addition in Additions.EnumerateByNamespace(ns)) { diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs index 3e69d3ef89..f411556e9a 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs @@ -35,7 +35,7 @@ private void WriteBaseStrings() // Skip ComInteropExtensions if Windows is not included string fileName = resName[(resName.IndexOf(ResourcesBaseSegment, StringComparison.Ordinal) + ResourcesBaseSegment.Length)..]; - if (fileName == "ComInteropExtensions.cs" && !_settings.Filter.Includes("Windows")) + if (fileName == "ComInteropExtensions.cs" && !_settings.Filter.IncludesNamespace("Windows")) { continue; } diff --git a/src/WinRT.Projection.Writer/Generation/Settings.cs b/src/WinRT.Projection.Writer/Generation/Settings.cs index f5c2dc29c8..4f7ff74b71 100644 --- a/src/WinRT.Projection.Writer/Generation/Settings.cs +++ b/src/WinRT.Projection.Writer/Generation/Settings.cs @@ -57,10 +57,15 @@ internal sealed class Settings public int MaxDegreesOfParallelism { get; init; } = -1; /// - /// Gets the namespace prefixes to include in projection (when empty, all namespaces are included). + /// Gets the namespace or type-name prefixes to include in the projection. /// public HashSet Include { get; } = []; + /// + /// Gets the fully qualified type names to include in the projection, matched exactly. + /// + public HashSet IncludeTypes { get; } = []; + /// /// Gets the namespace prefixes to exclude from projection. /// @@ -72,7 +77,7 @@ internal sealed class Settings public HashSet AdditionExclude { get; } = []; /// - /// Gets the compiled type-name filter built from and . + /// Gets the compiled type-name filter built from , , and . /// Only valid after has been called. /// /// @@ -85,7 +90,8 @@ public TypeFilter Filter } /// - /// Gets the compiled type-name filter built from and , used for namespace-additions resources only. + /// Gets the compiled filter used for namespace-additions resources only. + /// Exact type includes keep an otherwise empty filter from including all namespaces, but do not match namespaces themselves. /// Only valid after has been called. /// /// @@ -141,8 +147,8 @@ public void MakeReadOnly() throw WellKnownProjectionWriterExceptions.SettingsAlreadyReadOnly(); } - Filter = new TypeFilter(Include, Exclude); - AdditionFilter = new TypeFilter(Include, AdditionExclude); + Filter = new TypeFilter(Include, Exclude, IncludeTypes); + AdditionFilter = new TypeFilter(Include, AdditionExclude, IncludeTypes); _isReadOnly = true; } } diff --git a/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs b/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs index ef66d12489..c47fddcc73 100644 --- a/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs +++ b/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs @@ -10,6 +10,7 @@ namespace WindowsRuntime.ProjectionWriter.Helpers; /// /// Include/exclude type filter using longest-prefix-match semantics: type/namespace is checked /// against each prefix in the include/exclude lists, and the longest matching prefix wins. +/// Exact type includes match only the specified type, not namespaces or other type-name prefixes. /// /// /// The semantics are: @@ -26,28 +27,53 @@ internal sealed class TypeFilter { private readonly List _include; private readonly List _exclude; + private readonly HashSet _includeTypes; /// /// Initializes a new with the given include and exclude prefix lists. /// /// The include prefixes (a type matches if any prefix matches). /// The exclude prefixes (a type is rejected if any prefix matches and no longer include prefix wins). - public TypeFilter(IEnumerable include, IEnumerable exclude) + /// Optional fully qualified type names to include, matched exactly. + public TypeFilter(IEnumerable include, IEnumerable exclude, IEnumerable? includeTypes = null) { _include = [.. include.OrderByDescending(s => s.Length)]; _exclude = [.. exclude.OrderByDescending(s => s.Length)]; + _includeTypes = new HashSet(includeTypes ?? [], StringComparer.Ordinal); } /// /// Returns whether the given type name passes the include/exclude filter. + /// Exact type includes win over shorter prefixes, but not an identical exclude. + /// + public bool Includes(string fullName) + { + if (_includeTypes.Contains(fullName)) + { + return !_exclude.Contains(fullName); + } + + return IncludesPrefix(fullName); + } + + /// + /// Returns whether a namespace passes the prefix filter, without matching exact type includes. + /// + public bool IncludesNamespace(string ns) + { + return IncludesPrefix(ns); + } + + /// + /// Returns whether the given name passes the include/exclude prefix filter. /// Rules are sorted by descending prefix length (with excludes winning ties over includes); /// the first matching rule wins. Match semantics split the full type name into /// namespace.typeName parts and treat the rule prefix as either a namespace-prefix or /// a namespace + typename-prefix. /// - public bool Includes(string fullName) + private bool IncludesPrefix(string fullName) { - if (_include.Count == 0 && _exclude.Count == 0) + if (_include.Count == 0 && _exclude.Count == 0 && _includeTypes.Count == 0) { return true; } @@ -115,7 +141,7 @@ public bool Includes(string fullName) } } - // No rule matched. Since at least one rule exists (the both-empty case returned true + // No rule matched. Since at least one rule exists (the all-empty case returned true // above), default to exclude. This means an excludes-only configuration (no includes) // projects nothing rather than everything-but-excluded. return false; diff --git a/src/WinRT.Projection.Writer/ProjectionWriter.cs b/src/WinRT.Projection.Writer/ProjectionWriter.cs index e07785cd8b..dc2177ca6f 100644 --- a/src/WinRT.Projection.Writer/ProjectionWriter.cs +++ b/src/WinRT.Projection.Writer/ProjectionWriter.cs @@ -55,6 +55,7 @@ public static void Run(ProjectionWriterOptions options) settings.Input.UnionWith(options.InputPaths); settings.Include.UnionWith(options.Include); + settings.IncludeTypes.UnionWith(options.IncludeTypes); settings.Exclude.UnionWith(options.Exclude); settings.AdditionExclude.UnionWith(options.AdditionExclude); settings.ComponentImplementationAssemblies.UnionWith(options.ComponentImplementationAssemblyPaths); diff --git a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs index 03a56008c0..6f10285f0e 100644 --- a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs +++ b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs @@ -27,12 +27,19 @@ public sealed class ProjectionWriterOptions public required string OutputFolder { get; init; } /// - /// Optional list of namespace prefixes to include in the projection. + /// Optional list of namespace or type-name prefixes to include in the projection. /// public IReadOnlyList Include { get; init; } = []; /// - /// Optional list of namespace prefixes to exclude from the projection. + /// Optional list of fully qualified type names to include in the projection, matched exactly. + /// These entries do not include namespace additions or other types with the same name prefix. + /// An exact include wins over shorter exclude prefixes; an identical exclude wins the tie. + /// + public IReadOnlyList IncludeTypes { get; init; } = []; + + /// + /// Optional list of namespace or type-name prefixes to exclude from the projection. /// public IReadOnlyList Exclude { get; init; } = [];