Block device abstraction for OCaml 5 with Eio direct-style I/O and Bytesrw integration
at main 200 lines 7.0 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** Block device abstraction for Eio. 7 8 This library provides a minimal block device interface inspired by MirageOS 9 but using Eio direct-style I/O instead of Lwt. 10 11 {1 Overview} 12 13 Block devices provide fixed-size sector access to storage. Unlike streaming 14 I/O, block devices support random access reads and writes at sector 15 granularity. 16 17 {[ 18 Eio_main.run @@ fun env -> 19 Eio.Switch.run @@ fun sw -> 20 let blk = Block.of_file ~sw env#fs "disk.img" ~sector_size:512 in 21 let info = Block.info blk in 22 Printf.printf "Sectors: %Ld, Size: %d\n" info.sectors info.sector_size; 23 let data = Block.read blk 0L in 24 (* ... *) 25 ]} 26 27 {1 Types} *) 28 29type info = { 30 read_write : bool; (** [true] if writes are permitted *) 31 sector_size : int; (** Bytes per sector (typically 512 or 4096) *) 32 sectors : int64; (** Total number of sectors *) 33} 34(** Device information. *) 35 36type t 37(** A block device handle. *) 38 39val pp : t Fmt.t 40(** [pp] pretty-prints the block device, showing sector size, sector count, and 41 read/write mode. *) 42 43type error = 44 [ `Disconnected (** Device has been disconnected *) 45 | `Read_error of string (** Read operation failed *) 46 | `Invalid_sector of int64 (** Sector number out of bounds *) ] 47(** Read errors. *) 48 49type write_error = 50 [ error 51 | `Write_error of string (** Write operation failed *) 52 | `Read_only (** Device is read-only *) ] 53(** Write errors. *) 54 55val pp_error : error Fmt.t 56(** Pretty-print a read error. *) 57 58val pp_write_error : write_error Fmt.t 59(** Pretty-print a write error. *) 60 61(** {1 Device Operations} *) 62 63val info : t -> info 64(** [info t] returns device information. *) 65 66val read : t -> int64 -> (string, error) result 67(** [read t sector] reads a single sector. Returns sector data or error. *) 68 69val read_many : t -> int64 -> int -> (string, error) result 70(** [read_many t start count] reads [count] consecutive sectors starting at 71 [start]. Returns concatenated data or error on first failure. *) 72 73val write : t -> int64 -> string -> (unit, write_error) result 74(** [write t sector data] writes [data] to [sector]. The length of [data] must 75 equal the sector size. *) 76 77val write_many : t -> int64 -> string list -> (unit, write_error) result 78(** [write_many t start sectors] writes multiple sectors starting at [start]. 79 Each string must have length equal to sector size. *) 80 81val sync : t -> unit 82(** [sync t] flushes all pending writes to the underlying storage. *) 83 84val close : t -> unit 85(** [close t] releases resources. Further operations will return 86 [`Disconnected]. *) 87 88(** {1 Bytesrw Integration} *) 89 90val to_reader : t -> offset:int64 -> length:int64 -> Bytesrw.Bytes.Reader.t 91(** [to_reader t ~offset ~length] creates a sequential reader starting at byte 92 [offset] for [length] bytes. Offset must be sector-aligned. *) 93 94val to_writer : t -> offset:int64 -> Bytesrw.Bytes.Writer.t 95(** [to_writer t ~offset] creates a sequential writer starting at byte [offset]. 96 Offset must be sector-aligned. Writes are buffered to sector boundaries. *) 97 98(** {1 Implementations} *) 99 100val of_memory : sector_size:int -> sectors:int64 -> t 101(** [of_memory ~sector_size ~sectors] creates an in-memory block device. Useful 102 for testing. *) 103 104val of_bigarray : 105 sector_size:int -> 106 (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t -> 107 t 108(** [of_bigarray ~sector_size ba] wraps an existing bigarray as a block device. 109 The bigarray size must be a multiple of [sector_size]. *) 110 111val of_string : sector_size:int -> string -> t 112(** [of_string ~sector_size data] creates a read-only block device from a 113 string. Useful for testing with known data. *) 114 115val of_file : 116 sw:Eio.Switch.t -> 117 _ Eio.Path.t -> 118 sector_size:int -> 119 ?sectors:int64 -> 120 ?create:int64 -> 121 unit -> 122 t 123(** [of_file ~sw path ~sector_size ?sectors ?create ()] opens a file as a block 124 device. 125 126 @param sectors 127 Override the sector count instead of deriving it from file size. Required 128 for Linux block devices which report [st_size = 0]. 129 @param create 130 If provided, creates the file with this many sectors if it doesn't exist. 131 @param sector_size Sector size in bytes (must be power of 2, >= 512). *) 132 133(** {1 Combinators} *) 134 135val read_only : t -> t 136(** [read_only t] returns a read-only view of [t]. *) 137 138val sub : t -> start:int64 -> sectors:int64 -> t 139(** [sub t ~start ~sectors] returns a view of a subset of sectors. Useful for 140 partitioning. *) 141 142val with_crc32c : t -> t 143(** [with_crc32c t] wraps [t] with CRC32C checksums. Each sector reserves 4 144 bytes for the checksum, reducing usable space per sector by 4 bytes. Reads 145 verify checksums and return [`Read_error] on mismatch. *) 146 147(** {1 Generic Operations} 148 149 Operations inspired by MirageOS mirage-block. *) 150 151val fold : 152 f:(int64 -> string -> 'a -> ('a, error) result) -> 153 t -> 154 'a -> 155 ('a, error) result 156(** [fold ~f t init] folds [f] over every sector in the device. 157 [f sector data acc] is called for each sector in order. Stops on first 158 error. *) 159 160val iter : 161 f:(int64 -> string -> (unit, error) result) -> t -> (unit, error) result 162(** [iter ~f t] iterates [f] over every sector. *) 163 164type compare_error = 165 [ error 166 | `Different_sizes (** Devices have different sector counts *) 167 | `Different_sector_sizes (** Devices have different sector sizes *) 168 | `Contents_differ of int64 (** Contents differ at this sector *) ] 169(** Comparison errors. *) 170 171val pp_compare_error : compare_error Fmt.t 172 173val compare : t -> t -> (unit, compare_error) result 174(** [compare a b] compares two block devices sector by sector. Returns [Ok ()] 175 if they are identical, or an error describing the first difference. *) 176 177type copy_error = 178 [ write_error 179 | `Different_sizes (** Source larger than destination *) 180 | `Different_sector_sizes (** Devices have different sector sizes *) ] 181(** Copy errors. *) 182 183val pp_copy_error : copy_error Fmt.t 184 185val copy : src:t -> dst:t -> (unit, copy_error) result 186(** [copy ~src ~dst] copies all sectors from [src] to [dst]. The destination 187 must be at least as large as the source. *) 188 189val sparse_copy : src:t -> dst:t -> (unit, copy_error) result 190(** [sparse_copy ~src ~dst] copies non-zero sectors from [src] to [dst]. Sectors 191 that are all zeros are skipped, preserving sparseness. *) 192 193val fill : t -> char -> (unit, write_error) result 194(** [fill t c] fills every sector with byte [c]. Useful for wiping a device. *) 195 196val zero : t -> (unit, write_error) result 197(** [zero t] fills every sector with zeros. Equivalent to [fill t '\x00']. *) 198 199val is_zero : string -> bool 200(** [is_zero data] returns [true] if [data] contains only zero bytes. *)