65.9K
CodeProject 正在变化。 阅读更多。
Home

利用 LINQ to XML:查询混淆映射:第 2 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2009年10月5日

CPOL

7分钟阅读

viewsIcon

21497

downloadIcon

194

LINQ to XML 技术的一个实际应用。

引言

本文介绍 LINQ to XML 的实际用法。

这是“利用 LINQ to XML:查询混淆映射”系列文章的第二部分,也是最后一部分。建议阅读上一部分 - 这将帮助您更好地理解下面的内容。

上一部分中,我们使用 LINQ to XML 查询,通过提供混淆后的名称来搜索混淆映射中的原始类型名称。现在,我想向您展示如何使用高级 LINQ 查询来提供更复杂的搜索任务,例如搜索原始类型成员名称。

任务定义

正如之前定义的,用户界面应该尽可能简单。因此,我决定将 UI 限制为两个编辑器。第一个用于提供混淆映射文件名,另一个用于用户搜索请求。

让我们来看三个可能的用作搜索条件的用户输入

  1. a - 这显然是对混淆类型“a”的搜索请求。
  2. a.b - 这个不太清楚 - 它可能是一个复杂类型名称“a.b”,或者是一个类型为“a”的名为“b”的字段。我们无法区分这两种情况,因此我们将同时搜索类型和字段。
  3. a.b(System.String, int) - 这显然是一个方法搜索请求,其签名是(string, int)

注意:完整的方法签名包含一个结果类型,但由于 .NET 不支持基于结果类型的方法重载,因此我们不需要它。

成员搜索任务可以分为两个步骤

  1. 类型搜索。
  2. 基于第一步结果的成员搜索。

我们已经有了完成第一步的代码。要使用它,我们只需要提供混淆的类型名称。因此,更详细的步骤列表是

  • 检测输入类型;
  • 解析输入以分隔类型名称和成员名称;
  • 解析成员名称以获取方法签名;
  • 搜索具有解析类型名称的类型;
  • 在找到的类型成员中搜索匹配解析成员名称的字段或方法。

成员名称搜索

为了将类型名称与成员名称分开,我们可以使用“.”分隔符分割输入字符串,并将最后一部分作为成员名称。但正如我们所见,方法签名中也存在点,因此我决定将签名中的点替换为其他内容,以便我可以使用带点的类型名称分割。下面的代码将方法签名中的“.”替换为“/”,如果存在的话。

// Replace dots within method parameters with '/' to correctly split names.
int sigIndex = obfuscatedName.IndexOf('(');
if (sigIndex > 0) // Signature is present.

  // Explicit conversion required for IEnumerable extension methods call.
  obfuscatedName = new string(((IEnumerable<char>)obfuscatedName) 
    .Select((c, i) => i <= sigIndex ?  // If index less then signature start
        c :                  // bypass characters.
        c == '.' ? '/' : c)  // Otherwise if character inside signature 
                             // is '.' replace it with '/' otherwise bypass.
    .ToArray());

