···11+ISC License
22+33+Copyright (c) 2025 Thomas Gazagnaire
44+55+Permission to use, copy, modify, and/or distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1010+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1111+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1212+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1313+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1414+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1515+PERFORMANCE OF THIS SOFTWARE.
+90
README.md
···11+# block
22+33+Block device abstraction for OCaml 5 with Eio direct-style I/O and Bytesrw integration.
44+55+## Overview
66+77+This library provides a minimal block device interface inspired by MirageOS
88+[mirage-block](https://github.com/mirage/mirage-block) but using Eio's
99+effects-based concurrency instead of Lwt.
1010+1111+## Features
1212+1313+- Eio direct-style I/O (no monads)
1414+- Bytesrw integration for streaming access
1515+- Memory, file, and flow backends
1616+- CRC32C integrity checking wrapper
1717+- Sub-device views for partitioning
1818+- Read-only wrapper
1919+2020+## Installation
2121+2222+```
2323+opam install block
2424+```
2525+2626+## Usage
2727+2828+```ocaml
2929+Eio_main.run @@ fun env ->
3030+Eio.Switch.run @@ fun sw ->
3131+3232+(* Create a file-backed block device *)
3333+let blk = Block.of_file ~sw env#fs "disk.img" ~sector_size:512 ~create:1000L () in
3434+let info = Block.info blk in
3535+Printf.printf "Sectors: %Ld\n" info.sectors;
3636+3737+(* Write a sector *)
3838+let data = String.make 512 'A' in
3939+Block.write blk 0L data |> Result.get_ok;
4040+4141+(* Read it back *)
4242+let read = Block.read blk 0L |> Result.get_ok in
4343+assert (data = read);
4444+4545+(* Add CRC32C integrity checking *)
4646+let safe_blk = Block.with_crc32c blk in
4747+(* Now reads verify checksums, writes compute them *)
4848+```
4949+5050+## API
5151+5252+### Core Operations
5353+5454+- `Block.info` - Get device info (sector size, count, read/write)
5555+- `Block.read` - Read a single sector
5656+- `Block.write` - Write a single sector
5757+- `Block.read_many` - Read multiple consecutive sectors
5858+- `Block.write_many` - Write multiple consecutive sectors
5959+- `Block.sync` - Flush pending writes
6060+- `Block.close` - Release resources
6161+6262+### Backends
6363+6464+- `Block.of_memory` - In-memory device (for testing)
6565+- `Block.of_string` - Read-only device from string
6666+- `Block.of_file` - File-backed device
6767+- `Block.of_flow` - Eio flow-backed device
6868+- `Block.of_bigarray` - Bigarray-backed device
6969+7070+### Wrappers
7171+7272+- `Block.read_only` - Read-only view
7373+- `Block.sub` - Sub-device view (for partitions)
7474+- `Block.with_crc32c` - CRC32C integrity checking
7575+7676+### Bytesrw Integration
7777+7878+- `Block.to_reader` - Create sequential reader from offset
7979+- `Block.to_writer` - Create sequential writer from offset
8080+8181+## Related Work
8282+8383+- [mirage-block](https://github.com/mirage/mirage-block) - MirageOS block
8484+ interface (Lwt-based)
8585+- [mirage-block-unix](https://github.com/mirage/mirage-block-unix) - Unix
8686+ backend for mirage-block
8787+8888+## License
8989+9090+ISC License. See [LICENSE.md](LICENSE.md) for details.
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Crowbar
77+88+let sector_size = 512
99+1010+(* Test that write followed by read returns same data *)
1111+let test_roundtrip data sector =
1212+ let sectors = 100L in
1313+ let blk = Block.of_memory ~sector_size ~sectors in
1414+ let sector = Int64.of_int (sector mod 100) in
1515+ (* Pad or truncate data to sector size *)
1616+ let padded =
1717+ if String.length data >= sector_size then String.sub data 0 sector_size
1818+ else data ^ String.make (sector_size - String.length data) '\x00'
1919+ in
2020+ match Block.write blk sector padded with
2121+ | Error _ -> () (* Acceptable - may be invalid sector *)
2222+ | Ok () -> (
2323+ match Block.read blk sector with
2424+ | Error e -> fail (Fmt.str "read failed: %a" Block.pp_error e)
2525+ | Ok read_data -> check_eq ~pp:Fmt.string padded read_data)
2626+2727+(* Test that sub-device reads map correctly *)
2828+let test_sub_mapping data start_sector sub_start sub_len read_sector =
2929+ let sectors = 100L in
3030+ let blk = Block.of_memory ~sector_size ~sectors in
3131+ let start_sector = Int64.of_int (start_sector mod 50) in
3232+ let sub_start = Int64.of_int (sub_start mod 25) in
3333+ let sub_len = Int64.of_int (max 1 (sub_len mod 20)) in
3434+ let read_sector = Int64.of_int (read_sector mod (Int64.to_int sub_len)) in
3535+ (* Write to original device *)
3636+ let padded =
3737+ if String.length data >= sector_size then String.sub data 0 sector_size
3838+ else data ^ String.make (sector_size - String.length data) '\x00'
3939+ in
4040+ let target = Int64.add sub_start read_sector in
4141+ if target >= 0L && target < sectors then (
4242+ match Block.write blk target padded with
4343+ | Error _ -> ()
4444+ | Ok () ->
4545+ let sub = Block.sub blk ~start:sub_start ~sectors:sub_len in
4646+ if read_sector >= 0L && read_sector < sub_len then
4747+ match Block.read sub read_sector with
4848+ | Error _ -> ()
4949+ | Ok read_data -> check_eq ~pp:Fmt.string padded read_data)
5050+5151+(* Test CRC32C detects any corruption *)
5252+let test_crc_detects_corruption data corrupt_pos corrupt_byte =
5353+ let sectors = 10L in
5454+ let blk = Block.of_memory ~sector_size ~sectors in
5555+ let crc_blk = Block.with_crc32c blk in
5656+ let data_size = sector_size - 4 in
5757+ let padded =
5858+ if String.length data >= data_size then String.sub data 0 data_size
5959+ else data ^ String.make (data_size - String.length data) '\x00'
6060+ in
6161+ match Block.write crc_blk 0L padded with
6262+ | Error _ -> ()
6363+ | Ok () -> (
6464+ (* Read raw sector and corrupt it *)
6565+ match Block.read blk 0L with
6666+ | Error _ -> ()
6767+ | Ok raw ->
6868+ let corrupt_pos = corrupt_pos mod sector_size in
6969+ let corrupted = Bytes.of_string raw in
7070+ let orig_byte = Bytes.get corrupted corrupt_pos in
7171+ if Char.chr corrupt_byte <> orig_byte then (
7272+ Bytes.set corrupted corrupt_pos (Char.chr corrupt_byte);
7373+ match Block.write blk 0L (Bytes.to_string corrupted) with
7474+ | Error _ -> ()
7575+ | Ok () -> (
7676+ match Block.read crc_blk 0L with
7777+ | Ok _ -> fail "CRC should have detected corruption"
7878+ | Error (`Read_error _) -> () (* Expected *)
7979+ | Error e -> fail (Fmt.str "unexpected error: %a" Block.pp_error e))))
8080+8181+(* Test read_many returns concatenated sectors *)
8282+let test_read_many_concat count =
8383+ let sectors = 100L in
8484+ let blk = Block.of_memory ~sector_size ~sectors in
8585+ let count = max 1 (count mod 10) in
8686+ (* Write distinct patterns *)
8787+ for i = 0 to count - 1 do
8888+ let data = String.make sector_size (Char.chr ((i mod 26) + 65)) in
8989+ match Block.write blk (Int64.of_int i) data with
9090+ | Ok () -> ()
9191+ | Error _ -> ()
9292+ done;
9393+ match Block.read_many blk 0L count with
9494+ | Error _ -> ()
9595+ | Ok data ->
9696+ check_eq ~pp:Fmt.int (count * sector_size) (String.length data);
9797+ (* Verify each sector's pattern *)
9898+ for i = 0 to count - 1 do
9999+ let expected = Char.chr ((i mod 26) + 65) in
100100+ let actual = data.[i * sector_size] in
101101+ check_eq ~pp:(fun ppf c -> Fmt.pf ppf "%C" c) expected actual
102102+ done
103103+104104+let () =
105105+ add_test ~name:"block: roundtrip" [ bytes; range 200 ] test_roundtrip;
106106+ add_test ~name:"block: sub mapping"
107107+ [ bytes; range 100; range 50; range 30; range 30 ]
108108+ test_sub_mapping;
109109+ add_test ~name:"block: crc corruption"
110110+ [ bytes; range 512; range 256 ]
111111+ test_crc_detects_corruption;
112112+ add_test ~name:"block: read_many" [ range 20 ] test_read_many_concat
+549
lib/block.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type info = { read_write : bool; sector_size : int; sectors : int64 }
77+88+type error =
99+ [ `Disconnected | `Read_error of string | `Invalid_sector of int64 ]
1010+1111+type write_error = [ error | `Write_error of string | `Read_only ]
1212+1313+let pp_error ppf = function
1414+ | `Disconnected -> Fmt.string ppf "disconnected"
1515+ | `Read_error s -> Fmt.pf ppf "read error: %s" s
1616+ | `Invalid_sector n -> Fmt.pf ppf "invalid sector: %Ld" n
1717+1818+let pp_write_error ppf = function
1919+ | #error as e -> pp_error ppf e
2020+ | `Write_error s -> Fmt.pf ppf "write error: %s" s
2121+ | `Read_only -> Fmt.string ppf "read-only device"
2222+2323+module type IMPL = sig
2424+ type state
2525+2626+ val info : state -> info
2727+ val read : state -> int64 -> (string, error) result
2828+ val write : state -> int64 -> string -> (unit, write_error) result
2929+ val sync : state -> unit
3030+ val close : state -> unit
3131+end
3232+3333+type t = T : { state : 'a; impl : (module IMPL with type state = 'a) } -> t
3434+3535+let info (T { state; impl = (module I) }) = I.info state
3636+let read (T { state; impl = (module I) }) sector = I.read state sector
3737+let write (T { state; impl = (module I) }) sector data = I.write state sector data
3838+let sync (T { state; impl = (module I) }) = I.sync state
3939+let close (T { state; impl = (module I) }) = I.close state
4040+4141+let read_many t start count =
4242+ let info = info t in
4343+ let buf = Buffer.create (count * info.sector_size) in
4444+ let rec loop i =
4545+ if i >= count then Ok (Buffer.contents buf)
4646+ else
4747+ match read t Int64.(add start (of_int i)) with
4848+ | Error e -> Error e
4949+ | Ok data ->
5050+ Buffer.add_string buf data;
5151+ loop (i + 1)
5252+ in
5353+ loop 0
5454+5555+let write_many t start sectors =
5656+ let rec loop i = function
5757+ | [] -> Ok ()
5858+ | data :: rest -> (
5959+ match write t Int64.(add start (of_int i)) data with
6060+ | Error e -> Error e
6161+ | Ok () -> loop (i + 1) rest)
6262+ in
6363+ loop 0 sectors
6464+6565+(* Memory implementation *)
6666+module Memory = struct
6767+ type state = {
6868+ mutable data : bytes;
6969+ sector_size : int;
7070+ sectors : int64;
7171+ mutable closed : bool;
7272+ }
7373+7474+ let info t =
7575+ { read_write = true; sector_size = t.sector_size; sectors = t.sectors }
7676+7777+ let read t sector =
7878+ if t.closed then Error `Disconnected
7979+ else if sector < 0L || sector >= t.sectors then Error (`Invalid_sector sector)
8080+ else
8181+ let off = Int64.to_int sector * t.sector_size in
8282+ Ok (Bytes.sub_string t.data off t.sector_size)
8383+8484+ let write t sector data =
8585+ if t.closed then Error `Disconnected
8686+ else if sector < 0L || sector >= t.sectors then
8787+ Error (`Invalid_sector sector)
8888+ else if String.length data <> t.sector_size then
8989+ Error (`Write_error "data length must equal sector size")
9090+ else
9191+ let off = Int64.to_int sector * t.sector_size in
9292+ Bytes.blit_string data 0 t.data off t.sector_size;
9393+ Ok ()
9494+9595+ let sync _ = ()
9696+ let close t = t.closed <- true
9797+end
9898+9999+let of_memory ~sector_size ~sectors =
100100+ let size = Int64.to_int sectors * sector_size in
101101+ let state =
102102+ { Memory.data = Bytes.make size '\x00'; sector_size; sectors; closed = false }
103103+ in
104104+ T { state; impl = (module Memory) }
105105+106106+let of_bigarray ~sector_size ba =
107107+ let len = Bigarray.Array1.dim ba in
108108+ if len mod sector_size <> 0 then
109109+ invalid_arg "bigarray size must be multiple of sector_size";
110110+ let sectors = Int64.of_int (len / sector_size) in
111111+ let data = Bytes.create len in
112112+ for i = 0 to len - 1 do
113113+ Bytes.set data i (Bigarray.Array1.get ba i)
114114+ done;
115115+ let state =
116116+ { Memory.data; sector_size; sectors; closed = false }
117117+ in
118118+ T { state; impl = (module Memory) }
119119+120120+let of_string ~sector_size data =
121121+ let len = String.length data in
122122+ if len mod sector_size <> 0 then
123123+ invalid_arg "string length must be multiple of sector_size";
124124+ let sectors = Int64.of_int (len / sector_size) in
125125+ let state =
126126+ {
127127+ Memory.data = Bytes.of_string data;
128128+ sector_size;
129129+ sectors;
130130+ closed = false;
131131+ }
132132+ in
133133+ (* Return read-only by wrapping *)
134134+ let t = T { state; impl = (module Memory) } in
135135+ read_only t
136136+137137+(* File implementation *)
138138+module File = struct
139139+ type state = {
140140+ file : Eio.File.rw_ty Eio.Resource.t;
141141+ sector_size : int;
142142+ sectors : int64;
143143+ mutable closed : bool;
144144+ }
145145+146146+ let info t =
147147+ { read_write = true; sector_size = t.sector_size; sectors = t.sectors }
148148+149149+ let read t sector =
150150+ if t.closed then Error `Disconnected
151151+ else if sector < 0L || sector >= t.sectors then Error (`Invalid_sector sector)
152152+ else
153153+ try
154154+ let off = Optint.Int63.of_int64 (Int64.mul sector (Int64.of_int t.sector_size)) in
155155+ let buf = Cstruct.create t.sector_size in
156156+ Eio.File.pread_exact t.file ~file_offset:off [ buf ];
157157+ Ok (Cstruct.to_string buf)
158158+ with exn -> Error (`Read_error (Printexc.to_string exn))
159159+160160+ let write t sector data =
161161+ if t.closed then Error `Disconnected
162162+ else if sector < 0L || sector >= t.sectors then
163163+ Error (`Invalid_sector sector)
164164+ else if String.length data <> t.sector_size then
165165+ Error (`Write_error "data length must equal sector size")
166166+ else
167167+ try
168168+ let off = Optint.Int63.of_int64 (Int64.mul sector (Int64.of_int t.sector_size)) in
169169+ let buf = Cstruct.of_string data in
170170+ Eio.File.pwrite_all t.file ~file_offset:off [ buf ];
171171+ Ok ()
172172+ with exn -> Error (`Write_error (Printexc.to_string exn))
173173+174174+ let sync t =
175175+ if not t.closed then Eio.File.sync t.file
176176+177177+ let close t =
178178+ if not t.closed then (
179179+ t.closed <- true;
180180+ Eio.Resource.close t.file)
181181+end
182182+183183+let of_file ~sw path ~sector_size ?create () =
184184+ if sector_size < 512 || sector_size land (sector_size - 1) <> 0 then
185185+ invalid_arg "sector_size must be power of 2 and >= 512";
186186+ let file =
187187+ match create with
188188+ | Some sectors ->
189189+ let size = Int64.mul sectors (Int64.of_int sector_size) in
190190+ let f =
191191+ Eio.Path.open_out ~sw path ~create:(`Or_truncate 0o644)
192192+ in
193193+ (* Extend file to size *)
194194+ Eio.File.truncate f (Optint.Int63.of_int64 size);
195195+ (f :> Eio.File.rw_ty Eio.Resource.t)
196196+ | None ->
197197+ Eio.Path.open_out ~sw path ~create:`Never
198198+ |> fun f -> (f :> Eio.File.rw_ty Eio.Resource.t)
199199+ in
200200+ let stat = Eio.File.stat file in
201201+ let size = Optint.Int63.to_int64 stat.size in
202202+ let sectors = Int64.div size (Int64.of_int sector_size) in
203203+ let state = { File.file; sector_size; sectors; closed = false } in
204204+ T { state; impl = (module File) }
205205+206206+(* Flow implementation *)
207207+module Flow = struct
208208+ type state = {
209209+ flow : Eio.Flow.two_way_ty Eio.Resource.t;
210210+ info : info;
211211+ mutable closed : bool;
212212+ }
213213+214214+ let info t = t.info
215215+216216+ let read t sector =
217217+ if t.closed then Error `Disconnected
218218+ else if sector < 0L || sector >= t.info.sectors then
219219+ Error (`Invalid_sector sector)
220220+ else
221221+ try
222222+ let off = Optint.Int63.of_int64 (Int64.mul sector (Int64.of_int t.info.sector_size)) in
223223+ let buf = Cstruct.create t.info.sector_size in
224224+ let file = Eio.Resource.get t.flow Eio.File.Pi.Rw in
225225+ Eio.File.Rw.pread_exact file ~file_offset:off [ buf ];
226226+ Ok (Cstruct.to_string buf)
227227+ with exn -> Error (`Read_error (Printexc.to_string exn))
228228+229229+ let write t sector data =
230230+ if t.closed then Error `Disconnected
231231+ else if not t.info.read_write then Error `Read_only
232232+ else if sector < 0L || sector >= t.info.sectors then
233233+ Error (`Invalid_sector sector)
234234+ else if String.length data <> t.info.sector_size then
235235+ Error (`Write_error "data length must equal sector size")
236236+ else
237237+ try
238238+ let off = Optint.Int63.of_int64 (Int64.mul sector (Int64.of_int t.info.sector_size)) in
239239+ let buf = Cstruct.of_string data in
240240+ let file = Eio.Resource.get t.flow Eio.File.Pi.Rw in
241241+ Eio.File.Rw.pwrite_all file ~file_offset:off [ buf ];
242242+ Ok ()
243243+ with exn -> Error (`Write_error (Printexc.to_string exn))
244244+245245+ let sync t =
246246+ if not t.closed then
247247+ try
248248+ let file = Eio.Resource.get t.flow Eio.File.Pi.Rw in
249249+ Eio.File.Rw.sync file
250250+ with _ -> ()
251251+252252+ let close t = t.closed <- true
253253+end
254254+255255+let of_flow ~sw:_ ~info flow =
256256+ let state =
257257+ { Flow.flow = (flow :> Eio.Flow.two_way_ty Eio.Resource.t); info; closed = false }
258258+ in
259259+ T { state; impl = (module Flow) }
260260+261261+(* Read-only wrapper *)
262262+module ReadOnly = struct
263263+ type state = { inner : t }
264264+265265+ let info t =
266266+ let i = info t.inner in
267267+ { i with read_write = false }
268268+269269+ let read t sector = read t.inner sector
270270+ let write _ _ _ = Error `Read_only
271271+ let sync t = sync t.inner
272272+ let close t = close t.inner
273273+end
274274+275275+let read_only t =
276276+ let state = { ReadOnly.inner = t } in
277277+ T { state; impl = (module ReadOnly) }
278278+279279+(* Sub-device wrapper *)
280280+module Sub = struct
281281+ type state = { inner : t; start : int64; sectors : int64 }
282282+283283+ let info t =
284284+ let i = info t.inner in
285285+ { i with sectors = t.sectors }
286286+287287+ let read t sector =
288288+ if sector < 0L || sector >= t.sectors then Error (`Invalid_sector sector)
289289+ else read t.inner Int64.(add t.start sector)
290290+291291+ let write t sector data =
292292+ if sector < 0L || sector >= t.sectors then Error (`Invalid_sector sector)
293293+ else write t.inner Int64.(add t.start sector) data
294294+295295+ let sync t = sync t.inner
296296+ let close _ = () (* Don't close underlying device *)
297297+end
298298+299299+let sub t ~start ~sectors =
300300+ let i = info t in
301301+ if start < 0L || sectors < 0L || Int64.add start sectors > i.sectors then
302302+ invalid_arg "sub: invalid range";
303303+ let state = { Sub.inner = t; start; sectors } in
304304+ T { state; impl = (module Sub) }
305305+306306+(* CRC32C wrapper *)
307307+module WithCrc = struct
308308+ type state = { inner : t; data_size : int }
309309+310310+ let crc32c data =
311311+ (* Simple CRC32C implementation - in production use Wal.crc32c *)
312312+ let crc = ref 0xFFFFFFFFl in
313313+ for i = 0 to String.length data - 1 do
314314+ let byte = Char.code data.[i] in
315315+ crc := Int32.logxor !crc (Int32.of_int byte);
316316+ for _ = 0 to 7 do
317317+ let mask = Int32.neg (Int32.logand !crc 1l) in
318318+ crc := Int32.logxor (Int32.shift_right_logical !crc 1) (Int32.logand 0x82F63B78l mask)
319319+ done
320320+ done;
321321+ Int32.logxor !crc 0xFFFFFFFFl
322322+323323+ let encode_crc crc =
324324+ let b = Bytes.create 4 in
325325+ Bytes.set b 0 (Char.chr (Int32.to_int (Int32.logand crc 0xFFl)));
326326+ Bytes.set b 1 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical crc 8) 0xFFl)));
327327+ Bytes.set b 2 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical crc 16) 0xFFl)));
328328+ Bytes.set b 3 (Char.chr (Int32.to_int (Int32.shift_right_logical crc 24)));
329329+ Bytes.to_string b
330330+331331+ let decode_crc s off =
332332+ let b0 = Int32.of_int (Char.code s.[off]) in
333333+ let b1 = Int32.of_int (Char.code s.[off + 1]) in
334334+ let b2 = Int32.of_int (Char.code s.[off + 2]) in
335335+ let b3 = Int32.of_int (Char.code s.[off + 3]) in
336336+ Int32.logor b0
337337+ (Int32.logor (Int32.shift_left b1 8)
338338+ (Int32.logor (Int32.shift_left b2 16) (Int32.shift_left b3 24)))
339339+340340+ let info t =
341341+ let i = info t.inner in
342342+ { i with sector_size = t.data_size }
343343+344344+ let read t sector =
345345+ match read t.inner sector with
346346+ | Error e -> Error e
347347+ | Ok raw ->
348348+ let data = String.sub raw 0 t.data_size in
349349+ let stored_crc = decode_crc raw t.data_size in
350350+ let computed_crc = crc32c data in
351351+ if stored_crc <> computed_crc then
352352+ Error (`Read_error (Printf.sprintf "CRC mismatch: stored=%lx computed=%lx" stored_crc computed_crc))
353353+ else Ok data
354354+355355+ let write t sector data =
356356+ if String.length data <> t.data_size then
357357+ Error (`Write_error (Printf.sprintf "data length must be %d" t.data_size))
358358+ else
359359+ let crc = crc32c data in
360360+ let raw = data ^ encode_crc crc in
361361+ write t.inner sector raw
362362+363363+ let sync t = sync t.inner
364364+ let close t = close t.inner
365365+end
366366+367367+let with_crc32c t =
368368+ let i = info t in
369369+ let data_size = i.sector_size - 4 in
370370+ if data_size < 1 then invalid_arg "sector_size too small for CRC";
371371+ let state = { WithCrc.inner = t; data_size } in
372372+ T { state; impl = (module WithCrc) }
373373+374374+(* Bytesrw integration *)
375375+let to_reader t ~offset ~length =
376376+ let i = info t in
377377+ if Int64.rem offset (Int64.of_int i.sector_size) <> 0L then
378378+ invalid_arg "offset must be sector-aligned";
379379+ let start_sector = Int64.div offset (Int64.of_int i.sector_size) in
380380+ let current_sector = ref start_sector in
381381+ let current_pos = ref 0 in
382382+ let remaining = ref length in
383383+ let current_data = ref "" in
384384+ Bytesrw.Bytes.Reader.create ~slice_length:i.sector_size @@ fun () ->
385385+ if !remaining <= 0L then Bytesrw.Bytes.Slice.eod
386386+ else if !current_pos >= String.length !current_data then (
387387+ (* Read next sector *)
388388+ match read t !current_sector with
389389+ | Error _ -> Bytesrw.Bytes.Slice.eod
390390+ | Ok data ->
391391+ current_data := data;
392392+ current_pos := 0;
393393+ incr_sector current_sector;
394394+ let len = min (String.length data) (Int64.to_int !remaining) in
395395+ remaining := Int64.sub !remaining (Int64.of_int len);
396396+ current_pos := len;
397397+ Bytesrw.Bytes.Slice.make (Bytes.unsafe_of_string data) ~first:0 ~length:len)
398398+ else Bytesrw.Bytes.Slice.eod
399399+400400+and incr_sector r = r := Int64.succ !r
401401+402402+let to_writer t ~offset =
403403+ let i = info t in
404404+ if Int64.rem offset (Int64.of_int i.sector_size) <> 0L then
405405+ invalid_arg "offset must be sector-aligned";
406406+ let start_sector = Int64.div offset (Int64.of_int i.sector_size) in
407407+ let current_sector = ref start_sector in
408408+ let buffer = Buffer.create i.sector_size in
409409+ let flush () =
410410+ if Buffer.length buffer > 0 then (
411411+ (* Pad to sector size if needed *)
412412+ while Buffer.length buffer < i.sector_size do
413413+ Buffer.add_char buffer '\x00'
414414+ done;
415415+ let _ = write t !current_sector (Buffer.contents buffer) in
416416+ Buffer.clear buffer;
417417+ current_sector := Int64.succ !current_sector)
418418+ in
419419+ Bytesrw.Bytes.Writer.create ~slice_length:i.sector_size @@ fun slice ->
420420+ let data = Bytes.sub_string (Bytesrw.Bytes.Slice.bytes slice)
421421+ (Bytesrw.Bytes.Slice.first slice)
422422+ (Bytesrw.Bytes.Slice.length slice)
423423+ in
424424+ Buffer.add_string buffer data;
425425+ while Buffer.length buffer >= i.sector_size do
426426+ let sector_data = Buffer.sub buffer 0 i.sector_size in
427427+ let _ = write t !current_sector sector_data in
428428+ current_sector := Int64.succ !current_sector;
429429+ let remaining = Buffer.sub buffer i.sector_size (Buffer.length buffer - i.sector_size) in
430430+ Buffer.clear buffer;
431431+ Buffer.add_string buffer remaining
432432+ done;
433433+ Bytesrw.Bytes.Slice.length slice
434434+435435+(* Generic operations *)
436436+437437+let fold ~f t init =
438438+ let i = info t in
439439+ let rec loop sector acc =
440440+ if sector >= i.sectors then Ok acc
441441+ else
442442+ match read t sector with
443443+ | Error e -> Error e
444444+ | Ok data -> (
445445+ match f sector data acc with
446446+ | Error e -> Error e
447447+ | Ok acc' -> loop (Int64.succ sector) acc')
448448+ in
449449+ loop 0L init
450450+451451+let iter ~f t =
452452+ fold ~f:(fun sector data () -> f sector data) t ()
453453+454454+type compare_error =
455455+ [ error
456456+ | `Different_sizes
457457+ | `Different_sector_sizes
458458+ | `Contents_differ of int64 ]
459459+460460+let pp_compare_error ppf = function
461461+ | #error as e -> pp_error ppf e
462462+ | `Different_sizes -> Fmt.string ppf "different sizes"
463463+ | `Different_sector_sizes -> Fmt.string ppf "different sector sizes"
464464+ | `Contents_differ n -> Fmt.pf ppf "contents differ at sector %Ld" n
465465+466466+let compare a b =
467467+ let ia = info a and ib = info b in
468468+ if ia.sector_size <> ib.sector_size then Error `Different_sector_sizes
469469+ else if ia.sectors <> ib.sectors then Error `Different_sizes
470470+ else
471471+ let rec loop sector =
472472+ if sector >= ia.sectors then Ok ()
473473+ else
474474+ match (read a sector, read b sector) with
475475+ | Error e, _ -> Error (e :> compare_error)
476476+ | _, Error e -> Error (e :> compare_error)
477477+ | Ok da, Ok db ->
478478+ if da <> db then Error (`Contents_differ sector)
479479+ else loop (Int64.succ sector)
480480+ in
481481+ loop 0L
482482+483483+type copy_error =
484484+ [ write_error
485485+ | `Different_sizes
486486+ | `Different_sector_sizes ]
487487+488488+let pp_copy_error ppf = function
489489+ | #write_error as e -> pp_write_error ppf e
490490+ | `Different_sizes -> Fmt.string ppf "different sizes"
491491+ | `Different_sector_sizes -> Fmt.string ppf "different sector sizes"
492492+493493+let copy ~src ~dst =
494494+ let is = info src and id = info dst in
495495+ if is.sector_size <> id.sector_size then Error `Different_sector_sizes
496496+ else if is.sectors > id.sectors then Error `Different_sizes
497497+ else
498498+ let rec loop sector =
499499+ if sector >= is.sectors then Ok ()
500500+ else
501501+ match read src sector with
502502+ | Error e -> Error (e :> copy_error)
503503+ | Ok data -> (
504504+ match write dst sector data with
505505+ | Error e -> Error (e :> copy_error)
506506+ | Ok () -> loop (Int64.succ sector))
507507+ in
508508+ loop 0L
509509+510510+let is_zero data =
511511+ let rec loop i =
512512+ if i >= String.length data then true
513513+ else if data.[i] <> '\x00' then false
514514+ else loop (i + 1)
515515+ in
516516+ loop 0
517517+518518+let sparse_copy ~src ~dst =
519519+ let is = info src and id = info dst in
520520+ if is.sector_size <> id.sector_size then Error `Different_sector_sizes
521521+ else if is.sectors > id.sectors then Error `Different_sizes
522522+ else
523523+ let rec loop sector =
524524+ if sector >= is.sectors then Ok ()
525525+ else
526526+ match read src sector with
527527+ | Error e -> Error (e :> copy_error)
528528+ | Ok data ->
529529+ if is_zero data then loop (Int64.succ sector)
530530+ else
531531+ match write dst sector data with
532532+ | Error e -> Error (e :> copy_error)
533533+ | Ok () -> loop (Int64.succ sector)
534534+ in
535535+ loop 0L
536536+537537+let fill t c =
538538+ let i = info t in
539539+ let data = String.make i.sector_size c in
540540+ let rec loop sector =
541541+ if sector >= i.sectors then Ok ()
542542+ else
543543+ match write t sector data with
544544+ | Error e -> Error e
545545+ | Ok () -> loop (Int64.succ sector)
546546+ in
547547+ loop 0L
548548+549549+let zero t = fill t '\x00'
+194
lib/block.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Block device abstraction for Eio.
77+88+ This library provides a minimal block device interface inspired by MirageOS
99+ but using Eio direct-style I/O instead of Lwt.
1010+1111+ {1 Overview}
1212+1313+ Block devices provide fixed-size sector access to storage. Unlike streaming
1414+ I/O, block devices support random access reads and writes at sector
1515+ granularity.
1616+1717+ {[
1818+ Eio_main.run @@ fun env ->
1919+ Eio.Switch.run @@ fun sw ->
2020+ let blk = Block.of_file ~sw env#fs "disk.img" ~sector_size:512 in
2121+ let info = Block.info blk in
2222+ Printf.printf "Sectors: %Ld, Size: %d\n" info.sectors info.sector_size;
2323+ let data = Block.read blk 0L in
2424+ (* ... *)
2525+ ]}
2626+2727+ {1 Types} *)
2828+2929+type info = {
3030+ read_write : bool; (** [true] if writes are permitted *)
3131+ sector_size : int; (** Bytes per sector (typically 512 or 4096) *)
3232+ sectors : int64; (** Total number of sectors *)
3333+}
3434+(** Device information. *)
3535+3636+type t
3737+(** A block device handle. *)
3838+3939+type error =
4040+ [ `Disconnected (** Device has been disconnected *)
4141+ | `Read_error of string (** Read operation failed *)
4242+ | `Invalid_sector of int64 (** Sector number out of bounds *) ]
4343+(** Read errors. *)
4444+4545+type write_error =
4646+ [ error
4747+ | `Write_error of string (** Write operation failed *)
4848+ | `Read_only (** Device is read-only *) ]
4949+(** Write errors. *)
5050+5151+val pp_error : error Fmt.t
5252+val pp_write_error : write_error Fmt.t
5353+5454+(** {1 Device Operations} *)
5555+5656+val info : t -> info
5757+(** [info t] returns device information. *)
5858+5959+val read : t -> int64 -> (string, error) result
6060+(** [read t sector] reads a single sector. Returns sector data or error. *)
6161+6262+val read_many : t -> int64 -> int -> (string, error) result
6363+(** [read_many t start count] reads [count] consecutive sectors starting at
6464+ [start]. Returns concatenated data or error on first failure. *)
6565+6666+val write : t -> int64 -> string -> (unit, write_error) result
6767+(** [write t sector data] writes [data] to [sector]. The length of [data] must
6868+ equal the sector size. *)
6969+7070+val write_many : t -> int64 -> string list -> (unit, write_error) result
7171+(** [write_many t start sectors] writes multiple sectors starting at [start].
7272+ Each string must have length equal to sector size. *)
7373+7474+val sync : t -> unit
7575+(** [sync t] flushes all pending writes to the underlying storage. *)
7676+7777+val close : t -> unit
7878+(** [close t] releases resources. Further operations will return
7979+ [`Disconnected]. *)
8080+8181+(** {1 Bytesrw Integration} *)
8282+8383+val to_reader : t -> offset:int64 -> length:int64 -> Bytesrw.Bytes.Reader.t
8484+(** [to_reader t ~offset ~length] creates a sequential reader starting at byte
8585+ [offset] for [length] bytes. Offset must be sector-aligned. *)
8686+8787+val to_writer : t -> offset:int64 -> Bytesrw.Bytes.Writer.t
8888+(** [to_writer t ~offset] creates a sequential writer starting at byte [offset].
8989+ Offset must be sector-aligned. Writes are buffered to sector boundaries. *)
9090+9191+(** {1 Implementations} *)
9292+9393+val of_memory : sector_size:int -> sectors:int64 -> t
9494+(** [of_memory ~sector_size ~sectors] creates an in-memory block device. Useful
9595+ for testing. *)
9696+9797+val of_bigarray :
9898+ sector_size:int ->
9999+ (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t ->
100100+ t
101101+(** [of_bigarray ~sector_size ba] wraps an existing bigarray as a block device.
102102+ The bigarray size must be a multiple of [sector_size]. *)
103103+104104+val of_string : sector_size:int -> string -> t
105105+(** [of_string ~sector_size data] creates a read-only block device from a
106106+ string. Useful for testing with known data. *)
107107+108108+val of_file :
109109+ sw:Eio.Switch.t ->
110110+ _ Eio.Path.t ->
111111+ sector_size:int ->
112112+ ?create:int64 ->
113113+ unit ->
114114+ t
115115+(** [of_file ~sw path ~sector_size ?create ()] opens a file as a block device.
116116+117117+ @param create
118118+ If provided, creates the file with this many sectors if it doesn't exist.
119119+ @param sector_size Sector size in bytes (must be power of 2, >= 512). *)
120120+121121+val of_flow :
122122+ sw:Eio.Switch.t ->
123123+ info:info ->
124124+ #Eio.Flow.two_way ->
125125+ t
126126+(** [of_flow ~sw ~info flow] wraps an Eio two-way flow as a block device. The
127127+ flow must support seeking. Used for raw device access. *)
128128+129129+(** {1 Combinators} *)
130130+131131+val read_only : t -> t
132132+(** [read_only t] returns a read-only view of [t]. *)
133133+134134+val sub : t -> start:int64 -> sectors:int64 -> t
135135+(** [sub t ~start ~sectors] returns a view of a subset of sectors. Useful for
136136+ partitioning. *)
137137+138138+val with_crc32c : t -> t
139139+(** [with_crc32c t] wraps [t] with CRC32C checksums. Each sector reserves 4
140140+ bytes for the checksum, reducing usable space per sector by 4 bytes. Reads
141141+ verify checksums and return [`Read_error] on mismatch. *)
142142+143143+(** {1 Generic Operations}
144144+145145+ Operations inspired by MirageOS mirage-block. *)
146146+147147+val fold :
148148+ f:(int64 -> string -> 'a -> ('a, error) result) ->
149149+ t ->
150150+ 'a ->
151151+ ('a, error) result
152152+(** [fold ~f t init] folds [f] over every sector in the device. [f sector data acc]
153153+ is called for each sector in order. Stops on first error. *)
154154+155155+val iter : f:(int64 -> string -> (unit, error) result) -> t -> (unit, error) result
156156+(** [iter ~f t] iterates [f] over every sector. *)
157157+158158+type compare_error =
159159+ [ error
160160+ | `Different_sizes (** Devices have different sector counts *)
161161+ | `Different_sector_sizes (** Devices have different sector sizes *)
162162+ | `Contents_differ of int64 (** Contents differ at this sector *) ]
163163+(** Comparison errors. *)
164164+165165+val pp_compare_error : compare_error Fmt.t
166166+167167+val compare : t -> t -> (unit, compare_error) result
168168+(** [compare a b] compares two block devices sector by sector. Returns [Ok ()]
169169+ if they are identical, or an error describing the first difference. *)
170170+171171+type copy_error =
172172+ [ write_error
173173+ | `Different_sizes (** Source larger than destination *)
174174+ | `Different_sector_sizes (** Devices have different sector sizes *) ]
175175+(** Copy errors. *)
176176+177177+val pp_copy_error : copy_error Fmt.t
178178+179179+val copy : src:t -> dst:t -> (unit, copy_error) result
180180+(** [copy ~src ~dst] copies all sectors from [src] to [dst]. The destination
181181+ must be at least as large as the source. *)
182182+183183+val sparse_copy : src:t -> dst:t -> (unit, copy_error) result
184184+(** [sparse_copy ~src ~dst] copies non-zero sectors from [src] to [dst]. Sectors
185185+ that are all zeros are skipped, preserving sparseness. *)
186186+187187+val fill : t -> char -> (unit, write_error) result
188188+(** [fill t c] fills every sector with byte [c]. Useful for wiping a device. *)
189189+190190+val zero : t -> (unit, write_error) result
191191+(** [zero t] fills every sector with zeros. Equivalent to [fill t '\x00']. *)
192192+193193+val is_zero : string -> bool
194194+(** [is_zero data] returns [true] if [data] contains only zero bytes. *)