序列化的.NET类到PHP

shr*_*gon 3 .net php c# converter type-conversion

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

U:??S?@??-??v?Y?????????An?@AMAUI??????
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*far 8

简短回答:

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





答案很长:

但是,如果不可能,那当然有办法......

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

我的所有研究都基于.NET Remoting:二进制格式数据结构规范.



示例类:

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

A看起来像这样:

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

    public int SomeValue
    {
        get;
        set;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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



示例结果数据:

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

示例结果数据



让我们解释一下示例结果数据:

根据上述规范(这里是PDF的直接链接:[MS-NRBF] .pdf),流中的每个记录都由RecordTypeEnumeration.部分2.1.2.1 RecordTypeNumeration说明:

此枚举标识记录的类型.每条记录(MemberPrimitiveUnTyped除外)都以记录类型枚举开头.枚举的大小是一个BYTE.



SerializationHeaderRecord:

因此,如果我们回顾一下我们得到的数据,我们就可以开始解释第一个字节了:

SerializationHeaderRecord_RecordTypeEnumeration

如标识2.1.2.1 RecordTypeEnumeration值中0所述SerializationHeaderRecord,其中指定了2.6.1 SerializationHeaderRecord:

SerializationHeaderRecord记录必须是二进制序列化中的第一条记录.此记录具有格式的主要版本和次要版本以及顶部对象和标题的ID.

它包括:

  • RecordTypeEnum(1个字节)
  • RootId(4个字节)
  • HeaderId(4个字节)
  • MajorVersion(4个字节)
  • MinorVersion(4个字节)



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

SerializationHeaderRecord_Complete

00表示RecordTypeEnumeration这是SerializationHeaderRecord在我们的例子.

01 00 00 00 代表着 RootId

如果序列化流中既不存在BinaryMethodCall也不存在BinaryMethodReturn记录,则该字段的值必须包含序列化流中包含的Class,Array或BinaryObjectString记录的ObjectId.

所以在我们的情况下,这应该是ObjectId1(因为数据使用little-endian序列化),我们希望再次看到;-)

FF FF FF FF 代表着 HeaderId

01 00 00 00 代表着 MajorVersion

00 00 00 00代表MinorVersion



BinaryLibrary:

按照规定,每条记录必须以RecordTypeEnumeration.当最后一条记录完成时,我们必须假设一条新记录开始.

让我们解释下一个字节:

BinaryLibraryRecord_RecordTypeEnumeration

正如我们所看到的,在我们的例子中,SerializationHeaderRecord它后跟着BinaryLibrary记录:

BinaryLibrary记录将INT32 ID(在[MS-DTYP]部分2.2.22中指定)与库名称相关联.这允许其他记录使用ID引用库名称.当有多个引用相同库名称的记录时,此方法会减小线缆大小.

它包括:

  • RecordTypeEnum(1个字节)
  • LibraryId(4个字节)
  • LibraryName(可变字节数(即a LengthPrefixedString))



2.1.1.6 LengthPrefixedString......中所述

LengthPrefixedString表示字符串值.该字符串以UTF-8编码字符串的长度为前缀,以字节为单位.长度编码在可变长度字段中,最小为1个字节,最多为5个字节.为了最小化线尺寸,将长度编码为可变长度字段.

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

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C表示RecordTypeEnumeration识别BinaryLibrary记录的内容.

02 00 00 00表示LibraryId这是2在我们的例子.



现在LengthPrefixedString如下:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

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

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

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



ClassWithMembersAndTypes:

再次,记录完成,所以我们解释RecordTypeEnumeration下一个:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

05识别ClassWithMembersAndTypes记录.部分2.3.2.1 ClassWithMembersAndTypes说明:

ClassWithMembersAndTypes记录是类记录中最详细的.它包含有关成员的元数据,包括成员的名称和远程处理类型.它还包含一个引用类的库名称的库ID.

它包括:

  • RecordTypeEnum(1个字节)
  • ClassInfo(可变字节数)
  • MemberTypeInfo(可变字节数)
  • LibraryId(4个字节)



的ClassInfo:

2.3.1.1 ClassInfo记录中所述,包括:

  • ObjectId(4个字节)
  • 名称(可变字节数(再次为a LengthPrefixedString))
  • MemberCount(4个字节)
  • MemberNames(这是一个序列,LengthPrefixedString其中项的数量必须等于MemberCount字段中指定的值.)



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

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

01 00 00 00代表着ObjectId.我们已经看到了这一个,它被指定为RootIdSerializationHeaderRecord.

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

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

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

02 00 00 00代表它MemberCount,它告诉我们,2个成员,两个代表LengthPrefixedString将跟随.

第一个成员的名字: ClassWithMembersAndTypesRecord_MemberNameOne

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代表第一个MemberName,1B又是字符串的长度,长度为27个字节,结果如下:<SomeString>k__BackingField.

第二个成员的名字: ClassWithMembersAndTypesRecord_MemberNameTwo

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表示第二个MemberName,1A指定字符串长度为26个字节.结果如下:<SomeValue>k__BackingField.



MemberTypeInfo:

之后ClassInfoMemberTypeInfo如下.

部分2.3.1.2 - MemberTypeInfo说明,该结构包含:

  • BinaryTypeEnums(长度可变)

一系列BinaryTypeEnumeration值,表示正在传输的成员类型.数组必须:

  • 与ClassInfo结构的MemberNames字段具有相同数量的项目.

  • 按顺序排列,使BinaryTypeEnumeration对应于ClassInfo结构的MemberNames字段中的成员名称.

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

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

所以考虑到这一点,我们几乎就在那里......我们期待2个BinaryTypeEnumeration值(因为我们有2个成员MemberNames).



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

ClassWithMembersAndTypesRecord_MemberTypeInfo

01代表BinaryTypeEnumeration第一个成员,根据2.1.2.2 BinaryTypeEnumeration我们可以预期a String和它用a表示LengthPrefixedString.

00代表BinaryTypeEnumeration第二个成员,再次,根据规范,它是一个Primitive.如上所述,Primitive其后是附加信息,在这种情况下是a PrimitiveTypeEnumeration.这就是为什么我们需要读取下一个字节,即08与表中所述的表匹配,2.1.2.3 PrimitiveTypeEnumeration并惊讶地发现我们可以预期一个Int32由4个字节表示,如其他一些关于基本数据类型的文档中所述.



库Id:

在后MemerTypeInfoLibraryId如下,它由4个字节表示:

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00代表LibraryId哪个是2.



价值:

由于在规定2.3 Class Records:

必须将类的成员的值序列化为遵循此记录的记录,如2.7节所述.记录的顺序必须与ClassInfo(第2.3.1.1节)结构中指定的MemberNames的顺序相匹配.

这就是我们现在可以期待成员价值的原因.

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

BinaryObjectStringRecord_RecordTypeEnumeration

06识别出一个BinaryObjectString.它代表了我们SomeString财产的价值(<SomeString>k__BackingField确切地说).

根据2.5.7 BinaryObjectString它包含:

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



所以我们可以清楚地认识到这一点

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

03 00 00 00代表着ObjectId.

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

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

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

必须是Value我们的第二个成员.7B十六进制等于123十进制,似乎适合我们的示例代码.

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



MessageEnd:

MessageEnd_RecordTypeEnumeration

最后,最后一个字节0B代表MessageEnd记录.

  • 哇.真棒的答案! (3认同)