From 42e195dcbde3ebcc9615b661c233109956ff5c2a Mon Sep 17 00:00:00 2001 From: Hamcha Date: Sat, 30 Nov 2024 15:48:01 +0100 Subject: [PATCH] check in --- .forgejo/workflows/test.yml | 21 ++++++++++ .gitignore | 4 ++ README.md | 12 ++++++ gleam.toml | 13 +++++++ manifest.toml | 11 ++++++ src/kmer.gleam | 51 +++++++++++++++++++++++++ test/kmer_test.gleam | 76 +++++++++++++++++++++++++++++++++++++ 7 files changed, 188 insertions(+) create mode 100644 .forgejo/workflows/test.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 gleam.toml create mode 100644 manifest.toml create mode 100644 src/kmer.gleam create mode 100644 test/kmer_test.gleam diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..8c8883b --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,21 @@ +name: test + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + runs-on: docker + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.1.2" + gleam-version: "1.6.2" + rebar3-version: "3" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..603ebb4 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Kmer, father of [Pdor] + +Decoder library for CBOR. + +Based on Noah Pederson's [gleemor] module, licensed under MIT + +## License + +This library is licensed under the GNU Affero General Public License version 3, see `LICENSE`. + +[Pdor]:https://www.youtube.com/watch?v=k3aehC2SSPw +[gleemor]:https://github.com/chiefnoah/gleebor diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..7a4e5a5 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,13 @@ +name = "kmer" +version = "0.1.0" + +description = "CBOR decoder library" +licences = ["AGPL-3.0-only"] +repository = { type = "forgejo", host = "git.fromouter.space", user = "hamcha", repo = "kmer" } +gleam = ">= 1.6.2" + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..55e0da1 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/kmer.gleam b/src/kmer.gleam new file mode 100644 index 0000000..cbf52dc --- /dev/null +++ b/src/kmer.gleam @@ -0,0 +1,51 @@ +import gleam/result.{try} + +pub type DecodedValue { + Integer(value: Int) +} + +type DecoderResult(t) = + Result(#(t, BitArray), DecodeError) + +pub type DecodeResult = + DecoderResult(DecodedValue) + +pub opaque type DecodeError { + // More data was expected but file/stream ended prematurely + UnexpectedEnd + + // Malformed sequence found + Malformed +} + +pub fn decode_next_value(bits: BitArray) -> DecodeResult { + case bits { + <<0:3, rest:bits>> -> + decode_int(rest) + |> map_decoded(Integer) + <<1:3, rest:bits>> -> + decode_int(rest) + |> map_decoded(fn(x) { Integer(-1 - x) }) + _ -> Error(Malformed) + } +} + +fn decode_int(bits: BitArray) -> DecoderResult(Int) { + case bits { + <<24:int-size(5), val:int-unsigned-size(8), rest:bits>> -> Ok(#(val, rest)) + <<25:int-size(5), val:int-unsigned-size(16), rest:bits>> -> Ok(#(val, rest)) + <<26:int-size(5), val:int-unsigned-size(32), rest:bits>> -> Ok(#(val, rest)) + <<27:int-size(5), val:int-unsigned-size(64), rest:bits>> -> Ok(#(val, rest)) + <> if x < 24 -> Ok(#(x, rest)) + <> if x >= 24 -> Error(Malformed) + _ -> Error(UnexpectedEnd) + } +} + +fn map_decoded( + result: DecoderResult(t), + map_fn: fn(t) -> DecodedValue, +) -> DecodeResult { + use #(value, rest) <- try(result) + Ok(#(map_fn(value), rest)) +} diff --git a/test/kmer_test.gleam b/test/kmer_test.gleam new file mode 100644 index 0000000..d356734 --- /dev/null +++ b/test/kmer_test.gleam @@ -0,0 +1,76 @@ +import gleam/list +import gleeunit +import gleeunit/should +import kmer.{type DecodeResult, type DecodedValue, Integer} + +pub fn main() { + gleeunit.main() +} + +fn table_test_decode_next_value(test_case: BitArray, expected: DecodedValue) { + kmer.decode_next_value(test_case) |> is_value(expected) +} + +fn is_value(val: DecodeResult, expected: DecodedValue) { + should.be_ok(val) |> should.equal(#(expected, <<>>)) +} + +pub fn decode_immediate_int_test() { + [ + // Small integers under 24 + #(<<0>>, Integer(0)), + #(<<10>>, Integer(10)), + #(<<23>>, Integer(23)), + ] + |> table_fn(table_test_decode_next_value) +} + +pub fn decode_int_test() { + [ + // 1-byte length (Maj.arg 24) + #(<<24, 24>>, Integer(24)), + #(<<24, 100>>, Integer(100)), + // 2-byte length (Maj.arg 25) + #(<<25, 1234:16>>, Integer(1234)), + #(<<25, 12_345:16>>, Integer(12_345)), + // 3-byte length (Maj.arg 26) + #(<<26, 1_234_567:32>>, Integer(1_234_567)), + #(<<26, 999_999_999:32>>, Integer(999_999_999)), + // 4-byte length (Maj.arg 27) + #(<<27, 2_099_210_021_019_898_881:64>>, Integer(2_099_210_021_019_898_881)), + #(<<27, 3:64>>, Integer(3)), + ] + |> table_fn(table_test_decode_next_value) +} + +pub fn decode_negative_int_test() { + [ + // Immediate negative (under 25) + #(<<1:3, 0:5>>, Integer(-1)), + #(<<1:3, 8:5>>, Integer(-9)), + #(<<1:3, 23:5>>, Integer(-24)), + // 1-byte length (Maj.arg 24) + #(<<1:3, 24:5, 24:8>>, Integer(-25)), + #(<<1:3, 24:5, 100:8>>, Integer(-101)), + // 2-byte length (Maj.arg 25) + #(<<1:3, 25:5, 1234:16>>, Integer(-1235)), + #(<<1:3, 25:5, 12_345:16>>, Integer(-12_346)), + // 3-byte length (Maj.arg 26) + #(<<1:3, 26:5, 1_234_567:32>>, Integer(-1_234_568)), + #(<<1:3, 26:5, 999_999_999:32>>, Integer(-1_000_000_000)), + // 4-byte length (Maj.arg 27) + #( + <<1:3, 27:5, 2_099_210_021_019_898_881:64>>, + Integer(-2_099_210_021_019_898_882), + ), + #(<<1:3, 27:5, 3:64>>, Integer(-4)), + ] + |> table_fn(table_test_decode_next_value) +} + +fn table_fn(tests: List(#(a, b)), test_fn: fn(a, b) -> Nil) { + list.map(tests, fn(t) { + let #(test_case, expected) = t + test_fn(test_case, expected) + }) +}