Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/guard.rs

15.9 KiB, 1 run

created by r2848102244:740, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The guard that will not let a credential into a history nothing leaves.
2//!
3//! Every key here is spelled in two pieces and joined at run time, so that the scanners which read
4//! this very file -- the git pre-commit hook, and the guard under test -- find nothing in it.
5//!
6//! The refusal is only worth having if the same fixture would otherwise be recorded, so each test
7//! that asserts a refusal is paired with one asserting that the store is clean afterwards, and the
8//! marker test asserts the opposite: that the excused line does reach a segment. Two states the
9//! tests can tell apart is the whole point of them.
10
11mod support;
12
13use support::{
14 ore,
15 write,
16 Scratch,
17};
18
19use oxedyne_fe2o3_core::prelude::*;
20
21use std::fs;
22use std::os::unix::fs::MetadataExt;
23use std::path::Path;
24use std::process::Command;
25
26
27// A key of a shape that is a credential and nothing else, and a second one so that a test can add
28// a credential to a file already holding one. Both in two pieces.
29const KEY: (&str, &str) = ("fw", "_3ZjKq81mAbCdEfGhIjKlMnOpQrSt");
30const OTHER: (&str, &str) = ("sk-ant", "-api03-AbCdEfGhIjKlMnOpQrStUvWx");
31
32
33/// The whole of a key, as it stands in a file.
34fn key(pieces: (&str, &str)) -> String {
35 fmt!("{}{}", pieces.0, pieces.1)
36}
37
38/// A line of source assigning one.
39fn line(pieces: (&str, &str)) -> String {
40 fmt!("let key = \"{}\";\n", key(pieces))
41}
42
43/// Does the store hold these bytes anywhere at all?
44///
45/// The question the whole guard exists to answer no to. Asked of the bytes on disk rather than
46/// through a verb, because there is no verb that does not capture: asking one would write.
47fn in_store(root: &Path, needle: &[u8])
48 -> Outcome<bool>
49{
50 found(&root.join(".ore"), needle)
51}
52
53/// Reads a directory, recursing, looking for the bytes.
54fn found(dir: &Path, needle: &[u8])
55 -> Outcome<bool>
56{
57 for entry in res!(fs::read_dir(dir)) {
58 let entry = res!(entry);
59 let kind = res!(entry.file_type());
60 if kind.is_dir() {
61 if res!(found(&entry.path(), needle)) {
62 return Ok(true);
63 }
64 continue;
65 }
66 if !kind.is_file() {
67 continue;
68 }
69 let bytes = res!(fs::read(entry.path()));
70 if bytes.windows(needle.len()).any(|w| w == needle) {
71 return Ok(true);
72 }
73 }
74 Ok(false)
75}
76
77/// Prepares a repository holding one ordinary file, with everything captured.
78fn started(scratch: &Scratch, name: &str)
79 -> Outcome<std::path::PathBuf>
80{
81 let root = res!(scratch.sub(name));
82 res!(write(&root, "main.rs", b"fn main() {\n\tprintln!(\"hello\");\n}\n"));
83 res!(res!(ore(&root, &["init"])).good("init"));
84 Ok(root)
85}
86
87
88/// A key in the working copy refuses the next verb, and reaches no segment.
89#[test]
90fn a_credential_refuses_the_verb_and_is_not_recorded() -> Outcome<()> {
91 let scratch = res!(Scratch::new("guard_refuse"));
92 let root = res!(started(&scratch, "repo"));
93 res!(write(&root, "main.rs",
94 fmt!("fn main() {{}}\n{}", line(KEY)).as_bytes()));
95
96 let ran = res!(ore(&root, &["log"]));
97 assert!(!ran.ok, "the verb was refused: {}{}", ran.out, ran.err);
98 assert!(ran.err.contains("main.rs:2"),
99 "and it names the file and the line: {}", ran.err);
100 assert!(ran.err.contains("Fireworks key"),
101 "and what it found: {}", ran.err);
102 assert!(!ran.err.contains(&key(KEY)),
103 "and never the value itself: {}", ran.err);
104 assert!(!res!(in_store(&root, key(KEY).as_bytes())),
105 "and nothing of it reached the store");
106 Ok(())
107}
108
109/// Every verb refuses, because every verb captures.
110#[test]
111fn no_verb_is_a_way_round_it() -> Outcome<()> {
112 let scratch = res!(Scratch::new("guard_verbs"));
113 let root = res!(started(&scratch, "repo"));
114 res!(write(&root, "main.rs", fmt!("fn main() {{}}\n{}", line(KEY)).as_bytes()));
115
116 for verb in [
117 vec!["log"],
118 vec!["flags"],
119 vec!["who", "main.rs"],
120 vec!["mark", "here"],
121 vec!["undo"],
122 ] {
123 let ran = res!(ore(&root, &verb));
124 assert!(!ran.ok, "`ore {}` was refused: {}{}", verb.join(" "), ran.out, ran.err);
125 }
126 assert!(!res!(in_store(&root, key(KEY).as_bytes())),
127 "and none of them recorded it");
128 Ok(())
129}
130
131/// The marker excuses the line, and taking it away un-excuses it.
132///
133/// The half that proves the rest: the excused line is captured, so the fixture really is one the
134/// guard would otherwise have stopped.
135#[test]
136fn the_marker_excuses_a_line_and_only_while_it_is_there() -> Outcome<()> {
137 let scratch = res!(Scratch::new("guard_marker"));
138 let root = res!(started(&scratch, "repo"));
139 let marked = fmt!("fn main() {{}}\nlet key = \"{}\"; // allowlist secret\n", key(KEY));
140 res!(write(&root, "main.rs", marked.as_bytes()));
141
142 let ran = res!(ore(&root, &["log"]));
143 assert!(ran.ok, "a fixture the author marked is captured: {}{}", ran.out, ran.err);
144 assert!(res!(in_store(&root, key(KEY).as_bytes())),
145 "and it is in the store, which is what makes the refusals elsewhere mean something");
146
147 // The same bytes without the marker are a line the history has never seen.
148 res!(write(&root, "main.rs", fmt!("fn main() {{}}\n{}", line(KEY)).as_bytes()));
149 let ran = res!(ore(&root, &["log"]));
150 assert!(!ran.ok, "and without it the line is refused again: {}{}", ran.out, ran.err);
151 Ok(())
152}
153
154/// A placeholder is not a credential.
155///
156/// The rule that decides whether the guard survives the week. A documentation example refused is a
157/// guard somebody switches off, and a guard switched off protects nothing.
158#[test]
159fn a_placeholder_is_not_refused() -> Outcome<()> {
160 let scratch = res!(Scratch::new("guard_placeholder"));
161 let root = res!(started(&scratch, "repo"));
162 res!(write(&root, "README.md", b"Set it yourself:\n\n api_key = \"your-key-here\"\n"));
163 res!(write(&root, "cfg.toml", b"token = \"PLACEHOLDER_VALUE_HERE_OK\"\n"));
164
165 let ran = res!(ore(&root, &["log"]));
166 assert!(ran.ok, "an example nobody has filled in is captured: {}{}", ran.out, ran.err);
167 Ok(())
168}
169
170/// A lockfile is not scanned, because its hashes read like keys.
171#[test]
172fn a_lockfile_is_left_alone() -> Outcome<()> {
173 let scratch = res!(Scratch::new("guard_lock"));
174 let root = res!(started(&scratch, "repo"));
175 res!(write(&root, "Cargo.lock", fmt!("checksum = \"{}\"\n", key(KEY)).as_bytes()));
176
177 let ran = res!(ore(&root, &["log"]));
178 assert!(ran.ok, "the lockfile is captured: {}{}", ran.out, ran.err);
179 Ok(())
180}
181
182/// A marker on the line above, taken away, leaves the line itself unchanged -- and unchanged is
183/// what the history already holds.
184///
185/// The other half of the marker's story, and deliberately not the same answer as
186/// [`the_marker_excuses_a_line_and_only_while_it_is_there`]. Taking a *trailing* marker off a line
187/// rewrites that line, so it is a line the history has never seen and it is refused. Taking away a
188/// marker that sat on the line *above* leaves the credential line byte for byte as the history
189/// already carries it, so it is not what this command would be introducing.
190///
191/// The asymmetry is the price of matching on the exact line, and the exact line is worth the
192/// price: anything looser -- the same file, the same shape of credential -- would let the value
193/// itself be edited freely, which is a new key every time.
194#[test]
195fn dropping_a_marker_from_the_line_above_is_not_introducing_the_line() -> Outcome<()> {
196 let scratch = res!(Scratch::new("guard_above"));
197 let root = res!(started(&scratch, "repo"));
198 res!(write(&root, "lib.rs",
199 fmt!("one\ntwo\n// allowlist secret\n{}", line(KEY)).as_bytes()));
200 let ran = res!(ore(&root, &["log"]));
201 assert!(ran.ok, "the marked fixture is captured: {}{}", ran.out, ran.err);
202 assert!(res!(in_store(&root, key(KEY).as_bytes())),
203 "and it is in the log, which is what the rest of this rests on");
204
205 res!(write(&root, "lib.rs", fmt!("one\ntwo\n{}", line(KEY)).as_bytes()));
206 let ran = res!(ore(&root, &["log"]));
207 assert!(ran.ok, "and dropping the marker line is captured: {}{}", ran.out, ran.err);
208 Ok(())
209}
210
211/// A credential a peer sent does not brick the file it landed in, and a new one still refuses.
212///
213/// This is the route the carve-out exists for. A capture is guarded and an import is guarded, so
214/// the one way a credential still reaches a log its owner did not write is `ore sync`: a peer's
215/// operations were authored and signed on their machine and arrive whole, and refusing one would
216/// be refusing their history. Nothing at this end can prevent it.
217///
218/// What this end can decide is what happens next. While the file is untouched nothing scans it at
219/// all -- the capture skips a file whose bytes are what the history says -- so the cost falls
220/// exactly where the carve-out is: on the owner's next edit to that file. Without it a peer could
221/// forbid, permanently and from across a relay, any further edit to any file they put a credential
222/// in.
223#[test]
224fn a_line_a_peer_sent_does_not_brick_the_file() -> Outcome<()> {
225 let scratch = res!(Scratch::new("guard_peer"));
226
227 // The peer arrives at an unmarked credential the only way anybody can: marked, captured, and
228 // then the marker taken off the line above it.
229 let peer = res!(scratch.sub("peer"));
230 res!(write(&peer, "lib.rs",
231 fmt!("one\ntwo\n// allowlist secret\n{}", line(KEY)).as_bytes()));
232 res!(res!(ore(&peer, &["init"])).good("init"));
233 res!(res!(ore(&peer, &["log"])).good("log"));
234 res!(write(&peer, "lib.rs", fmt!("one\ntwo\n{}", line(KEY)).as_bytes()));
235 res!(res!(ore(&peer, &["log"])).good("log"));
236
237 let mine = res!(started(&scratch, "mine"));
238 res!(res!(ore(&mine, &["sync", &fmt!("{}", peer.display())])).good("sync"));
239 assert!(res!(in_store(&mine, key(KEY).as_bytes())),
240 "the sync put the peer's credential in my log, which I had no way to refuse");
241 let landed = res!(fs::read(mine.join("lib.rs")));
242 assert!(landed.windows(key(KEY).len()).any(|w| w == key(KEY).as_bytes()),
243 "and in my working copy");
244
245 // Untouched, the file is never scanned, so this says nothing about the carve-out.
246 let ran = res!(ore(&mine, &["log"]));
247 assert!(ran.ok, "a verb with nothing changed runs: {}{}", ran.out, ran.err);
248
249 // Editing it is the moment the carve-out is the only thing standing between a peer and a file
250 // I can no longer touch.
251 res!(write(&mine, "lib.rs", fmt!("one\ntwo\nthree\n{}", line(KEY)).as_bytes()));
252 let ran = res!(ore(&mine, &["log"]));
253 assert!(ran.ok, "and my own edit to that file is captured: {}{}", ran.out, ran.err);
254
255 // And the carve-out is not a hole: a line the file did not already carry is refused.
256 res!(write(&mine, "lib.rs",
257 fmt!("one\ntwo\nthree\n{}{}", line(KEY), line(OTHER)).as_bytes()));
258 let ran = res!(ore(&mine, &["log"]));
259 assert!(!ran.ok, "a new credential in that same file is refused: {}{}", ran.out, ran.err);
260 assert!(ran.err.contains("Anthropic key"), "and named: {}", ran.err);
261 assert!(!res!(in_store(&mine, key(OTHER).as_bytes())),
262 "and it reached no segment");
263 Ok(())
264}
265
266/// Runs `touch` with the arguments given, which is how a test forges a timestamp.
267///
268/// There is no way to set a modification time through `std`, and this file will not take a
269/// dependency for one. `touch` is the same tool somebody would reach for by hand, which is the
270/// point: the case being built is what a restore, a build system or a person with `touch -r` does.
271fn touch(dir: &Path, args: &[&str])
272 -> Outcome<()>
273{
274 let out = match Command::new("touch").current_dir(dir).args(args).output() {
275 Ok(o) => o,
276 Err(e) => return Err(err!(e, "`touch {}` could not be run.", args.join(" "); Test, IO)),
277 };
278 match out.status.success() {
279 true => Ok(()),
280 false => Err(err!("`touch {}` failed: {}", args.join(" "),
281 String::from_utf8_lossy(&out.stderr); Test, IO)),
282 }
283}
284
285/// Runs two verbs over an unchanged working copy, which is what leaves an index behind.
286///
287/// The first capture records what is there and writes no index -- a capture that appended
288/// something has moved the history and cannot say the disk matches it. The second appends nothing,
289/// and that is the one moment the index may be written.
290fn arm_the_index(root: &Path)
291 -> Outcome<()>
292{
293 res!(res!(ore(root, &["log"])).good("log"));
294 res!(res!(ore(root, &["log"])).good("log"));
295 assert!(root.join(".ore").join("stat").is_file(),
296 "the working copy index was not written, so this test is not exercising the fast path \
297 it exists for; if the index moved or was renamed, this precondition is what needs \
298 changing");
299 Ok(())
300}
301
302/// An index saying the working copy is clean does not carry a credential past the guard.
303///
304/// `capture` skips the render entirely where a stat index says every file is where it was, and the
305/// guard runs after the render. So the guard's whole contract now rests on a claim it does not
306/// make itself: that a file which arrived, or changed, is never one the index calls clean.
307///
308/// Three ways of arriving, and the third is the one worth the test. A same-size write with the
309/// modification time put back leaves the two fields a conventional index compares -- size and
310/// mtime -- exactly as they were recorded, which the assertions below check rather than assume. If
311/// that file is ever believed, a credential reaches a log that cannot forget it and no test but
312/// this one would notice.
313#[test]
314fn an_index_that_says_clean_does_not_carry_a_credential_past() -> Outcome<()> {
315 let scratch = res!(Scratch::new("guard_index"));
316 let root = res!(scratch.sub("repo"));
317 // Long enough to hold the credential later without changing size, and shaped like nothing.
318 let decoy = fmt!("let key = \"qq_{}\";\n", &key(KEY)[3..]);
319 res!(write(&root, "lib.rs", b"one\ntwo\n"));
320 res!(write(&root, "swap.rs", decoy.as_bytes()));
321 // Older than the index's own reading of the clock, so that the entries are believed rather
322 // than re-read for being too recent to mean anything.
323 res!(touch(&root, &["-d", "1 hour ago", "lib.rs", "swap.rs"]));
324 res!(res!(ore(&root, &["init"])).good("init"));
325 res!(arm_the_index(&root));
326
327 // A path the index has never heard of.
328 res!(write(&root, "arrived.rs", line(KEY).as_bytes()));
329 let ran = res!(ore(&root, &["log"]));
330 assert!(!ran.ok, "a file that arrived is refused: {}{}", ran.out, ran.err);
331 assert!(ran.err.contains("arrived.rs:1"), "and named: {}", ran.err);
332 res!(fs::remove_file(root.join("arrived.rs")));
333 res!(touch(&root, &["-d", "1 hour ago", "lib.rs", "swap.rs"]));
334 res!(arm_the_index(&root));
335
336 // A path it knows, taken away and put back under the same name.
337 res!(fs::remove_file(root.join("swap.rs")));
338 res!(write(&root, "swap.rs", line(KEY).as_bytes()));
339 let ran = res!(ore(&root, &["log"]));
340 assert!(!ran.ok, "a path removed and re-created is refused: {}{}", ran.out, ran.err);
341 assert!(ran.err.contains("swap.rs:1"), "and named: {}", ran.err);
342 res!(write(&root, "swap.rs", decoy.as_bytes()));
343 res!(touch(&root, &["-d", "1 hour ago", "lib.rs", "swap.rs"]));
344 res!(arm_the_index(&root));
345
346 // And the one a size and a time cannot tell apart: the same number of bytes, written in
347 // place so the inode is kept, with the modification time put back afterwards.
348 let before = res!(fs::metadata(root.join("swap.rs")));
349 let planted = line(KEY);
350 req!(planted.len(), decoy.len(), "the fixture only tests what it means to if the two lines \
351 are the same length");
352 // The reference the time is copied from lives OUTSIDE the working copy. Put it inside and it
353 // is a file that arrived, the index does not know it, and the capture takes the slow path for
354 // that reason alone -- which would leave this case proving nothing about the swap.
355 let stamp = scratch.path.join("stamp.ref");
356 res!(fs::write(&stamp, b""));
357 res!(touch(&root, &["-r", "swap.rs", &fmt!("{}", stamp.display())]));
358 res!(write(&root, "swap.rs", planted.as_bytes()));
359 res!(touch(&root, &["-r", &fmt!("{}", stamp.display()), "swap.rs"]));
360 let after = res!(fs::metadata(root.join("swap.rs")));
361 req!(after.len(), before.len(), "the size is what it was");
362 req!(after.mtime(), before.mtime(), "and so is the modification time, to the second");
363 req!(after.mtime_nsec(), before.mtime_nsec(), "and to the nanosecond");
364 req!(after.ino(), before.ino(), "and it is the same file, not a replacement");
365
366 let ran = res!(ore(&root, &["log"]));
367 assert!(!ran.ok,
368 "a credential written over the same number of bytes, with the time put back, is still \
369 refused: {}{}", ran.out, ran.err);
370 assert!(ran.err.contains("swap.rs:1"), "and named: {}", ran.err);
371 assert!(!res!(in_store(&root, key(KEY).as_bytes())),
372 "and none of the three reached a segment");
373 Ok(())
374}