Files
ARSV/Docs/set:version.md
T
2026-07-05 20:56:08 +03:00

536 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()`
Под `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];
```
pre-compiled таблица соответствия любой комбинации из двух байт, битовому представлению из 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 бит для golumb-декодера
`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++;
```
## `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()`
В таблице `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