扫描 COM 引用以帮助生成 Reg-Free COM 清单

Scan for COM references to help generate Reg-Free COM manifests

我正在 InstallShield 2015 中将一个大型安装项目重新实施为 MSI 安装程序(它以前是来自更旧版本 InstallShield 的 InstallScript 安装程序)。我想做的许多改进之一是允许通过使用清单文件和遵循免注册 COM 激活模式以免注册方式解析所有 COM 引用。我认为这在所有情况下都是不可能的,因为我们有一些 MMC 管理单元,据我所知,它不能使用 reg-free COM,但我想尽可能接近。

挑战在于找出我需要创建的所有清单。我们提供数以千计的文件。我正在寻找可以帮助扫描文件以确定的实用程序:

  1. 哪些文件代表 COM 服务器,它们包含哪些 CLSID
  2. 哪些文件代表 COM 客户端,它们引用了哪些 CLSID

希望有了这些信息,我可以使用 InstallShield 的 Reg-Free COM 向导生成必要的清单文件。

有没有什么可以帮助解决这个问题的?如果还没有这样的实用程序,我将尝试通过以下方式编写自己的实用程序:

  1. 查看 COM 服务器中的一些已知 CLSID 并尝试识别它们周围的一些签名字节,这可能有助于我确定如何识别 COM 服务器中的 CLSID。
  2. 编写代码在我们所有的二进制文件中查找此签名并获取所有 CLSID
  3. 在我们所有的二进制文件中搜索已知的 CLSID,并假设那些来自服务器外部的代表客户端。

如果事实证明我们使用的是 ProgId 而不仅仅是 CLSID,我可能不得不调整我的策略,但我希望这将涵盖我们的大部分 COM 引用。

如果已经有任何可能对此有帮助的实用程序,我愿意使用它而不是自己编写。因此,我正在寻找有关自己编写此内容的任何提示 有关查找已完成此操作的提示。

How to read TLB (type libraries) of unmanaged code from C#?, which led me to the C# source code of a type library reading class at http://clrinterop.codeplex.com/releases/view/17579 的已删除答案的帮助下,我能够读取类型库以获取 CLSID 值(如果尝试加载类型库失败则跳过文件 - 只是尝试加载类型 a来自每个可执行文件的库)。然后我对所有可执行文件进行了简单搜索以查找那些 CLSID 值。我不确定结果有多完整,但至少它们看起来有点有效。

GetDirCLSIDRefs 函数是顶级函数,它 returns 一个可执行文件名列表,对于每个文件名,还有一个关联文件列表,其类型库包含文件引用的 CLSID。

Private Function GetDirCLSIDRefs(Directory As String, CLSIDs As KeyValuePair(Of Guid, String)()) As KeyValuePair(Of String, String())()
   Dim result As New List(Of KeyValuePair(Of String, String()))
   Dim entries = New System.IO.DirectoryInfo(Directory).GetFileSystemInfos()
   For Each entry In entries
      If TypeOf entry Is System.IO.FileInfo AndAlso _
         (entry.Name.EndsWith(".exe") OrElse _
          entry.Name.EndsWith(".dll") OrElse _
          entry.Name.EndsWith(".ocx")) Then
         Dim refs = ScanForCLSIDs(entry.FullName, CLSIDs)
         If refs IsNot Nothing AndAlso refs.Length > 0 Then result.Add(New KeyValuePair(Of String, String())(entry.FullName, refs))
      ElseIf TypeOf entry Is System.IO.DirectoryInfo Then
         result.AddRange(GetDirCLSIDRefs(entry.FullName, CLSIDs))
      End If
   Next
   Return result.ToArray()
End Function

Private Function GetDirCLSIDs(Directory As String) As KeyValuePair(Of Guid, String)()
   Dim result As New List(Of KeyValuePair(Of Guid, String))
   Dim entries = New System.IO.DirectoryInfo(Directory).GetFileSystemInfos()
   For Each entry In entries
      If TypeOf entry Is System.IO.FileInfo AndAlso _
         (entry.Name.EndsWith(".exe") OrElse _
          entry.Name.EndsWith(".dll") OrElse _
          entry.Name.EndsWith(".ocx")) Then
         For Each clsid In GetCLSIDs(entry.FullName)
            result.Add(New KeyValuePair(Of Guid, String)(clsid, entry.FullName))
         Next
      ElseIf TypeOf entry Is System.IO.DirectoryInfo Then
         result.AddRange(GetDirCLSIDs(entry.FullName))
      End If
   Next
   Return result.ToArray()
End Function

Private Function GetCLSIDs(FileName As String) As Guid()
   Dim tlb = TypeLibTypes.Interop.TypeLib.Load(FileName)
   If tlb.GetTypeLib() Is Nothing Then Return {}
   Dim result As New List(Of Guid)
   Try
      For typeIndex As Integer = 0 To tlb.GetTypeInfoCount() - 1
         Using attr = tlb.GetTypeInfo(typeIndex).GetTypeAttr()
            If (attr.wTypeFlags And TypeLibTypes.Interop.TYPEFLAGS.TYPEFLAG_FHIDDEN) = 0 _
               AndAlso attr.typekind = TypeLibTypes.Interop.TYPEKIND.TKIND_COCLASS Then
               result.Add(attr.Guid)
            End If
         End Using
      Next
   Catch ex As System.Runtime.InteropServices.COMException When ex.ErrorCode = &H80029C4A
      Return {}
   End Try
   Return result.ToArray()
