返回 CodeWhale
windows_tests.rs
根目录 / crates / tui / src / extension_host / windows_tests.rs
1 //! Real Windows kernel/host receipts. These are not source-only substitutes.
2 use super::*;
3 use crate::config::ExtensionHostRuntime;
4 use crate::extension_host::supervisor::{HostSandbox, MemoryEnforcement};
5 use crate::extension_host::tests::{FixturePlugins, host_tool};
6 use crate::extension_host::{ExtensionHostManager, ExtensionHostOptions, HostStatus};
7 use crate::plugins::activation::TestPolicyGuard;
8 use crate::tools::spec::ToolContext;
9 use serde_json::json;
10
11 fn runtime(choice: ExtensionHostRuntime, override_path: Option<&Path>) -> Option<HostRuntime> {
12 let resolution = crate::dependencies::resolve_extension_host_runtime(
13 choice,
14 (choice == ExtensionHostRuntime::Node)
15 .then_some(override_path)
16 .flatten(),
17 (choice == ExtensionHostRuntime::Bun)
18 .then_some(override_path)
19 .flatten(),
20 );
21 match resolution.selected {
22 Some(runtime) => Some(runtime),
23 None if std::env::var_os("CODEWHALE_EXT_HOST_TESTS").is_some()
24 || std::env::var_os("CODEWHALE_EXT_HOST_BUN_TESTS").is_some() =>
25 {
26 panic!("required Windows runtime: {}", resolution.failure())
27 }
28 None => {
29 eprintln!(
30 "Windows runtime receipt unavailable: {}",
31 resolution.failure()
32 );
33 None
34 }
35 }
36 }
37
38 #[cfg(test)]
39 async fn owner_boundary(runtime: HostRuntime) {
40 let _policy = TestPolicyGuard::extension_host(true);
41 let fixture = FixturePlugins::new(&["secret-probe"]).await;
42 let choice = match runtime.kind {
43 crate::dependencies::HostRuntimeKind::Node => ExtensionHostRuntime::Node,
44 crate::dependencies::HostRuntimeKind::Bun => ExtensionHostRuntime::Bun,
45 };
46 let registry = fixture.registry();
47 let plugin = registry.get("secret-probe").unwrap();
48 let source = plugin.base_path.join("index.mjs");
49 let staged = plugin.staged_root.as_ref().unwrap().join("index.mjs");
50 assert_ne!(source, staged);
51 let manager = Arc::new(ExtensionHostManager::new(ExtensionHostOptions {
52 runtime: choice,
53 node_override: (choice == ExtensionHostRuntime::Node).then_some(runtime.path.clone()),
54 bun_override: (choice == ExtensionHostRuntime::Bun).then_some(runtime.path),
55 root: Some(fixture.root.clone()),
56 ..Default::default()
57 }));
58 let engine = manager.attach(registry);
59 engine.sync().await.unwrap();
60 let HostStatus::Ready {
61 sandbox, memory, ..
62 } = manager.status()
63 else {
64 panic!(
65 "Windows Native failed: {:?}; {:?}",
66 manager.status(),
67 manager.diagnostics()
68 );
69 };
70 assert_eq!(sandbox, HostSandbox::Wrapped("windows-lpac".into()));
71 assert_eq!(memory, MemoryEnforcement::JobObject);
72 let context = ToolContext::new(fixture.workspace()).with_plugin_registry(engine.plugin_view());
73 let read = host_tool(&engine, fixture.workspace(), "probe_read");
74 let write = host_tool(&engine, fixture.workspace(), "probe_write");
75 let staged_read = read
76 .execute(json!({"path":staged}), &context)
77 .await
78 .unwrap();
79 assert_eq!(
80 serde_json::from_str::<serde_json::Value>(&staged_read.content).unwrap()["ok"],
81 true,
82 "reviewed runtime root must be readable by the real Native module"
83 );
84 for path in [
85 source,
86 fixture.workspace().join("ungranted.txt"),
87 fixture
88 .root
89 .join("extension-host/builtin-data/private.json"),
90 ] {
91 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
92 if !path.exists() {
93 std::fs::write(&path, b"synthetic-private-control").unwrap();
94 }
95 let result = read.execute(json!({"path":path}), &context).await.unwrap();
96 let result: serde_json::Value = serde_json::from_str(&result.content).unwrap();
97 assert_eq!(
98 result["ok"], false,
99 "mutable/ungranted roots must remain unreadable"
100 );
101 assert!(
102 matches!(result["code"].as_str(), Some("EACCES" | "EPERM")),
103 "must be an access denial, not a missing file"
104 );
105 }
106 let data = fixture.root.join("extension-host/data/positive-write.txt");
107 let result = write.execute(json!({"path":data}), &context).await.unwrap();
108 assert_eq!(
109 serde_json::from_str::<serde_json::Value>(&result.content).unwrap()["ok"],
110 true
111 );
112 let denied = staged.parent().unwrap().join("forbidden-write.txt");
113 let result = write
114 .execute(json!({"path":denied}), &context)
115 .await
116 .unwrap();
117 assert_eq!(
118 serde_json::from_str::<serde_json::Value>(&result.content).unwrap()["ok"],
119 false
120 );
121 assert!(!denied.exists(), "readonly snapshot was modified");
122 engine.set_plugins(fixture.disable("secret-probe"));
123 engine.sync().await.unwrap();
124 assert!(
125 read.execute(json!({"path":staged}), &context)
126 .await
127 .is_err(),
128 "revoked caller cannot use an earlier handle"
129 );
130 manager.shutdown().await;
131 }
132
133 #[tokio::test(flavor = "current_thread")]
134 async fn windows_native_lpac_node_owner_boundary() {
135 if let Some(runtime) = runtime(ExtensionHostRuntime::Node, None) {
136 owner_boundary(runtime).await;
137 }
138 }
139
140 #[tokio::test(flavor = "current_thread")]
141 async fn windows_native_lpac_bun_owner_boundary() {
142 if let Some(runtime) = runtime(ExtensionHostRuntime::Bun, None) {
143 owner_boundary(runtime).await;
144 }
145 }
146
147 #[tokio::test(flavor = "current_thread")]
148 async fn windows_native_lpac_compiled_owner_boundary() {
149 let Some(binary) = std::env::var_os("CODEWHALE_COMPILED_HOST_TEST_BINARY") else {
150 assert!(
151 std::env::var_os("CODEWHALE_EXT_HOST_TESTS").is_none(),
152 "required Windows compiled-host receipt is missing"
153 );
154 eprintln!(
155 "compiled Windows receipt unavailable: compile the canonical bundle and name its image"
156 );
157 return;
158 };
159 let runtime = runtime(ExtensionHostRuntime::Bun, Some(Path::new(&binary))).unwrap();
160 assert!(
161 runtime.compiled,
162 "test must use the exact canonical compiled-host filename/identity"
163 );
164 owner_boundary(runtime).await;
165 }
166
167 #[test]
168 fn windows_native_profiles_are_distinct_and_acl_grant_refuses_junctions() {
169 let first = Profile::create().unwrap();
170 let second = Profile::create().unwrap();
171 assert_ne!(first.name, second.name);
172 assert_eq!(
173 unsafe { EqualSid(first.sid, second.sid) },
174 0,
175 "retired profile ACLs cannot be inherited by a new host"
176 );
177 let root = tempfile::tempdir().unwrap();
178 let outside = tempfile::tempdir().unwrap();
179 let link = root.path().join("junction");
180 let outside_file = outside.path().join("ungranted.txt");
181 std::fs::write(&outside_file, b"outside control").unwrap();
182 // Junction creation needs no symlink privilege; every failure is a red
183 // Windows receipt, never a silent environment skip.
184 let status = std::process::Command::new("cmd.exe")
185 .args(["/d", "/c", "mklink", "/J"])
186 .arg(&link)
187 .arg(outside.path())
188 .output()
189 .unwrap();
190 assert!(
191 status.status.success(),
192 "cannot create the actual junction control"
193 );
194 assert!(WindowsDirectory::open(&link).is_err());
195 let sandbox = NativeSandbox {
196 profile: Arc::new(first),
197 _assets: Arc::new(tempfile::tempdir().unwrap()),
198 program: PathBuf::new(),
199 data: root.path().to_path_buf(),
200 };
201 let result = sandbox.admit_root(root.path());
202 // Remove only the exact junction we created before tempfile cleanup.
203 std::fs::remove_dir(&link).unwrap();
204 assert!(
205 result.is_err(),
206 "grant traversal must refuse reparse points"
207 );
208 assert_no_profile_grant(outside.path(), sandbox.profile.sid);
209 assert_no_profile_grant(&outside_file, sandbox.profile.sid);
210 }
211
212 #[test]
213 fn windows_sandbox_args_use_granted_bun_config_without_changing_other_values() {
214 let runtime_dir = Path::new("runtime copy 鲸鱼");
215 let args = vec![
216 "--no-addons".into(),
217 "--config=NUL".into(),
218 "--input-type=module".into(),
219 "-e".into(),
220 "console.log('--config=NUL')".into(),
221 String::new(),
222 ];
223 let mapped = sandbox_args(&args, runtime_dir);
224 let mut expected = args.clone();
225 expected[1] = format!("--config={}", runtime_dir.join(EMPTY_BUN_CONFIG).display());
226 assert_eq!(mapped, expected);
227 assert_eq!(sandbox_args(&mapped, runtime_dir), mapped);
228 }
229
230 #[test]
231 fn windows_sandbox_args_preserve_node_and_compiled_arguments() {
232 for args in [
233 vec!["--no-addons".into(), "--input-type=module".into()],
234 vec!["--tier=plugin".into(), "--windows-sandbox-probe".into()],
235 vec!["--config=NUL.toml".into(), "--config=custom.toml".into()],
236 Vec::new(),
237 ] {
238 assert_eq!(sandbox_args(&args, Path::new("runtime copy")), args);
239 }
240 }
241
242 #[test]
243 fn windows_argv_and_environment_keep_exact_values_and_reject_nul() {
244 let args = vec![
245 String::new(),
246 "C:\\folder with space\\".into(),
247 "a\"b".into(),
248 "鲸鱼".into(),
249 ];
250 let line = command_line(OsStr::new("C:\\runtime folder\\node.exe"), &args).unwrap();
251 assert_eq!(
252 String::from_utf16(&line[..line.len() - 1]).unwrap(),
253 "\"C:\\runtime folder\\node.exe\" \"\" \"C:\\folder with space\\\\\" \"a\\\"b\" \"鲸鱼\""
254 );
255 assert!(command_line(OsStr::new("node.exe"), &["x\0y".into()]).is_err());
256 assert!(environment_block(&[("A".into(), "value\0tail".into())]).is_err());
257 assert!(environment_block(&[("A=B".into(), "value".into())]).is_err());
258 let env = environment_block(&[
259 ("Z".into(), "last".into()),
260 ("a".into(), "first=literal".into()),
261 ])
262 .unwrap();
263 assert_eq!(
264 String::from_utf16(&env).unwrap(),
265 "a=first=literal\0Z=last\0\0"
266 );
267 }
268
269 // Check the actual external ACL, not only the resolver's return value. The
270 // current user still owns these controls; our AppContainer SID must never gain
271 // an allow ACE through SetSecurityInfo's descendant propagation.
272 fn assert_no_profile_grant(path: &Path, profile: PSID) {
273 assert_eq!(
274 acl_snapshot(path, None),
275 acl_snapshot(path, Some(profile)),
276 "outside control {} received the sandbox SID",
277 path.display()
278 );
279 }
280 fn acl_snapshot(path: &Path, exclude: Option<PSID>) -> Vec<Vec<u8>> {
281 acl_snapshot_view(path, exclude, false).1
282 }
283 fn acl_snapshot_view(path: &Path, exclude: Option<PSID>, raw: bool) -> (u16, Vec<Vec<u8>>) {
284 use windows_sys::Win32::Security::Authorization::{GetSecurityInfo, SE_FILE_OBJECT};
285 use windows_sys::Win32::Security::{
286 ACCESS_ALLOWED_ACE, ACE_HEADER, GetAce, GetSecurityDescriptorControl,
287 };
288 use windows_sys::Win32::System::SystemServices::ACCESS_ALLOWED_ACE_TYPE;
289 let directory;
290 let file;
291 let opened = if path.is_dir() {
292 directory = WindowsDirectory::open(path).unwrap();
293 directory.acl_handle().unwrap()
294 } else {
295 file = fs::OpenOptions::new()
296 .access_mode(READ_CONTROL | FILE_READ_ATTRIBUTES)
297 .share_mode(1)
298 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
299 .open(path)
300 .unwrap();
301 &file
302 };
303 let raw_descriptor = raw.then(|| RawDacl::read(opened).unwrap());
304 let mut _descriptor = None;
305 let mut control = 0_u16;
306 let mut dacl = null_mut();
307 if let Some(raw) = &raw_descriptor {
308 dacl = raw.acl();
309 control = raw.control;
310 } else {
311 let mut descriptor = null_mut();
312 let error = unsafe {
313 GetSecurityInfo(
314 opened.as_raw_handle(),
315 SE_FILE_OBJECT,
316 DACL_SECURITY_INFORMATION,
317 null_mut(),
318 null_mut(),
319 &mut dacl,
320 null_mut(),
321 &mut descriptor,
322 )
323 };
324 assert_eq!(error, 0, "cannot inspect control ACL");
325 _descriptor = Some(LocalAllocation(descriptor));
326 let mut revision = 0;
327 assert_ne!(
328 unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) },
329 0
330 );
331 }
332 let dacl = std::ptr::NonNull::new(dacl).expect("control must have an actual DACL");
333 let mut result = Vec::new();
334 for index in 0..unsafe { dacl.as_ref().AceCount as u32 } {
335 let mut ace = null_mut();
336 assert_ne!(unsafe { GetAce(dacl.as_ptr(), index, &mut ace) }, 0);
337 let ace = std::ptr::NonNull::new(ace).expect("GetAce must return an actual ACE");
338 let header = unsafe { ace.cast::<ACE_HEADER>().as_ref() };
339 assert!(header.AceSize as usize >= std::mem::size_of::<ACE_HEADER>());
340 if header.AceType as u32 == ACCESS_ALLOWED_ACE_TYPE {
341 assert!(header.AceSize as usize >= std::mem::size_of::<ACCESS_ALLOWED_ACE>());
342 let sid = unsafe {
343 std::ptr::addr_of_mut!((*ace.cast::<ACCESS_ALLOWED_ACE>().as_ptr()).SidStart)
344 };
345 if exclude.is_some_and(|profile| unsafe { EqualSid(profile, sid.cast()) } != 0) {
346 continue;
347 }
348 }
349 result.push(
350 unsafe {
351 std::slice::from_raw_parts(ace.cast::<u8>().as_ptr(), header.AceSize as usize)
352 }
353 .to_vec(),
354 );
355 }
356 (control, result)
357 }
358
359 #[test]
360 fn windows_raw_dacl_rejects_truncated_unrestricted_and_malformed_descriptors() {
361 use windows_sys::Win32::Security::{
362 ACL_REVISION, InitializeAcl, SE_DACL_PRESENT, SE_SELF_RELATIVE,
363 SECURITY_DESCRIPTOR_RELATIVE,
364 };
365 fn fixture() -> Vec<u32> {
366 let length = size_of::<SECURITY_DESCRIPTOR_RELATIVE>() + size_of::<ACL>();
367 let mut storage = vec![0_u32; length.div_ceil(size_of::<u32>())];
368 let descriptor = storage.as_mut_ptr().cast::<SECURITY_DESCRIPTOR_RELATIVE>();
369 // SAFETY: the aligned allocation contains both complete structures.
370 unsafe {
371 (*descriptor).Revision = 1;
372 (*descriptor).Control = SE_SELF_RELATIVE | SE_DACL_PRESENT;
373 (*descriptor).Dacl = size_of::<SECURITY_DESCRIPTOR_RELATIVE>() as u32;
374 let acl = storage
375 .as_mut_ptr()
376 .cast::<u8>()
377 .add(size_of::<SECURITY_DESCRIPTOR_RELATIVE>())
378 .cast::<ACL>();
379 assert_ne!(InitializeAcl(acl, size_of::<ACL>() as u32, ACL_REVISION), 0);
380 }
381 storage
382 }
383 let valid = fixture();
384 let length = valid.len() * size_of::<u32>();
385 let descriptor = RawDacl::from_storage(valid.clone(), length).unwrap();
386 assert_eq!(descriptor.control, SE_SELF_RELATIVE | SE_DACL_PRESENT);
387 assert_eq!(
388 unsafe { (*descriptor.acl()).AceCount },
389 0,
390 "an empty restrictive ACL is valid"
391 );
392 assert!(RawDacl::from_storage(Vec::new(), 0).is_err());
393 assert!(RawDacl::from_storage(valid.clone(), length + 1).is_err());
394 assert!(
395 RawDacl::from_storage(valid.clone(), size_of::<SECURITY_DESCRIPTOR_RELATIVE>() - 1)
396 .is_err()
397 );
398 for (control, offset) in [
399 (
400 SE_DACL_PRESENT,
401 size_of::<SECURITY_DESCRIPTOR_RELATIVE>() as u32,
402 ),
403 (SE_SELF_RELATIVE, 0),
404 (SE_SELF_RELATIVE | SE_DACL_PRESENT, 0),
405 (SE_SELF_RELATIVE | SE_DACL_PRESENT, 4),
406 (SE_SELF_RELATIVE | SE_DACL_PRESENT, 21),
407 (SE_SELF_RELATIVE | SE_DACL_PRESENT, length as u32),
408 ] {
409 let mut storage = valid.clone();
410 let header = storage.as_mut_ptr().cast::<SECURITY_DESCRIPTOR_RELATIVE>();
411 unsafe {
412 (*header).Control = control;
413 (*header).Dacl = offset;
414 }
415 assert!(
416 RawDacl::from_storage(storage, length).is_err(),
417 "control={control:#x}, offset={offset}"
418 );
419 }
420 for (size, count) in [(0, 0), (u16::MAX, 0), (size_of::<ACL>() as u16, 1)] {
421 let mut storage = valid.clone();
422 let acl = unsafe {
423 storage
424 .as_mut_ptr()
425 .cast::<u8>()
426 .add(size_of::<SECURITY_DESCRIPTOR_RELATIVE>())
427 .cast::<ACL>()
428 };
429 unsafe {
430 (*acl).AclSize = size;
431 (*acl).AceCount = count;
432 }
433 assert!(
434 RawDacl::from_storage(storage, length).is_err(),
435 "size={size}, count={count}"
436 );
437 }
438 // A complete allow ACE with S-1-5-32 provides the positive control. A
439 // bounded ACL alone must not authorize EqualSid to read past that ACE.
440 let mut allowed = fixture();
441 let entry_offset = length;
442 allowed.resize(allowed.len() + 5, 0);
443 let allowed_length = allowed.len() * size_of::<u32>();
444 unsafe {
445 let bytes =
446 std::slice::from_raw_parts_mut(allowed.as_mut_ptr().cast::<u8>(), allowed_length);
447 bytes[size_of::<SECURITY_DESCRIPTOR_RELATIVE>() + 2..][..2]
448 .copy_from_slice(&((size_of::<ACL>() + 20) as u16).to_le_bytes());
449 bytes[size_of::<SECURITY_DESCRIPTOR_RELATIVE>() + 4] = 1; // AceCount
450 bytes[entry_offset + 2..][..2].copy_from_slice(&20_u16.to_le_bytes());
451 bytes[entry_offset + 4] = 1; // Mask
452 bytes[entry_offset + 8..][..12].copy_from_slice(&[1, 1, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0]);
453 }
454 assert!(RawDacl::from_storage(allowed.clone(), allowed_length).is_ok());
455 for (offset, value) in [(2, 12), (8, 2), (9, 2)] {
456 let mut malformed = allowed.clone();
457 unsafe {
458 *malformed
459 .as_mut_ptr()
460 .cast::<u8>()
461 .add(entry_offset + offset) = value;
462 }
463 assert!(
464 RawDacl::from_storage(malformed, allowed_length).is_err(),
465 "malformed allow ACE at byte {offset}"
466 );
467 }
468 }
469
470 #[test]
471 fn windows_profile_retirement_removes_only_its_grants_and_inherited_data_on_restarts() {
472 use windows_sys::Win32::Security::GetLengthSid;
473 let data = tempfile::tempdir().unwrap();
474 let original_file = data.path().join("existing-state.json");
475 fs::write(&original_file, b"retained user state").unwrap();
476 let original_root_acl = acl_snapshot(data.path(), None);
477 let original_file_acl = acl_snapshot(&original_file, None);
478 let original_raw_root_acl = acl_snapshot_view(data.path(), None, true);
479 let original_raw_file_acl = acl_snapshot_view(&original_file, None, true);
480 let make = || NativeSandbox {
481 profile: Arc::new(Profile::create().unwrap()),
482 _assets: Arc::new(tempfile::tempdir().unwrap()),
483 program: PathBuf::new(),
484 data: data.path().to_path_buf(),
485 };
486 // A concurrent/new profile's exact grants must survive old-profile cleanup.
487 let other = make();
488 other.grant_tree(data.path(), true).unwrap();
489 // Check stored ACE bytes and all control bits before taking a later
490 // baseline. GetSecurityInfo can temporarily normalize the child's view
491 // while its parent carries a grant; no flag masking is allowed here.
492 assert_eq!(
493 acl_snapshot_view(data.path(), Some(other.profile.sid), true),
494 original_raw_root_acl,
495 "first profile grant changed pre-existing directory ACEs"
496 );
497 assert_eq!(
498 acl_snapshot_view(&original_file, Some(other.profile.sid), true),
499 original_raw_file_acl,
500 "first profile grant changed pre-existing file ACEs"
501 );
502 for turn in 0..8 {
503 let current = make();
504 current.grant_tree(data.path(), true).unwrap();
505 let root_without_current = acl_snapshot(data.path(), Some(current.profile.sid));
506 let file_without_current = acl_snapshot(&original_file, Some(current.profile.sid));
507 // Creation after admission exercises inherited grants, not only the
508 // immutable list of files that happened to exist during prepare.
509 let nested = data.path().join(format!("new-state-{turn}"));
510 fs::create_dir(&nested).unwrap();
511 let created = nested.join("state.json");
512 fs::write(&created, b"new persisted state").unwrap();
513 let length = unsafe { GetLengthSid(current.profile.sid) } as usize;
514 let mut sid_copy = vec![0_usize; length.div_ceil(std::mem::size_of::<usize>())];
515 unsafe {
516 std::ptr::copy_nonoverlapping(
517 current.profile.sid as *const u8,
518 sid_copy.as_mut_ptr().cast::<u8>(),
519 length,
520 );
521 }
522 let retired_sid = sid_copy.as_mut_ptr().cast();
523 assert_ne!(
524 acl_snapshot(&created, None),
525 acl_snapshot(&created, Some(retired_sid)),
526 "control must really inherit the current profile grant"
527 );
528 // A new/live owner may still have a data writer open. Exact ACL
529 // retirement must not fail only because legitimate writes continue.
530 let writer = fs::OpenOptions::new()
531 .write(true)
532 .open(&original_file)
533 .unwrap();
534 drop(current); // no Tokio context here: exact retirement is synchronous
535 drop(writer);
536 assert_eq!(acl_snapshot(data.path(), None), root_without_current);
537 assert_eq!(acl_snapshot(&original_file, None), file_without_current);
538 assert_no_profile_grant(&nested, retired_sid);
539 assert_no_profile_grant(&created, retired_sid);
540 assert_eq!(fs::read(&created).unwrap(), b"new persisted state");
541 assert_eq!(fs::read(&original_file).unwrap(), b"retained user state");
542 }
543 drop(other);
544 assert_eq!(
545 acl_snapshot(data.path(), None),
546 original_root_acl,
547 "final profile retirement changed pre-existing directory ACEs"
548 );
549 assert_eq!(
550 acl_snapshot(&original_file, None),
551 original_file_acl,
552 "final profile retirement changed pre-existing file ACEs"
553 );
554 }
555
556 #[test]
557 fn windows_profile_directory_budget_and_recorded_identity_refuse_before_overwrite() {
558 let root = tempfile::tempdir().unwrap();
559 fs::write(root.path().join("first"), b"one").unwrap();
560 fs::write(root.path().join("second"), b"two").unwrap();
561 // Budget includes pending paths, so even a flat directory fails before
562 // growing the allocation past the bound. Admission and retirement share
563 // this exact production helper.
564 let mut pending = vec![PathBuf::from("already-pending"); MAX_GRANT_ENTRIES - 3];
565 assert!(enqueue_children(root.path(), &mut pending, 2).is_err());
566 assert_eq!(2 + pending.len(), MAX_GRANT_ENTRIES);
567 let profile = Profile::create().unwrap();
568 let path = root.path().join("first");
569 let original = File::open(&path).unwrap();
570 profile.remember(&path, &original, false).unwrap();
571 let identity = profile.grants.lock().unwrap().get(&path).unwrap().identity;
572 drop(original);
573 fs::rename(&path, root.path().join("moved-original")).unwrap();
574 fs::write(&path, b"different object").unwrap();
575 let replacement = File::open(&path).unwrap();
576 assert!(profile.remember(&path, &replacement, false).is_err());
577 assert_eq!(
578 profile.grants.lock().unwrap().get(&path).unwrap().identity,
579 identity
580 );
581 // No ACL was granted in this accounting-only check. Do not schedule an
582 // irrelevant retirement against the deliberately replaced fixture path.
583 profile.grants.lock().unwrap().clear();
584 }
585
585 lines RUST