此代码通过搜索“("”符号来检测签名的存在,并使用索引的 `Select` 扩展方法将签名内的所有“.”替换为“/”。

现在,我们可以通过简单地调用 `obfuscatedName.Split('.')` 方法来分割混淆的名称。最后一个子字符串可以是方法签名或字段名称。

方法签名需要一些额外的解析来检测参数类型,以及用于比较签名的相等性例程。我在 `Signature` 类中实现了此逻辑。为了实现代码的统一,我还使用 `Signature` 类表示 *字段* 搜索请求 - 因此任何类型成员搜索请求都将是 `Signature` 实例。`Signature` 有三个主要成员

  • bool IsMethod - 指示这是一个方法签名。
  • bool MemberName - 类型成员的名称。
  • bool SygEquals(string sig) - 检查传递的签名字符串是否与实例相等。

我将不提供 `Signature` 类的实现细节,因为它们非常简单,您可以在文章开头的代码示例中找到它们。

有两种类型的类型需要搜索

  • 第一个是完整的类型,它涵盖了整个搜索字符串(例如,类型名称“a.b”输入是“a.b”)
  • 另一个是不完整的类型,它部分涵盖了输入字符串,但不包括最后一个名称元素(例如,类型名称“a”输入是“a.b”)。正如我们之前定义的,这种类型对于成员搜索是必需的。

注意:我还想展示的一件事是如何在填充数据之前声明一个匿名类型数组。例如,您想声明一个匿名类型数组,并根据某些条件填充数据,然后处理该数组。在这种情况下,我使用以下模式

// Decalare anonymous array.
var arr = new[] { new { Value = 0, Text = "" } };

// Fill array with data.
if (SomeCondition)
  // You can fill it with new instance.
  arr = new[] { new { Value = 1, Text = "First" }, 
                new { Value = 2, Text = "Second" } };
else
  // Or use existing elements.
  arr[0] =  new { Value = -1, Text = "Failed" };

// Process array data.
foreach (var e in arr)
  Console.WriteLine(e.Text + ", " + e.Value);

这种方法允许您在不需要时不必定义命名类型。我使用此模式来定义一个类型数组,并带有一个指示类型是否完整的标志。

// Define array of anonymous types  - tricky but its works.
var typeNames = new[] { new { Name = "", IsComplete = false } };

现在我们需要重写原始类型搜索查询以使用 `typeNames` 数组。

// Define type query.
var types = from type in map.Descendants("type")
           join tname in typeNames on 
             (type.Element("newname") ?? type.Element("name")).Value 
             equals tname.Name
           select new { TypeElement = type, IsComplete = tname.IsComplete };

这里使用的 `join` 操作允许我方便地过滤 `typeNames` 数组。将为查询结果执行匿名类型投影。此投影包含找到的类型元素以及指示类型完整性的标志。要获取所有找到的类型,我们可以使用一个简单的表达式

types.Where(t => t.IsComplete);

注意:LINQ 查询基于延迟执行;这意味着查询的执行被推迟到您访问数据的那一刻,甚至更多 - 每次访问查询时都会执行查询。以下代码演示了此行为

XElement xml = new XElement("parent",
               new XElement("child1")
             );

// Query that retrieve all child elements.
var qry = from e in xml.Elements()
        select e;

// Add child element to show deferred execuiton.
xml.Add(new XElement("child2"));


foreach(var q in qry)
    Console.Write(q.Name + " ");
    // Print: "child1 child2".
Console.WriteLine();

// Add new child element to show "each time" execution.
xml.Add(new XElement("child3"));

foreach (var q in qry)
    Console.Write(q.Name + " ");
    // Print: "child1 child2 child3".

为了避免重复执行,您可以使用 `.ToArray()` 或 `.ToList()` 扩展方法缓存查询结果。我打算将 *types* 查询的结果用于完整类型搜索和类型成员搜索,因此在最坏的情况下,*types* 查询将执行两次。为了避免这种情况,我使用 `.ToArray()` 函数缓存查询结果。

现在,当我们找到类型后,我们可以继续进行类型成员搜索。直接的方法是使用嵌套的select

var members = // For each incoplete types
from type in types
where !type.IsComplete

// Select all type methods that has matched name and signature.
from member in
     type.TypeElement.Element("methodlist").Elements("method")
where member.Element("newname").Value == signature.MemberName &&
      signature.SygEquals(member.Element("signature").Value)
select member;

在这里,我们从不完整类型中选择方法,并通过匹配 `Signature` 对象进行过滤。此查询将被编译为类似以下内容

var members =
// Filter incomplete types.
types.Where(type => !type.IsComplete)

// Flatten all types members in one enumeration.
.SelectMany(
   type => type.TypeElement.Element("methodlist").Elements("method"),
    // Projecting result to temporary anonymous type.
   (type, member) => new { type = type, member = member }   
)

// Filter out type members by signature matching.
.Where(typeMember => 
   typeMember.member.Element("newname").Value == signature.MemberName &&
   signature.SygEquals(typeMember.member.Element("signature").Value)
)

// Select only member XElement from result
.Select(typeMember => typeMember.member);

在这里,我们可以看到带有匿名类型投影的 `SelectMany` 调用。在一般情况下,`types` 数组中的每个 `type` 元素都包含一个方法集合,因此它看起来像一个二维集合,由类型组成,每个类型都包含一个方法集合。`SelectMany` 调用将此二维集合展平为一维,然后我们进行过滤。

这是一个一般情况,但在我们的情况下,我们将有一个类型集合,其中最多包含一个方法的集合(因为我们的搜索过滤器)。因此,我们有一个冗余的 `SelectMany` 调用和一个匿名类型投影,这将影响性能。我们无法使用清晰的 LINQ 语法来修复此问题,但我们可以将 LINQ 与扩展方法和 lambda 表达式结合使用来实现所需的结果

var members = ( // For each incoplete types
 from type in types
 where !type.IsComplete

 // Select single method that mathes name and signature.
 select type.TypeElement.Element("methodlist").Elements("method")
  .SingleOrDefault(member => 
      member.Element("newname").Value == signature.MemberName &&
      signature.SygEquals(member.Element("signature").Value)
   )
).Where(m => m != null);

在这里,我将 LINQ select 与 `SingleOrDefault` 扩展方法调用结合使用,这使我能够删除 `SelectMany` 调用和匿名类型投影。最后的 `Where` 方法调用会滤除结果中的默认值 - 如果找不到任何内容,这将返回一个空枚举。以下是它将被编译为的内容

var members =
    // Filter incomplete types.
    types.Where(type => !type.IsComplete)

    // Select method that mathes name and signature.
    .Select(type =>
        type.TypeElement.Element("methodlist").Elements("method").SingleOrDefault(
          member =>
            member.Element("newname").Value == signature.MemberName &&
            signature.SygEquals(member.Element("signature").Value)
        )
    ).Where(m => m != null);

最后,我们可以将成员查询结果投影到匿名类型,这有助于我们将来处理它。

var membersPrj = from member in members
                 select new {
                   ModuleName = member.Parent.Parent.Parent.Element("name").Value,
                   TypeName = member.Parent.Parent.Element("name").Value,
                   MemberName = member.Element("name").Value,
                   Signature = member.Element("signature").Value
                 };

由于统一的 `Signature` 类解决方案,方法和字段搜索之间没有区别,因此我将此方法推广到字段和方法搜索。现在,您可以按需处理找到的成员,例如,将它们输出到控制台

foreach (var member in membersPrj) {
    Console.WriteLine("Module:      {0}", member.ModuleName);
    Console.WriteLine("Type:        {0}", member.TypeName);
    Console.WriteLine("Member:      {0}", member.MemberName);
    Console.WriteLine();
}

完整的解决方案可以从文章顶部的链接下载。

摘要

就这样。还有很多事情要做,例如,应用程序可以处理整个调用堆栈并检索其反混淆版本;此外,拥有一个外部 API 很有用,例如命令行或类似的,但这超出了本文的范围。

我还要提及一些我在开发过程中遇到过的问题。首先是由于匿名类型不能用作方法参数,代码分解很困难。另一个是 LINQ 查询调试非常困难(但感谢 LINQ Pad,它不像可能的那样困难)。其他问题都不太明显,因此我认为它们不值得在此提及。

本文描述了我第一次实际使用 LINQ to SQL 技术的经验。希望您阅读愉快,并且本文的材料将为您带来一些新的有用的经验,您可以在实践中应用。

© . All rights reserved.