bazaar 3.5.3

Rust implementation of the Bazaar formats and protocols
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
//! Storage transport abstraction.
//!
//! [`Transport`] is the path-keyed byte store that knit (and eventually
//! groupcompress, pack_repo, etc.) reads and writes through. It mirrors
//! the duck-typed Python `bzrformats.transport.Transport` interface but
//! exposes only the methods the format-handling crates actually call —
//! not the dozens of housekeeping operations the full Python interface
//! carries.
//!
//! Pure-Rust callers implement this trait directly (local FS, S3,
//! in-memory test fixtures). The pyo3 layer provides a `PyTransport`
//! adapter that wraps any Python object satisfying the equivalent
//! Python interface, so a `KnitVersionedFiles` instance built on
//! pure-Rust traits can still run on top of the existing Python
//! transport stack.
//!
//! ## Error handling
//!
//! All operations return `Result<_, TransportError>`. The variants are
//! deliberately coarse — most callers either propagate the error or
//! match on `NoSuchFile` for the not-found path. Detailed I/O errors
//! are normalised into `(ErrorKind, String)` so the enum stays
//! `Clone + PartialEq + Eq` and tests can compare error values.

/// Errors returned by [`Transport`] operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportError {
    /// The requested path does not exist.
    NoSuchFile(String),
    /// The transport refused a write because it is read-only.
    ReadOnly(String),
    /// An underlying I/O error. The `(ErrorKind, message)` pair is
    /// preserved so callers can branch on kind without losing the
    /// original diagnostic.
    Io {
        kind: std::io::ErrorKind,
        message: String,
    },
    /// Catch-all for transport-specific failures that don't map to
    /// any of the above (typically wrapped Python exceptions on the
    /// pyo3 adapter side).
    Other(String),
}

impl From<std::io::Error> for TransportError {
    fn from(e: std::io::Error) -> Self {
        if e.kind() == std::io::ErrorKind::NotFound {
            TransportError::NoSuchFile(e.to_string())
        } else {
            TransportError::Io {
                kind: e.kind(),
                message: e.to_string(),
            }
        }
    }
}

impl std::fmt::Display for TransportError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TransportError::NoSuchFile(p) => write!(f, "No such file: {}", p),
            TransportError::ReadOnly(p) => write!(f, "Read-only transport: {}", p),
            TransportError::Io { kind, message } => {
                write!(f, "I/O error ({:?}): {}", kind, message)
            }
            TransportError::Other(s) => write!(f, "Transport error: {}", s),
        }
    }
}

impl std::error::Error for TransportError {}

/// One range request handed to [`Transport::readv`]: byte offset plus
/// length to read.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadRange {
    pub offset: u64,
    pub length: usize,
}

/// One byte range returned from [`Transport::readv`]. The `offset` /
/// `length` echo the request the bytes correspond to so callers can
/// match each result against its request without tracking order
/// themselves (the implementation is allowed to coalesce adjacent
/// requests and yield the merged bytes in any order).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadResult {
    pub offset: u64,
    pub length: usize,
    pub bytes: Vec<u8>,
}

/// Path-keyed byte store. The minimal method set needed by the knit
/// reader and writer — additional operations can be added as more
/// modules port to this trait.
///
/// `path` is always interpreted as relative to the transport's root.
/// Implementations are responsible for whatever path normalisation
/// their backing store requires.
pub trait Transport {
    /// Read the entire contents of `path`.
    fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError>;

    /// Write `bytes` to `path`, creating parent directories if
    /// `create_parent_dir` is true. Replaces any existing content.
    fn put_file_non_atomic(
        &self,
        path: &str,
        bytes: &[u8],
        create_parent_dir: bool,
    ) -> Result<(), TransportError>;

    /// Atomically write `bytes` to `path`, replacing any existing content.
    /// `mode` is an optional Unix permission bits value for the new file.
    ///
    /// The default implementation defers to [`Transport::put_file_non_atomic`]
    /// (ignoring `mode`); backends with a native atomic put should override it.
    fn put_bytes(&self, path: &str, bytes: &[u8], mode: Option<u32>) -> Result<(), TransportError> {
        let _ = mode;
        self.put_file_non_atomic(path, bytes, false)
    }

