SxGeoSharp. Интерфейс на C# для базы данных SypexGeo. — Часть III. Универсальный формат упаковки данных и получение данных из справочников.

Данные в справочниках хранятся в «универсальном формате упаковки данных», каждая запись идет последовательно, без разделителей. Сама запись имеет переменный размер, и состоит как из бинарных, так и из строковых данных. Вот тут разработчиками был подложен второй поросеночек — прочитать записи переменной длины и загрузить их в удобный 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 RecordData = null;
private Dictionary RecordTypes = null;
private Dictionary SxTypeCodes = null;

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 Unpack(byte[] Record, out int RealLength)
Функция возвращает ассоциативный массив object'ов, представляющий собой набор элементов конкретной записи. Запись передается функции в виде массива байт. Сам массив может быть большего размера, чем запись. Функция, опираясь на строку описания формата, считает, что ей передана вся запись сначала, лишние байты игнорируются.
Функция также подсчитывает, в ходе "распаковки", настоящую длину записи и сохраняет ее в переменную RealLength.
Внутри функция устроена просто. С помощью оператора switch/case перебираем коды типов (они на этапе инициализации добавлены в словарь SxTypeCodes) и в зависимости от типа данных, передаем нужное количество байт специфической функции, которая будет далее анализировать соответствующий кусочек записи. Также функция подсчитывает размер записи (сама, в зависимости от типа данных, или руководствуясь полученными от функций анализа сведениями).

Чтоб не загромождать текст, вынесу код функции на PasteBin

Заодно делаем функцию-обертку, которую можно использовать если длина записи не нужна:

public Dictionary Unpack(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;            
    List tmpl = 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 Dictionary GetRecordTypes()
{
    return RecordTypes;
}
public Dictionary GetSxTypeCodes()
{
    return SxTypeCodes;
}

И статическая функция, анализирующая строку формата и возвращающая ассошиативный массив вида - имя_поля - тип C#.

public static Dictionary GetRecordTypes(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;
}

Весь код класса на PasteBin

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *