Block device abstraction for OCaml 5 with Eio direct-style I/O and Bytesrw integration
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6type info = { read_write : bool; sector_size : int; sectors : int64 }
7type error = [ `Disconnected | `Read_error of string | `Invalid_sector of int64 ]
8type write_error = [ error | `Write_error of string | `Read_only ]
9
10let pp_error ppf = function
11 | `Disconnected -> Fmt.string ppf "disconnected"
12 | `Read_error s -> Fmt.pf ppf "read error: %s" s
13 | `Invalid_sector n -> Fmt.pf ppf "invalid sector: %Ld" n
14
15let pp_write_error ppf = function
16 | #error as e -> pp_error ppf e
17 | `Write_error s -> Fmt.pf ppf "write error: %s" s
18 | `Read_only -> Fmt.string ppf "read-only device"
19
20module type IMPL = sig
21 type state
22
23 val info : state -> info
24 val read : state -> int64 -> (string, error) result
25 val write : state -> int64 -> string -> (unit, write_error) result
26 val sync : state -> unit
27 val close : state -> unit
28end
29
30type t = T : { state : 'a; impl : (module IMPL with type state = 'a) } -> t
31
32let pp ppf (T { state; impl = (module I) }) =
33 let i = I.info state in
34 Fmt.pf ppf "<block %s sectors=%Ld sector-size=%d>"
35 (if i.read_write then "rw" else "ro")
36 i.sectors i.sector_size
37
38let info (T { state; impl = (module I) }) = I.info state
39let read (T { state; impl = (module I) }) sector = I.read state sector
40
41let write (T { state; impl = (module I) }) sector data =
42 I.write state sector data
43
44let sync (T { state; impl = (module I) }) = I.sync state
45let close (T { state; impl = (module I) }) = I.close state
46
47let read_many t start count =
48 let info = info t in
49 let buf = Buffer.create (count * info.sector_size) in
50 let rec loop i =
51 if i >= count then Ok (Buffer.contents buf)
52 else
53 match read t Int64.(add start (of_int i)) with
54 | Error e -> Error e
55 | Ok data ->
56 Buffer.add_string buf data;
57 loop (i + 1)
58 in
59 loop 0
60
61let write_many t start sectors =
62 let rec loop i = function
63 | [] -> Ok ()
64 | data :: rest -> (
65 match write t Int64.(add start (of_int i)) data with
66 | Error e -> Error e
67 | Ok () -> loop (i + 1) rest)
68 in
69 loop 0 sectors
70
71(* Memory implementation *)
72module Memory = struct
73 type state = {
74 mutable data : bytes;
75 sector_size : int;
76 sectors : int64;
77 mutable closed : bool;
78 }
79
80 let info t =
81 { read_write = true; sector_size = t.sector_size; sectors = t.sectors }
82
83 let read t sector =
84 if t.closed then Error `Disconnected
85 else if sector < 0L || sector >= t.sectors then
86 Error (`Invalid_sector sector)
87 else
88 let off = Int64.to_int sector * t.sector_size in
89 Ok (Bytes.sub_string t.data off t.sector_size)
90
91 let write t sector data =
92 if t.closed then Error `Disconnected
93 else if sector < 0L || sector >= t.sectors then
94 Error (`Invalid_sector sector)
95 else if String.length data <> t.sector_size then
96 Error (`Write_error "data length must equal sector size")
97 else
98 let off = Int64.to_int sector * t.sector_size in
99 Bytes.blit_string data 0 t.data off t.sector_size;
100 Ok ()
101
102 let sync _ = ()
103 let close t = t.closed <- true
104end
105
106let of_memory ~sector_size ~sectors =
107 let size = Int64.to_int sectors * sector_size in
108 let state =
109 {
110 Memory.data = Bytes.make size '\x00';
111 sector_size;
112 sectors;
113 closed = false;
114 }
115 in
116 T { state; impl = (module Memory) }
117
118let of_bigarray ~sector_size ba =
119 let len = Bigarray.Array1.dim ba in
120 if len mod sector_size <> 0 then
121 invalid_arg "bigarray size must be multiple of sector_size";
122 let sectors = Int64.of_int (len / sector_size) in
123 let data = Bytes.create len in
124 for i = 0 to len - 1 do
125 Bytes.set data i (Bigarray.Array1.get ba i)
126 done;
127 let state = { Memory.data; sector_size; sectors; closed = false } in
128 T { state; impl = (module Memory) }
129
130(* Read-only wrapper *)
131module Read_only = struct
132 type state = { inner : t }
133
134 let info t =
135 let i = info t.inner in
136 { i with read_write = false }
137
138 let read t sector = read t.inner sector
139 let write _ _ _ = Error `Read_only
140 let sync t = sync t.inner
141 let close t = close t.inner
142end
143
144let read_only t =
145 let state = { Read_only.inner = t } in
146 T { state; impl = (module Read_only) }
147
148let of_string ~sector_size data =
149 let len = String.length data in
150 if len mod sector_size <> 0 then
151 invalid_arg "string length must be multiple of sector_size";
152 let sectors = Int64.of_int (len / sector_size) in
153 let state =
154 { Memory.data = Bytes.of_string data; sector_size; sectors; closed = false }
155 in
156 (* Return read-only by wrapping *)
157 let t = T { state; impl = (module Memory) } in
158 read_only t
159
160(* File implementation *)
161module File = struct
162 type state = {
163 file : Eio.File.rw_ty Eio.Resource.t;
164 sector_size : int;
165 sectors : int64;
166 mutable closed : bool;
167 }
168
169 let info t =
170 { read_write = true; sector_size = t.sector_size; sectors = t.sectors }
171
172 let read t sector =
173 if t.closed then Error `Disconnected
174 else if sector < 0L || sector >= t.sectors then
175 Error (`Invalid_sector sector)
176 else
177 try
178 let off =
179 Optint.Int63.of_int64 (Int64.mul sector (Int64.of_int t.sector_size))
180 in
181 let buf = Cstruct.create t.sector_size in
182 Eio.File.pread_exact t.file ~file_offset:off [ buf ];
183 Ok (Cstruct.to_string buf)
184 with exn -> Error (`Read_error (Printexc.to_string exn))
185
186 let write t sector data =
187 if t.closed then Error `Disconnected
188 else if sector < 0L || sector >= t.sectors then
189 Error (`Invalid_sector sector)
190 else if String.length data <> t.sector_size then
191 Error (`Write_error "data length must equal sector size")
192 else
193 try
194 let off =
195 Optint.Int63.of_int64 (Int64.mul sector (Int64.of_int t.sector_size))
196 in
197 let buf = Cstruct.of_string data in
198 Eio.File.pwrite_all t.file ~file_offset:off [ buf ];
199 Ok ()
200 with exn -> Error (`Write_error (Printexc.to_string exn))
201
202 let sync t = if not t.closed then Eio.File.sync t.file
203
204 let close t =
205 if not t.closed then (
206 t.closed <- true;
207 Eio.Resource.close t.file)
208end
209
210let of_file ~sw path ~sector_size ?sectors:sectors_override ?create () =
211 if sector_size < 512 || sector_size land (sector_size - 1) <> 0 then
212 invalid_arg "sector_size must be power of 2 and >= 512";
213 let file =
214 match create with
215 | Some sectors ->
216 let size = Int64.mul sectors (Int64.of_int sector_size) in
217 let f = Eio.Path.open_out ~sw path ~create:(`Or_truncate 0o644) in
218 (* Extend file to size *)
219 Eio.File.truncate f (Optint.Int63.of_int64 size);
220 (f :> Eio.File.rw_ty Eio.Resource.t)
221 | None ->
222 Eio.Path.open_out ~sw path ~create:`Never |> fun f ->
223 (f :> Eio.File.rw_ty Eio.Resource.t)
224 in
225 let sectors =
226 match sectors_override with
227 | Some s -> s
228 | None ->
229 let stat = Eio.File.stat file in
230 let size = Optint.Int63.to_int64 stat.size in
231 Int64.div size (Int64.of_int sector_size)
232 in
233 let state = { File.file; sector_size; sectors; closed = false } in
234 T { state; impl = (module File) }
235
236(* Sub-device wrapper *)
237module Sub = struct
238 type state = { inner : t; start : int64; sectors : int64 }
239
240 let info t =
241 let i = info t.inner in
242 { i with sectors = t.sectors }
243
244 let read t sector =
245 if sector < 0L || sector >= t.sectors then Error (`Invalid_sector sector)
246 else read t.inner Int64.(add t.start sector)
247
248 let write t sector data =
249 if sector < 0L || sector >= t.sectors then Error (`Invalid_sector sector)
250 else write t.inner Int64.(add t.start sector) data
251
252 let sync t = sync t.inner
253 let close _ = () (* Don't close underlying device *)
254end
255
256let sub t ~start ~sectors =
257 let i = info t in
258 if start < 0L || sectors < 0L || Int64.add start sectors > i.sectors then
259 invalid_arg "sub: invalid range";
260 let state = { Sub.inner = t; start; sectors } in
261 T { state; impl = (module Sub) }
262
263(* CRC32C wrapper *)
264module With_crc = struct
265 type state = { inner : t; data_size : int }
266
267 let crc32c data =
268 (* Simple CRC32C implementation - in production use Wal.crc32c *)
269 let crc = ref 0xFFFFFFFFl in
270 for i = 0 to String.length data - 1 do
271 let byte = Char.code data.[i] in
272 crc := Int32.logxor !crc (Int32.of_int byte);
273 for _ = 0 to 7 do
274 let mask = Int32.neg (Int32.logand !crc 1l) in
275 crc :=
276 Int32.logxor
277 (Int32.shift_right_logical !crc 1)
278 (Int32.logand 0x82F63B78l mask)
279 done
280 done;
281 Int32.logxor !crc 0xFFFFFFFFl
282
283 let encode_crc crc =
284 let b = Bytes.create 4 in
285 Bytes.set b 0 (Char.chr (Int32.to_int (Int32.logand crc 0xFFl)));
286 Bytes.set b 1
287 (Char.chr
288 (Int32.to_int (Int32.logand (Int32.shift_right_logical crc 8) 0xFFl)));
289 Bytes.set b 2
290 (Char.chr
291 (Int32.to_int (Int32.logand (Int32.shift_right_logical crc 16) 0xFFl)));
292 Bytes.set b 3 (Char.chr (Int32.to_int (Int32.shift_right_logical crc 24)));
293 Bytes.to_string b
294
295 let decode_crc s off =
296 let b0 = Int32.of_int (Char.code s.[off]) in
297 let b1 = Int32.of_int (Char.code s.[off + 1]) in
298 let b2 = Int32.of_int (Char.code s.[off + 2]) in
299 let b3 = Int32.of_int (Char.code s.[off + 3]) in
300 Int32.logor b0
301 (Int32.logor (Int32.shift_left b1 8)
302 (Int32.logor (Int32.shift_left b2 16) (Int32.shift_left b3 24)))
303
304 let info t =
305 let i = info t.inner in
306 { i with sector_size = t.data_size }
307
308 let read t sector =
309 match read t.inner sector with
310 | Error e -> Error e
311 | Ok raw ->
312 let data = String.sub raw 0 t.data_size in
313 let stored_crc = decode_crc raw t.data_size in
314 let computed_crc = crc32c data in
315 if stored_crc <> computed_crc then
316 Error
317 (`Read_error
318 (Fmt.str "CRC mismatch: stored=%lx computed=%lx" stored_crc
319 computed_crc))
320 else Ok data
321
322 let write t sector data =
323 if String.length data <> t.data_size then
324 Error (`Write_error (Fmt.str "data length must be %d" t.data_size))
325 else
326 let crc = crc32c data in
327 let raw = data ^ encode_crc crc in
328 write t.inner sector raw
329
330 let sync t = sync t.inner
331 let close t = close t.inner
332end
333
334let with_crc32c t =
335 let i = info t in
336 let data_size = i.sector_size - 4 in
337 if data_size < 1 then invalid_arg "sector_size too small for CRC";
338 let state = { With_crc.inner = t; data_size } in
339 T { state; impl = (module With_crc) }
340
341(* Bytesrw integration *)
342let to_reader t ~offset ~length =
343 let i = info t in
344 if Int64.rem offset (Int64.of_int i.sector_size) <> 0L then
345 invalid_arg "offset must be sector-aligned";
346 let start_sector = Int64.div offset (Int64.of_int i.sector_size) in
347 let current_sector = ref start_sector in
348 let current_pos = ref 0 in
349 let remaining = ref length in
350 let current_data = ref "" in
351 Bytesrw.Bytes.Reader.make ~slice_length:i.sector_size @@ fun () ->
352 if !remaining <= 0L then Bytesrw.Bytes.Slice.eod
353 else if !current_pos >= String.length !current_data then (
354 (* Read next sector *)
355 match read t !current_sector with
356 | Error _ -> Bytesrw.Bytes.Slice.eod
357 | Ok data ->
358 current_data := data;
359 current_pos := 0;
360 current_sector := Int64.succ !current_sector;
361 let len = min (String.length data) (Int64.to_int !remaining) in
362 remaining := Int64.sub !remaining (Int64.of_int len);
363 current_pos := len;
364 Bytesrw.Bytes.Slice.make
365 (Bytes.unsafe_of_string data)
366 ~first:0 ~length:len)
367 else Bytesrw.Bytes.Slice.eod
368
369let to_writer t ~offset =
370 let i = info t in
371 if Int64.rem offset (Int64.of_int i.sector_size) <> 0L then
372 invalid_arg "offset must be sector-aligned";
373 let start_sector = Int64.div offset (Int64.of_int i.sector_size) in
374 let current_sector = ref start_sector in
375 let buffer = Buffer.create i.sector_size in
376 let flush () =
377 if Buffer.length buffer > 0 then (
378 (* Pad to sector size if needed *)
379 while Buffer.length buffer < i.sector_size do
380 Buffer.add_char buffer '\x00'
381 done;
382 let _ = write t !current_sector (Buffer.contents buffer) in
383 Buffer.clear buffer;
384 current_sector := Int64.succ !current_sector)
385 in
386 Bytesrw.Bytes.Writer.make ~slice_length:i.sector_size @@ fun slice ->
387 if Bytesrw.Bytes.Slice.is_eod slice then flush ()
388 else begin
389 let data =
390 Bytes.sub_string
391 (Bytesrw.Bytes.Slice.bytes slice)
392 (Bytesrw.Bytes.Slice.first slice)
393 (Bytesrw.Bytes.Slice.length slice)
394 in
395 Buffer.add_string buffer data;
396 while Buffer.length buffer >= i.sector_size do
397 let sector_data = Buffer.sub buffer 0 i.sector_size in
398 let _ = write t !current_sector sector_data in
399 current_sector := Int64.succ !current_sector;
400 let remaining =
401 Buffer.sub buffer i.sector_size (Buffer.length buffer - i.sector_size)
402 in
403 Buffer.clear buffer;
404 Buffer.add_string buffer remaining
405 done
406 end
407
408(* Generic operations *)
409
410let fold ~f t init =
411 let i = info t in
412 let rec loop sector acc =
413 if sector >= i.sectors then Ok acc
414 else
415 match read t sector with
416 | Error e -> Error e
417 | Ok data -> (
418 match f sector data acc with
419 | Error e -> Error e
420 | Ok acc' -> loop (Int64.succ sector) acc')
421 in
422 loop 0L init
423
424let iter ~f t = fold ~f:(fun sector data () -> f sector data) t ()
425
426type compare_error =
427 [ error
428 | `Different_sizes
429 | `Different_sector_sizes
430 | `Contents_differ of int64 ]
431
432let pp_compare_error ppf = function
433 | #error as e -> pp_error ppf e
434 | `Different_sizes -> Fmt.string ppf "different sizes"
435 | `Different_sector_sizes -> Fmt.string ppf "different sector sizes"
436 | `Contents_differ n -> Fmt.pf ppf "contents differ at sector %Ld" n
437
438let compare a b =
439 let ia = info a and ib = info b in
440 if ia.sector_size <> ib.sector_size then Error `Different_sector_sizes
441 else if ia.sectors <> ib.sectors then Error `Different_sizes
442 else
443 let rec loop sector =
444 if sector >= ia.sectors then Ok ()
445 else
446 match (read a sector, read b sector) with
447 | Error e, _ -> Error (e :> compare_error)
448 | _, Error e -> Error (e :> compare_error)
449 | Ok da, Ok db ->
450 if da <> db then Error (`Contents_differ sector)
451 else loop (Int64.succ sector)
452 in
453 loop 0L
454
455type copy_error = [ write_error | `Different_sizes | `Different_sector_sizes ]
456
457let pp_copy_error ppf = function
458 | #write_error as e -> pp_write_error ppf e
459 | `Different_sizes -> Fmt.string ppf "different sizes"
460 | `Different_sector_sizes -> Fmt.string ppf "different sector sizes"
461
462let copy ~src ~dst =
463 let is = info src and id = info dst in
464 if is.sector_size <> id.sector_size then Error `Different_sector_sizes
465 else if is.sectors > id.sectors then Error `Different_sizes
466 else
467 let rec loop sector =
468 if sector >= is.sectors then Ok ()
469 else
470 match read src sector with
471 | Error e -> Error (e :> copy_error)
472 | Ok data -> (
473 match write dst sector data with
474 | Error e -> Error (e :> copy_error)
475 | Ok () -> loop (Int64.succ sector))
476 in
477 loop 0L
478
479let is_zero data =
480 let rec loop i =
481 if i >= String.length data then true
482 else if data.[i] <> '\x00' then false
483 else loop (i + 1)
484 in
485 loop 0
486
487let sparse_copy ~src ~dst =
488 let is = info src and id = info dst in
489 if is.sector_size <> id.sector_size then Error `Different_sector_sizes
490 else if is.sectors > id.sectors then Error `Different_sizes
491 else
492 let rec loop sector =
493 if sector >= is.sectors then Ok ()
494 else
495 match read src sector with
496 | Error e -> Error (e :> copy_error)
497 | Ok data -> (
498 if is_zero data then loop (Int64.succ sector)
499 else
500 match write dst sector data with
501 | Error e -> Error (e :> copy_error)
502 | Ok () -> loop (Int64.succ sector))
503 in
504 loop 0L
505
506let fill t c =
507 let i = info t in
508 let data = String.make i.sector_size c in
509 let rec loop sector =
510 if sector >= i.sectors then Ok ()
511 else
512 match write t sector data with
513 | Error e -> Error e
514 | Ok () -> loop (Int64.succ sector)
515 in
516 loop 0L
517
518let zero t = fill t '\x00'