# set:version what is happening inside [set:version](https://git.altlinux.org/gears/r/rpm.git?a=blob;f=lib/set.c) Актуальный код с наработками расположен в [репозитории](https://github.com/kr0sh512/alt-rpm-set-version.git) ## Зачем set:version в alt-rpm позволяет сопоставлять Provides и Requires пакетов не обычным сравнением версий, а сравнением специальных set-строк зависимостей вида ```text libfoo.so.X = set: ``` `encoded-set` формируется на основе символов, необходимых/предоставляемых пакетом. Данный механизм позволяет гарантировать (с точностью до коллизий хэша, об этом будет далее) наличие всех требуемых символов в библиотеке. Это исключает ситуации, при которых ">= версий" ломается при удалении символа из библиотеки, а также ситуаций совпадения SONAME библиотек с разным набором символов. set-строки (являющиеся перекодированным списком символов) генерируются способом, который позволяет их сравнивать между собой на предмет включения одного множества символов в другой. ## Реализация `set.c` `set.c` предоставляет 5 "публичных" API для работы set:version ```c int rpmsetcmp(const char *set1, const char *set2); struct set *set_new(void); void set_add(struct set *set, const char *sym); const char *set_fini(struct set *set, int bpp); struct set *set_free(struct set *set); ``` ### `rpmsetcmp()` Основная функция, [сравнивающая строки](#сравнение-set-строк) и выдающая результат в зависимости от включения: - 1: set1 > set2 - 0: set1 == set2 - -1: set1 < set2 - -2: set1 != set2 - -3: set1 decoder error - -4: set2 decoder error на основе [данной](https://github.com/svpv/rpmss/blob/4256d86cc9ba1aa4ceb8c0f03f7d48675d9d27bb/set.h#L12) заметки, `set1` лучше делать как `Provides` для лучшей производительности. ### `set_new()` Создаёт пустой объект `struct set` — контейнер для строк символов и их будущих hash-значений. Из реализации: > internally struct set is just a bag of strings and their hash values. ### `set_add()` Добавляет строковый символ в set. Использование: ```c set_add(s, "printf@@GLIBC_2.2.5"); set_add(s, "malloc@@GLIBC_2.2.5"); ``` ### `set_fini()` Финализирует множество и возвращает готовую set-version строку. В качестве параметров принимает `struct set *set` и `int bpp` При `bpp` < 10 или `bpp` > 32 функция возвращает `NULL`. Работа функции [описана далее](#работа-внутри-set_fini). ### `set_free()` Освобождает struct set и его внутренние строки. !!! NOTE В данный момент не освобождается сама структура `struct set`, память под которую в `set_new()` выделяется с `xmalloc` ## Работа внутри `set_fini()` Основной процесс преобразования массива строк в set-строку происходит следующим образом: ``` массив строк | (Jenkins OAAT) v массив хэшей | (qsort) v отсортированный массив хэшей | (вычисление разницы между элементами) v массив delta | (Rice-Golomb преобразование) v битовый массив | (base62 преобразование) v set-строка ``` ### hash Для каждого элемента (строки) высчитывается 32-битный хэш и обрезается до `bpp` младших бит ```c unsigned mask = (1u << bpp) - 1; set->sv[i].v = hash(set->sv[i].s) & mask; ``` В качестве хэш-функции используется `Jenkins OAAT`. Получившийся массив сортируется по хэш-значениям по возрастанию. При коллизии печатается `warning: hash collision` в `stderr` С помощью функции `uniqv()` в массиве остаются только уникальные значения, возвращаемое значение - размер массива после удаления повторов. Все дальнейшие преобразования происходят внутри функции `encode_set()`. Возвращаемое значение - длина итоговой set-строки, включая bpp и Mshift первыми двумя символами. !!! NOTE Возвращаемое значение `encode_set()` может использоваться как код ошибки при отрицательных значениях ### delta Внутри функции `encode_delta()` происходит преобразование массива отсортированных по возрастанию чисел в массив дельт между числами, т.е. Пример исходного массива: ```c unsigned *v = {1, 4, 16, 22}; ``` Пример результирующего массива: ```c unsigned *v = {1, 3, 12, 8}; ``` ### Rice-Golomb > `encode_golomb()` - main golomb encoding routine: package integers into bits. Для сокращения длины итоговой строки применяется Rice-Golomb кодирование, которое позволяет с параметром `Mshift` записать число `n` как `n >> Mshift`, записанный последовательностью нулей, и `n & 2^Mshift`, записанный стандартным двоичным кодированием. Целая часть и остаток разделяются единичным битом. `Mshift` выбирается как `bpp - log2(c) - 1`, т.к. при равномерном распределении `c` хэшей по диапазону `2^bpp`, средняя `delta` будет равна `2^bpp / c` Пример: ``` Mshift = 8 n = 553 q = 553 >> 8 = 2 r = 553 & 255 = 42 bits = 00 1 00101010 ``` Функция `encode_golomb()` возвращает длину итоговой битовой последовательности. Сама последовательность находится в `char *bitv` (`char = [1,0]`) ### base62 > `encode_base62()` - pack bitv into base62 string Последним шагом является преобразование битовой последовательности в `base62` строку Алфавитом для кодирования является `0-9,a-z,A-Z`, но символ `Z` кодирует сразу 61, 62 и 63 следующим образом: 1. в битовую последовательность после кодирования `Z` добавляется 2 бита: - `00` для 61 - `01` для 62 - `10` для 63 2. битовая последовательность продолжает кодироваться стандартным образом Заметим, что данным образом невозможно получить последовательность `ZZ`, т.к. символ `Z` требует двух старших бит, выставленных в `11` Пример преобразования: ``` Mshift = 5 bits = 1 01111 00 1 00010 ``` Или же ``` bits = 101111 001000 10 ``` Биты читаются младшим битом вперёд. Первые 6 бит: ``` 101111 = 1*1 + 0*2 + 1*4 + 1*8 + 1*16 + 1*32 = 61 = Z ``` Тогда в последовательность добавляется два бита `00` сразу после битов, кодирующих Z. ``` bits = 101111 000010 0010 000010 = 16 = g 0010(00) = 4 = 4 ``` Итоговая строка в `base62`: ``` 10111100100010 = Zg4 ``` ## Сравнение set-строк При сравнении set-строк происходит обратный процесс преобразования до получения хэш-значений ``` set-строка | (обратное base62 преобразование) v битовый массив | (обратное Rice-Golomb преобразование) v массив delta | (вычисление изначальных значений) v массив хэшей ``` После массивы хэшей сравниваются между собой на наличие элементов одного массива в другом. ## Магия внутри `rpmsetcmp()` 1. У строки обрезается префикс `set:`, если он присутствует, декодируется значение `bpp` и `Mshift` с помощью `decode_set_init()` 2. `set1` декодируется с помощью `cache_decode_set()` 3. `set2` декодируется с помощью `decode_set()` 4. для каждого массива хэшей (`v1` и `v2`) создаётся два буферных массива `v(1|2)buf(A|B)` для функции `downsample_set()` 5. С помощью функции `downsample_set()` `bpp` обоих хэшей выравнивается до минимального из `bpp1` и `bpp2`. 6. Создаётся два флага `ge` и `le`, для определения вложенности множеств 7. Вложенность множеств проверяется с помощью трёх макросов: `IFGE` `IFLT4` `IFLT8` 8. Возвращается значение в зависимости от вложенности: - 1: set1 > set2 - 0: set1 == set2 - -1: set1 < set2 - -2: set1 != set2 ### `decode_set()` Под `if(0)` в функции `decode_set()` описано интуитивное преобразование с помощью функций `decode_base62()`, `decode_golomb()` и `decode_delta()`. На деле же используется оптимизированный вариант из `decode_base62_golomb()` и `decode_delta()`, благодаря которым set-строка сразу преобразуется в массив `delta` значений и после восстанавливается до массива хэшей. ### `decode_base62_golomb()` `decode_base62_golomb()` - оптимизированная версия стадий `decode_base62` и `decode_golomb`. Функция считывает сразу по два байта, с помощью таблицы `word_to_num` преобразует их в битовую последовательность, затем, набирая до 24 бит, декодирует по Rice-Golomb. #### `enum` > `enum` - word types (when two bytes from base62 string cast to unsigned short). В коде используется `enum` для обозначения особых случаев при считывании символов: ```c enum { W_AA = 0x0000, // два обычных символа (явно не используется) W_AZ = 0x1000, // обычный символ + Z W_ZA = 0x2000, // Z + обычный символ W_A0 = 0x3000, // обычный символ + конец строки W_0X = 0x4000, // конец строки W_EE = 0xeeee, // невозможная ситуация }; ``` #### `CCI` macros `CCI` - макрос, объединяющий два символа в индекс таблицы `word_to_num`, с учётом порядка байтов #### `word_to_num[]` ```c static const unsigned short word_to_num[65536]; ``` Предварительно скомпилированная таблица соответствия любой комбинации из двух байт с битовым представлением из 12 бит + возможный флаг Таблица строится следующим образом: 1. все значения заполняются `W_EE` как ошибочные 2. с помощью макроса `AA1` строятся макросы `AA1x2`, `AA1x25` и т.д. вплоть до `AA10x10` и ему аналогичных. - итоговые макросы позволяют быстро заполнить таблицу значениями `[CCI(c1, c2)] = (c1 - b1) | ((c2 - b2) << 6)` 3. аналогичный процесс заполнения для `AZ`, но с добавлением флага `W_AZ` к значениям в таблице. 4. заполняется таблица для `ZA` значений с флагом `W_ZA`. Недопустимость старших бит `11` во втором символе (т.е. символы со значения 48) проверяется лишь внутри функций, `W_EE` для таких значений не возвращается 5. заполняется таблица для `A0` и `0X` значений с флагами `W_A0` и `W_0X` соответственно. #### Считывание Функция считывает с помощью макросов `GetXX` несколько бит (стандартно блоками до 24 бит) и передаёт их на декодирование при 24 битах: - берутся именно 4 base62-символа (две пары) - значение помещается в 32-битный `unsigned` - при ограничении `Mshift >= 7` в одну последовательность может поместиться максимум 3 закодированных Golomb-числа. "12 бит" считыватель используется в ситуациях, когда первая пара обычная, а вторая содержит специальный случай "10 бит" считыватель используется в ситуациях с Z-escape "6 бит" считыватель используется для одного символа. #### Rice-Golomb декодер Декодер постоянно находится в двух состояниях: - `q-state`: ищет unary-префикс и разделительную `1` - `r-state`: добирает Mshift бит остатка `r` декодированное число восстанавливается как ```c value = (q << Mshift) | r; ``` #### Обработка Z-escape Для обработки Z-escape используются макросы `Esc1` и `Esc2`. `Esc1` вызывается при обнаружении символа `Z` и считывает старшие 2 бита у следующего символа. В результате `Esc1` получает 10 бит для Golomb-декодера `Esc2` вызывается после завершения escape-пары и решает, что делать дальше: - обычный символ — обработать 6 бит - конец строки — завершить декодирование - недопустимый символ — вернуть ошибку - новый `Z` — снова перейти в `Esc1` #### `QMake` и `RMake` макросы `QMake` ищет разделительную единицу в текущем блоке бит. Если блок содержит только нули, все они добавляются к `q`, после чего функция читает следующий блок. Если в блоке есть единица, используется `__builtin_ffs(bits)` для определения позиции первого бита, разделительной для Golomb-кода. Оставшиеся биты заполняют `r`. `RMake` проверяет, набралось ли `Mshift` бит остатка, записывает готовое значение ```c *v++ = (q << Mshift) | r; ``` #### Завершение строки Допустимо завершение строки в `q-state`, но с не более чем 5 нулями. Завершение в `r-state` невозможно, т.к. остаток длиной `Mshift` не набрался. ### `cache_decode_set()` > `cache_decode_set()` - special decode_set version with LRU caching. Возвращает количество элементов хэша в массиве, помещает указатель `*pv` на массив хэшей. Внутри `cache_decode_set()` создаются статические массивы: - `static unsigned hv[CACHE_SIZE]` - для хранения fingerprint set-строки - `static struct cache_ent *ev[CACHE_SIZE]` - для хранения полной информации о декодированной строке В качестве fingerprint используется простой и быстро вычисляемый ```c unsigned hash = str[0] | (str[2] << 8) | (str[3] << 16); ``` массивы `hv` и `ev` представляют собой аналог LRU-кэша размером `CACHE_SIZE=256`. Если fingerprint set-строки на декодирование совпал с имеющимся в `hv` массиве, проверяется полное соответствие строки с `ev[i]->str`. В случае hit - элемент помещается на нулевую позицию, остальные элементы сдвигаются. При неудачной проверке полной set-строки, поиск по кэшу продолжается. В случае, если элемента не оказалось в кэше, строка декодируется функцией `decode_set()`, добавляется `SENTINELS=8` бит и помещается в кэш следующим образом: - Если кэш не заполнен, декодируемая строка записывается на первую свободную позицию - Если кэш заполнен, элемент ставится на `PIVOT_SIZE=243` позицию, элементы с этой позиции сдвигаются. #### `SENTINELS` значения Данные `~0u` значения необходимы при дальнейшем проходе по массиву внутри функции `rpmsetcmp()`, т.к. там происходят прыжки по 4 и 8 элементов. ### `downsample_set()` > `downsample_set()` - reduce a set of (bpp + 1) values to a set of bpp values Входной массив для работы - `v`. Итоговый массив будет доступен по указателю `w`. Возвращаемое значение - количество элементов в новом массиве `w`. Т.к. первоначальный входной массив отсортирован, после обрезания до `bpp` бит массив будет поделён на две части, обе из которых будут отсортированы. Деление массива будет происходить в месте, где старший бит на позиции `bpp+1` становится равным `1`. Далее обе половины массива объединяются в буфере `w`, дубликаты значений удаляются. Пример: ```text v = [1, 3, 6, 8, 10, 14] bpp = 3 mask = 7 v_mask = [1, 3, 6, 0, 2, 6] w = [0, 1, 2, 3, 6] return value = 5 ``` ### Макросы `IFGE` `IFLT4` `IFLT8` #### Макросы `IFLT*` `IFLT8` быстро продвигает `v1`, пока `*v1 < v2val`. Сперва макрос "грубо" прыгает по 8 элементов, далее уточняет с шагом 4, 2, 1. ```text +8 +8 +8 ... // грубый поиск -4 // откат ±2 // уточнение ±1 // уточнение +1 возможно // финальная коррекция ``` В итоге `v1` оказывается на первом элементе, который не меньше `v2val`. `IFLT4` делает аналогичную работу, но с шагом в `4`. `IFLT8` выбирается в случае, когда массив `v1` превосходит по размеру массив `v2` более чем в `16` раз. #### Макрос `IFGE` После `IFLT*` вариант `*v1 < v2val` уже невозможен. Значит остаётся: ```c *v1 > v2val ``` То есть текущий элемент `v2val` есть во втором множестве, но отсутствует в первом. Следовательно `v1` уже не может быть надмножеством `v2`: ```c ge = 0; v2++; ``` ## `SELF_TEST` флаг При выставленном `SELF_TEST` флаге происходит следующее: 1. Явно отключается `NDEBUG` для работы `assert()` 2. Компилируются `test_*` функции 3. Компилируется `main()`, запускающая все `test_*` функции ### `test_base62()` Проверяет работу функций: ```c encode_base62() decode_base62() ``` ### `test_golomb()` Проверяет работу функций: ```c encode_golomb() decode_golomb() ``` ### `test_word_table()` В таблице `word_to_num[65536]`, необходимой для функции `decode_base62_golomb()`, проверяет, чтобы последовательность `AA` (двух не escape-символов) была равна `(char_to_num[i] | (char_to_num[j] << 6)`. Для всех остальных ситуаций проверяется лишь значение одного из символов большее 61 `char_to_num[i] >= 61 || char_to_num[j] >= 61`. ### `test_base62_golomb()` Проверяет оптимизированный комбинированный декодер `decode_base62_golomb()`, сравнивая с эталонными: ```c decode_base62() decode_golomb() ``` ### `test_delta()` Проверяет работу функций: ```c encode_delta() decode_delta() ``` ### `test_set()` Проверяет полный encode/decode pipeline для множества чисел. Используемый `bpp = 16`. #### Примечание о `encode_set()` Внутри `encode_set()` есть строки: ```c #ifdef SELF_TEST decode_delta(c, v); #endif ``` это необходимо, т.к. далее в функции `test_set()` сравниваются изначальный и "после pipeline" массивы. ### `test_api()` Проверяет публичный API: ```c set_new() set_add() set_fini() rpmsetcmp() set_free() ``` ## Комментарии ## additional [коды](https://altlinux.space/arseny/atsv-research) от Арсения для наглядности происходящего some funny [msg's](https://lists.pld-linux.org/mailman/pipermail/pld-devel-en/2013-November/012467.html) по коду неоднократно раскидано `bpp < 10 || bpp > 32` проверки запись `char = [1,0]` мне не нравится. про "отрицательные значения = код ошибки" встречается в многих местах, стоит вынести отдельно проверить кодом примеры, особенно base62 кэш вечно копируется и переносится, немного странно PIVOT_SIZE странный прыжки IFLT8 и IFLT4 я бы возможно делал как c1/c2 учитывая оптимизации, возможно стоит самостоятельно менять массивы местами до начала всех операций (но проблема с кэшем возможна) хочу себе день, чтобы переписать это всё на English