1use alloc::{string::ToString, vec::Vec};
2#[cfg(feature = "hash")]
3use core::hash::Hasher;
4use core::{convert::TryFrom, fmt};
5use std::io::{self, Read, Write};
6
7use super::{levels::c_port, CompressionLevel, CompressionTuning, ContentSizePolicy};
8use crate::{
9 blocks::block::BlockType,
10 common::MAX_BLOCK_SIZE,
11 encoding::{block_header::BlockHeader, frame_header::FrameHeader},
12};
13
14pub const DEFAULT_MEMORY_LIMIT: usize = 96 * 1024 * 1024;
16pub const DEFAULT_FRAME_CHUNK_SIZE: usize = 8 * 1024 * 1024;
18
19#[derive(Clone)]
21pub struct EncoderOptions {
22 level: CompressionLevel,
23 memory_limit: usize,
24 frame_chunk_size: usize,
25 dictionary: Option<EncoderDictionary>,
26 checksum: bool,
27 tuning: CompressionTuning,
28 pledged_source_size: Option<u64>,
29 content_size_policy: ContentSizePolicy,
30}
31
32impl EncoderOptions {
33 pub const fn new(level: CompressionLevel) -> Self {
35 Self {
36 level,
37 memory_limit: DEFAULT_MEMORY_LIMIT,
38 frame_chunk_size: DEFAULT_FRAME_CHUNK_SIZE,
39 dictionary: None,
40 checksum: false,
41 tuning: CompressionTuning::new(),
42 pledged_source_size: None,
43 content_size_policy: ContentSizePolicy::Include,
44 }
45 }
46
47 pub const fn level(&self) -> CompressionLevel {
48 self.level
49 }
50
51 pub const fn memory_limit(&self) -> usize {
52 self.memory_limit
53 }
54
55 pub const fn frame_chunk_size(&self) -> usize {
56 self.frame_chunk_size
57 }
58
59 pub fn dictionary(&self) -> Option<&EncoderDictionary> {
60 self.dictionary.as_ref()
61 }
62
63 pub const fn checksum(&self) -> bool {
64 self.checksum
65 }
66
67 pub const fn tuning(&self) -> CompressionTuning {
68 self.tuning
69 }
70
71 pub const fn pledged_source_size(&self) -> Option<u64> {
72 self.pledged_source_size
73 }
74
75 pub const fn content_size_policy(&self) -> ContentSizePolicy {
76 self.content_size_policy
77 }
78
79 pub fn estimated_memory_usage(&self) -> usize {
81 let required = c_port::estimated_frame_memory(self.level.c_level(), self.frame_chunk_size);
82 self.dictionary.as_ref().map_or(required, |dictionary| {
83 let retained_copies = if cfg!(feature = "multithreading") {
84 3
85 } else {
86 2
87 };
88 required.saturating_add(dictionary.raw_size().saturating_mul(retained_copies))
89 })
90 }
91
92 pub const fn with_memory_limit(mut self, bytes: usize) -> Self {
94 self.memory_limit = bytes;
95 self
96 }
97
98 pub const fn with_frame_chunk_size(mut self, bytes: usize) -> Self {
103 self.frame_chunk_size = bytes;
104 self
105 }
106
107 pub fn with_dictionary(mut self, dictionary: EncoderDictionary) -> Self {
109 self.dictionary = Some(dictionary);
110 self
111 }
112
113 pub const fn with_checksum(mut self, enabled: bool) -> Self {
115 self.checksum = enabled;
116 self
117 }
118
119 pub const fn with_tuning(mut self, tuning: CompressionTuning) -> Self {
121 self.tuning = tuning;
122 self
123 }
124
125 pub const fn with_pledged_source_size(mut self, bytes: u64) -> Self {
127 self.pledged_source_size = Some(bytes);
128 self
129 }
130
131 pub const fn with_content_size_policy(mut self, policy: ContentSizePolicy) -> Self {
133 self.content_size_policy = policy;
134 self
135 }
136
137 pub(super) fn validate(&self) -> Result<(), EncodeError> {
138 if self.frame_chunk_size == 0 {
139 return Err(EncodeError::InvalidOptions(
140 "frame chunk size must be greater than zero",
141 ));
142 }
143 if self.level.is_uncompressed() && self.dictionary.is_some() {
144 return Err(EncodeError::InvalidOptions(
145 "an uncompressed encoder cannot use a dictionary",
146 ));
147 }
148 if self.level.is_uncompressed() && self.tuning != CompressionTuning::new() {
149 return Err(EncodeError::InvalidOptions(
150 "an uncompressed encoder cannot use compression tuning",
151 ));
152 }
153 self.tuning
154 .validate()
155 .map_err(EncodeError::InvalidOptions)?;
156 if self.checksum && !cfg!(feature = "hash") {
157 return Err(EncodeError::InvalidOptions(
158 "checksums require the zstd-complete `hash` feature",
159 ));
160 }
161 let required = self.estimated_memory_usage();
162 if required > self.memory_limit {
163 return Err(EncodeError::MemoryLimitExceeded {
164 limit: self.memory_limit,
165 required,
166 });
167 }
168 Ok(())
169 }
170}
171
172impl fmt::Debug for EncoderOptions {
173 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174 formatter
175 .debug_struct("EncoderOptions")
176 .field("level", &self.level)
177 .field("memory_limit", &self.memory_limit)
178 .field("frame_chunk_size", &self.frame_chunk_size)
179 .field("dictionary", &self.dictionary)
180 .field("checksum", &self.checksum)
181 .field("tuning", &self.tuning)
182 .field("pledged_source_size", &self.pledged_source_size)
183 .field("content_size_policy", &self.content_size_policy)
184 .finish()
185 }
186}
187
188#[derive(Clone)]
190pub struct EncoderDictionary {
191 prepared: c_port::PreparedDictionary,
192 raw_size: usize,
193 #[cfg(feature = "multithreading")]
194 raw: Vec<u8>,
195}
196
197impl EncoderDictionary {
198 pub fn copy(dictionary: &[u8]) -> Result<Self, DictionaryError> {
200 let prepared = c_port::PreparedDictionary::from_bytes(dictionary)
201 .map_err(|error| match error {
202 c_port::DictionaryParseError::WrongDictionary => DictionaryError::WrongDictionary,
203 c_port::DictionaryParseError::CorruptedDictionary => {
204 DictionaryError::CorruptedDictionary
205 }
206 })?
207 .ok_or(DictionaryError::WrongDictionary)?;
208 Ok(Self {
209 prepared,
210 raw_size: dictionary.len(),
211 #[cfg(feature = "multithreading")]
212 raw: dictionary.to_vec(),
213 })
214 }
215
216 pub const fn raw_size(&self) -> usize {
217 self.raw_size
218 }
219
220 #[cfg(feature = "multithreading")]
221 pub(super) fn raw(&self) -> &[u8] {
222 &self.raw
223 }
224}
225
226impl fmt::Debug for EncoderDictionary {
227 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228 formatter
229 .debug_struct("EncoderDictionary")
230 .field("raw_size", &self.raw_size)
231 .finish_non_exhaustive()
232 }
233}
234
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub enum DictionaryError {
238 WrongDictionary,
239 CorruptedDictionary,
240}
241
242impl fmt::Display for DictionaryError {
243 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244 formatter.write_str(match self {
245 Self::WrongDictionary => "input is not a usable Zstandard dictionary",
246 Self::CorruptedDictionary => "Zstandard dictionary metadata is corrupted",
247 })
248 }
249}
250
251impl std::error::Error for DictionaryError {}
252
253impl Default for EncoderOptions {
254 fn default() -> Self {
255 Self::new(CompressionLevel::DEFAULT)
256 }
257}
258
259#[derive(Debug)]
261pub enum EncodeError {
262 Io(io::Error),
263 InvalidOptions(&'static str),
264 MemoryLimitExceeded {
265 limit: usize,
266 required: usize,
267 },
268 PledgedSourceSizeMismatch {
269 pledged: u64,
270 actual: u64,
271 },
272 #[cfg(feature = "multithreading")]
273 WorkerFailed,
274}
275
276impl fmt::Display for EncodeError {
277 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
278 match self {
279 Self::Io(error) => error.fmt(formatter),
280 Self::InvalidOptions(message) => formatter.write_str(message),
281 Self::MemoryLimitExceeded { limit, required } => write!(
282 formatter,
283 "encoder needs an estimated {required} bytes, exceeding the {limit}-byte memory limit"
284 ),
285 Self::PledgedSourceSizeMismatch { pledged, actual } => write!(
286 formatter,
287 "encoder received {actual} bytes but was pledged exactly {pledged} bytes"
288 ),
289 #[cfg(feature = "multithreading")]
290 Self::WorkerFailed => formatter.write_str("a compression worker failed"),
291 }
292 }
293}
294
295impl std::error::Error for EncodeError {
296 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
297 match self {
298 Self::Io(error) => Some(error),
299 _ => None,
300 }
301 }
302}
303
304impl From<io::Error> for EncodeError {
305 fn from(error: io::Error) -> Self {
306 Self::Io(error)
307 }
308}
309
310pub struct Encoder<W: Write> {
315 inner: W,
316 options: EncoderOptions,
317 input: Vec<u8>,
318 emitted_frame: bool,
319 accepted_input: u64,
320}
321
322impl<W: Write> Encoder<W> {
323 pub fn new(inner: W, options: EncoderOptions) -> Result<Self, EncodeError> {
324 options.validate()?;
325 let input = Vec::with_capacity(options.frame_chunk_size);
326 Ok(Self {
327 inner,
328 options,
329 input,
330 emitted_frame: false,
331 accepted_input: 0,
332 })
333 }
334
335 pub fn get_ref(&self) -> &W {
336 &self.inner
337 }
338
339 pub fn get_mut(&mut self) -> &mut W {
340 &mut self.inner
341 }
342
343 pub fn finish(mut self) -> Result<W, EncodeError> {
345 self.verify_pledged_size()?;
346 if !self.input.is_empty() || !self.emitted_frame {
347 self.emit_frame()?;
348 }
349 self.inner.flush()?;
350 Ok(self.inner)
351 }
352
353 fn emit_frame(&mut self) -> io::Result<()> {
354 let mut compressed = encode_frame_payload(&self.input, &self.options);
355 apply_content_size_policy(&mut compressed, self.options.content_size_policy)?;
356 if self.options.checksum {
357 append_checksum(&mut compressed, &self.input);
358 }
359 self.inner.write_all(&compressed)?;
360 self.input.clear();
361 self.emitted_frame = true;
362 Ok(())
363 }
364
365 fn verify_pledged_size(&self) -> Result<(), EncodeError> {
366 if let Some(pledged) = self.options.pledged_source_size {
367 if pledged != self.accepted_input {
368 return Err(EncodeError::PledgedSourceSizeMismatch {
369 pledged,
370 actual: self.accepted_input,
371 });
372 }
373 }
374 Ok(())
375 }
376}
377
378fn encode_frame_payload(source: &[u8], options: &EncoderOptions) -> Vec<u8> {
379 if options.level.is_uncompressed() {
380 return encode_uncompressed_frame(source);
381 }
382 let has_tuning = options.tuning != CompressionTuning::new();
383 match (options.dictionary.as_ref(), has_tuning) {
384 (Some(dictionary), false) => c_port::encode_frame_with_prepared_dictionary(
385 source,
386 options.level.c_level(),
387 &dictionary.prepared,
388 ),
389 (Some(dictionary), true) => c_port::encode_frame_with_prepared_dictionary_and_tuning(
390 source,
391 options.level.c_level(),
392 &dictionary.prepared,
393 options.tuning,
394 ),
395 (None, false) => c_port::encode_frame_no_dict(source, options.level.c_level()),
396 (None, true) => c_port::encode_frame_no_dict_with_tuning(
397 source,
398 options.level.c_level(),
399 options.tuning,
400 ),
401 }
402}
403
404fn apply_content_size_policy(frame: &mut Vec<u8>, policy: ContentSizePolicy) -> io::Result<()> {
405 if policy == ContentSizePolicy::Include {
406 return Ok(());
407 }
408 let (parsed, old_header_size) = crate::decoding::frame::read_frame_header(frame.as_slice())
409 .map_err(|error| io::Error::other(error.to_string()))?;
410 let window_size = parsed.window_size().unwrap_or(1024).max(1024);
411 let mut header = Vec::with_capacity(18);
412 FrameHeader {
413 frame_content_size: None,
414 single_segment: false,
415 content_checksum: false,
416 dictionary_id: parsed.dictionary_id().map(u64::from),
417 window_size: Some(window_size),
418 }
419 .serialize(&mut header).expect("owned legacy codec allocation failed");
420 frame.splice(..usize::from(old_header_size), header);
421 Ok(())
422}
423
424fn append_checksum(frame: &mut Vec<u8>, source: &[u8]) {
425 #[cfg(feature = "hash")]
426 {
427 debug_assert!(frame.len() >= 5);
428 frame[4] |= 1 << 2;
429 let mut hasher = twox_hash::XxHash64::with_seed(0);
430 hasher.write(source);
431 frame.extend_from_slice(&(hasher.finish() as u32).to_le_bytes());
432 }
433 #[cfg(not(feature = "hash"))]
434 {
435 let _ = (frame, source);
436 unreachable!("checksum options are rejected without the hash feature");
437 }
438}
439
440#[cfg(feature = "multithreading")]
441pub(super) fn encode_frame_for_options(source: &[u8], options: &EncoderOptions) -> Vec<u8> {
442 let mut compressed = encode_frame_payload(source, options);
443 apply_content_size_policy(&mut compressed, options.content_size_policy)
444 .expect("validated frame header can be rewritten");
445 if options.checksum {
446 append_checksum(&mut compressed, source);
447 }
448 compressed
449}
450
451fn encode_uncompressed_frame(source: &[u8]) -> Vec<u8> {
452 let mut output = Vec::with_capacity(source.len().saturating_add(16));
453 FrameHeader {
454 frame_content_size: Some(source.len() as u64),
455 single_segment: true,
456 content_checksum: false,
457 dictionary_id: None,
458 window_size: None,
459 }
460 .serialize(&mut output).expect("owned legacy codec allocation failed");
461
462 if source.is_empty() {
463 BlockHeader {
464 last_block: true,
465 block_type: BlockType::Raw,
466 block_size: 0,
467 }
468 .serialize(&mut output).expect("owned legacy codec allocation failed");
469 return output;
470 }
471
472 for (index, block) in source.chunks(MAX_BLOCK_SIZE as usize).enumerate() {
473 BlockHeader {
474 last_block: (index + 1) * MAX_BLOCK_SIZE as usize >= source.len(),
475 block_type: BlockType::Raw,
476 block_size: block.len() as u32,
477 }
478 .serialize(&mut output).expect("owned legacy codec allocation failed");
479 output.extend_from_slice(block);
480 }
481 output
482}
483
484impl<W: Write> Write for Encoder<W> {
485 fn write(&mut self, source: &[u8]) -> io::Result<usize> {
486 if source.is_empty() {
487 return Ok(0);
488 }
489 if self.input.len() == self.options.frame_chunk_size {
490 self.emit_frame()?;
491 }
492 let available = self.options.frame_chunk_size - self.input.len();
493 let consumed = available.min(source.len());
494 if let Some(pledged) = self.options.pledged_source_size {
495 let remaining = pledged.saturating_sub(self.accepted_input);
496 if remaining == 0 {
497 return Err(io::Error::other(EncodeError::PledgedSourceSizeMismatch {
498 pledged,
499 actual: self.accepted_input.saturating_add(source.len() as u64),
500 }));
501 }
502 let consumed = consumed.min(usize::try_from(remaining).unwrap_or(usize::MAX));
503 self.input.extend_from_slice(&source[..consumed]);
504 self.accepted_input += consumed as u64;
505 return Ok(consumed);
506 }
507 self.input.extend_from_slice(&source[..consumed]);
508 self.accepted_input += consumed as u64;
509 Ok(consumed)
510 }
511
512 fn flush(&mut self) -> io::Result<()> {
514 if !self.input.is_empty() {
515 self.emit_frame()?;
516 }
517 self.inner.flush()
518 }
519}
520
521pub fn encode<R: Read, W: Write>(
523 mut source: R,
524 target: W,
525 options: EncoderOptions,
526) -> Result<(), EncodeError> {
527 let mut encoder = Encoder::new(target, options)?;
528 io::copy(&mut source, &mut encoder)?;
529 encoder.finish()?;
530 Ok(())
531}
532
533pub fn encode_all<R: Read>(source: R, options: EncoderOptions) -> Result<Vec<u8>, EncodeError> {
535 let mut output = Vec::new();
536 encode(source, &mut output, options)?;
537 Ok(output)
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::decoding::FrameDecoder;
544 use alloc::vec;
545
546 #[test]
547 fn multi_frame_stream_roundtrips() {
548 let input = b"abcdefgh".repeat(4096);
549 let options = EncoderOptions::new(CompressionLevel::FASTEST)
550 .with_frame_chunk_size(1024)
551 .with_memory_limit(16 * 1024 * 1024);
552 let compressed = encode_all(input.as_slice(), options).unwrap();
553 let mut decoded = Vec::with_capacity(input.len());
554 FrameDecoder::new()
555 .decode_all_to_vec(&compressed, &mut decoded)
556 .unwrap();
557 assert_eq!(decoded, input);
558 }
559
560 #[test]
561 fn empty_input_is_a_valid_frame() {
562 let compressed = encode_all(&[][..], EncoderOptions::default()).unwrap();
563 let mut decoded = Vec::with_capacity(1);
564 FrameDecoder::new()
565 .decode_all_to_vec(&compressed, &mut decoded)
566 .unwrap();
567 assert!(decoded.is_empty());
568 }
569
570 #[test]
571 fn uncompressed_stream_roundtrips() {
572 let input = vec![42; 300_000];
573 let options =
574 EncoderOptions::new(CompressionLevel::UNCOMPRESSED).with_frame_chunk_size(150_000);
575 let compressed = encode_all(input.as_slice(), options).unwrap();
576 assert_eq!(zstd::decode_all(compressed.as_slice()).unwrap(), input);
577 }
578
579 #[test]
580 fn archive_is_interoperable_with_the_c_decoder() {
581 let input = b"interoperable streaming input".repeat(10_000);
582 let options = EncoderOptions::new(CompressionLevel::DEFAULT)
583 .with_frame_chunk_size(32 * 1024)
584 .with_memory_limit(32 * 1024 * 1024);
585 let compressed = encode_all(input.as_slice(), options).unwrap();
586 assert_eq!(zstd::decode_all(compressed.as_slice()).unwrap(), input);
587 }
588
589 #[test]
590 fn default_options_keep_the_existing_specialized_frame_path() {
591 let input = b"unchanged default single-thread frame path".repeat(10_000);
592 let expected = c_port::encode_frame_no_dict(&input, CompressionLevel::DEFAULT.c_level());
593 let actual = encode_all(input.as_slice(), EncoderOptions::default()).unwrap();
594 assert_eq!(actual, expected);
595 }
596
597 #[cfg(feature = "hash")]
598 #[test]
599 fn checksummed_frames_roundtrip() {
600 let input = b"checksummed content".repeat(10_000);
601 let options = EncoderOptions::new(CompressionLevel::DEFAULT)
602 .with_frame_chunk_size(32 * 1024)
603 .with_memory_limit(32 * 1024 * 1024)
604 .with_checksum(true);
605 let compressed = encode_all(input.as_slice(), options).unwrap();
606 assert_eq!(zstd::decode_all(compressed.as_slice()).unwrap(), input);
607 }
608
609 #[test]
610 fn prepared_dictionary_stream_roundtrips() {
611 let dictionary_bytes = b"dictionary words and repeated prefixes for the test".repeat(8);
612 let input = dictionary_bytes.repeat(200);
613 let dictionary = EncoderDictionary::copy(&dictionary_bytes).unwrap();
614 let options = EncoderOptions::new(CompressionLevel::DEFAULT)
615 .with_frame_chunk_size(16 * 1024)
616 .with_memory_limit(32 * 1024 * 1024)
617 .with_dictionary(dictionary);
618 let compressed = encode_all(input.as_slice(), options).unwrap();
619
620 let mut decoded = Vec::with_capacity(input.len());
621 let mut decoder =
622 zstd::stream::read::Decoder::with_dictionary(compressed.as_slice(), &dictionary_bytes)
623 .unwrap();
624 decoder.read_to_end(&mut decoded).unwrap();
625 assert_eq!(decoded, input);
626 }
627
628 #[test]
629 fn too_short_dictionary_is_rejected_instead_of_silently_ignored() {
630 assert_eq!(
631 EncoderDictionary::copy(b"short").unwrap_err(),
632 DictionaryError::WrongDictionary
633 );
634 }
635
636 #[test]
637 fn invalid_budget_is_reported_before_writing() {
638 let options = EncoderOptions::new(CompressionLevel::MAXIMUM);
639 assert!(matches!(
640 Encoder::new(io::sink(), options),
641 Err(EncodeError::MemoryLimitExceeded { .. })
642 ));
643 }
644
645 #[test]
646 fn target_io_errors_are_preserved() {
647 struct FailingWriter;
648
649 impl Write for FailingWriter {
650 fn write(&mut self, _source: &[u8]) -> io::Result<usize> {
651 Err(io::Error::new(io::ErrorKind::BrokenPipe, "test failure"))
652 }
653
654 fn flush(&mut self) -> io::Result<()> {
655 Ok(())
656 }
657 }
658
659 let error = encode(
660 b"accepted input".as_slice(),
661 FailingWriter,
662 EncoderOptions::default(),
663 )
664 .unwrap_err();
665 assert!(matches!(
666 error,
667 EncodeError::Io(ref error) if error.kind() == io::ErrorKind::BrokenPipe
668 ));
669 }
670
671 #[test]
672 fn input_buffer_never_exceeds_the_configured_chunk() {
673 let options = EncoderOptions::new(CompressionLevel::FASTEST)
674 .with_frame_chunk_size(1024)
675 .with_memory_limit(16 * 1024 * 1024);
676 let mut encoder = Encoder::new(io::sink(), options).unwrap();
677 for _ in 0..1024 {
678 encoder.write_all(&[7; 4096]).unwrap();
679 assert!(encoder.input.len() <= 1024);
680 assert_eq!(encoder.input.capacity(), 1024);
681 }
682 encoder.finish().unwrap();
683 }
684
685 #[test]
686 fn large_logical_source_does_not_require_a_large_input_allocation() {
687 struct RepeatingReader {
688 remaining: usize,
689 }
690
691 impl Read for RepeatingReader {
692 fn read(&mut self, target: &mut [u8]) -> io::Result<usize> {
693 let count = self.remaining.min(target.len());
694 target[..count].fill(19);
695 self.remaining -= count;
696 Ok(count)
697 }
698 }
699
700 let options = EncoderOptions::new(CompressionLevel::UNCOMPRESSED)
701 .with_frame_chunk_size(1024 * 1024)
702 .with_memory_limit(4 * 1024 * 1024);
703 encode(
704 RepeatingReader {
705 remaining: 64 * 1024 * 1024,
706 },
707 io::sink(),
708 options,
709 )
710 .unwrap();
711 }
712
713 #[test]
714 fn negative_fast_levels_interoperate() {
715 let input = b"fast acceleration content".repeat(20_000);
716 let level = CompressionLevel::fast(8).unwrap();
717 let compressed = encode_all(input.as_slice(), EncoderOptions::new(level)).unwrap();
718 assert_eq!(zstd::decode_all(compressed.as_slice()).unwrap(), input);
719 }
720
721 #[test]
722 fn typed_tuning_changes_real_compressor_parameters() {
723 let input = b"typed compressor tuning".repeat(20_000);
724 let tuning = CompressionTuning::new()
725 .with_strategy(super::super::CompressionStrategy::Greedy)
726 .with_target_compressed_block_size(16 * 1024);
727 let options = EncoderOptions::default().with_tuning(tuning);
728 let compressed = encode_all(input.as_slice(), options).unwrap();
729 assert_eq!(zstd::decode_all(compressed.as_slice()).unwrap(), input);
730 }
731
732 #[test]
733 fn omitted_content_size_interoperates() {
734 let input = b"content size omitted".repeat(10_000);
735 let options = EncoderOptions::default().with_content_size_policy(ContentSizePolicy::Omit);
736 let compressed = encode_all(input.as_slice(), options).unwrap();
737 let (header, _) = crate::decoding::frame::read_frame_header(compressed.as_slice()).unwrap();
738 assert!(!header.descriptor.single_segment_flag());
739 assert_eq!(header.frame_content_size(), 0);
740 assert_eq!(zstd::decode_all(compressed.as_slice()).unwrap(), input);
741 }
742
743 #[test]
744 fn pledged_size_must_match_exactly() {
745 let options = EncoderOptions::default().with_pledged_source_size(4);
746 let error = encode_all(b"abc".as_slice(), options).unwrap_err();
747 assert!(matches!(
748 error,
749 EncodeError::PledgedSourceSizeMismatch {
750 pledged: 4,
751 actual: 3
752 }
753 ));
754 }
755}