zesven 3.2.0

A pure Rust implementation of the 7z archive format
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
//! A large entry has nothing to be compressed alongside, so the codec splits
//! it into blocks and compresses those across cores instead. Three things have
//! to hold for that to be worth doing, and each is a property rather than a
//! case:
//!
//! - what comes back out is what went in, at every size around a block edge;
//! - the archive does not depend on how many cores produced it, so the same
//!   input is the same file on every machine;
//! - memory follows the window and not the entry, which is the whole reason
//!   this path exists and the one thing a naive implementation gets wrong.

#![cfg(all(feature = "lzma2", feature = "parallel"))]

use std::io::{Cursor, Read, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

use zesven::read::Archive;
use zesven::write::{EntryMeta, WriteOptions, Writer};
use zesven::{ArchivePath, Threads};

/// Entries at or above this go through the write-through path.
///
/// Mirrors `write::streaming_entry::STREAMING_THRESHOLD`, which is internal.
/// A test that quietly stopped exercising that path would still pass, so the
/// sizes below are stated relative to this rather than as bare numbers.
const STREAMING_THRESHOLD: usize = 64 * 1024 * 1024;

/// Data that compresses a little, as a video container does.
///
/// Neither incompressible nor repetitive: the first would hide a matcher that
/// silently produced nothing, and the second compresses so far that a block
/// boundary stops being visible in the output at all.
fn payload(len: usize) -> Vec<u8> {
    let mut data = Vec::with_capacity(len);
    let mut state = 0x243F_6A88_85A3_08D3u64;
    while data.len() < len {
        state ^= state << 13;
        state ^= state >> 7;
        state ^= state << 17;
        data.extend_from_slice(&state.to_le_bytes());
        if state % 8 == 0 {
            // Occasional structure, so matches exist to be found.
            data.extend_from_slice(b"........................");
        }
    }
    data.truncate(len);
    data
}

fn write_archive(data: &[u8], threads: Threads) -> Vec<u8> {
    let options = WriteOptions::new()
        .level(1)
        .expect("level")
        .threads(threads);
    let mut writer = Writer::create(Cursor::new(Vec::new()))
        .expect("writer")
        .options(options);
    writer
        .add_bytes(ArchivePath::new("big.bin").expect("path"), data)
        .expect("adds");
    let (_result, cursor) = writer.finish_into_inner().expect("finishes");
    cursor.into_inner()
}

fn read_back(archive: &[u8]) -> Vec<u8> {
    let mut archive = Archive::open(Cursor::new(archive.to_vec())).expect("opens");
    archive.extract_to_vec("big.bin").expect("extracts")
}

/// Every size around a block edge has to survive the trip.
///
/// Blocks start at one dictionary and grow to four, and a block is handed to a
/// worker only once a quarter of the next one has arrived behind it. At level 1
/// the dictionary is 1 MiB, so the first eight blocks account for 12 MiB and
/// every one after them is 4 MiB: 64 MiB is exactly a boundary, and 69 MiB is
/// exactly where the block after it becomes worth dispatching. Off-by-one in
/// the split shows up at those two sizes and nowhere else.
#[test]
fn test_large_entries_round_trip_around_block_edges() {
    let mib = 1024 * 1024;

    for len in [
        STREAMING_THRESHOLD - 1,
        STREAMING_THRESHOLD,
        STREAMING_THRESHOLD + 1,
        STREAMING_THRESHOLD + 4 * mib,
        STREAMING_THRESHOLD + 5 * mib - 1,
        STREAMING_THRESHOLD + 5 * mib,
        STREAMING_THRESHOLD + 5 * mib + 1,
    ] {
        let data = payload(len);
        let archive = write_archive(&data, Threads::count_or_single(4));
        assert_eq!(read_back(&archive), data, "len={len}");
    }
}

/// What an entry claims to be must not reach the bytes of the archive.
///
/// The length in `EntryMeta` is whatever the caller had to hand: a `stat` taken
/// before the file was finished being written, a guess about a pipe, or nothing
/// at all. It decides which entries are worth reporting progress for and
/// nothing else - if it also decided the dictionary, the block size, or which
/// path the entry took, then the same bytes described differently would produce
/// different archives, and a length declared too small would send an entry far
/// larger than memory down the path that holds it whole.
#[test]
fn test_the_declared_size_does_not_reach_the_archive() {
    let data = payload(STREAMING_THRESHOLD + 6 * 1024 * 1024);

    let write = |declared: u64| {
        let options = WriteOptions::new()
            .level(1)
            .expect("level")
            .threads(Threads::count_or_single(4));
        let mut writer = Writer::create(Cursor::new(Vec::new()))
            .expect("writer")
            .options(options);
        writer
            .add_stream(
                ArchivePath::new("big.bin").expect("path"),
                &mut Cursor::new(data.clone()),
                EntryMeta::file(declared),
            )
            .expect("adds");
        let (_result, cursor) = writer.finish_into_inner().expect("finishes");
        cursor.into_inner()
    };

    let truthful = write(data.len() as u64);
    for declared in [0, 1, (STREAMING_THRESHOLD - 1) as u64, 4 << 30] {
        assert_eq!(
            write(declared),
            truthful,
            "a declared size of {declared} changed the archive",
        );
    }
    assert_eq!(read_back(&truthful), data);
}

/// The archive must not depend on how many cores wrote it.
///
/// Block boundaries come from the dictionary alone, so a two-core laptop and a
/// sixty-four-core server have to produce the same file. Without this the
/// worker count would leak into the output and an archive would stop being
/// reproducible off the machine that made it.
#[test]
fn test_output_does_not_depend_on_worker_count() {
    let data = payload(STREAMING_THRESHOLD + 3 * 1024 * 1024);

    let reference = write_archive(&data, Threads::count_or_single(2));
    for threads in [4, 8, 16] {
        assert_eq!(
            write_archive(&data, Threads::count_or_single(threads)),
            reference,
            "{threads} workers changed the archive",
        );
    }
    assert_eq!(read_back(&reference), data);
}

/// A single thread writes one unbroken stream, which still has to read back.
///
/// That output is deliberately different - it is the smallest the level can
/// produce - so the guarantee here is the round trip, not the bytes.
#[test]
fn test_single_thread_round_trips() {
    let data = payload(STREAMING_THRESHOLD + 1024 * 1024);
    let archive = write_archive(&data, Threads::Single);
    assert_eq!(read_back(&archive), data);
}

/// A sink that records how much has reached it.
struct CountingSink {
    inner: Cursor<Vec<u8>>,
    written: Arc<AtomicU64>,
}

impl Write for CountingSink {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let n = self.inner.write(buf)?;
        self.written.fetch_add(n as u64, Ordering::Release);
        Ok(n)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.inner.flush()
    }
}

