Skip to main content

amiss_git/
resources.rs

1use std::collections::BTreeSet;
2
3use amiss_wire::controls::ResourceName;
4
5use crate::Error;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct GitLimits {
9    pub inflated_object_bytes: u64,
10    pub compressed_stream_bytes: u64,
11    pub aggregate_compressed_bytes: u64,
12    pub pack_directory_entries: u64,
13    pub pack_files: u64,
14    pub pack_index_bytes: u64,
15    pub aggregate_pack_index_bytes: u64,
16    pub delta_depth: u64,
17    pub index_bytes: u64,
18    pub tree_entries_per_snapshot: u64,
19    pub raw_path_bytes: u64,
20}
21
22/// A smaller contextual inflated cap (a document, target, or control blob)
23/// that applies before the general Git object cap when the object header
24/// declares a larger value.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct ValueCap {
27    pub resource: ResourceName,
28    pub limit: u64,
29}
30
31impl GitLimits {
32    pub const CONTRACT: Self = Self {
33        inflated_object_bytes: 134_217_728,
34        compressed_stream_bytes: 268_435_456,
35        aggregate_compressed_bytes: 2_147_483_648,
36        pack_directory_entries: 8_192,
37        pack_files: 4_096,
38        pack_index_bytes: 536_870_912,
39        aggregate_pack_index_bytes: 1_073_741_824,
40        delta_depth: 128,
41        index_bytes: 268_435_456,
42        tree_entries_per_snapshot: 1_000_000,
43        raw_path_bytes: 4_096,
44    };
45}
46
47impl Default for GitLimits {
48    fn default() -> Self {
49        Self::CONTRACT
50    }
51}
52
53pub(crate) fn crossing(resource: ResourceName, configured_limit: u64, observed: u64) -> Error {
54    Error::ResourceLimit {
55        resource,
56        configured_limit,
57        observed_lower_bound: observed,
58    }
59}
60
61/// Byte charging for one evaluation side: each member is charged once per
62/// selected member key, so cache hits never recharge.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct GitResources {
65    limits: GitLimits,
66    aggregate_compressed: u64,
67    aggregate_index: u64,
68    charged: BTreeSet<String>,
69}
70
71impl GitResources {
72    #[must_use]
73    pub fn new(limits: GitLimits) -> Self {
74        Self {
75            limits,
76            aggregate_compressed: 0,
77            aggregate_index: 0,
78            charged: BTreeSet::new(),
79        }
80    }
81
82    #[must_use]
83    pub const fn limits(&self) -> GitLimits {
84        self.limits
85    }
86
87    /// # Errors
88    ///
89    /// Fails when the stream crosses the per-stream cap or the running total
90    /// crosses the aggregate cap.
91    pub fn charge_compressed(&mut self, member: &str, bytes: u64) -> Result<(), Error> {
92        if bytes > self.limits.compressed_stream_bytes {
93            return Err(crossing(
94                ResourceName::GitCompressedObjectBytes,
95                self.limits.compressed_stream_bytes,
96                bytes,
97            ));
98        }
99        if self.charged.contains(member) {
100            return Ok(());
101        }
102        let total = self.aggregate_compressed.saturating_add(bytes);
103        if total > self.limits.aggregate_compressed_bytes {
104            return Err(crossing(
105                ResourceName::AggregateGitCompressedObjectBytesPerEvaluation,
106                self.limits.aggregate_compressed_bytes,
107                total,
108            ));
109        }
110        self.aggregate_compressed = total;
111        self.charged.insert(member.to_owned());
112        Ok(())
113    }
114
115    /// # Errors
116    ///
117    /// Fails when one index crosses the per-index cap or the running total
118    /// crosses the aggregate cap.
119    pub fn charge_index(&mut self, member: &str, bytes: u64) -> Result<(), Error> {
120        if bytes > self.limits.pack_index_bytes {
121            return Err(crossing(
122                ResourceName::GitPackIndexBytes,
123                self.limits.pack_index_bytes,
124                bytes,
125            ));
126        }
127        let key = format!("idx:{member}");
128        if self.charged.contains(&key) {
129            return Ok(());
130        }
131        let total = self.aggregate_index.saturating_add(bytes);
132        if total > self.limits.aggregate_pack_index_bytes {
133            return Err(crossing(
134                ResourceName::AggregateGitPackIndexBytes,
135                self.limits.aggregate_pack_index_bytes,
136                total,
137            ));
138        }
139        self.aggregate_index = total;
140        self.charged.insert(key);
141        Ok(())
142    }
143}