本章导读

文件下章讲目录的三个能力——从单文件操作升到"文件的容器与边界"。目录枚举(xrtDirOpen/Next):三态逐项枚举,且一次系统调用带出类型与尺寸——传统 readdir 每项补一次 stat 的 2N 次系统调用在这里是 N 次,大目录枚举性能差一个数量级。递归遍历:目录树的深度优先行走(dir_tour 范例展开)。沙箱根(xrtRootOpen):目录句柄作为安全边界——根内操作只收相对名、穿越与绝对路径系统级拒绝,是第 44 章词法校验(IsSafeEntry)之外的第二道防线(纵深防御的系统级一环)(词法可被绕过时,系统级语义兜底)。

47.1引入

解压用户上传的压缩包:包内条目名是不可信输入——../../evil.so 的穿越条目若被直接落盘就写到了沙箱外。第 44 章的词法校验(IsSafeEntry)能拦住绝大多数,但词法层有个根本弱点:它判断的是字符串,符号链接、平台特例、规范化差异都可能让词法判断与文件系统实际行为脱节。

沙箱根从系统级解决:xrtRootOpen 打开目录得到句柄,此后一切操作(建文件、开文件、建目录)都以句柄为基准、只收相对名——穿越在操作系统层面就无处遁形(不是"检查后放行"而是"根本没有逃出路径")。根句柄还有个附带福利:目录被改名/移动后依然有效(句柄不依赖路径字符串)——日志轮转、目录重组不打断进行中的操作。

47.2概念

目录枚举:三态接口与零额外 stat 的性能红利

1打开xrtDirOpen(路径, 标志) → 目录流句柄
2枚举xrtDirNext(句柄, 条目出参) → 三态:ITEM 取到 / END 结束 / ERROR 失败
3条目xdirentry 携带名字视图 + 类型/尺寸(一次调用全带出)
4关闭xrtDirClose

"零额外 stat"的性能含义:传统 readdir 拿到名字后,想知道"是文件还是目录"要再调一次 stat——N 个条目 2N 次系统调用;XRT 的目录 API 直接带出类型与尺寸(Windows/Linux 的原生目录信息本就包含)——N 次调用,大目录枚举快一个数量级。条目里的名字是借用视图,时效止于下一次 Next(第 18 章迭代纪律)。

递归遍历(目录树的深度优先行走)

目录树的深度优先行走:枚举当前层 → 遇子目录递归进入 → 完成后回溯。递归深度受第 3 章资源边界保护(防恶意深目录树);遍历中目录被并发增删是真实风险——枚举期间内容被外部修改——遍历器语义按"快照尽力"处理,需要严格一致时先收集再处理(第 18 章坑 2 的目录版)。

沙箱根:句柄即边界(系统级而非词法级)

xrtRootOpen / xrtRootOpenIn(嵌套子根)打开目录为根;xrtRootFileOpen(根内开文件)、xrtRootDirCreate/Remove(根内建删目录)全部只收相对名——../ 与绝对路径被拒绝。与第 44 章词法校验的配合关系:词法层做第一道过滤(快速拒绝明显的恶意形态,零成本),系统级根做最终保障(语义正确,覆盖词法盲区)——两层防线不是冗余,是纵深防御——工程安全的标准姿势:便宜的挡量、结实的兜底。

临时文件与目录的配合(批量暂存的标准容器)

xrtDirTemp(第 46 章提到)在指定目录创建唯一命名的临时目录——"一批临时文件需要一个独立家"的场景(解压暂存、构建中间产物):建临时目录 → 里面随便放 → 整目录删除收尾。比散落的临时文件干净一个量级——"家"的语义让清理变成一次调用。

47.3示例

完整程序:零额外 stat 枚举

来自仓库范例 examples/file/directory/main.c:

examples/file/directory/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>























 */


