Skip to main content

deps_core/
interpolation.rs

1//! Bounded property/variable values for manifest interpolation (issue #1481).
2//!
3//! Maven's `${property}` and Gradle's `version.ref`/`$name`/`${name}` substitutions each
4//! resolve through a `HashMap` keyed by property name. Without a per-value bound, a manifest
5//! declaring up to [`deps_core::MAX_DEPENDENCIES_PER_DOCUMENT`](crate::MAX_DEPENDENCIES_PER_DOCUMENT)
6//! kept dependencies, each resolving an unbounded property value, retains
7//! `kept_deps * value_len` bytes with no cap on the second factor (CWE-400). [`PropertyValue`]
8//! makes an oversized value unrepresentable in the map that feeds resolution, so every
9//! reference to it falls through the existing "unresolved" path instead of retaining the raw
10//! text.
11
12/// Maximum byte length of a single interpolated property/variable value.
13///
14/// Real version strings and version ranges are well under 100 bytes; 1 KiB leaves headroom
15/// for `systemPath`-style path values while still bounding worst-case retained memory to
16/// `MAX_DEPENDENCIES_PER_DOCUMENT * MAX_INTERPOLATED_VALUE_BYTES` per resolution pass.
17pub const MAX_INTERPOLATED_VALUE_BYTES: usize = 1024;
18
19/// A property/variable value that has been checked against
20/// [`MAX_INTERPOLATED_VALUE_BYTES`] and is guaranteed not to exceed it.
21///
22/// This is the type-level gate for issue #1481: `HashMap<String, PropertyValue>` (rather than
23/// `HashMap<String, String>`) as the map type that feeds Maven's `${property}` and Gradle's
24/// `version.ref`/`$name`/`${name}` resolution makes an oversized retained value
25/// unrepresentable, instead of relying on a runtime check at every call site.
26///
27/// # Examples
28///
29/// ```
30/// use deps_core::interpolation::{MAX_INTERPOLATED_VALUE_BYTES, PropertyValue};
31///
32/// let value = PropertyValue::new("1.2.3".to_string()).unwrap();
33/// assert_eq!(value.as_str(), "1.2.3");
34///
35/// let oversized = "x".repeat(MAX_INTERPOLATED_VALUE_BYTES + 1);
36/// assert!(PropertyValue::new(oversized).is_err());
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub struct PropertyValue(String);
40
41impl PropertyValue {
42    /// Wraps `value` as a `PropertyValue`, rejecting it if longer than
43    /// [`MAX_INTERPOLATED_VALUE_BYTES`].
44    ///
45    /// # Errors
46    ///
47    /// Returns [`OversizedPropertyValue`] if `value.len()` exceeds
48    /// [`MAX_INTERPOLATED_VALUE_BYTES`].
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use deps_core::interpolation::PropertyValue;
54    ///
55    /// assert!(PropertyValue::new("2.0.16".to_string()).is_ok());
56    /// ```
57    pub fn new(value: String) -> Result<Self, OversizedPropertyValue> {
58        if value.len() > MAX_INTERPOLATED_VALUE_BYTES {
59            return Err(OversizedPropertyValue { len: value.len() });
60        }
61        Ok(Self(value))
62    }
63
64    /// Returns the property value as a string slice.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use deps_core::interpolation::PropertyValue;
70    ///
71    /// let value = PropertyValue::new("3.2.0".to_string()).unwrap();
72    /// assert_eq!(value.as_str(), "3.2.0");
73    /// ```
74    #[must_use]
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78
79    /// Returns the byte length of the property value.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use deps_core::interpolation::PropertyValue;
85    ///
86    /// let value = PropertyValue::new("3.2.0".to_string()).unwrap();
87    /// assert_eq!(value.len(), 5);
88    /// ```
89    #[must_use]
90    pub fn len(&self) -> usize {
91        self.0.len()
92    }
93
94    /// Returns whether the property value is empty.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use deps_core::interpolation::PropertyValue;
100    ///
101    /// let value = PropertyValue::new(String::new()).unwrap();
102    /// assert!(value.is_empty());
103    /// ```
104    #[must_use]
105    pub fn is_empty(&self) -> bool {
106        self.0.is_empty()
107    }
108}
109
110/// Inserts `value` into `map` under `key`, dropping it instead if it exceeds
111/// [`MAX_INTERPOLATED_VALUE_BYTES`] (#1481) rather than retaining an unbounded value.
112///
113/// Every ecosystem's own bounded-property map (`deps-maven`'s `properties`, `deps-gradle`'s
114/// `version_refs` and `gradle.properties` map) shares this same insert-time gate instead of
115/// each reimplementing the check-log-drop pattern independently. Logs at `debug` with the
116/// value's length and `context` only — never the value's content — so a caller can tell
117/// which map dropped a value without leaking a potentially large or sensitive payload into
118/// tracing output. A dropped value falls through to whatever "unresolved" behavior the
119/// caller's own lookup path already has for a missing key.
120///
121/// # Examples
122///
123/// ```
124/// use deps_core::interpolation::{MAX_INTERPOLATED_VALUE_BYTES, insert_bounded};
125/// use std::collections::HashMap;
126///
127/// let mut map = HashMap::new();
128/// insert_bounded(&mut map, "ver".to_string(), "1.2.3".to_string(), "example");
129/// assert_eq!(map.get("ver").map(|v| v.as_str()), Some("1.2.3"));
130///
131/// insert_bounded(
132///     &mut map,
133///     "big".to_string(),
134///     "x".repeat(MAX_INTERPOLATED_VALUE_BYTES + 1),
135///     "example",
136/// );
137/// assert!(!map.contains_key("big"));
138/// ```
139pub fn insert_bounded(
140    map: &mut std::collections::HashMap<String, PropertyValue>,
141    key: String,
142    value: String,
143    context: &str,
144) {
145    match PropertyValue::new(value) {
146        Ok(value) => {
147            map.insert(key, value);
148        }
149        Err(err) => {
150            tracing::debug!(
151                context,
152                len = err.len,
153                "property value exceeds cap; dropping"
154            );
155        }
156    }
157}
158
159/// A property/variable value exceeded [`MAX_INTERPOLATED_VALUE_BYTES`] and was rejected by
160/// [`PropertyValue::new`].
161///
162/// # Examples
163///
164/// ```
165/// use deps_core::interpolation::{MAX_INTERPOLATED_VALUE_BYTES, PropertyValue};
166///
167/// let err = PropertyValue::new("x".repeat(MAX_INTERPOLATED_VALUE_BYTES + 1)).unwrap_err();
168/// assert_eq!(err.len, MAX_INTERPOLATED_VALUE_BYTES + 1);
169/// ```
170#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
171#[error("property value length {len} exceeds cap of {MAX_INTERPOLATED_VALUE_BYTES} bytes")]
172pub struct OversizedPropertyValue {
173    /// The rejected value's byte length.
174    pub len: usize,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn accepts_value_at_exact_cap() {
183        let value = "x".repeat(MAX_INTERPOLATED_VALUE_BYTES);
184        assert!(PropertyValue::new(value).is_ok());
185    }
186
187    #[test]
188    fn rejects_value_one_byte_over_cap() {
189        let value = "x".repeat(MAX_INTERPOLATED_VALUE_BYTES + 1);
190        let err = PropertyValue::new(value).unwrap_err();
191        assert_eq!(err.len, MAX_INTERPOLATED_VALUE_BYTES + 1);
192    }
193}