536 lines
24 KiB
Markdown
536 lines
24 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 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
|