/* 展示零额外 stat 的目录枚举。 */
int main(void)
{
	xdir Dir = xrtDirOpen(".", 0u);
	xdirentry Entry;
	xdirnext Next;

	if ( Dir == NULL ) {
		return 1;
	}
	while ( (Next = xrtDirNext(Dir, &Entry)) == XDIR_NEXT_ITEM ) {
		printf("%.*s\n", (int)Entry.Name.Size, Entry.Name.Data);
	}
	if ( Next == XDIR_NEXT_ERROR ) {
		(void)xrtDirClose(Dir);
		return 1;
	}
	return xrtDirClose(Dir) ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/file/directory/main.c -lws2_32 -liphlpapi.git.gitattributes...

刚才发生了什么。 ① xrtDirOpen(".", 0) 打开当前目录——返回目录流句柄(不是文件句柄,两种句柄不可混用)。② xrtDirNext 三态循环:ITEM 打印条目名、END 自然结束、ERROR 中止并报告——与第 41 章行读取器的三态(LINE/END/失败)同构,XRT 流式接口的统一形态在目录上延续。③ 每个条目一次调用就带出名字与类型尺寸——内容随当前目录自然变化(输出清单因仓库而异)——本例只打印名字,但 Entry 里的类型字段(文件/目录)随手可用,不需要再 stat。④ 名字是借用视图(第 3 章约定)——%.*s 打印后即弃,需要保留自己拷贝(坑 1 的 good 形态正是"先拷贝再收集")。

完整程序:沙箱根操作

来自 examples/file/root/main.c——不可信相对名的安全存储:

examples/file/root/main.c源码 ↗
#include <stdio.h>
#include <string.h>

#include <xrt.h>
























 */


/* 使用目录能力安全处理不可信的相对文件名。 */
int main(int argc, char** argv)
{
	cstr sName = argc > 1 ? argv[1] : "message.txt";
	const cstr sDirectory = ".xrt-root-example-data";
	xfileoptions Options;
	xfile File;
	xroot Parent;
	xroot Root;

	Parent = xrtRootOpen(".");
	if ( Parent == NULL ) {
		fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError()));
		return 1;
	}
	if ( !xrtRootDirCreate(Parent, sDirectory, 0700u) ) {
		fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError()));
		(void)xrtRootClose(Parent);
		return 1;
	}
	Root = xrtRootOpenIn(Parent, sDirectory);
	if ( Root == NULL ) {
		fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError()));
		(void)xrtRootRemove(Parent, sDirectory);
		(void)xrtRootClose(Parent);
		return 1;
	}
	xrtFileOptionsInit(&Options);
	Options.Flags = XFILE_WRITE | XFILE_CREATE | XFILE_TRUNCATE;
	File = xrtRootFileOpen(Root, sName, &Options);
	if ( File == NULL ) {
		fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError()));
		(void)xrtRootClose(Root);
		(void)xrtRootRemove(Parent, sDirectory);
		(void)xrtRootClose(Parent);
		return 1;
	}
	if ( !xrtWriteFull(File, "stored inside root\n", 19u, NULL) ||
		 !xrtClose(File) || !xrtRootRemove(Root, sName) ||
		 !xrtRootClose(Root) ||
		 !xrtRootRemove(Parent, sDirectory) ||
		 !xrtRootClose(Parent) ) {
		fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError()));
		return 1;
	}
	printf("stored and removed %s/%s\n", sDirectory, sName);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/file/root/main.c -lws2_32 -liphlpapistored and removed .xrt-root-example-data/message.txt

刚才发生了什么。 ① xrtRootOpenIn 在父目录下开根——嵌套形态允许"每个上传一个根"。② xrtRootDirCreate 在根内建目录、xrtRootFileOpen 在根内开文件——全部相对名:传 ../escape 或绝对路径会被系统级拒绝(不是词法检查,是句柄语义的自然结果——操作就发生在根内,没有"根外"这个选项)。③ 存储完成、清理后 xrtRootClose 关根——根内资源随根的生命周期统一管理。④ 输出的路径带根前缀(审计可读),但操作从未依赖这个字符串——句柄不依赖路径,改名移动不打断。把 dir_tour 与 root_tour 留作进阶阅读:递归遍历与根操作的完整接口面各成一组断言输出。

47.4契约

  • 三态枚举:ITEM/END/ERROR 三态与全库流式接口同构;名字视图借用不出迭代期。
  • 零额外 stat:类型尺寸随条目带出——大目录枚举 N 次调用不是 2N。
  • 遍历边界:深度受资源边界保护;遍历中增删按"快照尽力",严格一致先收集再处理。
  • 沙箱语义:根内只收相对名;穿越与绝对路径系统级拒绝;根句柄不依赖路径字符串(改名移动后仍有效)。
  • 纵深防线:词法校验(第 44 章)快速过滤 + 系统级根语义兜底——两层配合不是冗余。
  • 临时目录:xrtDirTemp 唯一命名整目录管理——批量暂存的标准容器。

从示例到工程:目录能力的三个宿主

上传与解压(沙箱根的主场):每个上传一个根(RootOpenIn 嵌套)——穿越条目系统级拒收、正常条目根内还原、处理完整根删除;第 44 章词法校验做前置快速过滤,根句柄做最终保障。资源目录访问(枚举+根的组合):插件资源、静态文件目录——根句柄固定后目录可被运维改名重组(句柄不依赖路径),服务不中断。批量构建暂存(临时目录的主场):构建/转换任务的中间产物整目录管理——DirTemp 建家、任务完删除,比散落临时文件干净一个量级(第 46 章原子交付的批量版)。

遍历的工程纪律

递归遍历看似简单,工程化要补四条纪律。深度上限:恶意深目录树(或符号链接环)能打爆朴素递归——资源边界在枚举入口设闸。符号链接策略:跟随(默认可能成环)或不跟随(按类型字段识别后跳过)——策略显式声明并写进注释。错误恢复:单目录枚举失败(权限)是中止还是跳过——按遍历目的定(审计中止、清理跳过)。顺序假设:枚举顺序由文件系统决定(无序)——需要排序的清单在收集后自行排序(第 14 章)。四条纪律合起来:遍历器是框架代码不是循环样板——写之前先答四个问题。

