Skip to main content

dex_blob_cache/
config.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Super Durable Source License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Super-Durable-1.0
8
9use std::path::{Path, PathBuf};
10
11use super::BlobCacheError;
12
13pub(crate) const MAX_BLOB_ID_BYTES: usize = 1 << 20;
14const DEFAULT_FREQUENCY_COUNTERS: i64 = 10_000;
15
16#[derive(Clone, Debug)]
17/// Configures one persistent [`crate::BlobCache`].
18///
19/// The directory contains cache-owned files and must not be shared with unrelated data. The byte
20/// limit bounds admitted payloads. Frequency counters tune admission accuracy; pass `0` to use the
21/// default of 10,000.
22pub struct BlobCacheConfig {
23    directory: PathBuf,
24    max_bytes: i64,
25    frequency_counters: usize,
26}
27
28impl BlobCacheConfig {
29    /// Validates and creates a cache configuration.
30    ///
31    /// # Arguments
32    ///
33    /// * `directory` - Cache-owned filesystem directory.
34    /// * `max_bytes` - Positive maximum number of payload bytes admitted to the cache.
35    /// * `frequency_counters` - Admission-policy counters, or `0` for the 10,000 default.
36    ///
37    /// # Errors
38    ///
39    /// Returns [`BlobCacheError::InvalidConfig`] when the directory is empty, the byte limit is not
40    /// positive, the counter count is negative, or the count cannot fit in [`usize`].
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use dex_blob_cache::BlobCacheConfig;
46    ///
47    /// let config = BlobCacheConfig::new("/tmp/dex-blobs", 64 * 1024 * 1024, 0)?;
48    /// assert_eq!(config.max_bytes(), 64 * 1024 * 1024);
49    /// assert_eq!(config.frequency_counters(), 10_000);
50    /// # Ok::<(), dex_blob_cache::BlobCacheError>(())
51    /// ```
52    pub fn new(
53        directory: impl Into<PathBuf>,
54        max_bytes: i64,
55        frequency_counters: i64,
56    ) -> Result<Self, BlobCacheError> {
57        let directory = directory.into();
58        if directory.as_os_str().is_empty() {
59            return Err(BlobCacheError::InvalidConfig(
60                "directory must not be empty".to_owned(),
61            ));
62        }
63        if max_bytes <= 0 {
64            return Err(BlobCacheError::InvalidConfig(
65                "max_bytes must be positive".to_owned(),
66            ));
67        }
68        if frequency_counters < 0 {
69            return Err(BlobCacheError::InvalidConfig(
70                "frequency_counters must not be negative".to_owned(),
71            ));
72        }
73        let frequency_counters = if frequency_counters == 0 {
74            DEFAULT_FREQUENCY_COUNTERS
75        } else {
76            frequency_counters
77        };
78        let frequency_counters = usize::try_from(frequency_counters).map_err(|_| {
79            BlobCacheError::InvalidConfig("frequency_counters overflows usize".to_owned())
80        })?;
81
82        Ok(Self {
83            directory,
84            max_bytes,
85            frequency_counters,
86        })
87    }
88
89    /// Returns the cache-owned filesystem directory.
90    pub fn directory(&self) -> &Path {
91        &self.directory
92    }
93
94    /// Returns the maximum admitted payload bytes.
95    pub fn max_bytes(&self) -> i64 {
96        self.max_bytes
97    }
98
99    /// Returns the admission-policy frequency counter count.
100    pub fn frequency_counters(&self) -> usize {
101        self.frequency_counters
102    }
103}
104
105pub(crate) fn validate_blob_id(blob_id: &str) -> Result<(), BlobCacheError> {
106    if blob_id.is_empty() {
107        return Err(BlobCacheError::InvalidBlob(
108            "blob ID must not be empty".to_owned(),
109        ));
110    }
111    if blob_id.len() > MAX_BLOB_ID_BYTES {
112        return Err(BlobCacheError::InvalidBlob(format!(
113            "blob ID exceeds {MAX_BLOB_ID_BYTES} bytes"
114        )));
115    }
116    Ok(())
117}