composefs/fsverity/
hashvalue.rs

1//! Hash value types and trait definitions for fs-verity.
2//!
3//! This module defines the FsVerityHashValue trait and concrete implementations
4//! for SHA-256 and SHA-512 hash values, including parsing from hex strings
5//! and object pathnames.
6
7use core::{fmt, hash::Hash};
8
9use hex::FromHexError;
10use sha2::{digest::FixedOutputReset, digest::Output, Digest, Sha256, Sha512};
11use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
12
13/// Trait for fs-verity hash value types supporting SHA-256 and SHA-512.
14///
15/// This trait defines the interface for hash values used in fs-verity operations,
16/// including serialization to/from hex strings and object store pathnames.
17pub trait FsVerityHashValue
18where
19    Self: Clone,
20    Self: From<Output<Self::Digest>>,
21    Self: FromBytes + Immutable + IntoBytes + KnownLayout + Unaligned,
22    Self: Hash + Eq,
23    Self: fmt::Debug,
24    Self: Send + Sync + Unpin + 'static,
25{
26    /// The underlying hash digest algorithm type.
27    type Digest: Digest + FixedOutputReset + fmt::Debug;
28    /// The fs-verity algorithm identifier (1 for SHA-256, 2 for SHA-512).
29    const ALGORITHM: u8;
30    /// An empty hash value with all bytes set to zero.
31    const EMPTY: Self;
32    /// The algorithm identifier string ("sha256" or "sha512").
33    const ID: &str;
34
35    /// Parse a hash value from a hexadecimal string.
36    ///
37    /// # Arguments
38    /// * `hex` - A hexadecimal string representation of the hash
39    ///
40    /// # Returns
41    /// The parsed hash value, or an error if the input is invalid.
42    fn from_hex(hex: impl AsRef<[u8]>) -> Result<Self, FromHexError> {
43        let mut value = Self::EMPTY;
44        hex::decode_to_slice(hex.as_ref(), value.as_mut_bytes())?;
45        Ok(value)
46    }
47
48    /// Parse a hash value from an object store directory number and basename.
49    ///
50    /// Object stores typically use a two-level hierarchy where the first byte
51    /// of the hash determines the directory name and the remaining bytes form
52    /// the basename.
53    ///
54    /// # Arguments
55    /// * `dirnum` - The directory number (first byte of the hash)
56    /// * `basename` - The hexadecimal basename (remaining bytes)
57    ///
58    /// # Returns
59    /// The parsed hash value, or an error if the input is invalid.
60    fn from_object_dir_and_basename(
61        dirnum: u8,
62        basename: impl AsRef<[u8]>,
63    ) -> Result<Self, FromHexError> {
64        let expected_size = 2 * (size_of::<Self>() - 1);
65        let bytes = basename.as_ref();
66        if bytes.len() != expected_size {
67            return Err(FromHexError::InvalidStringLength);
68        }
69        let mut result = Self::EMPTY;
70        result.as_mut_bytes()[0] = dirnum;
71        hex::decode_to_slice(bytes, &mut result.as_mut_bytes()[1..])?;
72        Ok(result)
73    }
74
75    /// Parse a hash value from a full object pathname.
76    ///
77    /// Parses a pathname in the format "xx/yyyyyy" where "xxyyyyyy" is the
78    /// full hexadecimal hash. The prefix before the two-level hierarchy is ignored.
79    ///
80    /// # Arguments
81    /// * `pathname` - The object pathname (e.g., "ab/cdef1234...")
82    ///
83    /// # Returns
84    /// The parsed hash value, or an error if the input is invalid.
85    fn from_object_pathname(pathname: impl AsRef<[u8]>) -> Result<Self, FromHexError> {
86        // We want to the trailing part of "....../xx/yyyyyy" where xxyyyyyy is our hex length
87        let min_size = 2 * size_of::<Self>() + 1;
88        let bytes = pathname.as_ref();
89        if bytes.len() < min_size {
90            return Err(FromHexError::InvalidStringLength);
91        }
92
93        let trailing = &bytes[bytes.len() - min_size..];
94        let mut result = Self::EMPTY;
95        hex::decode_to_slice(&trailing[0..2], &mut result.as_mut_bytes()[0..1])?;
96        if trailing[2] != b'/' {
97            return Err(FromHexError::InvalidHexCharacter {
98                c: trailing[2] as char,
99                index: 2,
100            });
101        }
102        hex::decode_to_slice(&trailing[3..], &mut result.as_mut_bytes()[1..])?;
103        Ok(result)
104    }
105
106    /// Convert the hash value to an object pathname.
107    ///
108    /// Formats the hash as "xx/yyyyyy" where xx is the first byte in hex
109    /// and yyyyyy is the remaining bytes in hex.
110    ///
111    /// # Returns
112    /// A string in object pathname format.
113    fn to_object_pathname(&self) -> String {
114        format!(
115            "{:02x}/{}",
116            self.as_bytes()[0],
117            hex::encode(&self.as_bytes()[1..])
118        )
119    }
120
121    /// Convert the hash value to an object directory name.
122    ///
123    /// Returns just the first byte of the hash as a two-character hex string.
124    ///
125    /// # Returns
126    /// A string representing the directory name.
127    fn to_object_dir(&self) -> String {
128        format!("{:02x}", self.as_bytes()[0])
129    }
130
131    /// Convert the hash value to a hexadecimal string.
132    ///
133    /// # Returns
134    /// The full hash as a hex string.
135    fn to_hex(&self) -> String {
136        hex::encode(self.as_bytes())
137    }
138
139    /// Convert the hash value to an identifier string with algorithm prefix.
140    ///
141    /// # Returns
142    /// A string in the format "algorithm:hexhash" (e.g., "sha256:abc123...").
143    fn to_id(&self) -> String {
144        format!("{}:{}", Self::ID, self.to_hex())
145    }
146}
147
148impl fmt::Debug for Sha256HashValue {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "sha256:{}", self.to_hex())
151    }
152}
153
154impl fmt::Debug for Sha512HashValue {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "sha512:{}", self.to_hex())
157    }
158}
159
160/// A SHA-256 hash value for fs-verity operations.
161///
162/// This is a 32-byte hash value using the SHA-256 algorithm.
163#[derive(Clone, Eq, FromBytes, Hash, Immutable, IntoBytes, KnownLayout, PartialEq, Unaligned)]
164#[repr(C)]
165pub struct Sha256HashValue([u8; 32]);
166
167impl From<Output<Sha256>> for Sha256HashValue {
168    fn from(value: Output<Sha256>) -> Self {
169        Self(value.into())
170    }
171}
172
173impl FsVerityHashValue for Sha256HashValue {
174    type Digest = Sha256;
175    const ALGORITHM: u8 = 1;
176    const EMPTY: Self = Self([0; 32]);
177    const ID: &str = "sha256";
178}
179
180/// A SHA-512 hash value for fs-verity operations.
181///
182/// This is a 64-byte hash value using the SHA-512 algorithm.
183#[derive(Clone, Eq, FromBytes, Hash, Immutable, IntoBytes, KnownLayout, PartialEq, Unaligned)]
184#[repr(C)]
185pub struct Sha512HashValue([u8; 64]);
186
187impl From<Output<Sha512>> for Sha512HashValue {
188    fn from(value: Output<Sha512>) -> Self {
189        Self(value.into())
190    }
191}
192
193impl FsVerityHashValue for Sha512HashValue {
194    type Digest = Sha512;
195    const ALGORITHM: u8 = 2;
196    const EMPTY: Self = Self([0; 64]);
197    const ID: &str = "sha512";
198}
199
200#[cfg(test)]
201mod test {
202    use super::*;
203
204    fn test_fsverity_hash<H: FsVerityHashValue>() {
205        let len = size_of::<H>();
206        let hexlen = len * 2;
207
208        let hex = H::EMPTY.to_hex();
209        assert_eq!(hex.as_bytes(), [b'0'].repeat(hexlen));
210
211        assert_eq!(H::EMPTY.to_id(), format!("{}:{}", H::ID, hex));
212        assert_eq!(format!("{:?}", H::EMPTY), format!("{}:{}", H::ID, hex));
213
214        assert_eq!(H::from_hex(&hex), Ok(H::EMPTY));
215
216        assert_eq!(H::from_hex("lol"), Err(FromHexError::OddLength));
217        assert_eq!(H::from_hex("lolo"), Err(FromHexError::InvalidStringLength));
218        assert_eq!(
219            H::from_hex([b'l'].repeat(hexlen)),
220            Err(FromHexError::InvalidHexCharacter { c: 'l', index: 0 })
221        );
222
223        assert_eq!(H::from_object_dir_and_basename(0, &hex[2..]), Ok(H::EMPTY));
224
225        assert_eq!(H::from_object_dir_and_basename(0, &hex[2..]), Ok(H::EMPTY));
226
227        assert_eq!(
228            H::from_object_dir_and_basename(0, "lol"),
229            Err(FromHexError::InvalidStringLength)
230        );
231
232        assert_eq!(
233            H::from_object_dir_and_basename(0, [b'l'].repeat(hexlen - 2)),
234            Err(FromHexError::InvalidHexCharacter { c: 'l', index: 0 })
235        );
236
237        assert_eq!(
238            H::from_object_pathname(format!("{}/{}", &hex[0..2], &hex[2..])),
239            Ok(H::EMPTY)
240        );
241
242        assert_eq!(
243            H::from_object_pathname(format!("../this/is/ignored/{}/{}", &hex[0..2], &hex[2..])),
244            Ok(H::EMPTY)
245        );
246
247        assert_eq!(
248            H::from_object_pathname(&hex),
249            Err(FromHexError::InvalidStringLength)
250        );
251
252        assert_eq!(
253            H::from_object_pathname("lol"),
254            Err(FromHexError::InvalidStringLength)
255        );
256
257        assert_eq!(
258            H::from_object_pathname([b'l'].repeat(hexlen + 1)),
259            Err(FromHexError::InvalidHexCharacter { c: 'l', index: 0 })
260        );
261
262        assert_eq!(
263            H::from_object_pathname(format!("{}0{}", &hex[0..2], &hex[2..])),
264            Err(FromHexError::InvalidHexCharacter { c: '0', index: 2 })
265        );
266    }
267
268    #[test]
269    fn test_sha256hashvalue() {
270        test_fsverity_hash::<Sha256HashValue>();
271    }
272
273    #[test]
274    fn test_sha512hashvalue() {
275        test_fsverity_hash::<Sha512HashValue>();
276    }
277}