Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository = { type = "github", user = "Beaudidly", repo = "gbor" }
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
ieee_float = ">= 1.5.0 and < 2.0.0"
gleam_erlang = ">= 1.2.0 and < 2.0.0"
gleam_time = ">= 1.3.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
3 changes: 3 additions & 0 deletions src/gbor.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/// > **Note**: This type may become opaque in future major types as we evaluate
/// > the API needs for this package.
///
import gleam/time/timestamp.{type Timestamp}

pub type CBOR {
CBInt(Int)
CBString(String)
Expand All @@ -18,6 +20,7 @@ pub type CBOR {
CBUndefined
CBBinary(BitArray)
CBTagged(Int, CBOR)
CBTime(Timestamp)
}

// Placeholder for LSP
Expand Down
71 changes: 62 additions & 9 deletions src/gbor/decode.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import gleam/bool
import gleam/dynamic
import gleam/dynamic/decode as gdd
import gleam/erlang/atom
import gleam/float
import gleam/int
import gleam/list
import gleam/result
import gleam/string
import gleam/time/duration
import gleam/time/timestamp

import ieee_float as i

Expand Down Expand Up @@ -39,24 +42,29 @@ pub fn cbor_to_dynamic(cbor: g.CBOR) -> dynamic.Dynamic {
g.CBUndefined -> dynamic.nil()
g.CBBinary(v) -> dynamic.bit_array(v)
g.CBTagged(tag, value) -> ffi_to_tagged(tag, cbor_to_dynamic(value))
g.CBTime(v) ->
ffi_to_tagged(
0,
dynamic.string(timestamp.to_rfc3339(v, duration.hours(0))),
)
}
}

