├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── shard.lock ├── shard.yml ├── spec ├── bindata_array_spec.cr ├── bindata_asn1_spec.cr ├── bindata_enum_spec.cr ├── bindata_group_spec.cr ├── bindata_inheritance_spec.cr ├── bindata_remaining_bytes_spec.cr ├── bindata_spec.cr ├── bindata_verify_spec.cr ├── bitfield_spec.cr ├── callback_spec.cr ├── helper.cr └── string_encoding_spec.cr └── src ├── bindata.cr └── bindata ├── asn1.cr ├── asn1 ├── data_types.cr ├── identifier.cr └── length.cr ├── bitfield.cr └── exceptions.cr /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "0 6 * * 1" 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [ubuntu-latest] 14 | crystal: 15 | - latest 16 | - nightly 17 | runs-on: ${{ matrix.os }} 18 | container: crystallang/crystal:${{ matrix.crystal }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Add encodings 22 | run: | 23 | apt-get update 24 | apt-get install -y locales 25 | sed -i 's/^# \(.*\)$/\1/' /etc/locale.gen 26 | locale-gen 27 | - name: Install dependencies 28 | run: shards install --ignore-crystal-version --skip-postinstall --skip-executables 29 | - name: Format 30 | run: crystal tool format --check 31 | - name: Run tests 32 | run: crystal spec -v --error-trace 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | lib 3 | .crystal 4 | .shards 5 | app 6 | *.dwarf 7 | *.DS_Store 8 | bin 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | install: 3 | - shards install 4 | script: 5 | - crystal spec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 ACA Projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BinData - Parsing Binary Data in Crystal Lang 2 | 3 | BinData provides a declarative way to read and write structured binary data. 4 | 5 | This means the programmer specifies what the format of the binary data is, and BinData works out how to read and write data in this format. It is an easier (and more readable) alternative. 6 | 7 | [![Build Status](https://github.com/spider-gazelle/bindata/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/spider-gazelle/bindata/actions/workflows/CI.yml) 8 | 9 | ## Usage 10 | 11 | Firstly, it's recommended that you specify the datas endian. 12 | 13 | ```crystal 14 | class Header < BinData 15 | endian big 16 | end 17 | ``` 18 | 19 | Then you can specify the structures fields. There are a few different field types: 20 | 21 | 1. Core types 22 | * `UInt8` to `UInt128` values respectively 23 | * You can use endian-aware types to mix endianess: `, endian: IO::ByteFormat::LittleEndian` 24 | 2. Custom types 25 | * anything that is [io serialisable](https://crystal-lang.org/api/0.27.2/IO.html#write_bytes%28object%2Cformat%3AIO%3A%3AByteFormat%3DIO%3A%3AByteFormat%3A%3ASystemEndian%29-instance-method) 26 | 3. Bit Fields 27 | * These are a group of fields who values are defined by the number of bits used to represent their value 28 | * The total number of bits in a bit field must be divisible by 8 29 | 4. Groups 30 | * These are embedded BinData class with access to the parent fields 31 | * Useful when a group of fields are related or optional 32 | 5. Enums 33 | 6. Bools 34 | 6. Arrays and Sets (fixed size and dynamic) 35 | 36 | 37 | ### Examples 38 | 39 | see the [spec helper](https://github.com/spider-gazelle/bindata/blob/master/spec/helper.cr) for all possible manipulations 40 | 41 | ```crystal 42 | enum Inputs 43 | VGA 44 | HDMI 45 | HDMI2 46 | end 47 | 48 | class Packet < BinData 49 | endian big 50 | 51 | # Default sets the value at initialisation. 52 | field start : UInt8 = 0xFF_u8 53 | 54 | # Value procs assign these values before writing to an IO, overwriting any 55 | # existing value 56 | field size : UInt16, value: ->{ text.bytesize + 1 } 57 | 58 | # String fields without a length use `\0` null byte termination 59 | # Length is being calculated by the size field above 60 | field text : String, length: ->{ size - 1 } 61 | 62 | # Bit fields should only be used when one or more fields are not byte aligned 63 | # The sum of the bits in a bit field must be divisible by 8 64 | bit_field do 65 | # a bits value can be between 1 and 128 bits long 66 | bits 5, reserved 67 | 68 | # Bool values are a single bit 69 | bool set_input = false 70 | 71 | # This enum is represented by 2 bits 72 | bits 2, input : Inputs = Inputs::HDMI2 73 | end 74 | 75 | # isolated namespace 76 | group :extended, onlyif: ->{ start == 0xFF } do 77 | field start : UInt8 = 0xFF_u8 78 | 79 | # Supports custom objects as long as they implement `from_io` 80 | field header : ExtHeader = ExtHeader.new 81 | end 82 | 83 | # optionally read the remaining bytes out of io 84 | remaining_bytes :rest 85 | end 86 | ``` 87 | 88 | The object above can then be accessed like any other object 89 | 90 | ```crystal 91 | pack = io.read_bytes(Packet) 92 | pack.size # => 12 93 | pack.text # => "hello world" 94 | pack.input # => Inputs::HDMI 95 | pack.set_input # => true 96 | pack.extended.start # => 255 97 | ``` 98 | 99 | Additionally, BinData fields support a `verify` proc, which allows data to be verified while reading and writing io. 100 | 101 | ```crystal 102 | class VerifyData < BinData 103 | endian big 104 | 105 | field size : UInt8 106 | field bytes : Bytes, length: ->{ size } 107 | field checksum : UInt8, verify: ->{ checksum == bytes.reduce(0) { |acc, i| acc + i } } 108 | end 109 | ``` 110 | 111 | If the `verify` proc returns `false`, a `BinData::VerificationException` is raised with a message matching the following format. 112 | 113 | ``` 114 | Failed to verify reading basic at VerifyData.checksum 115 | ``` 116 | 117 | Inheritance is also supported 118 | 119 | ## Callbacks 120 | 121 | Callbacks can helpful for providing accessors for simplified representations of the data. 122 | 123 | ```crystal 124 | class CallbackTest < BinData 125 | endian little 126 | 127 | field integer : UInt8 128 | 129 | property external_representation : UInt16 = 0 130 | 131 | before_serialize { self.integer = (external_representation // 2).to_u8 } 132 | after_deserialize { self.external_representation = integer.to_u16 * 2_u16 } 133 | end 134 | ``` 135 | 136 | ## ASN.1 Helpers 137 | 138 | Included in this library are helpers for decoding and writing ASN.1 data, such as those used in SNMP and LDAP 139 | 140 | ```crystal 141 | require "bindata/asn1" 142 | 143 | # Build an object 144 | ber = ASN1::BER.new 145 | ber.tag_number = ASN1::BER::UniversalTags::Integer 146 | ber.payload = Bytes[1] 147 | 148 | # Write it to an IO: 149 | io.write_bytes(ber) 150 | 151 | # Read data out of an IO: 152 | ber = io.read_bytes(ASN1::BER) 153 | ber.tag_class # => ASN1::BER::TagClass::Universal 154 | 155 | ``` 156 | 157 | ## Real World Examples 158 | 159 | * ASN.1 160 | * https://github.com/crystal-community/jwt/blob/master/src/jwt.cr#L251 161 | * https://github.com/spider-gazelle/crystal-ldap 162 | * enums and bit fields 163 | * https://github.com/spider-gazelle/knx/blob/master/src/knx/cemi.cr#L195 164 | * variable sized arrays 165 | * https://github.com/spider-gazelle/crystal-bacnet/blob/master/src/bacnet/virtual_link_control/secure_bvlci.cr#L54 166 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | shards: 3 | ameba: 4 | git: https://github.com/veelenga/ameba.git 5 | version: 1.6.1 6 | 7 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: bindata 2 | version: 2.1.0 3 | crystal: ">= 1.0.0" 4 | 5 | development_dependencies: 6 | ameba: 7 | github: veelenga/ameba 8 | -------------------------------------------------------------------------------- /spec/bindata_array_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "should parse an object with an array from an IO" do 5 | io = IO::Memory.new 6 | io.write_byte(2) 7 | io.write_bytes 0x0F09_i16, IO::ByteFormat::BigEndian 8 | io.write_bytes 0x0F09_i16, IO::ByteFormat::BigEndian 9 | io.write_byte(0) 10 | io.rewind 11 | 12 | r = io.read_bytes(ArrayData) 13 | r.flen.should eq(2_u8) 14 | r.first.should eq([0x0F09_u16, 0x0F09_u16]) 15 | r.slen.should eq(0_u8) 16 | r.second.should eq([] of Int8) 17 | end 18 | 19 | it "should write an object with an array to an IO" do 20 | io = IO::Memory.new 21 | io.write_byte(2) 22 | io.write_bytes 0x0F09_i16, IO::ByteFormat::BigEndian 23 | io.write_bytes 0x0F09_i16, IO::ByteFormat::BigEndian 24 | io.write_byte(0) 25 | io.rewind 26 | 27 | r = ArrayData.new 28 | r.first = [0x0F09_i16, 0x0F09_i16] 29 | io2 = IO::Memory.new 30 | r.write(io2) 31 | io2.rewind 32 | io2.to_slice.should eq(io.to_slice) 33 | end 34 | 35 | it "should read and write a variably sized array" do 36 | io = IO::Memory.new 37 | io.write_byte(5) 38 | io.write_byte(4) 39 | io.write_byte(3) 40 | io.write_byte(2) 41 | io.write_byte(1) 42 | io.rewind 43 | 44 | # Test read 45 | r = io.read_bytes(VariableArrayData) 46 | r.total_size.should eq(5) 47 | r.test.should eq([4_u8, 3_u8, 2_u8]) 48 | r.afterdata.should eq(1) 49 | 50 | # test write 51 | io2 = IO::Memory.new 52 | r.write(io2) 53 | io2.to_slice.should eq(io.to_slice) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/bindata_asn1_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe ASN1 do 4 | it "should parse basic universal BER Objects" do 5 | io = IO::Memory.new(Bytes[2, 1, 1]) 6 | ber = io.read_bytes(ASN1::BER) 7 | ber.inspect 8 | 9 | ber.tag_class.should eq(ASN1::BER::TagClass::Universal) 10 | ber.constructed.should eq(false) 11 | ber.tag_number.should eq(2) 12 | ber.extended?.should eq(nil) 13 | ber.size.should eq(1) 14 | ber.payload.should eq(Bytes[1]) 15 | end 16 | 17 | it "should be able to write basic universal BER Objects" do 18 | goal = Bytes[2, 1, 1] 19 | ber = ASN1::BER.new 20 | ber.payload = Bytes[1] 21 | ber.tag_number = ASN1::BER::UniversalTags::Integer 22 | 23 | io = IO::Memory.new 24 | io.write_bytes(ber) 25 | io.rewind 26 | 27 | io.to_slice.should eq(goal) 28 | end 29 | 30 | it "should be able to read and write children" do 31 | b = Bytes[48, 129, 139, 2, 1, 0, 4, 11, 53, 114, 78, 84, 103, 33, 112, 109, 49, 99, 107, 164, 121, 6, 8, 43, 6, 1, 6, 3, 1, 1, 5, 64, 4, 10, 230, 254, 28, 2, 1, 3, 2, 1, 0, 67, 4, 14, 162, 200, 72, 48, 91, 48, 15, 6, 10, 43, 6, 1, 2, 1, 2, 2, 1, 1, 26, 2, 1, 26, 48, 35, 6, 10, 43, 6, 1, 2, 1, 2, 2, 1, 2, 26, 4, 21, 71, 105, 103, 97, 98, 105, 116, 69, 116, 104, 101, 114, 110, 101, 116, 49, 47, 48, 47, 49, 57, 48, 15, 6, 10, 43, 6, 1, 2, 1, 2, 2, 1, 3, 26, 2, 1, 6, 48, 18, 6, 12, 43, 6, 1, 4, 1, 9, 2, 2, 1, 1, 20, 26, 4, 2, 117, 112] 32 | io = IO::Memory.new(b) 33 | orig = io.read_bytes(ASN1::BER) 34 | children = orig.children 35 | children.size.should eq(3) 36 | 37 | io2 = IO::Memory.new 38 | ber = ASN1::BER.new 39 | ber.tag_number = ASN1::BER::UniversalTags::Sequence 40 | ber.children = children 41 | ber.write(io2) 42 | 43 | ber.size.should eq(orig.size) 44 | io2.to_slice.should eq(b) 45 | end 46 | 47 | it "should be able to read Object Identifiers" do 48 | b = Bytes[6, 8, 43, 6, 1, 6, 3, 1, 1, 5] 49 | io = IO::Memory.new(b) 50 | io.read_bytes(ASN1::BER).get_object_id.should eq("1.3.6.1.6.3.1.1.5") 51 | 52 | b = Bytes[6, 9, 0x2b, 6, 1, 4, 1, 0x82, 0x37, 0x15, 0x14] 53 | io = IO::Memory.new(b) 54 | io.read_bytes(ASN1::BER).get_object_id.should eq("1.3.6.1.4.1.311.21.20") 55 | end 56 | 57 | it "should be able to write Object Identifiers" do 58 | b = Bytes[6, 9, 0x2b, 6, 1, 4, 1, 0x82, 0x37, 0x15, 0x14] 59 | io = IO::Memory.new 60 | 61 | test = ASN1::BER.new 62 | test.set_object_id "1.3.6.1.4.1.311.21.20" 63 | test.tag.should eq(ASN1::BER::UniversalTags::ObjectIdentifier) 64 | 65 | io.write_bytes(test) 66 | io.to_slice.should eq(b) 67 | end 68 | 69 | it "should be able to read UTF8 strings" do 70 | b = Bytes[0x0c, 0x07, 0x63, 0x65, 0x72, 0x74, 0x72, 0x65, 0x71] 71 | io = IO::Memory.new(b) 72 | io.read_bytes(ASN1::BER).get_string.should eq("certreq") 73 | end 74 | 75 | it "should be able to write UTF8 strings" do 76 | b = Bytes[0x0c, 0x07, 0x63, 0x65, 0x72, 0x74, 0x72, 0x65, 0x71] 77 | 78 | io = IO::Memory.new 79 | test = ASN1::BER.new 80 | test.set_string "certreq" 81 | 82 | io.write_bytes(test) 83 | io.to_slice.should eq(b) 84 | end 85 | 86 | it "should be able to read Bools" do 87 | b = Bytes[0x01, 0x01, 0x0] 88 | io = IO::Memory.new(b) 89 | io.read_bytes(ASN1::BER).get_boolean.should eq(false) 90 | 91 | b = Bytes[0x01, 0x01, 0xFF] 92 | io = IO::Memory.new(b) 93 | io.read_bytes(ASN1::BER).get_boolean.should eq(true) 94 | end 95 | 96 | it "should be able to write Bools" do 97 | b = Bytes[0x01, 0x01, 0xFF] 98 | 99 | io = IO::Memory.new 100 | test = ASN1::BER.new 101 | test.set_boolean true 102 | 103 | io.write_bytes(test) 104 | io.to_slice.should eq(b) 105 | 106 | b = Bytes[0x01, 0x01, 0x00] 107 | 108 | io = IO::Memory.new 109 | test = ASN1::BER.new 110 | test.set_boolean false 111 | 112 | io.write_bytes(test) 113 | io.to_slice.should eq(b) 114 | end 115 | 116 | it "should be able to read Integers" do 117 | b = Bytes[0x02, 0x01, 0x5] 118 | io = IO::Memory.new(b) 119 | io.read_bytes(ASN1::BER).get_integer.should eq(5) 120 | 121 | b = Bytes[0x02, 0x02, 0x5, 0x0] 122 | io = IO::Memory.new(b) 123 | io.read_bytes(ASN1::BER).get_integer.should eq(0x500) 124 | 125 | b = Bytes[0x02, 0x01, 0xFB] 126 | io = IO::Memory.new(b) 127 | io.read_bytes(ASN1::BER).get_integer.should eq(-5) 128 | 129 | b = Bytes[0x02, 0x02, 0x00, 0xFB] 130 | io = IO::Memory.new(b) 131 | io.read_bytes(ASN1::BER).get_integer.should eq(0xFB) 132 | 133 | b = Bytes[0x02, 0x02, 0xFB, 0x00] 134 | io = IO::Memory.new(b) 135 | io.read_bytes(ASN1::BER).get_integer.should eq(-0x500) 136 | 137 | b = Bytes[0x02, 0x02, 0xFA, 0xFE] 138 | io = IO::Memory.new(b) 139 | io.read_bytes(ASN1::BER).get_integer.should eq(-0x502) 140 | end 141 | 142 | it "should be able to write Integers" do 143 | b = Bytes[0x02, 0x02, 0x5, 0x0] 144 | io = IO::Memory.new 145 | test = ASN1::BER.new 146 | test.set_integer 0x500 147 | io.write_bytes(test) 148 | io.to_slice.should eq(b) 149 | 150 | b = Bytes[0x02, 0x01, 0xFB] 151 | io = IO::Memory.new 152 | test = ASN1::BER.new 153 | test.set_integer -5 154 | io.write_bytes(test) 155 | io.to_slice.should eq(b) 156 | 157 | b = Bytes[0x02, 0x02, 0xFB, 0x00] 158 | io = IO::Memory.new 159 | test = ASN1::BER.new 160 | test.set_integer -0x500 161 | io.write_bytes(test) 162 | io.to_slice.should eq(b) 163 | 164 | b = Bytes[0x02, 0x02, 0xFA, 0xFE] 165 | io = IO::Memory.new 166 | test = ASN1::BER.new 167 | test.set_integer -0x502 168 | io.write_bytes(test) 169 | io.to_slice.should eq(b) 170 | 171 | # Positive integers can't start with 0xff 172 | b = Bytes[0x02, 0x03, 0, 255, 227] 173 | io = IO::Memory.new 174 | test = ASN1::BER.new 175 | test.set_integer 65507 176 | io.write_bytes(test) 177 | io.to_slice.should eq(b) 178 | end 179 | 180 | it "should be able to get a Bitstring" do 181 | b = Bytes[0x03, 0x02, 0x0, 0x1] 182 | io = IO::Memory.new(b) 183 | io.read_bytes(ASN1::BER).get_bitstring.should eq(Bytes[0x1]) 184 | 185 | # Not implemented: 186 | # b = Bytes[0x03, 0x02, 0x4, 0x0, 0xF0] 187 | # io = IO::Memory.new(b) 188 | # io.read_bytes(ASN1::BER).get_bitstring.should eq(Bytes[0x0, 0xF]) 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /spec/bindata_enum_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "should parse an object with an enum from an IO" do 5 | io = IO::Memory.new 6 | io.write_byte(0) 7 | io.write_bytes 2_u16, IO::ByteFormat::BigEndian 8 | io.write_byte(0) 9 | io.write_byte(0) 10 | io.rewind 11 | 12 | r = io.read_bytes(EnumData) 13 | r.start.should eq(0_u8) 14 | r.inputs.should eq(EnumData::Inputs::HDMI2) 15 | r.input.should eq(EnumData::Inputs::VGA) 16 | r.end.should eq(0_u8) 17 | end 18 | 19 | it "should write an object with an enum to an IO" do 20 | io = IO::Memory.new 21 | io.write_byte(0) 22 | io.write_bytes 1_u16, IO::ByteFormat::BigEndian 23 | io.write_byte(5) 24 | io.write_byte(0) 25 | io.rewind 26 | 27 | r = EnumData.new 28 | r.input = EnumData::Inputs::HDMI 29 | r.enabled = true 30 | io2 = IO::Memory.new 31 | r.write(io2) 32 | io2.rewind 33 | 34 | io2.to_slice.should eq(io.to_slice) 35 | end 36 | 37 | it "should work with differently types" do 38 | io = IO::Memory.new 39 | io.write_bytes 0x0111_u16, IO::ByteFormat::BigEndian 40 | io.rewind 41 | 42 | p = io.read_bytes(Packet) 43 | p.type.should eq(Packet::Type::Reply) 44 | end 45 | end 46 | 47 | class Packet < BinData 48 | endian big 49 | 50 | enum Type : UInt16 51 | Command = 0x0100 52 | Inquiry = 0x0110 53 | Reply = 0x0111 54 | end 55 | 56 | field type : Type = Type::Command 57 | end 58 | -------------------------------------------------------------------------------- /spec/bindata_group_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "should parse a complex object from an IO" do 5 | io = IO::Memory.new 6 | io.write_byte(0) 7 | io.write_bytes 5 8 | io.write "hello".to_slice 9 | io.write_byte(1) 10 | io.write_byte(3) 11 | io.write_byte(0) 12 | io.rewind 13 | 14 | r = Wow.new 15 | r.read io 16 | r.start.should eq(0_u8) 17 | r.head.size.should eq(5) 18 | r.head.name.should eq("hello") 19 | r.body.start.should eq(1_u8) 20 | r.body.end.should eq(3_u8) 21 | r.end.should eq(0_u8) 22 | end 23 | 24 | it "should parse a very complex object from an IO" do 25 | io = IO::Memory.new 26 | io.write_byte(0) 27 | io.write_bytes 0 28 | io.write_byte(0) 29 | io.rewind 30 | 31 | r = Wow.new 32 | r.read io 33 | r.start.should eq(0_u8) 34 | r.head.size.should eq(0) 35 | r.head.name.should eq("") 36 | r.body.start.should eq(0) 37 | r.body.end.should eq(0) 38 | r.end.should eq(0_u8) 39 | end 40 | 41 | it "should write a complex object to an IO" do 42 | io = IO::Memory.new 43 | io.write_byte(0) 44 | io.write_bytes 8 45 | io.write "whatwhat".to_slice 46 | io.write_byte(1) 47 | io.write_byte(3) 48 | io.write_byte(0) 49 | io.rewind 50 | 51 | r = Wow.new 52 | r.head = Header.new 53 | r.head.name = "whatwhat" 54 | 55 | io2 = IO::Memory.new 56 | r.write(io2) 57 | io2.rewind 58 | 59 | io2.to_slice.should eq(io.to_slice) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/bindata_inheritance_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "should parse an inherited class object" do 5 | io = IO::Memory.new 6 | io.write_byte(0) 7 | io.write_bytes 2_u16, IO::ByteFormat::BigEndian 8 | io.write_byte(0) 9 | io.write_byte(0) 10 | io.write_byte(1) 11 | io.rewind 12 | 13 | r = io.read_bytes(Inherited) 14 | r.start.should eq(0_u8) 15 | r.inputs.should eq(Inherited::Inputs::HDMI2) 16 | r.input.should eq(Inherited::Inputs::VGA) 17 | r.end.should eq(0_u8) 18 | r.other_low.should eq(1_u8) 19 | end 20 | 21 | it "should write an inherited class to an IO" do 22 | io = IO::Memory.new 23 | io.write_byte(0) 24 | io.write_bytes 1_u16, IO::ByteFormat::BigEndian 25 | io.write_byte(13) 26 | io.write_byte(0) 27 | io.write_byte(1) 28 | io.rewind 29 | 30 | r = Inherited.new 31 | r.input = Inherited::Inputs::HDMI 32 | r.enabled = true 33 | r.reserved = 1_u8 34 | r.other_low = 1_u8 35 | io2 = IO::Memory.new 36 | r.write(io2) 37 | io2.rewind 38 | 39 | io2.to_slice.should eq(io.to_slice) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/bindata_remaining_bytes_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "reads the remaining bytes into Bytes" do 5 | io = IO::Memory.new 6 | io.write_byte 0x02 7 | rest = Bytes.new 4, &.to_u8 8 | io.write rest 9 | io.rewind 10 | 11 | r = io.read_bytes RemainingBytesData 12 | r.first.should eq 0x02 13 | r.rest.should eq rest 14 | end 15 | 16 | it "reads the remaining bytes into empty Bytes if none remain" do 17 | io = IO::Memory.new 18 | io.write_byte 0x02 19 | io.write Bytes.new 0 20 | io.rewind 21 | 22 | r = io.read_bytes RemainingBytesData 23 | r.first.should eq 0x02 24 | r.rest.size.should eq 0 25 | end 26 | 27 | it "reads the rest even if io has already been partially read" do 28 | io = IO::Memory.new 29 | io.write Bytes.new 10 30 | io.write_byte 0x02 31 | rest = Bytes.new 4, &.to_u8 32 | io.write rest 33 | io.rewind 34 | 35 | io.read Bytes.new 10 36 | r = io.read_bytes RemainingBytesData 37 | r.first.should eq 0x02 38 | r.rest.should eq rest 39 | end 40 | 41 | it "runs verification as expected" do 42 | io = IO::Memory.new 43 | io.write_byte 0x02 44 | io.write Bytes.new 15 45 | io.rewind 46 | 47 | ex = expect_raises BinData::ReadingVerificationException, "Failed to verify reading bytes at RemainingBytesData.rest" do 48 | io.read_bytes RemainingBytesData 49 | end 50 | ex.klass.should eq("RemainingBytesData") 51 | ex.field.should eq("rest") 52 | ex.field_type.should eq("bytes") 53 | end 54 | 55 | it "runs verification as expected while writing" do 56 | r = RemainingBytesData.new 57 | r.first = 0x02 58 | r.rest = Bytes.new 15 59 | io2 = IO::Memory.new 60 | 61 | ex = expect_raises BinData::WritingVerificationException, "Failed to verify writing bytes at RemainingBytesData.rest" do 62 | r.write io2 63 | end 64 | ex.klass.should eq("RemainingBytesData") 65 | ex.field.should eq("rest") 66 | ex.field_type.should eq("bytes") 67 | end 68 | 69 | it "abides by onlyif as expected" do 70 | io = IO::Memory.new 71 | io.write_byte 0x01 72 | io.write Bytes.new 4 73 | io.rewind 74 | 75 | r = io.read_bytes RemainingBytesData 76 | r.first.should eq 0x01 77 | r.rest.size.should eq 0 78 | io.pos.should eq 1 79 | end 80 | 81 | it "abides by onlyif as expected while writing" do 82 | r = RemainingBytesData.new 83 | r.first = 0x01 84 | r.rest = Bytes.new 4 85 | io2 = IO::Memory.new 86 | r.write io2 87 | io2.rewind 88 | 89 | io2.size.should eq 1 90 | end 91 | 92 | it "writes remaining bytes" do 93 | io = IO::Memory.new 94 | io.write_byte 0x02 95 | rest = Bytes.new 4, &.to_u8 96 | io.write rest 97 | io.rewind 98 | 99 | r = RemainingBytesData.new 100 | r.first = 0x02 101 | r.rest = rest 102 | 103 | io2 = IO::Memory.new 104 | r.write io2 105 | io2.rewind 106 | 107 | io2.to_slice.should eq(io.to_slice) 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/bindata_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "should parse an object from a Slice" do 5 | r = Header.new 6 | r.name = "foo" 7 | slice = r.to_slice 8 | Header.from_slice(slice).name.should eq "foo" 9 | end 10 | 11 | it "should parse an object from an IO" do 12 | io = IO::Memory.new 13 | io.write_bytes 5 14 | io.write "hello".to_slice 15 | io.rewind 16 | 17 | r = Header.new 18 | r.read io 19 | r.size.should eq(5) 20 | r.name.should eq("hello") 21 | end 22 | 23 | it "should write an object to an IO" do 24 | io = IO::Memory.new 25 | io.write_bytes 8 26 | io.write "whatwhat".to_slice 27 | io.rewind 28 | 29 | r = Header.new 30 | r.name = "whatwhat" 31 | 32 | io2 = IO::Memory.new 33 | r.write(io2) 34 | io2.rewind 35 | 36 | io2.to_slice.should eq(io.to_slice) 37 | end 38 | 39 | it "should allow mixed endianess" do 40 | io = IO::Memory.new 41 | io.write_bytes 0xBE_i16, IO::ByteFormat::BigEndian 42 | io.write_bytes 0xFEED_i32, IO::ByteFormat::LittleEndian 43 | io.write_bytes 0xDADFEDBEEF_i128, IO::ByteFormat::LittleEndian 44 | io.rewind 45 | 46 | r = MixedEndianLittle.new 47 | r.read io 48 | 49 | r.big.should eq(0xBE_i16) 50 | r.little.should eq(0xFEED_i32) 51 | r.default.should eq(0xDADFEDBEEF_i128) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/bindata_verify_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData do 4 | it "succeeds reading when the verify proc is true" do 5 | io = IO::Memory.new 6 | io.write_byte 0x02 7 | io.write_byte 0x05 8 | io.write_byte 0x06 9 | io.write_byte 0x0B 10 | io.rewind 11 | 12 | r = io.read_bytes VerifyData 13 | r.checksum.should eq 0x0B 14 | end 15 | 16 | it "succeeds writing when the verify proc is true" do 17 | io = IO::Memory.new 18 | io.write_byte 0x02 19 | io.write_byte 0x05 20 | io.write_byte 0x06 21 | io.write_byte 0x0B 22 | io.rewind 23 | 24 | r = VerifyData.new 25 | r.size = 0x02 26 | r.bytes = Bytes.new 2 27 | r.bytes[0] = 0x05 28 | r.bytes[1] = 0x06 29 | r.checksum = 0x0B 30 | io2 = IO::Memory.new 31 | r.write io2 32 | io2.rewind 33 | 34 | io2.to_slice.should eq io.to_slice 35 | end 36 | 37 | it "raises an exception when it fails to verify on read" do 38 | io = IO::Memory.new 39 | io.write_byte 0x02 40 | io.write_byte 0x05 41 | io.write_byte 0x06 42 | io.write_byte 0xFF 43 | io.rewind 44 | 45 | expect_raises BinData::VerificationException, "Failed to verify reading basic at VerifyData.checksum" do 46 | io.read_bytes VerifyData 47 | end 48 | end 49 | 50 | it "raises an exception when it fails to verify on write" do 51 | io = IO::Memory.new 52 | io.write_byte 0x02 53 | io.write_byte 0x05 54 | io.write_byte 0x06 55 | io.write_byte 0x0B 56 | io.rewind 57 | 58 | r = io.read_bytes VerifyData 59 | r.bytes[0] = 0x0F 60 | io2 = IO::Memory.new 61 | 62 | expect_raises BinData::VerificationException, "Failed to verify writing basic at VerifyData.checksum" do 63 | r.write io2 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/bitfield_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | describe BinData::BitField do 4 | it "should parse values out of dense binary structures" do 5 | io = IO::Memory.new 6 | # io.write_bytes(0b1110_1110_1000_0000_u16, IO::ByteFormat::BigEndian) 7 | io.write_byte(0b1110_1110_u8) 8 | io.write_byte(0b1000_0000_u8) 9 | io.write_bytes(0_u16) 10 | io.write "hello".to_slice 11 | io.rewind 12 | 13 | bf = BinData::BitField.new 14 | bf.bits 7, :seven 15 | bf.bits 2, :two 16 | bf.bits 23, :three 17 | bf.apply 18 | 19 | bf.read(io, IO::ByteFormat::LittleEndian) 20 | bf[:seven].should eq(0b1110111) 21 | bf[:two].should eq(0b01) 22 | bf[:three].should eq(0) 23 | end 24 | 25 | it "should parse an object from an IO" do 26 | io = IO::Memory.new 27 | io.write_byte 0b0_u8 28 | io.write_byte 0b1110_1101_u8 29 | io.write_byte 0b1100_1110_u8 30 | io.write_byte 0b1111_1101_u8 31 | io.write_byte 0b0_u8 32 | io.write_bytes 0xF0_E0_D0_C0_B0_A0_91_04_u64, IO::ByteFormat::BigEndian 33 | io.write_byte 0b0_u8 34 | io.rewind 35 | 36 | r = Body.new 37 | r.read io 38 | r.start.should eq(0) 39 | r.six.should eq(0b1110_11) 40 | r.three.should eq(0b011) 41 | r.four.should eq(0b1001) 42 | r.teen.should eq(0b1101_1111_101) 43 | r.mid.should eq(0) 44 | r.five.should eq(0xF0_E0_D0_C0_B0_A0_9_u64) 45 | r.eight.should eq(0x104_u16) 46 | r.end.should eq(0) 47 | end 48 | 49 | it "should write an object to an IO" do 50 | io = IO::Memory.new 51 | io.write_byte 0b0_u8 52 | io.write_byte 0b1110_1101_u8 53 | io.write_byte 0b1100_1110_u8 54 | io.write_byte 0b1111_1101_u8 55 | io.write_byte 0b0_u8 56 | io.write_bytes 0xF0_E0_D0_C0_B0_A0_91_04_u64, IO::ByteFormat::BigEndian 57 | io.write_byte 0b0_u8 58 | io.rewind 59 | 60 | io2 = IO::Memory.new 61 | b = Body.new 62 | b.write(io2) 63 | io2.rewind 64 | 65 | io2.to_slice.should eq(io.to_slice) 66 | end 67 | 68 | it "should write an aligned object to an IO" do 69 | io2 = IO::Memory.new 70 | b = Aligned.new 71 | b.write(io2) 72 | io2.to_slice.should eq(Bytes[0x01]) 73 | end 74 | 75 | it "should write an object with a byte sized field to an IO" do 76 | io2 = IO::Memory.new 77 | b = ByteSized.new 78 | b.write(io2) 79 | io2.to_slice.should eq(Bytes[0x00, 0x80]) 80 | end 81 | 82 | it "should read a complex bitfield object" do 83 | bytes = "023fffff".hexbytes 84 | io = IO::Memory.new(bytes) 85 | obj = io.read_bytes(ObjectIdentifier) 86 | obj.object_type.should eq(8) 87 | obj.instance_number.should eq(4194303) 88 | obj.to_slice.should eq(bytes) 89 | end 90 | 91 | it "should raise an error when there is not enough data" do 92 | io = IO::Memory.new 93 | io.write_byte(0x80) 94 | 95 | ex = expect_raises BinData::ParseError, "Failed to parse ByteSized.bitfield.header" do 96 | io.read_bytes(ByteSized) 97 | end 98 | ex.klass.should eq("ByteSized") 99 | ex.field.should eq("bitfield.header") 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/callback_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | class CallbackTest < BinData 4 | endian little 5 | 6 | field integer : UInt8 7 | 8 | property external_representation : UInt16 = 0 9 | 10 | before_serialize { self.integer = (external_representation // 2).to_u8 } 11 | after_deserialize { self.external_representation = integer.to_u16 * 2_u16 } 12 | end 13 | 14 | describe "callbacks" do 15 | it "should run before serialize callbacks" do 16 | cb = CallbackTest.new 17 | cb.external_representation = 10 18 | 19 | cb.to_slice[0].should eq 5_u8 20 | end 21 | 22 | it "should run after deserialize callbacks" do 23 | io = IO::Memory.new(Bytes[200_u8]) 24 | obj = io.read_bytes(CallbackTest) 25 | 26 | obj.external_representation.should eq 400_u16 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/bindata" 3 | require "../src/bindata/asn1" 4 | 5 | class Header < BinData 6 | endian little 7 | 8 | field size : Int32, value: ->{ name.bytesize } 9 | field name : String, length: ->{ size } 10 | end 11 | 12 | class Body < BinData 13 | endian big 14 | 15 | field start : UInt8, value: ->{ 0_u8 } 16 | 17 | bit_field onlyif: ->{ start == 0 } do 18 | bits 6, :six, value: ->{ 0b1110_11_u8 } 19 | # bits 3, :three, default: 0b011 20 | bits 3, three = 0b011 21 | bits 4, four, value: ->{ 0b1001_u8 } 22 | bits 11, :teen, value: ->{ 0b1101_1111_101_u16 } 23 | end 24 | 25 | field mid : UInt8, value: ->{ 0_u8 } 26 | 27 | bit_field do 28 | bits 52, :five, value: ->{ 0xF0_E0_D0_C0_B0_A0_9_u64 } 29 | bits 12, :eight, value: ->{ 0x104_u16 } 30 | end 31 | 32 | field end : UInt8, value: ->{ 0_u8 } 33 | end 34 | 35 | class Wow < BinData 36 | endian big 37 | 38 | field start : UInt8, value: ->{ 0_u8 } 39 | 40 | # this is a shortcut for the `Header < BinData` class 41 | header :head 42 | 43 | group :body, onlyif: ->{ head.size > 0 } do 44 | field start : UInt8, value: ->{ 1_u8 }, onlyif: ->{ parent.start == 0 } 45 | field end : UInt8, value: ->{ 3_u8 } 46 | end 47 | 48 | field end : UInt8, value: ->{ 0_u8 } 49 | end 50 | 51 | class EnumData < BinData 52 | endian big 53 | 54 | enum Inputs : UInt16 55 | VGA 56 | HDMI 57 | HDMI2 58 | end 59 | 60 | field start : UInt8, value: ->{ 0_u8 } 61 | field inputs : Inputs = Inputs::HDMI 62 | 63 | bit_field do 64 | bits 5, :reserved 65 | bool enabled = false 66 | bits 2, input : Inputs = Inputs::HDMI2 67 | end 68 | 69 | field end : UInt8, value: ->{ 0_u8 } 70 | end 71 | 72 | class Inherited < EnumData 73 | endian big 74 | 75 | bit_field do 76 | bits 4, :other_high 77 | bits 4, :other_low 78 | end 79 | end 80 | 81 | class Aligned < BinData 82 | endian big 83 | 84 | bit_field do 85 | bits 8, :other, default: 1_u8 86 | end 87 | end 88 | 89 | class ByteSized < BinData 90 | endian big 91 | 92 | bit_field do 93 | bits 1, :header 94 | bits 8, :other, default: 1_u8 95 | bits 7, :footer 96 | end 97 | end 98 | 99 | class ArrayData < BinData 100 | endian big 101 | 102 | field flen : UInt8 = 1, value: ->{ first.size } 103 | field first : Array(Int16) = [15_i16], length: ->{ flen } 104 | field slen : UInt8, value: ->{ 0_u8 | second.size } 105 | field second : Array(Int8), length: ->{ slen } 106 | end 107 | 108 | class VariableArrayData < BinData 109 | endian big 110 | 111 | field total_size : UInt8 112 | field test : Array(Int8), read_next: ->{ 113 | # Will continue reading data into the array until 114 | # the array size + 2 buffer bytes equals the total size 115 | (test.size + 2) < total_size 116 | } 117 | field afterdata : UInt8 = 1 118 | end 119 | 120 | class VerifyData < BinData 121 | endian big 122 | 123 | field size : UInt8 124 | field bytes : Bytes, length: ->{ size } 125 | field checksum : UInt8, verify: ->{ checksum == bytes.reduce(0) { |acc, i| acc + i } } 126 | end 127 | 128 | class RemainingBytesData < BinData 129 | endian big 130 | 131 | field first : UInt8 132 | remaining_bytes :rest, onlyif: ->{ first == 0x02 }, verify: ->{ rest.size % 2 == 0 } 133 | end 134 | 135 | class ObjectIdentifier < BinData 136 | endian :big 137 | 138 | bit_field do 139 | bits 10, :object_type 140 | bits 22, :instance_number 141 | end 142 | end 143 | 144 | class MixedEndianLittle < BinData 145 | endian :little 146 | 147 | field big : Int16, endian: IO::ByteFormat::BigEndian 148 | field little : Int32, endian: IO::ByteFormat::LittleEndian 149 | field default : Int128 150 | end 151 | -------------------------------------------------------------------------------- /spec/string_encoding_spec.cr: -------------------------------------------------------------------------------- 1 | require "./helper" 2 | 3 | class StrEncodingTest < BinData 4 | endian little 5 | 6 | field str : String, encoding: "GB2312", length: ->{ 2 } 7 | end 8 | 9 | describe "string encoding" do 10 | it "should serialize" do 11 | obj = StrEncodingTest.new 12 | obj.str = "好" 13 | obj.to_slice.should eq Bytes[186, 195] 14 | obj.str.to_slice.should eq Bytes[229, 165, 189] 15 | end 16 | 17 | it "should deserialize" do 18 | io = IO::Memory.new(Bytes[186, 195]) 19 | obj = io.read_bytes(StrEncodingTest) 20 | obj.str.should eq "好" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/bindata.cr: -------------------------------------------------------------------------------- 1 | require "./bindata/exceptions" 2 | require "./bindata/bitfield" 3 | 4 | abstract class BinData 5 | INDEX = [-1] 6 | BIT_PARTS = [] of Nil 7 | CUSTOM_TYPES = [] of BinData.class 8 | RESERVED_NAMES = ["inherited", "included", "extended", "method_missing", 9 | "method_added", "finished"] 10 | 11 | macro inherited 12 | PARTS = [] of Nil 13 | ENDIAN = ["system"] 14 | KLASS_NAME = [{{@type.name.id}}] 15 | REMAINING = [] of Nil 16 | BEFORE_SERIALIZE = [] of Nil 17 | AFTER_DESERIALIZE = [] of Nil 18 | {% BinData::CUSTOM_TYPES << @type.name.id %} 19 | 20 | {% for custom_type in BinData::CUSTOM_TYPES %} 21 | {% method_name = custom_type.gsub(/::/, "_").underscore.id %} 22 | {% unless RESERVED_NAMES.includes? method_name.stringify %} 23 | macro {{ method_name }}(name, onlyif = nil, verify = nil, value = nil) 24 | field \{{name.id}} : {{custom_type}} = {{ custom_type }}.new 25 | end 26 | {% end %} 27 | {% end %} 28 | 29 | def self.bit_fields 30 | {{@type.ancestors[0].id}}.bit_fields.merge(@@bit_fields) 31 | end 32 | 33 | macro finished 34 | __build_methods__ 35 | end 36 | end 37 | 38 | @@bit_fields = {} of String => BitField 39 | 40 | def self.bit_fields 41 | @@bit_fields 42 | end 43 | 44 | def __format__ : IO::ByteFormat 45 | IO::ByteFormat::SystemEndian 46 | end 47 | 48 | def self.from_slice(bytes : Slice, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) 49 | io = IO::Memory.new(bytes) 50 | from_io(io, format) 51 | end 52 | 53 | def to_slice 54 | io = IO::Memory.new 55 | io.write_bytes self 56 | io.to_slice 57 | end 58 | 59 | macro endian(format) 60 | def __format__ : IO::ByteFormat 61 | {% format = format.id.stringify %} 62 | {% ENDIAN[0] = format.id.stringify %} 63 | {% if format == "little" %} 64 | IO::ByteFormat::LittleEndian 65 | {% elsif format == "big" %} 66 | IO::ByteFormat::BigEndian 67 | {% elsif format == "network" %} 68 | IO::ByteFormat::NetworkEndian 69 | {% else %} 70 | IO::ByteFormat::SystemEndian 71 | {% end %} 72 | end 73 | end 74 | 75 | def read(io : IO) : IO 76 | __perform_read__(io) 77 | end 78 | 79 | protected def __perform_read__(io : IO) : IO 80 | io 81 | end 82 | 83 | def write(io : IO) 84 | __perform_write__(io) 85 | 0_i64 86 | end 87 | 88 | protected def __perform_write__(io : IO) : IO 89 | io 90 | end 91 | 92 | def to_io(io : IO, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) 93 | write(io) 94 | end 95 | 96 | def self.from_io(io : IO, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) 97 | data = self.new 98 | data.read(io) 99 | data 100 | end 101 | 102 | macro __build_methods__ 103 | protected def __perform_read__(io : IO) : IO 104 | # Support inheritance 105 | super(io) 106 | 107 | part_name = "" 108 | 109 | begin 110 | {% for part in PARTS %} 111 | %endian = {% if part[:endian] %}{{ part[:endian] }}{% else %}__format__{% end %} 112 | {% if part[:type] == "bitfield" %} 113 | part_name = {{"bitfield." + BIT_PARTS[part[:name]].keys[0].id.stringify}} 114 | {% else %} 115 | part_name = {{part[:name].id.stringify}} 116 | {% end %} 117 | 118 | {% if part[:onlyif] %} 119 | %onlyif = ({{part[:onlyif]}}).call 120 | if %onlyif 121 | {% end %} 122 | 123 | {% if part[:type] == "basic" %} 124 | {% part_type = part[:cls].resolve %} 125 | {% if part_type.is_a?(Union) %} 126 | @{{part[:name]}} = io.read_bytes({{part_type.types.reject(&.nilable?)[0]}}, %endian) 127 | {% elsif part_type.union? %} 128 | @{{part[:name]}} = io.read_bytes({{part_type.union_types.reject(&.nilable?)[0]}}, %endian) 129 | {% else %} 130 | @{{part[:name]}} = io.read_bytes({{part[:cls]}}, %endian) 131 | {% end %} 132 | 133 | {% elsif part[:type] == "array" %} 134 | %size = ({{part[:length]}}).call.not_nil! 135 | @{{part[:name]}} = [] of {{part[:cls]}} 136 | (0...%size).each do 137 | @{{part[:name]}} << io.read_bytes({{part[:cls]}}, %endian) 138 | end 139 | 140 | {% elsif part[:type] == "variable_array" %} 141 | @{{part[:name]}} = [] of {{part[:cls]}} 142 | loop do 143 | # Stop if the callback indicates there is no more 144 | break unless ({{part[:read_next]}}).call 145 | @{{part[:name]}} << io.read_bytes({{part[:cls]}}, %endian) 146 | end 147 | 148 | {% elsif part[:type] == "enum" %} 149 | @{{part[:name]}} = {{part[:enum_type]}}.from_value(io.read_bytes({{part[:cls]}}, %endian)) 150 | 151 | {% elsif part[:type] == "group" %} 152 | @{{part[:name]}} = {{part[:cls]}}.new 153 | @{{part[:name]}}.parent = self 154 | @{{part[:name]}}.read(io) 155 | 156 | {% elsif part[:type] == "bytes" %} 157 | # There is a length calculation 158 | %size = ({{part[:length]}}).call.not_nil! 159 | %buf = Bytes.new(%size) 160 | io.read_fully(%buf) 161 | @{{part[:name]}} = %buf 162 | 163 | {% elsif part[:type] == "string" %} 164 | {% if part[:length] %} 165 | # There is a length calculation 166 | %size = ({{part[:length]}}).call.not_nil! 167 | %buf = Bytes.new(%size) 168 | io.read_fully(%buf) 169 | {% if part[:encoding] %} 170 | @{{part[:name]}} = String.new(%buf, {{ part[:encoding] }}) 171 | {% else %} 172 | @{{part[:name]}} = String.new(%buf) 173 | {% end %} 174 | {% else %} 175 | # Assume the string is 0 terminated 176 | @{{part[:name]}} = (io.gets('\0') || "")[0..-2] 177 | {% end %} 178 | 179 | {% elsif part[:type] == "bitfield" %} 180 | %bitfield = self.class.bit_fields["{{part[:cls]}}_{{part[:name]}}"] 181 | %bitfield.read(io, %endian) 182 | 183 | # Apply the values (with their correct type) 184 | {% for name, value in BIT_PARTS[part[:name]] %} 185 | %value = %bitfield[{{name.id.stringify}}] 186 | @{{name}} = %value.as({{value[0]}}) 187 | {% end %} 188 | {% end %} 189 | 190 | {% if part[:onlyif] %} 191 | end 192 | {% end %} 193 | 194 | {% if part[:verify] %} 195 | if !({{part[:verify]}}).call 196 | raise ReadingVerificationException.new "{{@type}}", "{{part[:name]}}", "{{part[:type].id}}" 197 | end 198 | {% end %} 199 | {% end %} 200 | 201 | {% if REMAINING.size > 0 %} 202 | part_name = {{REMAINING[0][:name].id.stringify}} 203 | 204 | {% if REMAINING[0][:onlyif] %} 205 | %onlyif = ({{REMAINING[0][:onlyif]}}).call 206 | if %onlyif 207 | {% end %} 208 | %buf = Bytes.new io.size - io.pos 209 | io.read_fully %buf 210 | @{{REMAINING[0][:name]}} = %buf 211 | {% if REMAINING[0][:onlyif] %} 212 | end 213 | {% end %} 214 | {% if REMAINING[0][:verify] %} 215 | if !({{REMAINING[0][:verify]}}).call 216 | raise ReadingVerificationException.new "{{@type}}", "{{REMAINING[0][:name]}}", "{{REMAINING[0][:type].id}}" 217 | end 218 | {% end %} 219 | {% end %} 220 | 221 | rescue ex : VerificationException | ParseError 222 | raise ex 223 | rescue error 224 | raise ParseError.new "{{@type.id}}", "#{part_name}", error 225 | end 226 | 227 | begin 228 | {% for callback in AFTER_DESERIALIZE %} 229 | begin 230 | {{ callback.body }} 231 | end 232 | {% end %} 233 | rescue error 234 | raise RuntimeError.new("error in after deserialize callback", cause: error) 235 | end 236 | 237 | io 238 | end 239 | 240 | protected def __perform_write__(io : IO) : IO 241 | # Support inheritance 242 | super(io) 243 | 244 | begin 245 | {% for callback in BEFORE_SERIALIZE %} 246 | begin 247 | {{ callback.body }} 248 | end 249 | {% end %} 250 | rescue error 251 | raise RuntimeError.new("error in before serialize callback", cause: error) 252 | end 253 | 254 | part_name = "" 255 | 256 | begin 257 | {% for part in PARTS %} 258 | %endian = {% if part[:endian] %}{{ part[:endian] }}{% else %}__format__{% end %} 259 | 260 | {% if part[:type] == "bitfield" %} 261 | part_name = {{"bitfield." + BIT_PARTS[part[:name]].keys[0].id.stringify}} 262 | {% else %} 263 | part_name = {{part[:name].id.stringify}} 264 | {% end %} 265 | 266 | {% if part[:onlyif] %} 267 | %onlyif = ({{part[:onlyif]}}).call 268 | if %onlyif 269 | {% end %} 270 | 271 | {% if part[:value] %} 272 | # check if we need to configure the value 273 | %value = ({{part[:value]}}).call 274 | # This ensures numbers are cooerced to the correct type 275 | # NOTE:: `if %value.is_a?(Number)` had issues with `String` due to `.new(0)` 276 | {% if part[:type] == "basic" %} 277 | @{{part[:name]}} = {{part[:cls]}}.new(0) | %value 278 | {% else %} 279 | @{{part[:name]}} = %value || @{{part[:name]}} 280 | {% end %} 281 | {% end %} 282 | 283 | {% if part[:type] == "basic" %} 284 | {% part_type = part[:cls].resolve %} 285 | {% if part_type.is_a?(Union) || part_type.union? %} 286 | if __temp_{{part[:name]}} = @{{part[:name]}} 287 | io.write_bytes(__temp_{{part[:name]}}, %endian) 288 | else 289 | raise NilAssertionError.new("unable to write nil value for #{self.class}##{{{part[:name].stringify}}}") 290 | end 291 | {% else %} 292 | io.write_bytes(@{{part[:name]}}, %endian) 293 | {% end %} 294 | 295 | {% elsif part[:type] == "array" || part[:type] == "variable_array" %} 296 | @{{part[:name]}}.each do |part| 297 | io.write_bytes(part, %endian) 298 | end 299 | 300 | {% elsif part[:type] == "enum" %} 301 | %value = {{part[:cls]}}.new(@{{part[:name]}}.value) 302 | io.write_bytes(%value, %endian) 303 | 304 | {% elsif part[:type] == "group" %} 305 | @{{part[:name]}}.parent = self 306 | io.write_bytes(@{{part[:name]}}, %endian) 307 | 308 | {% elsif part[:type] == "bytes" %} 309 | io.write(@{{part[:name]}}) 310 | 311 | {% elsif part[:type] == "string" %} 312 | {% if part[:encoding] %} 313 | io.write(@{{part[:name]}}.encode({{ part[:encoding] }})) 314 | {% else %} 315 | io.write(@{{part[:name]}}.to_slice) 316 | {% end %} 317 | 318 | {% if !part[:length] %} 319 | io.write_byte(0_u8) 320 | {% end %} 321 | 322 | {% elsif part[:type] == "bitfield" %} 323 | # Apply any values 324 | %bitfield = self.class.bit_fields["{{part[:cls]}}_{{part[:name]}}"] 325 | {% for name, value in BIT_PARTS[part[:name]] %} 326 | {% if value[1] %} 327 | %value = ({{value[1]}}).call 328 | @{{name}} = %value || @{{name}} 329 | {% end %} 330 | 331 | %bitfield[{{name.id.stringify}}] = @{{name}}.not_nil! 332 | {% end %} 333 | 334 | %bitfield.write(io, %endian) 335 | {% end %} 336 | 337 | {% if part[:onlyif] %} 338 | end 339 | {% end %} 340 | 341 | {% if part[:verify] %} 342 | if !({{part[:verify]}}).call 343 | raise WritingVerificationException.new "{{@type}}", "{{part[:name]}}", "{{part[:type].id}}" 344 | end 345 | {% end %} 346 | {% end %} 347 | 348 | {% if REMAINING.size > 0 %} 349 | part_name = {{REMAINING[0][:name].id.stringify}} 350 | 351 | {% if REMAINING[0][:onlyif] %} 352 | %onlyif = ({{REMAINING[0][:onlyif]}}).call 353 | if %onlyif 354 | {% end %} 355 | io.write(@{{REMAINING[0][:name]}}) 356 | {% if REMAINING[0][:onlyif] %} 357 | end 358 | {% end %} 359 | {% if REMAINING[0][:verify] %} 360 | if !({{REMAINING[0][:verify]}}).call 361 | raise WritingVerificationException.new "{{@type}}", "{{REMAINING[0][:name]}}", "{{REMAINING[0][:type].id}}" 362 | end 363 | {% end %} 364 | {% end %} 365 | 366 | rescue ex : VerificationException | WriteError 367 | raise ex 368 | rescue error 369 | raise WriteError.new "{{@type.id}}", "#{part_name}", error 370 | end 371 | 372 | io 373 | end 374 | end 375 | 376 | macro bits(size, name, value = nil, default = nil) 377 | {% resolved_type = nil %} 378 | 379 | {% if name.is_a?(TypeDeclaration) %} 380 | {% if name.value %} 381 | {% default = name.value %} 382 | {% end %} 383 | {% if name.type %} 384 | {% resolved_type = name.type.resolve %} 385 | {% end %} 386 | {% name = name.var %} 387 | {% elsif name.is_a?(Assign) %} 388 | {% if name.value %} 389 | {% default = name.value %} 390 | {% end %} 391 | {% name = name.target %} 392 | {% end %} 393 | 394 | %field = @@bit_fields["{{KLASS_NAME[0]}}_{{INDEX[0]}}"]? 395 | raise "#{KLASS_NAME[0]}#{ '#' }{{name}} is not defined in a bitfield. Using bitfield macro outside of a bitfield" unless %field 396 | %field.bits({{size}}, {{name.id.stringify}}) 397 | 398 | {% if size <= 8 %} 399 | {% BIT_PARTS[INDEX[0]][name.id] = {"UInt8".id, value} %} 400 | property {{name.id}} : UInt8 = {% if default %} {{default}}.to_u8 {% else %} 0 {% end %} 401 | {% elsif size <= 16 %} 402 | {% BIT_PARTS[INDEX[0]][name.id] = {"UInt16".id, value} %} 403 | property {{name.id}} : UInt16 = {% if default %} {{default}}.to_u16 {% else %} 0 {% end %} 404 | {% elsif size <= 32 %} 405 | {% BIT_PARTS[INDEX[0]][name.id] = {"UInt32".id, value} %} 406 | property {{name.id}} : UInt32 = {% if default %} {{default}}.to_u32 {% else %} 0 {% end %} 407 | {% elsif size <= 64 %} 408 | {% BIT_PARTS[INDEX[0]][name.id] = {"UInt64".id, value} %} 409 | property {{name.id}} : UInt64 = {% if default %} {{default}}.to_u64 {% else %} 0 {% end %} 410 | {% elsif size <= 128 %} 411 | {% BIT_PARTS[INDEX[0]][name.id] = {"UInt128".id, value} %} 412 | property {{name.id}} : UInt128 = {% if default %} {{default}}.to_u128 {% else %} 0 {% end %} 413 | {% else %} 414 | {{ "bits greater than 128 are not supported".id }} 415 | {% end %} 416 | 417 | {% if resolved_type && resolved_type < Enum %} 418 | def {{name.id}} : {{resolved_type}} 419 | {{resolved_type}}.from_value(@{{name.id}}) 420 | end 421 | 422 | def {{name.id}}=(value : {{resolved_type}}) 423 | # Ensure the correct type is being assigned 424 | @{{name.id}} = @{{name.id}}.class.new(0) | value.value 425 | end 426 | {% end %} 427 | end 428 | 429 | @[Deprecated("Use `#bits` instead")] 430 | macro enum_bits(size, name) 431 | {% if name.is_a?(SymbolLiteral) %} 432 | {% name = name.stringify[1..-1].id %} 433 | {% end %} 434 | 435 | bits {{size}}, {{name}} 436 | end 437 | 438 | macro bool(name, default = false) 439 | {% if name.is_a?(Assign) %} 440 | {% if name.value %} 441 | {% default = name.value %} 442 | {% end %} 443 | {% name = name.target %} 444 | {% end %} 445 | 446 | bits(1, {{name}}, default: ({{default}} ? 1 : 0)) 447 | 448 | def {{name.id}} : Bool 449 | @{{name.id}} == 1 450 | end 451 | 452 | def {{name.id}}=(value : Bool) 453 | # Ensure the correct type is being assigned 454 | @{{name.id}} = UInt8.new(value ? 1 : 0) 455 | end 456 | end 457 | 458 | macro bit_field(onlyif = nil, verify = nil, &block) 459 | {% INDEX[0] = INDEX[0] + 1 %} 460 | {% BIT_PARTS << {} of Nil => Nil %} 461 | %bitfield = @@bit_fields["{{KLASS_NAME[0]}}_{{INDEX[0]}}"] = BitField.new 462 | 463 | {{block.body}} 464 | 465 | %bitfield.apply 466 | {% PARTS << {type: "bitfield", name: INDEX[0], cls: KLASS_NAME[0], onlyif: onlyif, verify: verify} %} 467 | end 468 | 469 | # }# Encapsulates a bunch of fields by creating a nested BinData class 470 | macro group(name, onlyif = nil, verify = nil, value = nil, &block) 471 | class {{name.id.stringify.camelcase.id}} < BinData 472 | endian({{ENDIAN[0]}}) 473 | 474 | # Group fields might need access to data in the parent 475 | property parent : {{@type.id}}? 476 | def parent 477 | @parent.not_nil! 478 | end 479 | 480 | {{block.body}} 481 | end 482 | 483 | property {{name.id}} = {{name.id.stringify.camelcase.id}}.new 484 | 485 | {% PARTS << {type: "group", name: name.id, cls: name.id.stringify.camelcase.id, onlyif: onlyif, verify: verify, value: value} %} 486 | end 487 | 488 | macro remaining_bytes(name, onlyif = nil, verify = nil, default = nil) 489 | {% REMAINING << {type: "bytes", name: name.id, onlyif: onlyif, verify: verify} %} 490 | property {{name.id}} : Bytes = {% if default %} {{default}}.to_slice {% else %} Bytes.new(0) {% end %} 491 | end 492 | 493 | # this needs to be split out so we can resolve the enum base_type 494 | macro __add_enum_field(name, cls, onlyif, verify, value, encoding, enum_type) 495 | {% PARTS << {type: "enum", name: name, cls: cls, onlyif: onlyif, verify: verify, value: value, encoding: encoding, enum_type: enum_type} %} 496 | end 497 | 498 | macro field(type_declaration, onlyif = nil, verify = nil, value = nil, length = nil, read_next = nil, encoding = nil, endian = nil) 499 | {% if !type_declaration.is_a?(TypeDeclaration) %} 500 | {% raise "#{type_declaration} must be a TypeDeclaration" %} 501 | {% end %} 502 | 503 | {% resolved_type = type_declaration.type.resolve %} 504 | {% default = type_declaration.value %} 505 | {% name = type_declaration.var %} 506 | 507 | {% if {Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float32, Float64}.includes? resolved_type %} 508 | {% PARTS << {type: "basic", name: name, cls: resolved_type, onlyif: onlyif, verify: verify, value: value, endian: endian} %} 509 | property {{name.id}} : {{resolved_type}} = {% if default %} {{resolved_type}}.new({{default}}) {% else %} 0 {% end %} 510 | {% elsif resolved_type == String %} 511 | {% if encoding %} 512 | {% raise "String fields require a length for alternative encodings, #{name} (#{encoding})" unless length %} 513 | {% end %} 514 | {% PARTS << {type: "string", name: name, cls: resolved_type, onlyif: onlyif, verify: verify, length: length, value: value, encoding: encoding} %} 515 | property {{name.id}} : String = {% if default %} {{default}} {% else %} "" {% end %} 516 | {% elsif {Bytes, Slice(UInt8)}.includes? resolved_type %} 517 | {% PARTS << {type: "bytes", name: name, cls: resolved_type, onlyif: onlyif, verify: verify, length: length, value: value} %} 518 | {% raise "Bytes fields require a length callback" unless length %} 519 | property {{name.id}} : Bytes = {% if default %} {{default}}.to_slice {% else %} Bytes.new(0) {% end %} 520 | {% elsif resolved_type < Enum %} 521 | property {{type_declaration}} 522 | {% raise "Enum fields require a default value to be provided (#{name})" unless default %} 523 | __add_enum_field name: {{name}}, cls: typeof({{default}}.value), onlyif: {{onlyif}}, verify: {{verify}}, value: {{value}}, encoding: {{encoding}}, enum_type: {{resolved_type}} 524 | {% elsif resolved_type <= Array || resolved_type <= Set %} 525 | {% if length %} 526 | {% PARTS << {type: "array", name: name, cls: resolved_type.type_vars[0], onlyif: onlyif, verify: verify, length: length, value: value} %} 527 | {% elsif read_next %} 528 | {% PARTS << {type: "variable_array", name: name, cls: resolved_type.type_vars[0], onlyif: onlyif, verify: verify, read_next: read_next, value: value} %} 529 | {% else %} 530 | {% raise "Array and Set fields require a length callback or read_next callback" %} 531 | {% end %} 532 | property {{name.id}} : {{resolved_type}} = {% if default %} {{default}} {% else %} {{resolved_type}}.new {% end %} 533 | {% else %} 534 | {% PARTS << {type: "basic", name: name, cls: resolved_type, onlyif: onlyif, verify: verify, value: value} %} 535 | property {{type_declaration}} 536 | {% end %} 537 | end 538 | 539 | macro before_serialize(&block) 540 | {% BEFORE_SERIALIZE << block %} 541 | end 542 | 543 | macro after_deserialize(&block) 544 | {% AFTER_DESERIALIZE << block %} 545 | end 546 | 547 | # deprecated: 548 | 549 | @[Deprecated("Use `#field` instead")] 550 | macro custom(name, onlyif = nil, verify = nil, value = nil) 551 | {% PARTS << {type: "basic", name: name.var, cls: name.type, onlyif: onlyif, verify: verify, value: value} %} 552 | property {{name.id}} 553 | end 554 | 555 | @[Deprecated("Use `#field` instead")] 556 | macro enum_field(size, name, onlyif = nil, verify = nil, value = nil) 557 | {% PARTS << {type: "enum", name: name.var, cls: size, onlyif: onlyif, verify: verify, value: value, enum_type: name.type} %} 558 | property {{name.id}} 559 | end 560 | 561 | @[Deprecated("Use `#field` instead")] 562 | macro array(name, length, onlyif = nil, verify = nil, value = nil) 563 | {% PARTS << {type: "array", name: name.var, cls: name.type, onlyif: onlyif, verify: verify, length: length, value: value} %} 564 | property {{name.var}} : Array({{name.type}}) = {% if name.value %} {{name.value}} {% else %} [] of {{name.type}} {% end %} 565 | end 566 | 567 | @[Deprecated("Use `#field` instead")] 568 | macro variable_array(name, read_next, onlyif = nil, verify = nil, value = nil) 569 | {% PARTS << {type: "variable_array", name: name.var, cls: name.type, onlyif: onlyif, verify: verify, read_next: read_next, value: value} %} 570 | property {{name.var}} : Array({{name.type}}) = {% if name.value %} {{name.value}} {% else %} [] of {{name.type}} {% end %} 571 | end 572 | 573 | {% for vartype in ["UInt8", "Int8", "UInt16", "Int16", "UInt32", "Int32", "UInt64", "Int64", "UInt128", "Int128", "Float32", "Float64"] %} 574 | {% name = vartype.downcase.id %} 575 | 576 | @[Deprecated("Use `#field` instead")] 577 | macro {{name}}(name, onlyif = nil, verify = nil, value = nil, default = nil) 578 | \{% PARTS << {type: "basic", name: name.id, cls: {{vartype.id}}, onlyif: onlyif, verify: verify, value: value} %} 579 | property \{{name.id}} : {{vartype.id}} = \{% if default %} {{vartype.id}}.new(\{{default}}) \{% else %} 0 \{% end %} 580 | end 581 | 582 | @[Deprecated("Use `#field` instead")] 583 | macro {{name}}be(name, onlyif = nil, verify = nil, value = nil, default = nil) 584 | \{% PARTS << {type: "basic", name: name.id, cls: {{vartype.id}}, onlyif: onlyif, verify: verify, value: value, endian: IO::ByteFormat::BigEndian} %} 585 | property \{{name.id}} : {{vartype.id}} = \{% if default %} {{vartype.id}}.new(\{{default}}) \{% else %} 0 \{% end %} 586 | end 587 | 588 | @[Deprecated("Use `#field` instead")] 589 | macro {{name}}le(name, onlyif = nil, verify = nil, value = nil, default = nil) 590 | \{% PARTS << {type: "basic", name: name.id, cls: {{vartype.id}}, onlyif: onlyif, verify: verify, value: value, endian: IO::ByteFormat::LittleEndian} %} 591 | property \{{name.id}} : {{vartype.id}} = \{% if default %} {{vartype.id}}.new(\{{default}}) \{% else %} 0 \{% end %} 592 | end 593 | {% end %} 594 | 595 | @[Deprecated("Use `#field` instead")] 596 | macro string(name, onlyif = nil, verify = nil, length = nil, value = nil, encoding = nil, default = nil) 597 | {% PARTS << {type: "string", name: name.id, cls: "String".id, onlyif: onlyif, verify: verify, length: length, value: value, encoding: encoding} %} 598 | property {{name.id}} : String = {% if default %} {{default}}.to_s {% else %} "" {% end %} 599 | end 600 | 601 | @[Deprecated("Use `#field` instead")] 602 | macro bytes(name, length, onlyif = nil, verify = nil, value = nil, default = nil) 603 | {% PARTS << {type: "bytes", name: name.id, cls: "Bytes".id, onlyif: onlyif, verify: verify, length: length, value: value} %} 604 | property {{name.id}} : Bytes = {% if default %} {{default}}.to_slice {% else %} Bytes.new(0) {% end %} 605 | end 606 | end 607 | -------------------------------------------------------------------------------- /src/bindata/asn1.cr: -------------------------------------------------------------------------------- 1 | require "../bindata" 2 | 3 | module ASN1; end 4 | 5 | class BER < BinData; end 6 | 7 | require "./asn1/identifier" 8 | require "./asn1/length" 9 | require "./asn1/data_types" 10 | 11 | module ASN1 12 | class BER < BinData 13 | endian big 14 | 15 | # Components of a BER object 16 | field identifier : Identifier = Identifier.new 17 | field length : Length = Length.new 18 | property payload : Bytes = Bytes.new(0) 19 | 20 | def tag_class 21 | @identifier.tag_class 22 | end 23 | 24 | def tag_class=(tag : TagClass) 25 | @identifier.tag_class = tag 26 | end 27 | 28 | def constructed 29 | @identifier.constructed 30 | end 31 | 32 | def constructed=(custom : Bool) 33 | @identifier.constructed = custom 34 | end 35 | 36 | def tag_number 37 | @identifier.tag_number 38 | end 39 | 40 | def tag_number=(tag_type : Int | UniversalTags) 41 | @identifier.tag_number = tag_type.to_i.to_u8 42 | end 43 | 44 | def tag 45 | raise "only valid for universal tags" unless tag_class == TagClass::Universal 46 | UniversalTags.new tag_number.to_i 47 | end 48 | 49 | def extended? 50 | @identifier.extended? ? @identifier.extended : nil 51 | end 52 | 53 | def extended=(parts : Array(ExtendedIdentifier)) 54 | @identifier.extended = parts 55 | end 56 | 57 | def extended 58 | @identifier.extended 59 | end 60 | 61 | def size 62 | @length.length 63 | end 64 | 65 | def read(io : IO) : IO 66 | super(io) 67 | if @length.indefinite? 68 | temp = IO::Memory.new 69 | # init to 1 as we need two 0 bytes to indicate end of stream 70 | previous_byte = 1_u8 71 | loop do 72 | current_byte = io.read_byte.not_nil! 73 | break if previous_byte == 0_u8 && current_byte == 0_u8 74 | temp.write_byte previous_byte 75 | previous_byte = current_byte 76 | end 77 | 78 | @payload = Bytes.new(temp.pos) 79 | temp.rewind 80 | temp.read_fully(@payload) 81 | else 82 | begin 83 | @payload = Bytes.new(@length.length) 84 | io.read_fully(@payload) 85 | rescue ArgumentError 86 | # Typically occurs if length is negative 87 | raise ArgumentError.new("invalid ASN.1 length: #{@length.length}") 88 | end 89 | end 90 | io 91 | end 92 | 93 | def write(io : IO) 94 | @length.length = @payload.size 95 | super(io) 96 | io.write(@payload) 97 | io.write_bytes(0_u16) if @length.indefinite? 98 | 0_i64 99 | end 100 | 101 | # Check if this can be expanded into multiple sub-entries 102 | def sequence? 103 | return false unless tag_class == TagClass::Universal 104 | tag = UniversalTags.new tag_number.to_i 105 | constructed && {UniversalTags::Sequence, UniversalTags::Set}.includes?(tag) 106 | end 107 | 108 | # Extracts children from the payload 109 | def children 110 | parts = [] of BER 111 | io = IO::Memory.new(@payload) 112 | while io.pos < io.size 113 | parts << io.read_bytes(ASN1::BER) 114 | end 115 | parts 116 | end 117 | 118 | def children=(parts) 119 | self.constructed = true 120 | io = IO::Memory.new 121 | parts.each(&.write(io)) 122 | @payload = io.to_slice 123 | parts 124 | end 125 | 126 | def inspect(io : IO) : Nil 127 | io << "#<" << {{@type.name.id.stringify}} << ":0x" 128 | object_id.to_s(io, 16) 129 | 130 | io << " tag_class=" 131 | tag_class.to_s(io) 132 | io << " constructed=" 133 | constructed.to_s(io) 134 | if tag_class == TagClass::Universal 135 | io << " tag=" 136 | tag.to_s(io) 137 | end 138 | io << " tag_number=" 139 | tag_number.to_s(io) 140 | io << " extended=" 141 | @identifier.extended?.to_s(io) 142 | io << " size=" 143 | size.to_s(io) 144 | io << " payload=" 145 | @payload.inspect(io) 146 | 147 | io << ">" 148 | nil 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /src/bindata/asn1/data_types.cr: -------------------------------------------------------------------------------- 1 | class ASN1::BER < BinData 2 | class InvalidTag < Exception; end 3 | 4 | class InvalidObjectId < Exception; end 5 | 6 | enum UniversalTags 7 | EndOfContent 8 | Boolean 9 | Integer 10 | BitString # Binary data 11 | OctetString # Hex values of the payload. Bytes[0x01, 0x02] == "0102" 12 | Null 13 | ObjectIdentifier # The tree like structure for objects 1.234.2.45.23 etc 14 | ObjectDescriptor 15 | External 16 | Float 17 | Enumerated 18 | EmbeddedPDV 19 | UTF8String 20 | RelativeOID 21 | Reserved1 22 | Reserved2 23 | Sequence # like a c-struct ordered list of objects 24 | Set # set of objects no ordering 25 | NumericString 26 | PrintableString 27 | T61String 28 | VideotexString 29 | IA5String 30 | UTCTime 31 | GeneralizedTime 32 | GraphicString 33 | VisibleString 34 | GeneralString 35 | UniversalString 36 | CharacterString # Probably ASCII or UTF8 37 | BMPString 38 | end 39 | 40 | private def ensure_universal(check_tag) 41 | raise InvalidTag.new("not a universal tag: #{tag_class}") unless tag_class == TagClass::Universal 42 | raise InvalidTag.new("object is a #{tag}, expecting #{check_tag}") unless tag == check_tag 43 | end 44 | 45 | # Returns the object ID in string format 46 | def get_object_id 47 | ensure_universal(UniversalTags::ObjectIdentifier) 48 | return "" if @payload.size == 0 49 | 50 | value0 = @payload[0].to_i32 51 | second = value0 % 40 52 | first = (value0 - second) // 40 53 | raise InvalidObjectId.new(@payload.inspect) if first > 2 54 | object_id = [first, second] 55 | 56 | # Some crazy shit going on here: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-object-identifier 57 | n = 0 58 | (1...@payload.size).each do |i| 59 | if @payload[i] > 0x80 && n == 0 60 | n = (@payload[i].to_i32 & 0x7f) << 8 61 | elsif n > 0 62 | # We need to ignore the high bit of the 2nd byte 63 | n = n + (@payload[i] << 1) 64 | object_id << (n >> 1) 65 | n = 0 66 | else 67 | object_id << @payload[i].to_i32 68 | end 69 | end 70 | 71 | object_id.join(".") 72 | end 73 | 74 | # Sets a string representing an object ID 75 | def set_object_id(oid) 76 | value = oid.split(".").map &.to_i 77 | 78 | raise InvalidObjectId.new(value.inspect) if value.size < 1 79 | raise InvalidObjectId.new(value.inspect) if value[0] > 2 80 | 81 | # Set the appropriate tags 82 | self.tag_class = TagClass::Universal 83 | self.tag_number = UniversalTags::ObjectIdentifier 84 | data = IO::Memory.new 85 | 86 | # Convert the string to bytes 87 | if value.size > 1 88 | raise InvalidObjectId.new(value.inspect) if value[0] < 2 && value[1] > 40 89 | # First two parts are combined 90 | data.write_byte (40 * value[0] + value[1]).to_u8 91 | (2...value.size).each do |i| 92 | if value[i] < 0x80 93 | data.write_byte(value[i].to_u8) 94 | else 95 | # Parts bigger than 128 are represented by 2 bytes (14 usable bits) 96 | bytes = value[i] << 1 97 | data.write_byte (((bytes & 0xFF00) >> 8) | 0x80).to_u8 98 | data.write_byte ((bytes & 0xFF) >> 1).to_u8 99 | end 100 | end 101 | else 102 | data.write_byte((40 * value[0]).to_u8) 103 | end 104 | 105 | @payload = data.to_slice 106 | 107 | self 108 | end 109 | 110 | # Gets a hex representation of the bytes 111 | def get_hexstring(universal = true, tag = UniversalTags::OctetString) 112 | ensure_universal(tag) if universal 113 | @payload.hexstring 114 | end 115 | 116 | # Sets bytes from a hexstring 117 | def set_hexstring(string, tag = UniversalTags::OctetString, tag_class = TagClass::Universal) 118 | self.tag_class = tag_class 119 | self.tag_number = tag 120 | 121 | string = string.gsub(/(0x|[^0-9A-Fa-f])*/, "") 122 | string = "0#{string}" if string.size % 2 > 0 123 | @payload = string.hexbytes 124 | self 125 | end 126 | 127 | # Returns the raw bytes 128 | def get_bytes 129 | @payload 130 | end 131 | 132 | def set_bytes(data, tag = UniversalTags::OctetString, tag_class = TagClass::Universal) 133 | self.tag_class = tag_class 134 | self.tag_number = tag 135 | 136 | @payload = data.to_slice 137 | self 138 | end 139 | 140 | # Returns a UTF8 string 141 | def get_string 142 | check_tags = {UniversalTags::UTF8String, UniversalTags::CharacterString, UniversalTags::PrintableString, UniversalTags::IA5String, UniversalTags::OctetString} 143 | raise InvalidTag.new("not a universal tag: #{tag_class}") unless tag_class == TagClass::Universal 144 | raise InvalidTag.new("object is a #{tag}, expecting one of #{check_tags}") unless check_tags.includes?(tag) 145 | 146 | String.new(@payload) 147 | end 148 | 149 | # Sets a UTF8 string 150 | def set_string(string, tag = UniversalTags::UTF8String, tag_class = TagClass::Universal) 151 | self.tag_class = tag_class 152 | self.tag_number = tag 153 | 154 | @payload = string.to_slice 155 | self 156 | end 157 | 158 | def get_boolean 159 | ensure_universal(UniversalTags::Boolean) 160 | @payload[0] != 0_u8 161 | end 162 | 163 | def set_boolean(value) 164 | self.tag_class = TagClass::Universal 165 | self.tag_number = UniversalTags::Boolean 166 | 167 | @payload = value ? Bytes[0xFF] : Bytes[0x0] 168 | self 169 | end 170 | 171 | def get_integer(check_tags = {UniversalTags::Integer, UniversalTags::Enumerated}, check_class = TagClass::Universal) : Int64 172 | raise InvalidTag.new("not a universal tag: #{tag_class}") unless tag_class == check_class 173 | raise InvalidTag.new("object is a #{tag}, expecting one of #{check_tags}") unless check_tags.includes?(tag) 174 | return 0_i64 if @payload.size == 0 175 | 176 | # Check if first bit is set indicating negativity 177 | negative = (@payload[0] & 0x80) > 0 178 | reverse_index = @payload.size - 1 179 | 180 | # initialize the result with the first byte 181 | start = if negative 182 | (~@payload[0]).to_i64 << (8 * reverse_index) 183 | else 184 | @payload[0].to_i64 << (8 * reverse_index) 185 | end 186 | 187 | # place the remaining bytes into the structure 188 | reverse_index -= 1 189 | @payload[1..-1].each do |byte| 190 | byte = ~byte if negative 191 | start += (byte.to_i64 << (reverse_index * 8)) 192 | reverse_index -= 1 193 | end 194 | 195 | return -(start + 1) if negative 196 | start 197 | end 198 | 199 | def get_integer_bytes : Bytes 200 | ensure_universal(UniversalTags::Integer) 201 | return Bytes.new(0) if @payload.size == 0 202 | return Bytes[0] if @payload.size == 1 && {0xFF_u8, 0_u8}.includes?(@payload[0]) 203 | return @payload[1..-1] if @payload[0] == 0_u8 204 | @payload 205 | end 206 | 207 | # ameba:disable Metrics/CyclomaticComplexity 208 | def set_integer(value, tag = UniversalTags::Integer, tag_class = TagClass::Universal) 209 | self.tag_class = tag_class 210 | self.tag_number = tag 211 | 212 | # extract the bytes from the value 213 | if value.responds_to?(:to_io) 214 | io = IO::Memory.new 215 | io.write_bytes value, IO::ByteFormat::BigEndian 216 | 217 | data = io.to_slice 218 | negative = value < 0 219 | else 220 | data = value.to_slice 221 | negative = false 222 | end 223 | 224 | # The bytes to write 225 | bytes = IO::Memory.new 226 | 227 | # Ignore padding bytes 228 | ignore = true 229 | if negative 230 | data.each do |byte| 231 | if ignore 232 | next if byte == 0xFF 233 | ignore = false 234 | end 235 | bytes.write_byte byte 236 | end 237 | else 238 | data.each do |byte| 239 | if ignore 240 | next if byte == 0x00 241 | ignore = false 242 | end 243 | bytes.write_byte byte 244 | end 245 | end 246 | 247 | # ensure there is at least one byte 248 | bytes.write_byte(negative ? 0xFF_u8 : 0x00_u8) if bytes.size == 0 249 | 250 | # Make sure positive integers don't start with 0xFF 251 | payload_bytes = bytes.to_slice 252 | if !negative && (payload_bytes[0] & 0b10000000) > 0 253 | io = IO::Memory.new 254 | io.write_bytes 0x00_u8 255 | io.write payload_bytes 256 | payload_bytes = io.to_slice 257 | end 258 | 259 | @payload = payload_bytes 260 | self 261 | end 262 | 263 | def get_bitstring 264 | ensure_universal(UniversalTags::BitString) 265 | if @payload[0] == 0 266 | @payload[1, @payload.size - 1] 267 | else 268 | # skip = @payload[0] 269 | raise "skip not implemented" 270 | end 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /src/bindata/asn1/identifier.cr: -------------------------------------------------------------------------------- 1 | class ASN1::BER < BinData 2 | enum TagClass 3 | Universal 4 | Application 5 | ContextSpecific 6 | Private 7 | end 8 | 9 | class ExtendedIdentifier < BinData 10 | endian big 11 | 12 | bit_field do 13 | bool more, default: false 14 | bits 7, :tag_number 15 | end 16 | end 17 | 18 | class Identifier < BinData 19 | endian big 20 | 21 | bit_field do 22 | bits 2, tag_class : TagClass = TagClass::Universal 23 | bool constructed, default: false 24 | bits 5, :tag_number 25 | end 26 | 27 | property extended : Array(ExtendedIdentifier) = [] of ExtendedIdentifier 28 | 29 | def extended? 30 | tag_class != TagClass::Universal && tag_number == 0b11111_u8 31 | end 32 | 33 | def read(io : IO) : IO 34 | super(io) 35 | if extended? 36 | @extended = [] of ExtendedIdentifier 37 | loop do 38 | extended_id = io.read_bytes(ExtendedIdentifier) 39 | @extended << extended_id 40 | break unless extended_id.more 41 | end 42 | end 43 | io 44 | end 45 | 46 | def write(io : IO) 47 | @tag_number = 0b11111_u8 if extended.size > 0 48 | super(io) 49 | extended.each_with_index do |ext, index| 50 | ext.more = (index + 1) < extended.size 51 | ext.write(io) 52 | end 53 | 0_i64 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/bindata/asn1/length.cr: -------------------------------------------------------------------------------- 1 | class ASN1::BER < BinData 2 | class Length < BinData 3 | endian big 4 | 5 | bit_field do 6 | bool long, default: false 7 | bits 7, :length_indicator 8 | end 9 | 10 | field long_bytes : Array(UInt8), length: ->{ 11 | if long && !indefinite? 12 | raise "invalid ASN.1 BER length. Number of length bytes: #{length_indicator}" if length_indicator > 4 13 | 0 | length_indicator 14 | else 15 | 0 16 | end 17 | } 18 | 19 | # We can pretty much safely assume no protocol is implementing 20 | # more than positive Int32 length datagrams 21 | property length : Int32 = 0 22 | 23 | def indefinite? 24 | long && length_indicator == 0_u8 25 | end 26 | 27 | def read(io : IO) : IO 28 | super(io) 29 | 30 | # set length field 31 | if indefinite? 32 | @length = 0 33 | elsif long 34 | @length = 0 35 | long_bytes.reverse.each_with_index do |byte, index| 36 | @length = @length | (byte.to_i32 << (index * 8)) 37 | end 38 | else 39 | @length = length_indicator.to_i32 40 | end 41 | io 42 | end 43 | 44 | def write(io : IO) 45 | self.long = true if @length >= 127 46 | 47 | if long 48 | @long_bytes = [] of UInt8 49 | temp_io = IO::Memory.new(4) 50 | temp_io.write_bytes @length, IO::ByteFormat::BigEndian 51 | 52 | skip = true 53 | temp_io.to_slice.each do |byte| 54 | if skip && byte == 0 55 | next 56 | else 57 | skip = false 58 | @long_bytes << byte 59 | end 60 | end 61 | 62 | @length_indicator = @long_bytes.size.to_u8 63 | else 64 | @length_indicator = @length.to_u8 65 | @long_bytes = [] of UInt8 66 | end 67 | 68 | super(io) 69 | 0_i64 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /src/bindata/bitfield.cr: -------------------------------------------------------------------------------- 1 | class BinData::BitField 2 | def initialize 3 | @bitsize = 0 4 | @mappings = {} of String => Int32 5 | @values = {} of String => UInt8 | UInt16 | UInt32 | UInt64 | UInt128 6 | # 4 + 12 == 2bytes 7 | end 8 | 9 | @buffer : Bytes? 10 | 11 | def bits(size, name) 12 | raise "no support for structures larger than 128 bits" if size > 128 13 | @bitsize += size 14 | @mappings[name.to_s] = size 15 | end 16 | 17 | def apply 18 | raise "bit mappings must be divisible by 8" if @bitsize % 8 > 0 19 | # Extra byte used when writing to IO 20 | @buffer = Bytes.new(@bitsize // 8) 21 | end 22 | 23 | def shift(buffer, num_bits, start_byte = 0) 24 | bytes = buffer.size 25 | remaining_bytes = bytes - start_byte 26 | return buffer if remaining_bytes <= 0 27 | 28 | # Shift the one byte 29 | if remaining_bytes == 1 30 | buffer[start_byte] = buffer[start_byte] << num_bits 31 | return buffer 32 | end 33 | 34 | # Shift the two bytes 35 | io = IO::Memory.new(buffer) 36 | io.pos = start_byte 37 | value = io.read_bytes(UInt16, IO::ByteFormat::BigEndian) 38 | value = value << num_bits 39 | 40 | io.pos = start_byte 41 | io.write_bytes(value, IO::ByteFormat::BigEndian) 42 | 43 | return buffer if remaining_bytes == 2 44 | 45 | # Adjust all the remaining bytes 46 | index = start_byte + 2 47 | loop do 48 | previous = index - 1 49 | 50 | # Shift the next bit (as a 16bit var so we get the overflow) 51 | value = (0_u16 | buffer[index]) << num_bits 52 | # Save the adjustment 53 | buffer[index] = 0_u8 | value 54 | # Save the shifted value 55 | buffer[previous] = buffer[previous] | (value >> 8) 56 | 57 | # Move forward by 1 byte 58 | index += 1 59 | break if index >= bytes 60 | end 61 | buffer 62 | end 63 | 64 | def read(input, format) # ameba:disable Metrics/CyclomaticComplexity 65 | # Fill the buffer 66 | buffer = @buffer.not_nil! 67 | input.read_fully(buffer) 68 | 69 | # TODO:: Check if we need to re-order the bytes 70 | # if format == IO::ByteFormat::LittleEndian 71 | # end 72 | 73 | @mappings.each do |name, size| 74 | # Read out the data we are after using this buffer 75 | io = IO::Memory.new(buffer) 76 | io.rewind 77 | 78 | if size <= 8 79 | value = io.read_bytes(UInt8, IO::ByteFormat::BigEndian) 80 | 81 | if size < 8 82 | # Shift the bits we're interested in towards 0 (as they are high bits) 83 | value = value >> (8 - size) 84 | # Mask the bits we are interested in 85 | value = value & ((1_u8 << size) - 1_u8) 86 | end 87 | elsif size <= 16 88 | value = io.read_bytes(UInt16, IO::ByteFormat::BigEndian) 89 | 90 | if size < 16 91 | # Shift the bits we're interested in towards 0 (as they are high bits) 92 | value = value >> (16 - size) 93 | # Mask the bits we are interested in 94 | value = value & ((1_u16 << size) - 1_u16) 95 | end 96 | elsif size <= 32 97 | # adjust buffer as required to read the value 98 | if (io.size - io.pos) < 4 99 | io_new = IO::Memory.new(Bytes.new(4)) 100 | io_new.write io.to_slice[io.pos..-1] 101 | io = io_new 102 | io.rewind 103 | end 104 | 105 | value = io.read_bytes(UInt32, IO::ByteFormat::BigEndian) 106 | if size < 32 107 | # Shift the bits we're interested in towards 0 (as they are high bits) 108 | value = value >> (32 - size) 109 | # Mask the bits we are interested in 110 | value = value & ((1_u32 << size) - 1_u32) 111 | end 112 | elsif size <= 64 113 | # adjust buffer as required to read the value 114 | if (io.size - io.pos) < 8 115 | io_new = IO::Memory.new(Bytes.new(8)) 116 | io_new.write io.to_slice[io.pos..-1] 117 | io = io_new 118 | io.rewind 119 | end 120 | 121 | value = io.read_bytes(UInt64, IO::ByteFormat::BigEndian) 122 | if size < 64 123 | # Shift the bits we're interested in towards 0 (as they are high bits) 124 | value = value >> (64 - size) 125 | # Mask the bits we are interested in 126 | value = value & ((1_u64 << size) - 1_u64) 127 | end 128 | elsif size <= 128 129 | # adjust buffer as required to read the value 130 | if (io.size - io.pos) < 16 131 | io_new = IO::Memory.new(Bytes.new(16)) 132 | io_new.write io.to_slice[io.pos..-1] 133 | io = io_new 134 | io.rewind 135 | end 136 | 137 | value = io.read_bytes(UInt128, IO::ByteFormat::BigEndian) 138 | if size < 128 139 | # Shift the bits we're interested in towards 0 (as they are high bits) 140 | value = value >> (128 - size) 141 | # Mask the bits we are interested in 142 | value = value & ((1_u128 << size) - 1_u128) 143 | end 144 | else 145 | raise "no support for structures larger than 128 bits" 146 | end 147 | 148 | # relies on integer division rounding down 149 | reduce_buffer = size // 8 150 | buffer = buffer[reduce_buffer, buffer.size - reduce_buffer] 151 | 152 | # Adjust the buffer 153 | shift_by = size % 8 154 | shift(buffer, shift_by) if shift_by > 0 155 | 156 | @values[name] = value 157 | end 158 | 159 | input 160 | end 161 | 162 | # ameba:disable Metrics/CyclomaticComplexity 163 | def write(io, format) 164 | # Fill the buffer 165 | bytes = (@bitsize // 8) + 1 166 | buffer = Bytes.new(bytes) 167 | output = IO::Memory.new(buffer) 168 | bitpos = 0 169 | @mappings.each do |name, size| 170 | offset = bitpos % 8 171 | start_byte = bitpos // 8 172 | 173 | num_bytes = size // 8 174 | num_bytes += 1 if (size % 8) > 0 175 | 176 | # The extra byte lets us easily write to the buffer 177 | # without overwriting existing bytes 178 | start_byte += 1 if offset != 0 179 | value = @values[name] 180 | 181 | # Make sure there is enough space to write the bits 182 | temp_size = case value 183 | when UInt8 184 | 1 185 | when UInt16 186 | 2 187 | when UInt32 188 | 4 189 | when UInt64 190 | 8 191 | when UInt128 192 | 16 193 | else 194 | 0 195 | end 196 | temp_io = IO::Memory.new(Bytes.new(temp_size)) 197 | temp_io.write_bytes(value, IO::ByteFormat::BigEndian) 198 | 199 | output.pos = start_byte 200 | output.write(temp_io.to_slice[(temp_size - num_bytes)..-1]) 201 | bitpos += size 202 | 203 | # Calculate how many full bytes to move back 204 | extra_bytes = (((output.pos - start_byte) * 8) - size) // 8 205 | if extra_bytes > 0 206 | first_byte = start_byte + extra_bytes 207 | (first_byte...bytes).each do |index| 208 | buffer[index - extra_bytes] = buffer[index] 209 | end 210 | end 211 | 212 | # Align the first bit with the start of the byte 213 | shift_size = 8 - (size % 8) 214 | shift(buffer, shift_size, start_byte) unless shift_size == 8 215 | 216 | # We need to shift the bytes into the previous byte 217 | if offset != 0 218 | num_bits = 8 - offset 219 | index = start_byte 220 | 221 | loop do 222 | previous = index - 1 223 | 224 | # Shift the next bit (as a 16bit var so we catch the overflow) 225 | value = (0_u16 | buffer[index]) << num_bits 226 | # Save the adjustment 227 | buffer[index] = 0_u8 | value 228 | # Save the shifted value 229 | buffer[previous] = buffer[previous] | (value >> 8) 230 | 231 | # Move forward by 1 byte 232 | index += 1 233 | break if index >= bytes 234 | end 235 | end 236 | end 237 | 238 | io.write(buffer[0, bytes - 1]) 239 | 240 | 0_i64 241 | end 242 | 243 | def []=(name, value) 244 | @values[name.to_s] = value 245 | end 246 | 247 | def [](name) 248 | @values[name.to_s] 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /src/bindata/exceptions.cr: -------------------------------------------------------------------------------- 1 | abstract class BinData 2 | class CustomException < Exception 3 | getter klass : String? 4 | getter field : String? 5 | getter field_type : String? 6 | 7 | def initialize(message, ex : Exception) 8 | super(message, ex) 9 | end 10 | 11 | def initialize(message) 12 | super(message) 13 | end 14 | end 15 | 16 | class VerificationException < CustomException; end 17 | 18 | class WritingVerificationException < VerificationException 19 | def initialize(@klass, @field, @field_type) 20 | super("Failed to verify writing #{field_type} at #{klass}.#{field}") 21 | end 22 | end 23 | 24 | class ReadingVerificationException < VerificationException 25 | def initialize(@klass, @field, @field_type) 26 | super("Failed to verify reading #{field_type} at #{klass}.#{field}") 27 | end 28 | end 29 | 30 | class ParseError < CustomException 31 | def initialize(@klass, @field, ex : Exception) 32 | super("Failed to parse #{klass}.#{field}", ex) 33 | end 34 | end 35 | 36 | class WriteError < CustomException 37 | def initialize(@klass, @field, ex : Exception) 38 | super("Failed to write #{klass}.#{field}", ex) 39 | end 40 | end 41 | end 42 | --------------------------------------------------------------------------------