    /// Append `bytes` to the end of `path`, creating it if missing.
    /// Returns the byte offset where the appended data starts.
    fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError>;

    /// Create directory `path`. It is not an error if the directory already
    /// exists; implementations should silently succeed in that case.
    fn mkdir(&self, path: &str) -> Result<(), TransportError>;

    /// Test whether `path` exists.
    fn has(&self, path: &str) -> Result<bool, TransportError>;

    /// Read multiple byte ranges from `path` in a single call.
    /// Implementations are encouraged (but not required) to coalesce
    /// adjacent ranges and issue a single underlying read; the order
    /// of returned [`ReadResult`]s is not specified, but each result
    /// carries its `offset`/`length` so callers can match it back to
    /// the request.
    ///
    /// The default implementation falls back to a `get_bytes` of the
    /// whole file followed by per-range slicing — efficient enough
    /// for in-memory and small-file backends, but real network
    /// transports should override this with a true range read.
    fn readv(&self, path: &str, ranges: &[ReadRange]) -> Result<Vec<ReadResult>, TransportError> {
        let data = self.get_bytes(path)?;
        let mut out = Vec::with_capacity(ranges.len());
        for r in ranges {
            let start = r.offset as usize;
            let end = start.checked_add(r.length).ok_or_else(|| {
                TransportError::Other(format!(
                    "readv range overflow: offset={} length={}",
                    r.offset, r.length
                ))
            })?;
            if end > data.len() {
                return Err(TransportError::Other(format!(
                    "readv range past end: offset={} length={} data_len={}",
                    r.offset,
                    r.length,
                    data.len()
                )));
            }
            out.push(ReadResult {
                offset: r.offset,
                length: r.length,
                bytes: data[start..end].to_vec(),
            });
        }
        Ok(out)
    }

    /// List all files under the transport root recursively, returning
    /// relative paths. Used by [`crate::knit::KndxIndex::keys`] to
    /// enumerate prefixes when the mapper is not constant.
    fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError>;

    /// Resolve `path` relative to the transport root into an absolute
    /// identifier (typically a filesystem path or URL). Used for error
    /// messages and reload-tracking; implementations are free to
    /// return any stable string.
    fn abspath(&self, path: &str) -> Result<String, TransportError>;

    /// Rename `from` to `to`. For the lockdir protocol this must fail
    /// (rather than overwrite) when `to` already exists, so that the
    /// atomic "claim the lock by renaming into place" step is reliable.
    ///
    /// The default returns [`TransportError::Other`]; backends that
    /// support renaming must override it.
    fn rename(&self, from: &str, to: &str) -> Result<(), TransportError> {
        let _ = (from, to);
        Err(TransportError::Other(
            "rename not supported by this transport".to_string(),
        ))
    }

    /// Delete the file at `path`.
    fn delete(&self, path: &str) -> Result<(), TransportError> {
        let _ = path;
        Err(TransportError::Other(
            "delete not supported by this transport".to_string(),
        ))
    }

    /// Remove the (empty) directory at `path`.
    fn rmdir(&self, path: &str) -> Result<(), TransportError> {
        let _ = path;
        Err(TransportError::Other(
            "rmdir not supported by this transport".to_string(),
        ))
    }

    /// List the immediate entries of directory `path`, returning their
    /// names (not full paths) in unspecified order.
    fn list_dir(&self, path: &str) -> Result<Vec<String>, TransportError> {
        let _ = path;
        Err(TransportError::Other(
            "list_dir not supported by this transport".to_string(),
        ))
    }

    /// Return metadata about `path`.
    fn stat(&self, path: &str) -> Result<Stat, TransportError> {
        let _ = path;
        Err(TransportError::Other(
            "stat not supported by this transport".to_string(),
        ))
    }

    /// Return a new transport rooted at `path` relative to this one.
    ///
    /// Used to descend from a `.bzr` directory into its `repository`,
    /// `branch` and `checkout` components. The default returns
    /// [`TransportError::Other`]; backends that can be re-rooted (e.g.
    /// [`LocalTransport`]) override it.
    fn subtransport(&self, path: &str) -> Result<SharedTransport, TransportError> {
        let _ = path;
        Err(TransportError::Other(
            "subtransport not supported by this transport".to_string(),
        ))
    }

