···11+ISC License
22+33+Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
44+55+Permission to use, copy, modify, and 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
1010+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1111+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1212+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1313+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1414+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+53
README.md
···11+# bytesrw-eio - OCaml Bytesrw adapters for Eio
22+33+This OCaml library provides adapters to create `Bytesrw.Bytes.Reader.t` and
44+`Bytesrw.Bytes.Writer.t` from Eio flows, mirroring the API of `Bytesrw_unix`
55+for Eio's effect-based I/O.
66+77+## Usage
88+99+```ocaml
1010+open Eio.Std
1111+1212+(* Create a reader from an Eio flow *)
1313+let read_from_flow flow =
1414+ let reader = Bytesrw_eio.bytes_reader_of_flow flow in
1515+ (* Use reader with Bytesrw decoders *)
1616+ reader
1717+1818+(* Create a writer to an Eio flow *)
1919+let write_to_flow flow =
2020+ let writer = Bytesrw_eio.bytes_writer_of_flow flow in
2121+ (* Use writer with Bytesrw encoders *)
2222+ writer
2323+```
2424+2525+For custom slice sizes:
2626+2727+```ocaml
2828+(* Specify custom slice length for reading *)
2929+let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length:4096 flow in
3030+3131+(* Specify custom slice length for writing *)
3232+let writer = Bytesrw_eio.bytes_writer_of_flow ~slice_length:4096 flow in
3333+()
3434+```
3535+3636+## Installation
3737+3838+```
3939+opam install bytesrw-eio
4040+```
4141+4242+## Documentation
4343+4444+API documentation is available via:
4545+4646+```
4747+opam install bytesrw-eio
4848+odig doc bytesrw-eio
4949+```
5050+5151+## License
5252+5353+ISC
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Bytesrw adapters for Eio
77+88+ This module provides adapters to create {!Bytesrw.Bytes.Reader.t} and
99+ {!Bytesrw.Bytes.Writer.t} from Eio flows, mirroring the API of
1010+ {!Bytesrw_unix} for Eio's effect-based I/O. *)
1111+1212+open Bytesrw
1313+1414+(** Create a [Bytes.Reader.t] from an Eio source flow.
1515+1616+ Reads directly from the flow without intermediate buffering.
1717+1818+ @param slice_length
1919+ Maximum bytes per slice (default: 65536, which is
2020+ {!Bytes.Slice.unix_io_buffer_size}) *)
2121+let bytes_reader_of_flow ?(slice_length = Bytes.Slice.unix_io_buffer_size)
2222+ (flow : _ Eio.Flow.source) : Bytes.Reader.t =
2323+ let buf_size = Bytes.Slice.check_length slice_length in
2424+ let read () =
2525+ let cstruct = Cstruct.create buf_size in
2626+ match Eio.Flow.single_read flow cstruct with
2727+ | 0 -> Bytes.Slice.eod
2828+ | count ->
2929+ let data_cs = Cstruct.sub cstruct 0 count in
3030+ let buf = Cstruct.to_bytes data_cs in
3131+ Bytes.Slice.make buf ~first:0 ~length:count
3232+ | exception End_of_file -> Bytes.Slice.eod
3333+ in
3434+ Bytes.Reader.make ~slice_length read
3535+3636+(** Create a [Bytes.Writer.t] from an Eio sink flow.
3737+3838+ Writes directly to the flow without intermediate buffering.
3939+4040+ @param slice_length
4141+ Suggested slice length for upstream (default: 65536, which is
4242+ {!Bytes.Slice.unix_io_buffer_size}) *)
4343+let bytes_writer_of_flow ?(slice_length = Bytes.Slice.unix_io_buffer_size)
4444+ (flow : _ Eio.Flow.sink) : Bytes.Writer.t =
4545+ let rec write slice =
4646+ if Bytes.Slice.is_eod slice then ()
4747+ else begin
4848+ let bytes = Bytes.Slice.bytes slice in
4949+ let first = Bytes.Slice.first slice in
5050+ let length = Bytes.Slice.length slice in
5151+ let cstruct = Cstruct.of_bytes ~off:first ~len:length bytes in
5252+ match Eio.Flow.single_write flow [ cstruct ] with
5353+ | count when count = length -> ()
5454+ | count -> write (Option.get (Bytes.Slice.drop count slice))
5555+ end
5656+ in
5757+ Bytes.Writer.make ~slice_length write
+33
src/bytesrw_eio.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Bytesrw adapters for Eio
77+88+ This module provides adapters to create {!Bytesrw.Bytes.Reader.t} and
99+ {!Bytesrw.Bytes.Writer.t} from Eio flows, mirroring the API of
1010+ {!Bytesrw_unix} for Eio's effect-based I/O.
1111+1212+ Unlike the Buf_read/Buf_write wrappers, these adapters read and write
1313+ directly to the flow, allowing bytesrw to handle its own buffering. *)
1414+1515+(** {1 Readers} *)
1616+1717+val bytes_reader_of_flow :
1818+ ?slice_length:int -> _ Eio.Flow.source -> Bytesrw.Bytes.Reader.t
1919+(** [bytes_reader_of_flow flow] creates a reader from an Eio source flow.
2020+2121+ Reads directly from the flow without intermediate buffering.
2222+2323+ @param slice_length Maximum bytes per slice (default: 65536) *)
2424+2525+(** {1 Writers} *)
2626+2727+val bytes_writer_of_flow :
2828+ ?slice_length:int -> _ Eio.Flow.sink -> Bytesrw.Bytes.Writer.t
2929+(** [bytes_writer_of_flow flow] creates a writer from an Eio sink flow.
3030+3131+ Writes directly to the flow without intermediate buffering.
3232+3333+ @param slice_length Suggested slice length for upstream (default: 65536) *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(* Test reading from a mock flow *)
77+let test_reader_basic () =
88+ Eio_main.run @@ fun _env ->
99+ let test_data = "Hello, World!" in
1010+ let flow = Eio.Flow.string_source test_data in
1111+ let reader = Bytesrw_eio.bytes_reader_of_flow flow in
1212+1313+ (* Read first slice *)
1414+ let slice1 = Bytesrw.Bytes.Reader.read reader in
1515+ Alcotest.(check bool)
1616+ "slice is not eod" false
1717+ (Bytesrw.Bytes.Slice.is_eod slice1);
1818+1919+ let read_data =
2020+ Bytes.sub_string
2121+ (Bytesrw.Bytes.Slice.bytes slice1)
2222+ (Bytesrw.Bytes.Slice.first slice1)
2323+ (Bytesrw.Bytes.Slice.length slice1)
2424+ in
2525+ Alcotest.(check string) "data matches" test_data read_data;
2626+2727+ (* Next read should be eod *)
2828+ let slice2 = Bytesrw.Bytes.Reader.read reader in
2929+ Alcotest.(check bool)
3030+ "second read is eod" true
3131+ (Bytesrw.Bytes.Slice.is_eod slice2)
3232+3333+(* Test reading with custom slice length *)
3434+let test_reader_custom_slice_length () =
3535+ Eio_main.run @@ fun _env ->
3636+ let test_data = "Hello, World!" in
3737+ let flow = Eio.Flow.string_source test_data in
3838+ let slice_length = 5 in
3939+ let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length flow in
4040+4141+ (* Read should respect slice_length as maximum *)
4242+ let slice = Bytesrw.Bytes.Reader.read reader in
4343+ Alcotest.(check bool)
4444+ "slice length <= custom size" true
4545+ (Bytesrw.Bytes.Slice.length slice <= slice_length)
4646+4747+(* Test reading empty flow *)
4848+let test_reader_empty () =
4949+ Eio_main.run @@ fun _env ->
5050+ let flow = Eio.Flow.string_source "" in
5151+ let reader = Bytesrw_eio.bytes_reader_of_flow flow in
5252+5353+ let slice = Bytesrw.Bytes.Reader.read reader in
5454+ Alcotest.(check bool)
5555+ "empty flow returns eod" true
5656+ (Bytesrw.Bytes.Slice.is_eod slice)
5757+5858+(* Test writing to a mock flow *)
5959+let test_writer_basic () =
6060+ Eio_main.run @@ fun _env ->
6161+ let buf = Buffer.create 100 in
6262+ let flow = Eio.Flow.buffer_sink buf in
6363+ let writer = Bytesrw_eio.bytes_writer_of_flow flow in
6464+6565+ let test_data = "Hello, World!" in
6666+ let bytes = Bytes.of_string test_data in
6767+ let slice =
6868+ Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes)
6969+ in
7070+7171+ Bytesrw.Bytes.Writer.write writer slice;
7272+7373+ let written = Buffer.contents buf in
7474+ Alcotest.(check string) "written data matches" test_data written
7575+7676+(* Test writing with custom slice length *)
7777+let test_writer_custom_slice_length () =
7878+ Eio_main.run @@ fun _env ->
7979+ let buf = Buffer.create 100 in
8080+ let flow = Eio.Flow.buffer_sink buf in
8181+ let slice_length = 8 in
8282+ let writer = Bytesrw_eio.bytes_writer_of_flow ~slice_length flow in
8383+8484+ let test_data = "Hello, World!" in
8585+ let bytes = Bytes.of_string test_data in
8686+ let slice =
8787+ Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes)
8888+ in
8989+9090+ Bytesrw.Bytes.Writer.write writer slice;
9191+9292+ let written = Buffer.contents buf in
9393+ Alcotest.(check string)
9494+ "written data matches regardless of slice_length" test_data written
9595+9696+(* Test writing eod slice (should be no-op) *)
9797+let test_writer_eod () =
9898+ Eio_main.run @@ fun _env ->
9999+ let buf = Buffer.create 100 in
100100+ let flow = Eio.Flow.buffer_sink buf in
101101+ let writer = Bytesrw_eio.bytes_writer_of_flow flow in
102102+103103+ Bytesrw.Bytes.Writer.write writer Bytesrw.Bytes.Slice.eod;
104104+105105+ let written = Buffer.contents buf in
106106+ Alcotest.(check string) "eod writes nothing" "" written
107107+108108+(* Test writing partial slice *)
109109+let test_writer_partial_slice () =
110110+ Eio_main.run @@ fun _env ->
111111+ let buf = Buffer.create 100 in
112112+ let flow = Eio.Flow.buffer_sink buf in
113113+ let writer = Bytesrw_eio.bytes_writer_of_flow flow in
114114+115115+ let test_data = "Hello, World!" in
116116+ let bytes = Bytes.of_string test_data in
117117+ (* Write only "World" *)
118118+ let slice = Bytesrw.Bytes.Slice.make bytes ~first:7 ~length:5 in
119119+120120+ Bytesrw.Bytes.Writer.write writer slice;
121121+122122+ let written = Buffer.contents buf in
123123+ Alcotest.(check string) "partial slice written" "World" written
124124+125125+(* Test multiple reads to ensure data isolation - buffers from previous reads
126126+ should not be corrupted by subsequent reads *)
127127+let test_reader_multiple_reads () =
128128+ Eio_main.run @@ fun _env ->
129129+ let test_data = "ABCDEFGHIJ" in
130130+ (* 10 bytes *)
131131+ let flow = Eio.Flow.string_source test_data in
132132+ let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length:5 flow in
133133+134134+ (* Read first 5 bytes *)
135135+ let slice1 = Bytesrw.Bytes.Reader.read reader in
136136+ let bytes1 = Bytesrw.Bytes.Slice.bytes slice1 in
137137+ let data1 =
138138+ Bytes.sub_string bytes1
139139+ (Bytesrw.Bytes.Slice.first slice1)
140140+ (Bytesrw.Bytes.Slice.length slice1)
141141+ in
142142+143143+ (* Read next 5 bytes *)
144144+ let slice2 = Bytesrw.Bytes.Reader.read reader in
145145+ let data2 =
146146+ Bytes.sub_string
147147+ (Bytesrw.Bytes.Slice.bytes slice2)
148148+ (Bytesrw.Bytes.Slice.first slice2)
149149+ (Bytesrw.Bytes.Slice.length slice2)
150150+ in
151151+152152+ (* Critical test: verify first read's data is STILL intact after second read
153153+ This would fail if we were reusing buffers or if Cstruct.to_bytes created a view *)
154154+ let data1_check =
155155+ Bytes.sub_string bytes1
156156+ (Bytesrw.Bytes.Slice.first slice1)
157157+ (Bytesrw.Bytes.Slice.length slice1)
158158+ in
159159+160160+ Alcotest.(check string) "first read" "ABCDE" data1;
161161+ Alcotest.(check string) "second read" "FGHIJ" data2;
162162+ Alcotest.(check string)
163163+ "first read still intact after second" "ABCDE" data1_check
164164+165165+(* Test round-trip: write then read *)
166166+let test_roundtrip () =
167167+ Eio_main.run @@ fun _env ->
168168+ let test_data = "Round-trip test data" in
169169+170170+ (* Write to buffer *)
171171+ let buf = Buffer.create 100 in
172172+ let write_flow = Eio.Flow.buffer_sink buf in
173173+ let writer = Bytesrw_eio.bytes_writer_of_flow write_flow in
174174+175175+ let bytes = Bytes.of_string test_data in
176176+ let slice =
177177+ Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes)
178178+ in
179179+ Bytesrw.Bytes.Writer.write writer slice;
180180+181181+ (* Read back from buffer *)
182182+ let read_flow = Eio.Flow.string_source (Buffer.contents buf) in
183183+ let reader = Bytesrw_eio.bytes_reader_of_flow read_flow in
184184+185185+ let read_slice = Bytesrw.Bytes.Reader.read reader in
186186+ let read_data =
187187+ Bytes.sub_string
188188+ (Bytesrw.Bytes.Slice.bytes read_slice)
189189+ (Bytesrw.Bytes.Slice.first read_slice)
190190+ (Bytesrw.Bytes.Slice.length read_slice)
191191+ in
192192+193193+ Alcotest.(check string) "round-trip data matches" test_data read_data
194194+195195+let () =
196196+ Alcotest.run "Bytesrw_eio"
197197+ [
198198+ ( "reader",
199199+ [
200200+ Alcotest.test_case "basic read" `Quick test_reader_basic;
201201+ Alcotest.test_case "custom slice length" `Quick
202202+ test_reader_custom_slice_length;
203203+ Alcotest.test_case "empty flow" `Quick test_reader_empty;
204204+ Alcotest.test_case "multiple reads data isolation" `Quick
205205+ test_reader_multiple_reads;
206206+ ] );
207207+ ( "writer",
208208+ [
209209+ Alcotest.test_case "basic write" `Quick test_writer_basic;
210210+ Alcotest.test_case "custom slice length" `Quick
211211+ test_writer_custom_slice_length;
212212+ Alcotest.test_case "eod write" `Quick test_writer_eod;
213213+ Alcotest.test_case "partial slice" `Quick test_writer_partial_slice;
214214+ ] );
215215+ ("integration", [ Alcotest.test_case "round-trip" `Quick test_roundtrip ]);
216216+ ]