diff --git a/lib/oxygene/base32.rb b/lib/oxygene/base32.rb index 5465236..4c87947 100644 --- a/lib/oxygene/base32.rb +++ b/lib/oxygene/base32.rb @@ -22,7 +22,7 @@ module Oxygene # A single base32 character is one of the 32 values in the BASE32_ALPHABET string # above, i.e. 5 bits. The BASE32_PAIRS array stores a (flattened) 32x32 table # of all possible combinations of two-character pairs (10 bits). - # + # # Instead of taking the 40 bits of a 5-byte slice of the original string, slicing # it into 8 5-bit pieces and looking up 8 separate base32 characters, we process # 10 bits at a time here, looking up 4 two-character pairs. This allows us to do diff --git a/lib/oxygene/car_archive.rb b/lib/oxygene/car_archive.rb index d527a5c..df6cfaa 100644 --- a/lib/oxygene/car_archive.rb +++ b/lib/oxygene/car_archive.rb @@ -25,6 +25,7 @@ module Oxygene @sections = [] @section_map = {} @buffer = StringIO.new(data) + @map_needs_update = false read_header(@buffer) end diff --git a/spec/base32_spec.rb b/spec/base32_spec.rb new file mode 100644 index 0000000..bc6eacd --- /dev/null +++ b/spec/base32_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +describe Oxygene::Base32 do + describe ".encode" do + it "should encode strings to lowercase base32 without padding" do + { + "" => "", + "f" => "my", + "fo" => "mzxq", + "foo" => "mzxw6", + "foob" => "mzxw6yq", + "fooba" => "mzxw6ytb", + "foobar" => "mzxw6ytboi" + }.each do |input, output| + Oxygene::Base32.encode(input).should == output + end + end + + it "should encode binary strings" do + data = "\x00\x01\xFE\xFF".b + + Oxygene::Base32.encode(data).should == "aaa757y" + end + + it "should not modify original string" do + data = "frozen".freeze + + Oxygene::Base32.encode(data) + end + + it "should start encoding at a given byte offset" do + Oxygene::Base32.encode("testsomething", 4).should == "onxw2zlunbuw4zy" + Oxygene::Base32.encode("something").should == "onxw2zlunbuw4zy" + end + + it "should add a prefix if passed" do + prefix = "base32:".freeze + + Oxygene::Base32.encode("teststringtotest", 0, prefix).should == "base32:orsxg5dtorzgs3thorxxizltoq" + end + + it "should combine a byte offset with a prefix" do + Oxygene::Base32.encode("discardfoobar", 7, "b").should == "bmzxw6ytboi" + end + + it "should return only the prefix when the offset is at the end" do + Oxygene::Base32.encode("foobar", 6, "b").should == "b" + end + end +end diff --git a/spec/car_archive_spec.rb b/spec/car_archive_spec.rb index bc4ec36..8822828 100644 --- a/spec/car_archive_spec.rb +++ b/spec/car_archive_spec.rb @@ -87,6 +87,14 @@ describe Oxygene::CARArchive do sections.last.cid.should == archive.roots.first end + it "should free the data buffer for GC cleanup" do + archive.instance_variable_get(:@buffer).should_not be_nil + + sections = archive.sections + + archive.instance_variable_get(:@buffer).should be_nil + end + it "should reject truncated sections" do truncated_data = fixture_data.byteslice(0, fixture_data.bytesize - 1) truncated_archive = Oxygene::CARArchive.new(truncated_data) @@ -133,6 +141,27 @@ describe Oxygene::CARArchive do expect { truncated_archive.sections }.to raise_error(Oxygene::DecodeError, /CID too short/) end + + it "should reject a non-canonically encoded CID prefix" do + cid_body = "\x00".b * 32 + dag_cbor_codec = "\x71".b + sha256_multihash = "\x12\x20".b + cid_body + canonical_cid = "\x00\x01".b + dag_cbor_codec + sha256_multihash + + header = CBOR.encode({ + "roots" => [CBOR::Tagged.new(42, canonical_cid)], + "version" => 1 + }) + + # 81 00 is a non-canonical but technically valid two-byte varint encoding of the CID version 1 + weird_version_tag = "\x81\x00".b + section = weird_version_tag + dag_cbor_codec + sha256_multihash + CBOR.encode({}) + + archive_data = [header.bytesize].pack("C") + header + [section.bytesize].pack("C") + section + archive = Oxygene::CARArchive.new(archive_data) + + expect { archive.sections }.to raise_error(Oxygene::UnsupportedError, "Non-canonical CID prefix") + end end describe "#section_with_cid" do @@ -143,10 +172,62 @@ describe Oxygene::CARArchive do body.should be_a(Hash) end - it "should return nil when the CID is not in the archive" do - missing_cid = Oxygene::CID.from_json("bafyreihsl77homddrramzhzdrgfneatthwnlz47ikzy4mezxz7kxndmjsm") + context "with return_body: true" do + it "should find a section with given CID and return its body" do + cid = Oxygene::CID.from_json(record_cid) + + body = archive.section_with_cid(cid, return_body: true) + body.should be_a(Hash) + end + + it "should return the body converted to ATProto JSON representation" do + root = archive.section_with_cid(archive.roots.first) + + root["did"].should == "did:plc:rnpkyqnmsw4ipey6eotbdnnf" + root["rev"].should == "3msigk4u5hz2z" + root["version"].should == 3 + root["prev"].should be_nil + root["data"].should == { "$link" => Oxygene::CID.from_json(data_cid) } + root["sig"].keys.should == ["$bytes"] + Base64.decode64(root["sig"]["$bytes"]).bytesize.should == 64 + end + end + + context "with return_body: false" do + it "should find a section with given CID and return the section object" do + cid = Oxygene::CID.from_json(record_cid) + + section = archive.section_with_cid(cid, return_body: false) + section.should be_a(Oxygene::CARSection) + section.cid.should == cid + end + end + + it "should also accept CID binary data as a string" do + cid = Oxygene::CID.from_json(record_cid) + + section = archive.section_with_cid(cid.data, return_body: false) + section.should_not be_nil + section.cid.should == cid + + section2 = archive.section_with_cid(cid.data, return_body: false) + section2.should equal(section) + end + + context "when the CID is not in the archive" do + let(:missing_cid) { Oxygene::CID.from_json("bafyreihsl77homddrramzhzdrgfneatthwnlz47ikzy4mezxz7kxndmjsm") } - archive.section_with_cid(missing_cid).should be_nil + it "should return nil" do + archive.section_with_cid(missing_cid).should be_nil + end + + it "should free the data buffer for GC cleanup" do + archive.instance_variable_get(:@buffer).should_not be_nil + + archive.section_with_cid(missing_cid) + + archive.instance_variable_get(:@buffer).should be_nil + end end it "should decode the firehose record body" do @@ -160,16 +241,161 @@ describe Oxygene::CARArchive do } end - it "should convert CID links and byte strings in section data" do - root = archive.section_with_cid(archive.roots.first) + context "with use_map: true" do + let(:early_cid) { Oxygene::CID.from_json("bafyreihglybf3ix6ctig27d53547wrc4zwcohkah76hq55nohacsdacfm4") } + + let(:loaded_sections) { archive.instance_variable_get(:@sections) } + let(:section_map) { archive.instance_variable_get(:@section_map) } + + def map_needs_update + archive.instance_variable_get(:@map_needs_update) + end + + it "should add lazily parsed sections to the map" do + section = archive.section_with_cid(early_cid, use_map: true, return_body: false) + section.should_not be_nil + + loaded_sections.length.should == 3 + section_map.values.should == loaded_sections + section_map[early_cid.data].should equal(section) + end + + context "if some sections were earlier parsed without being added to map" do + it "should update the map on next call with use_map" do + # loading some sections with use_map + s = archive.section_with_cid(early_cid, use_map: true) + s.should_not be_nil + + # loading some more without use_map + record_section = archive.section_with_cid(Oxygene::CID.from_json(record_cid), return_body: false) + record_section.should_not be_nil + + loaded_sections.length.should == 10 + section_map.length.should == 3 + map_needs_update.should == true + + # map should be updated and used for lookup + section = archive.section_with_cid(record_section.cid, use_map: true, return_body: false) + section.should equal(record_section) + + section_map.length.should == 10 + section_map[record_section.cid.data].should equal(record_section) + map_needs_update.should == false + end + end + + context "after a call to #sections loads all sections" do + it "should update the map on next call with use_map" do + # loading some sections with use_map + s = archive.section_with_cid(early_cid, use_map: true) + s.should_not be_nil + + # archive.sections loads the rest, without map + archive.sections + + loaded_sections.length.should == 11 + section_map.length.should == 3 + map_needs_update.should == true + + # map should be updated and used for lookup + root = archive.section_with_cid(archive.roots.first, use_map: true, return_body: false) + root.should_not be_nil + + section_map.length.should == 11 + section_map[archive.roots.first.data].should equal(root) + map_needs_update.should == false + end + + it "should not mark the map as dirty if sections were already all loaded" do + # load up to last section + s = archive.section_with_cid(Oxygene::CID.from_json(root_cid), use_map: true) + s.should_not be_nil + + # archive.sections finds eof + archive.sections + + map_needs_update.should == false + end + + it "should not mark the map as dirty on second call to sections" do + # loading some sections with use_map + s = archive.section_with_cid(early_cid, use_map: true) + s.should_not be_nil + + # archive.sections loads the rest, without map + archive.sections + + # force map refresh + s = archive.section_with_cid(early_cid, use_map: true) + s.should_not be_nil + + archive.sections + map_needs_update.should == false + end + end + + it "should also accept CID binary data as a string" do + cid = Oxygene::CID.from_json(record_cid) + + section = archive.section_with_cid(cid.data, use_map: true, return_body: false) + section.should_not be_nil + section.cid.should == cid + + section2 = archive.section_with_cid(cid.data, use_map: true, return_body: false) + section2.should equal(section) + end + end + + context "with use_map: false" do + let(:early_cid) { Oxygene::CID.from_json("bafyreihglybf3ix6ctig27d53547wrc4zwcohkah76hq55nohacsdacfm4") } + + let(:loaded_sections) { archive.instance_variable_get(:@sections) } + let(:section_map) { archive.instance_variable_get(:@section_map) } + + it "should not add parsed sections to the map" do + section = archive.section_with_cid(early_cid, use_map: false, return_body: false) + section.should_not be_nil + + loaded_sections.length.should == 3 + section_map.length.should == 0 + end + + it "should not use the map for lookup" do + section = archive.section_with_cid(early_cid, use_map: false, return_body: false) + section.should_not be_nil + + record_section = archive.section_with_cid(Oxygene::CID.from_json(record_cid), use_map: false, return_body: false) + record_section.should_not be_nil + + section2 = archive.section_with_cid(early_cid, use_map: false, return_body: false) + section2.should equal(section) + + loaded_sections.length.should == 10 + section_map.length.should == 0 + end + end + end + + describe ".make_bytes" do + it "should convert a binary string to a JSON representation with Base64 encoding" do + binary = "base64".b + + Oxygene::CARArchive.make_bytes(binary).should == { "$bytes" => "YmFzZTY0" } + end + + it "should include no '=' padding" do + binary = "\xEB\e8Z\xE1\xDC\x8F\xE1\xBA\xD3\xB9\xF4\xF7P\x89\x01".b + + Oxygene::CARArchive.make_bytes(binary).should == { "$bytes" => "6xs4WuHcj+G607n091CJAQ" } + end + + it "should remove intermediate newlines from longer Base64 data" do + data = "a".b * 64 + base64_string = Base64.encode64(data) + base64_string.chomp.should include("\n") - root["did"].should == "did:plc:rnpkyqnmsw4ipey6eotbdnnf" - root["rev"].should == "3msigk4u5hz2z" - root["version"].should == 3 - root["prev"].should be_nil - root["data"].should == { "$link" => Oxygene::CID.from_json(data_cid) } - root["sig"].keys.should == ["$bytes"] - Base64.decode64(root["sig"]["$bytes"]).bytesize.should == 64 + output = Oxygene::CARArchive.make_bytes(data) + output.should == { '$bytes' => base64_string.gsub(/\n/, '').gsub(/=+$/, '') } end end end diff --git a/spec/car_repo_spec.rb b/spec/car_repo_spec.rb index cca97e1..5756a64 100644 --- a/spec/car_repo_spec.rb +++ b/spec/car_repo_spec.rb @@ -26,5 +26,43 @@ describe Oxygene::CARRepo do profile["$type"].should == "app.bsky.actor.profile" profile["displayName"].should == "Bluesky" end + + context "given a starting CID" do + it "should start from a section with that CID" do + records = [] + starting_cid = Oxygene::CID.from_json("bafyreiajotznusc27wzjwm3li6okuvnmdstm6ymy6bk26jqauj4qidjqfe") + + repo.walk_all_nodes(starting_cid) do |key, cid| + records << key + end + + records.should == [ + 'app.bsky.feed.generator/thevids', + 'app.bsky.feed.generator/whats-hot', + 'app.bsky.feed.like/3jt6wh3ckcc2y', + 'app.bsky.feed.like/3jt6wh4b3tv2z', + 'app.bsky.feed.generator/with-friends', + ] + end + end + + context "given a starting CID as binary data string with 0 prefix" do + it "should start from a section with that CID" do + records = [] + starting_cid = Oxygene::CID.from_json("bafyreiajotznusc27wzjwm3li6okuvnmdstm6ymy6bk26jqauj4qidjqfe") + + repo.walk_all_nodes(starting_cid.data) do |key, cid| + records << key + end + + records.should == [ + 'app.bsky.feed.generator/thevids', + 'app.bsky.feed.generator/whats-hot', + 'app.bsky.feed.like/3jt6wh3ckcc2y', + 'app.bsky.feed.like/3jt6wh4b3tv2z', + 'app.bsky.feed.generator/with-friends', + ] + end + end end end diff --git a/spec/car_section_spec.rb b/spec/car_section_spec.rb new file mode 100644 index 0000000..a4f7d4a --- /dev/null +++ b/spec/car_section_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +describe Oxygene::CARSection do + cid_data = "\x00\x01\x71\x12\x20".b + ("\x11".b * 32) + tagged_cid = CBOR::Tagged.new(42, cid_data) + + body_object = { + "type" => "example", + "bytes" => "\x00\x01\x02\xFF".b, + "link" => tagged_cid + } + + encoded_data = CBOR.encode(body_object) + + let(:cid) { Oxygene::CID.new(cid_data, true, true) } + let(:section) { Oxygene::CARSection.new(cid, encoded_data) } + + describe "#data" do + it "should return the CBOR data that was passed to initializer" do + section.data.should == encoded_data + end + end + + describe "#decoded_body" do + it "should return decoded CBOR values without DAG-JSON conversion" do + body = section.decoded_body + body.should be_a(Hash) + + body['type'].should == "example" + body['bytes'].should == "\x00\x01\x02\xFF".b + body['link'].should be_a(CBOR::Tagged) + body['link'].value.should == cid_data + end + end + + describe "#json_body" do + it "should convert byte strings and CID tags to DAG-JSON values" do + body = section.json_body + body.should be_a(Hash) + + body['type'].should == "example" + body['bytes'].should == { "$bytes" => "AAEC/w" } + body['link'].should == { "$link" => Oxygene::CID.new(cid_data, true, true) } + end + + it "should not modify the value returned from #decoded_body" do + decoded_body = section.decoded_body + + section.json_body + + decoded_body['bytes'].should == "\x00\x01\x02\xFF".b + decoded_body['link'].should be_a(CBOR::Tagged) + end + end + + describe "#body" do + it "should be an alias for #json_body" do + section.body.should equal(section.json_body) + end + end +end diff --git a/spec/cid_spec.rb b/spec/cid_spec.rb index 7e95455..c0e9e63 100644 --- a/spec/cid_spec.rb +++ b/spec/cid_spec.rb @@ -4,15 +4,15 @@ describe Oxygene::CID do test_cids = [ [ "bafyreihsl77homddrramzhzdrgfneatthwnlz47ikzy4mezxz7kxndmjsm", - "\x01q\x12 \xF2_\xFEw0c\x8C@\xCC\x9F#\x89\x8A\xD2\x02s=\x9A\xBC\xF3\xE8Vq\xC6\x137\xCF\xD5v\x8D\x89\x93".b + "\x01q\x12 \xF2_\xFEw0c\x8C@\xCC\x9F#\x89\x8A\xD2\x02s=\x9A\xBC\xF3\xE8Vq\xC6\x137\xCF\xD5v\x8D\x89\x93".b.freeze ], [ "bafyreigpanzjuqvy3uzqwhbymw7vok2ue5v5cp7dhf2bgnz3wg53cuumn4", - "\x01q\x12 \xCF\x03r\x9AB\xB8\xDD3\v\x1C8e\xBFW+T'k\xD1?\xE39t\x137;\xB1\xBB\xB1R\x8Co".b + "\x01q\x12 \xCF\x03r\x9AB\xB8\xDD3\v\x1C8e\xBFW+T'k\xD1?\xE39t\x137;\xB1\xBB\xB1R\x8Co".b.freeze ], [ "bafyreia2rdzvv7fjlpilxfzwvx7pp4afj3h3aphdmk4ankikmnxqt2vtjy", - "\x01q\x12 \x1A\x88\xF3Z\xFC\xA9[\xD0\xBB\x976\xAD\xFE\xF7\xF0\x05N\xCF\xB0<\xE3b\xB8\x06\xA9\nco\t\xEA\xB3N".b + "\x01q\x12 \x1A\x88\xF3Z\xFC\xA9[\xD0\xBB\x976\xAD\xFE\xF7\xF0\x05N\xCF\xB0<\xE3b\xB8\x06\xA9\nco\t\xEA\xB3N".b.freeze ] ] @@ -33,6 +33,25 @@ describe Oxygene::CID do expect { Oxygene::CID.from_cbor_tag(tag) }.to raise_error(Oxygene::DecodeError) end + + it "should store the CID data with the 0 prefix" do + data = test_cids.first.last + tag = CBOR::Tagged.new(42, ("\x00".b + data).freeze) + + cid = Oxygene::CID.from_cbor_tag(tag) + cid.instance_variable_get('@binary_data').should == tag.value + end + + it "should not generate JSON form until needed" do + data = test_cids.first.last + tag = CBOR::Tagged.new(42, ("\x00".b + data).freeze) + + cid = Oxygene::CID.from_cbor_tag(tag) + cid.instance_variable_get('@json_form').should be_nil + + cid.to_s + cid.instance_variable_get('@json_form').should_not be_nil + end end describe ".from_json" do @@ -61,23 +80,123 @@ describe Oxygene::CID do expect { Oxygene::CID.from_json(string) }.to raise_error(Oxygene::DecodeError) end + + it "should not generate binary form until needed" do + string = test_cids.first.first + + cid = Oxygene::CID.from_json(string) + cid.instance_variable_get('@binary_data').should be_nil + + cid.data + cid.instance_variable_get('@binary_data').should_not be_nil + end end describe "CID.new" do - it "should accept binary data without the \\x00 prefix" do + it "should accept binary data including the prefix" do + string, data = test_cids.first + prefixed_data = "\x00".b + data + cid = Oxygene::CID.new(prefixed_data, true, true) + + cid.data.should == prefixed_data + cid.json_form.should == string + end + + it "should accept binary data explicitly marked as not including the prefix" do + string, data = test_cids.first + cid = Oxygene::CID.new(data, true, false) + + cid.data.should == "\x00".b + data + cid.json_form.should == string + end + + it "should treat binary data as not including the prefix by default" do + string, data = test_cids.first + cid = Oxygene::CID.new(data, true) + + cid.data.should == "\x00".b + data + cid.json_form.should == string + end + + it "should treat data as binary without prefix if only one argument is passed" do test_cids.each do |string, data| cid = Oxygene::CID.new(data) + cid.data.should == "\x00".b + data - cid.should == Oxygene::CID.from_json(string) + cid.json_form.should == string end end + + it "should accept JSON form input when second argument is false" do + string, data = test_cids.first + + [nil, true].each do |includes_prefix| + cid = Oxygene::CID.new(string, false, includes_prefix) + + cid.json_form.should == string + cid.data.should == "\x00".b + data + end + end + + it "should reject JSON data with includes_prefix = false" do + string = test_cids.first.first + + expect { Oxygene::CID.new(string, false, false) }.to raise_error(ArgumentError) + end + + it "should reject nil data" do + expect { Oxygene::CID.new(nil) }.to raise_error(ArgumentError, "Data cannot be nil") + expect { Oxygene::CID.new(nil, true) }.to raise_error(ArgumentError, "Data cannot be nil") + expect { Oxygene::CID.new(nil, false) }.to raise_error(ArgumentError, "Data cannot be nil") + end + end + + describe "#data" do + it "should return input data for binary CIDs with prefix" do + string, data = test_cids.first + + cid = Oxygene::CID.new(data, true, true) + cid.data.should equal(data) + end + + it "should return input data with prefix added for binary CIDs without prefix" do + string, data = test_cids.first + + cid = Oxygene::CID.new(data, true) + cid.data.should == "\x00".b + data + end + + it "should decode JSON to prefixed binary data for JSON CIDs" do + string, data = test_cids.first + + cid = Oxygene::CID.new(string, false) + cid.data.should == "\x00".b + data + end + end + + describe "#json_form" do + it "should encode binary data without including the binary prefix" do + string, data = test_cids.first + + cid = Oxygene::CID.new("\x00".b + data, true, true) + cid.json_form.should == string + end + + it "should return JSON input unchanged" do + string = test_cids.first.first + + cid = Oxygene::CID.new(string, false) + cid.json_form.should equal(string) + end end describe "#to_s" do it "should return the JSON form of the CID" do test_cids.each do |string, data| cid = Oxygene::CID.new(data) + cid.to_s.should == string + cid = Oxygene::CID.new(string, false) cid.to_s.should == string end end @@ -102,5 +221,71 @@ describe Oxygene::CID do (first == different).should == false (first == first.data).should == false end + + context "for two binary CIDs" do + it "should only look at their binary forms" do + first = Oxygene::CID.new(test_cids[0][1]) + first2 = Oxygene::CID.new(test_cids[0][1]) + second = Oxygene::CID.new(test_cids[1][1]) + + (first == first2).should == true + (first == second).should == false + + first.instance_variable_get('@json_form').should be_nil + first2.instance_variable_get('@json_form').should be_nil + second.instance_variable_get('@json_form').should be_nil + end + end + + context "for two JSON CIDs" do + it "should only look at their JSON forms" do + first = Oxygene::CID.new(test_cids[0][0], false) + first2 = Oxygene::CID.new(test_cids[0][0], false) + second = Oxygene::CID.new(test_cids[1][0], false) + + (first == first2).should == true + (first == second).should == false + + first.instance_variable_get('@binary_data').should be_nil + first2.instance_variable_get('@binary_data').should be_nil + second.instance_variable_get('@binary_data').should be_nil + end + end + + context "for one binary and one JSON CID" do + it "should compare their binary forms" do + first_j = Oxygene::CID.new(test_cids[0][0], false) + first2_b = Oxygene::CID.new(test_cids[0][1]) + second_b = Oxygene::CID.new(test_cids[1][1]) + + (first_j == first2_b).should == true + (first_j == second_b).should == false + + first_j.instance_variable_get('@binary_data').should_not be_nil + first2_b.instance_variable_get('@json_form').should be_nil + second_b.instance_variable_get('@json_form').should be_nil + + third_b = Oxygene::CID.new(test_cids[0][1]) + third2_j = Oxygene::CID.new(test_cids[0][0], false) + fourth_j = Oxygene::CID.new(test_cids[1][0], false) + + (third_b == third2_j).should == true + (third_b == fourth_j).should == false + + third_b.instance_variable_get('@json_form').should be_nil + third2_j.instance_variable_get('@binary_data').should_not be_nil + fourth_j.instance_variable_get('@binary_data').should_not be_nil + end + end + end + + describe "#hash" do + it "should use hash of the binary data" do + binary_cid = Oxygene::CID.new(test_cids[0][1]) + json_cid = Oxygene::CID.new(test_cids[0][0], false) + + binary_cid.hash.should == binary_cid.data.hash + json_cid.hash.should == json_cid.data.hash + end end end diff --git a/spec/extensions_spec.rb b/spec/extensions_spec.rb new file mode 100644 index 0000000..a2cfe8e --- /dev/null +++ b/spec/extensions_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +using Oxygene::Extensions + +describe CBOR do + def encoded_sequence(items) + items.map { |item| CBOR.encode(item) }.join + end + + describe ".decode_sequence" do + it "should decode a sequence with no items" do + CBOR.decode_sequence("".b).should == [] + end + + it "should decode a sequence with one item" do + items = [1] + + CBOR.decode_sequence(encoded_sequence(items)).should == items + end + + it "should decode a sequence with two items" do + items = [1, "two"] + + CBOR.decode_sequence(encoded_sequence(items)).should == items + end + + it "should decode a sequence with three items" do + items = [1, "two", { "three" => 3 }] + + CBOR.decode_sequence(encoded_sequence(items)).should == items + end + end +end