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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! GH-239: the store must be usable by a process that is not root.
//!
//! `STORE_BASE` was a compile-time constant, `/var/lib/forjar/store`, with no
//! environment or config override, and `store_entry_path()` — the public path
//! API — could not be pointed anywhere else. `/var/lib` is root-owned on every
//! mainstream distribution, so an ordinary user got:
//!
//! ```text
//! $ forjar store list
//! error: read /var/lib/forjar/store: No such file or directory (os error 2)
//! $ mkdir -p /var/lib/forjar/store
//! mkdir: cannot create directory '/var/lib/forjar': Permission denied
//! ```
//!
//! with no next move. That is every non-root caller: CI, library consumers, and
//! anyone evaluating forjar without handing it root.
//!
//! These run the real binary, because the claim is about what a user can do.
use std::process::Command;
fn forjar() -> Command {
Command::new(env!("CARGO_BIN_EXE_forjar"))
}
#[test]
fn store_list_works_in_a_directory_the_caller_owns() {
// The reproduction from the issue, inverted: given a store root the caller
// can actually write, `store list` must succeed rather than fail on a
// root-owned path it was never able to reach.
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("store");
std::fs::create_dir_all(&store).unwrap();
let out = forjar()
.arg("store")
.arg("list")
.env("FORJAR_STORE", &store)
.output()
.expect("forjar must run");
assert!(
out.status.success(),
"store list failed for a writable store root:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn the_store_env_var_is_not_ignored() {
// The precise assertion the issue's repro failed on:
// "FORJAR_STORE env is ignored: set=Err(NotPresent)"
// An explicit root must win over both the system default and the per-user
// fallback, so that a misconfiguration surfaces on the path the operator
// named instead of being silently redirected.
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("explicit-store");
std::fs::create_dir_all(&store).unwrap();
let out = forjar()
.arg("store")
.arg("list")
.arg("--json")
.env("FORJAR_STORE", &store)
.output()
.expect("forjar must run");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!combined.contains("/var/lib/forjar/store"),
"FORJAR_STORE was ignored — output still names the root-owned default:\n{combined}"
);
}
#[test]
fn an_unprivileged_process_does_not_resolve_to_the_root_owned_default() {
// With no FORJAR_STORE and no write access to /var/lib, the resolved root
// must fall back to the caller's own data directory. If /var/lib/forjar IS
// writable here (CI running as root, or a real system install), the system
// path is correct and the fallback must NOT engage — so the assertion is
// conditional on what this host actually allows, not on an assumption.
let system_writable = std::fs::create_dir_all("/var/lib/forjar/store").is_ok();
let home = tempfile::tempdir().unwrap();
let out = forjar()
.arg("store")
.arg("list")
.env_remove("FORJAR_STORE")
.env("HOME", home.path())
.env("XDG_DATA_HOME", home.path().join("data"))
.output()
.expect("forjar must run");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
if system_writable {
// Privileged/system install: unchanged behaviour is the requirement.
assert!(
out.status.success(),
"system store is writable but store list failed:\n{combined}"
);
} else {
assert!(
!combined.contains("/var/lib/forjar/store"),
"unprivileged process still resolved to the root-owned default, \
which is the GH-239 dead end:\n{combined}"
);
}
}
#[test]
fn store_gc_reaches_the_same_root_as_store_list() {
// `gc` and `list` took the store path by separate routes. If they disagree,
// `gc --dry-run` reports on a directory the user never writes to — a
// reclaim tool that observes something other than what it defends.
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("store");
std::fs::create_dir_all(&store).unwrap();
for args in [vec!["store", "list"], vec!["store", "gc", "--dry-run"]] {
let out = forjar()
.args(&args)
.env("FORJAR_STORE", &store)
.output()
.expect("forjar must run");
assert!(
out.status.success(),
"`forjar {}` failed against a writable store root:\nstdout: {}\nstderr: {}",
args.join(" "),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn an_absent_store_reads_as_empty_not_as_an_error() {
// The first run. The store is created by the first import, not by listing
// it, so `list` and `gc` on a not-yet-existing store must report an empty
// store. Reporting ENOENT failed the operator at exactly the moment they
// were checking whether the store was safe to enable.
let dir = tempfile::tempdir().unwrap();
let never_created = dir.path().join("no-store-here");
assert!(!never_created.exists());
for args in [vec!["store", "list"], vec!["store", "gc", "--dry-run"]] {
let out = forjar()
.args(&args)
.env("FORJAR_STORE", &never_created)
.output()
.expect("forjar must run");
assert!(
out.status.success(),
"`forjar {}` treated an absent store as an error:\nstdout: {}\nstderr: {}",
args.join(" "),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
// And listing must not have created it as a side effect: a read is a read.
assert!(
!never_created.exists(),
"listing an absent store created it; reads must not mutate the store"
);
}
#[test]
fn a_permission_error_is_still_an_error() {
// The ENOENT-is-empty rule must not swallow the case the operator has to
// know about. Guarded on the probe actually being unreadable, so this does
// not silently pass when run as root.
let dir = tempfile::tempdir().unwrap();
let locked = dir.path().join("locked");
std::fs::create_dir_all(&locked).unwrap();
let mut perms = std::fs::metadata(&locked).unwrap().permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o000);
std::fs::set_permissions(&locked, perms).unwrap();
let readable = std::fs::read_dir(&locked).is_ok();
if readable {
// Running as root, or on a filesystem that ignores mode bits.
return;
}
let out = forjar()
.args(["store", "list"])
.env("FORJAR_STORE", &locked)
.output()
.expect("forjar must run");
assert!(
!out.status.success(),
"an unreadable store must be an error, not silently empty"
);
}