1use crate::intent::{Intent, Phase};
4use crate::{Invalid, Label, Operation, Request, Response, MAX_MESSAGE_BYTES};
5use std::fs::{self, File, OpenOptions};
6use std::io::{Read, Write};
7use std::os::fd::AsRawFd;
8use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
9use std::path::{Path, PathBuf};
10use std::time::{Duration, Instant};
11
12const RECORD: &str = "intent.json";
13
14#[cfg(test)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum CommitStage {
17 FileSync,
18 Publish,
19 DirectorySync,
20}
21type Result<T> = std::result::Result<T, Invalid>;
22
23fn io<T>(value: std::io::Result<T>) -> Result<T> {
24 value.map_err(|_| Invalid("intent store I/O failure; retain state"))
25}
26
27pub struct Store {
31 root: PathBuf,
32 directory: File,
33 expected: Request,
34 bytes: Option<Vec<u8>>,
35 intent: Option<Intent>,
36 poisoned: bool,
37 prepared_verified: bool,
38}
39
40#[must_use]
58pub struct DispatchAuthorization<'a> {
59 store: &'a mut Store,
60}
61
62impl DispatchAuthorization<'_> {
63 pub fn request(&self) -> &Request {
64 &self.store.expected
65 }
66
67 pub fn observe(self, response: Option<&Response>) -> Result<()> {
70 self.store.observe_dispatch(response)
71 }
72}
73
74fn private(metadata: &fs::Metadata) -> Result<()> {
75 if metadata.uid() != unsafe { libc::geteuid() } || metadata.mode() & 0o077 != 0 {
77 return Err(Invalid("intent store must be private to the current user"));
78 }
79 Ok(())
80}
81
82impl Store {
83 pub fn open(
84 root: &Path,
85 expected: &Request,
86 timeout: Duration,
87 cancelled: impl Fn() -> bool,
88 ) -> Result<Self> {
89 expected.validate()?;
90 if !matches!(expected.operation, Operation::Remove { .. }) {
91 return Err(Invalid("intent store requires a removal request"));
92 }
93 if timeout.is_zero() || timeout > Duration::from_secs(30) {
94 return Err(Invalid("invalid intent lock timeout"));
95 }
96 if !root.is_absolute() || io(fs::canonicalize(root))? != root {
97 return Err(Invalid("intent directory must be canonical and absolute"));
98 }
99 let directory = io(OpenOptions::new()
100 .read(true)
101 .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
102 .open(root))?;
103 private(&io(directory.metadata())?)?;
104 lock(&directory, timeout, &cancelled)?;
105 let mut store = Self {
106 root: root.into(),
107 directory,
108 expected: expected.clone(),
109 bytes: None,
110 intent: None,
111 poisoned: false,
112 prepared_verified: false,
113 };
114 store.check_directory()?;
115 let bytes = store.read()?;
116 store.intent = bytes
117 .as_deref()
118 .map(|bytes| Intent::restore(bytes, expected))
119 .transpose()?;
120 store.bytes = bytes;
121 if cancelled() {
122 return Err(Invalid("intent open cancelled"));
123 }
124 Ok(store)
125 }
126
127 pub fn phase(&self) -> Option<Phase> {
129 self.intent.as_ref().map(Intent::phase)
130 }
131
132 pub fn prepare(&mut self, inspection: &Response, labels: &[Label]) -> Result<()> {
133 if self.intent.is_some() {
134 return Err(Invalid("intent already exists"));
135 }
136 let next = Intent::prepare(&self.expected, inspection, labels)?;
137 self.commit(next)?;
138 self.prepared_verified = true;
139 Ok(())
140 }
141
142 pub fn resume_prepared(&mut self, inspection: &Response, labels: &[Label]) -> Result<()> {
146 self.prepared_verified = false;
147 if self.current()?.phase() != Phase::Prepared {
148 return Err(Invalid("intent is not prepared for ownership revalidation"));
149 }
150 Intent::prepare(&self.expected, inspection, labels)?;
151 if self.read()? != self.bytes {
152 self.poisoned = true;
153 return Err(Invalid("intent changed outside its held lock"));
154 }
155 self.prepared_verified = true;
156 Ok(())
157 }
158
159 pub fn begin_dispatch(&mut self) -> Result<DispatchAuthorization<'_>> {
161 if !self.prepared_verified {
162 return Err(Invalid("fresh prepared ownership verification required"));
163 }
164 let next = self.current()?.begin_dispatch(&self.expected)?;
165 self.prepared_verified = false;
166 self.commit(next)?;
167 Ok(DispatchAuthorization { store: self })
168 }
169
170 pub fn recovery_request(&self) -> Result<Request> {
173 if self.current()?.phase() != Phase::Unknown {
174 return Err(Invalid("intent is not awaiting reconciliation"));
175 }
176 let Operation::Remove { object_id } = &self.expected.operation else {
177 return Err(Invalid("intent requires removal"));
178 };
179 Ok(Request {
180 binding: self.expected.binding.clone(),
181 operation: Operation::Inspect {
182 object_id: object_id.clone(),
183 },
184 })
185 }
186
187 pub fn observe_dispatch(&mut self, response: Option<&Response>) -> Result<()> {
188 let next = self.current()?.observe_dispatch(&self.expected, response)?;
189 self.commit(next)
190 }
191
192 pub fn reconcile(&mut self, inspection: &Response) -> Result<()> {
193 let next = self.current()?.reconcile(&self.expected, inspection)?;
194 self.commit(next)
195 }
196
197 fn current(&self) -> Result<&Intent> {
198 if self.poisoned {
199 return Err(Invalid("intent store requires reopen after failed commit"));
200 }
201 self.intent.as_ref().ok_or(Invalid("intent not prepared"))
202 }
203
204 fn check_directory(&self) -> Result<()> {
205 let held = io(self.directory.metadata())?;
206 let named = io(fs::symlink_metadata(&self.root))?;
207 private(&held)?;
208 if !named.is_dir() || held.dev() != named.dev() || held.ino() != named.ino() {
209 return Err(Invalid("intent directory identity changed"));
210 }
211 Ok(())
212 }
213
214 fn read(&self) -> Result<Option<Vec<u8>>> {
215 self.check_directory()?;
216 let file = match OpenOptions::new()
217 .read(true)
218 .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC)
219 .open(self.root.join(RECORD))
220 {
221 Ok(file) => file,
222 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
223 Err(_) => return Err(Invalid("cannot open intent record")),
224 };
225 let metadata = io(file.metadata())?;
226 private(&metadata)?;
227 if !metadata.is_file() || metadata.nlink() != 1 {
228 return Err(Invalid(
229 "intent record must be a private regular single-link file",
230 ));
231 }
232 let mut bytes = Vec::new();
233 io(file
234 .take(MAX_MESSAGE_BYTES as u64 + 1)
235 .read_to_end(&mut bytes))?;
236 if bytes.len() > MAX_MESSAGE_BYTES {
237 return Err(Invalid("intent record exceeds size limit"));
238 }
239 Ok(Some(bytes))
240 }
241
242 #[cfg(test)]
243 fn fail_at(&self, stage: CommitStage) -> Result<()> {
244 if commit_failpoint::take(self, stage) {
245 return Err(Invalid("injected intent commit failure"));
246 }
247 Ok(())
248 }
249
250 fn commit(&mut self, next: Intent) -> Result<()> {
251 if self.poisoned {
252 return Err(Invalid("intent store requires reopen after failed commit"));
253 }
254 self.poisoned = true;
257 if self.read()? != self.bytes {
258 return Err(Invalid("intent changed outside its held lock"));
259 }
260 let bytes = serde_json::to_vec(&next).map_err(|_| Invalid("cannot serialize intent"))?;
261 if bytes.len() > MAX_MESSAGE_BYTES {
262 return Err(Invalid("intent record exceeds size limit"));
263 }
264 let mut temporary = io(tempfile::NamedTempFile::new_in(&self.root))?;
265 io(temporary.write_all(&bytes))?;
266 #[cfg(test)]
267 self.fail_at(CommitStage::FileSync)?;
268 io(temporary.as_file().sync_all())?;
269 self.check_directory()?;
270 #[cfg(test)]
271 self.fail_at(CommitStage::Publish)?;
272 if self.bytes.is_none() {
273 temporary
274 .persist_noclobber(self.root.join(RECORD))
275 .map_err(|_| Invalid("cannot create intent record; reopen required"))?;
276 } else {
277 temporary
278 .persist(self.root.join(RECORD))
279 .map_err(|_| Invalid("cannot commit intent record; reopen required"))?;
280 }
281 #[cfg(test)]
282 self.fail_at(CommitStage::DirectorySync)?;
283 io(self.directory.sync_all())?;
284 self.bytes = Some(bytes);
285 self.intent = Some(next);
286 self.poisoned = false;
287 Ok(())
288 }
289}
290
291fn lock(directory: &File, timeout: Duration, cancelled: &impl Fn() -> bool) -> Result<()> {
292 let started = Instant::now();
293 loop {
294 if cancelled() {
295 return Err(Invalid("intent lock cancelled"));
296 }
297 if started.elapsed() >= timeout {
298 return Err(Invalid("intent lock deadline exceeded"));
299 }
300 if unsafe { libc::flock(directory.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0 {
302 return Ok(());
303 }
304 let error = std::io::Error::last_os_error();
305 if error.kind() != std::io::ErrorKind::WouldBlock
306 && error.kind() != std::io::ErrorKind::Interrupted
307 {
308 return Err(Invalid("cannot lock intent directory"));
309 }
310 std::thread::sleep(
311 Duration::from_millis(10).min(timeout.saturating_sub(started.elapsed())),
312 );
313 }
314}
315
316#[cfg(test)]
319mod commit_failpoint {
320 use super::*;
321 use std::sync::Mutex;
322
323 #[derive(PartialEq, Eq)]
324 struct Key {
325 root: PathBuf,
326 device: u64,
327 inode: u64,
328 }
329
330 static ARMED: Mutex<Option<(Key, CommitStage)>> = Mutex::new(None);
333
334 fn key(store: &Store) -> Key {
335 let metadata = store.directory.metadata().expect("test directory metadata");
336 Key {
337 root: store.root.clone(),
338 device: metadata.dev(),
339 inode: metadata.ino(),
340 }
341 }
342
343 pub(super) fn arm(store: &Store, stage: CommitStage) {
344 *ARMED.lock().expect("test failpoint lock") = Some((key(store), stage));
345 }
346
347 pub(super) fn take(store: &Store, stage: CommitStage) -> bool {
348 let mut armed = ARMED.lock().expect("test failpoint lock");
349 if armed.as_ref() == Some(&(key(store), stage)) {
350 *armed = None;
351 true
352 } else {
353 false
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::intent::Intent;
362 use crate::{Binding, Engine, Namespace, Object, Outcome, Protocol, Response};
363 use serde_json::json;
364 use std::io::{BufRead, BufReader};
365 use std::os::unix::fs::PermissionsExt;
366 use std::process::{Child, Command, Stdio};
367 use tempfile::TempDir;
368
369 fn id() -> String {
373 "a".repeat(64)
374 }
375
376 fn binding() -> Binding {
377 Binding {
378 protocol: Protocol::V1,
379 adapter_sha256: "b".repeat(64),
380 adapter_version: "1.0.0".into(),
381 invocation: "inv-1".into(),
382 operation_key: "op-1".into(),
383 generation: "gen-1".into(),
384 namespace: Namespace {
385 engine: Engine::Docker,
386 endpoint: "unix:///run/docker.sock".into(),
387 context: "ctx-1".into(),
388 principal: "uid:1000".into(),
389 rootless: true,
390 scope: "scope-1".into(),
391 },
392 }
393 }
394
395 fn removal() -> Request {
396 Request {
397 binding: binding(),
398 operation: Operation::Remove { object_id: id() },
399 }
400 }
401
402 fn labels() -> Vec<Label> {
403 vec![Label {
404 key: "harness-gate.owner".into(),
405 value: "harness-gate".into(),
406 }]
407 }
408
409 fn present() -> Response {
410 Response {
411 binding: binding(),
412 operation: Operation::Inspect { object_id: id() },
413 outcome: Outcome::Present {
414 object: Object {
415 object_id: id(),
416 labels: labels(),
417 },
418 },
419 }
420 }
421
422 fn absent() -> Response {
423 Response {
424 binding: binding(),
425 operation: Operation::Inspect { object_id: id() },
426 outcome: Outcome::Absent,
427 }
428 }
429
430 fn private_root() -> (TempDir, PathBuf) {
431 let temporary = tempfile::tempdir().unwrap();
432 let directory = temporary.path().join("intent");
433 fs::create_dir(&directory).unwrap();
434 fs::set_permissions(&directory, fs::Permissions::from_mode(0o700)).unwrap();
435 (temporary, fs::canonicalize(directory).unwrap())
436 }
437
438 fn open(root: &Path) -> Store {
439 Store::open(root, &removal(), Duration::from_secs(1), || false).unwrap()
440 }
441
442 fn prepared_record() -> Vec<u8> {
443 serde_json::to_vec(&Intent::prepare(&removal(), &present(), &labels()).unwrap()).unwrap()
444 }
445
446 fn write_record(root: &Path, bytes: &[u8]) {
447 fs::write(root.join(RECORD), bytes).unwrap();
448 fs::set_permissions(root.join(RECORD), fs::Permissions::from_mode(0o600)).unwrap();
449 }
450
451 fn prepare(store: &mut Store) {
452 store.prepare(&present(), &labels()).unwrap();
453 }
454
455 fn open_prepared(root: &Path) {
456 let mut store = open(root);
457 prepare(&mut store);
458 }
459
460 fn binding_mutations() -> Vec<Binding> {
461 let mut values = Vec::new();
462 let mutations: [fn(&mut Binding); 11] = [
463 |b| b.adapter_sha256 = "c".repeat(64),
464 |b| b.adapter_version = "2".into(),
465 |b| b.invocation = "inv-2".into(),
466 |b| b.operation_key = "op-2".into(),
467 |b| b.generation = "gen-2".into(),
468 |b| b.namespace.engine = Engine::Podman,
469 |b| b.namespace.endpoint = "unix:///other".into(),
470 |b| b.namespace.context = "ctx-2".into(),
471 |b| b.namespace.principal = "uid:1001".into(),
472 |b| b.namespace.rootless = false,
473 |b| b.namespace.scope = "scope-2".into(),
474 ];
475 for mutate in mutations {
476 let mut value = binding();
477 mutate(&mut value);
478 values.push(value);
479 }
480 values
481 }
482
483 struct LockChild(Child);
484
485 impl Drop for LockChild {
486 fn drop(&mut self) {
487 let _ = self.0.kill();
488 let _ = self.0.wait();
489 }
490 }
491
492 fn hold_lock(root: &Path) -> LockChild {
493 let mut child = Command::new("python3")
494 .args([
495 "-c",
496 "import fcntl,os,sys,time\nfd=os.open(sys.argv[1],os.O_RDONLY|os.O_DIRECTORY)\nfcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)\nprint('ready',flush=True)\ntime.sleep(30)",
497 ])
498 .arg(root)
499 .stdout(Stdio::piped())
500 .spawn()
501 .unwrap();
502 let stdout = child.stdout.take().unwrap();
503 let mut line = String::new();
504 BufReader::new(stdout).read_line(&mut line).unwrap();
505 assert_eq!(line.trim(), "ready");
506 LockChild(child)
507 }
508
509 #[test]
510 fn prepare_unknown_reopen_reconcile_completed_cycle() {
511 let (_temporary, root) = private_root();
512 {
513 let mut store = open(&root);
514 assert_eq!(store.phase(), None);
515 assert!(store.begin_dispatch().is_err());
516 prepare(&mut store);
517 assert_eq!(store.phase(), Some(Phase::Prepared));
518 assert!(store.prepare(&present(), &labels()).is_err());
519 }
520 {
521 let mut store = open(&root);
522 assert_eq!(store.phase(), Some(Phase::Prepared));
523 store.resume_prepared(&present(), &labels()).unwrap();
524 let authorization = store.begin_dispatch().unwrap();
525 drop(authorization);
526 assert_eq!(store.phase(), Some(Phase::Unknown));
527 }
528 let mut store = open(&root);
529 assert_eq!(store.phase(), Some(Phase::Unknown));
530 store.observe_dispatch(None).unwrap();
531 assert_eq!(store.phase(), Some(Phase::Unknown));
532 store.reconcile(&absent()).unwrap();
533 assert_eq!(store.phase(), Some(Phase::Completed));
534 assert!(store.begin_dispatch().is_err());
535 assert!(store.observe_dispatch(None).is_err());
536 assert!(store.reconcile(&absent()).is_err());
537 drop(store);
538 let store = open(&root);
539 assert_eq!(store.phase(), Some(Phase::Completed));
540 }
541
542 #[test]
543 fn conflicting_expected_is_rejected() {
544 let (_temporary, root) = private_root();
545 open_prepared(&root);
546 for changed in binding_mutations() {
547 let expected = Request {
548 binding: changed,
549 operation: removal().operation,
550 };
551 assert!(Store::open(&root, &expected, Duration::from_secs(1), || false).is_err());
552 }
553 let expected = Request {
554 binding: binding(),
555 operation: Operation::Remove {
556 object_id: "b".repeat(64),
557 },
558 };
559 assert!(Store::open(&root, &expected, Duration::from_secs(1), || false).is_err());
560 }
561
562 #[test]
563 fn lock_timeout_and_cancel_are_bounded_in_a_subprocess() {
564 let (_temporary, root) = private_root();
565 let child = hold_lock(&root);
566 assert!(Store::open(&root, &removal(), Duration::from_millis(100), || false).is_err());
567 drop(child);
568 let store = open(&root);
569 drop(store);
570
571 let child = hold_lock(&root);
572 let calls = std::cell::Cell::new(0_u32);
573 let result = Store::open(&root, &removal(), Duration::from_secs(5), || {
574 let current = calls.get();
575 calls.set(current + 1);
576 current > 0
577 });
578 assert!(result.is_err());
579 drop(child);
580 }
581
582 #[test]
583 fn record_inputs_reject_links_fifo_modes_size_and_legacy() {
584 type Case = (&'static str, fn(&Path));
585 let cases: [Case; 6] = [
586 ("symlink", |root| {
587 let target = root.join("record-target");
588 write_record(root, &prepared_record());
589 fs::rename(root.join(RECORD), &target).unwrap();
590 std::os::unix::fs::symlink(&target, root.join(RECORD)).unwrap();
591 }),
592 ("hardlink", |root| {
593 let target = root.join("record-target");
594 write_record(root, &prepared_record());
595 fs::hard_link(root.join(RECORD), &target).unwrap();
596 }),
597 ("fifo", |root| {
598 let status = Command::new("mkfifo")
599 .arg(root.join(RECORD))
600 .status()
601 .unwrap();
602 assert!(status.success());
603 }),
604 ("mode", |root| {
605 write_record(root, &prepared_record());
606 fs::set_permissions(root.join(RECORD), fs::Permissions::from_mode(0o644)).unwrap();
607 }),
608 ("oversize", |root| {
609 let mut bytes = prepared_record();
610 bytes.extend(std::iter::repeat_n(b' ', MAX_MESSAGE_BYTES));
611 write_record(root, &bytes);
612 }),
613 ("legacy-v2", |root| {
614 write_record(
615 root,
616 &serde_json::to_vec(&json!({
617 "owner_marker": "harness-gate",
618 "schema_version": 2,
619 "runtime": "docker",
620 "runtime_object_id": id()
621 }))
622 .unwrap(),
623 );
624 }),
625 ];
626 for (name, setup) in cases {
627 let (_temporary, root) = private_root();
628 setup(&root);
629 assert!(
630 Store::open(&root, &removal(), Duration::from_secs(1), || false).is_err(),
631 "{name}"
632 );
633 }
634 }
635
636 #[test]
637 fn record_change_and_directory_rebind_are_rejected() {
638 let (_temporary, root) = private_root();
639 open_prepared(&root);
640 let mut store = open(&root);
641 store.resume_prepared(&present(), &labels()).unwrap();
642 fs::write(root.join(RECORD), b"{}").unwrap();
643 assert!(store.begin_dispatch().is_err());
644 assert!(store.reconcile(&absent()).is_err());
645 drop(store);
646
647 let (_temporary, root) = private_root();
648 let moved = root.parent().unwrap().join("moved");
649 let mut store = open(&root);
650 fs::rename(&root, &moved).unwrap();
651 fs::create_dir(&root).unwrap();
652 fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).unwrap();
653 assert!(store.prepare(&present(), &labels()).is_err());
654 }
655
656 #[test]
657 fn reopened_prepared_requires_resume_and_failed_resume_clears_gate() {
658 let (_temporary, root) = private_root();
659 open_prepared(&root);
660 let mut store = open(&root);
661 assert_eq!(store.phase(), Some(Phase::Prepared));
662 assert!(store.begin_dispatch().is_err());
663 let mut wrong_id = present();
664 if let Outcome::Present { object } = &mut wrong_id.outcome {
665 object.object_id = "b".repeat(64);
666 }
667 assert!(store.resume_prepared(&wrong_id, &labels()).is_err());
668 assert!(store.begin_dispatch().is_err());
669 let wrong_labels = [Label {
670 key: "harness-gate.owner".into(),
671 value: "other".into(),
672 }];
673 assert!(store.resume_prepared(&present(), &wrong_labels).is_err());
674 assert!(store.begin_dispatch().is_err());
675 assert!(store.resume_prepared(&present(), &labels()).is_ok());
676 let authorization = store.begin_dispatch().unwrap();
677 drop(authorization);
678 assert_eq!(store.phase(), Some(Phase::Unknown));
679 }
680
681 #[test]
682 fn dispatch_authorization_binds_request_and_drop_leaves_unknown() {
683 let (_temporary, root) = private_root();
684 let mut store = open(&root);
685 prepare(&mut store);
686 let authorization = store.begin_dispatch().unwrap();
687 assert_eq!(authorization.request(), &removal());
688 drop(authorization);
689 assert_eq!(store.phase(), Some(Phase::Unknown));
690 assert_eq!(store.phase(), Some(Phase::Unknown));
691 assert!(store.begin_dispatch().is_err());
692 let recovery = store.recovery_request().unwrap();
693 assert_eq!(recovery.binding, binding());
694 assert_eq!(recovery.operation, Operation::Inspect { object_id: id() });
695 store.reconcile(&absent()).unwrap();
696 assert_eq!(store.phase(), Some(Phase::Completed));
697 assert!(store.recovery_request().is_err());
698 }
699
700 #[test]
701 fn recovery_request_is_unknown_only_and_exact() {
702 let (_temporary, root) = private_root();
703 let mut store = open(&root);
704 prepare(&mut store);
705 assert!(store.recovery_request().is_err());
706 let authorization = store.begin_dispatch().unwrap();
707 drop(authorization);
708 let recovery = store.recovery_request().unwrap();
709 assert_eq!(recovery.binding, binding());
710 assert_eq!(recovery.operation, Operation::Inspect { object_id: id() });
711 assert!(store.reconcile(&present()).is_err());
712 assert_eq!(store.phase(), Some(Phase::Unknown));
713 store.reconcile(&absent()).unwrap();
714 assert_eq!(store.phase(), Some(Phase::Completed));
715 assert!(store.recovery_request().is_err());
716 }
717
718 #[test]
719 fn commit_failure_stages_issue_no_token_and_poison() {
720 for (stage, expected_phase) in [
721 (CommitStage::FileSync, Phase::Prepared),
722 (CommitStage::Publish, Phase::Prepared),
723 (CommitStage::DirectorySync, Phase::Unknown),
724 ] {
725 let (_temporary, root) = private_root();
726 open_prepared(&root);
727 let mut store = open(&root);
728 store.resume_prepared(&present(), &labels()).unwrap();
729 commit_failpoint::arm(&store, stage);
730 assert!(store.begin_dispatch().is_err());
731 assert!(store.resume_prepared(&present(), &labels()).is_err());
732 assert!(store.observe_dispatch(None).is_err());
733 drop(store);
734 let store = open(&root);
735 assert_eq!(store.phase(), Some(expected_phase), "{stage:?}");
736 }
737 }
738
739 #[test]
740 fn initial_noclobber_and_commit_failure_poison_until_reopen() {
741 let (_temporary, root) = private_root();
742 let mut store = open(&root);
743 write_record(&root, &prepared_record());
744 assert!(store.prepare(&present(), &labels()).is_err());
745
746 let (_temporary, root) = private_root();
747 let mut store = open(&root);
748 fs::set_permissions(&root, fs::Permissions::from_mode(0o500)).unwrap();
749 assert!(store.prepare(&present(), &labels()).is_err());
750 fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).unwrap();
751 assert!(store.begin_dispatch().is_err());
752 assert!(store.prepare(&present(), &labels()).is_err());
753 drop(store);
754 let mut store = open(&root);
755 assert_eq!(store.phase(), None);
756 prepare(&mut store);
757 assert_eq!(store.phase(), Some(Phase::Prepared));
758 }
759}