1use bevy_reflect::Reflect;
12use std::collections::{BTreeMap, BTreeSet};
13
14use base64::{
15 Engine, alphabet,
16 engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
17 prelude::BASE64_STANDARD,
18};
19use bevy_ecs::prelude::*;
20use rig_core::message::DocumentSourceKind;
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23
24#[derive(
26 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Reflect,
27)]
28#[serde(try_from = "String", into = "String")]
29pub struct BinaryId(pub [u8; 32]);
30
31impl BinaryId {
32 pub fn of(bytes: &[u8]) -> Self {
34 Self(Sha256::digest(bytes).into())
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct BinaryLimits {
41 pub per_asset: usize,
43 pub total: usize,
45 pub count: usize,
47}
48
49impl Default for BinaryLimits {
50 fn default() -> Self {
51 Self {
52 per_asset: 64 * 1024 * 1024,
53 total: 256 * 1024 * 1024,
54 count: 65_536,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
61pub enum BinaryError {
62 #[error("binary content exceeds its allocation limit")]
64 Limit,
65 #[error("invalid base64 binary source")]
67 Base64,
68 #[error("unresolved binary asset")]
70 Missing,
71 #[error("binary asset hash mismatch")]
73 Corrupt,
74 #[error("invalid base64 spelling metadata")]
76 Spelling,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
81pub enum BinaryEncoding {
82 Raw,
84 Base64 {
87 padding: u8,
89 last_symbol: Option<u8>,
91 },
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
97pub enum PartSource {
98 Url(String),
100 FileId(String),
102 String(String),
104 Unknown,
106 Binary {
108 id: BinaryId,
109 encoding: BinaryEncoding,
110 },
111}
112
113#[derive(Resource, Debug, Default)]
117pub struct BinaryAssets {
118 payloads: BTreeMap<BinaryId, Vec<u8>>,
119 spellings: BTreeMap<[u8; 32], BinaryId>,
122 bytes: usize,
123 limits: BinaryLimits,
124}
125
126impl BinaryAssets {
127 pub fn with_limits(limits: BinaryLimits) -> Self {
129 Self {
130 limits,
131 ..Self::default()
132 }
133 }
134
135 pub fn byte_len(&self) -> usize {
137 self.bytes
138 }
139
140 pub fn len(&self) -> usize {
142 self.payloads.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.payloads.is_empty()
148 }
149
150 pub fn get(&self, id: BinaryId) -> Result<&[u8], BinaryError> {
152 self.payloads
153 .get(&id)
154 .map(Vec::as_slice)
155 .ok_or(BinaryError::Missing)
156 }
157
158 pub fn insert(&mut self, bytes: Vec<u8>) -> Result<BinaryId, BinaryError> {
161 if bytes.len() > self.limits.per_asset {
162 return Err(BinaryError::Limit);
163 }
164 let id = BinaryId::of(&bytes);
165 if let Some(existing) = self.payloads.get(&id) {
166 return if *existing == bytes {
167 Ok(id)
168 } else {
169 Err(BinaryError::Corrupt)
170 };
171 }
172 let total = self
173 .bytes
174 .checked_add(bytes.len())
175 .ok_or(BinaryError::Limit)?;
176 if total > self.limits.total || self.payloads.len() >= self.limits.count {
177 return Err(BinaryError::Limit);
178 }
179 self.bytes = total;
180 self.payloads.insert(id, bytes);
181 Ok(id)
182 }
183
184 pub fn intern(&mut self, source: DocumentSourceKind) -> Result<PartSource, BinaryError> {
188 Ok(match source {
189 DocumentSourceKind::Url(value) => PartSource::Url(value),
190 DocumentSourceKind::FileId(value) => PartSource::FileId(value),
191 DocumentSourceKind::String(value) => PartSource::String(value),
192 DocumentSourceKind::Unknown => PartSource::Unknown,
193 DocumentSourceKind::Raw(bytes) => PartSource::Binary {
194 id: self.insert(bytes)?,
195 encoding: BinaryEncoding::Raw,
196 },
197 DocumentSourceKind::Base64(value) => {
198 let max_encoded = self
199 .limits
200 .per_asset
201 .checked_add(2)
202 .and_then(|n| n.checked_div(3))
203 .and_then(|n| n.checked_mul(4))
204 .ok_or(BinaryError::Limit)?;
205 if value.len() > max_encoded {
206 return Err(BinaryError::Limit);
207 }
208 let key: [u8; 32] = Sha256::digest(value.as_bytes()).into();
209 let id = if let Some(id) = self.spellings.get(&key) {
210 *id
211 } else {
212 let decoder = GeneralPurpose::new(
213 &alphabet::STANDARD,
214 GeneralPurposeConfig::new()
215 .with_decode_padding_mode(DecodePaddingMode::Indifferent)
216 .with_decode_allow_trailing_bits(true),
217 );
218 let bytes = decoder.decode(&value).map_err(|_| BinaryError::Base64)?;
219 let id = self.insert(bytes)?;
220 if self.spellings.len() < self.limits.count {
223 self.spellings.insert(key, id);
224 }
225 id
226 };
227 let canonical = BASE64_STANDARD.encode(self.get(id)?);
228 let bare = value.trim_end_matches('=');
229 let padding =
230 u8::try_from(value.len() - bare.len()).map_err(|_| BinaryError::Base64)?;
231 let last_symbol = (bare.as_bytes().last()
232 != canonical.trim_end_matches('=').as_bytes().last())
233 .then(|| bare.as_bytes().last().copied())
234 .flatten();
235 PartSource::Binary {
236 id,
237 encoding: BinaryEncoding::Base64 {
238 padding,
239 last_symbol,
240 },
241 }
242 }
243 })
244 }
245
246 pub(crate) fn resolved_len(&self, source: &PartSource) -> Result<usize, BinaryError> {
248 let PartSource::Binary { id, encoding } = source else {
249 return Ok(match source {
250 PartSource::Url(value) | PartSource::FileId(value) | PartSource::String(value) => {
251 value.len()
252 }
253 _ => 0,
254 });
255 };
256 let bytes = self.get(*id)?;
257 let BinaryEncoding::Base64 {
258 padding,
259 last_symbol,
260 } = encoding
261 else {
262 return Ok(bytes.len());
263 };
264 let remainder = bytes.len() % 3;
265 let required_padding = (3 - remainder) % 3;
266 if usize::from(*padding) > required_padding {
267 return Err(BinaryError::Spelling);
268 }
269 if let Some(symbol) = last_symbol {
270 let last = bytes.last().ok_or(BinaryError::Spelling)?;
271 let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
272 let actual = alphabet
273 .iter()
274 .position(|value| value == symbol)
275 .ok_or(BinaryError::Spelling)?;
276 let (canonical, unused) = match remainder {
277 1 => (usize::from(last & 3) << 4, 4),
278 2 => (usize::from(last & 15) << 2, 2),
279 _ => (usize::from(last & 63), 0),
280 };
281 if actual >> unused != canonical >> unused {
282 return Err(BinaryError::Spelling);
283 }
284 }
285 (bytes.len() / 3)
286 .checked_mul(4)
287 .and_then(|size| size.checked_add(if remainder == 0 { 0 } else { remainder + 1 }))
288 .and_then(|size| size.checked_add(usize::from(*padding)))
289 .ok_or(BinaryError::Limit)
290 }
291
292 pub fn resolve(&self, source: &PartSource) -> Result<DocumentSourceKind, BinaryError> {
295 self.resolved_len(source)?;
296 Ok(match source {
297 PartSource::Url(value) => DocumentSourceKind::Url(value.clone()),
298 PartSource::FileId(value) => DocumentSourceKind::FileId(value.clone()),
299 PartSource::String(value) => DocumentSourceKind::String(value.clone()),
300 PartSource::Unknown => DocumentSourceKind::Unknown,
301 PartSource::Binary { id, encoding } => {
302 let bytes = self.get(*id)?;
303 match encoding {
304 BinaryEncoding::Raw => DocumentSourceKind::Raw(bytes.to_vec()),
305 BinaryEncoding::Base64 {
306 padding,
307 last_symbol,
308 } => {
309 let canonical = BASE64_STANDARD.encode(bytes);
310 let required_padding =
311 canonical.len() - canonical.trim_end_matches('=').len();
312 if usize::from(*padding) > required_padding {
313 return Err(BinaryError::Spelling);
314 }
315 let mut value = canonical.trim_end_matches('=').as_bytes().to_vec();
316 if let Some(symbol) = last_symbol {
317 let last = value.last_mut().ok_or(BinaryError::Spelling)?;
318 let alphabet =
321 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
322 let before = alphabet
323 .iter()
324 .position(|b| b == last)
325 .ok_or(BinaryError::Spelling)?;
326 let after = alphabet
327 .iter()
328 .position(|b| b == symbol)
329 .ok_or(BinaryError::Spelling)?;
330 let unused = match bytes.len() % 3 {
331 1 => 4,
332 2 => 2,
333 _ => 0,
334 };
335 if (before >> unused) != (after >> unused) {
336 return Err(BinaryError::Spelling);
337 }
338 *last = *symbol;
339 }
340 value.extend(std::iter::repeat_n(b'=', usize::from(*padding)));
341 DocumentSourceKind::Base64(
342 String::from_utf8(value).map_err(|_| BinaryError::Spelling)?,
343 )
344 }
345 }
346 }
347 })
348 }
349
350 pub fn retain(&mut self, roots: impl IntoIterator<Item = BinaryId>) -> Result<(), BinaryError> {
353 let roots: BTreeSet<_> = roots.into_iter().collect();
354 if roots.iter().any(|id| !self.payloads.contains_key(id)) {
355 return Err(BinaryError::Missing);
356 }
357 self.payloads.retain(|id, _| roots.contains(id));
358 self.spellings.retain(|_, id| roots.contains(id));
359 self.bytes = self.payloads.values().map(Vec::len).sum();
360 Ok(())
361 }
362
363 pub fn iter(&self) -> impl Iterator<Item = (BinaryId, &[u8])> {
365 self.payloads
366 .iter()
367 .map(|(id, bytes)| (*id, bytes.as_slice()))
368 }
369
370 pub fn from_payloads(
374 payloads: impl IntoIterator<Item = (BinaryId, Vec<u8>)>,
375 limits: BinaryLimits,
376 ) -> Result<Self, BinaryError> {
377 let mut assets = Self::with_limits(limits);
378 for (id, bytes) in payloads {
379 if bytes.len() > limits.per_asset {
380 return Err(BinaryError::Limit);
381 }
382 if BinaryId::of(&bytes) != id {
383 return Err(BinaryError::Corrupt);
384 }
385 if assets.payloads.contains_key(&id) {
386 return Err(BinaryError::Corrupt);
387 }
388 assets.insert(bytes)?;
389 }
390 Ok(assets)
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct BinaryRecord {
398 pub id: BinaryId,
400 pub data: String,
402}
403
404impl BinaryAssets {
405 pub fn snapshot(&self) -> Vec<BinaryRecord> {
407 self.iter()
408 .map(|(id, bytes)| BinaryRecord {
409 id,
410 data: BASE64_STANDARD.encode(bytes),
411 })
412 .collect()
413 }
414
415 pub fn merged(&self, records: &[BinaryRecord]) -> Result<Self, BinaryError> {
419 if records.len() > self.limits.count {
420 return Err(BinaryError::Limit);
421 }
422 let mut merged = Self::from_payloads(
423 self.iter().map(|(id, bytes)| (id, bytes.to_vec())),
424 self.limits,
425 )?;
426 let mut seen = BTreeSet::new();
427 for record in records {
428 let max_encoded = self
429 .limits
430 .per_asset
431 .checked_add(2)
432 .and_then(|n| n.checked_div(3))
433 .and_then(|n| n.checked_mul(4))
434 .ok_or(BinaryError::Limit)?;
435 if record.data.len() > max_encoded {
436 return Err(BinaryError::Limit);
437 }
438 if !seen.insert(record.id) {
439 return Err(BinaryError::Corrupt);
440 }
441 let source = merged.intern(DocumentSourceKind::Base64(record.data.clone()))?;
442 if !matches!(source, PartSource::Binary { id, .. } if id == record.id) {
443 return Err(BinaryError::Corrupt);
444 }
445 }
446 Ok(merged)
447 }
448}
449
450impl From<BinaryId> for String {
451 fn from(id: BinaryId) -> Self {
452 let mut value = String::with_capacity(64);
453 for byte in id.0 {
454 for half in [byte >> 4, byte & 15] {
455 value.push(char::from(if half < 10 {
456 b'0' + half
457 } else {
458 b'a' + half - 10
459 }));
460 }
461 }
462 value
463 }
464}
465
466impl TryFrom<String> for BinaryId {
467 type Error = BinaryError;
468 fn try_from(value: String) -> Result<Self, Self::Error> {
469 if value.len() != 64
470 || !value
471 .bytes()
472 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
473 {
474 return Err(BinaryError::Corrupt);
475 }
476 let mut id = [0; 32];
477 for (position, byte) in id.iter_mut().enumerate() {
478 let digits = value
479 .get(position * 2..position * 2 + 2)
480 .ok_or(BinaryError::Corrupt)?;
481 *byte = u8::from_str_radix(digits, 16).map_err(|_| BinaryError::Corrupt)?;
482 }
483 Ok(Self(id))
484 }
485}