    /// The local filesystem path of `path` relative to this transport, when
    /// the transport is backed by the local filesystem.
    ///
    /// Returns `None` for non-local backends. Used by operations that need
    /// a real OS path (e.g. taking an fcntl lock to rewrite the dirstate).
    fn local_path(&self, path: &str) -> Option<std::path::PathBuf> {
        let _ = path;
        None
    }
}

/// Forward [`Transport`] through the shared `Arc`, so a `SharedTransport`
/// can be used wherever a `T: Transport` is needed (e.g. the knit
/// `KndxIndex<T, M>` / `KnitKeyAccess<T, M>` stores).
impl Transport for std::sync::Arc<dyn Transport + Send + Sync> {
    fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
        (**self).get_bytes(path)
    }

    fn put_file_non_atomic(
        &self,
        path: &str,
        bytes: &[u8],
        create_parent_dir: bool,
    ) -> Result<(), TransportError> {
        (**self).put_file_non_atomic(path, bytes, create_parent_dir)
    }

    fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
        (**self).append_bytes(path, bytes)
    }

    fn mkdir(&self, path: &str) -> Result<(), TransportError> {
        (**self).mkdir(path)
    }

    fn has(&self, path: &str) -> Result<bool, TransportError> {
        (**self).has(path)
    }

    fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
        (**self).iter_files_recursive()
    }

    fn abspath(&self, path: &str) -> Result<String, TransportError> {
        (**self).abspath(path)
    }

    fn readv(&self, path: &str, ranges: &[ReadRange]) -> Result<Vec<ReadResult>, TransportError> {
        (**self).readv(path, ranges)
    }

    fn put_bytes(&self, path: &str, bytes: &[u8], mode: Option<u32>) -> Result<(), TransportError> {
        (**self).put_bytes(path, bytes, mode)
    }

    fn rename(&self, from: &str, to: &str) -> Result<(), TransportError> {
        (**self).rename(from, to)
    }

    fn delete(&self, path: &str) -> Result<(), TransportError> {
        (**self).delete(path)
    }

    fn rmdir(&self, path: &str) -> Result<(), TransportError> {
        (**self).rmdir(path)
    }

    fn list_dir(&self, path: &str) -> Result<Vec<String>, TransportError> {
        (**self).list_dir(path)
    }

    fn stat(&self, path: &str) -> Result<Stat, TransportError> {
        (**self).stat(path)
    }

    fn subtransport(&self, path: &str) -> Result<SharedTransport, TransportError> {
        (**self).subtransport(path)
    }

    fn local_path(&self, path: &str) -> Option<std::path::PathBuf> {
        (**self).local_path(path)
    }
}

/// A transport shared across the opener objects (`BzrDir`, `Branch`,
/// `Repository`, `WorkingTree`).
///
/// They own their transport via this `Arc` rather than borrowing it, so a
/// `BzrDir` can hand out sub-objects that outlive it, and the 2a
/// repository's CHK store (which needs `Arc<S>` with `S: Send + Sync`) is
/// satisfiable. `Send + Sync` is required because the groupcompress stores
/// implement the `Send + Sync` `VersionedFiles` trait.
pub type SharedTransport = std::sync::Arc<dyn Transport + Send + Sync>;

/// Minimal file metadata returned by [`Transport::stat`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stat {
    /// Size in bytes (0 for directories).
    pub size: u64,
    /// Whether the entry is a directory.
    pub is_dir: bool,
}

/// A [`Transport`] rooted at a local filesystem directory.
///
/// All `path` arguments are interpreted relative to [`root`](LocalTransport::root)
/// and joined onto it; the transport does not guard against `..`
/// escaping the root, matching the trust model of the rest of the
/// format code (callers pass paths they constructed themselves).
pub struct LocalTransport {
    root: std::path::PathBuf,
}

impl LocalTransport {
    /// Create a transport rooted at `root`.
    pub fn new<P: Into<std::path::PathBuf>>(root: P) -> Self {
        LocalTransport { root: root.into() }
    }

    /// The directory this transport is rooted at.
    pub fn root(&self) -> &std::path::Path {
        &self.root
    }

