1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! The disk-aware half of the generated-output write boundary.
//!
//! Split out of `write.rs` rather than added to it: that file sits close to this repository's
//! 1,000-line cap, and "does this path still land inside the project once the filesystem has had
//! its say" is a self-contained concern from "how bytes reach disk". ~keep
use ;
/// Refuse an emitted path whose already-existing ancestor chain leaves `base_dir` through a
/// symlink.
///
/// [`super::contained_output_path`]'s lexical check is the first half of this boundary and stays
/// exactly as it is: it runs on the string, before anything exists on disk, and rejects
/// absoluteness, `..` and drive prefixes. What it cannot see is the disk. `packages/node/index.ts`
/// passes every component test there is and still lands outside the project the moment
/// `packages/node` is a symlink to somewhere else, because both writers that follow resolve
/// symlinks: `std::fs::create_dir_all` walks through a symlinked directory, and
/// `tempfile::NamedTempFile::new_in(parent)` creates its temporary inside whatever `parent`
/// really is. A repository can ship that symlink in its own tracked tree, so the escape needs no
/// hostile config value at all -- which is why the lexical pass alone is not the boundary.
///
/// Resolution stops at the deepest ancestor that **exists**. The leaf almost never does -- it is
/// what this run is about to create -- and `canonicalize` on a missing path returns an error
/// rather than an answer, so canonicalizing the full emitted path would fail on the ordinary
/// case. The remaining components are left to the lexical pass that already cleared them: a
/// component that does not exist cannot be a symlink, and one created later is created by us,
/// underneath a parent this walk has already resolved and contained.
///
/// Every comparison is canonical-to-canonical, because `base_dir` itself being reached through a
/// symlink is the common case, not the exotic one: on macOS `/tmp` is a symlink to `/private/tmp`
/// (so is every `tempfile::tempdir()` handed out under `/var/folders`, `/var` -> `private/var`),
/// and a checkout under a symlinked home or mounted volume behaves the same. Comparing a resolved
/// descendant against the *uncanonicalized* base would reject all of those legitimate writes,
/// which is the likeliest way a containment check breaks real usage. Canonicalizing the base once
/// and joining each component onto the previously-resolved parent keeps both sides in the same
/// namespace. ~keep
pub