impl std::io::Seek for CountingSink {
    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
        self.inner.seek(pos)
    }
}

/// A source that records how much has been taken from it, and notes the
/// largest gap between what it has given out and what has reached the sink.
struct GapReader<'a> {
    data: &'a [u8],
    position: usize,
    written: Arc<AtomicU64>,
    largest_gap: u64,
}

impl Read for GapReader<'_> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let n = buf.len().min(self.data.len() - self.position);
        buf[..n].copy_from_slice(&self.data[self.position..self.position + n]);
        self.position += n;

        let gap = (self.position as u64).saturating_sub(self.written.load(Ordering::Acquire));
        self.largest_gap = self.largest_gap.max(gap);

        Ok(n)
    }
}

/// Data with nothing to find in it, so that what is written tracks what was
/// read.
///
/// The measurement below is read-position minus bytes-written. On data that
/// compresses, most of that difference is the compression itself and grows
/// with the entry whatever the writer holds, which would make the test pass
/// for the wrong reason. Incompressible data makes the difference the
/// writer's own backlog and nothing else.
fn incompressible(len: usize) -> Vec<u8> {
    let mut data = Vec::with_capacity(len);
    let mut state = 0x2545_F491_4F6C_DD1Du64;
    while data.len() < len {
        state ^= state << 13;
        state ^= state >> 7;
        state ^= state << 17;
        data.extend_from_slice(&state.to_le_bytes());
    }
    data.truncate(len);
    data
}

/// Runs one entry through the writer and returns the largest backlog seen.
fn largest_backlog(data: &[u8]) -> u64 {
    let written = Arc::new(AtomicU64::new(0));
    let sink = CountingSink {
        inner: Cursor::new(Vec::new()),
        written: Arc::clone(&written),
    };
    let options = WriteOptions::new()
        .level(1)
        .expect("level")
        .threads(Threads::count_or_single(4));
    let mut writer = Writer::create(sink).expect("writer").options(options);

    let mut source = GapReader {
        data,
        position: 0,
        written: Arc::clone(&written),
        largest_gap: 0,
    };

    writer
        .add_stream(
            ArchivePath::new("big.bin").expect("path"),
            &mut source,
            EntryMeta::file(data.len() as u64),
        )
        .expect("adds");
    let gap = source.largest_gap;
    let _ = writer.finish_into_inner().expect("finishes");
    gap
}