与文件族前两章的合流

至此文件三章合流成完整地图:第 45 章的句柄与读写(单文件生命周期)、第 46 章的映射锁原子交付(并发与性能)、本章的目录与沙箱(组织与安全)。三者的典型组合贯穿全书后续:卷七静态文件服务(根句柄 + 枚举 + 映射)、卷十二构建工具(目录遍历 + 临时目录 + 原子交付)、解压器(沙箱根 + 三态枚举 + Full 读写)。文件能力是系统服务的地基——地基铺完,下一章的异步与信号补上最后两块砖。

常见错误

坑 1:遍历时删除目录内容(快照尽意的陷阱)

症状:遍历偶发漏项或 ERROR——并发清理脚本正在删遍历目标里的文件;复现依赖时序。

原因:目录枚举是"尽力快照"——底层目录流的语义不保证看到遍历期间的所有变更。

错误写法
while ( (Next = xrtDirNext(Dir, &Entry)) == XDIR_NEXT_ITEM ) {
	if ( IsOld(Entry.Name) ) {
		DeleteByName(DirPath, Entry.Name);   /* 边遍历边删——底层目录流受扰 */
	}
}
正确写法
xarray tOld;
xrtArrayInit(&tOld, sizeof(str));
while ( (Next = xrtDirNext(Dir, &Entry)) == XDIR_NEXT_ITEM ) {
	if ( IsOld(Entry.Name) ) {
		xrtArrayPush(&tOld, &(str){ xrtStrDupView(Entry.Name) });   /* 先收集名字 */
	}
}
xrtDirClose(Dir);
for ( size_t i = 0; i < tOld.Count; i++ ) {
	DeleteByName(DirPath, *(str*)xrtArrayGet(&tOld, i));   /* 遍历结束后再删 */
}
坑 2:拿词法校验当唯一防线(纵深缺失)

症状:渗透测试绕过了 IsSafeEntry——符号链接、大小写混淆或平台路径规范化差异让"词法上安全"的路径实际指向根外——字符串判断与文件系统解析在边界处脱节。

原因:词法判断的是字符串,文件系统解析的是真实路径——两者在符号链接等场景脱节。

错误写法
if ( xrtPathIsSafeEntry(Name, false) ) {
	str sFull = xrtPathJoin(BaseDir, NameBuf);
	xrtFileOpen(sFull, ...);   /* 词法过了就开——符号链接可能指向根外 */
}
正确写法
xroot Root = xrtRootOpen(BaseDir, 0);
xfile File = xrtRootFileOpen(Root, NameBuf, ...);   /* 系统级边界:穿越无处发生 */
/* 词法校验仍可前置做快速过滤——但根句柄才是最终保障 */

编程练习

基础:枚举统计(零额外 stat 的数据利用)

枚举一个目录,按类型(文件/目录)分别计数并输出总尺寸——零额外 stat 的一次调用数据全部用上。

进阶:递归清单器(遍历四纪律的首演)

实现 list_tree(路径, 深度上限):缩进输出目录树(深度优先)、每层计数文件与子目录、超限目录标注跳过——递归遍历与资源边界的组合。

挑战:安全解压器(纵深防线的完全体)

用沙箱根实现压缩包落盘:根内创建目录结构(RootDirCreate 逐级)、文件落盘(RootFileOpen+Write)、恶意条目(穿越/绝对/设备名)全部拒收并列清单。验收标准:构造含 5 个恶意条目的包(穿越/绝对路径/设备名/符号链接环/超深嵌套各一),全部拒收且根外零文件;正常条目完整还原;整根删除后零残留——五类攻击的处置各写一行结论。

小结

知识点速查
三态枚举DirOpen/Next/Close:ITEM=取到/END=结束/ERROR=失败,与全库流式同构
零额外 stat类型与尺寸随条目一次带出——N 次调用而非 2N,大目录快一个数量级
递归遍历深度优先行走 + 资源边界深度闸;遍历中增删按快照尽力语义
沙箱根RootOpen/RootOpenIn/RootFileOpen/RootDirCreate:只收相对名,系统级拒绝穿越
纵深防线词法校验(第 44 章)快速过滤 + 根句柄语义兜底——便宜的挡量、结实的兜底
临时目录xrtDirTemp 唯一命名——批量暂存整目录管理
三宿主上传解压(根主场)/ 资源目录(枚举+根)/ 批量暂存(临时目录)
遍历四纪律深度上限 / 符号链接策略 / 错误恢复 / 顺序假设——框架不是样板

📖 本章涉及的 API 参考手册:file(132 个条目)path(41 个条目)

点击查看完整签名、参数约束、返回值与错误说明