some structure refactor

This commit is contained in:
2026-07-20 21:58:04 +03:00
parent fc0730c751
commit 1172683d9a
26 changed files with 493 additions and 38 deletions
+535
View File
@@ -0,0 +1,535 @@
# 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, 6};
```
### 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