/// Decode a tagged CBOR value.
///
///
/// Provided tag is the expected tag number for the value, and the decoder is run
/// on the value corresponding to the tag.
///
///
/// For example, for a CBOR value with a tag number of `0`, the expected data item
/// is a text string representing a standard time string, so one would call:
///
///
/// ```gleam
/// import gleam/dynamic/decode as gdd
/// tagged_decoder(0, gdd.string, "")
/// ```
///
///
/// Reference: [RFC 8949 : 3.4 Tagging of Items](https://www.rfc-editor.org/rfc/rfc8949.html#name-tagging-of-items)
///
///
pub fn tagged_decoder(
expected_tag: Int,
decoder: gdd.Decoder(a),
Expand All @@ -78,11 +86,11 @@ pub fn tagged_decoder(
}

/// Decode a CBOR value from a bit array
///
///
/// This function is the main entry point for decoding CBOR data into Gleam types.
///
///
/// It takes a bit array and returns a result containing the decoded CBOR value
///
///
pub fn from_bit_array(data: BitArray) -> Result(g.CBOR, CborDecodeError) {
case decode_helper(data) {
Ok(#(v, <<>>)) -> Ok(v)
Expand Down Expand Up @@ -392,8 +400,53 @@ fn decode_tagged(
})

use #(tag_value, rest) <- result.try(decode_helper(rest))
case tag_num {
0 | 1 | 2 | 3 ->
case decode_low_tag(tag_num, tag_value) {
Ok(val) -> Ok(#(val, rest))
Error(Nil) -> Error(MajorTypeError(6))
}
_ -> {
Ok(#(g.CBTagged(tag_num, tag_value), rest))
}
}
}

fn decode_low_tag(min: Int, value: g.CBOR) -> Result(g.CBOR, Nil) {
case min, value {
0, g.CBString(datetime) -> decode_datetime(datetime)
1, g.CBInt(time) -> Ok(g.CBTime(timestamp.from_unix_seconds(time)))
1, g.CBFloat(time) -> Ok(g.CBTime(decode_timestamp(time)))
2, g.CBBinary(value) -> {
let size = bit_array.byte_size(value)
case value {
<<res:unsigned-int-size(size)-unit(8)>> -> Ok(g.CBInt(res))
_ -> Error(Nil)
}
}
3, g.CBBinary(value) ->
case decode_low_tag(2, g.CBBinary(value)) {
Ok(g.CBInt(bigint)) -> Ok(g.CBInt({ -bigint } - 1))
u -> u
}
_, _ -> Error(Nil)
}
}

Ok(#(g.CBTagged(tag_num, tag_value), rest))
fn decode_datetime(dt: String) -> Result(g.CBOR, Nil) {
case timestamp.parse_rfc3339(dt) {
Ok(dt) -> Ok(g.CBTime(dt))
_ -> Error(Nil)
}
}

fn decode_timestamp(ts: Float) -> timestamp.Timestamp {
let seconds = float.floor(ts)
let nano = { ts -. seconds } *. 1.0e9
timestamp.from_unix_seconds_and_nanoseconds(
float.truncate(seconds),
float.truncate(nano),
)
}

@external(erlang, "erl_gbor", "to_tagged")
Expand Down
30 changes: 28 additions & 2 deletions src/gbor/encode.gleam
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//// Module where we can find the functions used for getting the CBOR binary representation of a value

import gleam/bit_array
import gleam/int
import gleam/list
import gleam/result
import gleam/string
import gleam/time/duration
import gleam/time/timestamp

import ieee_float as i

Expand All @@ -28,6 +31,11 @@ pub fn to_bit_array(value: g.CBOR) -> Result(BitArray, EncodeError) {
g.CBTagged(t, v) -> tagged_encode(t, v)
g.CBNull -> Ok(null_encode())
g.CBUndefined -> Ok(undefined_encode())
g.CBTime(v) ->
to_bit_array(g.CBTagged(
0,
g.CBString(timestamp.to_rfc3339(v, duration.hours(0))),
))
}
}

Expand All @@ -39,7 +47,15 @@ fn uint_encode(value: Int) -> Result(BitArray, EncodeError) {
v if v < 0x10000 -> Ok(<<0:3, 25:5, v:16>>)
v if v < 0x100000000 -> Ok(<<0:3, 26:5, v:32>>)
v if v < 0x10000000000000000 -> Ok(<<0:3, 27:5, v:64>>)
v -> Error(EncodeError("Int value too large: " <> string.inspect(v)))
v -> {
let l = string.length(int.to_base16(v))
let l = l - { l / 2 }
case l < 0b100000 {
True -> Ok(<<6:3, 2:5, 2:3, l:5, v:size(l)-unit(8)>>)
False ->
Error(EncodeError("Int value too large: " <> string.inspect(v)))
}
}
}
}

Expand All @@ -52,7 +68,17 @@ fn int_encode(value: Int) -> Result(BitArray, EncodeError) {
v if v < 0x10000 -> Ok(<<1:3, 25:5, v:16>>)
v if v < 0x100000000 -> Ok(<<1:3, 26:5, v:32>>)
v if v < 0x10000000000000000 -> Ok(<<1:3, 27:5, v:64>>)
v -> Error(EncodeError("Int value too large: " <> string.inspect(v)))
v -> {
let l = string.length(int.to_base16(v))
let l = l - { l / 2 }
case l < 0b100000 {
True -> Ok(<<6:3, 3:5, 2:3, l:5, v:size(l)-unit(8)>>)
False ->
Error(EncodeError(
"Absolute Int value too large: " <> string.inspect(v),
))
}
}
}
}

Expand Down
34 changes: 17 additions & 17 deletions test/round_trip_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
//// Negative values
//// Special values (Infinity, NaN)

import gbor as g
import gbor/decode as d
import gbor/encode as e
import gleam/bit_array
import gleam/bool
import gleam/dynamic/decode as gdd
import gleam/list
import gleam/result
import gleam/string
import gleam/time/timestamp
import gleeunit

import gbor as g
import gbor/decode as d
import gbor/encode as e

pub fn main() {
gleeunit.main()
}
Expand Down Expand Up @@ -184,28 +184,27 @@ pub fn decode_simple_test() {
}

pub fn decode_taggded_test() {
let assert Ok(payload) = bit_array.base16_decode("010000000000000000")
let assert Ok(_) =
round_trip(g.CBTagged(2, g.CBBinary(payload)), "c249010000000000000000")
round_trip(g.CBInt(18_446_744_073_709_551_616), "c249010000000000000000")

let assert Ok(payload) = bit_array.base16_decode("010000000000000000")
let assert Ok(_) =
round_trip(g.CBTagged(3, g.CBBinary(payload)), "c349010000000000000000")
round_trip(g.CBInt(-18_446_744_073_709_551_617), "c349010000000000000000")

let assert Ok(timestamp) = timestamp.parse_rfc3339("2013-03-21T20:04:00Z")
let assert Ok(_) =
round_trip(
g.CBTagged(0, g.CBString("2013-03-21T20:04:00Z")),
g.CBTime(timestamp),
"c074323031332d30332d32315432303a30343a30305a",
)

let assert Ok(_) =
round_trip(g.CBTagged(1, g.CBInt(1_363_896_240)), "c11a514b67b0")
// CBTagged(1, CBInt(1363896240)) <=>
// "c11a514b67b0" <=>
// "2013-03-21T20:04:00Z"
let assert Ok(_) = round_trip(g.CBTime(timestamp), "c11a514b67b0")

let assert Ok(_) =
round_trip(
g.CBTagged(1, g.CBFloat(1_363_896_240.5)),
"c1fb41d452d9ec200000",
)
// CBTagged(1, CBFloat(1363896240.5)) <=> "c1fb41d452d9ec200000"
let assert Ok(timestamp) = timestamp.parse_rfc3339("2013-03-21T20:04:00.50Z")
let assert Ok(_) = round_trip(g.CBTime(timestamp), "c1fb41d452d9ec200000")

let assert Ok(_) =
round_trip(
Expand Down Expand Up @@ -283,8 +282,9 @@ fn round_trip(expected: g.CBOR, hex: String) -> Result(Nil, String) {
),
)

// Time is always encoded to tag 0, so skip when decoded from tag 1
let assert Ok(encoded) = e.to_bit_array(v)
case encoded == binary {
case encoded == binary || string.starts_with(string.lowercase(hex), "c1") {
True -> Ok(Nil)
False -> {
let encoded_hex = bit_array.base16_encode(encoded)
Expand Down