End Function

Private Function ScanForCLSIDs(FileName As String, CLSIDFiles As KeyValuePair(Of Guid, String)()) As String()
   Dim content As Byte()
   Using file As New System.IO.FileStream(FileName, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.ReadWrite)
      content = DirectCast(Array.CreateInstance(GetType(Byte), CInt(file.Length)), Byte())
      file.Read(content, 0, CInt(file.Length))
   End Using
   Dim result As New List(Of String)
   For Each CLSIDFile In CLSIDFiles
      If FindBytes(content, CLSIDFile.Key.ToByteArray()) >= 0 Then
         If Not result.Contains(CLSIDFile.Value) Then result.Add(CLSIDFile.Value)
      End If
   Next
   Return result.ToArray()
End Function

Private Function FindBytes(data As Byte(), pattern As Byte(), Optional start As Integer = 0) As Int32
   If pattern.Length = 0 Then Return -1
   If data.Length = 0 Then Return 0
   Dim search = ProcessSearchPattern(pattern)
   Dim i As Integer = start + pattern.Length - 1
   Do While i < data.Length
      Dim j As Integer = pattern.Length - 1
      Do While j >= 0 AndAlso data(i) = pattern(j)
         i -= 1
         j -= 1
      Loop
      If j < 0 Then Return i + 1
      i += Math.Max(search.Delta1(data(i)), search.Delta2(j))
   Loop
   Return -1
End Function

Private Class SearchPattern
   Public Delta1 As Integer()
   Public Delta2 As Integer()
End Class

Private Class PatternBytes
   Private bytes As Byte()
   Public Sub New(value As Byte())
      bytes = value
   End Sub
   Public Overrides Function GetHashCode() As Integer
      Dim i As Integer = 0
      GetHashCode = 0
      Do While i <= bytes.Length - 4
         GetHashCode = GetHashCode Xor BitConverter.ToInt32(bytes, i)
         i += 4
      Loop
      If i <= bytes.Length - 2 Then
         GetHashCode = GetHashCode Xor BitConverter.ToInt16(bytes, i)
         i += 2
      End If
      If i < bytes.Length Then
         GetHashCode = GetHashCode Xor bytes(i)
      End If
   End Function

   Public Overrides Function Equals(obj As Object) As Boolean
      Dim compare As Byte()
      If TypeOf obj Is PatternBytes Then
         compare = DirectCast(obj, PatternBytes).bytes
      ElseIf TypeOf obj Is Byte() Then
         compare = DirectCast(obj, Byte())
      Else
         Return False
      End If
      If compare.Length <> bytes.Length Then Return False
      For i As Integer = 0 To bytes.Length - 1
         If bytes(i) <> compare(i) Then Return False
      Next
      Return True
   End Function
End Class

Private Function ProcessSearchPattern(pattern As Byte()) As SearchPattern
   Static cache As New Dictionary(Of PatternBytes, SearchPattern)
   ProcessSearchPattern = Nothing
   Dim pb As New PatternBytes(pattern)
   If cache.TryGetValue(pb, ProcessSearchPattern) Then Exit Function
   ProcessSearchPattern = New SearchPattern()
   Dim patLen = pattern.Length
   ProcessSearchPattern.Delta1 = DirectCast(Array.CreateInstance(GetType(Integer), Byte.MaxValue + 1), Integer())
   ProcessSearchPattern.Delta2 = DirectCast(Array.CreateInstance(GetType(Integer), patLen), Integer())
   Dim c As Integer ' Cannot run a For loop to Byte.MaxValue in a Byte!
   Dim i As Integer
   For c = 0 To Byte.MaxValue
      ProcessSearchPattern.Delta1(c) = patLen
   Next
   For i = 0 To patLen - 2
      ProcessSearchPattern.Delta1(pattern(i)) = patLen - 1 - i
   Next

   Dim m As Integer
   Dim lastPrefixIndex = patLen - 1
   Dim isPrefix As Boolean = False
   For i = patLen - 1 To 0 Step -1
      isPrefix = True
      If patLen - i - 2 > 0 Then
         For m = 0 To patLen - i - 2
            If pattern(m) <> pattern(m + i + 1) Then
               isPrefix = False
               Exit For
            End If
         Next
      End If
      If isPrefix Then lastPrefixIndex = i + 1
      ProcessSearchPattern.Delta2(i) = lastPrefixIndex + (patLen - 1 - i)
   Next

   For i = 0 To patLen - 2
      For m = 0 To i - 1
         If pattern(i - m) = pattern(patLen - 1 - m) Then
            Exit For
         End If
         If pattern(i - m) <> pattern(patLen - 1 - m) Then
            ProcessSearchPattern.Delta2(patLen - 1 - m) = patLen - 1 - i + m
         End If
      Next
   Next

   cache.Add(pb, ProcessSearchPattern)
End Function