Skip to main content

vhdx/io/
core.rs

1//! IO module: sector-level read/write operations for the virtual disk.
2//!
3//! This is the **sole data-plane entry point**. All virtual disk payload reads
4//! and writes must go through [`IO::sector`] → [`Sector`] implementing
5//! [`std::io::Read`], [`std::io::Write`], and [`std::io::Seek`].
6//! Direct reads via [`Medium::get_ref`](crate::medium::Medium::get_ref) are forbidden
7//! for payload data-plane access.
8//!
9//! # Differencing disk support
10//!
11//! For differencing (child) disks:
12//! - Sector bitmap blocks are checked for [`PayloadBlockState::PartiallyPresent`].
13//! - Sectors not present in the child fall back to the parent disk.
14//! - The parent medium is resolved lazily and cached.
15//!
16//! # Standard
17//!
18//! MS-VHDX §2.5.1 (BAT state semantics for payload blocks)
19
20use bitvec::prelude::*;
21use std::cell::RefCell;
22use std::sync::Arc;
23
24use std::io::{self, Read, Seek, SeekFrom, Write};
25
26use crate::bat::{Bat, BatState, PayloadBlockState, SectorBitmapState};
27use crate::constants::MIB;
28use crate::error::{Error, Result};
29use crate::log_replay::ReplayOverlay;
30use crate::medium::ReadSemanticsPolicy;
31use crate::medium::{
32    Len, Medium, ParentMedium, ParentRequest, ParentResolver, SetLen, SyncData, read_exact_at,
33};
34use crate::metadata::Metadata;
35
36// ---------------------------------------------------------------------------
37// IO
38// ---------------------------------------------------------------------------
39
40/// Virtual disk sector-level I/O.
41///
42/// Constructed internally from a mutable medium reference.
43/// The IO struct resolves BAT entries, manages block offsets, and provides
44/// the only path to sector-level reads and writes.
45///
46/// # Standard
47///
48/// MS-VHDX §2.5.1 — BAT entry state semantics for sector reads.
49pub struct IO<'a, T = std::fs::File> {
50    pub(super) file: &'a mut Medium<T>,
51    pub(super) block_size: u32,
52    pub(super) logical_sector_size: u32,
53    pub(super) chunk_ratio: u64,
54    max_sector: u64,
55    pub(super) has_parent: bool,
56    /// In-memory replay overlay for serving post-replay data through the read path.
57    pub(super) overlay: Option<Arc<ReplayOverlay>>,
58    parent_medium: RefCell<Option<Box<dyn ParentMedium>>>,
59}
60
61#[derive(Clone, Copy, Debug)]
62pub(super) struct ResolvedBatEntry {
63    pub(super) state: BatState,
64    file_offset_mb: u64,
65}
66
67impl ResolvedBatEntry {
68    pub(super) fn file_offset_mb(&self) -> u64 {
69        self.file_offset_mb
70    }
71}
72
73impl<'a, T> IO<'a, T>
74where
75    T: Read + Seek,
76{
77    /// Create a new IO context from a medium reference.
78    ///
79    /// Loads metadata from the file to extract block size, sector sizes,
80    /// parent status, and chunk ratio.
81    pub(crate) fn new(file: &'a mut Medium<T>) -> Result<Self> {
82        let overlay = file.replay_overlay_arc().cloned();
83        let meta_buf = file.metadata_buf()?.to_vec();
84        let metadata = Metadata::new(&meta_buf)?;
85        let items = metadata.items();
86
87        let fp = items
88            .file_parameters()
89            .map_err(|_| Error::InvalidMetadata("FileParameters metadata item not found".into()))?;
90        let block_size = fp.block_size();
91        let has_parent = fp.has_parent();
92        if block_size == 0 {
93            return Err(Error::InvalidMetadata("block size must be non-zero".into()));
94        }
95
96        let logical_sector_size = items.logical_sector_size().ok().unwrap_or(512);
97        if logical_sector_size == 0 {
98            return Err(Error::InvalidMetadata(
99                "logical sector size must be non-zero".into(),
100            ));
101        }
102
103        let virtual_size = items.virtual_disk_size().map_err(|_| {
104            Error::InvalidMetadata("VirtualDiskSize metadata item not found".into())
105        })?;
106
107        let max_sector = virtual_size / u64::from(logical_sector_size);
108
109        // chunk_ratio = (2^23 * LogicalSectorSize) / BlockSize
110        let chunk_ratio = (1u64 << 23) * u64::from(logical_sector_size) / u64::from(block_size);
111
112        Ok(Self {
113            file,
114            block_size,
115            logical_sector_size,
116            chunk_ratio,
117            max_sector: max_sector.saturating_sub(1),
118            has_parent,
119            overlay,
120            parent_medium: RefCell::new(None),
121        })
122    }
123
124    /// Locate and return a [`Sector`] spanning `count` sectors starting at
125    /// global sector number `start`.
126    ///
127    /// # Errors
128    ///
129    /// - [`Error::InvalidParameter`] if `count == 0`, `start + count` overflows,
130    ///   or `count * logical_sector_size` overflows.
131    /// - [`Error::SectorOutOfBounds`] if the range exceeds the virtual disk.
132    pub fn sector<'io>(&'io mut self, start: u64, count: u64) -> Result<Sector<'io, 'a, T>> {
133        if count == 0 {
134            return Err(Error::InvalidParameter("count must be >= 1".into()));
135        }
136
137        let end_sector = start
138            .checked_add(count)
139            .ok_or_else(|| Error::InvalidParameter("start + count overflow".into()))?;
140
141        if end_sector - 1 > self.max_sector {
142            return Err(Error::SectorOutOfBounds {
143                sector: start,
144                max: self.max_sector,
145            });
146        }
147
148        let logical_sector_size = self.logical_sector_size;
149        let block_size = self.block_size;
150        let chunk_ratio = self.chunk_ratio;
151        let range_bytes = count
152            .checked_mul(u64::from(logical_sector_size))
153            .ok_or_else(|| Error::InvalidParameter("sector_count * lss overflow".into()))?;
154
155        Ok(Sector {
156            io: self,
157            start,
158            count,
159            logical_sector_size,
160            block_size,
161            chunk_ratio,
162            pos: 0,
163            range_bytes,
164            semantics: ReadSemanticsPolicy::default(),
165        })
166    }
167}
168
169// ---------------------------------------------------------------------------
170// Sector
171// ---------------------------------------------------------------------------
172
173/// A handle to one or more logical sectors within a virtual disk block.
174///
175/// Created by [`IO::sector`].
176pub struct Sector<'io, 'medium, T = std::fs::File> {
177    pub(super) io: &'io mut IO<'medium, T>,
178    pub(super) start: u64,
179    pub(super) count: u64,
180    pub(super) logical_sector_size: u32,
181    pub(super) block_size: u32,
182    pub(super) chunk_ratio: u64,
183    pub(super) pos: u64,
184    pub(super) range_bytes: u64,
185    pub(super) semantics: ReadSemanticsPolicy,
186}
187
188impl<'medium, T> Sector<'_, 'medium, T>
189where
190    T: Read + Seek,
191{
192    pub(super) fn io(&self) -> &IO<'medium, T> {
193        self.io
194    }
195
196    pub(super) fn io_mut(&mut self) -> &mut IO<'medium, T> {
197        self.io
198    }
199
200    /// Set the read semantics policy for this sector range.
201    ///
202    /// Controls how Unmapped blocks are handled during reads:
203    /// - [`ReadSemanticsPolicy::EffectiveDataPreferred`] (default): return zeros.
204    /// - [`ReadSemanticsPolicy::RawDataPreferred`]: read raw on-disk data,
205    ///   falling back to zeros on error.
206    #[must_use]
207    pub fn semantics(mut self, policy: ReadSemanticsPolicy) -> Self {
208        self.semantics = policy;
209        self
210    }
211
212    /// Read data from this sector range at the given byte offset.
213    ///
214    /// `byte_offset` is relative to the first sector in this range (0-based).
215    /// The resulting byte range `[byte_offset, byte_offset + buf.len())` must
216    /// fit within `sector_count * logical_sector_size`.
217    ///
218    /// Respects the sector's semantics policy.
219    ///
220    /// # Panics
221    ///
222    /// Panics if arithmetic overflow occurs during sector/offset conversion.
223    /// This should not happen with well-formed VHDX files.
224    fn read_at(&mut self, buf: &mut [u8], byte_offset: u64) -> Result<()> {
225        let lss = self.logical_sector_size as usize;
226        let range_bytes = self.count * lss as u64;
227
228        // Empty read is a no-op
229        if buf.is_empty() {
230            return Ok(());
231        }
232
233        // Validate byte range
234        let byte_end = byte_offset
235            .checked_add(buf.len() as u64)
236            .ok_or_else(|| Error::InvalidParameter("byte_offset + buf.len() overflow".into()))?;
237        if byte_end > range_bytes {
238            return Err(Error::InvalidParameter(format!(
239                "byte range [{byte_offset}, {byte_end}) exceeds sector range of {range_bytes} bytes"
240            )));
241        }
242
243        let start_byte = usize::try_from(byte_offset)
244            .map_err(|_| Error::InvalidParameter("byte_offset does not fit usize".into()))?;
245        let end_byte = start_byte + buf.len();
246
247        let first_sector_rel = start_byte / lss; // relative to start_sector
248        let first_skip = start_byte % lss; // bytes to skip in first sector
249        let aligned_end = end_byte.is_multiple_of(lss);
250
251        // Fast path: sector-aligned start AND sector-aligned end
252        if first_skip == 0 && aligned_end {
253            let sectors_to_read = buf.len() / lss;
254            return self.read_full_sectors(
255                buf,
256                self.start + u64::try_from(first_sector_rel).expect("sector index fits u64"),
257                u64::try_from(sectors_to_read).expect("sector count fits u64"),
258            );
259        }
260
261        // Slow path: need to read full sectors and extract sub-range
262        let last_sector_rel = (end_byte - 1) / lss;
263        let affected_count = last_sector_rel - first_sector_rel + 1;
264        let mut temp = vec![0u8; affected_count * lss];
265        self.read_full_sectors(
266            &mut temp,
267            self.start + u64::try_from(first_sector_rel).expect("sector index fits u64"),
268            u64::try_from(affected_count).expect("sector count fits u64"),
269        )?;
270        buf.copy_from_slice(&temp[first_skip..first_skip + buf.len()]);
271        Ok(())
272    }
273
274    /// Read `sector_count` full sectors starting at absolute `start_sector` into `buf`.
275    /// `buf.len()` must equal `sector_count * logical_sector_size`.
276    /// Respects semantics policy for Unmapped blocks.
277    ///
278    /// # Panics
279    ///
280    /// Panics if arithmetic overflow occurs during sector/offset conversion.
281    /// This should not happen with well-formed VHDX files.
282    pub(super) fn read_full_sectors(
283        &mut self, buf: &mut [u8], start_sector: u64, sector_count: u64,
284    ) -> Result<()> {
285        let lss = self.logical_sector_size as usize;
286        let spb = self.sectors_per_block();
287        let mut buf_offset = 0usize;
288        let mut current_sector = start_sector;
289        let mut remaining = sector_count;
290
291        while remaining > 0 {
292            let block_idx = current_sector / spb;
293            let sector_in_block = current_sector % spb;
294            let remaining_in_block = spb - sector_in_block;
295            let sectors_this_round = remaining.min(remaining_in_block);
296            let bytes_this_round =
297                usize::try_from(sectors_this_round).expect("sector count fits usize") * lss;
298
299            let entry = self.resolve_bat_entry_for_block(block_idx)?;
300            let state = entry.state;
301
302            match state {
303                BatState::Payload(payload_state) => match payload_state {
304                    PayloadBlockState::FullyPresent => {
305                        self.read_block_range_from_file(
306                            entry.file_offset_mb(),
307                            sector_in_block,
308                            sectors_this_round,
309                            &mut buf[buf_offset..buf_offset + bytes_this_round],
310                        )?;
311                    }
312                    PayloadBlockState::PartiallyPresent => {
313                        self.read_partially_present_range(
314                            entry,
315                            block_idx,
316                            sector_in_block,
317                            sectors_this_round,
318                            &mut buf[buf_offset..buf_offset + bytes_this_round],
319                        )?;
320                    }
321                    PayloadBlockState::Unmapped => {
322                        if self.semantics == ReadSemanticsPolicy::RawDataPreferred {
323                            if self
324                                .read_block_range_from_file(
325                                    entry.file_offset_mb(),
326                                    sector_in_block,
327                                    sectors_this_round,
328                                    &mut buf[buf_offset..buf_offset + bytes_this_round],
329                                )
330                                .is_err()
331                            {
332                                buf[buf_offset..buf_offset + bytes_this_round].fill(0);
333                            }
334                        } else {
335                            buf[buf_offset..buf_offset + bytes_this_round].fill(0);
336                        }
337                    }
338                    PayloadBlockState::NotPresent if self.io().has_parent => {
339                        self.read_parent_range(
340                            block_idx,
341                            sector_in_block,
342                            sectors_this_round,
343                            &mut buf[buf_offset..buf_offset + bytes_this_round],
344                        )?;
345                    }
346                    PayloadBlockState::Zero
347                    | PayloadBlockState::NotPresent
348                    | PayloadBlockState::Undefined => {
349                        buf[buf_offset..buf_offset + bytes_this_round].fill(0);
350                    }
351                },
352                BatState::SectorBitmap(_) => {
353                    return Err(Error::BlockNotPresent {
354                        block_idx,
355                        state: "sector bitmap entry (expected payload)".into(),
356                    });
357                }
358            }
359
360            buf_offset += bytes_this_round;
361            current_sector += sectors_this_round;
362            remaining -= sectors_this_round;
363        }
364
365        Ok(())
366    }
367
368    // -- Internal helpers ---------------------------------------------------
369
370    /// Number of logical sectors per payload block.
371    pub(super) fn sectors_per_block(&self) -> u64 {
372        u64::from(self.block_size) / u64::from(self.logical_sector_size)
373    }
374
375    pub(super) fn sector_bitmap_bat_index(&self, block_idx: u64) -> u64 {
376        let stride = self.chunk_ratio + 1;
377        let chunk_idx = block_idx / self.chunk_ratio;
378        chunk_idx * stride + self.chunk_ratio
379    }
380
381    fn read_parent_range(
382        &mut self, block_idx: u64, start_sector_in_block: u64, sector_count: u64, buf: &mut [u8],
383    ) -> Result<()> {
384        let lss = self.logical_sector_size as usize;
385        let spb = self.sectors_per_block();
386        for i in 0..sector_count {
387            let offset = usize::try_from(i).expect("sector offset fits usize") * lss;
388            self.read_from_parent_sector(
389                block_idx * spb + start_sector_in_block + i,
390                &mut buf[offset..offset + lss],
391            )?;
392        }
393        Ok(())
394    }
395
396    /// Resolve the BAT entry for this sector's block.
397    #[cfg(test)]
398    pub(super) fn resolve_bat_entry(&mut self) -> Result<ResolvedBatEntry> {
399        let block_idx = self.start / self.sectors_per_block();
400        self.resolve_bat_entry_for_block(block_idx)
401    }
402
403    /// Resolve the BAT entry for a specific block index.
404    pub(super) fn resolve_bat_entry_for_block(
405        &mut self, block_idx: u64,
406    ) -> Result<ResolvedBatEntry> {
407        let bat_buf = self.io_mut().file.bat_buf()?;
408        let bat = Bat::new(&bat_buf, self.chunk_ratio);
409        let bat_array_idx = block_idx + block_idx / self.chunk_ratio;
410        let entry = bat.entry(bat_array_idx)?;
411        Ok(ResolvedBatEntry {
412            state: entry.state()?,
413            file_offset_mb: entry.file_offset_mb(),
414        })
415    }
416
417    /// Read a contiguous range of sectors from a payload block in the file.
418    ///
419    /// `sector_in_block` is the offset of the first sector within the block,
420    /// `buf` determines the amount of data to read (its length sets the read size),
421    /// and `_sector_count` is currently unused.
422    fn read_block_range_from_file(
423        &mut self, file_offset_mb: u64, sector_in_block: u64, _sector_count: u64, buf: &mut [u8],
424    ) -> Result<()> {
425        let lss = self.logical_sector_size as usize;
426        let file_offset = file_offset_mb * u64::from(MIB) + sector_in_block * lss as u64;
427
428        // Consult replay overlay first (per-block-span)
429        if let Some(ref overlay) = self.io().overlay {
430            let n = overlay.read(file_offset, buf);
431            if n > 0 {
432                return Ok(());
433            }
434
435            // T20: check physical file size gap
436            let last_file_offset = overlay.last_file_offset();
437            if last_file_offset > 0 && file_offset < last_file_offset {
438                buf.fill(0);
439                return Ok(());
440            }
441        }
442
443        read_exact_at(self.io_mut().file.inner_mut(), file_offset, buf)?;
444        Ok(())
445    }
446
447    /// Read a range of sectors from a `PartiallyPresent` payload block.
448    ///
449    /// Each sector is checked against the sector bitmap: if the bit is set
450    /// the sector is read from the child file, otherwise from the parent.
451    ///
452    /// # Panics
453    ///
454    /// Panics if arithmetic overflow occurs during sector/offset conversion.
455    /// This should not happen with well-formed VHDX files.
456    fn read_partially_present_range(
457        &mut self, entry: ResolvedBatEntry, block_idx: u64, start_sector_in_block: u64,
458        sector_count: u64, buf: &mut [u8],
459    ) -> Result<()> {
460        let lss = self.logical_sector_size as usize;
461
462        // Load sector bitmap for this block
463        let stride = self.chunk_ratio + 1;
464        let chunk_idx = block_idx / self.chunk_ratio;
465        let sb_bat_idx = chunk_idx * stride + self.chunk_ratio;
466
467        let bat_buf = self.io_mut().file.bat_buf()?;
468        let bat = Bat::new(&bat_buf, self.chunk_ratio);
469        let sb_entry = bat.entry(sb_bat_idx)?;
470
471        let sb_state = sb_entry
472            .sector_bitmap_state()
473            .ok_or(Error::InvalidSectorBitmapState(sb_entry.raw_state()))?;
474        if sb_state != SectorBitmapState::Present {
475            return Err(Error::StateMismatch {
476                state: sb_entry.raw_state(),
477                description: "sector bitmap not Present for PartiallyPresent payload".into(),
478            });
479        }
480
481        let sb_file_offset = sb_entry.file_offset_mb() * u64::from(MIB);
482        let bitmap_size = MIB as usize;
483        let mut bitmap = vec![0u8; bitmap_size];
484        read_exact_at(self.io_mut().file.inner_mut(), sb_file_offset, &mut bitmap)?;
485
486        let spb = self.sectors_per_block();
487        let block_in_chunk = block_idx % self.chunk_ratio;
488
489        for i in 0..sector_count {
490            let sib = start_sector_in_block + i;
491            let sector_in_chunk = block_in_chunk * spb + sib;
492            let byte_idx =
493                usize::try_from(sector_in_chunk / 8).expect("bitmap byte index fits usize");
494
495            if byte_idx >= bitmap.len() {
496                return Err(Error::InvalidMetadata(format!(
497                    "sector bitmap index out of range: byte {byte_idx}"
498                )));
499            }
500
501            let in_child = bitmap.view_bits::<Lsb0>()
502                [usize::try_from(sector_in_chunk).expect("bitmap bit index fits usize")];
503            let offset = usize::try_from(i).expect("sector offset fits usize") * lss;
504
505            if in_child {
506                self.read_block_range_from_file(
507                    entry.file_offset_mb(),
508                    sib,
509                    1,
510                    &mut buf[offset..offset + lss],
511                )?;
512            } else {
513                self.read_from_parent_sector(
514                    block_idx * spb + start_sector_in_block + i,
515                    &mut buf[offset..offset + lss],
516                )?;
517            }
518        }
519
520        Ok(())
521    }
522
523    /// Read a single sector from the parent disk at the given global sector number.
524    ///
525    /// Resolves and caches the parent medium on first access.
526    ///
527    /// # Panics
528    ///
529    /// Panics if arithmetic overflow occurs during sector/offset conversion.
530    /// This should not happen with well-formed VHDX files.
531    fn read_from_parent_sector(&mut self, global_sector: u64, buf: &mut [u8]) -> Result<()> {
532        self.ensure_parent_resolved()?;
533        let mut parent_ref = self.io().parent_medium.borrow_mut();
534        let parent = parent_ref.as_mut().ok_or(Error::ParentResolverRequired)?;
535        let parent_lss = parent.logical_sector_size()?;
536        if parent_lss != self.logical_sector_size {
537            return Err(Error::ParentSectorSizeMismatch {
538                child: self.logical_sector_size,
539                parent: parent_lss,
540            });
541        }
542        parent.read_sector(global_sector, buf)
543    }
544
545    fn ensure_parent_resolved(&mut self) -> Result<()> {
546        if self.io().parent_medium.borrow().is_some() {
547            return Ok(());
548        }
549
550        let meta_buf = self.io_mut().file.metadata_buf()?.to_vec();
551        let meta = Metadata::new(&meta_buf)?;
552        let items = meta.items();
553        let locator = items.parent_locator().map_err(|_| Error::ParentNotFound)?;
554        let expected_data_write_guid = locator
555            .entries()
556            .find_map(|entry| {
557                let kv_data = locator.key_value_data();
558                let key = entry.key(kv_data).ok()?;
559                if key == "parent_linkage" {
560                    let value = entry.value(kv_data).ok()?;
561                    crate::types::Guid::parse_braced(&value).ok()
562                } else {
563                    None
564                }
565            })
566            .ok_or_else(|| {
567                Error::InvalidParentLocator("parent_linkage missing or invalid".into())
568            })?;
569        let child_virtual_disk_size = items.virtual_disk_size()?;
570        let request = ParentRequest {
571            locator,
572            expected_data_write_guid,
573            child_logical_sector_size: self.logical_sector_size,
574            child_virtual_disk_size,
575        };
576        let mut resolver_ref = self
577            .io()
578            .file
579            .parent_resolver
580            .lock()
581            .map_err(|_| Error::InvalidFile("parent resolver lock poisoned".into()))?;
582        let resolver = resolver_ref.as_mut().ok_or(Error::ParentResolverRequired)?;
583        let mut parent = resolver.resolve_parent(request)?;
584        if parent.data_write_guid()? != expected_data_write_guid {
585            return Err(Error::ParentLocatorGuidMismatch {
586                expected: expected_data_write_guid,
587                actual: parent.data_write_guid()?,
588            });
589        }
590        let parent_lss = parent.logical_sector_size()?;
591        if parent_lss != self.logical_sector_size {
592            return Err(Error::ParentSectorSizeMismatch {
593                child: self.logical_sector_size,
594                parent: parent_lss,
595            });
596        }
597        *self.io().parent_medium.borrow_mut() = Some(parent);
598        Ok(())
599    }
600}
601
602impl<T> ParentMedium for Medium<T>
603where
604    T: Read + Seek,
605{
606    fn data_write_guid(&mut self) -> Result<crate::types::Guid> {
607        let header_buf = self.header_buf_arc()?;
608        let header = crate::header::Header::new(&header_buf)?;
609        Ok(header.header(0)?.data_write_guid())
610    }
611
612    fn logical_sector_size(&mut self) -> Result<u32> {
613        let meta_buf = self.metadata_buf()?;
614        let meta = Metadata::new(&meta_buf)?;
615        meta.items().logical_sector_size()
616    }
617
618    fn read_sector(&mut self, sector: u64, buf: &mut [u8]) -> Result<()> {
619        let logical_sector_size = self.logical_sector_size()?;
620        if buf.len() != logical_sector_size as usize {
621            return Err(Error::InvalidParameter(format!(
622                "parent sector buffer length must equal logical sector size: got {}, expected {logical_sector_size}",
623                buf.len()
624            )));
625        }
626        let mut io = self.io()?;
627        io.sector(sector, 1)?.read_exact(buf)?;
628        Ok(())
629    }
630}
631
632impl<F, T> ParentResolver for F
633where
634    F: Fn(ParentRequest<'_>) -> Result<Medium<T>> + 'static,
635    T: Read + Seek + 'static,
636{
637    fn resolve_parent(&mut self, request: ParentRequest<'_>) -> Result<Box<dyn ParentMedium>> {
638        Ok(Box::new(std::cell::RefCell::new(self(request)?)))
639    }
640}
641
642impl<T> std::fmt::Debug for Sector<'_, '_, T> {
643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644        f.debug_struct("Sector")
645            .field("start", &self.start)
646            .field("count", &self.count)
647            .field("logical_sector_size", &self.logical_sector_size)
648            .field("block_size", &self.block_size)
649            .field("chunk_ratio", &self.chunk_ratio)
650            .field("pos", &self.pos)
651            .field("range_bytes", &self.range_bytes)
652            .field("semantics", &self.semantics)
653            .finish_non_exhaustive()
654    }
655}
656
657// PartialEq cannot be derived because Medium does not implement PartialEq.
658impl<T> PartialEq for Sector<'_, '_, T> {
659    fn eq(&self, other: &Self) -> bool {
660        std::ptr::eq(self.io, other.io) && self.start == other.start && self.count == other.count
661    }
662}
663
664// ---------------------------------------------------------------------------
665// std::io trait implementations — cursor-based I/O
666// ---------------------------------------------------------------------------
667
668impl<T> io::Read for Sector<'_, '_, T>
669where
670    T: Read + Seek,
671{
672    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
673        if self.pos >= self.range_bytes {
674            return Ok(0); // EOF
675        }
676        let available = usize::try_from(self.range_bytes - self.pos).unwrap_or(usize::MAX);
677        let to_read = buf.len().min(available);
678        self.read_at(&mut buf[..to_read], self.pos)?;
679        self.pos += to_read as u64;
680        Ok(to_read)
681    }
682}
683
684impl<T> io::Write for Sector<'_, '_, T>
685where
686    T: Read + Write + Seek + Len + SetLen + SyncData,
687{
688    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
689        if self.pos >= self.range_bytes {
690            return Ok(0); // EOF
691        }
692        let available = usize::try_from(self.range_bytes - self.pos).unwrap_or(usize::MAX);
693        let to_write = buf.len().min(available);
694        self.write_at(&buf[..to_write], self.pos)?;
695        self.pos += to_write as u64;
696        Ok(to_write)
697    }
698    fn flush(&mut self) -> io::Result<()> {
699        Ok(()) // No buffering — writes go directly to file
700    }
701}
702
703impl<T> io::Seek for Sector<'_, '_, T> {
704    fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
705        let new_pos = match from {
706            SeekFrom::Start(offset) => offset,
707            SeekFrom::End(offset) => {
708                // offset is i64; negative = before end, positive = past end
709                i64::try_from(self.range_bytes)
710                    .ok()
711                    .and_then(|v| v.checked_add(offset))
712                    .and_then(|v| u64::try_from(v.max(0)).ok())
713                    .unwrap_or(0)
714            }
715            SeekFrom::Current(offset) => i64::try_from(self.pos)
716                .ok()
717                .and_then(|v| v.checked_add(offset))
718                .and_then(|v| u64::try_from(v.max(0)).ok())
719                .unwrap_or(0),
720        };
721        self.pos = new_pos.min(self.range_bytes);
722        Ok(self.pos)
723    }
724}