Bytesrw adapter for Eio
ocaml codec

Compare changes

Choose any two refs to compare.

+17 -1
.gitignore
···
-
_build
···
+
# OCaml build artifacts
+
_build/
+
*.install
+
*.merlin
+
+
# Third-party sources (fetch locally with opam source)
+
third_party/
+
+
# Editor and OS files
+
.DS_Store
+
*.swp
+
*~
+
.vscode/
+
.idea/
+
+
# Opam local switch
+
_opam/
+1
.ocamlformat
···
···
+
version=0.28.1
+49
.tangled/workflows/build.yml
···
···
+
when:
+
- event: ["push", "pull_request"]
+
branch: ["main"]
+
+
engine: nixery
+
+
dependencies:
+
nixpkgs:
+
- shell
+
- stdenv
+
- findutils
+
- binutils
+
- libunwind
+
- ncurses
+
- opam
+
- git
+
- gawk
+
- gnupatch
+
- gnum4
+
- gnumake
+
- gnutar
+
- gnused
+
- gnugrep
+
- diffutils
+
- gzip
+
- bzip2
+
- gcc
+
- ocaml
+
+
steps:
+
- name: opam
+
command: |
+
opam init --disable-sandboxing -a -y
+
- name: switch
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only
+
- name: build
+
command: |
+
opam exec -- dune build
+
- name: switch-test
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only --with-test
+
- name: test
+
command: |
+
opam exec -- dune runtest --verbose
+
- name: doc
+
command: |
+
opam install -y odoc
+
opam exec -- dune build @doc
+15
LICENSE.md
···
···
+
ISC License
+
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
+
+
Permission to use, copy, modify, and distribute this software for any
+
purpose with or without fee is hereby granted, provided that the above
+
copyright notice and this permission notice appear in all copies.
+
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+53
README.md
···
···
+
# bytesrw-eio - OCaml Bytesrw adapters for Eio
+
+
This OCaml library provides adapters to create `Bytesrw.Bytes.Reader.t` and
+
`Bytesrw.Bytes.Writer.t` from Eio flows, mirroring the API of `Bytesrw_unix`
+
for Eio's effect-based I/O.
+
+
## Usage
+
+
```ocaml
+
open Eio.Std
+
+
(* Create a reader from an Eio flow *)
+
let read_from_flow flow =
+
let reader = Bytesrw_eio.bytes_reader_of_flow flow in
+
(* Use reader with Bytesrw decoders *)
+
reader
+
+
(* Create a writer to an Eio flow *)
+
let write_to_flow flow =
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
(* Use writer with Bytesrw encoders *)
+
writer
+
```
+
+
For custom slice sizes:
+
+
```ocaml
+
(* Specify custom slice length for reading *)
+
let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length:4096 flow in
+
+
(* Specify custom slice length for writing *)
+
let writer = Bytesrw_eio.bytes_writer_of_flow ~slice_length:4096 flow in
+
()
+
```
+
+
## Installation
+
+
```
+
opam install bytesrw-eio
+
```
+
+
## Documentation
+
+
API documentation is available via:
+
+
```
+
opam install bytesrw-eio
+
odig doc bytesrw-eio
+
```
+
+
## License
+
+
ISC
+7
bytesrw-eio.opam
···
synopsis: "Bytesrw readers and writers for Eio"
description:
"Provides Bytesrw.Bytes.Reader and Writer adapters for Eio Flows"
depends: [
"dune" {>= "3.18"}
"ocaml" {>= "5.0"}
"bytesrw" {>= "0.2"}
"eio" {>= "1.0"}
"odoc" {with-doc}
]
build: [
["dune" "subst"] {dev}
···
synopsis: "Bytesrw readers and writers for Eio"
description:
"Provides Bytesrw.Bytes.Reader and Writer adapters for Eio Flows"
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
homepage: "https://tangled.org/@anil.recoil.org/ocaml-bytesrw-eio"
+
bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-bytesrw-eio/issues"
depends: [
"dune" {>= "3.18"}
"ocaml" {>= "5.0"}
"bytesrw" {>= "0.2"}
"eio" {>= "1.0"}
"odoc" {with-doc}
+
"alcotest" {with-test & >= "1.7.0"}
+
"eio_main" {with-test}
]
build: [
["dune" "subst"] {dev}
+12 -1
dune-project
···
(lang dune 3.18)
(name bytesrw-eio)
(generate_opam_files true)
(package
(name bytesrw-eio)
(synopsis "Bytesrw readers and writers for Eio")
···
(depends
(ocaml (>= 5.0))
(bytesrw (>= 0.2))
-
(eio (>= 1.0))))
···
(lang dune 3.18)
+
(name bytesrw-eio)
(generate_opam_files true)
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(homepage "https://tangled.org/@anil.recoil.org/ocaml-bytesrw-eio")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(bug_reports "https://tangled.org/@anil.recoil.org/ocaml-bytesrw-eio/issues")
+
(maintenance_intent "(latest)")
+
(package
(name bytesrw-eio)
(synopsis "Bytesrw readers and writers for Eio")
···
(depends
(ocaml (>= 5.0))
(bytesrw (>= 0.2))
+
(eio (>= 1.0))
+
(odoc :with-doc)
+
(alcotest (and :with-test (>= 1.7.0)))
+
(eio_main :with-test)))
+9 -6
src/bytesrw_eio.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
(** Bytesrw adapters for Eio
···
?(slice_length = Bytes.Slice.unix_io_buffer_size)
(flow : _ Eio.Flow.source)
: Bytes.Reader.t =
-
let buf = Bytes.create (Bytes.Slice.check_length slice_length) in
-
let cstruct = Cstruct.of_bytes buf in
let read () =
match Eio.Flow.single_read flow cstruct with
| 0 -> Bytes.Slice.eod
-
| count -> Bytes.Slice.make buf ~first:0 ~length:count
| exception End_of_file -> Bytes.Slice.eod
in
Bytes.Reader.make ~slice_length read
···
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Bytesrw adapters for Eio
···
?(slice_length = Bytes.Slice.unix_io_buffer_size)
(flow : _ Eio.Flow.source)
: Bytes.Reader.t =
+
let buf_size = Bytes.Slice.check_length slice_length in
let read () =
+
let cstruct = Cstruct.create buf_size in
match Eio.Flow.single_read flow cstruct with
| 0 -> Bytes.Slice.eod
+
| count ->
+
let data_cs = Cstruct.sub cstruct 0 count in
+
let buf = Cstruct.to_bytes data_cs in
+
Bytes.Slice.make buf ~first:0 ~length:count
| exception End_of_file -> Bytes.Slice.eod
in
Bytes.Reader.make ~slice_length read
+5
src/bytesrw_eio.mli
···
(** Bytesrw adapters for Eio
This module provides adapters to create {!Bytesrw.Bytes.Reader.t} and
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Bytesrw adapters for Eio
This module provides adapters to create {!Bytesrw.Bytes.Reader.t} and
+3
test/dune
···
···
+
(test
+
(name test_bytesrw_eio)
+
(libraries bytesrw-eio bytesrw eio eio_main alcotest))
+186
test/test_bytesrw_eio.ml
···
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Test reading from a mock flow *)
+
let test_reader_basic () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "Hello, World!" in
+
let flow = Eio.Flow.string_source test_data in
+
let reader = Bytesrw_eio.bytes_reader_of_flow flow in
+
+
(* Read first slice *)
+
let slice1 = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "slice is not eod" false (Bytesrw.Bytes.Slice.is_eod slice1);
+
+
let read_data = Bytes.sub_string
+
(Bytesrw.Bytes.Slice.bytes slice1)
+
(Bytesrw.Bytes.Slice.first slice1)
+
(Bytesrw.Bytes.Slice.length slice1) in
+
Alcotest.(check string) "data matches" test_data read_data;
+
+
(* Next read should be eod *)
+
let slice2 = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "second read is eod" true (Bytesrw.Bytes.Slice.is_eod slice2)
+
+
(* Test reading with custom slice length *)
+
let test_reader_custom_slice_length () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "Hello, World!" in
+
let flow = Eio.Flow.string_source test_data in
+
let slice_length = 5 in
+
let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length flow in
+
+
(* Read should respect slice_length as maximum *)
+
let slice = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "slice length <= custom size" true
+
(Bytesrw.Bytes.Slice.length slice <= slice_length)
+
+
(* Test reading empty flow *)
+
let test_reader_empty () =
+
Eio_main.run @@ fun _env ->
+
let flow = Eio.Flow.string_source "" in
+
let reader = Bytesrw_eio.bytes_reader_of_flow flow in
+
+
let slice = Bytesrw.Bytes.Reader.read reader in
+
Alcotest.(check bool) "empty flow returns eod" true (Bytesrw.Bytes.Slice.is_eod slice)
+
+
(* Test writing to a mock flow *)
+
let test_writer_basic () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
+
let test_data = "Hello, World!" in
+
let bytes = Bytes.of_string test_data in
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in
+
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "written data matches" test_data written
+
+
(* Test writing with custom slice length *)
+
let test_writer_custom_slice_length () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let slice_length = 8 in
+
let writer = Bytesrw_eio.bytes_writer_of_flow ~slice_length flow in
+
+
let test_data = "Hello, World!" in
+
let bytes = Bytes.of_string test_data in
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in
+
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "written data matches regardless of slice_length" test_data written
+
+
(* Test writing eod slice (should be no-op) *)
+
let test_writer_eod () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
+
Bytesrw.Bytes.Writer.write writer Bytesrw.Bytes.Slice.eod;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "eod writes nothing" "" written
+
+
(* Test writing partial slice *)
+
let test_writer_partial_slice () =
+
Eio_main.run @@ fun _env ->
+
let buf = Buffer.create 100 in
+
let flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow flow in
+
+
let test_data = "Hello, World!" in
+
let bytes = Bytes.of_string test_data in
+
(* Write only "World" *)
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:7 ~length:5 in
+
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
let written = Buffer.contents buf in
+
Alcotest.(check string) "partial slice written" "World" written
+
+
(* Test multiple reads to ensure data isolation - buffers from previous reads
+
should not be corrupted by subsequent reads *)
+
let test_reader_multiple_reads () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "ABCDEFGHIJ" in (* 10 bytes *)
+
let flow = Eio.Flow.string_source test_data in
+
let reader = Bytesrw_eio.bytes_reader_of_flow ~slice_length:5 flow in
+
+
(* Read first 5 bytes *)
+
let slice1 = Bytesrw.Bytes.Reader.read reader in
+
let bytes1 = Bytesrw.Bytes.Slice.bytes slice1 in
+
let data1 = Bytes.sub_string bytes1
+
(Bytesrw.Bytes.Slice.first slice1)
+
(Bytesrw.Bytes.Slice.length slice1) in
+
+
(* Read next 5 bytes *)
+
let slice2 = Bytesrw.Bytes.Reader.read reader in
+
let data2 = Bytes.sub_string
+
(Bytesrw.Bytes.Slice.bytes slice2)
+
(Bytesrw.Bytes.Slice.first slice2)
+
(Bytesrw.Bytes.Slice.length slice2) in
+
+
(* Critical test: verify first read's data is STILL intact after second read
+
This would fail if we were reusing buffers or if Cstruct.to_bytes created a view *)
+
let data1_check = Bytes.sub_string bytes1
+
(Bytesrw.Bytes.Slice.first slice1)
+
(Bytesrw.Bytes.Slice.length slice1) in
+
+
Alcotest.(check string) "first read" "ABCDE" data1;
+
Alcotest.(check string) "second read" "FGHIJ" data2;
+
Alcotest.(check string) "first read still intact after second" "ABCDE" data1_check
+
+
(* Test round-trip: write then read *)
+
let test_roundtrip () =
+
Eio_main.run @@ fun _env ->
+
let test_data = "Round-trip test data" in
+
+
(* Write to buffer *)
+
let buf = Buffer.create 100 in
+
let write_flow = Eio.Flow.buffer_sink buf in
+
let writer = Bytesrw_eio.bytes_writer_of_flow write_flow in
+
+
let bytes = Bytes.of_string test_data in
+
let slice = Bytesrw.Bytes.Slice.make bytes ~first:0 ~length:(Bytes.length bytes) in
+
Bytesrw.Bytes.Writer.write writer slice;
+
+
(* Read back from buffer *)
+
let read_flow = Eio.Flow.string_source (Buffer.contents buf) in
+
let reader = Bytesrw_eio.bytes_reader_of_flow read_flow in
+
+
let read_slice = Bytesrw.Bytes.Reader.read reader in
+
let read_data = Bytes.sub_string
+
(Bytesrw.Bytes.Slice.bytes read_slice)
+
(Bytesrw.Bytes.Slice.first read_slice)
+
(Bytesrw.Bytes.Slice.length read_slice) in
+
+
Alcotest.(check string) "round-trip data matches" test_data read_data
+
+
let () =
+
Alcotest.run "Bytesrw_eio" [
+
"reader", [
+
Alcotest.test_case "basic read" `Quick test_reader_basic;
+
Alcotest.test_case "custom slice length" `Quick test_reader_custom_slice_length;
+
Alcotest.test_case "empty flow" `Quick test_reader_empty;
+
Alcotest.test_case "multiple reads data isolation" `Quick test_reader_multiple_reads;
+
];
+
"writer", [
+
Alcotest.test_case "basic write" `Quick test_writer_basic;
+
Alcotest.test_case "custom slice length" `Quick test_writer_custom_slice_length;
+
Alcotest.test_case "eod write" `Quick test_writer_eod;
+
Alcotest.test_case "partial slice" `Quick test_writer_partial_slice;
+
];
+
"integration", [
+
Alcotest.test_case "round-trip" `Quick test_roundtrip;
+
];
+
]