├── .crystal-version ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── byte_len.cr ├── mapping.cr ├── refactor.cr ├── shard.yml ├── spec ├── bitfields_spec.cr └── spec_helper.cr └── src ├── bitfields.cr └── bitfields └── version.cr /.crystal-version: -------------------------------------------------------------------------------- 1 | 0.30.1 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Isaac Sloan 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 | # Bit Fields for Crystal-Lang 2 | 3 | Pure [Crystal](https://crystal-lang.org/) implementation of Bit Fields. Handles encoding/decoding of bytes. 4 | 5 | ## Installation 6 | 7 | 1. Add the dependency to your `shard.yml`: 8 | ```yaml 9 | dependencies: 10 | bitfields: 11 | github: elorest/bitfields 12 | ``` 13 | 2. Run `shards install` 14 | 15 | ## Usage 16 | 17 | ```crystal 18 | require "bitfields" 19 | 20 | class CrossBit < BitFields 21 | bf rpms : UInt32, 32 22 | bf temp : UInt8, 4 23 | bf psi : UInt16, 9 24 | bf power : UInt8, 1 25 | bf lights : UInt8, 2 26 | end 27 | 28 | crossbit = CrossBit.new(Bytes[109, 121, 110, 97, 109, 245]) 29 | crossbit.temp #=> 13_u8 30 | crossbit.psi #=> 342_u16 31 | crossbit.power #=> 1_u8 32 | crossbit.lights #=> 3_u8 33 | crossbit.to_slice #=> Bytes[109, 121, 110, 97, 109, 245] 34 | crossbit.to_s #=> |lights|power|psi|temp|rpms| 35 | #=> |11|1|101010110|1101|01100001011011100111100101101101| 36 | crossbit.power = 0 #=> 0_u8 37 | crossbit.to_slice #=> Bytes[109, 121, 110, 97, 109, 213] 38 | ``` 39 | 40 | ## Contributing 41 | 42 | 1. Fork it () 43 | 2. Create your feature branch (`git checkout -b my-new-feature`) 44 | 3. Commit your changes (`git commit -am 'Add some feature'`) 45 | 4. Push to the branch (`git push origin my-new-feature`) 46 | 5. Create a new Pull Request 47 | 48 | ## Contributors 49 | 50 | - [Isaac Sloan](https://github.com/elorest) - creator and maintainer 51 | -------------------------------------------------------------------------------- /byte_len.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | 3 | def byte_len(sbit, bit_length) 4 | ((bit_length + sbit%8) / 8.0).ceil.to_i 5 | end 6 | 7 | describe "byte_length" do 8 | it "should return 1" do 9 | byte_len(32, 8).should eq 1 10 | end 11 | it "should return 2" do 12 | byte_len(32, 16).should eq 2 13 | end 14 | it "should return 2" do 15 | byte_len(32, 12).should eq 2 16 | end 17 | it "should return 2" do 18 | byte_len(36, 8).should eq 2 19 | end 20 | it "should return 2" do 21 | byte_len(36, 12).should eq 2 22 | end 23 | it "should return 3" do 24 | byte_len(36, 16).should eq 3 25 | end 26 | it "should return 3" do 27 | byte_len(36, 15).should eq 3 28 | end 29 | it "should return 3" do 30 | byte_len(36, 17).should eq 3 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mapping.cr: -------------------------------------------------------------------------------- 1 | # module BitFields 2 | # macro mapping(properties, strict = false) 3 | # # {% for key, value in properties %} 4 | # # def {{key.var}} 5 | # # puts {{value}} 6 | # # puts "{{key.type}}" 7 | # # end 8 | # # {% end %} 9 | # end 10 | # end 11 | 12 | class BFTest 13 | @@hello = Array(Int32).new 14 | # @@hello = "" 15 | @@hello << 12 16 | puts @@hello 17 | @@hello << 4 18 | puts @@hello 19 | @@hello << 8 20 | puts @@hello 21 | @@hello << 4 22 | puts @@hello 23 | @@hello << 4 24 | puts @@hello 25 | 26 | # BitFields.mapping( 27 | # name : UInt8 => 5, 28 | # house : UInt8 => 3, 29 | # ) 30 | def self.hello 31 | @@hello 32 | end 33 | end 34 | 35 | puts BFTest.hello 36 | -------------------------------------------------------------------------------- /refactor.cr: -------------------------------------------------------------------------------- 1 | annotation BitField 2 | end 3 | 4 | class BitFields 5 | @bytes : Bytes 6 | 7 | def initialize(b : Bytes) 8 | @bytes = b.clone 9 | end 10 | 11 | macro bf(field, length) 12 | @[BitField(length: "{{field.var}}_{{length}}")] 13 | def {{field.var}} 14 | range = ranges("{{field.var}}") 15 | puts range.inspect 16 | buffer = 0_u64 17 | @bytes[range[:sbyte], range[:byte_count]].each.with_index do |b, i| 18 | buffer ^= (b.to_u64 << i*8) 19 | end 20 | buffer >>= (range[:sbit] % 8) 21 | buffer &= (2**{{length}}-1) 22 | {{field.type}}.new(buffer) 23 | end 24 | end 25 | 26 | # starting_bit 27 | # bit_length 28 | # byte_length = (bit_length/8.0).ceil.to_i 29 | 30 | def fields 31 | fields = Hash(String, Int32).new 32 | {% for ivar in @type.methods %} 33 | {% if ann = ivar.annotation(BitField) %} 34 | f, l = {{ann[:length]}}.split("_") 35 | fields[f] = l.to_i 36 | {% end %} 37 | {% end %} 38 | fields 39 | end 40 | 41 | def lengths 42 | fields.values 43 | end 44 | 45 | def ranges(f) 46 | index = fields.keys.index(f) || 0 47 | s_bit = lengths[0...index].sum 48 | e_bit = lengths[index] + s_bit 49 | {sbit: s_bit, ebit: e_bit, sbyte: s_bit/8, byte_count: (lengths[index]/8.0).ceil.to_i} 50 | end 51 | end 52 | 53 | class CrossBit < BitFields 54 | bf rpms : UInt32, 32 55 | bf temp : UInt8, 4 56 | bf psi : UInt16, 9 57 | bf power : UInt8, 1 58 | bf lights : UInt8, 2 59 | bf v1 : UInt16, 16 60 | bf v2 : UInt8, 4 61 | bf v3 : UInt8, 8 62 | bf v4 : UInt8, 4 63 | bf v5 : UInt8, 8 64 | end 65 | 66 | bytes = Bytes[109, 121, 110, 97, 221, 181, 220, 0, 28, 156, 9] 67 | crossbit = CrossBit.new(bytes) 68 | 69 | # crossbit = CrossBit.new(Bytes[109, 121, 110, 97, 221, 181, 220, 0, 113, 101, 38]) 70 | puts crossbit.rpms 71 | puts crossbit.temp # => 13_u8 72 | puts crossbit.psi # => 342_u16 73 | puts crossbit.power # => 1_u8 74 | puts crossbit.lights # => 3_u8 75 | puts crossbit.v1 76 | puts crossbit.v2 77 | puts crossbit.v3 78 | puts crossbit.v4 79 | puts crossbit.v5 80 | 81 | # crossbit.to_slice #=> Bytes[109, 121, 110, 97, 109, 245] 82 | 83 | # class TestB < BitFields 84 | # bf f1 : UInt16, 10 85 | # bf f2 : UInt8, 2 86 | # bf f3 : UInt8, 4 87 | # end 88 | # 89 | # b = TestB.new(Bytes[234, 201]) 90 | # 91 | # # puts b.f1 92 | # # puts b.f2 93 | # # puts b.f3 94 | # puts b.f1 95 | # puts b.fields 96 | # # puts b.bit_range("f3") 97 | # # puts b.byte_range("f3") 98 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: bitfields 2 | version: 1.2.1 3 | 4 | authors: 5 | - Isaac Sloan 6 | 7 | crystal: "> 0.31.1, < 2.0" 8 | 9 | license: MIT 10 | -------------------------------------------------------------------------------- /spec/bitfields_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | class CrossBit < BitFields 4 | bf rpms : UInt32, 32 5 | bf temp : UInt8, 4 6 | bf psi : UInt16, 9 7 | bf power : UInt8, 1 8 | bf lights : UInt8, 2 9 | bf v1 : UInt16, 16 10 | bf v2 : UInt8, 4 11 | bf v3 : UInt8, 8 12 | bf v4 : UInt8, 4 13 | bf v5 : UInt8, 8 14 | end 15 | 16 | describe BitFields do 17 | describe "Class" do 18 | describe "byte_length" do 19 | it "should return 1" do 20 | BitFields.byte_len(32, 8).should eq 1 21 | end 22 | it "should return 2" do 23 | BitFields.byte_len(32, 16).should eq 2 24 | end 25 | it "should return 2" do 26 | BitFields.byte_len(32, 12).should eq 2 27 | end 28 | it "should return 2" do 29 | BitFields.byte_len(36, 8).should eq 2 30 | end 31 | it "should return 2" do 32 | BitFields.byte_len(36, 12).should eq 2 33 | end 34 | it "should return 3" do 35 | BitFields.byte_len(36, 16).should eq 3 36 | end 37 | it "should return 3" do 38 | BitFields.byte_len(36, 15).should eq 3 39 | end 40 | it "should return 3" do 41 | BitFields.byte_len(36, 17).should eq 3 42 | end 43 | end 44 | end 45 | 46 | describe "Instance" do 47 | bytes = Bytes[109, 121, 110, 97, 221, 181, 220, 0, 28, 252, 105] 48 | crossbit = CrossBit.new(bytes) 49 | 50 | it "should read 32 bits into a UInt32" do 51 | crossbit.rpms.should eq 1634629997 52 | crossbit.rpms.class.should eq UInt32 53 | end 54 | 55 | it "should read first 4 bits into a UInt8" do 56 | crossbit.temp.should eq 13 57 | crossbit.temp.class.should eq UInt8 58 | end 59 | 60 | it "should read the next 9 bits from across bytes into a UInt16" do 61 | crossbit.psi.should eq 349 62 | crossbit.psi.class.should eq UInt16 63 | end 64 | 65 | it "should read the next bit into UInt8" do 66 | crossbit.power.should eq 1 67 | crossbit.power.class.should eq UInt8 68 | end 69 | 70 | it "should read the next 2 bits to end of byte into UInt8" do 71 | crossbit.lights.should eq 2 72 | crossbit.lights.class.should eq UInt8 73 | end 74 | 75 | it "should read v1 correctly" do 76 | crossbit.v1.should eq 220 77 | end 78 | 79 | it "should read v2 correctly" do 80 | crossbit.v2.should eq 12 81 | end 82 | 83 | it "should read v3 correctly" do 84 | crossbit.v3.should eq 193 85 | end 86 | 87 | it "should read v4 correctly" do 88 | crossbit.v4.should eq 15 89 | end 90 | 91 | it "should read v5 correctly" do 92 | crossbit.v5.should eq 105 93 | end 94 | 95 | it "should output modified bytes if values are modified" do 96 | crossbit.lights = 1 97 | crossbit.psi = 319 98 | crossbit.v1 = 225 99 | crossbit.v2 = 13 100 | crossbit.v3 = 173 101 | crossbit.v4 = 14 102 | crossbit.v5 = 115 103 | new_bytes = Bytes[109, 121, 110, 97, 253, 115, 225, 0, 221, 234, 115] 104 | crossbit.to_slice.should eq new_bytes 105 | end 106 | 107 | it "should truncate bits to the correct size instead of overflowing." do 108 | crossbit.v4 = 254 109 | new_bytes = Bytes[109, 121, 110, 97, 253, 115, 225, 0, 221, 234, 115] 110 | crossbit.to_slice.should eq new_bytes 111 | end 112 | 113 | it "should set all 32 bits to the correct size instead of overflowing." do 114 | c2 = CrossBit.new(crossbit.to_slice) 115 | c2.rpms = 4294967295 116 | new_bytes = Bytes[255, 255, 255, 255, 253, 115, 225, 0, 221, 234, 115] 117 | c2.to_slice.should eq new_bytes 118 | end 119 | 120 | it "should read in new bytes and print out correct values" do 121 | c2 = CrossBit.new(crossbit.to_slice) 122 | 123 | crossbit.lights.should eq 1 124 | crossbit.psi.should eq 319 125 | crossbit.v1.should eq 225 126 | crossbit.v2.should eq 13 127 | crossbit.v3.should eq 173 128 | crossbit.v4.should eq 14 129 | crossbit.v5.should eq 115 130 | end 131 | 132 | it "should return string printout" do 133 | str = "rpms -- Binary:01100001011011100111100101101101 Hex:616E796D Decimal:1634629997\ntemp -- Binary:1101 Hex:D Decimal:13\npsi -- Binary:100111111 Hex:13F Decimal:319\npower -- Binary:1 Hex:1 Decimal:1\nlights -- Binary:01 Hex:1 Decimal:1\nv1 -- Binary:0000000011100001 Hex:E1 Decimal:225\nv2 -- Binary:1101 Hex:D Decimal:13\nv3 -- Binary:10101101 Hex:AD Decimal:173\nv4 -- Binary:1110 Hex:E Decimal:14\nv5 -- Binary:01110011 Hex:73 Decimal:115" 134 | crossbit.to_s.should eq str 135 | end 136 | 137 | it "should return tuple" do 138 | t = {rpms: {value: 1634629997_u32, length: 32}, temp: {value: 13_u8, length: 4}, psi: {value: 319_u16, length: 9}, power: {value: 1_u8, length: 1}, lights: {value: 1_u8, length: 2}, v1: {value: 225_u16, length: 16}, v2: {value: 13_u8, length: 4}, v3: {value: 173_u8, length: 8}, v4: {value: 14_u8, length: 4}, v5: {value: 115_u8, length: 8}} 139 | crossbit.to_t.should eq t 140 | #{name: {value: "isaac", length: 5}} 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/**" 3 | -------------------------------------------------------------------------------- /src/bitfields.cr: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | 3 | class BitFields 4 | COLORS = { {148, 0, 211}, {75, 0, 130}, {0, 0, 255}, {0, 255, 0}, {255, 255, 0}, {255, 127, 0}, {255, 0, 0} } 5 | 6 | def self.byte_len(sbit, bit_len) 7 | ((bit_len + sbit%8)/8.0).ceil.to_i 8 | end 9 | 10 | 11 | macro inherited 12 | FIELDS = {} of Nil => Nil 13 | LENGTHS = [] of Int32 14 | 15 | macro finished 16 | process_fields 17 | end 18 | end 19 | 20 | macro bf(field, length) 21 | {% FIELDS[field.var] = field.type %} 22 | {% LENGTHS << length %} 23 | end 24 | 25 | macro process_fields 26 | BIT_COUNT = LENGTHS.sum 27 | BYTE_COUNT = (BIT_COUNT/8.0).ceil.to_i 28 | 29 | {% for name, type, index in FIELDS %} 30 | getter {{name.id}} : {{type.id}} 31 | 32 | def {{name.id}}=(v : {{type.id}}) 33 | @{{name.id}} = (v & {{2u64**LENGTHS[index]-1}}) 34 | end 35 | {% end %} 36 | 37 | def initialize(bytes : Bytes) 38 | %bytes = bytes.clone 39 | %sbit = 0 40 | {% for name, type, index in FIELDS %} 41 | %bit_len = LENGTHS[{{index}}] 42 | %byte_len = self.class.byte_len(%sbit, %bit_len) 43 | %buffer = 0_u64 44 | %bytes[%sbit//8, %byte_len].each.with_index do |b, i| 45 | %buffer ^= (b.to_u64 << i*8) 46 | end 47 | %buffer >>= (%sbit % 8) 48 | %buffer &= (2u64**%bit_len-1) 49 | %sbit += %bit_len 50 | @{{name.id}} = {{type.id}}.new(%buffer) 51 | {% end %} 52 | end 53 | 54 | def to_slice 55 | %bytes = Bytes.new(BYTE_COUNT) 56 | %byte_index = 0 57 | %buffer : UInt64 = 0 58 | %head = 0 59 | {% for name, type, index in FIELDS %} 60 | %buffer ^= ({{name.id}}.to_u64 << %head) 61 | %head += {{LENGTHS[index]}} 62 | while %head > 8 63 | %bytes[%byte_index] = %buffer.to_u8! 64 | %buffer >>= 8 65 | %byte_index += 1 66 | %head -= 8 67 | end 68 | {% end %} 69 | %bytes[%byte_index] = %buffer.to_u8 70 | %bytes 71 | end 72 | 73 | def to_t 74 | { 75 | {% for name, type, index in FIELDS %} 76 | {{name.id}}: {value: @{{name.id}}, length: {{LENGTHS[index]}} }, 77 | {% end %} 78 | } 79 | end 80 | 81 | def to_s 82 | %str_arr = Array(String).new 83 | {% for name, type, index in FIELDS %} 84 | %str_arr << "{{name.id}} -- Binary:#{sprintf("%0{{LENGTHS[index]}}b", {{name.id}})} Hex:#{sprintf("%X", {{name.id}})} Decimal:#{ {{name.id}} }" 85 | {% end %} 86 | %str_arr.join("\n") 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /src/bitfields/version.cr: -------------------------------------------------------------------------------- 1 | class Bitfields 2 | VERSION = "1.2.1" 3 | end 4 | --------------------------------------------------------------------------------