dir_structure/
versioned_hash.rs

1//! A module for working with versioned values.
2//!
3//! Similar to [`versioned`](crate::versioned), but uses hashing to determine cleanliness of a value.
4//!
5//! See the [`VersionedHash`] struct for more details.
6
7use std::collections::hash_map::DefaultHasher;
8#[cfg(feature = "async")]
9use std::future::Future;
10use std::hash::Hash;
11use std::hash::Hasher;
12use std::marker;
13use std::ops::Deref;
14use std::ops::DerefMut;
15use std::path::Path;
16use std::pin::Pin;
17
18use crate::error::VfsResult;
19use crate::prelude::*;
20#[cfg(feature = "async")]
21use crate::traits::async_vfs::VfsAsync;
22#[cfg(feature = "async")]
23use crate::traits::async_vfs::WriteSupportingVfsAsync;
24use crate::traits::vfs;
25use crate::traits::vfs::PathType;
26#[cfg(feature = "async")]
27use crate::traits::vfs::VfsCore;
28
29/// A value with a hash to determine if it has changed.
30///
31/// You can use any hasher that implements [`std::hash::Hasher`] and [`std::default::Default`].
32/// By default, it uses [`std::collections::hash_map::DefaultHasher`].
33///
34/// To access the inner value, you can use [`Deref`] / [`DerefMut`]
35/// or the [`into_inner`](VersionedHash::into_inner) method.
36///
37/// # Examples
38///
39/// ```
40/// use std::path::PathBuf;
41/// use dir_structure::versioned_hash::VersionedHash;
42///
43/// let mut vh = VersionedHash::<String>::new_clean(PathBuf::from("test.txt"), "Hello, world!".to_owned());
44/// assert!(vh.is_clean());
45///
46/// vh.push_str(" Modified.");
47/// assert!(vh.is_dirty());
48/// assert!(!vh.is_clean());
49/// ```
50#[cfg_attr(feature = "assert_eq", derive(assert_eq::AssertEq))]
51pub struct VersionedHash<T: Hash, P: PathType + ?Sized = Path, H: Hasher + Default = DefaultHasher>
52{
53    value: T,
54    hash: u64,
55    path: P::OwnedPath,
56    _hasher: marker::PhantomData<H>,
57}
58
59impl<T: Hash, P: PathType + ?Sized, H: Hasher + Default> VersionedHash<T, P, H> {
60    /// Get the inner value. You can also use [`Deref`] / [`DerefMut`]
61    /// to get references to the inner value.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use std::path::PathBuf;
67    /// use dir_structure::versioned_hash::VersionedHash;
68    ///
69    /// let vh = VersionedHash::<String>::new_clean(PathBuf::from("test.txt"), "Hello, world!".to_owned());
70    /// assert_eq!(vh.into_inner(), "Hello, world!".to_owned());
71    ///
72    /// let vh = VersionedHash::<String>::new_dirty(PathBuf::from("test.txt"), "Hello, world!".to_owned());
73    /// assert_eq!(vh.into_inner(), "Hello, world!".to_owned());
74    /// ```
75    pub fn into_inner(self) -> T {
76        self.value
77    }
78
79    fn new_with_hash(path: P::OwnedPath, value: T, hash: u64) -> Self {
80        Self {
81            value,
82            hash,
83            path,
84            _hasher: marker::PhantomData,
85        }
86    }
87
88    fn hash_value(value: &T) -> u64 {
89        let mut hasher = H::default();
90        T::hash(value, &mut hasher);
91        hasher.finish()
92    }
93
94    /// Create a new clean value.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use std::path::PathBuf;
100    /// use dir_structure::versioned_hash::VersionedHash;
101    ///
102    /// let vh = VersionedHash::<String>::new_clean(PathBuf::from("test.txt"), "Hello, world!".to_owned());
103    /// assert!(vh.is_clean());
104    /// assert!(!vh.is_dirty());
105    /// ```
106    pub fn new_clean(path: P::OwnedPath, value: T) -> Self {
107        let hash = Self::hash_value(&value);
108
109        Self::new_with_hash(path, value, hash)
110    }
111
112    /// Create a new dirty value.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use std::path::PathBuf;
118    /// use dir_structure::versioned_hash::VersionedHash;
119    ///
120    /// let vh = VersionedHash::<String>::new_dirty(PathBuf::from("test.txt"), "Hello, world!".to_owned());
121    /// assert!(!vh.is_clean());
122    /// assert!(vh.is_dirty());
123    /// ```
124    pub fn new_dirty(path: P::OwnedPath, value: T) -> Self {
125        let hash = Self::hash_value(&value);
126        Self::new_with_hash(path, value, hash.wrapping_add(1))
127    }
128
129    /// Checks if the value has been modified since being read.
130    /// Returns true if the value is clean (i.e., has not been modified).
131    /// Returns false if the value is dirty (i.e., has been modified).
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// use std::path::PathBuf;
137    /// use dir_structure::versioned_hash::VersionedHash;
138    ///
139    /// let vh = VersionedHash::<String>::new_clean(PathBuf::from("test.txt"), "Hello, world!".to_owned());
140    /// assert!(vh.is_clean());
141    ///
142    /// let vh = VersionedHash::<String>::new_dirty(PathBuf::from("test.txt"), "Hello, world!".to_owned());
143    /// assert!(!vh.is_clean());
144    /// ```
145    pub fn is_clean(&self) -> bool {
146        self.hash == Self::hash_value(&self.value)
147    }
148
149    /// Checks if the value has been modified since being read.
150    /// Returns true if the value is dirty (i.e., has been modified).
151    /// Returns false if the value is clean (i.e., has not been modified).
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use std::path::PathBuf;
157    /// use dir_structure::versioned_hash::VersionedHash;
158    ///
159    /// let vh = VersionedHash::<String>::new_clean(PathBuf::from("test.txt"), "Hello, world!".to_owned());
160    /// assert!(!vh.is_dirty());
161    ///
162    /// let vh = VersionedHash::<String>::new_dirty(PathBuf::from("test.txt"), "Hello, world!".to_owned());
163    /// assert!(vh.is_dirty());
164    /// ```
165    pub fn is_dirty(&self) -> bool {
166        !self.is_clean()
167    }
168
169    /// Resets the hash to the current value's hash.
170    ///
171    /// # Safety
172    ///
173    /// This function is unsafe because it can lead to changes not being written if the value has changed.
174    /// Ideally this function should only be called after writing the value to disk.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use std::path::PathBuf;
180    /// use dir_structure::versioned_hash::VersionedHash;
181    ///
182    /// let mut vh = VersionedHash::<String>::new_clean(PathBuf::from("test.txt"), "Hello, world!".to_owned());
183    /// assert!(vh.is_clean());
184    ///
185    /// vh.push_str(" Modified.");
186    /// assert!(vh.is_dirty());
187    /// assert!(!vh.is_clean());
188    ///
189    /// unsafe {
190    ///   vh.reset();
191    /// }
192    /// assert!(vh.is_clean());
193    /// assert!(!vh.is_dirty());
194    /// ```
195    #[expect(unsafe_code, reason = "Inherently unsafe function, see documentation")]
196    pub unsafe fn reset(&mut self) {
197        self.hash = Self::hash_value(&self.value);
198    }
199}
200
201impl<T: Hash, P: PathType + ?Sized, H: Hasher + Default> Deref for VersionedHash<T, P, H> {
202    type Target = T;
203
204    fn deref(&self) -> &Self::Target {
205        &self.value
206    }
207}
208
209impl<T: Hash, P: PathType + ?Sized, H: Hasher + Default> DerefMut for VersionedHash<T, P, H> {
210    fn deref_mut(&mut self) -> &mut Self::Target {
211        &mut self.value
212    }
213}
214
215impl<'a, T, H, Vfs: vfs::Vfs<'a>> ReadFrom<'a, Vfs> for VersionedHash<T, Vfs::Path, H>
216where
217    T: ReadFrom<'a, Vfs> + Hash + 'a,
218    H: Hasher + Default + 'a,
219{
220    fn read_from(path: &Vfs::Path, vfs: Pin<&'a Vfs>) -> VfsResult<Self, Vfs> {
221        let value = T::read_from(path, vfs)?;
222        let mut hasher = H::default();
223        T::hash(&value, &mut hasher);
224        let hash = hasher.finish();
225        Ok(VersionedHash {
226            value,
227            hash,
228            path: path.owned(),
229            _hasher: marker::PhantomData,
230        })
231    }
232}
233
234impl<'a, T, H, Vfs: vfs::WriteSupportingVfs<'a>> WriteTo<'a, Vfs> for VersionedHash<T, Vfs::Path, H>
235where
236    T: WriteTo<'a, Vfs> + Hash,
237    Vfs::Path: PartialEq,
238    H: Hasher + Default,
239{
240    fn write_to(&self, path: &Vfs::Path, vfs: Pin<&'a Vfs>) -> VfsResult<(), Vfs> {
241        if self.path.as_ref() == path && self.is_clean() {
242            return Ok(());
243        }
244
245        T::write_to(&self.value, path, vfs)
246    }
247}
248
249#[cfg(feature = "async")]
250#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
251impl<'a, T, H, Vfs: VfsAsync + 'a> ReadFromAsync<'a, Vfs> for VersionedHash<T, Vfs::Path, H>
252where
253    T: ReadFromAsync<'a, Vfs> + Hash + 'a,
254    H: Hasher + Default + 'a,
255{
256    type Future
257        = Pin<Box<dyn Future<Output = VfsResult<Self, Vfs>> + Send + 'a>>
258    where
259        Self: 'a;
260    fn read_from_async(
261        path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
262        vfs: Pin<&'a Vfs>,
263    ) -> Self::Future {
264        use std::future::poll_fn;
265
266        let mut fut = Box::pin(T::read_from_async(path.clone(), vfs));
267
268        Box::pin(poll_fn(move |cx| {
269            fut.as_mut().poll(cx).map_ok(|value| {
270                let path = path.clone();
271                let mut hasher = H::default();
272                T::hash(&value, &mut hasher);
273                let hash = hasher.finish();
274                VersionedHash {
275                    value,
276                    hash,
277                    path,
278                    _hasher: marker::PhantomData,
279                }
280            })
281        }))
282    }
283}
284
285#[cfg(feature = "async")]
286#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
287impl<'a, T, P, H, Vfs: WriteSupportingVfsAsync<Path = P> + 'a> WriteToAsync<'a, Vfs>
288    for VersionedHash<T, P, H>
289where
290    T: WriteToAsync<'a, Vfs> + Hash + 'a,
291    P: PathType + ?Sized + PartialEq + 'a,
292    H: Hasher + Default + 'a,
293{
294    type Future
295        = Pin<Box<dyn Future<Output = VfsResult<(), Vfs>> + Send + 'a>>
296    where
297        Self: 'a;
298
299    fn write_to_async(
300        self,
301        path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
302        vfs: Pin<&'a Vfs>,
303    ) -> Self::Future {
304        if self.path.as_ref() == path.as_ref() && self.is_clean() {
305            return Box::pin(async { Ok(()) });
306        }
307
308        let fut = Box::pin(T::write_to_async(self.value, path, vfs));
309
310        Box::pin(fut)
311    }
312}
313
314#[cfg(feature = "async")]
315#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
316impl<'a, T, H, Vfs: WriteSupportingVfsAsync + 'a> WriteToAsyncRef<'a, Vfs>
317    for VersionedHash<T, Vfs::Path, H>
318where
319    T: WriteToAsyncRef<'a, Vfs> + Hash + 'a,
320    H: Hasher + Default + 'a,
321{
322    type Future<'f>
323        = Pin<Box<dyn Future<Output = VfsResult<(), Vfs>> + Send + 'f>>
324    where
325        Self: 'f,
326        'a: 'f,
327        Vfs: 'f;
328
329    fn write_to_async_ref<'f>(
330        &'f self,
331        path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
332        vfs: Pin<&'f Vfs>,
333    ) -> Self::Future<'f>
334    where
335        'a: 'f,
336    {
337        if self.path == path && self.is_clean() {
338            return Box::pin(async { Ok(()) });
339        }
340
341        let fut = Box::pin(T::write_to_async_ref(&self.value, path, vfs));
342
343        Box::pin(fut)
344    }
345}