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")?;
398 fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700))?;
399 Ok(directory)
400 }
401
402 fn query(bytes: &[u8], status: i32) -> std::process::Output {
403 std::process::Output {
404 status: std::process::ExitStatus::from_raw(status),
405 stdout: bytes.to_vec(),
406 stderr: Vec::new(),
407 }
408 }
409
410 fn injected(
411 root: &Path,
412 uid: u32,
413 data: &Path,
414 name: Option<&str>,
415 socket: Option<&Path>,
416 create: bool,
417 ) -> Result<Endpoint, Refusal> {
418 resolve_with(
419 data,
420 name,
421 socket,
422 create,
423 || Ok(root.to_path_buf()),
424 |_| Ok(uid),
425 )
426 }
427
428 #[test]
429 fn unavailable_os_queries_are_named() -> TestResult {
430 assert_eq!(
431 linux_root(None).err().ok_or("accepted missing root")?.code,
432 "runtime_location_unavailable"
433 );
434 let missing = io::Error::new(io::ErrorKind::NotFound, "missing query");
435 assert_eq!(
436 getconf_root(Err(missing))
437 .err()
438 .ok_or("accepted missing query")?
439 .code,
440 "runtime_location_unavailable"
441 );
442 assert_eq!(
443 getconf_root(Ok(query(b"ignored\n", 256)))
444 .err()
445 .ok_or("accepted failed query")?
446 .code,
447 "runtime_location_unavailable"
448 );
449 for bytes in [b"".as_slice(), b"\n"] {
450 assert_eq!(
451 getconf_root(Ok(query(bytes, 0)))
452 .err()
453 .ok_or("accepted empty query")?
454 .code,
455 "runtime_location_unavailable"
456 );
457 }
458 Ok(())
459 }
460
461 #[test]
462 fn query_output_requires_one_absolute_utf8_path() -> TestResult {
463 for bytes in [
464 b"relative\n".as_slice(),
465 b"\xff\n",
466 b"/one\n/two\n",
467 b"/one\0\n",
468 ] {
469 assert_eq!(
470 getconf_root(Ok(query(bytes, 0)))
471 .err()
472 .ok_or("accepted invalid query output")?
473 .code,
474 "invalid_runtime_location"
475 );
476 }
477 assert_eq!(
478 getconf_root(Ok(query(b"/private/runtime/\n", 0)))?,
479 PathBuf::from("/private/runtime/")
480 );
481 Ok(())
482 }
483
484 #[test]
485 fn root_refusals_leave_no_namespace() -> TestResult {
486 let directory = fixture()?;
487 let root = directory.path();
488 let uid = fs::metadata(root)?.uid();
489 let data = root.join("store");
490 fs::create_dir(&data)?;
491 for candidate in [PathBuf::from("relative"), root.join("missing")] {
492 assert_eq!(
493 injected(&candidate, uid, &data, None, None, false)
494 .err()
495 .ok_or("accepted invalid root")?
496 .code,
497 "invalid_runtime_location"
498 );
499 }
500 let wrong_uid = uid.checked_add(1).ok_or("uid overflow")?;
501 assert_eq!(
502 injected(root, wrong_uid, &data, None, None, false)
503 .err()
504 .ok_or("accepted foreign owner")?
505 .code,
506 "invalid_runtime_location"
507 );
508 for mode in [0o720, 0o702, 0o755] {
509 fs::set_permissions(root, fs::Permissions::from_mode(mode))?;
510 assert_eq!(
511 injected(root, uid, &data, None, None, false)
512 .err()
513 .ok_or("accepted nonprivate root")?
514 .code,
515 "invalid_runtime_location"
516 );
517 assert!(!root.join("haem").try_exists()?);
518 }
519 fs::set_permissions(root, fs::Permissions::from_mode(0o700))?;
520 let link = root.join("substituted");
521 std::os::unix::fs::symlink(root, &link)?;
522 assert_eq!(
523 injected(&link, uid, &data, None, None, false)
524 .err()
525 .ok_or("accepted root symlink")?
526 .code,
527 "invalid_runtime_location"
528 );
529 assert!(!root.join("haem").try_exists()?);
530 directory.close()?;
531 Ok(())
532 }
533
534 #[test]
535 fn names_are_validated_before_runtime_entries() -> TestResult {
536 let directory = fixture()?;
537 let root = directory.path();
538 let data = root.join("store");
539 fs::create_dir(&data)?;
540 let uid = fs::metadata(root)?.uid();
541 let long = "x".repeat(129);
542 for name in [
543 "",
544 ".",
545 "..",
546 "../store",
547 "back\\slash",
548 "nul\0name",
549 long.as_str(),
550 ] {
551 assert_eq!(
552 injected(root, uid, &data, Some(name), None, false)
553 .err()
554 .ok_or("accepted invalid name")?
555 .code,
556 "invalid_store_name"
557 );
558 assert_eq!(fs::read_dir(root)?.collect::<Result<Vec<_>, _>>()?.len(), 1);
559 }
560 directory.close()?;
561 Ok(())
562 }
563
564 #[test]
565 fn directory_names_and_explicit_names_choose_deterministic_private_endpoints() -> TestResult {
566 let directory = fixture()?;
567 let root = directory.path();
568 let uid = fs::metadata(root)?.uid();
569 let first = root.join("first");
570 let second = root.join("second");
571 fs::create_dir(&first)?;
572 fs::create_dir(&second)?;
573 let a = injected(root, uid, &first, None, None, false)?;
574 let b = injected(root, uid, &second, None, None, false)?;
575 assert_eq!(a.name, "first");
576 assert_eq!(b.name, "second");
577 assert_ne!(a.socket, b.socket);
578 assert_eq!(a.socket.parent(), Some(root.join("haem").as_path()));
579 assert_eq!(
580 fs::symlink_metadata(root.join("haem"))?
581 .permissions()
582 .mode()
583 & 0o7777,
584 0o700
585 );
586 assert_eq!(
587 injected(root, uid, &first, None, None, false)?.socket,
588 a.socket
589 );
590 let named = injected(root, uid, &first, Some("chosen"), None, false)?;
591 assert_eq!(named.name, "chosen");
592 assert_ne!(named.socket, a.socket);
593 SocketAddr::from_pathname(&named.socket)?;
594 let left = first.join("same");
595 let right = second.join("same");
596 fs::create_dir(&left)?;
597 fs::create_dir(&right)?;
598 assert_eq!(
599 injected(root, uid, &left, None, None, false)?.socket,
600 injected(root, uid, &right, None, None, false)?.socket
601 );
602 assert_ne!(
603 injected(root, uid, &left, Some("left"), None, false)?.socket,
604 injected(root, uid, &right, Some("right"), None, false)?.socket
605 );
606 directory.close()?;
607 Ok(())
608 }
609
610 #[test]
611 fn explicit_socket_is_preserved_without_querying_the_os() -> TestResult {
612 let directory = fixture()?;
613 let data = directory.path().join("store");
614 fs::create_dir(&data)?;
615 let socket = directory.path().join("legacy.sock");
616 let uid = fs::metadata(directory.path())?.uid();
617 let endpoint = resolve_with(
618 &data,
619 None,
620 Some(&socket),
621 false,
622 || {
623 Err(Refusal::new(
624 "runtime_location_unavailable",
625 "must not query",
626 ))
627 },
628 |_| Ok(uid),
629 )?;
630 assert_eq!(endpoint.socket, socket);
631 assert_eq!(endpoint.name, "store");
632 assert!(!directory.path().join("haem").try_exists()?);
633 directory.close()?;
634 Ok(())
635 }
636
637 #[test]
638 fn namespace_permission_and_symlink_substitution_are_refused() -> TestResult {
639 let directory = fixture()?;
640 let root = directory.path();
641 let uid = fs::metadata(root)?.uid();
642 let data = root.join("store");
643 fs::create_dir(&data)?;
644 let namespace = root.join("haem");
645 fs::create_dir(&namespace)?;
646 fs::set_permissions(&namespace, fs::Permissions::from_mode(0o755))?;
647 assert_eq!(
648 injected(root, uid, &data, None, None, false)
649 .err()
650 .ok_or("accepted wrong namespace mode")?
651 .code,
652 "invalid_runtime_location"
653 );
654 assert_eq!(
655 fs::metadata(&namespace)?.permissions().mode() & 0o777,
656 0o755
657 );
658 fs::remove_dir(&namespace)?;
659 std::os::unix::fs::symlink(&data, &namespace)?;
660 assert_eq!(
661 injected(root, uid, &data, None, None, false)
662 .err()
663 .ok_or("accepted namespace symlink")?
664 .code,
665 "invalid_runtime_location"
666 );
667 assert!(fs::symlink_metadata(&namespace)?.file_type().is_symlink());
668 directory.close()?;
669 Ok(())
670 }
671
672 #[test]
673 fn endpoint_length_is_refused_before_namespace_creation() -> TestResult {
674 let directory = fixture()?;
675 let root = directory.path().join("x".repeat(110));
676 fs::create_dir(&root)?;
677 fs::set_permissions(&root, fs::Permissions::from_mode(0o700))?;
678 let data = directory.path().join("store");
679 fs::create_dir(&data)?;
680 assert_eq!(
681 injected(&root, fs::metadata(&root)?.uid(), &data, None, None, false)
682 .err()
683 .ok_or("accepted oversized endpoint")?
684 .code,
685 "endpoint_too_long"
686 );
687 assert!(!root.join("haem").try_exists()?);
688 directory.close()?;
689 Ok(())
690 }
691
692 #[test]
693 fn creation_canonicalizes_parent_and_checks_the_created_path() -> TestResult {
694 let directory = fixture()?;
695 let root = directory.path();
696 let uid = fs::metadata(root)?.uid();
697 let data = root.join("new-store");
698 let endpoint = injected(root, uid, &data, None, None, true)?;
699 assert_eq!(endpoint.name, "new-store");
700 assert_eq!(endpoint.data_dir, fs::canonicalize(root)?.join("new-store"));
701 fs::create_dir(&data)?;
702 endpoint.check_created()?;
703 fs::remove_dir(&data)?;
704 let other = root.join("other");
705 fs::create_dir(&other)?;
706 std::os::unix::fs::symlink(&other, &data)?;
707 assert_eq!(
708 endpoint
709 .check_created()
710 .err()
711 .ok_or("accepted changed created path")?
712 .code,
713 "data_dir_changed"
714 );
715 directory.close()?;
716 Ok(())
717 }
718
719 #[test]
720 fn uid_probe_is_removed_and_matches_the_created_file_owner() -> TestResult {
721 let directory = fixture()?;
722 assert_eq!(
723 probe_uid(directory.path())?,
724 fs::metadata(directory.path())?.uid()
725 );
726 assert_eq!(
727 fs::read_dir(directory.path())?
728 .collect::<Result<Vec<_>, _>>()?
729 .len(),
730 0
731 );
732 directory.close()?;
733 Ok(())
734 }
735
736 #[test]
737 fn uid_read_and_cleanup_failures_are_preserved() -> TestResult {
738 let directory = fixture()?;
739 assert_eq!(
740 finish_probe(fs::metadata(directory.path()), Ok(()))
741 .err()
742 .ok_or("accepted invalid probe shape")?
743 .code,
744 "runtime_uid_unavailable"
745 );
746 let read = io::Error::other("read failed");
747 assert_eq!(
748 finish_probe(Err(read), Ok(()))
749 .err()
750 .ok_or("accepted failed uid read")?
751 .code,
752 "runtime_uid_unavailable"
753 );
754 let cleanup = io::Error::other("cleanup failed");
755 assert_eq!(
756 finish_probe(fs::metadata(directory.path()), Err(cleanup))
757 .err()
758 .ok_or("accepted failed cleanup")?
759 .code,
760 "runtime_cleanup_failed"
761 );
762 let both = finish_probe(
763 Err(io::Error::other("read failed")),
764 Err(io::Error::other("cleanup failed")),
765 )
766 .err()
767 .ok_or("accepted both failures")?;
768 assert_eq!(both.code, "runtime_cleanup_failed");
769 assert!(both.to_string().contains("read failed"));
770 assert!(both.to_string().contains("cleanup failed"));
771 directory.close()?;
772 Ok(())
773 }
774
775 #[test]
776 fn byte_limits_and_full_digest_encoding_preserve_names() -> TestResult {
777 validate_name(&"é".repeat(64))?;
778 assert_eq!(
779 validate_name(&"é".repeat(65))
780 .err()
781 .ok_or("accepted 130-byte name")?
782 .code,
783 "invalid_store_name"
784 );
785 let a = filename("store");
786 let b = filename("other");
787 assert_eq!(a.len(), 43);
788 assert_eq!(a, "gk2A1xmF8IKiaZeo24i10d1Ft3fXNYXQPSNjA-Ib3pc");
789 assert!(
790 a.bytes()
791 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
792 );
793 assert_ne!(a, b);
794 assert_eq!(a, filename("store"));
795 Ok(())
796 }
797}