/// Memory has to follow the window, not the size of the entry.
///
/// What the writer is holding is the gap between what it has read and what it
/// has written. The invariant is that this does not grow with the entry:
/// doubling the input must not double the backlog, which is what makes a file
/// larger than memory archivable at all. A multi-threaded encoder without
/// backpressure queues the whole entry, and that is what this catches.
///
/// Stated as a comparison between two sizes rather than as a fraction of one.
/// A fraction is satisfied by anything that buffers a constant share of the
/// input - including buffering all of it - so it would have passed for a
/// writer with no bound at all.
#[test]
fn test_memory_follows_the_window_not_the_entry() {
    let small = incompressible(STREAMING_THRESHOLD + 8 * 1024 * 1024);
    let large = incompressible(STREAMING_THRESHOLD + 136 * 1024 * 1024);

    let small_backlog = largest_backlog(&small);
    let large_backlog = largest_backlog(&large);

    // The entry grew by 128 MiB. The backlog is allowed to differ by a little
    // - it is sampled, and the last blocks of a run land differently - but not
    // to follow the entry.
    let slack = 16 * 1024 * 1024;
    assert!(
        large_backlog <= small_backlog + slack,
        "backlog went from {small_backlog} bytes on a {} byte entry to \
         {large_backlog} on a {} byte one: it is following the entry rather \
         than the window",
        small.len(),
        large.len(),
    );

    // And it really is bounded, not merely growing slowly. The bound is the
    // threshold - which is read before the entry can be recognised as large -
    // plus the blocks in flight behind it. What it is not is a share of the
    // entry: on the larger entry here that would allow four times as much.
    let bound = (STREAMING_THRESHOLD + 64 * 1024 * 1024) as u64;
    for (backlog, data) in [(small_backlog, &small), (large_backlog, &large)] {
        assert!(
            backlog < bound,
            "backlog {backlog} on a {} byte entry, over a bound of {bound}",
            data.len(),
        );
    }
}

/// A wrong declared size must not cost memory either.
///
/// The bound above holds when the caller knows how long the entry is. An entry
/// that claims to be small and is not has to be recognised by reading, or the
/// writer holds a file it was told would fit and does not.
#[test]
fn test_a_size_declared_too_small_still_bounds_memory() {
    let data = incompressible(STREAMING_THRESHOLD + 136 * 1024 * 1024);

    let written = Arc::new(AtomicU64::new(0));
    let sink = CountingSink {
        inner: Cursor::new(Vec::new()),
        written: Arc::clone(&written),
    };
    let options = WriteOptions::new()
        .level(1)
        .expect("level")
        .threads(Threads::count_or_single(4));
    let mut writer = Writer::create(sink).expect("writer").options(options);

    let mut source = GapReader {
        data: &data,
        position: 0,
        written: Arc::clone(&written),
        largest_gap: 0,
    };

    writer
        .add_stream(
            ArchivePath::new("big.bin").expect("path"),
            &mut source,
            // A tenth of the truth.
            EntryMeta::file(data.len() as u64 / 10),
        )
        .expect("adds");
    let backlog = source.largest_gap;
    let _ = writer.finish_into_inner().expect("finishes");

    assert!(
        backlog < (STREAMING_THRESHOLD + 64 * 1024 * 1024) as u64,
        "backlog {backlog} on a {} byte entry declared as {}: the declared size \
         decided how the entry was held",
        data.len(),
        data.len() / 10,
    );
}

/// A batch compressed while the entry behind it streams must not reach the
/// archive.
///
/// Entries waiting in the batch are written before the large entry that
/// follows them, but they may be compressed at the same time as it - so what
/// the writer does with the batch depends on the memory budget, and the
/// archive must not. The budgets below land on either side of that: under the
/// cramped one the batch is too large a share to be worth overlapping and is
/// compressed in order, while the roomy one sends it off alongside the entry
/// and writes it when it comes back.
#[test]
fn test_a_batch_compressed_alongside_does_not_reach_the_archive() {
    use zesven::MemoryLimit;

    let small: Vec<Vec<u8>> = (0..4).map(|i| payload(1 << 20 << (i % 3))).collect();
    let large = payload(STREAMING_THRESHOLD + (4 << 20));

    let build = |limit: MemoryLimit, threads: Threads| {
        let options = WriteOptions::new()
            .level(1)
            .expect("level")
            .memory_limit(limit)
            .threads(threads);
        let mut writer = Writer::create(Cursor::new(Vec::new()))
            .expect("writer")
            .options(options);
        // Small entries, then a large one, twice: the second batch is the one
        // that starts while the first large entry is still being written.
        for round in 0..2 {
            for (i, data) in small.iter().enumerate() {
                writer
                    .add_bytes(
                        ArchivePath::new(&format!("{round}-{i}.bin")).expect("path"),
                        data,
                    )
                    .expect("adds");
            }
            writer
                .add_bytes(
                    ArchivePath::new(&format!("{round}-big.bin")).expect("path"),
                    &large,
                )
                .expect("adds");
        }
        writer.finish_into_inner().expect("finishes").1.into_inner()
    };

    let cramped = build(
        MemoryLimit::bytes_or_auto(16 << 20),
        Threads::count_or_single(2),
    );
    let roomy = build(
        MemoryLimit::bytes_or_auto(4 << 30),
        Threads::count_or_single(8),
    );

    assert_eq!(
        cramped, roomy,
        "when the batch ahead was collected reached the bytes",
    );

    // And the archive says what it should: every entry, in the order it was
    // added, holding what it was given.
    let mut archive = Archive::open(Cursor::new(cramped)).expect("opens");
    let names: Vec<String> = archive
        .entries()
        .iter()
        .map(|entry| entry.path.to_string())
        .collect();
    assert_eq!(
        names,
        vec![
            "0-0.bin",
            "0-1.bin",
            "0-2.bin",
            "0-3.bin",
            "0-big.bin",
            "1-0.bin",
            "1-1.bin",
            "1-2.bin",
            "1-3.bin",
            "1-big.bin",
        ],
    );
    assert_eq!(
        archive.extract_to_vec("0-2.bin").expect("extracts"),
        small[2]
    );
    assert_eq!(
        archive.extract_to_vec("1-big.bin").expect("extracts"),
        large
    );

    // A single thread takes neither path: the batch is not sent anywhere,
    // because a caller asking for one thread is asking for one. What that
    // produces is deliberately different - an unbroken stream - so the promise
    // here is that it still reads back.
    let single = build(MemoryLimit::bytes_or_auto(4 << 30), Threads::Single);
    let mut archive = Archive::open(Cursor::new(single)).expect("opens");
    assert_eq!(
        archive.extract_to_vec("0-2.bin").expect("extracts"),
        small[2]
    );
    assert_eq!(
        archive.extract_to_vec("1-big.bin").expect("extracts"),
        large
    );
}

/// A batch waiting to be written must not turn the entry behind it into a
/// buffer.
///
/// Its compressed output has to be held back until the batch reaches the sink,
/// and holding it without a bound would undo the one property this path exists
/// for: a 10 GB entry would be held whole because four small ones were still
/// being compressed. What is held is capped instead, and the batch is collected
/// the moment either it finishes or that cap is reached.
#[test]
fn test_a_batch_ahead_does_not_hold_the_entry() {
    use zesven::MemoryLimit;

    // Enough budget that the batch is worth sending ahead at all: below that
    // the writer compresses it in order and this measures nothing.
    let budget = MemoryLimit::bytes_or_auto(1 << 30);
    // Fewer entries than there are threads, so the batch is still waiting when
    // the large entry arrives rather than having flushed itself on the way in.
    let batch: Vec<Vec<u8>> = (0..3).map(|_| incompressible(2 << 20)).collect();

    let backlog_of = |entry: &[u8]| -> u64 {
        let written = Arc::new(AtomicU64::new(0));
        let sink = CountingSink {
            inner: Cursor::new(Vec::new()),
            written: Arc::clone(&written),
        };
        let options = WriteOptions::new()
            .level(1)
            .expect("level")
            .memory_limit(budget)
            .threads(Threads::count_or_single(4));
        let mut writer = Writer::create(sink).expect("writer").options(options);
        for (i, data) in batch.iter().enumerate() {
            writer
                .add_bytes(ArchivePath::new(&format!("s{i}.bin")).expect("path"), data)
                .expect("adds");
        }

        let mut source = GapReader {
            data: entry,
            position: 0,
            written: Arc::clone(&written),
            largest_gap: 0,
        };
        writer
            .add_stream(
                ArchivePath::new("big.bin").expect("path"),
                &mut source,
                EntryMeta::file(entry.len() as u64),
            )
            .expect("adds");
        let gap = source.largest_gap;
        let _ = writer.finish_into_inner().expect("finishes");
        gap
    };

    let small = incompressible(STREAMING_THRESHOLD + 8 * 1024 * 1024);
    let large = incompressible(STREAMING_THRESHOLD + 136 * 1024 * 1024);
    let small_backlog = backlog_of(&small);
    let large_backlog = backlog_of(&large);

    // The entry grew by 128 MiB behind the same batch. What is held back is
    // the batch and the cap on the holding area, neither of which knows how
    // large the entry is.
    let slack = 16 * 1024 * 1024;
    assert!(
        large_backlog <= small_backlog + slack,
        "backlog went from {small_backlog} on a {} byte entry to \
         {large_backlog} on a {} byte one, behind the same batch: the batch \
         ahead turned the entry into a buffer",
        small.len(),
        large.len(),
    );
}

/// A sink that records how far the source had been read when it first
/// received anything.
struct FirstWriteWatcher {
    inner: Cursor<Vec<u8>>,
    read_so_far: Arc<AtomicU64>,
    read_at_first_write: Arc<AtomicU64>,
    seen_a_write: bool,
}

impl Write for FirstWriteWatcher {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        // Only once the entry has started arriving: the signature header is
        // written before anything is read, and is not what this is watching
        // for.
        let read = self.read_so_far.load(Ordering::Acquire);
        if !self.seen_a_write && !buf.is_empty() && read > 0 {
            self.seen_a_write = true;
            self.read_at_first_write.store(read, Ordering::Release);
        }
        self.inner.write(buf)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.inner.flush()
    }
}

impl std::io::Seek for FirstWriteWatcher {
    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
        self.inner.seek(pos)
    }
}

/// A source that records how much has been taken from it.
struct WatchedReader<'a> {
    data: &'a [u8],
    position: usize,
    read_so_far: Arc<AtomicU64>,
}

impl Read for WatchedReader<'_> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let taken = (self.data.len() - self.position).min(buf.len());
        buf[..taken].copy_from_slice(&self.data[self.position..self.position + taken]);
        self.position += taken;
        self.read_so_far
            .store(self.position as u64, Ordering::Release);
        Ok(taken)
    }
}

/// The batch really is compressed alongside the entry, not before it.
///
/// Everything else here holds whether or not the two overlap - an archive
/// written in order is a correct archive - so without this the whole thing
/// could stop happening and every other test would still pass.
///
/// What gives it away is when the batch reaches the sink. Compressed in turn it
/// is written before the entry is read at all, so nothing has been taken from
/// the source past the prefix that was already read to recognise it.
/// Compressed alongside, the entry goes on being read while the batch is still
/// going, and the source is well past that by the time anything is written.
///
/// The batch is one entry, so it holds one core and takes far longer than the
/// entry does on four; that is the shape the overlap exists for, and the shape
/// that makes this measurable.
#[test]
fn test_the_batch_is_compressed_while_the_entry_is_read() {
    use zesven::MemoryLimit;

    let read_so_far = Arc::new(AtomicU64::new(0));
    let read_at_first_write = Arc::new(AtomicU64::new(0));

    let options = WriteOptions::new()
        .level(1)
        .expect("level")
        // Roomy enough that a batch this size is worth sending ahead.
        .memory_limit(MemoryLimit::bytes_or_auto(4 << 30))
        .threads(Threads::count_or_single(4));
    let mut writer = Writer::create(FirstWriteWatcher {
        inner: Cursor::new(Vec::new()),
        read_so_far: Arc::clone(&read_so_far),
        read_at_first_write: Arc::clone(&read_at_first_write),
        seen_a_write: false,
    })
    .expect("writer")
    .options(options);

    // Two entries just under the write-through threshold - past it they would
    // be streamed rather than batched - so the batch is twice the work the
    // prefix is, and stays the slower of the two even where there are not
    // enough cores to run them side by side.
    let waiting = incompressible(STREAMING_THRESHOLD - (4 << 20));
    for name in ["batch-0.bin", "batch-1.bin"] {
        writer
            .add_bytes(ArchivePath::new(name).expect("path"), &waiting)
            .expect("adds");
    }

    let large = incompressible(110 << 20);
    writer
        .add_stream(
            ArchivePath::new("big.bin").expect("path"),
            &mut WatchedReader {
                data: &large,
                position: 0,
                read_so_far: Arc::clone(&read_so_far),
            },
            EntryMeta::file(large.len() as u64),
        )
        .expect("adds");
    let archive = writer
        .finish_into_inner()
        .expect("finishes")
        .1
        .inner
        .into_inner();

    let at_first_write = read_at_first_write.load(Ordering::Acquire);
    let prefix = STREAMING_THRESHOLD as u64;
    assert!(
        at_first_write > prefix,
        "the first bytes reached the sink after {at_first_write} bytes had been \
         read, against a {prefix} byte prefix that is read before the entry is \
         even recognised: the batch was compressed before the entry rather than \
         alongside it",
    );

    // And the archive is still an archive.
    let mut archive = Archive::open(Cursor::new(archive)).expect("opens");
    assert_eq!(archive.extract_to_vec("big.bin").expect("extracts"), large);
}