Files
ARSV/Docs/set:version_ru.md
2026-07-21 16:21:20 +03:00

23 KiB
Raw Permalink Blame History

set:version

what is happening inside set:version

Актуальный код с наработками расположен в репозитории

Зачем

set:version в alt-rpm позволяет сопоставлять Provides и Requires пакетов не обычным сравнением версий, а сравнением специальных set-строк зависимостей вида

libfoo.so.X = set:<encoded-set>

encoded-set формируется на основе символов, необходимых/предоставляемых пакетом. Данный механизм позволяет гарантировать (с точностью до коллизий хэша, об этом будет далее) наличие всех требуемых символов в библиотеке. Это исключает ситуации, при которых ">= версий" ломается при удалении символа из библиотеки, а также ситуаций совпадения SONAME библиотек с разным набором символов.

set-строки (являющиеся перекодированным списком символов) генерируются способом, который позволяет их сравнивать между собой на предмет включения одного множества символов в другой.

Реализация set.c

set.c предоставляет 5 "публичных" API для работы set:version

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()

Основная функция, сравнивающая строки и выдающая результат в зависимости от включения:

  • 1: set1 > set2
  • 0: set1 == set2
  • -1: set1 < set2
  • -2: set1 != set2
  • -3: set1 decoder error
  • -4: set2 decoder error

на основе данной заметки, set1 лучше делать как Provides для лучшей производительности.

set_new()

Создаёт пустой объект struct set - контейнер для строк символов и их будущих hash-значений. Из реализации:

internally struct set is just a bag of strings and their hash values.

set_add()

Добавляет строковый символ в set.

Использование:

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_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 младших бит

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 первыми двумя символами.

Возвращаемое значение encode_set() может использоваться как код ошибки при отрицательных значениях

delta

Внутри функции encode_delta() происходит преобразование массива отсортированных по возрастанию чисел в массив дельт между числами, т.е. Пример исходного массива:

unsigned *v = {1, 4, 16, 22};

Пример результирующего массива:

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

Сравнение 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 для обозначения особых случаев при считывании символов:

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[]

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

декодированное число восстанавливается как

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 бит остатка, записывает готовое значение

*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 используется простой и быстро вычисляемый

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, дубликаты значений удаляются.

Пример:

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.

+8 +8 +8 ...  // грубый поиск
-4            // откат
±2            // уточнение
±1            // уточнение
+1 возможно   // финальная коррекция

В итоге v1 оказывается на первом элементе, который не меньше v2val.

IFLT4 делает аналогичную работу, но с шагом в 4. IFLT8 выбирается в случае, когда массив v1 превосходит по размеру массив v2 более чем в 16 раз.

Макрос IFGE

После IFLT* вариант *v1 < v2val уже невозможен.

Значит остаётся:

*v1 > v2val

То есть текущий элемент v2val есть во втором множестве, но отсутствует в первом.

Следовательно v1 уже не может быть надмножеством v2:

ge = 0;
v2++;

SELF_TEST флаг

При выставленном SELF_TEST флаге происходит следующее:

  1. Явно отключается NDEBUG для работы assert()
  2. Компилируются test_* функции
  3. Компилируется main(), запускающая все test_* функции

test_base62()

Проверяет работу функций:

encode_base62()
decode_base62()

test_golomb()

Проверяет работу функций:

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(), сравнивая с эталонными:

decode_base62()
decode_golomb()

test_delta()

Проверяет работу функций:

encode_delta()
decode_delta()

test_set()

Проверяет полный encode/decode pipeline для множества чисел. Используемый bpp = 16.

Примечание о encode_set()

Внутри encode_set() есть строки:

#ifdef SELF_TEST
  decode_delta(c, v);
#endif

это необходимо, т.к. далее в функции test_set() сравниваются изначальный и "после pipeline" массивы.

test_api()

Проверяет публичный API:

set_new()
set_add()
set_fini()
rpmsetcmp()
set_free()