1use std::{collections::BTreeMap, fmt};
4
5pub const MAX_SOURCE_BUNDLE_FILES: usize = 4_096;
7
8pub const MAX_SOURCE_BUNDLE_FILE_BYTES: usize = 16 * 1024 * 1024;
10
11pub const MAX_SOURCE_BUNDLE_BYTES: usize = 64 * 1024 * 1024;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum SourceBundleErrorKind {
17 InvalidPath,
19 SourceTooLarge,
21 TooManySources,
23 BundleTooLarge,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct SourceBundleError {
30 path: String,
31 kind: SourceBundleErrorKind,
32 message: String,
33}
34
35impl SourceBundleError {
36 #[must_use]
38 pub fn path(&self) -> &str {
39 &self.path
40 }
41
42 #[must_use]
44 pub const fn kind(&self) -> SourceBundleErrorKind {
45 self.kind
46 }
47}
48
49impl fmt::Display for SourceBundleError {
50 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(formatter, "{}: {}", self.path, self.message)
52 }
53}
54
55impl std::error::Error for SourceBundleError {}
56
57#[derive(Clone, Debug, Default, Eq, PartialEq)]
64pub struct SourceBundle {
65 sources: BTreeMap<String, Vec<u8>>,
66 total_bytes: usize,
67}
68
69impl SourceBundle {
70 #[must_use]
72 pub const fn new() -> Self {
73 Self {
74 sources: BTreeMap::new(),
75 total_bytes: 0,
76 }
77 }
78
79 #[must_use]
81 pub fn len(&self) -> usize {
82 self.sources.len()
83 }
84
85 #[must_use]
87 pub fn is_empty(&self) -> bool {
88 self.sources.is_empty()
89 }
90
91 #[must_use]
93 pub const fn total_bytes(&self) -> usize {
94 self.total_bytes
95 }
96
97 #[must_use]
99 pub fn get(&self, path: &str) -> Option<&[u8]> {
100 self.sources.get(path).map(Vec::as_slice)
101 }
102
103 pub fn insert(
113 &mut self,
114 path: impl Into<String>,
115 source: impl Into<Vec<u8>>,
116 ) -> Result<Option<Vec<u8>>, SourceBundleError> {
117 let path = path.into();
118 validate_path(&path)?;
119 let source = source.into();
120 if source.len() > MAX_SOURCE_BUNDLE_FILE_BYTES {
121 return Err(error(
122 path,
123 SourceBundleErrorKind::SourceTooLarge,
124 format!(
125 "source has {} bytes; the maximum is {MAX_SOURCE_BUNDLE_FILE_BYTES}",
126 source.len()
127 ),
128 ));
129 }
130
131 let previous_len = self.sources.get(&path).map_or(0, Vec::len);
132 if previous_len == 0
133 && !self.sources.contains_key(&path)
134 && self.sources.len() == MAX_SOURCE_BUNDLE_FILES
135 {
136 return Err(error(
137 path,
138 SourceBundleErrorKind::TooManySources,
139 format!("bundle already contains {MAX_SOURCE_BUNDLE_FILES} sources"),
140 ));
141 }
142 let next_total = self
143 .total_bytes
144 .checked_sub(previous_len)
145 .and_then(|total| total.checked_add(source.len()))
146 .filter(|total| *total <= MAX_SOURCE_BUNDLE_BYTES)
147 .ok_or_else(|| {
148 error(
149 path.clone(),
150 SourceBundleErrorKind::BundleTooLarge,
151 format!("bundle would exceed {MAX_SOURCE_BUNDLE_BYTES} bytes"),
152 )
153 })?;
154
155 let previous = self.sources.insert(path, source);
156 self.total_bytes = next_total;
157 Ok(previous)
158 }
159
160 pub(crate) fn sources(&self) -> impl ExactSizeIterator<Item = (&str, &[u8])> {
161 self.sources
162 .iter()
163 .map(|(path, source)| (path.as_str(), source.as_slice()))
164 }
165}
166
167fn validate_path(path: &str) -> Result<(), SourceBundleError> {
168 let invalid = path.is_empty()
169 || path.starts_with('/')
170 || path.contains(['\\', '\0'])
171 || path
172 .split('/')
173 .any(|component| component.is_empty() || matches!(component, "." | ".."));
174 if invalid {
175 Err(error(
176 path.to_owned(),
177 SourceBundleErrorKind::InvalidPath,
178 "logical paths must be normalized relative paths using '/' separators".to_owned(),
179 ))
180 } else {
181 Ok(())
182 }
183}
184
185fn error(path: String, kind: SourceBundleErrorKind, message: String) -> SourceBundleError {
186 SourceBundleError {
187 path,
188 kind,
189 message,
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::{
196 MAX_SOURCE_BUNDLE_BYTES, MAX_SOURCE_BUNDLE_FILE_BYTES, MAX_SOURCE_BUNDLE_FILES,
197 SourceBundle, SourceBundleErrorKind,
198 };
199
200 #[test]
201 fn bundle_paths_are_exact_relative_posix_identities() {
202 let mut bundle = SourceBundle::new();
203 bundle
204 .insert("man3/foo.3", b"foo".to_vec())
205 .expect("insert normalized source");
206 assert_eq!(bundle.get("man3/foo.3"), Some(b"foo".as_slice()));
207
208 for path in [
209 "",
210 "/foo.1",
211 "foo\\bar.1",
212 "foo//bar.1",
213 "./foo.1",
214 "../foo.1",
215 ] {
216 let error = bundle
217 .insert(path, Vec::new())
218 .expect_err("reject unsafe logical path");
219 assert_eq!(error.kind(), SourceBundleErrorKind::InvalidPath, "{path:?}");
220 }
221 }
222
223 #[test]
224 fn replacing_a_source_updates_the_aggregate_size_transactionally() {
225 let mut bundle = SourceBundle::new();
226 assert_eq!(bundle.insert("foo.1", b"one".to_vec()), Ok(None));
227 assert_eq!(bundle.total_bytes(), 3);
228 assert_eq!(
229 bundle.insert("foo.1", b"replacement".to_vec()),
230 Ok(Some(b"one".to_vec()))
231 );
232 assert_eq!(bundle.total_bytes(), b"replacement".len());
233 }
234
235 #[test]
236 fn documented_bundle_limits_are_enforced_without_partial_mutation() {
237 let mut bundle = SourceBundle::new();
238 let error = bundle
239 .insert("oversized.1", vec![0; MAX_SOURCE_BUNDLE_FILE_BYTES + 1])
240 .expect_err("reject oversized source");
241 assert_eq!(error.kind(), SourceBundleErrorKind::SourceTooLarge);
242 assert!(bundle.is_empty());
243
244 for index in 0..MAX_SOURCE_BUNDLE_FILES {
245 bundle
246 .insert(format!("source-{index}.1"), Vec::new())
247 .expect("fill source count exactly");
248 }
249 let error = bundle
250 .insert("one-too-many.1", Vec::new())
251 .expect_err("reject source beyond count cap");
252 assert_eq!(error.kind(), SourceBundleErrorKind::TooManySources);
253 assert_eq!(bundle.len(), MAX_SOURCE_BUNDLE_FILES);
254
255 let mut aggregate = SourceBundle::new();
256 for index in 0..MAX_SOURCE_BUNDLE_BYTES / MAX_SOURCE_BUNDLE_FILE_BYTES {
257 aggregate
258 .insert(
259 format!("large-{index}.1"),
260 vec![0; MAX_SOURCE_BUNDLE_FILE_BYTES],
261 )
262 .expect("fill aggregate byte limit exactly");
263 }
264 let error = aggregate
265 .insert("aggregate-overflow.1", vec![0])
266 .expect_err("reject aggregate byte overflow");
267 assert_eq!(error.kind(), SourceBundleErrorKind::BundleTooLarge);
268 assert_eq!(aggregate.total_bytes(), MAX_SOURCE_BUNDLE_BYTES);
269 assert_eq!(aggregate.len(), 4);
270 }
271}