From 61977e48dd0cc73d463bf1f4a9d882094d43fc2d Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 22 Mar 2026 08:30:50 +0000 Subject: [PATCH 1/4] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/netkeep80/BinDiffSynchronizer/issues/206 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..280e4aa --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-03-22T08:30:50.612Z for PR creation at branch issue-206-9d24f61cc2c2 for issue https://github.com/netkeep80/BinDiffSynchronizer/issues/206 \ No newline at end of file From 90a429364e38e2b5cd8c6868514cc9bbbde2c45c Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 22 Mar 2026 08:40:55 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=BA=D0=B0=20RFC=206901=20(JSON=20Pointer)=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=83=D1=82=D0=B5=D0=B9=20=E2=80=94?= =?UTF-8?q?=20escaping=20~/slash=20=D0=B2=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0?= =?UTF-8?q?=D1=85=20(=D0=AD=D1=82=D0=B0=D0=BF=2010.2,=20Issue=20#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена функция pjson_decode_rfc6901_segment() для декодирования сегментов пути по RFC 6901: ~1 → /, ~0 → ~. Декодирование применяется в _walk_path() и pjson_split_path(). Ключи объектов, содержащие '/' и '~', теперь доступны через path-адресацию с экранированием. Добавлены 17 тестов. Все 676 тестов проходят. Co-Authored-By: Claude Opus 4.6 --- pjson_db_helpers.h | 43 +++++- pjson_db_pmm.h | 2 +- plan.md | 20 +-- readme.md | 19 ++- tests/CMakeLists.txt | 1 + tests/test_pjson_rfc6901.cpp | 246 +++++++++++++++++++++++++++++++++++ 6 files changed, 310 insertions(+), 21 deletions(-) create mode 100644 tests/test_pjson_rfc6901.cpp diff --git a/pjson_db_helpers.h b/pjson_db_helpers.h index d315bc7..61d65de 100644 --- a/pjson_db_helpers.h +++ b/pjson_db_helpers.h @@ -14,7 +14,44 @@ // Вспомогательные функции: путевая адресация // =========================================================================== +/// Декодировать сегмент пути по RFC 6901 (JSON Pointer): +/// ~1 → / +/// ~0 → ~ +/// Порядок декодирования важен: сначала ~1, затем ~0. +inline std::string pjson_decode_rfc6901_segment( const char* data, uintptr_t len ) +{ + std::string result; + result.reserve( len ); + for ( uintptr_t i = 0; i < len; ++i ) + { + if ( data[i] == '~' && i + 1 < len ) + { + if ( data[i + 1] == '1' ) + { + result += '/'; + ++i; + continue; + } + else if ( data[i + 1] == '0' ) + { + result += '~'; + ++i; + continue; + } + } + result += data[i]; + } + return result; +} + +/// Декодировать сегмент пути по RFC 6901 (JSON Pointer) из std::string. +inline std::string pjson_decode_rfc6901_segment( const std::string& seg ) +{ + return pjson_decode_rfc6901_segment( seg.c_str(), static_cast( seg.size() ) ); +} + /// Разбить путь на родительский путь и последний сегмент. +/// Последний сегмент декодируется по RFC 6901. inline void pjson_split_path( const char* path, std::string& parent, std::string& last ) { if ( path == nullptr || path[0] == '\0' ) @@ -33,17 +70,17 @@ inline void pjson_split_path( const char* path, std::string& parent, std::string if ( pos == std::string::npos ) { parent = ""; - last = full; + last = pjson_decode_rfc6901_segment( full ); } else if ( pos == 0 ) { parent = "/"; - last = full.substr( 1 ); + last = pjson_decode_rfc6901_segment( full.substr( 1 ) ); } else { parent = full.substr( 0, pos ); - last = full.substr( pos + 1 ); + last = pjson_decode_rfc6901_segment( full.substr( pos + 1 ) ); } } diff --git a/pjson_db_pmm.h b/pjson_db_pmm.h index 21b20b9..5372228 100644 --- a/pjson_db_pmm.h +++ b/pjson_db_pmm.h @@ -627,7 +627,7 @@ class pjson_db_pmm if ( seg_len == 0 ) continue; - std::string seg( seg_start, seg_len ); + std::string seg = pjson_decode_rfc6901_segment( seg_start, seg_len ); // --- разыменование $ref --- if ( deref_refs ) diff --git a/plan.md b/plan.md index 0fe8768..85d16e1 100644 --- a/plan.md +++ b/plan.md @@ -21,7 +21,7 @@ | 7. Унификация итераторов | CRTP-база pjson_iterator_base + шаблонный pjson_range; ~22 строки удалено | ✅ | **Итого удалено:** ~5 файлов (~1900 строк), ~381 строка дублирования. -**Тесты:** 659 тестов, ~360 000 assertion. +**Тесты:** 676 тестов, ~360 000 assertion. --- @@ -70,18 +70,9 @@ --- -### Проблема 7: Отсутствие escaping '/' в path-сегментах +### ~~Проблема 7: Отсутствие escaping '/' в path-сегментах~~ ✅ -**Файл:** `pjson_db_pmm.h`, метод `_walk_path()` - -Символ `/` используется как разделитель пути, но не поддерживается экранирование. Если ключ объекта содержит `/`, обратиться к нему через path-адресацию невозможно: - -```cpp -db.put("/config", R"({"a/b": 42})"); -db.get("/config/a/b"); // ищет config → "a" → "b" вместо config → "a/b" -``` - -**Решение:** Поддержать RFC 6901 (JSON Pointer): символ `~` экранируется как `~0`, `/` как `~1`. +**Решено в Этапе 10.2:** Реализована поддержка RFC 6901 (JSON Pointer) для путей. Добавлена функция `pjson_decode_rfc6901_segment()` в `pjson_db_helpers.h`, которая декодирует `~1` → `/` и `~0` → `~`. Декодирование применяется в `_walk_path()` и `pjson_split_path()`. Ключи, содержащие `/` и `~`, теперь доступны через path-адресацию с экранированием. Добавлены 17 тестов. --- @@ -190,7 +181,7 @@ pvector был бы предпочтительнее **только** при ч | # | Проблема | Файл | Сложность | Влияние | |---|----------|------|-----------|---------| | ~~3~~ | ~~Глобальное состояние PMM (Этап A)~~ | ~~pam_pmm.h~~ | ~~Высокая~~ | ✅ | -| 7 | Нет escaping '/' в путях | pjson_db_pmm.h | Средняя | Совместимость | +| ~~7~~ | ~~Нет escaping '/' в путях~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ | ✅ | | 10 | Многократный resolve в is_*() | pjson_node.h | Средняя | Производительность | | 11 | const-корректность _walk_path | pjson_db_pmm.h | Средняя | Корректность | @@ -215,7 +206,7 @@ pvector был бы предпочтительнее **только** при ч Этап 10: Приоритет 3 — архитектурные улучшения 10.1 ✅ Инкапсуляция глобального состояния pam_pmm (Этап A: структура pam_pmm_state + синглтон) - 10.2 Поддержка RFC 6901 (JSON Pointer) для путей + 10.2 ✅ Поддержка RFC 6901 (JSON Pointer) для путей 10.3 Оптимизация tag-проверок на горячих путях 10.4 const-корректность с явной передачей состояния ``` @@ -226,6 +217,7 @@ pvector был бы предпочтительнее **только** при ч | Дата | Изменение | |------|-----------| +| 2026-03-22 | Этап 10.2: поддержка RFC 6901 (JSON Pointer) для путей — escaping ~/slash в ключах (Issue #206) | | 2026-03-22 | Этап 10.1: инкапсуляция глобального состояния pam_pmm в структуру pam_pmm_state (Issue #205) | | 2026-03-22 | Этап 9.4: parse_object() в один проход без двойного парсинга (Issue #192) | | 2026-03-22 | Этап 9.3: _free_node_tree и _resolve_refs_in_subtree через pjson_traverse_subtree с visitor-функторами (Issue #191) | diff --git a/readme.md b/readme.md index d28ba31..611ac37 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ int main() { | **Два типа строк** | readonly (`pstringview_pmm`): ключи объектов, пути `$ref`, интернированы, сравнение O(1); readwrite (`PamManager::pstring`): строковые значения JSON, изменяемые на лету | | **Нет SSO** | Ни `pstringview_pmm`, ни `PamManager::pstring` не используют SSO — все строки хранятся в ПАП (необходимо для сквозного поиска) | | **jsonRVM-совместимость** | `pstring`-узлы могут модифицироваться непосредственно в БД библиотекой [jsonRVM](https://github.com/netkeep80/jsonRVM); `node_id`-ссылки стабильны при resize array/object | -| **Path-адресация** | Доступ к узлам через строковые пути вида `/a/b/0/c` | +| **Path-адресация** | Доступ к узлам через строковые пути вида `/a/b/0/c`; RFC 6901 escaping (`~1` для `/`, `~0` для `~`) | | **$ref как указатели** | `{ "$ref": "/path" }` при разборе становится прямым указателем в ПАП | | **Метрики** | Персистная структура `db_metrics_pmm` в ПАМ; обновляется при каждой мутации; доступ через `/$metrics/...` | | **pmap-интерфейс** | `operator[]`, `find`, `insert` для доступа по пути без явного указания типа | @@ -139,7 +139,7 @@ int main() { | `pjson_db_pmm.h` | D | Менеджер персистной JSON-БД: path-адресация, `put`/`get`/`erase`, `$ref`, метрики, поиск, клонирование | | `deps/pmm/pmm.h` | A | [PersistMemoryManager](https://github.com/netkeep80/PersistMemoryManager) — бэкенд ПАП | | `main.cpp` | — | Демонстрационная программа | -| `tests/` | — | Тесты на Catch2 (659 тестов, ~360 000 assertion) | +| `tests/` | — | Тесты на Catch2 (676 тестов, ~360 000 assertion) | | `CMakeLists.txt` | — | Система сборки (CMake 3.16+, C++20) | --- @@ -195,6 +195,19 @@ node_view age = db.get("/users/alice/age"); // age.as_int() -> 30 ``` +### RFC 6901 — ключи с `/` и `~` + +```cpp +// Ключ "a/b" экранируется как "a~1b" в пути (RFC 6901 JSON Pointer) +db.put("/config/a~1b", 42); +node_view v = db.get("/config/a~1b"); +// v.as_int() -> 42; фактический ключ в объекте — "a/b" + +// Ключ "x~y" экранируется как "x~0y" +db.put("/data/x~0y", "hello"); +// фактический ключ — "x~y" +``` + ### Работа с `$ref` ```cpp @@ -439,7 +452,7 @@ db.put("/copy/name", "Bob"); ## Известные ограничения - **Глобальное состояние PMM** — в одном процессе может быть открыта только одна БД (см. [plan.md](plan.md), Проблема 3); состояние инкапсулировано в `pam_pmm_state`, передача как параметра — в будущих версиях -- **Нет escaping `/` в путях** — ключи объектов, содержащие `/`, недоступны через path-адресацию (см. [plan.md](plan.md), Проблема 7) +- ~~**Нет escaping `/` в путях**~~ — **Исправлено** в Этапе 10.2: поддержка RFC 6901 (JSON Pointer) — `~1` для `/`, `~0` для `~` в сегментах путей - ~~**Утечка временных узлов метрик**~~ — **Исправлено** в Этапе 8.4: один pre-allocated узел переиспользуется для всех вызовов метрик - **Не потокобезопасно** — CacheManagerConfig (по умолчанию) использует NoLock; для многопоточности нужен PersistentDataConfig - **Строки не освобождаются** — словарь `pstringview_pmm` только растёт diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c5fa94a..46706d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,7 @@ set(TEST_SOURCES test_regression_pmm_types.cpp test_compat_save_load.cpp test_pjson_ref_stability.cpp + test_pjson_rfc6901.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_pjson_rfc6901.cpp b/tests/test_pjson_rfc6901.cpp new file mode 100644 index 0000000..6d417f6 --- /dev/null +++ b/tests/test_pjson_rfc6901.cpp @@ -0,0 +1,246 @@ +// test_pjson_rfc6901.cpp — Тесты для поддержки RFC 6901 (JSON Pointer) в путях. +// +// Покрытие: +// - Декодирование сегментов: ~1 → /, ~0 → ~ +// - put/get с ключами, содержащими '/' и '~' +// - erase с RFC 6901 путями +// - exists с RFC 6901 путями +// - clone с RFC 6901 путями +// - pjson_split_path с RFC 6901 декодированием +// - parse_into с RFC 6901 путями +// + +#include +#include + +#include "pjson_db_pmm.h" + +using namespace pjson; + +// =========================================================================== +// Вспомогательные функции +// =========================================================================== + +static void reset_pmm() +{ + pstringview_manager::reset(); + pam_pmm_reset(); +} + +// =========================================================================== +// Декодирование сегментов: pjson_decode_rfc6901_segment +// =========================================================================== + +TEST_CASE( "rfc6901: decode segment — plain text unchanged", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "hello" ) == "hello" ); +} + +TEST_CASE( "rfc6901: decode segment — ~1 becomes /", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "a~1b" ) == "a/b" ); +} + +TEST_CASE( "rfc6901: decode segment — ~0 becomes ~", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "a~0b" ) == "a~b" ); +} + +TEST_CASE( "rfc6901: decode segment — multiple escapes", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "a~1b~0c~1d" ) == "a/b~c/d" ); +} + +TEST_CASE( "rfc6901: decode segment — ~0 before ~1 decodes correctly", "[rfc6901]" ) +{ + // ~01 → ~1 (not /), т.к. ~0 декодируется в ~, а 1 остаётся. + REQUIRE( pjson_decode_rfc6901_segment( "~01" ) == "~1" ); +} + +TEST_CASE( "rfc6901: decode segment — trailing ~ unchanged", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "abc~" ) == "abc~" ); +} + +TEST_CASE( "rfc6901: decode segment — empty string", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "" ) == "" ); +} + +TEST_CASE( "rfc6901: decode segment — ~2 unknown escape stays as-is", "[rfc6901]" ) +{ + REQUIRE( pjson_decode_rfc6901_segment( "a~2b" ) == "a~2b" ); +} + +// =========================================================================== +// put/get с ключами, содержащими '/' +// =========================================================================== + +TEST_CASE( "rfc6901: put/get key with slash via ~1 escaping", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + // Ключ "a/b" экранируется как "a~1b" в пути + REQUIRE( db.put( "/config/a~1b", int64_t( 42 ) ) ); + + node_view v = db.get( "/config/a~1b" ); + REQUIRE( v.valid() ); + REQUIRE( v.is_integer() ); + REQUIRE( v.as_int() == 42 ); + + // Проверяем, что ключ в объекте действительно содержит '/' + node_view config = db.get( "/config" ); + REQUIRE( config.valid() ); + REQUIRE( config.is_object() ); + + node_view child = config.at( "a/b" ); + REQUIRE( child.valid() ); + REQUIRE( child.as_int() == 42 ); +} + +TEST_CASE( "rfc6901: put/get key with tilde via ~0 escaping", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + // Ключ "a~b" экранируется как "a~0b" в пути + REQUIRE( db.put( "/data/a~0b", int64_t( 7 ) ) ); + + node_view v = db.get( "/data/a~0b" ); + REQUIRE( v.valid() ); + REQUIRE( v.as_int() == 7 ); + + // Проверяем, что ключ в объекте действительно содержит '~' + node_view data = db.get( "/data" ); + node_view child = data.at( "a~b" ); + REQUIRE( child.valid() ); + REQUIRE( child.as_int() == 7 ); +} + +TEST_CASE( "rfc6901: put/get key with both slash and tilde", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + // Ключ "x/y~z" экранируется как "x~1y~0z" + REQUIRE( db.put( "/obj/x~1y~0z", "hello" ) ); + + node_view v = db.get( "/obj/x~1y~0z" ); + REQUIRE( v.valid() ); + REQUIRE( v.is_string() ); + REQUIRE( v.as_string() == "hello" ); + + node_view obj = db.get( "/obj" ); + node_view child = obj.at( "x/y~z" ); + REQUIRE( child.valid() ); + REQUIRE( child.as_string() == "hello" ); +} + +// =========================================================================== +// exists с RFC 6901 путями +// =========================================================================== + +TEST_CASE( "rfc6901: exists with escaped slash key", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + REQUIRE( db.put( "/items/a~1b", true ) ); + REQUIRE( db.exists( "/items/a~1b" ) ); + REQUIRE_FALSE( db.exists( "/items/a~1c" ) ); +} + +// =========================================================================== +// erase с RFC 6901 путями +// =========================================================================== + +TEST_CASE( "rfc6901: erase key with escaped slash", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + REQUIRE( db.put( "/items/a~1b", int64_t( 1 ) ) ); + REQUIRE( db.put( "/items/normal", int64_t( 2 ) ) ); + + REQUIRE( db.exists( "/items/a~1b" ) ); + REQUIRE( db.erase( "/items/a~1b" ) ); + REQUIRE_FALSE( db.exists( "/items/a~1b" ) ); + + // Другой ключ не затронут + REQUIRE( db.exists( "/items/normal" ) ); +} + +// =========================================================================== +// clone с RFC 6901 путями +// =========================================================================== + +TEST_CASE( "rfc6901: clone with escaped slash in source and destination", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + REQUIRE( db.put( "/src/a~1b", int64_t( 99 ) ) ); + REQUIRE( db.clone( "/src/a~1b", "/dst/c~1d" ) ); + + node_view v = db.get( "/dst/c~1d" ); + REQUIRE( v.valid() ); + REQUIRE( v.as_int() == 99 ); + + // Проверяем, что ключ назначения содержит '/' + node_view dst = db.get( "/dst" ); + node_view child = dst.at( "c/d" ); + REQUIRE( child.valid() ); + REQUIRE( child.as_int() == 99 ); +} + +// =========================================================================== +// parse_into с RFC 6901 путями +// =========================================================================== + +TEST_CASE( "rfc6901: parse_into with escaped path", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + REQUIRE( db.parse_into( "/conf/a~1b", R"({"x":1})" ) ); + node_view v = db.get( "/conf/a~1b" ); + REQUIRE( v.valid() ); + REQUIRE( v.is_object() ); + + // Навигация внутрь: /conf/a~1b/x → вложенный ключ "x" внутри объекта с ключом "a/b" + node_view x = db.get( "/conf/a~1b/x" ); + REQUIRE( x.valid() ); + REQUIRE( x.as_int() == 1 ); +} + +// =========================================================================== +// pjson_split_path с RFC 6901 декодированием +// =========================================================================== + +TEST_CASE( "rfc6901: pjson_split_path decodes last segment", "[rfc6901]" ) +{ + std::string parent, last; + + pjson_split_path( "/foo/a~1b", parent, last ); + REQUIRE( parent == "/foo" ); + REQUIRE( last == "a/b" ); + + pjson_split_path( "/x~0y", parent, last ); + REQUIRE( parent == "/" ); + REQUIRE( last == "x~y" ); +} + +// =========================================================================== +// Пути без escaping по-прежнему работают +// =========================================================================== + +TEST_CASE( "rfc6901: plain paths without escaping still work", "[rfc6901][pjson_db_pmm]" ) +{ + reset_pmm(); + pjson_db_pmm db; + + REQUIRE( db.put( "/a/b/c", int64_t( 123 ) ) ); + node_view v = db.get( "/a/b/c" ); + REQUIRE( v.valid() ); + REQUIRE( v.as_int() == 123 ); +} From fd1d2fdc9426591c269f86cefd208ebfb156a592 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 22 Mar 2026 08:41:51 +0000 Subject: [PATCH 3/4] Revert "Initial commit with task details" This reverts commit 61977e48dd0cc73d463bf1f4a9d882094d43fc2d. --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 280e4aa..0000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-03-22T08:30:50.612Z for PR creation at branch issue-206-9d24f61cc2c2 for issue https://github.com/netkeep80/BinDiffSynchronizer/issues/206 \ No newline at end of file From 31ef7523f85ca58986a06ca147a8b6e5159c5046 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 22 Mar 2026 08:55:59 +0000 Subject: [PATCH 4/4] fix: replace Unicode em dash with ASCII dash in test names for MSVC compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows/MSVC, CTest corrupts Unicode characters (em dash '—' becomes garbled 'ΓÇö') when passing test names to the Catch2 binary, causing all 8 decode segment tests to fail with "No test cases matched". Replace em dash with ASCII hyphen-minus in TEST_CASE names. Co-Authored-By: Claude Opus 4.6 --- tests/test_pjson_rfc6901.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_pjson_rfc6901.cpp b/tests/test_pjson_rfc6901.cpp index 6d417f6..33e28c9 100644 --- a/tests/test_pjson_rfc6901.cpp +++ b/tests/test_pjson_rfc6901.cpp @@ -31,43 +31,43 @@ static void reset_pmm() // Декодирование сегментов: pjson_decode_rfc6901_segment // =========================================================================== -TEST_CASE( "rfc6901: decode segment — plain text unchanged", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - plain text unchanged", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "hello" ) == "hello" ); } -TEST_CASE( "rfc6901: decode segment — ~1 becomes /", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - ~1 becomes /", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "a~1b" ) == "a/b" ); } -TEST_CASE( "rfc6901: decode segment — ~0 becomes ~", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - ~0 becomes ~", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "a~0b" ) == "a~b" ); } -TEST_CASE( "rfc6901: decode segment — multiple escapes", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - multiple escapes", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "a~1b~0c~1d" ) == "a/b~c/d" ); } -TEST_CASE( "rfc6901: decode segment — ~0 before ~1 decodes correctly", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - ~0 before ~1 decodes correctly", "[rfc6901]" ) { // ~01 → ~1 (not /), т.к. ~0 декодируется в ~, а 1 остаётся. REQUIRE( pjson_decode_rfc6901_segment( "~01" ) == "~1" ); } -TEST_CASE( "rfc6901: decode segment — trailing ~ unchanged", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - trailing ~ unchanged", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "abc~" ) == "abc~" ); } -TEST_CASE( "rfc6901: decode segment — empty string", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - empty string", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "" ) == "" ); } -TEST_CASE( "rfc6901: decode segment — ~2 unknown escape stays as-is", "[rfc6901]" ) +TEST_CASE( "rfc6901: decode segment - ~2 unknown escape stays as-is", "[rfc6901]" ) { REQUIRE( pjson_decode_rfc6901_segment( "a~2b" ) == "a~2b" ); }