Толик Панков
hex_laden
............ .................. ................
November 2020
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30

Толик Панков [userpic]
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#
ttinyint signed1Целое число от -128 до 127sbyte
Ttinyint unsigned1Целое число от 0 до 255byte
ssmallint signed2Целое число от -32 768 до 32 767short
Ssmallint unsigned2Целое число от 0 до 65 535ushort
mmediumint signed3Целое число от -8 388 608 до 8 388 607int*
Mmediumint unsigned3Целое число от 0 до 16 777 215uint*
iinteger signed4Целое число от -2 147 483 648 до 2 147 483 647int
Iinteger unsigned4Целое число от 0 до 4 294 967 295uint


ffloat44-байтовое число одиночной точности с плавающей запятойfloat
ddouble88-байтовое число двойной точности с плавающей запятойdouble
n#number 16bit2Число (2 байта) с фиксированным количеством знаков после запятой. После n указывается количество цифр после запятойfloat
N#number 32bit4Число (4 байта) с фиксированным количеством знаков после запятой. После N указывается количество цифр после запятойfloat
c#char-Строка фиксированного размера. После с указывается количество символовstring**
bblob-Строка завершающаяся нулевым символов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<string, object="object"> RecordData = null;
private Dictionary<string, type="Type"> RecordTypes = null;
private Dictionary<string, string="string"> 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<string,object>();
    RecordTypes = new Dictionary<string,type>();
    SxTypeCodes = new Dictionary<string,string>();

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

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

Заодно делаем функцию-обертку, которую можно использовать если длина записи не нужна:
public Dictionary<string, object="object"> 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<string, type="Type"> GetRecordTypes()
{
    return RecordTypes;
}
public Dictionary<string, string="string"> GetSxTypeCodes()
{
    return SxTypeCodes;
}


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

public static Dictionary<string, type="Type"> GetRecordTypes(string Format)
{
    Dictionary<string, type="Type"> tmpTypes = new Dictionary<string, type="Type">();
    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

Это репост с сайта http://tolik-punkoff.com
Оригинал: http://tolik-punkoff.com/2018/10/02/sxgeosharp-interfejs-na-c-dlya-bazy-dannyh-sypexgeo-chast-iii-universalnyj-format-upakovki-dannyh-i-poluchenie-dannyh-iz-spravochnikov/