1use std::{
2 collections::BTreeSet,
3 io::{self, BufReader, Cursor, Write},
4};
5
6use sha3::{Digest, Sha3_256};
7
8use crate::{
9 AsyncObjectSource, Error, Hash32, ObjectKind, RecipeData, Result, TransformDecoderRegistry, parse_recipe, recipe::preflight_recipe_limits,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct FileReadLimits {
14 pub max_chunk_count: u64,
15 pub max_stored_stream_bytes: u64,
16 pub max_original_file_bytes: u64,
17}
18
19impl Default for FileReadLimits {
20 fn default() -> Self {
21 Self {
22 max_chunk_count: 1_000_000,
23 max_stored_stream_bytes: 256 * 1024 * 1024,
24 max_original_file_bytes: 256 * 1024 * 1024,
25 }
26 }
27}
28
29pub struct FileReader<'a, S: ?Sized> {
30 source: &'a S,
31 decoders: &'a TransformDecoderRegistry,
32 limits: FileReadLimits,
33}
34
35impl<'a, S: crate::ObjectSource + ?Sized> FileReader<'a, S> {
36 pub fn new(source: &'a S, decoders: &'a TransformDecoderRegistry, limits: FileReadLimits) -> Self {
37 Self { source, decoders, limits }
38 }
39
40 pub fn resolve_closure(&self, recipe_hash: Hash32) -> Result<BTreeSet<Hash32>> {
41 let recipe = self.load_recipe(recipe_hash)?;
42 let mut hashes = BTreeSet::from([recipe_hash]);
43 for (hash, expected_len) in &recipe.chunks {
44 let object = self.source.read_object(hash)?.ok_or(Error::ObjectNotFound)?;
45 validate_chunk(&object.hash, object.kind, object.bytes.len() as u64, u64::from(*expected_len))?;
46 hashes.insert(*hash);
47 }
48 Ok(hashes)
49 }
50
51 pub fn read_stored_stream(&self, recipe_hash: Hash32) -> Result<Vec<u8>> {
52 let recipe = self.load_recipe(recipe_hash)?;
53 self.read_stored_for_recipe(&recipe)
54 }
55
56 pub fn restore_file(&self, recipe_hash: Hash32, output: &mut dyn Write) -> Result<()> {
57 let recipe = self.load_recipe(recipe_hash)?;
58 let stored = self.read_stored_for_recipe(&recipe)?;
59 decode_recipe(self.decoders, &recipe, stored, output)
60 }
61
62 pub fn read_file(&self, recipe_hash: Hash32) -> Result<Vec<u8>> {
63 let recipe = self.load_recipe(recipe_hash)?;
64 let stored = self.read_stored_for_recipe(&recipe)?;
65 read_decoded_file(self.decoders, &recipe, stored)
66 }
67
68 fn load_recipe(&self, recipe_hash: Hash32) -> Result<RecipeData> {
69 let object = self.source.read_object(&recipe_hash)?.ok_or(Error::ObjectNotFound)?;
70 load_recipe_from_object(object, recipe_hash, self.limits)
71 }
72
73 fn read_stored_for_recipe(&self, recipe: &RecipeData) -> Result<Vec<u8>> {
74 let mut stored = Vec::new();
75 for (hash, expected_len) in &recipe.chunks {
76 let object = self.source.read_object(hash)?.ok_or(Error::ObjectNotFound)?;
77 validate_chunk(&object.hash, object.kind, object.bytes.len() as u64, u64::from(*expected_len))?;
78 append_chunk(&mut stored, &object.bytes, recipe.stored_stream_size)?;
79 }
80 Ok(stored)
81 }
82}
83
84pub struct AsyncFileReader<'a, S: ?Sized> {
85 source: &'a S,
86 decoders: &'a TransformDecoderRegistry,
87 limits: FileReadLimits,
88}
89
90impl<'a, S: AsyncObjectSource + ?Sized> AsyncFileReader<'a, S> {
91 pub fn new(source: &'a S, decoders: &'a TransformDecoderRegistry, limits: FileReadLimits) -> Self {
92 Self { source, decoders, limits }
93 }
94
95 pub async fn resolve_closure(&self, recipe_hash: Hash32) -> Result<BTreeSet<Hash32>> {
96 let recipe = self.load_recipe(recipe_hash).await?;
97 let mut hashes = BTreeSet::from([recipe_hash]);
98 for (hash, expected_len) in &recipe.chunks {
99 let object = self.source.read_object(hash).await?.ok_or(Error::ObjectNotFound)?;
100 validate_chunk(&object.hash, object.kind, object.bytes.len() as u64, u64::from(*expected_len))?;
101 hashes.insert(*hash);
102 }
103 Ok(hashes)
104 }
105
106 pub async fn read_stored_stream(&self, recipe_hash: Hash32) -> Result<Vec<u8>> {
107 let recipe = self.load_recipe(recipe_hash).await?;
108 self.read_stored_for_recipe(&recipe).await
109 }
110
111 pub async fn restore_file(&self, recipe_hash: Hash32, output: &mut dyn Write) -> Result<()> {
112 let recipe = self.load_recipe(recipe_hash).await?;
113 let stored = self.read_stored_for_recipe(&recipe).await?;
114 decode_recipe(self.decoders, &recipe, stored, output)
115 }
116
117 pub async fn read_file(&self, recipe_hash: Hash32) -> Result<Vec<u8>> {
118 let recipe = self.load_recipe(recipe_hash).await?;
119 let stored = self.read_stored_for_recipe(&recipe).await?;
120 read_decoded_file(self.decoders, &recipe, stored)
121 }
122
123 async fn load_recipe(&self, recipe_hash: Hash32) -> Result<RecipeData> {
124 let object = self.source.read_object(&recipe_hash).await?.ok_or(Error::ObjectNotFound)?;
125 load_recipe_from_object(object, recipe_hash, self.limits)
126 }
127
128 async fn read_stored_for_recipe(&self, recipe: &RecipeData) -> Result<Vec<u8>> {
129 let mut stored = Vec::new();
130 for (hash, expected_len) in &recipe.chunks {
131 let object = self.source.read_object(hash).await?.ok_or(Error::ObjectNotFound)?;
132 validate_chunk(&object.hash, object.kind, object.bytes.len() as u64, u64::from(*expected_len))?;
133 append_chunk(&mut stored, &object.bytes, recipe.stored_stream_size)?;
134 }
135 Ok(stored)
136 }
137}
138
139fn load_recipe_from_object(object: crate::VerifiedObject, recipe_hash: Hash32, limits: FileReadLimits) -> Result<RecipeData> {
140 if object.kind != ObjectKind::Recipe {
141 return Err(Error::ObjectKindMismatch {
142 hash: recipe_hash,
143 expected: ObjectKind::Recipe,
144 actual: object.kind,
145 });
146 }
147 let (original_file_size, chunk_count) =
148 preflight_recipe_limits(&object.bytes).map_err(|error| Error::InvalidRecipe(error.to_string()))?;
149 check_limit("chunk count", limits.max_chunk_count, u64::from(chunk_count))?;
150 check_limit("original file bytes", limits.max_original_file_bytes, original_file_size)?;
151 let recipe = parse_recipe(&object.bytes).map_err(|error| Error::InvalidRecipe(error.to_string()))?;
152 check_limit("stored stream bytes", limits.max_stored_stream_bytes, recipe.stored_stream_size)?;
153 Ok(recipe)
154}
155
156fn append_chunk(stored: &mut Vec<u8>, bytes: &[u8], expected_total: u64) -> Result<()> {
157 let requested = stored
158 .len()
159 .checked_add(bytes.len())
160 .and_then(|length| u64::try_from(length).ok())
161 .ok_or(Error::FileReadAllocationFailed {
162 buffer: "stored stream",
163 requested: expected_total,
164 })?;
165 stored.try_reserve(bytes.len()).map_err(|_| Error::FileReadAllocationFailed {
166 buffer: "stored stream",
167 requested,
168 })?;
169 stored.extend_from_slice(bytes);
170 Ok(())
171}
172
173fn validate_chunk(hash: &Hash32, kind: ObjectKind, actual_len: u64, expected_len: u64) -> Result<()> {
174 if kind != ObjectKind::Chunk {
175 return Err(Error::ObjectKindMismatch {
176 hash: *hash,
177 expected: ObjectKind::Chunk,
178 actual: kind,
179 });
180 }
181 if actual_len != expected_len {
182 return Err(Error::RecipeChunkLengthMismatch {
183 hash: *hash,
184 expected: expected_len,
185 actual: actual_len,
186 });
187 }
188 Ok(())
189}
190
191fn check_limit(limit: &'static str, maximum: u64, actual: u64) -> Result<()> {
192 if actual > maximum {
193 return Err(Error::FileReadLimitExceeded { limit, maximum, actual });
194 }
195 Ok(())
196}
197
198fn read_decoded_file(decoders: &TransformDecoderRegistry, recipe: &RecipeData, stored: Vec<u8>) -> Result<Vec<u8>> {
199 let mut output = FallibleVecWriter::default();
200 let result = decode_recipe(decoders, recipe, stored, &mut output);
201 if let Some(requested) = output.allocation_failure {
202 return Err(Error::FileReadAllocationFailed {
203 buffer: "original file",
204 requested,
205 });
206 }
207 result?;
208 Ok(output.bytes)
209}
210
211fn decode_recipe(decoders: &TransformDecoderRegistry, recipe: &RecipeData, stored: Vec<u8>, output: &mut dyn Write) -> Result<()> {
212 let decoder = decoders.get(recipe.transform_id, recipe.transform_version)?;
213 let mut input = BufReader::new(Cursor::new(stored));
214 let mut verified = VerifyingWriter::new(output, recipe.original_file_size);
215 let result = decoder.decode(&mut input, &mut verified);
216 if verified.exceeded {
217 return Err(original_mismatch(recipe, &verified));
218 }
219 result?;
220 let actual_hash = verified.hash();
221 if verified.written != recipe.original_file_size || actual_hash != recipe.original_file_hash {
222 return Err(Error::OriginalFileMismatch {
223 expected_size: recipe.original_file_size,
224 actual_size: verified.written,
225 expected_hash: recipe.original_file_hash,
226 actual_hash,
227 });
228 }
229 Ok(())
230}
231
232fn original_mismatch(recipe: &RecipeData, writer: &VerifyingWriter<'_>) -> Error {
233 Error::OriginalFileMismatch {
234 expected_size: recipe.original_file_size,
235 actual_size: writer.written,
236 expected_hash: recipe.original_file_hash,
237 actual_hash: writer.hash(),
238 }
239}
240
241#[derive(Default)]
242struct FallibleVecWriter {
243 bytes: Vec<u8>,
244 allocation_failure: Option<u64>,
245}
246
247impl Write for FallibleVecWriter {
248 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
249 let requested = self
250 .bytes
251 .len()
252 .checked_add(bytes.len())
253 .and_then(|length| u64::try_from(length).ok())
254 .ok_or_else(|| io::Error::new(io::ErrorKind::OutOfMemory, "original file length exceeds address space"))?;
255 if self.bytes.try_reserve(bytes.len()).is_err() {
256 self.allocation_failure = Some(requested);
257 return Err(io::Error::new(
258 io::ErrorKind::OutOfMemory,
259 "unable to allocate original file buffer",
260 ));
261 }
262 self.bytes.extend_from_slice(bytes);
263 Ok(bytes.len())
264 }
265
266 fn flush(&mut self) -> io::Result<()> {
267 Ok(())
268 }
269}
270
271struct VerifyingWriter<'a> {
272 output: &'a mut dyn Write,
273 hasher: Sha3_256,
274 maximum: u64,
275 written: u64,
276 exceeded: bool,
277}
278
279impl<'a> VerifyingWriter<'a> {
280 fn new(output: &'a mut dyn Write, maximum: u64) -> Self {
281 Self {
282 output,
283 hasher: Sha3_256::new(),
284 maximum,
285 written: 0,
286 exceeded: false,
287 }
288 }
289
290 fn hash(&self) -> Hash32 {
291 Hash32::new(self.hasher.clone().finalize().into())
292 }
293}
294
295impl Write for VerifyingWriter<'_> {
296 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
297 let next = self.written.checked_add(bytes.len() as u64).ok_or_else(|| {
298 self.exceeded = true;
299 io::Error::new(io::ErrorKind::FileTooLarge, "decoded file length overflow")
300 })?;
301 if next > self.maximum {
302 self.exceeded = true;
303 return Err(io::Error::new(io::ErrorKind::FileTooLarge, "decoded file exceeds declared size"));
304 }
305 let written = self.output.write(bytes)?;
306 self.hasher.update(&bytes[..written]);
307 self.written += written as u64;
308 Ok(written)
309 }
310
311 fn flush(&mut self) -> io::Result<()> {
312 self.output.flush()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use std::{
319 collections::BTreeMap,
320 sync::{
321 Arc,
322 atomic::{AtomicUsize, Ordering},
323 },
324 };
325
326 use super::*;
327 use crate::{TRANSFORM_ID_NONE, TRANSFORM_VERSION_NONE, VerifiedObject, build_recipe};
328
329 #[derive(Default)]
330 struct MemorySource(BTreeMap<Hash32, VerifiedObject>);
331
332 impl AsyncObjectSource for MemorySource {
333 async fn read_object(&self, hash: &Hash32) -> Result<Option<VerifiedObject>> {
334 Ok(self.0.get(hash).cloned())
335 }
336 }
337
338 fn source_for(
339 chunks: &[&[u8]],
340 order: &[usize],
341 original_size: u64,
342 original_hash: Hash32,
343 transform: (u16, u16),
344 ) -> (MemorySource, Hash32) {
345 let mut source = MemorySource::default();
346 let hashes = chunks
347 .iter()
348 .map(|bytes| {
349 let hash = Hash32::sha3_256(bytes);
350 source.0.insert(
351 hash,
352 VerifiedObject {
353 hash,
354 kind: ObjectKind::Chunk,
355 bytes: bytes.to_vec(),
356 },
357 );
358 hash
359 })
360 .collect::<Vec<_>>();
361 let recipe_chunks = order
362 .iter()
363 .map(|index| (hashes[*index], chunks[*index].len() as u32))
364 .collect::<Vec<_>>();
365 let recipe = build_recipe(original_size, &recipe_chunks, original_hash, transform.0, transform.1);
366 let recipe_hash = Hash32::sha3_256(&recipe);
367 source.0.insert(
368 recipe_hash,
369 VerifiedObject {
370 hash: recipe_hash,
371 kind: ObjectKind::Recipe,
372 bytes: recipe,
373 },
374 );
375 (source, recipe_hash)
376 }
377
378 #[tokio::test]
379 async fn none_reader_handles_empty_single_multi_and_duplicate_chunks() {
380 for (chunks, order) in [
381 (vec![], vec![]),
382 (vec![b"alpha".as_slice()], vec![0]),
383 (vec![b"alpha".as_slice(), b"beta".as_slice()], vec![0, 1]),
384 (vec![b"alpha".as_slice(), b"beta".as_slice()], vec![0, 1, 0]),
385 ] {
386 let expected = order.iter().flat_map(|index| chunks[*index]).copied().collect::<Vec<_>>();
387 let (source, recipe_hash) = source_for(
388 &chunks,
389 &order,
390 expected.len() as u64,
391 Hash32::sha3_256(&expected),
392 (TRANSFORM_ID_NONE, TRANSFORM_VERSION_NONE),
393 );
394 let registry = TransformDecoderRegistry::default();
395 let reader = AsyncFileReader::new(&source, ®istry, FileReadLimits::default());
396 assert_eq!(reader.read_stored_stream(recipe_hash).await.unwrap(), expected);
397 assert_eq!(reader.read_file(recipe_hash).await.unwrap(), expected);
398 let mut restored = Vec::new();
399 reader.restore_file(recipe_hash, &mut restored).await.unwrap();
400 assert_eq!(restored, expected);
401 assert_eq!(reader.resolve_closure(recipe_hash).await.unwrap().len(), chunks.len() + 1);
402 }
403 }
404
405 #[tokio::test]
406 async fn errors_are_classified_by_file_reader_boundary() {
407 #[derive(Clone, Copy)]
408 enum ExpectedError {
409 NotFound,
410 KindMismatch,
411 InvalidRecipe,
412 ChunkLengthMismatch,
413 }
414
415 let expected = b"alpha";
416 type ErrorCase = (&'static str, fn(&mut MemorySource, Hash32, Hash32), ExpectedError);
417 let cases: [ErrorCase; 6] = [
418 (
419 "missing recipe",
420 |source, recipe_hash, _| {
421 source.0.remove(&recipe_hash);
422 },
423 ExpectedError::NotFound,
424 ),
425 (
426 "missing chunk",
427 |source, _, chunk_hash| {
428 source.0.remove(&chunk_hash);
429 },
430 ExpectedError::NotFound,
431 ),
432 (
433 "recipe kind is chunk",
434 |source, recipe_hash, _| {
435 source.0.get_mut(&recipe_hash).unwrap().kind = ObjectKind::Chunk;
436 },
437 ExpectedError::KindMismatch,
438 ),
439 (
440 "recipe bytes are invalid",
441 |source, recipe_hash, _| {
442 source.0.get_mut(&recipe_hash).unwrap().bytes = vec![0xff];
443 },
444 ExpectedError::InvalidRecipe,
445 ),
446 (
447 "chunk kind is recipe",
448 |source, _, chunk_hash| {
449 source.0.get_mut(&chunk_hash).unwrap().kind = ObjectKind::Recipe;
450 },
451 ExpectedError::KindMismatch,
452 ),
453 (
454 "chunk length mismatch",
455 |source, _, chunk_hash| {
456 source.0.get_mut(&chunk_hash).unwrap().bytes.push(0);
457 },
458 ExpectedError::ChunkLengthMismatch,
459 ),
460 ];
461 let registry = TransformDecoderRegistry::default();
462 for (label, mutate, expected_error) in cases {
463 let (mut source, recipe_hash) = source_for(
464 &[expected.as_slice()],
465 &[0],
466 expected.len() as u64,
467 Hash32::sha3_256(expected),
468 (TRANSFORM_ID_NONE, TRANSFORM_VERSION_NONE),
469 );
470 let chunk_hash = parse_recipe(&source.0[&recipe_hash].bytes).unwrap().chunks[0].0;
471 mutate(&mut source, recipe_hash, chunk_hash);
472 let reader = AsyncFileReader::new(&source, ®istry, FileReadLimits::default());
473 for result in [
474 reader.read_file(recipe_hash).await.map(|_| ()),
475 reader.resolve_closure(recipe_hash).await.map(|_| ()),
476 ] {
477 let error = result.unwrap_err();
478 let matched = matches!(
479 (&error, expected_error),
480 (Error::ObjectNotFound, ExpectedError::NotFound)
481 | (Error::ObjectKindMismatch { .. }, ExpectedError::KindMismatch)
482 | (Error::InvalidRecipe(_), ExpectedError::InvalidRecipe)
483 | (Error::RecipeChunkLengthMismatch { .. }, ExpectedError::ChunkLengthMismatch)
484 );
485 assert!(matched, "{label} returned unexpected error: {error}");
486 }
487 }
488 }
489
490 #[tokio::test]
491 async fn limits_and_transform_or_original_mismatch_are_structured() {
492 struct CountingDecoder(Arc<AtomicUsize>);
493 impl crate::TransformDecoder for CountingDecoder {
494 fn id(&self) -> u16 {
495 88
496 }
497 fn version(&self) -> u16 {
498 1
499 }
500 fn decode(&self, input: &mut dyn io::BufRead, output: &mut dyn Write) -> Result<()> {
501 self.0.fetch_add(1, Ordering::Relaxed);
502 io::copy(input, output)?;
503 Ok(())
504 }
505 }
506
507 let bytes = b"alpha";
508 let original_hash = Hash32::sha3_256(bytes);
509 let calls = Arc::new(AtomicUsize::new(0));
510 let registry = TransformDecoderRegistry::new([Arc::new(CountingDecoder(calls.clone())) as Arc<dyn crate::TransformDecoder>]).unwrap();
511 let exact_limits = FileReadLimits {
512 max_chunk_count: 1,
513 max_stored_stream_bytes: bytes.len() as u64,
514 max_original_file_bytes: bytes.len() as u64,
515 };
516 let (source, recipe_hash) = source_for(&[bytes], &[0], bytes.len() as u64, original_hash, (88, 1));
517 let reader = AsyncFileReader::new(&source, ®istry, exact_limits);
518 assert_eq!(reader.read_file(recipe_hash).await.unwrap(), bytes);
519 assert_eq!(calls.load(Ordering::Relaxed), 1);
520
521 let declared_size = 8 * 1024 * 1024 * 1024;
522 let (source, recipe_hash) = source_for(&[], &[], declared_size, Hash32::sha3_256([]), (0, 0));
523 let reader = AsyncFileReader::new(&source, ®istry, FileReadLimits::default());
524 assert!(matches!(
525 reader.read_file(recipe_hash).await,
526 Err(Error::FileReadLimitExceeded {
527 limit: "original file bytes",
528 actual,
529 ..
530 }) if actual == declared_size
531 ));
532 let reader = AsyncFileReader::new(
533 &source,
534 ®istry,
535 FileReadLimits {
536 max_original_file_bytes: declared_size,
537 ..FileReadLimits::default()
538 },
539 );
540 assert!(matches!(
541 reader.read_file(recipe_hash).await,
542 Err(Error::OriginalFileMismatch {
543 expected_size,
544 actual_size: 0,
545 ..
546 }) if expected_size == declared_size
547 ));
548
549 let cases = [
550 (
551 FileReadLimits {
552 max_chunk_count: 0,
553 ..FileReadLimits::default()
554 },
555 "chunk count",
556 ),
557 (
558 FileReadLimits {
559 max_stored_stream_bytes: 4,
560 ..FileReadLimits::default()
561 },
562 "stored stream bytes",
563 ),
564 (
565 FileReadLimits {
566 max_original_file_bytes: 4,
567 ..FileReadLimits::default()
568 },
569 "original file bytes",
570 ),
571 ];
572 for (limits, expected_limit) in cases {
573 let before = calls.load(Ordering::Relaxed);
574 let (source, recipe_hash) = source_for(&[bytes], &[0], bytes.len() as u64, original_hash, (88, 1));
575 let reader = AsyncFileReader::new(&source, ®istry, limits);
576 assert!(matches!(
577 reader.read_file(recipe_hash).await,
578 Err(Error::FileReadLimitExceeded { limit, .. }) if limit == expected_limit
579 ));
580 assert_eq!(calls.load(Ordering::Relaxed), before);
581 }
582
583 let (mut source, recipe_hash) = source_for(&[bytes], &[0], bytes.len() as u64, original_hash, (0, 0));
584 source.0.get_mut(&recipe_hash).unwrap().bytes[45..49].copy_from_slice(&u32::MAX.to_le_bytes());
585 let reader = AsyncFileReader::new(
586 &source,
587 ®istry,
588 FileReadLimits {
589 max_chunk_count: 0,
590 ..FileReadLimits::default()
591 },
592 );
593 assert!(matches!(
594 reader.read_file(recipe_hash).await,
595 Err(Error::FileReadLimitExceeded { limit: "chunk count", actual, .. }) if actual == u64::from(u32::MAX)
596 ));
597 assert_eq!(calls.load(Ordering::Relaxed), 1);
598
599 let (source, recipe_hash) = source_for(&[bytes], &[0], bytes.len() as u64, original_hash, (TRANSFORM_ID_NONE, 4));
600 let reader = AsyncFileReader::new(&source, ®istry, FileReadLimits::default());
601 assert!(matches!(
602 reader.read_file(recipe_hash).await,
603 Err(Error::UnsupportedTransform {
604 id: TRANSFORM_ID_NONE,
605 version: 4
606 })
607 ));
608
609 let (source, recipe_hash) = source_for(&[bytes], &[0], bytes.len() as u64, Hash32::sha3_256(b"other"), (0, 0));
610 let none_registry = TransformDecoderRegistry::default();
611 let reader = AsyncFileReader::new(&source, &none_registry, FileReadLimits::default());
612 assert_eq!(reader.read_stored_stream(recipe_hash).await.unwrap(), bytes);
613 assert!(matches!(
614 reader.read_file(recipe_hash).await,
615 Err(Error::OriginalFileMismatch { .. })
616 ));
617 let mut restored = Vec::new();
618 assert!(matches!(
619 reader.restore_file(recipe_hash, &mut restored).await,
620 Err(Error::OriginalFileMismatch { .. })
621 ));
622
623 let (source, recipe_hash) = source_for(&[bytes], &[0], bytes.len() as u64 + 1, original_hash, (0, 0));
624 let reader = AsyncFileReader::new(&source, &none_registry, FileReadLimits::default());
625 assert!(matches!(
626 reader.read_file(recipe_hash).await,
627 Err(Error::OriginalFileMismatch { expected_size, actual_size, .. }) if expected_size == bytes.len() as u64 + 1 && actual_size == bytes.len() as u64
628 ));
629 }
630
631 #[test]
632 fn registry_accepts_decoder_trait_objects_without_writer_features() {
633 struct Decoder;
634 impl crate::TransformDecoder for Decoder {
635 fn id(&self) -> u16 {
636 91
637 }
638 fn version(&self) -> u16 {
639 2
640 }
641 fn decode(&self, input: &mut dyn io::BufRead, output: &mut dyn Write) -> Result<()> {
642 io::copy(input, output)?;
643 Ok(())
644 }
645 }
646 let registry = TransformDecoderRegistry::new([Arc::new(Decoder) as Arc<dyn crate::TransformDecoder>]).unwrap();
647 assert_eq!(registry.get(91, 2).unwrap().version(), 2);
648 }
649}