Я получаю некоторые сериализованные строковые данные класса .NET из источника, и мне просто нужно превратить их в нечто читаемое в PHP. Не обязательно превращать его в «объект» или JSON, но мне нужно как-то его прочитать. Я думаю, что строка .NET — это просто класс с некоторыми заданными свойствами, но он двоичный и, очевидно, не переносимый. Я не собираюсь конвертировать .NET-код в PHP-код. Вот пример данных:
U:?�S�@��-��v�Y��?������An�@AMAUI������
Я понимаю, что это на самом деле бинарный и не для печати текст. Я просто использую это как пример того, что я вижу при загрузке файла.
Короткий ответ:
Я бы действительно предложил НЕ реализовывать интерпретацию двоичного представления самостоятельно. Я бы использовал другой формат вместо (JSON, XML, так далее.).
Длинный ответ:
Однако, если это невозможно, есть, конечно, способ …
Актуальный вопрос: Как выглядит двоичный формат сериализованных объектов .NET и как мы можем правильно его интерпретировать?
Я основал все свои исследования на .NET Remoting: структура данных в двоичном формате Спецификация.
Пример класса:
Чтобы иметь рабочий пример, я создал простой класс с именем A
который содержит 2 свойства, одну строку и одно целое значение, они называются SomeString
а также SomeValue
,
Учебный класс 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();
Как видно, я передал новый экземпляр класса A
содержащий abc
а также 123
как значения.
Пример результатов данных:
Если мы посмотрим на сериализованный результат в шестнадцатеричном редакторе, мы получим что-то вроде этого:
Давайте интерпретируем данные результата примера:
Согласно вышеупомянутой спецификации (вот прямая ссылка на PDF: [МС-NRBF] .pdf) каждая запись в потоке идентифицируется RecordTypeEnumeration
, Раздел 2.1.2.1 RecordTypeNumeration
состояния:
Это перечисление идентифицирует тип записи. Каждая запись (за исключением MemberPrimitiveUnTyped) начинается с перечисления типа записи. Размер перечисления составляет один байт.
SerializationHeaderRecord:
Поэтому, если мы оглянемся на полученные данные, мы можем начать интерпретацию первого байта:
Как указано в 2.1.2.1 RecordTypeEnumeration
значение 0
идентифицирует SerializationHeaderRecord
который указан в 2.6.1 SerializationHeaderRecord
:
Запись SerializationHeaderRecord ДОЛЖНА быть первой записью в двоичной сериализации. Эта запись имеет основной и вспомогательный вариант формата, а также идентификаторы верхнего объекта и заголовков.
Это состоит из:
С этим знанием мы можем интерпретировать запись, содержащую 17 байтов:
00
представляет RecordTypeEnumeration
который SerializationHeaderRecord
в нашем случае.
01 00 00 00
представляет RootId
Если в потоке сериализации нет ни записи BinaryMethodCall, ни BinaryMethodReturn, значение этого поля ДОЛЖНО содержать ObjectId записи Class, Array или BinaryObjectString, содержащейся в потоке сериализации.
Так что в нашем случае это должно быть ObjectId
со значением 1
(потому что данные сериализуются с использованием порядка байтов), который мы надеемся увидеть снова 😉
FF FF FF FF
представляет HeaderId
01 00 00 00
представляет MajorVersion
00 00 00 00
представляет MinorVersion
BinaryLibrary:
Как указано, каждая запись должна начинаться с RecordTypeEnumeration
, Поскольку последняя запись завершена, мы должны предположить, что начинается новая.
Давайте интерпретируем следующий байт:
Как мы видим, в нашем примере SerializationHeaderRecord
это сопровождается BinaryLibrary
запись:
Запись BinaryLibrary связывает идентификатор INT32 (как указано в разделе 2.2.22 [MS-DTYP]) с именем библиотеки. Это позволяет другим записям ссылаться на имя библиотеки с помощью идентификатора. Этот подход уменьшает размер провода, когда существует несколько записей, которые ссылаются на одно и то же имя библиотеки.
Это состоит из:
LengthPrefixedString
))Как указано в 2.1.1.6 LengthPrefixedString
…
LengthPrefixedString представляет строковое значение. Строка начинается с длины строки в кодировке UTF-8 в байтах. Длина кодируется в поле переменной длины с минимальным 1 байтом и максимальным 5 байтами. Чтобы минимизировать размер провода, длина кодируется как поле переменной длины.
В нашем простом примере длина всегда кодируется с использованием 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
состояния:
Запись ClassWithMembersAndTypes является наиболее подробной из записей класса. Он содержит метаданные об Участниках, включая имена и Типы Удаленного взаимодействия Участников. Он также содержит идентификатор библиотеки, который ссылается на имя библиотеки класса.
Это состоит из:
ClassInfo:
Как указано в 2.3.1.1 ClassInfo
запись состоит из:
LengthPrefixedString
))LengthPrefixedString
где количество элементов ДОЛЖНО быть равно значению, указанному в MemberCount
поле.)Вернуться к необработанным данным, шаг за шагом:
01 00 00 00
представляет ObjectId
, Мы уже видели этот, он был указан как RootId
в SerializationHeaderRecord
,
0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41
представляет Name
класса, который представлен с помощью LengthPrefixedString
, Как уже упоминалось, в нашем примере длина строки определяется 1 байтом, поэтому первый байт 0F
указывает, что 15 байтов должны быть прочитаны и декодированы с использованием UTF-8. Результат выглядит примерно так: StackOverFlow.A
— так очевидно, что я использовал StackOverFlow
как имя пространства имен.
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
представляет первый MemberName
, 1B
снова длина строки, которая составляет 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
представляет второй MemberName
, 1A
указывает, что длина строки составляет 26 байтов. В результате получается что-то вроде этого: <SomeValue>k__BackingField
,
MemberTypeInfo:
После ClassInfo
MemberTypeInfo
следующим образом.
Раздел 2.3.1.2 - MemberTypeInfo
утверждает, что структура содержит:
Последовательность значений BinaryTypeEnumeration, представляющая типы элементов, которые передаются. Массив ДОЛЖЕН:
Содержать то же количество элементов, что и поле MemberNames структуры ClassInfo.
Упорядочить так, чтобы BinaryTypeEnumeration соответствовал имени члена в поле MemberNames структуры ClassInfo.
BinaryTpeEnum
дополнительная информация может или не может присутствовать.
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
Итак, учитывая это, мы почти на месте …
Мы ожидаем 2 BinaryTypeEnumeration
значения (потому что у нас было 2 члена в MemberNames
).
Опять же, вернемся к необработанным данным полного MemberTypeInfo
запись:
01
представляет BinaryTypeEnumeration
первого члена, согласно 2.1.2.2 BinaryTypeEnumeration
мы можем ожидать String
и он представлен с использованием LengthPrefixedString
,
00
представляет BinaryTypeEnumeration
второго члена, и опять же, согласно спецификации, это Primitive
, Как указано выше, Primitive
сопровождаются дополнительной информацией, в данном случае PrimitiveTypeEnumeration
, Вот почему нам нужно прочитать следующий байт, который 08
сопоставьте его с таблицей, указанной в 2.1.2.3 PrimitiveTypeEnumeration
и с удивлением заметил, что мы можем ожидать Int32
который представлен 4 байтами, как указано в каком-то другом документе об основных типах данных.
LibraryId:
После MemerTypeInfo
LibraryId
следует, это представлено 4 байтами:
02 00 00 00
представляет LibraryId
который 2.
Ценности:
Как указано в 2.3 Class Records
:
Значения членов класса ДОЛЖНЫ быть сериализованы как записи, которые следуют за этой записью, как указано в разделе 2.7. Порядок записей ДОЛЖЕН соответствовать порядку MemberNames, указанному в структуре ClassInfo (раздел 2.3.1.1).
Вот почему мы можем теперь ожидать значения членов.
Давайте посмотрим на последние несколько байтов:
06
идентифицирует BinaryObjectString
, Это представляет ценность нашего SomeString
собственность ( <SomeString>k__BackingField
если быть точным).
В соответствии с 2.5.7 BinaryObjectString
это содержит:
LengthPrefixedString
)Итак, зная это, мы можем четко определить, что
03 00 00 00
представляет ObjectId
,
03 61 62 63
представляет Value
где 03
длина самой строки и 61 62 63
байты содержимого, которые переводятся в abc
,
Надеюсь, вы можете вспомнить, что был второй член, Int32
, Зная, что Int32
представлен 4 байтами, мы можем заключить, что
должен быть Value
нашего второго члена. 7B
шестнадцатеричное равно 123
десятичный, который, кажется, соответствует нашему примеру кода.
Итак, вот полный ClassWithMembersAndTypes
запись:
MessageEnd:
Наконец последний байт 0B
представляет MessageEnd
запись.
Других решений пока нет …