序列化 .NET class 到 PHP

Serialized .NET class to PHP

我正在从源中获取一些序列化的 .NET class 字符串数据,我只需要将其转换为 PHP 中可读的内容。不一定必须变成 "object" 或 JSON 但我需要以某种方式阅读它。我认为 .NET 字符串只是具有一些设置属性的 class,但它是二进制的,显然不可移植。我不想将 .NET 代码转换为 PHP 代码。以下是数据示例:

U:?�S�@��-��v�Y��?������An�@AMAUI������

我意识到这实际上是二进制的而不是可打印的文本。我只是用这个作为我在 catting 文件时看到的例子。

简答:

我真的建议不要自己实现二进制表示的解释。我会改用另一种格式(JSON, XML,等等)。





长答案:

然而,如果这是不可能的,当然有办法......

实际问题是:序列化 .NET 对象的二进制格式是什么样的,我们如何正确解释它?

我的所有研究都基于 .NET Remoting: Binary Format Data Structure 规范。



示例class:

为了有一个工作示例,我创建了一个名为 A 的简单 class,它包含 2 个属性,一个字符串和一个整数值,它们分别称为 SomeStringSomeValue.

Class A 看起来像这样:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}

对于序列化,我当然使用了BinaryFormatter

BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();

可以看出,我传递了一个包含 abc123 作为值的 class A 的新实例。



示例结果数据:

如果我们在十六进制编辑器中查看序列化结果,我们会得到如下内容:



让我们来解读示例结果数据:

根据上述规范(这里是直接 link 到 PDF:[MS-NRBF].pdf),流中的每条记录都由 RecordTypeEnumeration 标识。 2.1.2.1 RecordTypeNumeration 节指出:

This enumeration identifies the type of the record. Each record (except for MemberPrimitiveUnTyped) starts with a record type enumeration. The size of the enumeration is one BYTE.



SerializationHeaderRecord:

所以如果我们回头看看我们得到的数据,我们可以开始解释第一个字节:

2.1.2.1 RecordTypeEnumeration 所述,0 的值标识 2.6.1 SerializationHeaderRecord 中指定的 SerializationHeaderRecord:

The SerializationHeaderRecord record MUST be the first record in a binary serialization. This record has the major and minor version of the format and the IDs of the top object and the headers.

它包括:

  • RecordTypeEnum(1 字节)
  • RootId(4 字节)
  • HeaderId(4 字节)
  • 主要版本(4 字节)
  • 次要版本(4 字节)



有了这些知识,我们就可以解释包含 17 个字节的记录:

00 表示 RecordTypeEnumeration,在我们的例子中是 SerializationHeaderRecord

01 00 00 00表示RootId

If neither the BinaryMethodCall nor BinaryMethodReturn record is present in the serialization stream, the value of this field MUST contain the ObjectId of a Class, Array, or BinaryObjectString record contained in the serialization stream.

所以在我们的例子中,这应该是 ObjectId 和值 1(因为数据是使用小端序列化的),我们希望能再次看到它 ;-)

FF FF FF FF表示HeaderId

01 00 00 00表示MajorVersion

00 00 00 00表示MinorVersion



二进制库:

按照规定,每条记录必须以 RecordTypeEnumeration 开头。随着最后一个记录的完成,我们必须假设一个新的记录开始了。

让我们解读下一个字节:

如我们所见,在我们的示例中,SerializationHeaderRecord 后跟 BinaryLibrary 记录:

The BinaryLibrary record associates an INT32 ID (as specified in [MS-DTYP] section 2.2.22) with a Library name. This allows other records to reference the Library name by using the ID. This approach reduces the wire size when there are multiple records that reference the same Library name.

它包括:

  • RecordTypeEnum(1 字节)
  • LibraryId(4 字节)
  • LibraryName(可变字节数(这是一个LengthPrefixedString))



2.1.1.6 LengthPrefixedString...

所述

The LengthPrefixedString represents a string value. The string is prefixed by the length of the UTF-8 encoded string in bytes. The length is encoded in a variable-length field with a minimum of 1 byte and a maximum of 5 bytes. To minimize the wire size, length is encoded as a variable-length field.

在我们的简单示例中,长度始终使用 1 byte 进行编码。有了这些知识,我们可以继续解释流中的字节:

0C 表示 RecordTypeEnumeration 标识 BinaryLibrary 记录。

02 00 00 00 表示 LibraryId,在我们的例子中是 2



现在 LengthPrefixedString 如下:

42表示LengthPrefixedString的长度信息,其中包含LibraryName.

在我们的例子中,42(十进制 66)的长度信息告诉我们,我们需要读取接下来的 66 个字节并将它们解释为 LibraryName

如前所述,字符串是 UTF-8 编码的,因此上述字节的结果类似于:_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null



