429 lines
18 KiB
Markdown
429 lines
18 KiB
Markdown
# 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>
|
||
```
|
||
|
||
`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 OATT)
|
||
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. Вложенность множеств проверяется с помощью 3х макросов: `IFGE` `IFLT4` `IFLT8`
|
||
8. Возвращается значение в зависимости от вложенности:
|
||
- 1: set1 > set2
|
||
- 0: set1 == set2
|
||
- -1: set1 < set2
|
||
- -2: set1 != set2
|
||
|
||
### `decode_set()`
|
||
|
||
### `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++;
|
||
```
|
||
|
||
## `SEFT_TEST` флаг
|
||
|
||
При выставленном `SEFT_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()`
|
||
|
||
WIP
|
||
|
||
### `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
|
||
|
||
учитывая оптимизации, возможно стоит самостоятельно менять массивы местами до начала всех операций (но проблема с кэшем возможна)
|