|
| 1 | +using System.IO; |
| 2 | +using BitMono.Core.Renaming; |
| 3 | +#if !NETSTANDARD |
| 4 | +using System.Resources; |
| 5 | +using AsmResolver; |
| 6 | +#endif |
| 7 | + |
| 8 | +namespace BitMono.Core.Analyzing.Baml; |
| 9 | + |
| 10 | +/// <summary> |
| 11 | +/// Per-module view of the assembly's compiled WPF XAML (BAML in <c><assembly>.g.resources</c>). |
| 12 | +/// <para> |
| 13 | +/// It tells the renamer which types/members XAML references (so they aren't broken), and, when |
| 14 | +/// rewriting is enabled, renames the XAML-referenced type names and updates the BAML to match. |
| 15 | +/// Members of XAML types are always kept (binding paths and the like live in BAML as plain strings), |
| 16 | +/// and a type is only renamed when its name appears nowhere as a string value (which would be an |
| 17 | +/// <c>{x:Type}</c>/<c>TargetType</c> reference we don't rewrite). |
| 18 | +/// </para> |
| 19 | +/// </summary> |
| 20 | +public sealed class WpfBamlContext |
| 21 | +{ |
| 22 | + /// <summary>All in-module types referenced by XAML. Their members are always kept.</summary> |
| 23 | + public HashSet<TypeDefinition> XamlTypes { get; } = new(); |
| 24 | + |
| 25 | + /// <summary>Subset of <see cref="XamlTypes"/> whose name can be safely renamed (BAML updated).</summary> |
| 26 | + public HashSet<TypeDefinition> RenamableTypes { get; } = new(); |
| 27 | + |
| 28 | +#if !NETSTANDARD |
| 29 | + private readonly List<(TypeInfoRecord Record, TypeDefinition Type)> _typeEdits = new(); |
| 30 | + private readonly List<ResourceRewrite> _rewrites = new(); |
| 31 | + private bool _canRewrite = true; |
| 32 | + |
| 33 | + private sealed class ResourceRewrite |
| 34 | + { |
| 35 | + public ManifestResource Resource = null!; |
| 36 | + public Dictionary<string, BamlDocument> Documents = new(); |
| 37 | + } |
| 38 | +#endif |
| 39 | + |
| 40 | + public static WpfBamlContext Build(ModuleDefinition module, bool rewriteEnabled) |
| 41 | + { |
| 42 | + var context = new WpfBamlContext(); |
| 43 | +#if !NETSTANDARD |
| 44 | + var assemblyName = module.Assembly?.Name?.Value; |
| 45 | + if (string.IsNullOrEmpty(assemblyName)) |
| 46 | + { |
| 47 | + return context; |
| 48 | + } |
| 49 | + var typesByFullName = new Dictionary<string, TypeDefinition>(StringComparer.Ordinal); |
| 50 | + foreach (var type in module.GetAllTypes()) |
| 51 | + { |
| 52 | + var fullName = type.FullName; |
| 53 | + if (!string.IsNullOrEmpty(fullName)) |
| 54 | + { |
| 55 | + typesByFullName[fullName] = type; |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + foreach (var resource in module.Resources) |
| 60 | + { |
| 61 | + if (!resource.IsEmbedded) |
| 62 | + { |
| 63 | + continue; |
| 64 | + } |
| 65 | + var name = resource.Name?.Value; |
| 66 | + if (name == null || !name.EndsWith(".g.resources", StringComparison.OrdinalIgnoreCase)) |
| 67 | + { |
| 68 | + continue; |
| 69 | + } |
| 70 | + byte[]? data; |
| 71 | + try |
| 72 | + { |
| 73 | + data = resource.GetData(); |
| 74 | + } |
| 75 | + catch |
| 76 | + { |
| 77 | + continue; |
| 78 | + } |
| 79 | + if (data == null) |
| 80 | + { |
| 81 | + continue; |
| 82 | + } |
| 83 | + try |
| 84 | + { |
| 85 | + context.ProcessResource(resource, data, assemblyName!, typesByFullName); |
| 86 | + } |
| 87 | + catch |
| 88 | + { |
| 89 | + // A resource we can't read just means we exclude/rewrite nothing for it. |
| 90 | + context._canRewrite = false; |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + context.ComputeRenamable(rewriteEnabled); |
| 95 | +#endif |
| 96 | + return context; |
| 97 | + } |
| 98 | + |
| 99 | +#if !NETSTANDARD |
| 100 | + private void ProcessResource(ManifestResource resource, byte[] resourcesData, string assemblyName, |
| 101 | + Dictionary<string, TypeDefinition> typesByFullName) |
| 102 | + { |
| 103 | + var rewrite = new ResourceRewrite { Resource = resource }; |
| 104 | + using var stream = new MemoryStream(resourcesData, writable: false); |
| 105 | + using var reader = new ResourceReader(stream); |
| 106 | + var enumerator = reader.GetEnumerator(); |
| 107 | + while (enumerator.MoveNext()) |
| 108 | + { |
| 109 | + if (enumerator.Key is not string key |
| 110 | + || !key.EndsWith(".baml", StringComparison.OrdinalIgnoreCase)) |
| 111 | + { |
| 112 | + continue; |
| 113 | + } |
| 114 | + reader.GetResourceData(key, out _, out var rawData); |
| 115 | + if (rawData == null || rawData.Length <= 4) |
| 116 | + { |
| 117 | + continue; |
| 118 | + } |
| 119 | + var baml = new byte[rawData.Length - 4]; |
| 120 | + Array.Copy(rawData, 4, baml, 0, baml.Length); |
| 121 | + |
| 122 | + BamlDocument document; |
| 123 | + try |
| 124 | + { |
| 125 | + document = BamlReader.ReadDocument(new MemoryStream(baml, writable: false)); |
| 126 | + } |
| 127 | + catch |
| 128 | + { |
| 129 | + _canRewrite = false; |
| 130 | + continue; |
| 131 | + } |
| 132 | + rewrite.Documents[key] = document; |
| 133 | + CollectFromDocument(document, assemblyName, typesByFullName); |
| 134 | + } |
| 135 | + if (rewrite.Documents.Count > 0) |
| 136 | + { |
| 137 | + _rewrites.Add(rewrite); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + private void CollectFromDocument(BamlDocument document, string assemblyName, |
| 142 | + Dictionary<string, TypeDefinition> typesByFullName) |
| 143 | + { |
| 144 | + var assemblies = new Dictionary<ushort, string>(); |
| 145 | + var typeRecords = new Dictionary<ushort, TypeInfoRecord>(); |
| 146 | + foreach (var record in document) |
| 147 | + { |
| 148 | + switch (record) |
| 149 | + { |
| 150 | + case AssemblyInfoRecord assembly: |
| 151 | + assemblies[assembly.AssemblyId] = assembly.AssemblyFullName; |
| 152 | + break; |
| 153 | + case TypeInfoRecord type: |
| 154 | + typeRecords[type.TypeId] = type; |
| 155 | + break; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + bool IsLocal(ushort assemblyId) |
| 160 | + { |
| 161 | + if (!assemblies.TryGetValue(assemblyId, out var full) || full == null) |
| 162 | + { |
| 163 | + return false; |
| 164 | + } |
| 165 | + var simple = full; |
| 166 | + var comma = simple.IndexOf(','); |
| 167 | + if (comma >= 0) |
| 168 | + { |
| 169 | + simple = simple.Substring(0, comma); |
| 170 | + } |
| 171 | + return string.Equals(simple.Trim(), assemblyName, StringComparison.OrdinalIgnoreCase); |
| 172 | + } |
| 173 | + |
| 174 | + TypeDefinition? Resolve(TypeInfoRecord record) |
| 175 | + { |
| 176 | + if (!IsLocal(record.AssemblyId)) |
| 177 | + { |
| 178 | + return null; |
| 179 | + } |
| 180 | + return typesByFullName.TryGetValue(record.TypeFullName ?? string.Empty, out var def) ? def : null; |
| 181 | + } |
| 182 | + |
| 183 | + foreach (var record in typeRecords.Values) |
| 184 | + { |
| 185 | + var def = Resolve(record); |
| 186 | + if (def != null) |
| 187 | + { |
| 188 | + XamlTypes.Add(def); |
| 189 | + _typeEdits.Add((record, def)); |
| 190 | + } |
| 191 | + } |
| 192 | + foreach (var record in document) |
| 193 | + { |
| 194 | + if (record is AttributeInfoRecord attribute |
| 195 | + && typeRecords.TryGetValue(attribute.OwnerTypeId, out var ownerRecord) |
| 196 | + && Resolve(ownerRecord) is { } owner) |
| 197 | + { |
| 198 | + XamlTypes.Add(owner); |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + private void ComputeRenamable(bool rewriteEnabled) |
| 204 | + { |
| 205 | + if (!rewriteEnabled || !_canRewrite) |
| 206 | + { |
| 207 | + return; |
| 208 | + } |
| 209 | + // A type whose name appears in any BAML string value is referenced as text (e.g. {x:Type}, |
| 210 | + // TargetType, a binding path), which we don't rewrite - keep those names. Only types |
| 211 | + // referenced purely as elements/attributes are safe to rename. |
| 212 | + var stringValues = new List<string>(); |
| 213 | + foreach (var rewrite in _rewrites) |
| 214 | + { |
| 215 | + foreach (var document in rewrite.Documents.Values) |
| 216 | + { |
| 217 | + foreach (var record in document) |
| 218 | + { |
| 219 | + var value = record switch |
| 220 | + { |
| 221 | + PropertyRecord p => p.Value, |
| 222 | + TextRecord t => t.Value, |
| 223 | + StringInfoRecord s => s.Value, |
| 224 | + DefAttributeRecord d => d.Value, |
| 225 | + _ => null |
| 226 | + }; |
| 227 | + if (!string.IsNullOrEmpty(value)) |
| 228 | + { |
| 229 | + stringValues.Add(value!); |
| 230 | + } |
| 231 | + } |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + foreach (var type in XamlTypes) |
| 236 | + { |
| 237 | + var simpleName = type.Name?.Value; |
| 238 | + if (string.IsNullOrEmpty(simpleName)) |
| 239 | + { |
| 240 | + continue; |
| 241 | + } |
| 242 | + var mentioned = false; |
| 243 | + foreach (var value in stringValues) |
| 244 | + { |
| 245 | + if (value.IndexOf(simpleName!, StringComparison.Ordinal) >= 0) |
| 246 | + { |
| 247 | + mentioned = true; |
| 248 | + break; |
| 249 | + } |
| 250 | + } |
| 251 | + if (!mentioned) |
| 252 | + { |
| 253 | + RenamableTypes.Add(type); |
| 254 | + } |
| 255 | + } |
| 256 | + } |
| 257 | +#endif |
| 258 | + |
| 259 | + /// <summary> |
| 260 | + /// Renames the renamable XAML type names (keeping their namespace) and rewrites the BAML so it |
| 261 | + /// still points at them. Members are untouched. No-op when rewriting is disabled/unsafe. |
| 262 | + /// </summary> |
| 263 | + public void ApplyRewrite(ModuleDefinition module, Renamer renamer) |
| 264 | + { |
| 265 | +#if !NETSTANDARD |
| 266 | + if (RenamableTypes.Count == 0) |
| 267 | + { |
| 268 | + return; |
| 269 | + } |
| 270 | + foreach (var type in RenamableTypes) |
| 271 | + { |
| 272 | + if (!renamer.CanRename(type)) |
| 273 | + { |
| 274 | + continue; |
| 275 | + } |
| 276 | + // BAML resolves a type by its full name (namespace.name), so the new name must not |
| 277 | + // contain '.' - swap the renamer's dots for spaces, which are valid in type names. |
| 278 | + type.Name = renamer.RenameUnsafely().Replace('.', ' '); |
| 279 | + } |
| 280 | + foreach (var (record, type) in _typeEdits) |
| 281 | + { |
| 282 | + record.TypeFullName = type.FullName; |
| 283 | + } |
| 284 | + foreach (var rewrite in _rewrites) |
| 285 | + { |
| 286 | + WriteBack(module, rewrite); |
| 287 | + } |
| 288 | +#endif |
| 289 | + } |
| 290 | + |
| 291 | +#if !NETSTANDARD |
| 292 | + private static void WriteBack(ModuleDefinition module, ResourceRewrite rewrite) |
| 293 | + { |
| 294 | + var original = rewrite.Resource.GetData(); |
| 295 | + if (original == null) |
| 296 | + { |
| 297 | + return; |
| 298 | + } |
| 299 | + using var output = new MemoryStream(); |
| 300 | + using (var writer = new ResourceWriter(output)) |
| 301 | + using (var reader = new ResourceReader(new MemoryStream(original, writable: false))) |
| 302 | + { |
| 303 | + var enumerator = reader.GetEnumerator(); |
| 304 | + while (enumerator.MoveNext()) |
| 305 | + { |
| 306 | + var key = (string)enumerator.Key; |
| 307 | + reader.GetResourceData(key, out var typeName, out var resourceData); |
| 308 | + if (rewrite.Documents.TryGetValue(key, out var document)) |
| 309 | + { |
| 310 | + using var bamlStream = new MemoryStream(); |
| 311 | + bamlStream.Position = 4; |
| 312 | + BamlWriter.WriteDocument(document, bamlStream); |
| 313 | + var length = (int)bamlStream.Length - 4; |
| 314 | + bamlStream.Position = 0; |
| 315 | + bamlStream.Write(BitConverter.GetBytes(length), 0, 4); |
| 316 | + resourceData = bamlStream.ToArray(); |
| 317 | + } |
| 318 | + writer.AddResourceData(key, typeName, resourceData); |
| 319 | + } |
| 320 | + writer.Generate(); |
| 321 | + } |
| 322 | + var bytes = output.ToArray(); |
| 323 | + var index = module.Resources.IndexOf(rewrite.Resource); |
| 324 | + if (index >= 0) |
| 325 | + { |
| 326 | + module.Resources[index] = new ManifestResource(rewrite.Resource.Name, |
| 327 | + rewrite.Resource.Attributes, new DataSegment(bytes)); |
| 328 | + } |
| 329 | + } |
| 330 | +#endif |
| 331 | +} |
0 commit comments