Ориентация главы
Модуль path ведает лексическим слоем путей — склейка, разложение, зачистка, валидация — всё без единого касания файловой системы (чистые строковые операции: ноль IO, ноль побочных эффектов, тесты без диска). Три ключевые операции: xrtPathJoin — многосегментная склейка с автоматической лексической зачисткой (свёртка ../, нормализация разделителей); четырёхстороннее разложение (Name/Stem/Ext/каталог за один заход); xrtPathIsSafeEntry — проверка безопасности (отказ обходу каталога и именам устройств — досмотровая рамка для внешнего входа, входящего в путь). Плюс посегментная итерация без распределений и запросы системных путей (home/cwd/real).
44.1引入
Файл, загруженный пользователем, нужно положить в data/{用户名}/icon.png (имя пользователя) — а имя пользователя это внешний вход. Склейка напрямую: имя ../../etc — и файл записан вне песочницы; имя CON.txt (имя устройства Windows) ведёт себя причудливо. Такой «обход пути» — завсегдатай рейтингов уязвимостей, и корень всегда один: внешний вход вошёл в путь без лексической проверки.
Вторая ежедневная боль — зачистка рукописной склейки путей: "project" + "src/../include/xrt.h" — склеенный путь несёт избыточный сегмент src/..; большинство файловых систем это терпят, но сравнение журналов, ключи кеша и утверждения тестов загрязняются «один путь — много написаний». xrtPathJoin встраивает зачистку в склейку: выход всегда свёрнутая каноническая форма — один путь — единственное написание есть ключевой обет лексического слоя.
44.2概念
Склейка и автоматическая зачистка
xrtPathJoin("project", "src/../include/xrt.h") (два сегмента cstr на входе, владеющий str на выходе) → project\include\xrt.h — src/.. свёрнут, разделители нормализованы по платформе. Зачистка лексического уровня (без разрешения симлинков и без проверки существования на диске): .. сохраняется, когда свернуть нельзя (../secret ошибочно не сворачивается в пустоту — это и есть семантика безопасности). Кроссплатформенные формы: XPATH_NATIVE — по текущей платформе (обратные слеши Windows/прямые Unix); явное указание формы — для кроссплатформенно согласованных продуктов (снимки тестов, файлы манифестов).
Четырёхстороннее разложение и пересборка
| Разложение | Результат (для project\include\xrt.h) |
|---|---|
xrtPathName | xrt.h (последний сегмент) |
xrtPathStem | xrt (без расширения) |
xrtPathExt | .h (с точкой) |
| Часть каталога | пересборка склейкой или получение итерацией |
xrtPathWithName заменяет имя файла, сохраняя каталог (renamed=project\include\runtime.h) — стандартная поза смены расширения и добавления суффикса. Разложение и пересборка взаимно обратны: 目录 + Name == 原路径 (каталог + Name == исходный путь) — тесты утверждают именно этот закон обратимости.
Проверка безопасности: досмотровая рамка для внешнего входа
xrtPathIsSafeEntry — валидация с правом вето в один голос, отказывающая трём опасным формам: обход (последовательности ../ — побег из базового каталога), абсолютный путь (внешний вход не должен решать корень), имена устройств платформы (CON/NUL и другие зарезервированные имена Windows). Стандартный конвейер перед входом внешних данных в путь: IsSafeEntry 校验 → Join 拼接(自动清理)→ 使用 (проверка IsSafeEntry → склейка Join (с автозачисткой) → использование) — вывод образца safe (rejected) и есть протокол правоприменения этой рамки. Второй параметр управляет допустимостью подкаталоговых записей (распаковка архивов открывает по мере надобности).
Анатомическая схема одного пути
Посегментная итерация и системные пути
xrtPathIterInit/Next обходит по сегментам без распределений (представления-заимствования: каждый сегмент — срез исходного пути, конвейерное мышление главы 25); подсчёт сегментов и создание каталогов уровень за уровнем (глава 47) держатся на нём. Запросы системных путей: home (каталог пользователя), cwd (текущий каталог), real (истинный путь после разрешения симлинков — вот это требует касания файловой системы и есть единственное исключение модуля).
44.3示例
Полная программа: склейка с зачисткой и четырёхстороннее разложение
Из образца репозитория examples/path/basic/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { str sJoined = NULL; str sName = NULL; str sStem = NULL; str sExt = NULL; str sRenamed = NULL; xpathiter Iterator; xpathcomponent Component; size_t iComponents = 0; int iResult = 1; /* 拼接 + 清理:src/.. 折叠,得到 project\include\xrt.h。 */ sJoined = xrtPathJoin("project", "src/../include/xrt.h"); if ( sJoined == NULL ) { goto cleanup; } /* 四路分解:文件名 / 主干 / 扩展名 / 换名拷贝。 */ sName = xrtPathName(sJoined); sStem = xrtPathStem(sJoined); sExt = xrtPathExt(sJoined); sRenamed = xrtPathWithName(sJoined, "runtime.h"); if ( (sName == NULL) || (sStem == NULL) || (sExt == NULL) || (sRenamed == NULL) ) { goto cleanup; } */ if ( !xrtPathIterInit(&Iterator, xrtStrView(sJoined), XPATH_NATIVE) ) { goto cleanup; } while ( xrtPathNext(&Iterator, &Component) ) { iComponents++; } printf("path=%s\nname=%s\nstem=%s\next=%s\nrenamed=%s\n" "components=%zu\nlocal=%d\n", sJoined, sName, sStem, sExt, sRenamed, iComponents, xrtPathIsLocal(xrtStrView(sJoined), XPATH_NATIVE) ? 1 : 0); iResult = 0; cleanup: xrtFree(sJoined); xrtFree(sName); xrtFree(sStem); xrtFree(sExt); xrtFree(sRenamed); return iResult; }
Что только что произошло. (1) Вывод Join("project", "src/../include/xrt.h") уже не содержит src/.. — склейка и есть зачистка, продукт канонический; разделитель — обратный слеш Windows (та же программа на Linux выведет прямые слеши — форма платформы выбирается сама). (2) Четырёхстороннее разложение за один заход: Name — xrt.h, Stem отбрасывает .h, Ext содержит точку — три представления получены независимо. (3) WithName заменяет имя файла, каталог не тронут — стандартная поза переименования. (4) components=3 получен итерацией без распределений — три сегмента (project/include/xrt.h) пересчитаны по одному, на всём обходе ни одного распределения в куче. (5) local=1 — вердикт об относительности: развилка абсолютный/относительный нужна при маршрутизации путей (конфигурация относительно базового каталога).
Полная программа: тройной отказ проверки безопасности
Из examples/path/safe/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { cstr arrEntries[] = { "assets/icon.png", /* 合法相对条目 */ "../secret.txt", /* 穿越逃逸 */ "CON.txt" /* Windows 保留设备名 */ }; for ( size_t i = 0; i < sizeof(arrEntries) / sizeof(arrEntries[0]); i++ ) { printf("%s: %s\n", arrEntries[i], xrtPathIsSafeEntry(xrtStrView(arrEntries[i]), false) ? "safe" : "rejected"); } return 0; }
Что только что произошло. (1) Обычный относительный путь assets/icon.png проходит — проверка не задерживает легальный вход. (2) ../secret.txt отвергнут — последовательность обхода не выйдет за досмотровую рамку; третий вход (форма абсолютного пути) отвергнут так же — внешний вход не должен нести с собой корень. (3) CON.txt отвергнут — кроссплатформенное interception имён устройств Windows (на Linux отказ такой же — согласованность прежде всего). Образец tour оставлен чтением всех интерфейсов: структура разбора (root/flags/stem/ext), build и clean, преобразование в относительный вид, вердикты относительности/абсолютности — по ok на группу, четыре группы покрывают полную интерфейсную поверхность модуля path.
44.4契约
- Чистая лексика: всё, кроме
real(разрешение симлинков), не касается файловой системы — тестируемость на максимум. - Склейка есть зачистка: продукт Join (два сегмента cstr) — свёрнутая нормализованная каноническая форма; многосегментные пути — вложенными Join или одним Join после сборки; один путь — одно написание.
- Обратимость разложения: каталог + Name == исходный путь; Stem + Ext == Name (с точкой).
- Досмотровая дисциплина: внешний вход перед входом в путь обязан пройти
IsSafe; обход/абсолютный/имя устройства — тройной отказ. - Формы платформ: NATIVE по платформе; явное указание — для кроссплатформенно согласованных продуктов.
- Итерация без распределений: представления сегментов заимствуют исходный путь, срок действия от него же.
От примеров к инженерии: три хозяина путей
Разбор конфигурации и ресурсов: на старте приложения три базовых пути — «каталог конфигурации/каталог данных/каталог временного» — нормализуются (Join + real), и дальше все относительные пути приклеиваются к базе — маршрутизация путей централизована, голые строковые пути по закоулкам не разбросаны. Отображение пользовательского ввода: имена загружаемых файлов, пути статических ресурсов URL — привратник IsSafeEntry + склейка Join + белый список расширений (зеркало конвейера главы 36 на файловом слое).Кроссплатформенные продукты: пути в файлах манифестов, ключах кеша, снимках тестов — явная форма (не NATIVE) гарантирует одинаковые продукты на Linux и Windows, и только тогда хеш-отпечатки главы 12 воспроизводимы. Три хозяина делят одну нижнюю черту: сначала нормализуй путь, потом употребляй — голый строковый путь есть корень всех зол на пути.
Отношение к главам 44~47
path — лексическая предпосылка файлового семейства (главы 45~48): прежде чем открыть файл, есть путь, а путь прежде проходит нормализацию и досмотр Join/IsSafeEntry. Главы файлового семейства будут оглядываться сюда — «откуда путь» рассказано один раз у источника (в этой главе), файловые главы сосредоточены на «что после открытия». Есть и обратное отношение: запрос real этой главы (единственный интерфейс, касающийся диска) внутри опирается на операции с метаданными файловой системы — лексический слой и слой IO пожимают руки в точке real, а в остальном расходятся как банка и берег.
Три точки кроссплатформенных различий
Три различия стоит выучить наизусть при кроссплатформенном коде путей. Разделители: Join нормализует сам; рукописный / на Windows пройдёт в большинство API — но каноническая форма, ключи кеша и сравнение журналов сломаются о смешанные разделители. Форма корня: единый корень Unix / против буквы диска Windows C: и UNC \server — вердикт абсолютности (обратный запрос IsLocal) идёт по таблице форм; /foo на Unix абсолютно, на Windows — «относительно без диска».Имена устройств и зарезервированные имена: CON/NUL/COM1 — специфические капканы Windows; IsSafeEntry проверяет единообразно по самой строгой платформе (отказывает и на Linux) — только тогда продукт распространяем кроссплатформенно. Все три различия API этой главы сглаживает — при лексическом коде можно «забыть о платформе»; в этом и смысл существования модуля.
Типичные ошибки
Симптом: аудит безопасности репортит уязвимость обхода пути — вход ../../ читает и пишет вне песочницы; или ввод имени устройства порождает причудливое поведение.
Причина: перед склейкой нет этапа проверки — «пользователь даёт имя файла» приняли за неявный контракт.
str sPath = xrtPathJoin("data/", UserNameBuf); /* UserNameBuf - внешний вход */ SaveTo(sPath); /* при UserName="../etc/passwd": сегмент обхода убегает из песочницы data */
xstrview Name = (xstrview){ UserName, strlen(UserName) }; if ( !xrtPathIsSafeEntry(Name, false) ) { RejectUpload(); /* обход/абсолютный/имя устройства отвергнуты в дверях */ return false; }
Симптом: "dir/" + name на Windows даёт смешанные разделители (dir/\file); избыточные сегменты a/b/../c загрязняют ключи кеша и сравнение журналов.
Причина: минуя зачистку и нормализацию Join — работой путей занялся строковый Concat (глава 25).
str sPath = xrtStrConcat(XRT_STR_LITERAL("data/"), NameView); sPath = xrtStrConcat((xstrview){ sPath, strlen(sPath) }, XRT_STR_LITERAL("/icon.png")); /* каша разделителей + без зачистки + три распределения */
str sPath = xrtPathJoin("data/", NameBuf); /* один вызов: нормализация + зачистка + одно распределение */
Практические задания
Для пяти путей разных форм (относительный/абсолютный/многосегментный/с расширением/без) выполните «разложить и пересобрать»; утвердите совпадение пересобранного с канонической формой исходного — закон обратимости на практике.
Реализуйте resolve(基准目录, 输入路径) (базовый каталог, входной путь): вход относительный — приклеить базу Join-ом; абсолютный — отказ или пропуск по конфигурации; на выходе каноническая форма. Различие поведения переключателя зафиксируйте комментарием.
Комплексный валидатор: досмотр имени файла (тройной отказ IsSafe) + лимит длины + белый список расширений + уникализация итогового пути (при конфликте к Stem добавляется номер). Критерии приёмки: двадцать сконструированных входов (обход/устройство/сверхдлина/вне белого списка/конфликт) каждый получает ожидаемую обработку; на всём пути ноль вызовов файловой системы (чисто лексический слой — тестируемо).
Итоги
| Тема | Кратко |
|---|---|
| Чистая лексика | весь модуль без IO, кроме real (разрешение симлинков) - тесты без диска |
| Join | склейка есть зачистка: ../ свёрнут, разделители нормализованы, каноническая форма единственна |
| Четырёхстороннее разложение | Name (последний сегмент) / Stem (без расширения) / Ext (с точкой) / каталог пересборкой |
| Досмотр | IsSafeEntry(视图, 是否允许子目录) (представление, разрешать ли подкаталоги): обход/абсолютный путь/буква диска/имя устройства - отказы подряд; обязателен для внешнего входа |
| Итерация | IterInit/Next посегментно без распределений; представления сегментов заимствованы |
| Системные пути | запросы home/cwd; real разрешает симлинки (единственный интерфейс, касающийся диска) |
| Три хозяина | нормализация базовых путей / отображение пользовательского ввода (привратник + белый список) / кроссплатформенные продукты |
| Три кроссплатформенных различия | нормализация разделителей / вердикт формы корня / имена устройств единообразно на страже - всё сглажено API |
📖 Справочники API в этой главе:path (41 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки