Chapter Orientation

The path module handles paths' lexical layer — joining, splitting, cleanup, validation — none of it touching the file system (pure string operations, zero IO, zero side effects, disk-free testing). Three core operations: xrtPathJoin, multi-segment joining with automatic lexical cleanup (../ folding, separator normalization), four-way splitting (Name/Stem/Ext/directory in one pass), and xrtPathIsSafeEntry, the safety check (rejecting traversal and device names — the security gate for external input entering paths). Plus zero-allocation segment iteration and system-path queries (home/cwd/real).

44.1引入

A user-uploaded file goes to data/{用户名}/icon.png (username) — the username is external input. Concatenate directly: a username of ../../etc writes the file outside the sandbox; a username of CON.txt (a Windows device name) behaves bizarrely. This kind of "path traversal" is a regular on security-vulnerability charts, and the root cause is always external input entering a path without lexical validation.

The second daily pain is cleaning up hand-concatenated paths: "project" + "src/../include/xrt.h" — the joined path carries the redundant src/.. segment; most file systems tolerate it, but log comparisons, cache keys, and test assertions are all polluted by "one path, many spellings". xrtPathJoin builds cleanup into joining: the output is always the folded canonical form — one path, one spelling is the lexical layer's core promise.

44.2概念

Joining and automatic cleanup

xrtPathJoin("project", "src/../include/xrt.h") (two cstr segments in, an owning str out) → project\include\xrt.h — src/.. folded, separators normalized per platform. Cleanup is lexical-level (no symlink resolution, no disk existence check): .. is preserved when it cannot fold (../secret is not wrongly folded to empty — that is precisely the safety semantics). Cross-platform forms: XPATH_NATIVE follows the current platform (Windows backslashes/Unix forward slashes); explicitly specifying a form yields cross-platform-consistent products (test snapshots, manifest files).

Four-way splitting and rebuilding

SplitResult (for project\include\xrt.h)
xrtPathNamexrt.h (the last segment)
xrtPathStemxrt (extension removed)
xrtPathExt.h (dot included)
Directory partrebuilt by joining or obtained by iteration

xrtPathWithName replaces the file name and keeps the directory (renamed=project\include\runtime.h) — the standard posture for changing extensions or adding suffixes. Splitting and rebuilding are mutually inverse: 目录 + Name == 原路径 (directory + Name == original path) — tests assert exactly this inverse law.

The safety check: a security gate for external input

xrtPathIsSafeEntry is a one-vote-veto check rejecting three dangerous shapes: traversal (../ sequences — escaping the base directory), absolute paths (external input should never decide the root), and platform device names (CON/NUL and other Windows reserved names). The standard pipeline before external input enters a path: IsSafeEntry 校验 → Join 拼接(自动清理)→ 使用 (IsSafeEntry validation → Join (automatic cleanup) → use) — the safe sample's output (rejected) is this gate's enforcement record. The second parameter controls whether subdirectory entries are allowed (archive extraction opens it as needed).

Anatomy of one path

