1use std::fmt;
4use std::fs::{self, DirBuilder, Metadata, OpenOptions};
5use std::io;
6use std::os::unix::ffi::OsStrExt;
7use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
8use std::os::unix::net::SocketAddr;
9use std::path::{Component, Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use sha2::{Digest, Sha256};
13
14static PROBES: AtomicU64 = AtomicU64::new(0);
15
16#[derive(Debug)]
17pub struct Refusal {
18 pub code: &'static str,
19 detail: String,
20}
21
22impl Refusal {
23 fn new(code: &'static str, detail: impl fmt::Display) -> Self {
24 Self {
25 code,
26 detail: detail.to_string(),
27 }
28 }
29}
30
31impl fmt::Display for Refusal {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(formatter, "{}: {}", self.code, self.detail)
34 }
35}
36
37impl std::error::Error for Refusal {}
38
39#[derive(Debug)]
40pub struct Endpoint {
41 pub name: String,
42 pub socket: PathBuf,
43 pub data_dir: PathBuf,
44}
45
46impl Endpoint {
47 pub fn check_created(&self) -> Result<(), Refusal> {
52 let actual = fs::canonicalize(&self.data_dir)
53 .map_err(|error| Refusal::new("data_dir_changed", error))?;
54 let metadata =
55 fs::metadata(&actual).map_err(|error| Refusal::new("data_dir_changed", error))?;
56 if actual != self.data_dir || !metadata.is_dir() {
57 return Err(Refusal::new(
58 "data_dir_changed",
59 "created store location changed",
60 ));
61 }
62 Ok(())
63 }
64}
65
66pub fn resolve(
72 data_dir: &Path,
73 name: Option<&str>,
74 socket: Option<&Path>,
75 create: bool,
76) -> Result<Endpoint, Refusal> {
77 resolve_with(data_dir, name, socket, create, os_root, probe_uid)
78}
79
80fn resolve_with(
81 data_dir: &Path,
82 name: Option<&str>,
83 socket: Option<&Path>,
84 create: bool,
85 root: impl FnOnce() -> Result<PathBuf, Refusal>,
86 uid: impl FnOnce(&Path) -> Result<u32, Refusal>,
87) -> Result<Endpoint, Refusal> {
88 if let Some(name) = name {
89 validate_name(name)?;
90 }
91 let data_dir = canonical_data(data_dir, create)?;
92 let name = match name {
93 Some(name) => name.to_owned(),
94 None => data_dir
95 .file_name()
96 .and_then(|name| name.to_str())
97 .ok_or_else(|| {
98 Refusal::new(
99 "invalid_store_name",
100 "data directory needs a UTF-8 basename",
101 )
102 })?
103 .to_owned(),
104 };
105 validate_name(&name)?;
106 if let Some(socket) = socket {
107 check_endpoint(socket)?;
108 let parent = socket
109 .parent()
110 .ok_or_else(|| Refusal::new("invalid_socket", "endpoint needs a parent"))?;
111 let metadata = private_directory(parent)?;
112 let owner = uid(parent)?;
113 check_identity(parent, &metadata, owner)?;
114 return Ok(Endpoint {
115 name,
116 socket: socket.to_path_buf(),
117 data_dir,
118 });
119 }
120 let root = root()?;
121 let metadata = private_directory(&root)?;
122 let namespace = root.join("haem");
123 let socket = namespace.join(filename(&name));
124 check_endpoint(&socket)?;
125 let owner = uid(&root)?;
126 check_identity(&root, &metadata, owner)?;
127 match DirBuilder::new().mode(0o700).create(&namespace) {
128 Ok(()) => {}
129 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
130 Err(error) => return Err(Refusal::new("invalid_runtime_location", error)),
131 }
132 let namespace_metadata = private_directory(&namespace)?;
133 check_identity(&namespace, &namespace_metadata, owner)?;
134 check_identity(&root, &metadata, owner)?;
135 Ok(Endpoint {
136 name,
137 socket,
138 data_dir,
139 })
140}
141
142fn canonical_data(data: &Path, create: bool) -> Result<PathBuf, Refusal> {
143 if !data.is_absolute() {
144 return Err(Refusal::new(
145 "invalid_data_dir",
146 "data directory must be absolute",
147 ));
148 }
149 if create {
150 let Some(Component::Normal(name)) = data.components().next_back() else {
151 return Err(Refusal::new(
152 "invalid_data_dir",
153 "creation needs a normal final component",
154 ));
155 };
156 if name.to_str().is_none() {
157 return Err(Refusal::new(
158 "invalid_store_name",
159 "creation needs a UTF-8 final component",
160 ));
161 }
162 let parent = data
163 .parent()
164 .ok_or_else(|| Refusal::new("invalid_data_dir", "creation needs an existing parent"))?;
165 let parent =
166 fs::canonicalize(parent).map_err(|error| Refusal::new("invalid_data_dir", error))?;
167 let metadata =
168 fs::metadata(&parent).map_err(|error| Refusal::new("invalid_data_dir", error))?;
169 if !metadata.is_dir() {
170 return Err(Refusal::new(
171 "invalid_data_dir",
172 "creation parent is not a directory",
173 ));
174 }
175 Ok(parent.join(name))
176 } else {
177 let canonical =
178 fs::canonicalize(data).map_err(|error| Refusal::new("invalid_data_dir", error))?;
179 let metadata =
180 fs::metadata(&canonical).map_err(|error| Refusal::new("invalid_data_dir", error))?;
181 if !metadata.is_dir() {
182 return Err(Refusal::new("invalid_data_dir", "store is not a directory"));
183 }
184 Ok(canonical)
185 }
186}
187
188fn validate_name(name: &str) -> Result<(), Refusal> {
189 if name.is_empty()
190 || name.len() > 128
191 || matches!(name, "." | "..")
192 || name.contains(['/', '\\', '\0'])
193 {
194 return Err(Refusal::new(
195 "invalid_store_name",
196 "name must be 1 to 128 UTF-8 bytes without separators or NUL",
197 ));
198 }
199 Ok(())
200}
201
202fn check_endpoint(socket: &Path) -> Result<(), Refusal> {
203 if !socket.is_absolute() || socket.as_os_str().as_bytes().contains(&0) {
204 return Err(Refusal::new(
205 "invalid_socket",
206 "endpoint must be an absolute path without NUL",
207 ));
208 }
209 SocketAddr::from_pathname(socket).map_err(|error| Refusal::new("endpoint_too_long", error))?;
210 Ok(())
211}
212
213fn private_directory(path: &Path) -> Result<Metadata, Refusal> {
214 if !path.is_absolute() {
215 return Err(Refusal::new(
216 "invalid_runtime_location",
217 "runtime directory must be absolute",
218 ));
219 }
220 let metadata = fs::symlink_metadata(path)
221 .map_err(|error| Refusal::new("invalid_runtime_location", error))?;
222 if !metadata.is_dir() || metadata.permissions().mode() & 0o7777 != 0o700 {
223 return Err(Refusal::new(
224 "invalid_runtime_location",
225 "runtime directory must be a non-symlink directory with mode 0700",
226 ));
227 }
228 Ok(metadata)
229}
230
231fn check_identity(path: &Path, expected: &Metadata, owner: u32) -> Result<(), Refusal> {
232 let actual = private_directory(path)?;
233 if actual.uid() != owner || (actual.dev(), actual.ino()) != (expected.dev(), expected.ino()) {
234 return Err(Refusal::new(
235 "invalid_runtime_location",
236 "runtime directory owner or identity changed",
237 ));
238 }
239 Ok(())
240}
241
242fn probe_uid(root: &Path) -> Result<u32, Refusal> {
243 let sequence = PROBES
244 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
245 value.checked_add(1)
246 })
247 .map_err(|error| Refusal::new("runtime_uid_unavailable", error))?;
248 let path = root.join(format!(".haem-uid-{}-{sequence}", std::process::id()));
249 let file = OpenOptions::new()
250 .write(true)
251 .create_new(true)
252 .mode(0o600)
253 .open(&path)
254 .map_err(|error| Refusal::new("runtime_uid_unavailable", error))?;
255 let metadata = file.metadata();
256 let cleanup = fs::remove_file(&path);
257 drop(file);
258 finish_probe(metadata, cleanup)
259}
260
261fn finish_probe(metadata: io::Result<Metadata>, cleanup: io::Result<()>) -> Result<u32, Refusal> {
262 match (metadata, cleanup) {
263 (Ok(metadata), Ok(())) => {
264 if !metadata.is_file() || metadata.permissions().mode() & 0o7777 != 0o600 {
265 return Err(Refusal::new(
266 "runtime_uid_unavailable",
267 "uid probe must be a regular mode-0600 file",
268 ));
269 }
270 Ok(metadata.uid())
271 }
272 (Err(error), Ok(())) => Err(Refusal::new("runtime_uid_unavailable", error)),
273 (Ok(_), Err(error)) => Err(Refusal::new("runtime_cleanup_failed", error)),
274 (Err(read), Err(cleanup)) => Err(Refusal::new(
275 "runtime_cleanup_failed",
276 format!("uid read: {read}; probe removal: {cleanup}"),
277 )),
278 }
279}
280
281fn filename(name: &str) -> String {
282 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
283 let mut encoded = String::with_capacity(43);
284 let mut pending = 0_u16;
285 let mut bits = 0_u32;
286 for byte in Sha256::digest(name.as_bytes()) {
287 pending = ((pending & ((1 << bits) - 1)) << 8) | u16::from(byte);
288 bits += 8;
289 while bits >= 6 {
290 bits -= 6;
291 encoded.push(char::from(ALPHABET[usize::from((pending >> bits) & 63)]));
292 }
293 }
294 if bits != 0 {
295 encoded.push(char::from(
296 ALPHABET[usize::from((pending << (6 - bits)) & 63)],
297 ));
298 }
299 encoded
300}
301
302#[cfg(target_os = "linux")]
303fn os_root() -> Result<PathBuf, Refusal> {
304 linux_root(std::env::var_os("XDG_RUNTIME_DIR"))
305}
306
307#[cfg(target_os = "macos")]
308fn os_root() -> Result<PathBuf, Refusal> {
309 getconf_root(
310 std::process::Command::new("/usr/bin/getconf")
311 .arg("DARWIN_USER_TEMP_DIR")
312 .output(),
313 )
314}
315
316#[cfg(not(any(target_os = "linux", target_os = "macos")))]
317fn os_root() -> Result<PathBuf, Refusal> {
318 Err(Refusal::new(
319 "runtime_location_unavailable",
320 "platform has no supported OS runtime query",
321 ))
322}
323
324#[cfg(any(target_os = "linux", test))]
325fn linux_root(input: Option<std::ffi::OsString>) -> Result<PathBuf, Refusal> {
326 let input = input.filter(|value| !value.is_empty()).ok_or_else(|| {
327 Refusal::new(
328 "runtime_location_unavailable",
329 "XDG_RUNTIME_DIR is unavailable",
330 )
331 })?;
332 let path = PathBuf::from(input);
333 if !path.is_absolute() || path.to_str().is_none() || path.as_os_str().as_bytes().contains(&0) {
334 return Err(Refusal::new(
335 "invalid_runtime_location",
336 "XDG_RUNTIME_DIR is not an absolute runtime path",
337 ));
338 }
339 Ok(path)
340}
341
342#[cfg(any(target_os = "macos", test))]
343fn getconf_root(input: io::Result<std::process::Output>) -> Result<PathBuf, Refusal> {
344 let output = input.map_err(|error| Refusal::new("runtime_location_unavailable", error))?;
345 if !output.status.success() {
346 return Err(Refusal::new(
347 "runtime_location_unavailable",
348 format!("OS runtime query exited {}", output.status),
349 ));
350 }
351 let output = std::str::from_utf8(&output.stdout)
352 .map_err(|error| Refusal::new("invalid_runtime_location", error))?;
353 let output = output.strip_suffix('\n').map_or(output, |path| path);
354 if output.is_empty() {
355 return Err(Refusal::new(
356 "runtime_location_unavailable",
357 "OS runtime query returned no path",
358 ));
359 }
360 if output.contains(['\n', '\r', '\0']) || !Path::new(output).is_absolute() {
361 return Err(Refusal::new(
362 "invalid_runtime_location",
363 "OS runtime query needs one absolute UTF-8 path",
364 ));
365 }
366 Ok(PathBuf::from(output))
367}
368
369#[cfg(test)]
370pub(crate) fn resolve_input(
371 data_dir: &Path,
372 name: Option<&str>,
373 socket: Option<&Path>,
374 create: bool,
375 root: &Path,
376 uid: u32,
377) -> Result<Endpoint, Refusal> {
378 resolve_with(
379 data_dir,
380 name,
381 socket,
382 create,
383 || Ok(root.to_path_buf()),
384 |_| Ok(uid),
385 )
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use std::os::unix::process::ExitStatusExt;
392
393 type TestResult = Result<(), Box<dyn std::error::Error>>;
394
395 fn fixture() -> Result<tempfile::TempDir, std::io::Error> {
396 let directory = tempfile::Builder::new().prefix("r").tempdir_in("/tmp")?;
400 fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700))?;
401 Ok(directory)
402 }
403
404 fn query(bytes: &[u8], status: i32) -> std::process::Output {
405 std::process::Output {
406 status: std::process::ExitStatus::from_raw(status),
407 stdout: bytes.to_vec(),
408 stderr: Vec::new(),
409 }
410 }
411
412 fn injected(
413 root: &Path,
414 uid: u32,
415 data: &Path,
416 name: Option<&str>,
417 socket: Option<&Path>,
418 create: bool,
419 ) -> Result<Endpoint, Refusal> {
420 resolve_with(
421 data,
422 name,
423 socket,
424 create,
425 || Ok(root.to_path_buf()),
426 |_| Ok(uid),
427 )
428 }
429
430 #[test]
431 fn unavailable_os_queries_are_named() -> TestResult {
432 assert_eq!(
433 linux_root(None).err().ok_or("accepted missing root")?.code,
434 "runtime_location_unavailable"
435 );
436 let missing = io::Error::new(io::ErrorKind::NotFound, "missing query");
437 assert_eq!(
438 getconf_root(Err(missing))
439 .err()
440 .ok_or("accepted missing query")?
441 .code,
442 "runtime_location_unavailable"
443 );
444 assert_eq!(
445 getconf_root(Ok(query(b"ignored\n", 256)))
446 .err()
447 .ok_or("accepted failed query")?
448 .code,
449 "runtime_location_unavailable"
450 );
451 for bytes in [b"".as_slice(), b"\n"] {
452 assert_eq!(
453 getconf_root(Ok(query(bytes, 0)))
454 .err()
455 .ok_or("accepted empty query")?
456 .code,
457 "runtime_location_unavailable"
458 );
459 }
460 Ok(())
461 }
462
463 #[test]
464 fn query_output_requires_one_absolute_utf8_path() -> TestResult {
465 for bytes in [
466 b"relative\n".as_slice(),
467 b"\xff\n",
468 b"/one\n/two\n",
469 b"/one\0\n",
470 ] {
471 assert_eq!(
472 getconf_root(Ok(query(bytes, 0)))
473 .err()
474 .ok_or("accepted invalid query output")?
475 .code,
476 "invalid_runtime_location"
477 );
478 }
479 assert_eq!(
480 getconf_root(Ok(query(b"/private/runtime/\n", 0)))?,
481 PathBuf::from("/private/runtime/")
482 );
483 Ok(())
484 }
485
486 #[test]
487 fn root_refusals_leave_no_namespace() -> TestResult {
488 let directory = fixture()?;
489 let root = directory.path();
490 let uid = fs::metadata(root)?.uid();
491 let data = root.join("store");
492 fs::create_dir(&data)?;
493 for candidate in [PathBuf::from("relative"), root.join("missing")] {
494 assert_eq!(
495 injected(&candidate, uid, &data, None, None, false)
496 .err()
497 .ok_or("accepted invalid root")?
498 .code,
499 "invalid_runtime_location"
500 );
501 }
502 let wrong_uid = uid.checked_add(1).ok_or("uid overflow")?;
503 assert_eq!(
504 injected(root, wrong_uid, &data, None, None, false)
505 .err()
506 .ok_or("accepted foreign owner")?
507 .code,
508 "invalid_runtime_location"
509 );
510 for mode in [0o720, 0o702, 0o755] {
511 fs::set_permissions(root, fs::Permissions::from_mode(mode))?;
512 assert_eq!(
513 injected(root, uid, &data, None, None, false)
514 .err()
515 .ok_or("accepted nonprivate root")?
516 .code,
517 "invalid_runtime_location"
518 );
519 assert!(!root.join("haem").try_exists()?);
520 }
521 fs::set_permissions(root, fs::Permissions::from_mode(0o700))?;
522 let link = root.join("substituted");
523 std::os::unix::fs::symlink(root, &link)?;
524 assert_eq!(
525 injected(&link, uid, &data, None, None, false)
526 .err()
527 .ok_or("accepted root symlink")?
528 .code,
529 "invalid_runtime_location"
530 );
531 assert!(!root.join("haem").try_exists()?);
532 directory.close()?;
533 Ok(())
534 }
535
536 #[test]
537 fn names_are_validated_before_runtime_entries() -> TestResult {
538 let directory = fixture()?;
539 let root = directory.path();
540 let data = root.join("store");
541 fs::create_dir(&data)?;
542 let uid = fs::metadata(root)?.uid();
543 let long = "x".repeat(129);
544 for name in [
545 "",
546 ".",
547 "..",
548 "../store",
549 "back\\slash",
550 "nul\0name",
551 long.as_str(),
552 ] {
553 assert_eq!(
554 injected(root, uid, &data, Some(name), None, false)
555 .err()
556 .ok_or("accepted invalid name")?
557 .code,
558 "invalid_store_name"
559 );
560 assert_eq!(fs::read_dir(root)?.collect::<Result<Vec<_>, _>>()?.len(), 1);
561 }
562 directory.close()?;
563 Ok(())
564 }
565
566 #[test]
567 fn directory_names_and_explicit_names_choose_deterministic_private_endpoints() -> TestResult {
568 let directory = fixture()?;
569 let root = directory.path();
570 let uid = fs::metadata(root)?.uid();
571 let first = root.join("first");
572 let second = root.join("second");
573 fs::create_dir(&first)?;
574 fs::create_dir(&second)?;
575 let a = injected(root, uid, &first, None, None, false)?;
576 let b = injected(root, uid, &second, None, None, false)?;
577 assert_eq!(a.name, "first");
578 assert_eq!(b.name, "second");
579 assert_ne!(a.socket, b.socket);
580 assert_eq!(a.socket.parent(), Some(root.join("haem").as_path()));
581 assert_eq!(
582 fs::symlink_metadata(root.join("haem"))?
583 .permissions()
584 .mode()
585 & 0o7777,
586 0o700
587 );
588 assert_eq!(
589 injected(root, uid, &first, None, None, false)?.socket,
590 a.socket
591 );
592 let named = injected(root, uid, &first, Some("chosen"), None, false)?;
593 assert_eq!(named.name, "chosen");
594 assert_ne!(named.socket, a.socket);
595 SocketAddr::from_pathname(&named.socket)?;
596 let left = first.join("same");
597 let right = second.join("same");
598 fs::create_dir(&left)?;
599 fs::create_dir(&right)?;
600 assert_eq!(
601 injected(root, uid, &left, None, None, false)?.socket,
602 injected(root, uid, &right, None, None, false)?.socket
603 );
604 assert_ne!(
605 injected(root, uid, &left, Some("left"), None, false)?.socket,
606 injected(root, uid, &right, Some("right"), None, false)?.socket
607 );
608 directory.close()?;
609 Ok(())
610 }
611
612 #[test]
613 fn explicit_socket_is_preserved_without_querying_the_os() -> TestResult {
614 let directory = fixture()?;
615 let data = directory.path().join("store");
616 fs::create_dir(&data)?;
617 let socket = directory.path().join("legacy.sock");
618 let uid = fs::metadata(directory.path())?.uid();
619 let endpoint = resolve_with(
620 &data,
621 None,
622 Some(&socket),
623 false,
624 || {
625 Err(Refusal::new(
626 "runtime_location_unavailable",
627 "must not query",
628 ))
629 },
630 |_| Ok(uid),
631 )?;
632 assert_eq!(endpoint.socket, socket);
633 assert_eq!(endpoint.name, "store");
634 assert!(!directory.path().join("haem").try_exists()?);
635 directory.close()?;
636 Ok(())
637 }
638
639 #[test]
640 fn namespace_permission_and_symlink_substitution_are_refused() -> TestResult {
641 let directory = fixture()?;
642 let root = directory.path();
643 let uid = fs::metadata(root)?.uid();
644 let data = root.join("store");
645 fs::create_dir(&data)?;
646 let namespace = root.join("haem");
647 fs::create_dir(&namespace)?;
648 fs::set_permissions(&namespace, fs::Permissions::from_mode(0o755))?;
649 assert_eq!(
650 injected(root, uid, &data, None, None, false)
651 .err()
652 .ok_or("accepted wrong namespace mode")?
653 .code,
654 "invalid_runtime_location"
655 );
656 assert_eq!(
657 fs::metadata(&namespace)?.permissions().mode() & 0o777,
658 0o755
659 );
660 fs::remove_dir(&namespace)?;
661 std::os::unix::fs::symlink(&data, &namespace)?;
662 assert_eq!(
663 injected(root, uid, &data, None, None, false)
664 .err()
665 .ok_or("accepted namespace symlink")?
666 .code,
667 "invalid_runtime_location"
668 );
669 assert!(fs::symlink_metadata(&namespace)?.file_type().is_symlink());
670 directory.close()?;
671 Ok(())
672 }
673
674 #[test]
675 fn endpoint_length_is_refused_before_namespace_creation() -> TestResult {
676 let directory = fixture()?;
677 let root = directory.path().join("x".repeat(110));
678 fs::create_dir(&root)?;
679 fs::set_permissions(&root, fs::Permissions::from_mode(0o700))?;
680 let data = directory.path().join("store");
681 fs::create_dir(&data)?;
682 assert_eq!(
683 injected(&root, fs::metadata(&root)?.uid(), &data, None, None, false)
684 .err()
685 .ok_or("accepted oversized endpoint")?
686 .code,
687 "endpoint_too_long"
688 );
689 assert!(!root.join("haem").try_exists()?);
690 directory.close()?;
691 Ok(())
692 }
693
694 #[test]
695 fn creation_canonicalizes_parent_and_checks_the_created_path() -> TestResult {
696 let directory = fixture()?;
697 let root = directory.path();
698 let uid = fs::metadata(root)?.uid();
699 let data = root.join("new-store");
700 let endpoint = injected(root, uid, &data, None, None, true)?;
701 assert_eq!(endpoint.name, "new-store");
702 assert_eq!(endpoint.data_dir, fs::canonicalize(root)?.join("new-store"));
703 fs::create_dir(&data)?;
704 endpoint.check_created()?;
705 fs::remove_dir(&data)?;
706 let other = root.join("other");
707 fs::create_dir(&other)?;
708 std::os::unix::fs::symlink(&other, &data)?;
709 assert_eq!(
710 endpoint
711 .check_created()
712 .err()
713 .ok_or("accepted changed created path")?
714 .code,
715 "data_dir_changed"
716 );
717 directory.close()?;
718 Ok(())
719 }
720
721 #[test]
722 fn uid_probe_is_removed_and_matches_the_created_file_owner() -> TestResult {
723 let directory = fixture()?;
724 assert_eq!(
725 probe_uid(directory.path())?,
726 fs::metadata(directory.path())?.uid()
727 );
728 assert_eq!(
729 fs::read_dir(directory.path())?
730 .collect::<Result<Vec<_>, _>>()?
731 .len(),
732 0
733 );
734 directory.close()?;
735 Ok(())
736 }
737
738 #[test]
739 fn uid_read_and_cleanup_failures_are_preserved() -> TestResult {
740 let directory = fixture()?;
741 assert_eq!(
742 finish_probe(fs::metadata(directory.path()), Ok(()))
743 .err()
744 .ok_or("accepted invalid probe shape")?
745 .code,
746 "runtime_uid_unavailable"
747 );
748 let read = io::Error::other("read failed");
749 assert_eq!(
750 finish_probe(Err(read), Ok(()))
751 .err()
752 .ok_or("accepted failed uid read")?
753 .code,
754 "runtime_uid_unavailable"
755 );
756 let cleanup = io::Error::other("cleanup failed");
757 assert_eq!(
758 finish_probe(fs::metadata(directory.path()), Err(cleanup))
759 .err()
760 .ok_or("accepted failed cleanup")?
761 .code,
762 "runtime_cleanup_failed"
763 );
764 let both = finish_probe(
765 Err(io::Error::other("read failed")),
766 Err(io::Error::other("cleanup failed")),
767 )
768 .err()
769 .ok_or("accepted both failures")?;
770 assert_eq!(both.code, "runtime_cleanup_failed");
771 assert!(both.to_string().contains("read failed"));
772 assert!(both.to_string().contains("cleanup failed"));
773 directory.close()?;
774 Ok(())
775 }
776
777 #[test]
778 fn byte_limits_and_full_digest_encoding_preserve_names() -> TestResult {
779 validate_name(&"é".repeat(64))?;
780 assert_eq!(
781 validate_name(&"é".repeat(65))
782 .err()
783 .ok_or("accepted 130-byte name")?
784 .code,
785 "invalid_store_name"
786 );
787 let a = filename("store");
788 let b = filename("other");
789 assert_eq!(a.len(), 43);
790 assert_eq!(a, "gk2A1xmF8IKiaZeo24i10d1Ft3fXNYXQPSNjA-Ib3pc");
791 assert!(
792 a.bytes()
793 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
794 );
795 assert_ne!(a, b);
796 assert_eq!(a, filename("store"));
797 Ok(())
798 }
799}