Данные в справочниках хранятся в «универсальном формате упаковки данных», каждая запись идет последовательно, без разделителей. Сама запись имеет переменный размер, и состоит как из бинарных, так и из строковых данных. Вот тут разработчиками был подложен второй поросеночек — прочитать записи переменной длины и загрузить их в удобный DataSet
без бубна нельзя. Третий поросеночек, к сожалению, никак не отраженный в спецификации, был в том, что числовые данные в «универсальном формате» на самом деле в little-endian! Хотя в спецификации было указано, что данные хранятся в big-endian, и все работало до того момента, когда я не попытался прочитать данные из справочников.
Формат записей справочника описывается строкой вида:
T:id/c2:iso/n2:lat/n2:lon/b:name_ru/b:name_en
Формат данной строки таков:
код_типа_данных:имя_поля/
/
— разделитель описаний полей.
PHP-ребятам повезло, не только в том, что интерпретатор PHP весьма вольно обращается с типами данных, но и в том, что у них есть стандартные функции pack()/unpack()
, которые фактически выполняют бинарную сериализацию/десериализацию, т.е. преобразуют массив байтов в ассоциативный массив, согласно строке формата и наоборот.
Распаковщик формата реализован в отдельном классе SxGeoUnpack
.
В первую очередь, надо привести типы «универсального формата» к типам C#. Я свел эти данные в таблицу, вместе с описанием типов из спецификации.
Код типа | Тип | Размер | Описание | Тип C# |
t | tinyint signed | 1 | Целое число от -128 до 127 | sbyte |
T | tinyint unsigned | 1 | Целое число от 0 до 255 | byte |
s | smallint signed | 2 | Целое число от -32 768 до 32 767 | short |
S | smallint unsigned | 2 | Целое число от 0 до 65 535 | ushort |
m | mediumint signed | 3 | Целое число от -8 388 608 до 8 388 607 | int* |
M | mediumint unsigned | 3 | Целое число от 0 до 16 777 215 | uint* |
i | integer signed | 4 | Целое число от -2 147 483 648 до 2 147 483 647 | int |
I | integer unsigned | 4 | Целое число от 0 до 4 294 967 295 | uint |
f | float | 4 | 4-байтовое число одиночной точности с плавающей запятой | float |
d | double | 8 | 8-байтовое число двойной точности с плавающей запятой | double |
n# | number 16bit | 2 | Число (2 байта) с фиксированным количеством знаков после запятой. После n указывается количество цифр после запятой | float |
N# | number 32bit | 4 | Число (4 байта) с фиксированным количеством знаков после запятой. После N указывается количество цифр после запятой | float |
c# | char | — | Строка фиксированного размера. После с указывается количество символов | string** |
b | blob | — | Строка завершающаяся нулевым символов | string |
* Трехбайтовых чисел в C# нет, поэтому будем преобразовывать их к четырехбайтовым int
или uint
.
** Размер символа принят == 1 байту, поэтому неанглоязычные символы UTF-8 или многобайтные кодировки не поддерживаются (кто хочет, может прислать решение по вычислению длины символа, я добавлю).
Для приведения типов в классе SxGeoUnpack
создана соответствующая функция:
private static Type SxTypeToType(string SxTypeCode) { if (string.IsNullOrEmpty(SxTypeCode)) return null; //mediumint - такого типа в C# нет, приведем к int/uint switch (SxTypeCode[0]) { case 't': return typeof(sbyte); //tinyint signed - 1 - > sbyte case 'T': return typeof(byte); //tinyint unsigned - 1 - > byte case 's': return typeof(short); //smallint signed - 2 - > short case 'S': return typeof(ushort); //smallint unsigned - 2 - > ushort case 'm': return typeof(int); //mediumint signed - 3 - > int case 'M': return typeof(uint); //mediumint unsigned - 3 - > uint case 'i': return typeof(int); //integer signed - 4 - > int case 'I': return typeof(uint); //integer unsigned - 4 - > uint case 'f': return typeof(float); //float - 4 - > float case 'd': return typeof(double); //double - 8 - > double case 'n': //number 16bit - 2 case 'N': return typeof(double); //number 32bit - 4 - > float case 'c': //char - fixed size string case 'b': return typeof(string); //blob - string with \0 end } return null; }
Принцип работы прост. Нам необходимо на входе получить массив байт, содержащих запись, строку формата для ее расшифровки, кодировку строк и формат записи числовых данных — big- или little-endian. На выходе выдать запись в формате, с которым будет удобно далее работать и информацию о типах данных записи.
Соответственно, были созданы соответствующие переменные под структуры данных:
private Dictionary
private Dictionary
private Dictionary
private Encoding StringsEncoding = null;
public bool RevBO { get; set; }
Думаю, из названий переменных все понятно, первые 3 ассоциативных массива (словаря) хранят данные о записи, далее следует переменная, хранящая кодировку, а RevBO
— флаг, сообщающий, надо ли изменять порядок байт при получении числовых данных. Вообще не надо, но технологическое отверстие оставил, на всякий случай, чтоб если что весь класс не переписывать.
Инициализация класса. При создании класса запрашиваем 2 параметра — кодировка строк и строка, описывающая формат записи, которую необходимо распарсить.
Далее:
1. Инициализируем словари
2. Разбираем строку формата
3. Подготавливаем словари и заполняем те, которые можем на этапе инициализации
4. Устанавливаем кодировку строк
Конструктор класса:
public SxGeoUnpack(string Format, SxGeoEncoding DBEncoding) { RevBO = !BitConverter.IsLittleEndian; RecordData = new Dictionary(); RecordTypes = new Dictionary (); SxTypeCodes = new Dictionary (); //разбираем строку формата string[] fields = Format.Split('/'); foreach (string field in fields) { string[] buf = field.Split(':'); if (buf.Length < 2) break; string SxTypeCode = buf[0]; string FieldName = buf[1]; //подгатавливаем Dictionary'и RecordData.Add(FieldName, null); SxTypeCodes.Add(FieldName, SxTypeCode); RecordTypes.Add(FieldName,SxTypeToType(SxTypeCode)); } switch (DBEncoding) { case SxGeoEncoding.CP1251: StringsEncoding = Encoding.GetEncoding(1251); break; case SxGeoEncoding.UTF8: StringsEncoding = Encoding.UTF8; break; case SxGeoEncoding.Latin1: StringsEncoding = Encoding.GetEncoding(1252); break; } }
Для "распаковки" записи создана публичная функция Unpack()
:
public Dictionary
Функция возвращает ассоциативный массив
object
'ов, представляющий собой набор элементов конкретной записи. Запись передается функции в виде массива байт. Сам массив может быть большего размера, чем запись. Функция, опираясь на строку описания формата, считает, что ей передана вся запись сначала, лишние байты игнорируются.
Функция также подсчитывает, в ходе "распаковки", настоящую длину записи и сохраняет ее в переменную RealLength
.
Внутри функция устроена просто. С помощью оператора switch/case
перебираем коды типов (они на этапе инициализации добавлены в словарь SxTypeCodes
) и в зависимости от типа данных, передаем нужное количество байт специфической функции, которая будет далее анализировать соответствующий кусочек записи. Также функция подсчитывает размер записи (сама, в зависимости от типа данных, или руководствуясь полученными от функций анализа сведениями).
Чтоб не загромождать текст, вынесу код функции на PasteBin
Заодно делаем функцию-обертку, которую можно использовать если длина записи не нужна:
public DictionaryUnpack(byte[] Record) { int tmp = 0; Unpack(Record, out tmp); return RecordData; }
Тут я наделал много мелких суетливых движений, но мне показалось, что так лучше. Типы прямо приводимые к (u)int
, (u)short, (s)byte, float
и double
особо ничего не потребовали и обрабатывались стандартно - массив байт переворачивался, если флаг RevBO
был установлен в true
, т.е. необходимо было изменить последовательность байт, и массив конвертировался BitConverter
'ом в соответствующее число.
Правда насчет float
и double
я сомневаюсь, но в бесплатных БД такие поля не встречаются, а других баз для отладки у меня не было.
Пример функции преобразования:
private int GetIntSigned(byte[] DataArray, int StartPosition) { if (StartPosition >= DataArray.Length + 3) { return 0; } byte[] buf = new byte[4]; Array.Copy(DataArray, StartPosition, buf, 0, 4); if (RevBO) { Array.Reverse(buf); } return BitConverter.ToInt32(buf, 0); }
Но на некоторых типах данных стоит остановиться поподробнее.
- char
(строка фиксированного размера). Читаем указанное количество байт, преобразуем в строку:
private string GetFixedString(byte[] DataArray, int StartPosition, int Count) { if (StartPosition >= DataArray.Length + Count - 1) { return null; } //кириллица UTF8 для строк ограниченной длины не поддерживается //делаем буфер byte[] buf = new byte[Count]; //копируем нужное количество байт в буфер Array.Copy(DataArray, StartPosition, buf, 0, Count); return StringsEncoding.GetString(buf); }
- blob
, строка нефиксированного размера, заканчивающаяся символом 0x00
.
Да, тут использовал довольно медленный List
, но строки не такие чтоб уж гигантские, а работать так проще.
private int GetBlob(byte[] DataArray, int StartPosition, out string Result) { int i = StartPosition; Listtmpl = new List (); while (DataArray[i] != '\0') { tmpl.Add(DataArray[i]); i++; } i++; byte[] buf = tmpl.ToArray(); Result = StringsEncoding.GetString(buf); return i; }
- числа с фиксированной запятой. В оригинальном исходнике на PHP был элегантный алгоритм, где целое число делилось на 10 в степени количество_знаков_после_запятой
(обозначим за x
), т.е. число получалось по формуле N=n/10x
.
где N
- результат
n
- исходное целое число
x
- количество знаков после запятой.
private double GetN32(byte[] DataArray, int StartPosition, int Signs) { int tmpInt = GetIntSigned(DataArray, StartPosition); return tmpInt / Math.Pow(10, Signs); }
Все функции преобразования в одном месте
Тут три функции, две облегчающие доступ к данным, и возвращающие ассоциативный массив типов для записи и ассоциативный массив кодов по спецификации:
public DictionaryGetRecordTypes() { return RecordTypes; } public Dictionary GetSxTypeCodes() { return SxTypeCodes; }
И статическая функция, анализирующая строку формата и возвращающая ассошиативный массив вида - имя_поля - тип C#.
public static DictionaryGetRecordTypes(string Format) { Dictionary tmpTypes = new Dictionary (); string[] fields = Format.Split('/'); foreach (string field in fields) { string[] buf = field.Split(':'); if (buf.Length < 2) break; string SxTypeCode = buf[0]; string FieldName = buf[1]; //формируем Dictionary tmpTypes.Add(FieldName, SxTypeToType(SxTypeCode)); } return tmpTypes; }