1Inputproject/src/../include/xrt.h (users' varied spellings)
2Join cleanupfold src/.., normalize separators - produce the canonical form
3Four-way splitName=xrt.h / Stem=xrt / Ext=.h / directory=project/include
4Inverse rebuilddirectory + Name == the original; WithName swaps the name, keeps the directory

Segment iteration and system paths

xrtPathIterInit/Next walks segment by segment with zero allocation (borrowing — each segment is a view slicing the original path, Chapter 25's pipeline thinking); segment counting and level-by-level directory creation (Chapter 47) both rely on it. System-path queries: home (user directory), cwd (current directory), real (the true path after resolving symbolic links — this one does touch the file system, the module's sole exception).

44.3示例

Complete program: join-with-cleanup and four-way splitting

From the repository sample examples/path/basic/main.c:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/path/basic/main.c -lws2_32 -liphlpapipath=project\include\xrt.hname=xrt.hstem=xrtext=.hrenamed=project\include\runtime.hcomponents=3local=1

What just happened. (1) The output of Join("project", "src/../include/xrt.h") no longer contains src/.. — joining is cleanup, the product canonical; the separator is the Windows backslash (the same program on Linux prints forward slashes — platform form, automatic). (2) Four-way splitting in one pass: Name is xrt.h, Stem drops .h, Ext includes the dot — three views obtained independently. (3) WithName replaces the file name, directory untouched — the standard rename posture. (4) components=3 comes from zero-allocation iteration — three segments (project/include/xrt.h) counted one by one, no heap allocation throughout. (5) local=1 is the relative-path verdict — the absolute/relative divide serves path routing (config relative to a base directory).

Complete program: the safety check's triple rejection

From examples/path/safe/main.c:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/path/safe/main.c -lws2_32 -liphlpapiassets/icon.png: safe../secret.txt: rejectedCON.txt: rejected

What just happened. (1) The ordinary relative path assets/icon.png passes — the check doesn't block legitimate input. (2) ../secret.txt is rejected — traversal sequences don't escape the gate; the third input (an absolute-path shape) is likewise rejected — external input shouldn't carry a root. (3) CON.txt is rejected — cross-platform interception of Windows device names (Linux rejects it too; consistency first). The tour sample is left as full-interface reading: parse structure (root/flags/stem/ext), build and clean, relativization, and relative-vs-absolute verdicts — one ok per group, four groups covering the path module's complete interface surface.

44.4契约

  • Purely lexical: everything except real (symlink resolution) stays off the file system — testability maxed.
  • Joining is cleanup: Join's (two cstr segments) product is the folded, normalized canonical form; multi-segment paths use nested Joins or one Join after assembly; one path, one spelling.
  • Splitting inverse: directory + Name == original path; Stem + Ext == Name (with the dot).
  • Security discipline: external input passes IsSafe before entering a path; traversal/absolute/device names — the triple rejection.
  • Platform forms: NATIVE per platform; explicit forms for cross-platform-consistent products.
  • Zero-allocation iteration: segment views borrow the original path, validity tied to it.

From examples to engineering: three hosts of paths

Config and resource resolution: at startup, normalize the three base paths "config directory/data directory/temp directory" (Join + real); thereafter every relative path Joins onto a base — path routing centralized, avoiding bare string paths scattered everywhere. User-input mapping: upload file names, static-resource URL paths — the IsSafeEntry gatekeeper + Join + extension whitelisting (Chapter 36's pipeline mirrored at the file layer). Cross-platform products: paths in manifest files, cache keys, test snapshots — explicit form (not NATIVE) guarantees identical products on Linux and Windows, so Chapter 12's hash fingerprints are reproducible. The three hosts share one bottom line: normalize a path before using it — the bare string path is the root of all path problems.

The relation to Chapters 45~48

path is the lexical prelude to the file family (Chapters 45~48): a file is opened by path, and the path first passes Join/IsSafeEntry's normalization and security check. The file chapters all point back here — "where the path came from" is told once at the source (this chapter), and the file chapters focus on "what happens after opening". The reverse relation exists too: this chapter's real query (the only disk-touching interface) depends on file-system metadata operations internally — the lexical layer and the IO layer meet in a handshake at real and stay apart everywhere else.

Three cross-platform difference points

Three differences are worth memorizing for cross-platform path code. Separators: Join normalizes automatically; a hand-written / passes most Windows APIs — but canonical forms, cache keys, and log comparisons derail on mixed separators. Root shapes: Unix's single root / versus Windows drive letters C: plus UNC \server — the absolute-path verdict (via IsLocal) follows the shape table; /foo is absolute on Unix and "drive-less relative" on Windows. Device and reserved names: CON/NUL/COM1 are Windows-specific traps — IsSafeEntry checks uniformly against the strictest platform (rejecting on Linux too) so products are distributable cross-platform. All three differences are smoothed over by this chapter's APIs — you may "forget the platform" while writing lexical code; that is precisely why the module exists.

Common Mistakes

Pitfall 1: external input straight into a path:Pitfall 1: external input straight into a path

Symptom: a security audit reports a path traversal hole — ../../ input reads and writes outside the sandbox; or device-name input causes bizarre behavior.

Cause: no validation step before joining — "the user gives a file name" was assumed as an implicit contract.

错误写法
str sPath = xrtPathJoin("data/", UserNameBuf);   /* UserNameBuf is external input */
SaveTo(sPath);   /* UserName="../etc/passwd": the traversal segment escapes the data sandbox */
正确写法
xstrview Name = (xstrview){ UserName, strlen(UserName) };
if ( !xrtPathIsSafeEntry(Name, false) ) {
	RejectUpload();   /* traversal/absolute/device names rejected at the gate */
	return false;
}
Pitfall 2: hand-concatenating paths as strings:Pitfall 2: hand-concatenating paths as strings

Symptom: "dir/" + name produces mixed separators on Windows (dir/\file); a/b/../c redundant segments pollute cache keys and log comparisons.

Cause: skipping Join's cleanup and normalization — doing path work with string Concat (Chapter 25).

错误写法
str sPath = xrtStrConcat(XRT_STR_LITERAL("data/"), NameView);
sPath = xrtStrConcat((xstrview){ sPath, strlen(sPath) }, XRT_STR_LITERAL("/icon.png"));
/* mixed separators + no cleanup + three allocations */
正确写法
str sPath = xrtPathJoin("data/", NameBuf);   /* one call: normalize + cleanup + one allocation */

Programming Exercises

Basic: verify the inverse law (a regression test for the lexical layer)

Take five paths of different shapes (relative/absolute/multi-segment/with extension/without) and "split then rebuild"; assert the rebuilt result equals the original's canonical form — the inverse law, measured.

Advanced: a path router (one entrance for base directories)

Implement resolve(基准目录, 输入路径) (base directory, input path): if the input is relative, Join the base; if absolute, reject or allow per config; output canonical form. Write the config switch's behavioral difference into a comment.

Challenge: an upload sandbox (the security gate in full)

A composite validator: file-name check (IsSafe triple rejection) + length cap + extension whitelist + result-path uniquification (on conflict, append a number to the Stem). Acceptance criteria: twenty crafted inputs (traversal/device/over-long/out-of-whitelist/conflict) each handled as expected; zero file-system calls throughout (purely lexical, fully testable).

Summary

TopicQuick reference
Purely lexicalthe whole module does zero IO except real (symlink resolution) - disk-free testing
Joinjoining is cleanup: ../ folded, separators normalized, one canonical form
Four-way splitName (last segment) / Stem (extension removed) / Ext (dot included) / directory rebuilt
SecurityIsSafeEntry(视图, 是否允许子目录) (view, allow subdirectories): traversal/absolute/drive/device names all rejected - mandatory for external input
IterationIterInit/Next zero-allocation segment walk; segment views borrowed
System pathshome/cwd queries; real resolves symlinks (the only disk-touching interface)
Three hostsbase-path normalization / user-input mapping (gatekeeper + whitelist) / cross-platform products
Three cross-platform differencesseparator normalization / root-shape verdicts / device names uniformly checked - all smoothed by the API

📖 API references in this chapter:path (41 entries)

Click for full signatures, parameter constraints, return values and error notes