    fn resolve(&self, path: &str) -> std::path::PathBuf {
        // Relative transport paths are URL-escaped (the key mappers emit
        // percent-encoded relpaths, e.g. `%40` for `@`); the local transport
        // decodes them to a filesystem path, mirroring breezy's
        // `urlutils.local_path_from_url`. Plain ASCII paths are unaffected.
        self.root.join(crate::key_mapper::url_unquote(path))
    }
}

impl Transport for LocalTransport {
    fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
        Ok(std::fs::read(self.resolve(path))?)
    }

    fn put_file_non_atomic(
        &self,
        path: &str,
        bytes: &[u8],
        create_parent_dir: bool,
    ) -> Result<(), TransportError> {
        let full = self.resolve(path);
        if create_parent_dir {
            if let Some(parent) = full.parent() {
                std::fs::create_dir_all(parent)?;
            }
        }
        std::fs::write(full, bytes)?;
        Ok(())
    }

    fn put_bytes(&self, path: &str, bytes: &[u8], mode: Option<u32>) -> Result<(), TransportError> {
        // Atomic via write-to-temp-then-rename within the same directory.
        let _ = mode;
        let full = self.resolve(path);
        let parent = full.parent().ok_or_else(|| {
            TransportError::Other(format!("path has no parent directory: {path}"))
        })?;
        let tmp = parent.join(format!(".{}.tmp", crate::osutils::rand_chars(16)));
        std::fs::write(&tmp, bytes)?;
        if let Err(e) = std::fs::rename(&tmp, &full) {
            let _ = std::fs::remove_file(&tmp);
            return Err(e.into());
        }
        Ok(())
    }

    fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
        use std::io::{Seek, Write};
        let mut f = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(self.resolve(path))?;
        let offset = f.seek(std::io::SeekFrom::End(0))?;
        f.write_all(bytes)?;
        Ok(offset)
    }

    fn mkdir(&self, path: &str) -> Result<(), TransportError> {
        match std::fs::create_dir(self.resolve(path)) {
            Ok(()) => Ok(()),
            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
            Err(e) => Err(e.into()),
        }
    }

    fn has(&self, path: &str) -> Result<bool, TransportError> {
        Ok(self.resolve(path).exists())
    }

    fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
        let mut out = Vec::new();
        let mut stack = vec![self.root.clone()];
        while let Some(dir) = stack.pop() {
            for entry in std::fs::read_dir(&dir)? {
                let entry = entry?;
                let p = entry.path();
                if p.is_dir() {
                    stack.push(p);
                } else if let Ok(rel) = p.strip_prefix(&self.root) {
                    // Return URL-escaped relpaths, symmetric with `resolve`
                    // unquoting them (and with breezy's local transport), so
                    // a listed path round-trips through the key mappers'
                    // `unmap`. `/` separators stay literal.
                    let rel = rel.to_string_lossy().replace('\\', "/");
                    out.push(crate::key_mapper::url_quote(&rel));
                }
            }
        }
        Ok(out)
    }

    fn abspath(&self, path: &str) -> Result<String, TransportError> {
        Ok(self.resolve(path).to_string_lossy().into_owned())
    }

    fn rename(&self, from: &str, to: &str) -> Result<(), TransportError> {
        let to_path = self.resolve(to);
        // bzr's lockdir relies on rename failing when the target exists
        // (so two contenders can't both "win" the lock). std::fs::rename
        // would silently overwrite an empty target dir on some platforms,
        // so reject an existing target explicitly.
        if to_path.exists() {
            return Err(TransportError::Io {
                kind: std::io::ErrorKind::AlreadyExists,
                message: format!("rename target already exists: {to}"),
            });
        }
        std::fs::rename(self.resolve(from), to_path)?;
        Ok(())
    }

    fn delete(&self, path: &str) -> Result<(), TransportError> {
        std::fs::remove_file(self.resolve(path))?;
        Ok(())
    }

    fn rmdir(&self, path: &str) -> Result<(), TransportError> {
        std::fs::remove_dir(self.resolve(path))?;
        Ok(())
    }

    fn list_dir(&self, path: &str) -> Result<Vec<String>, TransportError> {
        let mut out = Vec::new();
        for entry in std::fs::read_dir(self.resolve(path))? {
            out.push(entry?.file_name().to_string_lossy().into_owned());
        }
        Ok(out)
    }

    fn stat(&self, path: &str) -> Result<Stat, TransportError> {
        let meta = std::fs::metadata(self.resolve(path))?;
        Ok(Stat {
            size: meta.len(),
            is_dir: meta.is_dir(),
        })
    }

    fn subtransport(&self, path: &str) -> Result<SharedTransport, TransportError> {
        Ok(std::sync::Arc::new(LocalTransport::new(self.resolve(path))))
    }

    fn local_path(&self, path: &str) -> Option<std::path::PathBuf> {
        Some(self.resolve(path))
    }
}

