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}