Skip to main content

objects/
name_encoding.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Portable names and verified filesystem identities for digest entries.
3
4#[cfg(feature = "fs")]
5use std::{fs, io, path::Path};
6
7pub use heddle_object_model::name_encoding::*;
8
9/// Persist long-name identity before publishing the entry's payload. Callers
10/// serialize writes with the same lock that protects the payload.
11#[cfg(feature = "fs")]
12pub fn write_name_entry(root: &Path, name: &str) -> io::Result<()> {
13    let relative = name_path(name);
14    if !is_digest_name_path(&relative) {
15        return Ok(());
16    }
17    let entry = root.join(&relative);
18    let identity = entry.join("name");
19    if identity.try_exists()? {
20        verify_name_entry(root, name)?;
21    } else {
22        crate::fs_atomic::write_file_atomic(&identity, name.as_bytes())?;
23    }
24    Ok(())
25}
26
27/// Point reads and deletes must verify an existing digest entry before use.
28/// An absent entry is a normal missing value; an unlabelled entry is corrupt.
29#[cfg(feature = "fs")]
30pub fn verify_name_entry(root: &Path, name: &str) -> io::Result<()> {
31    let relative = name_path(name);
32    if is_digest_name_path(&relative) && root.join(&relative).try_exists()? {
33        let stored = read_name_entry(root, &relative)?;
34        if stored.as_deref() != Some(name) {
35            return Err(io::Error::new(
36                io::ErrorKind::InvalidData,
37                "digest entry name does not match requested name",
38            ));
39        }
40    }
41    Ok(())
42}
43
44/// Decode a plain entry or verify a digest entry's exact UTF-8 name against
45/// its complete path. Corrupt identities are errors, never silent omissions.
46#[cfg(feature = "fs")]
47pub fn read_name_entry(root: &Path, relative: &Path) -> io::Result<Option<String>> {
48    if !is_digest_name_path(relative) {
49        return Ok(decode_name_path(relative));
50    }
51    let name = fs::read_to_string(root.join(relative).join("name"))?;
52    if name_path(&name) != relative {
53        return Err(io::Error::new(
54            io::ErrorKind::InvalidData,
55            "digest entry name does not match storage path",
56        ));
57    }
58    Ok(Some(name))
59}
60
61#[cfg(all(test, feature = "fs"))]
62mod tests {
63    use std::collections::HashMap;
64
65    use super::*;
66
67    #[test]
68    fn long_names_with_shared_digest_prefix_round_trip_and_verify() {
69        let root = tempfile::TempDir::new().expect("entries");
70        let mut prefixes = HashMap::new();
71        let (left, right) = (0..100_000)
72            .find_map(|index| {
73                let name = format!("{}{index}", "界".repeat(330));
74                let path = name_path(&name);
75                let digest = path
76                    .components()
77                    .next()
78                    .expect("digest")
79                    .as_os_str()
80                    .to_str()
81                    .expect("ASCII");
82                prefixes
83                    .insert(digest[..6].to_owned(), name.clone())
84                    .map(|other| (other, name))
85            })
86            .expect("16-bit digest prefix collision");
87        assert_ne!(name_path(&left), name_path(&right));
88        for name in [&left, &right] {
89            write_name_entry(root.path(), name).expect("identity write");
90            assert_eq!(
91                read_name_entry(root.path(), &name_path(name))
92                    .expect("read")
93                    .as_deref(),
94                Some(name.as_str())
95            );
96            verify_name_entry(root.path(), name).expect("verify");
97        }
98        fs::write(root.path().join(name_path(&left)).join("name"), &right)
99            .expect("corrupt identity");
100        assert!(verify_name_entry(root.path(), &left).is_err());
101        assert!(write_name_entry(root.path(), &left).is_err());
102    }
103}