Skip to main content

haematite_cli/serve/
runtime.rs

1//! Private runtime endpoints derived from the operating system and store name.
2
3use 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    /// Refuses a created store whose canonical location changed during creation.
48    ///
49    /// # Errors
50    /// Returns `data_dir_changed` if the created directory cannot be checked or changed location.
51    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
66/// Resolves one endpoint without opening a store or binding a socket.
67///
68/// # Errors
69/// Names invalid store names, data locations, runtime locations and endpoints,
70/// unavailable OS queries, uid-probe failures and probe-cleanup failures.
71pub 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        // A short, owned root keeps valid-endpoint fixtures below the socket-path bound.
397        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}