ClassWithMembersAndTypes:

同样,记录已经完成,所以我们解释下一个的RecordTypeEnumeration

05 标识一条 ClassWithMembersAndTypes 记录。 2.3.2.1 ClassWithMembersAndTypes 节指出:

The ClassWithMembersAndTypes record is the most verbose of the Class records. It contains metadata about Members, including the names and Remoting Types of the Members. It also contains a Library ID that references the Library Name of the Class.

它包括:

  • RecordTypeEnum(1 字节)
  • Class信息(可变字节数)
  • MemberTypeInfo(可变字节数)
  • LibraryId(4 字节)



Class信息:

2.3.1.1 ClassInfo 所述,记录包括:

  • ObjectId(4 字节)
  • 名称(可变字节数(又是一个LengthPrefixedString))
  • MemberCount(4 字节)
  • MemberNames(这是一个 LengthPrefixedString 的序列,其中项目数必须等于 MemberCount 字段中指定的值。)



回到原始数据,一步一步来:

01 00 00 00代表ObjectId。我们已经看到了这个,它被指定为 SerializationHeaderRecord 中的 RootId

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41表示class的Name,用LengthPrefixedString表示。如前所述,在我们的示例中,字符串的长度定义为 1 个字节,因此第一个字节 0F 指定必须使用 UTF-8 读取和解码 15 个字节。结果看起来像这样: Whosebug.A - 显然我使用 Whosebug 作为命名空间的名称。

02 00 00 00 代表 MemberCount,它告诉我们将有 2 个成员,都用 LengthPrefixedString 表示。

第一位成员姓名:

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 表示第一个 MemberName1B 也是字符串的长度,长度为 27 个字节,结果如下:<SomeString>k__BackingField

第二名成员姓名:

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64表示第二个MemberName1A指定字符串长度为26字节。结果是这样的:<SomeValue>k__BackingField.



成员类型信息:

ClassInfo 之后是 MemberTypeInfo

2.3.1.2 - MemberTypeInfo 节指出,该结构包含:

  • BinaryTypeEnums(长度可变)

A sequence of BinaryTypeEnumeration values that represents the Member Types that are being transferred. The Array MUST:

  • Have the same number of items as the MemberNames field of the ClassInfo structure.

  • Be ordered such that the BinaryTypeEnumeration corresponds to the Member name in the MemberNames field of the ClassInfo structure.

  • AdditionalInfos(长度可变),取决于 BinaryTpeEnum 附加信息可能存在也可能不存在。

| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |

考虑到这一点,我们就快完成了... 我们期望有 2 个 BinaryTypeEnumeration 值(因为我们在 MemberNames 中有 2 个成员)。



再次,返回完整 MemberTypeInfo 记录的原始数据:

01代表第一个成员的BinaryTypeEnumeration,根据2.1.2.2 BinaryTypeEnumeration我们可以期待一个String,用LengthPrefixedString表示。

00表示第二个成员的BinaryTypeEnumeration,同样,根据规范,它是一个Primitive。如上所述,Primitive 之后是附加信息,在本例中为 PrimitiveTypeEnumeration。这就是为什么我们需要读取下一个字节 08,将其与 2.1.2.3 PrimitiveTypeEnumeration 中声明的 table 进行匹配,然后惊讶地发现我们可以期待一个 Int32由 4 个字节表示,如其他一些关于基本数据类型的文档所述。



图书馆编号:

MemerTypeInfo之后是LibraryId,用4个字节表示:

02 00 00 00 表示 LibraryId 即 2.



值:

2.3 Class Records中所述:

The values of the Members of the Class MUST be serialized as records that follow this record, as specified in section 2.7. The order of the records MUST match the order of MemberNames as specified in the ClassInfo (section 2.3.1.1) structure.

这就是为什么我们现在可以期待成员的价值。

让我们看看最后几个字节:

06 标识一个 BinaryObjectString。它代表了我们 SomeString 属性 的值(确切地说是 <SomeString>k__BackingField)。

根据2.5.7 BinaryObjectString,它包含:

  • RecordTypeEnum(1 字节)
  • ObjectId(4 字节)
  • 值(可变长度,表示为LengthPrefixedString



所以知道这一点,我们可以清楚地识别

03 00 00 00表示ObjectId.

03 61 62 63 表示 Value,其中 03 是字符串本身的长度,61 62 63 是转换为 abc 的内容字节。

希望你能记住还有第二个成员,Int32。知道 Int32 用 4 个字节表示,我们可以得出结论,即

一定是我们第二个成员的Value7B 十六进制等于 123 十进制,这似乎符合我们的示例代码。

所以这里是完整的 ClassWithMembersAndTypes 记录:



消息结束:

最后一个字节0B表示MessageEnd条记录。