#[cfg(test)]
pub(crate) mod testing {
    //! In-memory `Transport` implementation, available to tests in
    //! other modules of this crate.
    use super::*;
    use std::collections::HashMap;
    use std::sync::Mutex;

    #[derive(Default)]
    pub struct MemoryTransport {
        files: Mutex<HashMap<String, Vec<u8>>>,
        root: String,
    }

    impl MemoryTransport {
        pub fn new() -> Self {
            Self {
                files: Mutex::new(HashMap::new()),
                root: "memory:///".to_string(),
            }
        }
    }

    impl Transport for MemoryTransport {
        fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
            let files = self.files.lock().unwrap();
            files
                .get(path)
                .cloned()
                .ok_or_else(|| TransportError::NoSuchFile(path.to_string()))
        }

        fn put_file_non_atomic(
            &self,
            path: &str,
            bytes: &[u8],
            _create_parent_dir: bool,
        ) -> Result<(), TransportError> {
            let mut files = self.files.lock().unwrap();
            files.insert(path.to_string(), bytes.to_vec());
            Ok(())
        }

        fn mkdir(&self, _path: &str) -> Result<(), TransportError> {
            Ok(())
        }

        fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
            let mut files = self.files.lock().unwrap();
            let entry = files.entry(path.to_string()).or_default();
            let offset = entry.len() as u64;
            entry.extend_from_slice(bytes);
            Ok(offset)
        }

        fn has(&self, path: &str) -> Result<bool, TransportError> {
            let files = self.files.lock().unwrap();
            Ok(files.contains_key(path))
        }

        fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
            let files = self.files.lock().unwrap();
            Ok(files.keys().cloned().collect())
        }

        fn abspath(&self, path: &str) -> Result<String, TransportError> {
            Ok(format!("{}{}", self.root, path))
        }
    }

    #[test]
    fn memory_transport_basic_round_trip() {
        let t = MemoryTransport::new();
        assert!(!t.has("foo").unwrap());
        t.put_file_non_atomic("foo", b"hello", false).unwrap();
        assert!(t.has("foo").unwrap());
        assert_eq!(t.get_bytes("foo").unwrap(), b"hello".to_vec());
    }

    #[test]
    fn memory_transport_append_returns_offset() {
        let t = MemoryTransport::new();
        assert_eq!(t.append_bytes("log", b"first ").unwrap(), 0);
        assert_eq!(t.append_bytes("log", b"second").unwrap(), 6);
        assert_eq!(t.get_bytes("log").unwrap(), b"first second".to_vec());
    }

    #[test]
    fn memory_transport_get_bytes_missing_is_error() {
        let t = MemoryTransport::new();
        assert_eq!(
            t.get_bytes("nope").unwrap_err(),
            TransportError::NoSuchFile("nope".to_string())
        );
    }

    #[test]
    fn default_readv_slices_via_get_bytes() {
        let t = MemoryTransport::new();
        t.put_file_non_atomic("data", b"0123456789", false).unwrap();
        let ranges = vec![
            ReadRange {
                offset: 0,
                length: 3,
            },
            ReadRange {
                offset: 5,
                length: 2,
            },
        ];
        let results = t.readv("data", &ranges).unwrap();
        assert_eq!(results.len(), 2);
        assert_eq!(results[0].bytes, b"012".to_vec());
        assert_eq!(results[1].bytes, b"56".to_vec());
    }

    #[test]
    fn default_readv_rejects_past_end() {
        let t = MemoryTransport::new();
        t.put_file_non_atomic("data", b"hi", false).unwrap();
        let err = t
            .readv(
                "data",
                &[ReadRange {
                    offset: 0,
                    length: 100,
                }],
            )
            .unwrap_err();
        assert!(matches!(err, TransportError::Other(_)));
    }
}