├── .bnsignore ├── .gitignore ├── History.txt ├── LICENSE ├── README.md ├── Rakefile ├── TODO ├── bit-struct.gemspec ├── examples ├── ara-player-data.rb ├── bignum.rb ├── bits.rb ├── byte-bdy.rb ├── field-ripper.rb ├── fixed-point.rb ├── ip.rb ├── longlong.rb ├── md.rb ├── modular-def.rb ├── native.rb ├── nested-block.rb ├── nested.rb ├── pad.rb ├── ping-recv.rb ├── ping.rb ├── player-data.rb ├── raw.rb ├── rest.rb ├── switch-endian.rb └── vector.rb ├── lib ├── bit-struct.rb └── bit-struct │ ├── bit-struct.rb │ ├── char-field.rb │ ├── field.rb │ ├── fields.rb │ ├── float-field.rb │ ├── hex-octet-field.rb │ ├── nested-field.rb │ ├── octet-field.rb │ ├── pad-field.rb │ ├── signed-field.rb │ ├── text-field.rb │ ├── unsigned-field.rb │ ├── vector-field.rb │ ├── vector.rb │ └── yaml.rb └── test ├── test-endian.rb ├── test-vector.rb └── test.rb /.bnsignore: -------------------------------------------------------------------------------- 1 | # The list of files that should be ignored by Mr Bones. 2 | # Lines that start with '#' are comments. 3 | # 4 | # A .gitignore file can be used instead by setting it as the ignore 5 | # file in your Rakefile: 6 | # 7 | # PROJ.ignore_file = '.gitignore' 8 | # 9 | # For a project with a C extension, the following would be a good set of 10 | # exclude patterns (uncomment them if you want to use them): 11 | # *.[oa] 12 | # *~ 13 | announcement.txt 14 | coverage 15 | doc 16 | pkg 17 | *.bck 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | bit-struct 0.17 2 | 3 | - updated for ruby 3.0 4 | 5 | bit-struct 0.16 6 | 7 | - updated for ruby 2.4 8 | 9 | bit-struct 0.15 10 | 11 | - modernize rakefile and gemspec 12 | - switch to minitest 13 | 14 | bit-struct 0.14 15 | 16 | - updated for ruby 2.0 17 | 18 | bit-struct 0.13.6 19 | 20 | - Fixed bug in #to_a. Thanks Michael Edgar. 21 | 22 | bit-struct 0.13 23 | 24 | - Packaged as a gem using bones. 25 | 26 | - Added nest fields defined with blocks. (Markus Fischer) 27 | 28 | - Vector length may be specified as argument or :length option. (Markus Fischer) 29 | 30 | bit-struct 0.12 31 | 32 | - Added vectors. 33 | 34 | - BitStruct#initialize can take an IO argument. 35 | 36 | bit-struct 0.11 37 | 38 | - Allow unaligned fields to cross up to two byte boundaries. 39 | See examples/byte-bdy.rb. 40 | 41 | bit-struct 0.10 42 | 43 | - Fixed a bug when calling #to_yaml on a BitStruct with a pad field. Thanks 44 | to Jay Reitz for reporting it and providing the solution. 45 | 46 | - Added BitStruct.default_options. Particularly useful for default endian 47 | settings--see examples/native.rb. 48 | 49 | - Fixed a bug that prevented warning about field name conflicts with 50 | existing methods when the name is given as a symbol. 51 | 52 | - Fixed point fields may now have fractional divisors (like :fixed => 0.001). 53 | 54 | bit-struct 0.9 55 | 56 | - Added examples/field-ripper.rb. 57 | 58 | - Added BitStruct#field_by_name. 59 | 60 | - Added more warnings about nested field accessors returning a *copy* of the 61 | nested data. 62 | 63 | - Added "pad" fields. See documentation in pad-field.rb and examples/pad.rb. 64 | 65 | - The #initial_value method now yields the value to a block, if given. 66 | 67 | - A BitStruct class is only closed when an instance is created. (After this 68 | point no fields can be added.) Formerly, a class was closed when any method 69 | requested the total length of the structure. This change makes it easier 70 | to add groups of fields modularly, each with their own initial_value block. 71 | 72 | - Added examples/modular-def.rb to explain how to factor a BitStruct 73 | definition into modules. 74 | 75 | bit-struct 0.8 76 | 77 | - Signed fields can now (like unsigned fields) be any multiple of 8 bits, and are accessed as fixnums or bignums, as needed. 78 | 79 | - It's easier to subclass BitStruct::OctetField. BitStruct::HexOctetField is now implemented in this way, just by defining three constants and two trivial class methods. 80 | 81 | bit-struct 0.7 82 | 83 | - BitStruct.describe now takes an option hash, and the :expand=>true option causes nested fields to be expanded in the description. 84 | 85 | - Unsigned integer fields can now be any multiple of 8 bytes: 8, 16, 24, 32, 40, ... (Fields of 1..15 bits in length are of course still supported.) 86 | 87 | - Added the :endian => :little option to signed integer, unsigned integer, and float fields. The full set of endian options is now [:little, :big, :network, :native]. The default is always :network. 88 | 89 | - Option names may be strings or symbols. Values that can be symbols can also be strings. 90 | 91 | - Added examples/bignum.rb. 92 | 93 | - Added support for the YAML in ruby 1.8.2 (the YAML in 1.8.4 was already supported). 94 | 95 | bit-struct 0.6 96 | 97 | - Added the :endian => :native option for numerical fields (signed, unsigned, float). 98 | 99 | - Fixed error message with 9..15 bit fields aligned on byte boundary. 100 | 101 | - The #initial_value is now inherited (before applying defaults). 102 | 103 | - New examples: raw.rb, native.rb. 104 | 105 | bit-struct 0.5 106 | 107 | - Integer fields may now cross byte boundaries, as long as the field fits within two whole bytes. 108 | 109 | bit-struct 0.4 110 | 111 | - Fixed a bug in reading text and char fields in YAML: if the value was interpreted by YAML as something other than a string, an error would result. 112 | 113 | - When BitStructs are loaded from yaml, the key is treated as a setter method, rather than a field. This is useful in case there is a field that needs special setters to accept humanly readable input. (For example, a char field with length-prefixed subfields.) 114 | 115 | bit-struct 0.3 116 | 117 | - BitStruct classes are now YAML friendly. 118 | 119 | - The default behavior of BitStruct#inspect and BitStruct#inspect_detailed is changed to print out the "rest" field, if any. This can be disabled by passing inspect an options hash with :include_rest => false. See BitStruct::DEFAULT_INSPECT_OPTS and BitStruct::DETAILED_INSPECT_OPTS. See examples/ip.rb and examples/rest.rb. 120 | 121 | - The default behavior of BitStruct#to_h is changed to include the "rest" field. As above, this can be disabled by passing to_h an options hash with :include_rest => false. 122 | 123 | - The default behavior of BitStruct#to_a is changed to include the "rest" field. As above, this can be disabled by passing false as an argument to to_a. 124 | 125 | bit-struct 0.2 126 | 127 | - first public release 128 | 129 | bit-struct 0.1 130 | 131 | - first release 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | bit-struct is copyrighted free software by Joel VanderWerf 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a) place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b) use the modified software only within your corporation or 18 | organization. 19 | 20 | c) give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d) make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a) distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b) accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c) give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d) make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitStruct # 2 | 3 | Class for packed binary data stored in ruby Strings. BitStruct accessors, generated from user declared fields, use pack/unpack to treat substrings as fields with a specified portable format. 4 | 5 | Field types include: 6 | 7 | * signed and unsigned integer (1..16 bits, or 24, 32, 40, 48... bits) 8 | 9 | * numeric fields (signed, unsigned, float) can be designated as any of the following endians: little, big, native, network (default) 10 | 11 | * fixed point, with arbitrary scale factor 12 | 13 | * fixed length character array 14 | 15 | * null-terminated character array for printable text 16 | 17 | * octets (hex and decimal representation options; useful for IP and MAC addrs) 18 | 19 | * float 20 | 21 | * nested BitStruct 22 | 23 | * vectors of embedded BitStructs 24 | 25 | * free-form "rest" field (e.g., for the variable-size payload of a packet) 26 | 27 | Field options (specifiable as :foo => val or "foo" => val) include: 28 | 29 | * *display_name*: used in BitStruct#inspect_detailed and BitStruct#describe outputs. 30 | 31 | * *default*: default field value 32 | 33 | * *format*: alternate format string for inspect 34 | 35 | * *endian*: for byte ordering of numeric fields (unsigned, signed, float): little, big, native, network (default) 36 | 37 | * *fixed*: float stored as fixed-point integer, with specified scale factor 38 | 39 | 40 | ## Installation ## 41 | 42 | For .gem: 43 | 44 | gem install bit-struct 45 | 46 | For .tgz, unpack and then: 47 | 48 | ruby install.rb config 49 | ruby install.rb setup 50 | ruby install.rb install 51 | 52 | ## Uses ## 53 | 54 | BitStruct is useful for defining packets used in network protocols. This is especially useful for raw IP--see examples/ping-recv.rb. All multibyte numeric fields are stored by default in network order. 55 | 56 | BitStruct is most efficient when your data is primarily treated as a binary string, and only secondarily treated as a data structure. (For instance, you are routing packets from one socket to another, possibly looking at one or two fields as it passes through or munging some headers.) If accessor operations are a bottleneck, a better approach is to define a class that wraps an array and uses pack/unpack when the object needs to behave like a binary string. 57 | 58 | ## Features ## 59 | 60 | * Extensible with user-defined field classes. 61 | 62 | * Fields are fully introspectable and can be defined programmatically. 63 | 64 | * BitStruct.describe prints out documentation of all the fields of a BitStruct subclass, based on declarations. This is useful for communicating with developers who are not using ruby, but need to talk the same protocols. See Example, below. 65 | 66 | * Fields are inherited by subclasses. (The free-form "rest" field does not inherit, because it usually represents a payload whose structure is defined in subclasses using the fixed-size fields.) 67 | 68 | * BitStruct#inspect and BitStruct#inspect_detailed can be used for prettified display of contents. (More generally, BitStruct#inspect takes some options that control formatting and detail level.) See Example, below. 69 | 70 | * BitStruct inherits from String, so all the usual methods are available, and string-sharing (copy-on-write) is in effect. 71 | 72 | * Easy access to a "prototype" instance of each BitStruct subclass, from which all instances of that subclass are initialized as a copy (in the absence of other initialization parameters, such as a hash, a string, or a block). See BitStruct.initial_value, and BitStruct#initialize. See Example, below. 73 | 74 | * Easy conversion to and from hashes, using BitStruct#to_h and BitStruct.new. 75 | 76 | * BitStructs can persist using Marshal (a BitStruct is after all just a string) or using YAML (with human readable representation of the fields). 77 | 78 | * Includes tests, examples, and rdoc API documentation. 79 | 80 | ## Limitations ## 81 | 82 | * Fields that are not aligned on byte boundaries may cross no more than two bytes boundaries. (See examples/byte-bdy.rb.) 83 | 84 | * No variable length fields (except the #rest field). 85 | 86 | ## Future plans ## 87 | 88 | * Currently, the library is written in pure ruby. The implementation uses Array#pack and String#unpack calls, as well as shifting and masking in pure ruby. Future versions will optionally generate a customized C extension for better efficiency. 89 | 90 | * A debug mode in which a class identifier is prepended to every BitStruct, so that protocol errors can be detected. (This feature has been implemented in an app that uses BitStruct, but needs to be refactored into the BitStruct library itself.) 91 | 92 | * Remove field size and alignment limitations. 93 | 94 | ## Example ## 95 | 96 | An IP packet can be defined and used like this: 97 | 98 | ```ruby 99 | require 'bit-struct' 100 | 101 | class IP < BitStruct 102 | unsigned :ip_v, 4, "Version" 103 | unsigned :ip_hl, 4, "Header length" 104 | unsigned :ip_tos, 8, "TOS" 105 | unsigned :ip_len, 16, "Length" 106 | unsigned :ip_id, 16, "ID" 107 | unsigned :ip_off, 16, "Frag offset" 108 | unsigned :ip_ttl, 8, "TTL" 109 | unsigned :ip_p, 8, "Protocol" 110 | unsigned :ip_sum, 16, "Checksum" 111 | octets :ip_src, 32, "Source addr" 112 | octets :ip_dst, 32, "Dest addr" 113 | rest :body, "Body of message" 114 | 115 | note " rest is application defined message body" 116 | 117 | initial_value.ip_v = 4 118 | initial_value.ip_hl = 5 119 | end 120 | 121 | ip = IP.new 122 | ip.ip_tos = 0 123 | ip.ip_len = 0 124 | ip.ip_id = 0 125 | ip.ip_off = 0 126 | ip.ip_ttl = 255 127 | ip.ip_p = 255 128 | ip.ip_sum = 0 129 | ip.ip_src = "192.168.1.4" 130 | ip.ip_dst = "192.168.1.255" 131 | ip.body = "This is the payload text." 132 | ip.ip_len = ip.length 133 | 134 | puts ip.inspect 135 | puts "-"*50 136 | puts ip.inspect_detailed 137 | puts "-"*50 138 | puts IP.describe 139 | ``` 140 | 141 | (Note that you can also construct an IP packet by passing a string to new, or by passing a hash of field,value pairs, or by providing a block that is yielded the new BitStruct.) 142 | 143 | The output of this fragment is: 144 | 145 | # 146 | -------------------------------------------------- 147 | IP: 148 | Version = 4 149 | Header length = 5 150 | TOS = 0 151 | Length = 45 152 | ID = 0 153 | Frag offset = 0 154 | TTL = 255 155 | Protocol = 255 156 | Checksum = 0 157 | Source addr = "192.168.1.4" 158 | Dest addr = "192.168.1.255" 159 | Body of message = "This is the payload text." 160 | -------------------------------------------------- 161 | 162 | Description of IP Packet: 163 | byte: type name [size] description 164 | ---------------------------------------------------------------------- 165 | @0: unsigned ip_v [ 4b] Version 166 | @0: unsigned ip_hl [ 4b] Header length 167 | @1: unsigned ip_tos [ 8b] TOS 168 | @2: unsigned ip_len [ 16b] Length 169 | @4: unsigned ip_id [ 16b] ID 170 | @6: unsigned ip_off [ 16b] Frag offset 171 | @8: unsigned ip_ttl [ 8b] TTL 172 | @9: unsigned ip_p [ 8b] Protocol 173 | @10: unsigned ip_sum [ 16b] Checksum 174 | @12: octets ip_src [ 32b] Source addr 175 | @16: octets ip_dst [ 32b] Dest addr 176 | rest is application defined message body 177 | 178 | ## Web site ## 179 | 180 | The current version of this software can be found at https://github.com/vjoel/bit-struct. 181 | 182 | ## License ## 183 | 184 | This software is distributed under the Ruby license. See http://www.ruby-lang.org. 185 | 186 | ## Author ## 187 | 188 | Joel VanderWerf, mailto:vjoel@users.sourceforge.net 189 | Copyright (c) 2005-2014, Joel VanderWerf. 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | 4 | PRJ = "bit-struct" 5 | 6 | def version 7 | @version ||= begin 8 | require 'bit-struct' 9 | warn "BitStruct::VERSION not a string" unless 10 | BitStruct::VERSION.kind_of? String 11 | BitStruct::VERSION 12 | end 13 | end 14 | 15 | def tag 16 | @tag ||= "#{PRJ}-#{version}" 17 | end 18 | 19 | desc "Run unit tests" 20 | Rake::TestTask.new :test do |t| 21 | t.libs << "lib" 22 | t.test_files = FileList["test/*.rb"] 23 | end 24 | 25 | desc "Commit, tag, and push repo; build and push gem" 26 | task :release => "release:is_new_version" do 27 | require 'tempfile' 28 | 29 | sh "gem build #{PRJ}.gemspec" 30 | 31 | file = Tempfile.new "template" 32 | begin 33 | file.puts "release #{version}" 34 | file.close 35 | sh "git commit --allow-empty -a -v -t #{file.path}" 36 | ensure 37 | file.close unless file.closed? 38 | file.unlink 39 | end 40 | 41 | sh "git tag #{tag}" 42 | sh "git push" 43 | sh "git push --tags" 44 | 45 | sh "gem push #{tag}.gem" 46 | end 47 | 48 | namespace :release do 49 | desc "Diff to latest release" 50 | task :diff do 51 | latest = `git describe --abbrev=0 --tags --match '#{PRJ}-*'`.chomp 52 | sh "git diff #{latest}" 53 | end 54 | 55 | desc "Log to latest release" 56 | task :log do 57 | latest = `git describe --abbrev=0 --tags --match '#{PRJ}-*'`.chomp 58 | sh "git log #{latest}.." 59 | end 60 | 61 | task :is_new_version do 62 | abort "#{tag} exists; update version!" unless `git tag -l #{tag}`.empty? 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | rest :name, :terminator => ... 2 | 3 | easy way to define wrappers 4 | 5 | generate C code from bit-struct spec? 6 | 7 | write extension to extract and insert bit-fields, and use that instead 8 | of pack/unpack 9 | 10 | variable-length embedded fields, with referenced length field 11 | 12 | terminated arrays? 13 | 14 | use with mmap; in general: bit struct that references a position in another string or string-like object (e.g. a Mmap). 15 | 16 | use require 'ipaddr'? 17 | 18 | See ideas in 19 | 20 | http://www.notwork.org/~gotoken/ruby/p/as-is/pack.rb 21 | 22 | use setbyte and getbyte ? 23 | -------------------------------------------------------------------------------- /bit-struct.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require 'bit-struct' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "bit-struct" 9 | s.version = BitStruct::VERSION 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") 12 | s.authors = ["Joel VanderWerf"] 13 | s.date = Time.now.strftime "%Y-%m-%d" 14 | s.description = "Library for packed binary data stored in ruby Strings. Useful for accessing fields in network packets and binary files." 15 | s.email = "vjoel@users.sourceforge.net" 16 | s.extra_rdoc_files = ["History.txt", "README.md", "LICENSE"] 17 | s.files = Dir[ 18 | "History.txt", "README.md", "LICENSE", "Rakefile", 19 | "lib/**/*.rb", 20 | "examples/*.{rb,txt}", 21 | "test/*.rb" 22 | ] 23 | s.test_files = Dir["test/*.rb"] 24 | s.homepage = "https://github.com/vjoel/bit-struct" 25 | s.license = "Ruby" 26 | s.rdoc_options = [ 27 | "--quiet", "--line-numbers", "--inline-source", 28 | "--title", "BitStruct", "--main", "README.md"] 29 | s.require_paths = ["lib"] 30 | s.summary = "Library for packed binary data stored in ruby Strings" 31 | end 32 | -------------------------------------------------------------------------------- /examples/ara-player-data.rb: -------------------------------------------------------------------------------- 1 | class PlayerData 2 | class << self 3 | def create(*a) 4 | new(a.pack(FORMAT)) 5 | end 6 | end 7 | 8 | SPEC = [ 9 | %w( pid i ), 10 | %w( x_position f ), 11 | %w( y_position f ), 12 | %w( z_position f ), 13 | %w( foobar i ), 14 | ] 15 | 16 | ATTRIBUTES = SPEC.map{|s| s.first} 17 | 18 | FORMAT = SPEC.map{|s| s.last}.join 19 | 20 | SPEC.each_with_index do |spec, ix| 21 | at, format = spec 22 | eval <<-src 23 | def #{ at } 24 | @#{ at } ||= @data[#{ ix }] 25 | end 26 | def #{ at }= value 27 | raise TypeError unless self.#{ at }.class == value.class 28 | uncache 29 | @#{ at } = @data[#{ ix }] = value 30 | end 31 | src 32 | end 33 | 34 | def update buffer 35 | uncache 36 | @data = buffer.unpack FORMAT 37 | end 38 | alias initialize update 39 | def uncache 40 | @to_s = @to_bin = nil 41 | end 42 | def to_s 43 | @to_s ||= ATTRIBUTES.inject(''){|s,a| s << "#{ a } : #{ send a }, " }.chop.chop 44 | end 45 | def to_bin 46 | @to_bin ||= @data.pack(FORMAT) 47 | end 48 | end 49 | 50 | id, x_position, y_position, z_position, foobar = 51 | 400, 1.0, 2.0, 3.0, 0b101010 52 | 53 | pd = PlayerData::create id, x_position, y_position, z_position, foobar 54 | 55 | p pd 56 | p pd.pid 57 | p pd.x_position 58 | p pd.foobar 59 | puts pd 60 | 61 | begin 62 | require 'timeout' 63 | n = 0 64 | sec = 10 65 | Timeout::timeout(sec) do 66 | loop do 67 | PlayerData::create id, x_position, y_position, z_position, foobar 68 | n += 1 69 | end 70 | end 71 | rescue Timeout::Error 72 | puts "creations per second : #{ n / sec }" 73 | end 74 | 75 | __END__ 76 | 77 | # 78 | 400 79 | 1.0 80 | 42 81 | pid : 400, x_position : 1.0, y_position : 2.0, z_position : 3.0, foobar : 42 82 | creations per second : 131746 83 | -------------------------------------------------------------------------------- /examples/bignum.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class C < BitStruct 4 | unsigned :x, 80, :endian => :big, :fixed => 1_000_000_000 5 | unsigned :y, 24, :endian => :native, :format => "0x%x" 6 | unsigned :z, 64, :format => "0x%x" # big-endian by default 7 | signed :w, 48 8 | end 9 | 10 | c = C.new 11 | 12 | c.x = 1.000_000_001 13 | c.y = 0xa00002 14 | c.z = 0x1234abcd5678efef 15 | c.w = -1 16 | 17 | p c 18 | #puts c.unpack("H*") 19 | -------------------------------------------------------------------------------- /examples/bits.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' # http://redshift.sourceforge.net/bit-struct 2 | 3 | class Bits < BitStruct 4 | unsigned :bit0, 1, "Foo bit" 5 | unsigned :bit1, 1, "Bar bit" 6 | unsigned :bit2, 1 7 | unsigned :bit3, 1 8 | unsigned :bit4, 1 9 | unsigned :bit5, 1 10 | unsigned :bit6, 1 11 | unsigned :bit7, 1 12 | end 13 | 14 | b = Bits.new 15 | 16 | b.bit3 = 1 17 | p b.bit3 # ==> 1 18 | p b.bit4 # ==> 0 19 | 20 | -------------------------------------------------------------------------------- /examples/byte-bdy.rb: -------------------------------------------------------------------------------- 1 | # an example where a field may crosse one or two byte boundaries 2 | # 3 | # try with arguments like 4, 12, and 13 to see the difference 4 | # 5 | # based on test case from Jon Hart 6 | 7 | require 'bit-struct' 8 | class Foo < BitStruct 9 | unsigned :a, 4 10 | unsigned :b, 8 11 | unsigned :c, (ARGV[0] || (raise "USAGE: #{$0} bits")).to_i 12 | end 13 | 14 | puts Foo.describe 15 | 16 | foo = Foo.new 17 | p foo 18 | p foo.unpack("B*").first.scan(/\d{8,8}/) 19 | puts 20 | 21 | foo.c = 3123 22 | p foo 23 | p foo.unpack("B*").first.scan(/\d{8,8}/) 24 | puts 25 | 26 | foo.c = (2**(ARGV[0].to_i)-1) 27 | p foo 28 | p foo.unpack("B*").first.scan(/\d{8,8}/) 29 | puts 30 | 31 | -------------------------------------------------------------------------------- /examples/field-ripper.rb: -------------------------------------------------------------------------------- 1 | # Example to show how to programmatically "cut and paste" some of the fields 2 | # from one BitStruct class into another. 3 | 4 | require 'bit-struct' 5 | 6 | class BS1 < BitStruct 7 | unsigned :f1, 8 8 | unsigned :f2, 8 9 | unsigned :f3, 8 10 | unsigned :f4, 8 11 | unsigned :f5, 8 12 | end 13 | 14 | class BS2 < BitStruct 15 | fields_to_add = BS1.fields.select {|f| f.name.to_s =~ /[234]/} 16 | 17 | fields_to_add.each do |field| 18 | add_field(field.name, field.length, field.options) 19 | end 20 | end 21 | 22 | puts BS2.describe 23 | -------------------------------------------------------------------------------- /examples/fixed-point.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class C < BitStruct 4 | unsigned :f1, 3, :fixed => 100 5 | unsigned :f2, 5, :fixed => 1000 6 | unsigned :f3, 8, :fixed => 10000 7 | unsigned :f4, 32, :fixed => 100000 8 | end 9 | 10 | c = C.new 11 | 12 | c.f1 = 0.03 13 | c.f2 = 0.005 14 | c.f3 = 0.0144 15 | c.f4 = 456.78912 16 | 17 | p c # ==> # 18 | -------------------------------------------------------------------------------- /examples/ip.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class IP < BitStruct 4 | unsigned :ip_v, 4, "Version" 5 | unsigned :ip_hl, 4, "Header length" 6 | unsigned :ip_tos, 8, "TOS" 7 | unsigned :ip_len, 16, "Length" 8 | unsigned :ip_id, 16, "ID" 9 | unsigned :ip_off, 16, "Frag offset" 10 | unsigned :ip_ttl, 8, "TTL" 11 | unsigned :ip_p, 8, "Protocol" 12 | unsigned :ip_sum, 16, "Checksum" 13 | octets :ip_src, 32, "Source addr" 14 | octets :ip_dst, 32, "Dest addr" 15 | rest :body, "Body of message" 16 | 17 | note " rest is application defined message body" 18 | 19 | initial_value.ip_v = 4 20 | initial_value.ip_hl = 5 21 | end 22 | 23 | if __FILE__ == $0 24 | ip1 = IP.new 25 | ip1.ip_tos = 0 26 | ip1.ip_len = 0 27 | ip1.ip_id = 0 28 | ip1.ip_off = 0 29 | ip1.ip_ttl = 255 30 | ip1.ip_p = 255 31 | ip1.ip_sum = 0 32 | ip1.ip_src = "192.168.1.4" 33 | ip1.ip_dst = "192.168.1.255" 34 | ip1.body = "This is the payload text." 35 | ip1.ip_len = ip1.length 36 | 37 | ip2 = IP.new do |ip| 38 | ip.ip_tos = 0 39 | ip.ip_len = 0 40 | ip.ip_id = 0 41 | ip.ip_off = 0 42 | ip.ip_ttl = 255 43 | ip.ip_p = 255 44 | ip.ip_sum = 0 45 | ip.ip_src = "192.168.1.4" 46 | ip.ip_dst = "192.168.1.255" 47 | ip.body = "This is the payload text." 48 | ip.ip_len = ip.length 49 | end 50 | 51 | ip3 = IP.new( 52 | :ip_tos => 0, 53 | :ip_len => 0, 54 | :ip_id => 0, 55 | :ip_off => 0, 56 | :ip_ttl => 255, 57 | :ip_p => 255, 58 | :ip_sum => 0, 59 | :ip_src => "192.168.1.4", 60 | :ip_dst => "192.168.1.255", 61 | :body => "This is the payload text." 62 | ) do |ip| 63 | ip.ip_len = ip.length 64 | end 65 | 66 | ip4 = IP.new(ip1) # Construct from a BitStruct (or String) 67 | 68 | raise unless ip1 == ip2 69 | raise unless ip1 == ip3 70 | raise unless ip1 == ip4 71 | 72 | ip = ip1 73 | 74 | puts ip.inspect 75 | puts "-"*50 76 | puts ip.inspect_detailed 77 | puts "-"*50 78 | puts 79 | puts "Description of IP Packet:" 80 | puts IP.describe 81 | end 82 | -------------------------------------------------------------------------------- /examples/longlong.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class MyPacket < BitStruct 4 | unsigned :x, 8*8, "The x field", :endian => :network 5 | # :network is the default, and it's the same as :big 6 | unsigned :y, 8*8, "The y field", :endian => :little 7 | end 8 | 9 | pkt = MyPacket.new 10 | pkt.x = 59843759843759843 11 | pkt.y = 59843759843759843 12 | 13 | p pkt.x # 59843759843759843 14 | p pkt.y # 59843759843759843 15 | 16 | p pkt 17 | # # 18 | p pkt.to_s 19 | # "\000\324\233\225\037\202\326\343\343\326\202\037\225\233\324\000" 20 | 21 | puts pkt.inspect_detailed 22 | # MyPacket: 23 | # The x field = 59843759843759843 24 | # The y field = 59843759843759843 25 | 26 | puts MyPacket.describe 27 | # byte: type name [size] description 28 | # ---------------------------------------------------------------------- 29 | # @0: unsigned x [ 8B] The x field 30 | # @8: unsigned y [ 8B] The y field 31 | -------------------------------------------------------------------------------- /examples/md.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class MD < BitStruct 4 | vector :row, :length => 3 do 5 | vector :col, :length => 10 do 6 | float :x, 32 7 | end 8 | end 9 | end 10 | 11 | md = MD.new 12 | rows = md.row 13 | row = rows[2] 14 | cols = row.col 15 | col = cols[7] 16 | col.x = 1.23 17 | cols[7] = col 18 | row.col = cols 19 | rows[2] = row 20 | md.row = rows 21 | 22 | p md 23 | p md.row[2].col[7].x 24 | -------------------------------------------------------------------------------- /examples/modular-def.rb: -------------------------------------------------------------------------------- 1 | # This example shows how to refactor a BitStruct class defininition 2 | # using modules. 3 | 4 | module ModuleMethodSaver 5 | def method_missing(meth, *args, &block) 6 | @saved ||= [] 7 | @saved << [meth, args, block] 8 | end 9 | 10 | def included(m) 11 | if @saved 12 | @saved.each do |meth, args, block| 13 | m.send(meth, *args, &block) 14 | end 15 | end 16 | end 17 | end 18 | 19 | 20 | require 'bit-struct' 21 | 22 | module M 23 | extend ModuleMethodSaver 24 | 25 | unsigned :x, 13 26 | signed :y, 7 27 | end 28 | 29 | class BS < BitStruct 30 | include M 31 | end 32 | 33 | 34 | bs = BS.new 35 | bs.x = 123 36 | bs.y = -63 37 | 38 | p bs # ==> # 39 | -------------------------------------------------------------------------------- /examples/native.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class NativeNumbers < BitStruct 4 | 5 | unsigned :x0, 1, :endian => :native 6 | unsigned :x1, 13, :endian => :native 7 | unsigned :x2, 2, :endian => :native 8 | unsigned :x3, 32, :endian => :native 9 | 10 | float :f1, 32, :endian => :native 11 | float :f2, 64, :endian => :native 12 | float :f3, 32 13 | float :f4, 64 14 | 15 | default_options :endian => :native 16 | # affects fields defined after this and in subclasses 17 | 18 | unsigned :y1, 32 19 | 20 | end 21 | 22 | n = NativeNumbers.new 23 | p n 24 | n.x1 = 5 25 | n.f1 = n.f3 = 1234.567 26 | n.f2 = n.f4 = 6543.321 27 | n.y1 = 1 28 | 29 | p n 30 | p n.unpack("C*") 31 | 32 | -------------------------------------------------------------------------------- /examples/nested-block.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class Container < BitStruct 4 | nest :n, "Nest" do 5 | unsigned :x, 5 6 | unsigned :y, 3 7 | char :s, 5*8 8 | end 9 | end 10 | 11 | cont = Container.new 12 | 13 | n = cont.n 14 | 15 | n.x = 5 16 | n.y = 0 17 | n.s = " xyz " 18 | 19 | cont.n = n # note copy semantics here! 20 | 21 | p cont 22 | puts 23 | puts cont.inspect_detailed 24 | -------------------------------------------------------------------------------- /examples/nested.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class NestedPart < BitStruct 4 | unsigned :x, 5 5 | unsigned :y, 3 6 | char :s, 5*8 7 | end 8 | 9 | class Container < BitStruct 10 | nest :n1, NestedPart, "Nest 1" 11 | nest :n2, NestedPart, "Nest 2" 12 | end 13 | 14 | cont = Container.new 15 | 16 | n = NestedPart.new(:x=>1, :y=>2, :s=>"abc") 17 | 18 | p n 19 | 20 | cont.n1 = n 21 | 22 | n.x = 5 23 | n.y = 0 24 | n.s = " xyz " 25 | 26 | cont.n2 = n # note copy semantics here! 27 | 28 | puts 29 | p cont 30 | puts 31 | puts cont.inspect_detailed 32 | 33 | puts "-"*80 34 | -------------------------------------------------------------------------------- /examples/pad.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class PadTest < BitStruct 4 | unsigned :x, 3 5 | pad :p1, 2 6 | unsigned :y, 3 7 | end 8 | 9 | pt = PadTest.new 10 | pt.x = 1 11 | pt.y = 2 12 | 13 | p pt 14 | y pt 15 | -------------------------------------------------------------------------------- /examples/ping-recv.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "./ip" 3 | 4 | # Example of using the IP class to receive ping (ICMP) messages. 5 | 6 | begin 7 | rsock = Socket.open(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP) 8 | rescue Errno::EPERM 9 | $stderr.puts "Must run #{$0} as root." 10 | exit! 11 | end 12 | 13 | Thread.new do 14 | loop do 15 | data, sender = rsock.recvfrom(8192) 16 | port, host = Socket.unpack_sockaddr_in(sender) 17 | puts "-"*80, 18 | "packet received from #{host}:#{port}:", 19 | IP.new(data).inspect_detailed, 20 | "-"*80 21 | $stdout.flush 22 | end 23 | end 24 | 25 | system "ping 127.0.0.1" 26 | -------------------------------------------------------------------------------- /examples/ping.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "./ip" 3 | 4 | ### example is broken 5 | 6 | # A more substantial example of sending and receiving ICMP packets. 7 | 8 | class ICMP < IP 9 | unsigned :icmp_type, 8, "Message type" 10 | unsigned :icmp_code, 8, "Message code" 11 | unsigned :icmp_cksum, 16, "ICMP checksum" 12 | unsigned :icmp_id, 16 13 | unsigned :icmp_seq, 16 14 | rest :body, "Body of ICMP message" 15 | end 16 | 17 | # Example of using the IP class to receive ping (ICMP) messages. 18 | 19 | begin 20 | rsock = Socket.open(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP) 21 | rescue Errno::EPERM 22 | $stderr.puts "Must run #{$0} as root." 23 | exit! 24 | end 25 | 26 | begin 27 | ssock = Socket.open(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP) 28 | ssock.setsockopt(Socket::SOL_IP, Socket::IP_HDRINCL, true) 29 | # IP_HDRINCL isn't necessary to send ICMP, but we are inheriting 30 | # ICMP from IP, so the header is included in this example. 31 | rescue Errno::EPERM 32 | $stderr.puts "Must run #{$0} as root." 33 | exit! 34 | end 35 | 36 | Thread.new do 37 | loop do 38 | data, sender = rsock.recvfrom(8192) 39 | port, host = Socket.unpack_sockaddr_in(sender) 40 | out = "-"*80, 41 | "packet received from #{host}:#{port}:", 42 | ICMP.new(data).inspect_detailed, 43 | "-"*80 44 | puts out 45 | $stdout.flush 46 | end 47 | end 48 | 49 | addr = Socket.pack_sockaddr_in(1024, "localhost") 50 | 5.times do |i| 51 | icmp = ICMP.new do |b| 52 | # ip_v and ip_hl are set for us by IP class 53 | b.ip_tos = 0 54 | b.ip_id = i 55 | b.ip_off = 0 ## ? 56 | b.ip_ttl = 64 57 | b.ip_p = Socket::IPPROTO_ICMP 58 | b.ip_src = "127.0.0.1" 59 | b.ip_dst = "127.0.0.1" 60 | b.body = "" ## ? 61 | b.ip_len = b.length 62 | b.ip_sum = 0 ## ? 63 | end 64 | 65 | out = "-"*80, 66 | "packet sent:", 67 | icmp.inspect_detailed, 68 | "-"*80 69 | puts out 70 | $stdout.flush 71 | ssock.send(icmp, 0, addr) 72 | sleep 1 73 | end 74 | -------------------------------------------------------------------------------- /examples/player-data.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | class PlayerData < BitStruct 4 | 5 | # type accessor size (bits) description (for #inspect_detailed) 6 | unsigned :pid, 32, "Player ID" 7 | float :x_position, 32, "X position", :format => "%8.3f" 8 | float :y_position, 32, "Y position" 9 | float :z_position, 32, "Z position" 10 | unsigned :foobar, 32, "Foobar" 11 | 12 | # def self.create(*args) 13 | # new(args.pack(field_format)) 14 | # end 15 | 16 | end 17 | 18 | pd = PlayerData.new( 19 | :pid => 400, 20 | :x_position => 1, 21 | :y_position => 2, 22 | :z_position => 3, 23 | :foobar => 0b101010 24 | ) 25 | 26 | p pd 27 | p pd.pid 28 | p pd.x_position 29 | p pd.foobar 30 | p String.new(pd) 31 | puts pd.inspect_detailed 32 | 33 | params = { 34 | :pid => 400, 35 | :x_position => 1, 36 | :y_position => 2, 37 | :z_position => 3, 38 | :foobar => 0b101010 39 | } 40 | param_string = String.new(pd) 41 | 42 | id, x_position, y_position, z_position, foobar = 43 | 400, 1.0, 2.0, 3.0, 0b101010 44 | 45 | begin 46 | require 'timeout' 47 | n = 0 48 | sec = 10 49 | Timeout::timeout(sec) do 50 | loop do 51 | PlayerData.new params 52 | #PlayerData.create id, x_position, y_position, z_position, foobar 53 | n += 1 54 | end 55 | end 56 | rescue Timeout::Error 57 | puts "creations per second : #{ n / sec }" 58 | end 59 | 60 | __END__ 61 | 62 | # 63 | 400 64 | 1 65 | 42 66 | "\000\000\001\220\000\000\000\001\000\000\000\002\000\000\000\003\000\000\000*" 67 | PlayerData: 68 | Player ID = 400 69 | X position = 1 70 | Y position = 2 71 | Z position = 3 72 | Foobar = 42 73 | creations per second : 22428 # using params 74 | creations per second : 243217 # using param_string 75 | creations per second : 94254 # using create 76 | -------------------------------------------------------------------------------- /examples/raw.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "./ip" 3 | 4 | # A more substantial example of sending and receiving RAW packets. 5 | 6 | begin 7 | rsock = Socket.open(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_RAW) 8 | rescue Errno::EPERM 9 | $stderr.puts "Must run #{$0} as root." 10 | exit! 11 | end 12 | 13 | begin 14 | ssock = Socket.open(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_RAW) 15 | unless ssock.getsockopt(Socket::SOL_IP, Socket::IP_HDRINCL) 16 | puts "IP_HDRINCL is supposed to be the default for IPPROTO_RAW!" 17 | puts "setting IP_HDRINCL anyway" 18 | ssock.setsockopt(Socket::SOL_IP, Socket::IP_HDRINCL, true) 19 | end 20 | rescue Errno::EPERM 21 | $stderr.puts "Must run #{$0} as root." 22 | exit! 23 | end 24 | 25 | Thread.new do 26 | loop do 27 | data, sender = rsock.recvfrom(8192) 28 | port, host = Socket.unpack_sockaddr_in(sender) 29 | out = "-"*80, 30 | "packet received from #{host}:#{port}:", 31 | IP.new(data).inspect_detailed, 32 | "-"*80 33 | puts out 34 | $stdout.flush 35 | end 36 | end 37 | 38 | addr = Socket.pack_sockaddr_in(1024, "localhost") 39 | 3.times do |i| 40 | ip = IP.new do |b| 41 | # ip_v and ip_hl are set for us by IP class 42 | b.ip_tos = 0 43 | b.ip_id = i+1 44 | b.ip_off = 0 45 | b.ip_ttl = 64 46 | b.ip_p = Socket::IPPROTO_RAW 47 | b.ip_src = "127.0.0.1" 48 | b.ip_dst = "127.0.0.1" 49 | b.body = "just another IP hacker" 50 | b.ip_len = b.length 51 | b.ip_sum = 0 # linux will calculate this for us (QNX won't?) 52 | end 53 | 54 | out = "-"*80, 55 | "packet sent:", 56 | ip.inspect_detailed, 57 | "-"*80 58 | puts out 59 | $stdout.flush 60 | ssock.send(ip, 0, addr) 61 | sleep 1 62 | end 63 | -------------------------------------------------------------------------------- /examples/rest.rb: -------------------------------------------------------------------------------- 1 | require './ip' 2 | 3 | class Point < BitStruct 4 | unsigned :x, 16 5 | unsigned :y, 16 6 | unsigned :z, 16 7 | end 8 | 9 | class MyPacket < IP 10 | rest :point, Point # treat this field as Point 11 | end 12 | 13 | packet = MyPacket.new 14 | point = Point.new({:x=>1, :y=>2, :z=>3}) 15 | packet.point = point 16 | 17 | p point 18 | p packet.point 19 | p packet 20 | 21 | puts packet.inspect_detailed 22 | puts "-" * 60 23 | 24 | require 'yaml' 25 | 26 | packet_yaml = packet.to_yaml 27 | puts packet_yaml 28 | 29 | packet2 = YAML.load( packet_yaml) 30 | p packet2 31 | -------------------------------------------------------------------------------- /examples/switch-endian.rb: -------------------------------------------------------------------------------- 1 | module ModuleMethodSaver 2 | def method_missing(meth, *args, &block) 3 | @saved ||= [] 4 | @saved << [meth, args, block] 5 | end 6 | 7 | def included(m) 8 | if @saved 9 | @saved.each do |meth, args, block| 10 | m.send(meth, *args, &block) 11 | end 12 | end 13 | end 14 | end 15 | 16 | 17 | require 'bit-struct' 18 | 19 | module MyFields 20 | extend ModuleMethodSaver 21 | 22 | unsigned :x, 16 23 | end 24 | 25 | class LittleEndian < BitStruct 26 | default_options :endian => :little 27 | include MyFields 28 | end 29 | 30 | class BigEndian < BitStruct 31 | default_options :endian => :big 32 | include MyFields 33 | end 34 | 35 | le = LittleEndian.new 36 | be = BigEndian.new 37 | 38 | le.x = be.x = 256 39 | 40 | p [le, le.to_s] 41 | p [be, be.to_s] 42 | 43 | 44 | __END__ 45 | 46 | Output: 47 | 48 | [#, "\000\001"] 49 | [#, "\001\000"] 50 | -------------------------------------------------------------------------------- /examples/vector.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct' 2 | 3 | # Example 1 4 | class Vec < BitStruct::Vector 5 | # these declarations apply to *each* entry in the vector: 6 | unsigned :x, 16 7 | signed :y, 32 8 | 9 | Entry = struct_class # Give it a name, just for inspect to look nice 10 | end 11 | 12 | v = Vec.new(5) # explicit length 13 | entry = v[3] 14 | entry.x = 2 15 | v[3] = entry 16 | 17 | entry = v[4] 18 | entry.y = -4 19 | v[4] = entry 20 | 21 | entry.x = 42 22 | v << entry 23 | 24 | p v 25 | puts 26 | 27 | v2 = Vec.new(v) # determines length from argument 28 | p v2 29 | puts 30 | 31 | # Example 2 32 | class Vec2 < BitStruct::Vector 33 | class Entry < BitStruct 34 | unsigned :x, 16 35 | signed :y, 32 36 | end 37 | 38 | struct_class Entry # use Entry as the struct_class for Vec2 39 | end 40 | 41 | v = Vec2.new(5) # explicit length 42 | entry = v[3] 43 | entry.x = 2 44 | v[3] = entry 45 | 46 | entry = v[4] 47 | entry.y = -4 48 | v[4] = entry 49 | 50 | p v 51 | puts 52 | 53 | puts v.inspect_detailed 54 | puts 55 | 56 | puts Vec2.describe 57 | 58 | # Example 3: inheritance 59 | class VecSub < Vec2 60 | float :z, 32 61 | end 62 | 63 | vsub = VecSub.new(3) 64 | entry = vsub[1] 65 | entry.x = 12 66 | entry.y = -1 67 | entry.z = 4.5 68 | vsub[1] = entry 69 | p vsub 70 | puts 71 | 72 | # Example 4: vector field in a bitstruct 73 | class Packet < BitStruct 74 | unsigned :stuff, 32, "whatever" 75 | 76 | # Using the Vec class defined above 77 | vector :v, Vec, "a vector", 5 78 | 79 | # equivalently, using an anonymous subclass of BitStruct::Vector 80 | vector :v2, "a vector 2", 5 do 81 | unsigned :x, 16 82 | signed :y, 32 83 | end 84 | end 85 | 86 | pkt = Packet.new 87 | vec = pkt.v 88 | entry = vec[2] 89 | entry.x = 123 90 | entry.y = -456 91 | vec[2] = entry 92 | pkt.v = vec 93 | p pkt, pkt.v, pkt.v[2], pkt.v[2].y 94 | puts 95 | puts pkt.inspect_detailed 96 | puts 97 | 98 | puts Packet.describe(:expand => true) 99 | -------------------------------------------------------------------------------- /lib/bit-struct.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct/bit-struct' 2 | require 'bit-struct/fields' 3 | require 'bit-struct/yaml' 4 | -------------------------------------------------------------------------------- /lib/bit-struct/bit-struct.rb: -------------------------------------------------------------------------------- 1 | # Class for packed binary data, with defined bitfields and accessors for them. 2 | # See {intro.txt}[link:../doc/files/intro_txt.html] for an overview. 3 | # 4 | # Data after the end of the defined fields is accessible using the +rest+ 5 | # declaration. See examples/ip.rb. Nested fields can be declared using +nest+. 6 | # See examples/nest.rb. 7 | # 8 | # Note that all string methods are still available: length, grep, etc. 9 | # The String#replace method is useful. 10 | # 11 | class BitStruct < String 12 | VERSION = "0.17" 13 | 14 | require 'bit-struct/field' 15 | NULL_FIELD = Field.new(0, 0, :null, :display_name => "null field") 16 | 17 | # Raised when a field is added after an instance has been created. Fields 18 | # cannot be added after this point. 19 | class ClosedClassError < StandardError; end 20 | 21 | # Raised if the chosen field name is not allowed, either because another 22 | # field by that name exists, or because a method by that name exists. 23 | class FieldNameError < StandardError; end 24 | 25 | @default_options = {} 26 | 27 | @initial_value = nil 28 | @closed = nil 29 | @rest_field = nil 30 | @note = nil 31 | 32 | class << self 33 | def inherited cl 34 | cl.instance_eval do 35 | @initial_value = nil 36 | @closed = nil 37 | @rest_field = nil 38 | @note = nil 39 | end 40 | end 41 | 42 | # ------------------------ 43 | # :section: field access methods 44 | # 45 | # For introspection and metaprogramming. 46 | # 47 | # ------------------------ 48 | 49 | # Return the list of fields for this class. 50 | def fields 51 | @fields ||= self == BitStruct ? [] : superclass.fields.dup 52 | end 53 | 54 | # Return the list of fields defined by this class, not inherited 55 | # from the superclass. 56 | def own_fields 57 | @own_fields ||= [] 58 | end 59 | 60 | # Add a field to the BitStruct (usually, this is only used internally). 61 | def add_field(name, length, opts = {}) 62 | round_byte_length ## just to make sure this has been calculated 63 | ## before adding anything 64 | 65 | name = name.to_sym 66 | 67 | if @closed 68 | raise ClosedClassError, "Cannot add field #{name}: " + 69 | "The definition of the #{self.inspect} BitStruct class is closed." 70 | end 71 | 72 | if fields.find {|f|f.name == name} 73 | raise FieldNameError, "Field #{name} is already defined as a field." 74 | end 75 | 76 | if instance_methods(true).find {|m| m == name} 77 | if opts[:allow_method_conflict] || opts["allow_method_conflict"] 78 | warn "Field #{name} is already defined as a method." 79 | else 80 | raise FieldNameError,"Field #{name} is already defined as a method." 81 | end 82 | end 83 | 84 | field_class = opts[:field_class] 85 | 86 | prev = fields[-1] || NULL_FIELD 87 | offset = prev.offset + prev.length 88 | field = field_class.new(offset, length, name, opts) 89 | field.add_accessors_to(self) 90 | fields << field 91 | own_fields << field 92 | @bit_length += field.length 93 | @round_byte_length = (bit_length/8.0).ceil 94 | 95 | if @initial_value 96 | diff = @round_byte_length - @initial_value.length 97 | if diff > 0 98 | @initial_value << "\0" * diff 99 | end 100 | end 101 | 102 | field 103 | end 104 | 105 | def parse_options(ary, default_name, default_field_class) # :nodoc: 106 | opts = ary.grep(Hash).first || {} 107 | opts = default_options.merge(opts) 108 | 109 | opts[:display_name] = ary.grep(String).first || default_name 110 | opts[:field_class] = ary.grep(Class).first || default_field_class 111 | 112 | opts 113 | end 114 | 115 | # Get or set the hash of default options for the class, which apply to all 116 | # fields. Changes take effect immediately, so can be used alternatingly with 117 | # blocks of field declarations. If +h+ is provided, update the default 118 | # options with that hash. Default options are inherited. 119 | # 120 | # This is especially useful with the :endian => val option. 121 | def default_options h = nil 122 | @default_options ||= superclass.default_options.dup 123 | if h 124 | @default_options.merge! h 125 | end 126 | @default_options 127 | end 128 | 129 | # Length, in bits, of this object. 130 | def bit_length 131 | @bit_length ||= fields.inject(0) {|a, f| a + f.length} 132 | end 133 | 134 | # Length, in bytes (rounded up), of this object. 135 | def round_byte_length 136 | @round_byte_length ||= (bit_length/8.0).ceil 137 | end 138 | 139 | def closed! # :nodoc: 140 | @closed = true 141 | end 142 | 143 | def field_by_name name 144 | @field_by_name ||= {} 145 | field = @field_by_name[name] 146 | unless field 147 | field = fields.find {|f| f.name == name} 148 | @field_by_name[name] = field if field 149 | end 150 | field 151 | end 152 | end 153 | 154 | # Return the list of fields for this class. 155 | def fields 156 | self.class.fields 157 | end 158 | 159 | # Return the rest field for this class. 160 | def rest_field 161 | self.class.rest_field 162 | end 163 | 164 | # Return the field with the given name. 165 | def field_by_name name 166 | self.class.field_by_name name 167 | end 168 | 169 | # ------------------------ 170 | # :section: metadata inspection methods 171 | # 172 | # Methods to textually describe the format of a BitStruct subclass. 173 | # 174 | # ------------------------ 175 | 176 | class << self 177 | # Default format for describe. Fields are byte, type, name, size, 178 | # and description. 179 | DESCRIBE_FORMAT = "%8s: %-12s %-14s[%4s] %s" 180 | 181 | # Can be overridden to use a different format. 182 | def describe_format 183 | DESCRIBE_FORMAT 184 | end 185 | 186 | # Textually describe the fields of this class of BitStructs. 187 | # Returns a printable table (array of line strings), based on +fmt+, 188 | # which defaults to #describe_format, which defaults to +DESCRIBE_FORMAT+. 189 | def describe(fmt = nil, opts = {}) 190 | if fmt.kind_of? Hash 191 | opts = fmt; fmt = nil 192 | end 193 | 194 | if block_given? 195 | fields.each do |field| 196 | field.describe(opts) do |desc| 197 | yield desc 198 | end 199 | end 200 | nil 201 | 202 | else 203 | fmt ||= describe_format 204 | 205 | result = [] 206 | 207 | unless opts[:omit_header] 208 | result << fmt % ["byte", "type", "name", "size", "description"] 209 | result << "-"*70 210 | end 211 | 212 | fields.each do |field| 213 | field.describe(opts) do |desc| 214 | result << fmt % desc 215 | end 216 | end 217 | 218 | unless opts[:omit_footer] 219 | result << @note if @note 220 | end 221 | 222 | result 223 | end 224 | end 225 | 226 | # Subclasses can use this to append a string (or several) to the #describe 227 | # output. Notes are not cumulative with inheritance. When used with no 228 | # arguments simply returns the note string 229 | def note(*str) 230 | @note = str unless str.empty? 231 | @note 232 | end 233 | end 234 | 235 | # ------------------------ 236 | # :section: initialization and conversion methods 237 | # 238 | # ------------------------ 239 | 240 | # Initialize the string with the given string or bitstruct, or with a hash of 241 | # field=>value pairs, or with the defaults for the BitStruct subclass, or 242 | # with an IO or other object with a #read method. Fields can be strings or 243 | # symbols. Finally, if a block is given, yield the instance for modification 244 | # using accessors. 245 | def initialize(value = nil) # :yields: instance 246 | self << self.class.initial_value 247 | 248 | case value 249 | when Hash 250 | value.each do |k, v| 251 | send "#{k}=", v 252 | end 253 | 254 | when nil 255 | 256 | else 257 | if value.respond_to?(:read) 258 | value = value.read(self.class.round_byte_length) 259 | end 260 | 261 | self[0, value.length] = value 262 | end 263 | 264 | self.class.closed! 265 | yield self if block_given? 266 | end 267 | 268 | DEFAULT_TO_H_OPTS = { 269 | :convert_keys => :to_sym, 270 | :include_rest => true 271 | } 272 | 273 | # Returns a hash of {name=>value,...} for each field. By default, include 274 | # the rest field. 275 | # Keys are symbols derived from field names using +to_sym+, unless 276 | # opts[:convert_keys]<\tt> is set to some other method name. 277 | def to_h(opts = DEFAULT_TO_H_OPTS) 278 | converter = opts[:convert_keys] || :to_sym 279 | 280 | fields_for_to_h = fields 281 | if opts[:include_rest] and (rest_field = self.class.rest_field) 282 | fields_for_to_h += [rest_field] 283 | end 284 | 285 | fields_for_to_h.inject({}) do |h,f| 286 | h[f.name.send(converter)] = send(f.name) 287 | h 288 | end 289 | end 290 | 291 | # Returns an array of values of the fields of the BitStruct. By default, 292 | # include the rest field. 293 | def to_a(include_rest = true) 294 | ary = 295 | fields.map do |f| 296 | send(f.name) 297 | end 298 | 299 | if include_rest and (rest_field = self.class.rest_field) 300 | ary << send(rest_field.name) 301 | end 302 | ary 303 | end 304 | 305 | if "a"[0].kind_of? String 306 | def [](*args) 307 | if args.size == 1 and args[0].kind_of?(Integer) 308 | super.ord 309 | else 310 | super 311 | end 312 | end 313 | 314 | def []=(*args) 315 | if args.size == 2 and (i=args[0]).kind_of?(Integer) 316 | super(i, args[1].chr) 317 | else 318 | super 319 | end 320 | end 321 | end 322 | 323 | class << self 324 | # The unique "prototype" object from which new instances are copied. 325 | # The fields of this instance can be modified in the class definition 326 | # to set default values for the fields in that class. (Otherwise, defaults 327 | # defined by the fields themselves are used.) A copy of this object is 328 | # inherited in subclasses, which they may override using defaults and 329 | # by writing to the initial_value object itself. 330 | # 331 | # If called with a block, yield the initial value object before returning 332 | # it. Useful for customization within a class definition. 333 | # 334 | def initial_value # :yields: the initial value 335 | unless @initial_value 336 | iv = defined?(superclass.initial_value) ? 337 | superclass.initial_value.dup : "" 338 | if iv.length < round_byte_length 339 | iv << "\0" * (round_byte_length - iv.length) 340 | end 341 | 342 | @initial_value = "" # Serves as initval while the real initval is inited 343 | @initial_value = new(iv) 344 | @closed = false # only creating the first _real_ instance closes. 345 | 346 | fields.each do |field| 347 | @initial_value.send("#{field.name}=", field.default) if field.default 348 | end 349 | end 350 | yield @initial_value if block_given? 351 | @initial_value 352 | end 353 | 354 | # Take +data+ (a string or BitStruct) and parse it into instances of 355 | # the +classes+, returning them in an array. The classes can be given 356 | # as an array or a separate arguments. (For parsing a string into a _single_ 357 | # BitStruct instance, just use the #new method with the string as an arg.) 358 | def parse(data, *classes) 359 | classes.flatten.map do |c| 360 | c.new(data.slice!(0...c.round_byte_length)) 361 | end 362 | end 363 | 364 | # Join the given structs (array or multiple args) as a string. 365 | # Actually, the inherited String#+ instance method is the same, as is using 366 | # Array#join. 367 | def join(*structs) 368 | structs.flatten.map {|struct| struct.to_s}.join("") 369 | end 370 | end 371 | 372 | # ------------------------ 373 | # :section: inspection methods 374 | # 375 | # ------------------------ 376 | 377 | DEFAULT_INSPECT_OPTS = { 378 | :format => "#<%s %s>", 379 | :field_format => "%s=%s", 380 | :separator => ", ", 381 | :field_name_meth => :name, 382 | :include_rest => true, 383 | :brackets => ["[", "]"], 384 | :include_class => true, 385 | :simple_format => "<%s>" 386 | } 387 | 388 | DETAILED_INSPECT_OPTS = { 389 | :format => "%s:\n%s", 390 | :field_format => "%30s = %s", 391 | :separator => "\n", 392 | :field_name_meth => :display_name, 393 | :include_rest => true, 394 | :brackets => [nil, "\n"], 395 | :include_class => true, 396 | :simple_format => "\n%s" 397 | } 398 | 399 | # A standard inspect method which does not add newlines. 400 | def inspect_with_options(opts = DEFAULT_INSPECT_OPTS) 401 | field_format = opts[:field_format] 402 | field_name_meth = opts[:field_name_meth] 403 | 404 | fields_for_inspect = fields.select {|field| field.inspectable?} 405 | if opts[:include_rest] and (rest_field = self.class.rest_field) 406 | fields_for_inspect << rest_field 407 | end 408 | 409 | ary = fields_for_inspect.map do |field| 410 | field_format % 411 | [field.send(field_name_meth), 412 | field.inspect_in_object(self, opts)] 413 | end 414 | 415 | body = ary.join(opts[:separator]) 416 | 417 | if opts[:include_class] 418 | opts[:format] % [self.class, body] 419 | else 420 | opts[:simple_format] % body 421 | end 422 | end 423 | 424 | alias inspect inspect_with_options 425 | 426 | # A more visually appealing inspect method that puts each field/value on 427 | # a separate line. Very useful when output is scrolling by on a screen. 428 | # 429 | # (This is actually a convenience method to call #inspect with the 430 | # DETAILED_INSPECT_OPTS opts.) 431 | def inspect_detailed 432 | inspect(DETAILED_INSPECT_OPTS) 433 | end 434 | 435 | # ------------------------ 436 | # :section: field declaration methods 437 | # 438 | # ------------------------ 439 | 440 | # Define accessors for a variable length substring from the end of 441 | # the defined fields to the end of the BitStruct. The _rest_ may behave as 442 | # a String or as some other String or BitStruct subclass. 443 | # 444 | # This does not add a field, which is useful because a superclass can have 445 | # a rest method which accesses subclass data. In particular, #rest does 446 | # not affect the #round_byte_length class method. Of course, any data 447 | # in rest does add to the #length of the BitStruct, calculated as a string. 448 | # Also, _rest_ is not inherited. 449 | # 450 | # The +ary+ argument(s) work as follows: 451 | # 452 | # If a class is provided, use it for the Field class (String by default). 453 | # If a string is provided, use it for the display_name (+name+ by default). 454 | # If a hash is provided, use it for options. 455 | # 456 | # *Warning*: the rest reader method returns a copy of the field, so 457 | # accessors on that returned value do not affect the original rest field. 458 | # 459 | def self.rest(name, *ary) 460 | if @rest_field 461 | raise ArgumentError, "Duplicate rest field: #{name.inspect}." 462 | end 463 | 464 | opts = parse_options(ary, name, String) 465 | offset = round_byte_length 466 | byte_range = offset..-1 467 | class_eval do 468 | field_class = opts[:field_class] 469 | define_method name do || 470 | field_class.new(self[byte_range]) 471 | end 472 | 473 | define_method "#{name}=" do |val| 474 | self[byte_range] = val 475 | end 476 | 477 | @rest_field = Field.new(offset, -1, name, { 478 | :display_name => opts[:display_name], 479 | :rest_class => field_class 480 | }) 481 | end 482 | end 483 | 484 | # Not included with the other fields, but accessible separately. 485 | def self.rest_field; @rest_field; end 486 | end 487 | -------------------------------------------------------------------------------- /lib/bit-struct/char-field.rb: -------------------------------------------------------------------------------- 1 | class BitStruct 2 | # Class for fixed length binary strings of characters. 3 | # Declared with BitStruct.char. 4 | class CharField < Field 5 | #def self.default 6 | # don't define this, since it must specify N nulls and we don't know N 7 | #end 8 | 9 | # Used in describe. 10 | def self.class_name 11 | @class_name ||= "char" 12 | end 13 | 14 | def add_accessors_to(cl, attr = name) # :nodoc: 15 | unless offset % 8 == 0 16 | raise ArgumentError, 17 | "Bad offset, #{offset}, for #{self.class} #{name}." + 18 | " Must be multiple of 8." 19 | end 20 | 21 | unless length % 8 == 0 22 | raise ArgumentError, 23 | "Bad length, #{length}, for #{self.class} #{name}." + 24 | " Must be multiple of 8." 25 | end 26 | 27 | offset_byte = offset / 8 28 | length_byte = length / 8 29 | last_byte = offset_byte + length_byte - 1 30 | byte_range = offset_byte..last_byte 31 | val_byte_range = 0..length_byte-1 32 | 33 | cl.class_eval do 34 | define_method attr do || 35 | self[byte_range].to_s 36 | end 37 | 38 | define_method "#{attr}=" do |val| 39 | val = val.to_s 40 | if val.length < length_byte 41 | val += "\0" * (length_byte - val.length) 42 | end 43 | self[byte_range] = val[val_byte_range] 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/bit-struct/field.rb: -------------------------------------------------------------------------------- 1 | 2 | class BitStruct 3 | class Field 4 | # Offset of field in bits. 5 | attr_reader :offset 6 | 7 | # Length of field in bits. 8 | attr_reader :length 9 | alias size length 10 | 11 | # Name of field (used for its accessors). 12 | attr_reader :name 13 | 14 | # Options, such as :default (varies for each field subclass). 15 | # In general, options can be provided as strings or as symbols. 16 | attr_reader :options 17 | 18 | # Display name of field (used for printing). 19 | attr_reader :display_name 20 | 21 | # Default value. 22 | attr_reader :default 23 | 24 | # Format for printed value of field. 25 | attr_reader :format 26 | 27 | # Subclasses can override this to define a default for all fields of this 28 | # class, not just the one currently being added to a BitStruct class, a 29 | # "default default" if you will. The global default, if #default returns 30 | # nil, is to fill the field with zero. Most field classes just let this 31 | # default stand. The default can be overridden per-field when a BitStruct 32 | # class is defined. 33 | def self.default; nil; end 34 | 35 | # Used in describe. 36 | def self.class_name 37 | @class_name ||= name[/\w+$/] 38 | end 39 | 40 | # Used in describe. Can be overridden per-subclass, as in NestedField. 41 | def class_name 42 | self.class.class_name 43 | end 44 | 45 | # Yield the description of this field, as an array of 5 strings: byte 46 | # offset, type, name, size, and description. The opts hash may have: 47 | # 48 | # :expand :: if the value is true, expand complex fields 49 | # 50 | # (Subclass implementations may yield more than once for complex fields.) 51 | # 52 | def describe opts 53 | bits = size 54 | if bits > 32 and bits % 8 == 0 55 | len_str = "%dB" % (bits/8) 56 | else 57 | len_str = "%db" % bits 58 | end 59 | 60 | byte_offset = offset / 8 + (opts[:byte_offset] || 0) 61 | 62 | yield ["@%d" % byte_offset, class_name, name, len_str, display_name] 63 | end 64 | 65 | # Options are _display_name_, _default_, and _format_ (subclasses of Field 66 | # may add other options). 67 | def initialize(offset, length, name, opts = {}) 68 | @offset, @length, @name, @options = 69 | offset, length, name, opts 70 | 71 | @display_name = opts[:display_name] || opts["display_name"] 72 | @default = opts[:default] || opts["default"] || self.class.default 73 | @format = opts[:format] || opts["format"] 74 | end 75 | 76 | # Inspect the value of this field in the specified _obj_. 77 | def inspect_in_object(obj, opts) 78 | val = obj.send(name) 79 | str = 80 | begin 81 | val.inspect_with_options(opts) 82 | rescue NoMethodError 83 | val.inspect 84 | end 85 | (f=@format) ? (f % str) : str 86 | end 87 | 88 | # Normally, all fields show up in inspect, but some, such as padding, 89 | # should not. 90 | def inspectable?; true; end 91 | end 92 | end 93 | 94 | 95 | -------------------------------------------------------------------------------- /lib/bit-struct/fields.rb: -------------------------------------------------------------------------------- 1 | class BitStruct 2 | class << self 3 | # Define a char string field in the current subclass of BitStruct, 4 | # with the given _name_ and _length_ (in bits). Trailing nulls _are_ 5 | # considered part of the string. 6 | # 7 | # If a class is provided, use it for the Field class. 8 | # If a string is provided, use it for the display_name. 9 | # If a hash is provided, use it for options. 10 | # 11 | # Note that the accessors have COPY semantics, not reference. 12 | # 13 | def char(name, length, *rest) 14 | opts = parse_options(rest, name, CharField) 15 | add_field(name, length, opts) 16 | end 17 | alias string char 18 | BitStruct.autoload :CharField, "bit-struct/char-field" 19 | 20 | # Define a floating point field in the current subclass of BitStruct, 21 | # with the given _name_ and _length_ (in bits). 22 | # 23 | # If a class is provided, use it for the Field class. 24 | # If a string is provided, use it for the display_name. 25 | # If a hash is provided, use it for options. 26 | # 27 | # The :endian => :native option overrides the default of 28 | # :network byte ordering, in favor of native byte ordering. Also 29 | # permitted are :big (same as :network) and 30 | # :little. 31 | # 32 | def float name, length, *rest 33 | opts = parse_options(rest, name, FloatField) 34 | add_field(name, length, opts) 35 | end 36 | BitStruct.autoload :FloatField, "bit-struct/float-field" 37 | 38 | # Define an octet string field in the current subclass of BitStruct, 39 | # with the given _name_ and _length_ (in bits). Trailing nulls are 40 | # not considered part of the string. The field is accessed using 41 | # period-separated hex digits. 42 | # 43 | # If a class is provided, use it for the Field class. 44 | # If a string is provided, use it for the display_name. 45 | # If a hash is provided, use it for options. 46 | # 47 | def hex_octets(name, length, *rest) 48 | opts = parse_options(rest, name, HexOctetField) 49 | add_field(name, length, opts) 50 | end 51 | BitStruct.autoload :HexOctetField, "bit-struct/hex-octet-field" 52 | 53 | # Define a nested field in the current subclass of BitStruct, 54 | # with the given _name_ and _nested_class_. Length is determined from 55 | # _nested_class_. 56 | # 57 | # If a class is provided, use it for the Field class (i.e. <=NestedField). 58 | # If a string is provided, use it for the display_name. 59 | # If a hash is provided, use it for options. 60 | # 61 | # For example: 62 | # 63 | # class Sub < BitStruct 64 | # unsigned :x, 8 65 | # end 66 | # 67 | # class A < BitStruct 68 | # nest :n, Sub 69 | # end 70 | # 71 | # a = A.new 72 | # 73 | # p a # ==> #> 74 | # 75 | # If a block is given, use it to define the nested fields. For example, the 76 | # following is equivalent to the above example: 77 | # 78 | # class A < BitStruct 79 | # nest :n do 80 | # unsigned :x, 8 81 | # end 82 | # end 83 | # 84 | # WARNING: the accessors have COPY semantics, not reference. When you call a 85 | # reader method to get the nested structure, you get a *copy* of that data. 86 | # Expressed in terms of the examples above: 87 | # 88 | # # This fails to set x in a. 89 | # a.n.x = 3 90 | # p a # ==> #> 91 | # 92 | # # This works 93 | # n = a.n 94 | # n.x = 3 95 | # a.n = n 96 | # p a # ==> #> 97 | # 98 | def nest(name, *rest, &block) 99 | nested_class = rest.grep(Class).find {|cl| cl <= BitStruct} 100 | rest.delete nested_class 101 | opts = parse_options(rest, name, NestedField) 102 | nested_class = opts[:nested_class] ||= nested_class 103 | 104 | unless (block and not nested_class) or (nested_class and not block) 105 | raise ArgumentError, 106 | "nested field must have either a nested_class option or a block," + 107 | " but not both" 108 | end 109 | 110 | unless nested_class 111 | nested_class = Class.new(BitStruct) 112 | nested_class.class_eval(&block) 113 | end 114 | 115 | opts[:default] ||= nested_class.initial_value.dup 116 | opts[:nested_class] = nested_class 117 | field = add_field(name, nested_class.bit_length, opts) 118 | field 119 | end 120 | alias struct nest 121 | BitStruct.autoload :NestedField, "bit-struct/nested-field" 122 | 123 | # Define an octet string field in the current subclass of BitStruct, 124 | # with the given _name_ and _length_ (in bits). Trailing nulls are 125 | # not considered part of the string. The field is accessed using 126 | # period-separated decimal digits. 127 | # 128 | # If a class is provided, use it for the Field class. 129 | # If a string is provided, use it for the display_name. 130 | # If a hash is provided, use it for options. 131 | # 132 | def octets(name, length, *rest) 133 | opts = parse_options(rest, name, OctetField) 134 | add_field(name, length, opts) 135 | end 136 | BitStruct.autoload :OctetField, "bit-struct/octet-field" 137 | 138 | # Define a padding field in the current subclass of BitStruct, 139 | # with the given _name_ and _length_ (in bits). 140 | # 141 | # If a class is provided, use it for the Field class. 142 | # If a string is provided, use it for the display_name. 143 | # If a hash is provided, use it for options. 144 | # 145 | def pad(name, length, *rest) 146 | opts = parse_options(rest, name, PadField) 147 | add_field(name, length, opts) 148 | end 149 | alias padding pad 150 | BitStruct.autoload :PadField, "bit-struct/pad-field" 151 | 152 | # Define a signed integer field in the current subclass of BitStruct, 153 | # with the given _name_ and _length_ (in bits). 154 | # 155 | # If a class is provided, use it for the Field class. 156 | # If a string is provided, use it for the display_name. 157 | # If a hash is provided, use it for options. 158 | # 159 | # SignedField adds the :fixed => divisor option, which specifies 160 | # that the internally stored value is interpreted as a fixed point real 161 | # number with the specified +divisor+. 162 | # 163 | # The :endian => :native option overrides the default of 164 | # :network byte ordering, in favor of native byte ordering. Also 165 | # permitted are :big (same as :network) and 166 | # :little. 167 | # 168 | def signed name, length, *rest 169 | opts = parse_options(rest, name, SignedField) 170 | add_field(name, length, opts) 171 | end 172 | BitStruct.autoload :SignedField, "bit-struct/signed-field" 173 | 174 | # Define a printable text string field in the current subclass of BitStruct, 175 | # with the given _name_ and _length_ (in bits). Trailing nulls are 176 | # _not_ considered part of the string. 177 | # 178 | # If a class is provided, use it for the Field class. 179 | # If a string is provided, use it for the display_name. 180 | # If a hash is provided, use it for options. 181 | # 182 | # Note that the accessors have COPY semantics, not reference. 183 | # 184 | def text(name, length, *rest) 185 | opts = parse_options(rest, name, TextField) 186 | add_field(name, length, opts) 187 | end 188 | BitStruct.autoload :TextField, "bit-struct/text-field" 189 | 190 | # Define a unsigned integer field in the current subclass of BitStruct, 191 | # with the given _name_ and _length_ (in bits). 192 | # 193 | # If a class is provided, use it for the Field class. 194 | # If a string is provided, use it for the display_name. 195 | # If a hash is provided, use it for options. 196 | # 197 | # UnsignedField adds the :fixed => divisor option, which specifies 198 | # that the internally stored value is interpreted as a fixed point real 199 | # number with the specified +divisor+. 200 | # 201 | # The :endian => :native option overrides the default of 202 | # :network byte ordering, in favor of native byte ordering. Also 203 | # permitted are :big (same as :network) and 204 | # :little. 205 | # 206 | def unsigned name, length, *rest 207 | opts = parse_options(rest, name, UnsignedField) 208 | add_field(name, length, opts) 209 | end 210 | BitStruct.autoload :UnsignedField, "bit-struct/unsigned-field" 211 | 212 | # Define a vector field in the current subclass of BitStruct, 213 | # with the given _name_. 214 | # 215 | # If a class is provided, use it for the Vector class, otherwise 216 | # the block must define the entry fields. The two forms looks like 217 | # this: 218 | # 219 | # class Vec < BitStruct::Vector 220 | # # these declarations apply to *each* entry in the vector: 221 | # unsigned :x, 16 222 | # signed :y, 32 223 | # end 224 | # 225 | # class Packet < BitStruct 226 | # # Using the Vec class defined above 227 | # vector :v, Vec, "a vector", :length => 5 228 | # 229 | # # equivalently, using an anonymous subclass of BitStruct::Vector 230 | # vector :v2, "a vector", :length => 5 do 231 | # unsigned :x, 16 232 | # signed :y, 32 233 | # end 234 | # end 235 | # 236 | # If a string is provided, use it for the display_name. 237 | # If a hash is provided, use it for options. 238 | # If a number is provided, use it for length (equivalent to using the 239 | # :length option). 240 | # 241 | # WARNING: the accessors have COPY semantics, not reference. When you call a 242 | # reader method to get the vector structure, you get a *copy* of that data. 243 | # 244 | # For example, to modify the numeric fields in a Packet as defined above: 245 | # 246 | # pkt = Packet.new 247 | # vec = pkt.v 248 | # entry = vec[2] 249 | # entry.x = 123 250 | # entry.y = -456 251 | # vec[2] = entry 252 | # pkt.v = vec 253 | # 254 | def vector(name, *rest, &block) 255 | opts = parse_options(rest, name, nil) 256 | cl = opts[:field_class] 257 | opts[:field_class] = VectorField 258 | 259 | unless (block and not cl) or (cl and not block) 260 | raise ArgumentError, 261 | "vector must have either a class or a block, but not both" 262 | end 263 | 264 | case 265 | when cl == nil 266 | vector_class = Class.new(BitStruct::Vector) 267 | vector_class.class_eval(&block) 268 | 269 | when cl < BitStruct 270 | vector_class = Class.new(BitStruct::Vector) 271 | vector_class.struct_class cl 272 | 273 | when cl < BitStruct::Vector 274 | vector_class = cl 275 | 276 | else raise ArgumentError, "Bad vector class: #{cl.inspect}" 277 | end 278 | 279 | vector_class.default_options default_options 280 | 281 | length = opts[:length] || rest.grep(Integer).first 282 | ## what about :length => :lenfield 283 | unless length 284 | raise ArgumentError, 285 | "Must provide length as argument N or as option :length => N" 286 | end 287 | 288 | opts[:default] ||= vector_class.new(length) ## nil if variable length 289 | opts[:vector_class] = vector_class 290 | 291 | bit_length = vector_class.struct_class.round_byte_length * 8 * length 292 | 293 | field = add_field(name, bit_length, opts) 294 | field 295 | end 296 | BitStruct.autoload :VectorField, "bit-struct/vector-field" 297 | end 298 | 299 | autoload :Vector, "bit-struct/vector" 300 | end 301 | -------------------------------------------------------------------------------- /lib/bit-struct/float-field.rb: -------------------------------------------------------------------------------- 1 | class BitStruct 2 | # Class for floats (single and double precision) in network order. 3 | # Declared with BitStruct.float. 4 | class FloatField < Field 5 | # Used in describe. 6 | def self.class_name 7 | @class_name ||= "float" 8 | end 9 | 10 | def add_accessors_to(cl, attr = name) # :nodoc: 11 | unless offset % 8 == 0 12 | raise ArgumentError, 13 | "Bad offset, #{offset}, for #{self.class} #{name}." + 14 | " Must be multiple of 8." 15 | end 16 | 17 | unless length == 32 or length == 64 18 | raise ArgumentError, 19 | "Bad length, #{length}, for #{self.class} #{name}." + 20 | " Must be 32 or 64." 21 | end 22 | 23 | offset_byte = offset / 8 24 | length_byte = length / 8 25 | last_byte = offset_byte + length_byte - 1 26 | byte_range = offset_byte..last_byte 27 | 28 | endian = (options[:endian] || options["endian"]).to_s 29 | case endian 30 | when "native" 31 | ctl = case length 32 | when 32; "f" 33 | when 64; "d" 34 | end 35 | when "little" 36 | ctl = case length 37 | when 32; "e" 38 | when 64; "E" 39 | end 40 | when "network", "big", "" 41 | ctl = case length 42 | when 32; "g" 43 | when 64; "G" 44 | end 45 | else 46 | raise ArgumentError, 47 | "Unrecognized endian option: #{endian.inspect}" 48 | end 49 | 50 | cl.class_eval do 51 | define_method attr do || 52 | self[byte_range].unpack(ctl).first 53 | end 54 | 55 | define_method "#{attr}=" do |val| 56 | self[byte_range] = [val].pack(ctl) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/bit-struct/hex-octet-field.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct/octet-field' 2 | 3 | class BitStruct 4 | # Class for char fields that can be accessed with values like 5 | # "xx:xx:xx:xx", where each xx is up to 2 hex digits representing a 6 | # single octet. The original string-based accessors are still available with 7 | # the _chars suffix. 8 | # 9 | # Declared with BitStruct.hex_octets. 10 | class HexOctetField < BitStruct::OctetField 11 | # Used in describe. 12 | def self.class_name 13 | @class_name ||= "hex_octets" 14 | end 15 | 16 | SEPARATOR = ":" 17 | FORMAT = "%02x" 18 | BASE = 16 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/bit-struct/nested-field.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct/bit-struct' 2 | 3 | class BitStruct 4 | # Class for nesting a BitStruct as a field within another BitStruct. 5 | # Declared with BitStruct.nest. 6 | class NestedField < Field 7 | def initialize(*args) 8 | super 9 | end 10 | 11 | # Used in describe. 12 | def self.class_name 13 | @class_name ||= "nest" 14 | end 15 | 16 | def class_name 17 | @class_name ||= nested_class.name[/\w+$/] 18 | end 19 | 20 | def nested_class 21 | @nested_class ||= options[:nested_class] || options["nested_class"] 22 | end 23 | 24 | def describe opts 25 | if opts[:expand] 26 | opts = opts.dup 27 | opts[:byte_offset] = offset / 8 28 | opts[:omit_header] = opts[:omit_footer] = true 29 | nested_class.describe(nil, opts) {|desc| yield desc} 30 | else 31 | super 32 | end 33 | end 34 | 35 | def add_accessors_to(cl, attr = name) # :nodoc: 36 | unless offset % 8 == 0 37 | raise ArgumentError, 38 | "Bad offset, #{offset}, for nested field #{name}." + 39 | " Must be multiple of 8." 40 | end 41 | 42 | unless length % 8 == 0 43 | raise ArgumentError, 44 | "Bad length, #{length}, for nested field #{name}." + 45 | " Must be multiple of 8." 46 | end 47 | 48 | offset_byte = offset / 8 49 | length_byte = length / 8 50 | last_byte = offset_byte + length_byte - 1 51 | byte_range = offset_byte..last_byte 52 | 53 | nc = nested_class 54 | 55 | cl.class_eval do 56 | define_method attr do || 57 | nc.new(self[byte_range]) 58 | end 59 | 60 | define_method "#{attr}=" do |val| 61 | if val.length != length_byte 62 | raise ArgumentError, "Size mismatch in nested struct assignment " + 63 | "to #{attr} with value #{val.inspect}" 64 | end 65 | 66 | if val.class != nc 67 | warn "Type mismatch in nested struct assignment " + 68 | "to #{attr} with value #{val.inspect}" 69 | end 70 | 71 | self[byte_range] = val 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/bit-struct/octet-field.rb: -------------------------------------------------------------------------------- 1 | require 'bit-struct/char-field' 2 | 3 | class BitStruct 4 | # Class for char fields that can be accessed with values like 5 | # "xxx.xxx.xxx.xxx", where each xxx is up to 3 decimal digits representing a 6 | # single octet. The original string-based accessors are still available with 7 | # the _chars suffix. 8 | # 9 | # Declared with BitStruct.octets. 10 | class OctetField < BitStruct::CharField 11 | # Used in describe. 12 | def self.class_name 13 | @class_name ||= "octets" 14 | end 15 | 16 | SEPARATOR = "." 17 | FORMAT = "%d" 18 | BASE = 10 19 | 20 | def add_accessors_to(cl, attr = name) # :nodoc: 21 | attr_chars = "#{attr}_chars" 22 | super(cl, attr_chars) 23 | sep = self.class::SEPARATOR 24 | base = self.class::BASE 25 | fmt = self.class::FORMAT 26 | 27 | cl.class_eval do 28 | define_method attr do || 29 | ary = [] 30 | send(attr_chars).each_byte do |c| 31 | ary << fmt % c 32 | end 33 | ary.join(sep) 34 | end 35 | 36 | old_writer = "#{attr_chars}=" 37 | 38 | define_method "#{attr}=" do |val| 39 | data = val.split(sep).map{|s|s.to_i(base)}.pack("c*") 40 | send(old_writer, data) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/bit-struct/pad-field.rb: -------------------------------------------------------------------------------- 1 | class BitStruct 2 | # Class for fixed length padding. 3 | class PadField < Field 4 | # Used in describe. 5 | def self.class_name 6 | @class_name ||= "padding" 7 | end 8 | 9 | def add_accessors_to(cl, attr = name) # :nodoc: 10 | # No accessors for padding. 11 | end 12 | 13 | def inspectable?; false; end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/bit-struct/signed-field.rb: -------------------------------------------------------------------------------- 1 | class BitStruct 2 | # Class for signed integers in network order, 1-16 bits, or 8n bits. 3 | # Declared with BitStruct.signed. 4 | class SignedField < Field 5 | # Used in describe. 6 | def self.class_name 7 | @class_name ||= "signed" 8 | end 9 | 10 | def add_accessors_to(cl, attr = name) # :nodoc: 11 | offset_byte = offset / 8 12 | offset_bit = offset % 8 13 | 14 | length_bit = offset_bit + length 15 | length_byte = (length_bit/8.0).ceil 16 | last_byte = offset_byte + length_byte - 1 17 | mid = 2**(length-1) 18 | max_unsigned = 2**length 19 | to_signed = proc {|n| (n>=mid) ? n - max_unsigned : n} 20 | 21 | divisor = options[:fixed] || options["fixed"] 22 | divisor_f = divisor && divisor.to_f 23 | 24 | endian = (options[:endian] || options["endian"]).to_s 25 | case endian 26 | when "native" 27 | ctl = length_byte <= 2 ? "s" : "l" 28 | if length == 16 or length == 32 29 | to_signed = proc {|n| n} 30 | # with pack support, to_signed can be replaced with no-op 31 | end 32 | when "little" 33 | ctl = length_byte <= 2 ? "v" : "V" 34 | when "network", "big", "" 35 | ctl = length_byte <= 2 ? "n" : "N" 36 | else 37 | raise ArgumentError, 38 | "Unrecognized endian option: #{endian.inspect}" 39 | end 40 | 41 | data_is_big_endian = 42 | ([1234].pack(ctl) == [1234].pack(length_byte <= 2 ? "n" : "N")) 43 | 44 | if length_byte == 1 45 | rest = 8 - length_bit 46 | mask = ["0"*offset_bit + "1"*length + "0"*rest].pack("B8")[0].ord 47 | mask2 = ["1"*offset_bit + "0"*length + "1"*rest].pack("B8")[0].ord 48 | 49 | cl.class_eval do 50 | if divisor 51 | define_method attr do || 52 | to_signed[(self[offset_byte] & mask) >> rest] / divisor_f 53 | end 54 | 55 | define_method "#{attr}=" do |val| 56 | val = (val * divisor).round 57 | self[offset_byte] = 58 | (self[offset_byte] & mask2) | ((val<> rest] 64 | end 65 | 66 | define_method "#{attr}=" do |val| 67 | self[offset_byte] = 68 | (self[offset_byte] & mask2) | ((val< 0 134 | bytes.push val % 256 135 | val = val >> 8 136 | end 137 | if bytes.length < length_byte 138 | bytes.concat [0] * (length_byte - bytes.length) 139 | end 140 | 141 | bytes.reverse! if data_is_big_endian 142 | bytes.pack("C*") 143 | end 144 | 145 | if divisor 146 | define_method attr do || 147 | to_signed[reader_helper[self[byte_range]] / divisor_f] 148 | end 149 | 150 | define_method "#{attr}=" do |val| 151 | self[byte_range] = writer_helper[(val * divisor).round] 152 | end 153 | 154 | else 155 | define_method attr do || 156 | to_signed[reader_helper[self[byte_range]]] 157 | end 158 | 159 | define_method "#{attr}=" do |val| 160 | self[byte_range] = writer_helper[val] 161 | end 162 | end 163 | end 164 | end 165 | 166 | elsif length_byte == 2 # unaligned field that fits within two whole bytes 167 | byte_range = offset_byte..last_byte 168 | rest = 16 - length_bit 169 | 170 | mask = ["0"*offset_bit + "1"*length + "0"*rest] 171 | mask = mask.pack("B16").unpack(ctl).first 172 | 173 | mask2 = ["1"*offset_bit + "0"*length + "1"*rest] 174 | mask2 = mask2.pack("B16").unpack(ctl).first 175 | 176 | cl.class_eval do 177 | if divisor 178 | define_method attr do || 179 | to_signed[(self[byte_range].unpack(ctl).first & mask) >> rest] / 180 | divisor_f 181 | end 182 | 183 | define_method "#{attr}=" do |val| 184 | val = (val * divisor).round 185 | x = (self[byte_range].unpack(ctl).first & mask2) | 186 | ((val<> rest] 193 | end 194 | 195 | define_method "#{attr}=" do |val| 196 | x = (self[byte_range].unpack(ctl).first & mask2) | 197 | ((val<> rest)] / 219 | divisor_f 220 | end 221 | 222 | define_method "#{attr}=" do |val| 223 | val = (val * divisor).round 224 | bytes = self[byte_range] 225 | bytes << 0 226 | x = (bytes.unpack(ctl).first & mask2) | 227 | ((val<> rest] 236 | end 237 | 238 | define_method "#{attr}=" do |val| 239 | bytes = self[byte_range] 240 | bytes << 0 241 | x = (bytes.unpack(ctl).first & mask2) | 242 | ((val<> rest) / divisor_f 46 | end 47 | 48 | define_method "#{attr}=" do |val| 49 | val = (val * divisor).round 50 | self[offset_byte] = 51 | (self[offset_byte] & mask2) | ((val<> rest 57 | end 58 | 59 | define_method "#{attr}=" do |val| 60 | self[offset_byte] = 61 | (self[offset_byte] & mask2) | ((val< 0 126 | bytes.push val % 256 127 | val = val >> 8 128 | end 129 | if bytes.length < length_byte 130 | bytes.concat [0] * (length_byte - bytes.length) 131 | end 132 | 133 | bytes.reverse! if data_is_big_endian 134 | bytes.pack("C*") 135 | end 136 | 137 | if divisor 138 | define_method attr do || 139 | reader_helper[self[byte_range]] / divisor_f 140 | end 141 | 142 | define_method "#{attr}=" do |val| 143 | self[byte_range] = writer_helper[(val * divisor).round] 144 | end 145 | 146 | else 147 | define_method attr do || 148 | reader_helper[self[byte_range]] 149 | end 150 | 151 | define_method "#{attr}=" do |val| 152 | self[byte_range] = writer_helper[val] 153 | end 154 | end 155 | end 156 | end 157 | 158 | elsif length_byte == 2 # unaligned field that fits within two whole bytes 159 | byte_range = offset_byte..last_byte 160 | rest = 16 - length_bit 161 | 162 | mask = ["0"*offset_bit + "1"*length + "0"*rest] 163 | mask = mask.pack("B16").unpack(ctl).first 164 | 165 | mask2 = ["1"*offset_bit + "0"*length + "1"*rest] 166 | mask2 = mask2.pack("B16").unpack(ctl).first 167 | 168 | cl.class_eval do 169 | if divisor 170 | define_method attr do || 171 | ((self[byte_range].unpack(ctl).first & mask) >> rest) / 172 | divisor_f 173 | end 174 | 175 | define_method "#{attr}=" do |val| 176 | val = (val * divisor).round 177 | x = (self[byte_range].unpack(ctl).first & mask2) | 178 | ((val<> rest 185 | end 186 | 187 | define_method "#{attr}=" do |val| 188 | x = (self[byte_range].unpack(ctl).first & mask2) | 189 | ((val<> rest) / 211 | divisor_f 212 | end 213 | 214 | define_method "#{attr}=" do |val| 215 | val = (val * divisor).round 216 | bytes = self[byte_range] 217 | bytes << 0 218 | x = (bytes.unpack(ctl).first & mask2) | 219 | ((val<> rest 228 | end 229 | 230 | define_method "#{attr}=" do |val| 231 | bytes = self[byte_range] 232 | bytes << 0 233 | x = (bytes.unpack(ctl).first & mask2) | 234 | ((val<:endian => val option. 63 | def default_options h = nil 64 | @default_options ||= superclass.default_options.dup 65 | if h 66 | @default_options.merge! h 67 | if @struct_class 68 | @struct_class.default_options h 69 | end 70 | end 71 | @default_options 72 | end 73 | 74 | def describe(*args) 75 | fmt = args[0] || BitStruct.describe_format 76 | if block_given? 77 | struct_class.describe(*args){|desc| yield desc} 78 | yield ["..."]*5 79 | else 80 | struct_class.describe(*args) + [fmt % (["..."]*5)] 81 | end 82 | end 83 | end 84 | 85 | # Convenience method for instances. Returns the BitStruct class that 86 | # describes each entry. 87 | def struct_class 88 | self.class.struct_class 89 | end 90 | 91 | # Convenience method for instances. Returns the string length in bytes of 92 | # each entry in the vector. 93 | def struct_class_length 94 | self.class.struct_class.round_byte_length 95 | end 96 | 97 | # +arg+ can be an integer (number of entries) or a string 98 | # (binary data, such as another Vector of the same size). 99 | def initialize arg # :yields: instance 100 | case arg 101 | when Integer 102 | super(struct_class.initial_value * arg) 103 | 104 | else 105 | begin 106 | super arg 107 | rescue NameError 108 | raise ArgumentError, "must be string or integer: #{arg.inspect}" 109 | end 110 | end 111 | 112 | yield self if block_given? 113 | end 114 | 115 | # Get the +i+-th entry. Returns a *copy* of the entry. If you want to 116 | # use this copy to modify the entry, you must modify the copy and then 117 | # use #[]= to replace the entry with the copy. 118 | def [](i) 119 | sc = self.class.struct_class 120 | entry_length = sc.round_byte_length 121 | 122 | unless (0...(length / entry_length)).include? i 123 | raise ArgumentError, "index out of range: #{i}" 124 | end 125 | 126 | sc.new slice(entry_length * i, entry_length) 127 | end 128 | 129 | alias _old_replace_substr []= 130 | 131 | # Set the +i+-th entry to +val+. 132 | def []=(i,val) 133 | entry_length = struct_class_length 134 | 135 | unless (0...(length / entry_length)).include? i 136 | raise ArgumentError, "index out of range: #{i}" 137 | end 138 | 139 | unless val.length == entry_length 140 | raise ArgumentError, "wrong entry length: #{val.length} != #{entry_length}" 141 | end 142 | 143 | _old_replace_substr(entry_length * i, entry_length, val) 144 | end 145 | 146 | ## TODO: [i..j] etc. 147 | 148 | # Iterate over entries. 149 | def each 150 | entry_length = struct_class_length 151 | (length / entry_length).times do |i| 152 | yield self[i] 153 | end 154 | end 155 | 156 | def inspect_with_options(opts = BitStruct::DEFAULT_INSPECT_OPTS) 157 | if opts[:include_class] 158 | opts = opts.dup 159 | opts[:include_class] = false 160 | s = self.class.inspect + ": " 161 | else 162 | s = "" 163 | end 164 | 165 | s << entries.map{|entry| entry.inspect(opts)}.join(opts[:separator]) 166 | lb, rb = opts[:brackets] 167 | [lb, s, rb].join 168 | end 169 | 170 | alias inspect inspect_with_options 171 | 172 | def inspect_detailed 173 | inspect(BitStruct::DETAILED_INSPECT_OPTS) 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/bit-struct/yaml.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | class BitStruct 4 | if RUBY_VERSION == "1.8.2" 5 | def is_complex_yaml? # :nodoc: 6 | true 7 | end 8 | 9 | YAML.add_ruby_type(/^bitstruct/) do |type, val| 10 | _subtype, subclass = YAML.read_type_class(type, Object) 11 | subclass.new(val) 12 | end 13 | 14 | def to_yaml_type # :nodoc: 15 | "!ruby/bitstruct:#{self.class}" 16 | end 17 | 18 | def to_yaml( opts = {} ) # :nodoc: 19 | opts[:DocType] = self.class if Hash === opts 20 | YAML.quick_emit(self.object_id, opts) do |out| 21 | out.map(to_yaml_type) do |map| 22 | fields.each do |field| 23 | fn = field.name 24 | map.add(fn, send(fn)) 25 | end 26 | end 27 | end 28 | end 29 | 30 | elsif RUBY_VERSION =~ /^2\./ 31 | def encode_with coder 32 | yaml_fields = fields.select {|field| field.inspectable?} 33 | props = yaml_fields.map {|f| f.name.to_s} 34 | if (rest_field = self.class.rest_field) 35 | props << rest_field.name.to_s 36 | end 37 | props.each do |prop| 38 | coder[prop] = send(prop) 39 | end 40 | end 41 | 42 | def init_with coder 43 | self << self.class.initial_value 44 | coder.map.each do |k, v| 45 | send("#{k}=", v) 46 | end 47 | end 48 | 49 | else 50 | yaml_tag "tag:github.com:vjoel/bit-struct" 51 | 52 | def to_yaml_properties # :nodoc: 53 | yaml_fields = fields.select {|field| field.inspectable?} 54 | props = yaml_fields.map {|f| f.name.to_s} 55 | if (rest_field = self.class.rest_field) 56 | props << rest_field.name.to_s 57 | end 58 | props 59 | end 60 | 61 | # Return YAML representation of the BitStruct. 62 | def to_yaml( opts = {} ) 63 | YAML::quick_emit( object_id, opts ) do |out| 64 | out.map( taguri, to_yaml_style ) do |map| 65 | to_yaml_properties.each do |m| 66 | map.add( m, send( m ) ) 67 | end 68 | end 69 | end 70 | end 71 | 72 | def self.yaml_new( klass, tag, val ) # :nodoc: 73 | unless Hash === val 74 | raise YAML::TypeError, "Invalid BitStruct: " + val.inspect 75 | end 76 | 77 | _bitstruct_name, bitstruct_type = YAML.read_type_class( tag, BitStruct ) 78 | 79 | st = bitstruct_type.new 80 | 81 | val.each do |k,v| 82 | st.send( "#{k}=", v ) 83 | end 84 | 85 | st 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/test-endian.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'bit-struct' 3 | 4 | class Test_Endian < Minitest::Test 5 | class Endian < BitStruct 6 | unsigned :f_big, 32, :endian => :big 7 | unsigned :f_little, 32, :endian => :little 8 | unsigned :f_native, 32, :endian => :native 9 | unsigned :f_network, 32, :endian => :network 10 | end 11 | 12 | attr_reader :bs 13 | 14 | def setup 15 | @bs = Endian.new 16 | bs.f_big = bs.f_little = bs.f_native = bs.f_network = 0x01020304 17 | end 18 | 19 | def test_readers 20 | assert_equal(0x01020304, bs.f_big) 21 | assert_equal(0x01020304, bs.f_little) 22 | assert_equal(0x01020304, bs.f_native) 23 | assert_equal(0x01020304, bs.f_network) 24 | end 25 | 26 | def test_writers 27 | bs.fields.each do |field| 28 | byte_offset = field.offset / 8 29 | valstr = bs.to_s[byte_offset, 4] 30 | case field.options[:endian] 31 | when :big, :network 32 | assert_equal("\01\02\03\04", valstr) 33 | when :little 34 | assert_equal("\04\03\02\01", valstr) 35 | when :native 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test-vector.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'bit-struct' 3 | 4 | class Test_Vector < Minitest::Test 5 | class Packet < BitStruct 6 | unsigned :stuff, 32, "whatever" 7 | 8 | vector :v, "a vector", :length => 5 do 9 | unsigned :x, 16 10 | signed :y, 32 11 | end 12 | 13 | unsigned :other, 16, "other stuff" 14 | end 15 | 16 | attr_reader :pkt 17 | 18 | def setup 19 | @pkt = Packet.new 20 | end 21 | 22 | def test_length 23 | assert_equal(Packet.round_byte_length, pkt.length) 24 | end 25 | 26 | def test_writers 27 | assert_equal(pkt.v[2].x, 0) 28 | v = pkt.v 29 | xy = v[2] 30 | xy.x = 3 31 | xy.y = -4 32 | v[2] = xy 33 | assert_equal(pkt.v[2].x, 0) 34 | pkt.v = v 35 | assert_equal(pkt.v[2].x, 3) 36 | assert_equal(pkt.v[2].y, -4) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'bit-struct' 3 | 4 | class Test_BitStruct < Minitest::Test 5 | 6 | class T1 < BitStruct 7 | unsigned :foo, 8 8 | end 9 | 10 | class T2 < BitStruct 11 | unsigned :bar, 8 12 | end 13 | 14 | class Rest < BitStruct 15 | unsigned :zap, 8 16 | rest :body, T2 17 | end 18 | 19 | class NestedPart < BitStruct 20 | unsigned :x, 5 21 | unsigned :y, 3, :default => 2 22 | char :s, 5*8 23 | end 24 | 25 | class Container < BitStruct 26 | nest :n1, NestedPart, :default => NestedPart.new(:x=>1, :s=>"deflt") 27 | nest :n2, NestedPart 28 | end 29 | 30 | class Overflow < BitStruct 31 | unsigned :f1, 3 32 | unsigned :f2, 5 33 | unsigned :f3, 8 34 | unsigned :f4, 16 35 | unsigned :f5, 32 36 | char :f6, 24 37 | unsigned :f7, 8 38 | end 39 | 40 | class BS < BitStruct 41 | def self.next_name 42 | (@@next_name ||= "f000").succ!.dup 43 | end 44 | 45 | unsigned next_name, 13 46 | unsigned next_name, 6 47 | signed next_name, 5 48 | 49 | char next_name, 16 50 | 51 | unsigned next_name, 2 52 | signed next_name, 3, :default => -2 53 | unsigned next_name, 3 54 | 55 | unsigned f1=next_name, 16 56 | 57 | signed next_name, 8 58 | signed next_name, 16, :endian => :little 59 | signed next_name, 16, :endian => :native 60 | signed f2=next_name, 32 61 | unsigned next_name, 56, :endian => :big 62 | signed next_name, 48, :endian => :little 63 | 64 | char next_name, 8, :default => "b" 65 | float next_name, 32, :default => 1.23 66 | float next_name, 64, :format => "%10.5f" 67 | 68 | octets next_name, 32, :default => "192.168.1.123" 69 | 70 | hex_octets next_name, 48, :default => "ab:cd:ef:01:23:45" 71 | 72 | unsigned next_name, 1 73 | unsigned next_name, 11, :fixed => 100 # unaligned! 74 | signed next_name, 7 # unaligned! 75 | signed next_name, 14 # unaligned and touches 3 bytes 76 | signed next_name, 7, :fixed => 1000 # unaligned! 77 | 78 | char next_name, 24 79 | 80 | rest :bs_body 81 | 82 | INITIAL_VALUES = { 83 | f1 => 1234, 84 | f2 => 5678 85 | } 86 | 87 | INITIAL_VALUES.each do |f, v| 88 | initial_value.send "#{f}=", v 89 | end 90 | 91 | end 92 | 93 | class BS_1 < BS 94 | 95 | signed next_name, 4, :fixed => 25 96 | unsigned next_name, 3, :fixed => 0.01 97 | unsigned next_name, 1 98 | 99 | unsigned next_name, 32, :fixed => 1000 100 | 101 | char next_name, 40 102 | text next_name, 40 103 | 104 | unsigned next_name, 8, :default => 0xEF 105 | 106 | unsigned next_name, 8 107 | 108 | unsigned next_name, 1 109 | 110 | rest :bs1_body 111 | 112 | end 113 | 114 | def setup 115 | srand(767343) 116 | @bs = BS.new 117 | @bs_1 = BS_1.new 118 | @simple = [T1.new, T2.new] 119 | @testers = @simple + [@bs, @bs_1] 120 | end 121 | 122 | def test_init 123 | @testers.each do |bs| 124 | if defined?(bs.class::INITIAL_VALUES) 125 | initial_values = bs.class::INITIAL_VALUES 126 | end 127 | bs.fields.each do |field| 128 | iv = initial_values && initial_values[field.name] 129 | iv ||= field.default 130 | 131 | if iv 132 | case field 133 | when BitStruct::FloatField 134 | assert_in_delta(iv, bs.send(field.name), 100, 135 | "In #{field.name} of a #{bs.class}") 136 | else 137 | assert_equal(iv, bs.send(field.name), 138 | "In #{field.name} of a #{bs.class}") 139 | end 140 | end 141 | end 142 | end 143 | end 144 | 145 | def test_init_with_value 146 | randomize(@bs_1) 147 | 148 | b = BS_1.new(@bs_1) 149 | assert_equal(@bs_1, b, "Initialize with argument failed.") 150 | 151 | c = BS_1.new(b) 152 | assert_equal(@bs_1, c, "Initialize with argument failed.") 153 | 154 | b1 = BS_1.new("") 155 | b2 = BS_1.new(nil) 156 | assert_equal(b1, b2, "Initialize with short argument failed.") 157 | end 158 | 159 | def test_init_with_hash 160 | randomize(@bs_1) 161 | 162 | h = {} 163 | @bs_1.fields.each do |f| 164 | h[f.name] = @bs_1.send(f.name) 165 | end 166 | 167 | b = BS_1.new(h) 168 | assert_equal(@bs_1, b, "Initialize with argument failed.") 169 | end 170 | 171 | def test_join 172 | assert_equal(@bs+@bs_1, BitStruct.join(@bs,@bs_1)) 173 | assert_equal(@bs+@bs_1, [@bs,@bs_1].join("")) 174 | end 175 | 176 | def test_parse 177 | orig = @testers 178 | orig.each do |bs| 179 | randomize(bs) 180 | end 181 | 182 | data = BitStruct.join(orig) 183 | round_trip = BitStruct.parse(data, orig.map{|bs|bs.class}) 184 | orig.zip(round_trip) do |bs1, bs2| 185 | assert_equal(bs1, bs2) 186 | end 187 | end 188 | 189 | def test_closed 190 | assert_raises(BitStruct::ClosedClassError) do 191 | BS.class_eval do 192 | unsigned :foo, 3 193 | end 194 | end 195 | end 196 | 197 | def test_rest 198 | len0 = @bs_1.length 199 | 200 | @bs_1.bs1_body = "a"*50 201 | assert_equal("a"*50, @bs_1.bs1_body) 202 | assert_equal(len0+50, @bs_1.length) 203 | 204 | @bs_1.bs1_body = "b"*60 205 | assert_equal("b"*60, @bs_1.bs1_body) 206 | assert_equal(len0+60, @bs_1.length) 207 | 208 | @bs_1.bs1_body = "c"*40 209 | assert_equal("c"*40, @bs_1.bs1_body) 210 | assert_equal(len0+40, @bs_1.length) 211 | 212 | @bs_1.bs1_body = "" 213 | assert_equal("", @bs_1.bs1_body) 214 | assert_equal(len0, @bs_1.length) 215 | end 216 | 217 | def test_rest_with_class 218 | r = Rest.new 219 | t2 = T2.new 220 | t2.bar = 123 221 | r.body = t2 222 | assert_equal(123, r.body.bar) 223 | end 224 | 225 | def test_rest_with_class_constructed 226 | r = Rest.new(['0011'].pack('H*')) 227 | assert_equal(0x00, r.zap) 228 | assert_equal(0x11, r.body.bar) 229 | end 230 | 231 | def test_nest 232 | cont = Container.new 233 | n1 = cont.n1 234 | 235 | assert_equal(1, n1.x) 236 | assert_equal(2, n1.y) 237 | assert_equal("deflt", n1.s) 238 | 239 | n1.sub(/./, " ") 240 | 241 | assert_equal(1, n1.x) 242 | assert_equal(2, n1.y) 243 | assert_equal("deflt", n1.s) 244 | 245 | n1 = cont.n1 246 | n2 = cont.n2 247 | 248 | assert_equal(0, n2.x) 249 | assert_equal(2, n2.y) 250 | assert_equal("\0"*5, n2.s) 251 | 252 | n = NestedPart.new(:x=>4, :y=>1, :s=>"qwert") 253 | cont.n1 = n 254 | 255 | assert_equal(4, cont.n1.x) 256 | assert_equal(1, cont.n1.y) 257 | assert_equal("qwert", cont.n1.s) 258 | 259 | assert_raises(ArgumentError) {cont.n2 = Container.new} 260 | end 261 | 262 | def test_overflow 263 | ov = Overflow.new 264 | empty = ov.dup 265 | 266 | ov.fields.each do |field| 267 | ov.gsub(/./, "\0") 268 | 269 | case field 270 | when BitStruct::CharField 271 | val1 = "a" * (field.length+5) 272 | val2 = "" 273 | 274 | when BitStruct::UnsignedField 275 | val1 = 2**32 - 5**10 # mixed bit pattern 276 | val2 = 0 277 | end 278 | 279 | ov.send("#{field.name}=", val1) 280 | ov.send("#{field.name}=", val2) 281 | 282 | assert_equal(empty, ov) 283 | end 284 | end 285 | 286 | def test_access 287 | repeat_access_test(@bs, 10) 288 | end 289 | 290 | def test_inheritance 291 | assert_equal(@bs.fields.size, @bs_1.fields.size - BS_1.own_fields.size, 292 | "Wrong number of fields inherited in BS_1.") 293 | 294 | repeat_access_test(@bs_1, 10) 295 | end 296 | 297 | def test_initial_value 298 | bs = @bs 299 | bs.class::INITIAL_VALUES.each do |f,v| 300 | assert_equal(v, bs.send(f), "In #{f} of a #{bs.class}") 301 | end 302 | end 303 | 304 | def test_inherited_initial_value 305 | bs = @bs_1 306 | bs.class::INITIAL_VALUES.each do |f,v| 307 | assert_equal(v, bs.send(f), "In #{f} of a #{bs.class}") 308 | end 309 | end 310 | 311 | def test_to_h 312 | h = @bs_1.to_h(:convert_keys => :to_s) 313 | field_names = @bs_1.fields.map{|f|f.name.to_s} 314 | assert_equal(field_names.sort, h.keys.sort) 315 | field_names.each do |name| 316 | assert_equal(@bs_1.send(name), h[name]) 317 | end 318 | end 319 | 320 | def test_to_a_exclude_rest 321 | include_rest = false 322 | a = @bs_1.to_a(include_rest) 323 | field_names = @bs_1.fields.map{|f|f.name.to_s} 324 | assert_equal(a.size, field_names.size) 325 | field_names.each_with_index do |name, i| 326 | assert_equal(@bs_1.send(name), a[i]) 327 | end 328 | end 329 | 330 | def test_to_a 331 | include_rest = true 332 | a = @bs_1.to_a(include_rest) 333 | field_names = @bs_1.fields.map{|f|f.name.to_s} 334 | field_names << @bs_1.rest_field.name 335 | assert_equal(a.size, field_names.size) 336 | field_names.each_with_index do |name, i| 337 | assert_equal(@bs_1.send(name), a[i]) 338 | end 339 | end 340 | 341 | def test_format_option 342 | formatted_fields = @bs.fields.select {|f|f.format} 343 | formatted_fields.each do |f| 344 | val = @bs.send(f.name) 345 | assert_equal(f.format % val, f.inspect_in_object(@bs, {})) 346 | end 347 | end 348 | 349 | def test_yaml 350 | assert_equal(@bs_1, YAML.load(@bs_1.to_yaml)) 351 | end 352 | 353 | def test_field_by_name 354 | name = :f007 355 | f = @bs.field_by_name(name) 356 | assert(f) 357 | assert_equal(f.name, name) 358 | end 359 | 360 | #-------- 361 | def repeat_access_test(bs, n) 362 | last_set_value = {} 363 | 364 | start_length = bs.length 365 | 366 | n.times do 367 | bs.fields.each do |field| 368 | last_set_value[field] = randomize_field(bs, field) 369 | 370 | bs.fields.each do |f2| 371 | lsv2 = last_set_value[f2] 372 | if lsv2 373 | case f2 374 | when BitStruct::FloatField 375 | assert_in_delta(lsv2, bs.send(f2.name), 100) 376 | else 377 | begin 378 | assert_equal(lsv2, bs.send(f2.name)) 379 | rescue Test::Unit::AssertionFailedError => ex 380 | msg = 381 | "In #{f2.inspect} after setting #{field.inspect} to" + 382 | " #{last_set_value[field].inspect}" 383 | raise ex, msg + "\n" + ex.message, ex.backtrace 384 | end 385 | end 386 | end 387 | end 388 | end 389 | end 390 | 391 | finish_length = bs.length 392 | 393 | assert_equal(start_length, finish_length, "Length differs after test!") 394 | end 395 | 396 | def randomize(bs) 397 | bs.fields.each do |f| 398 | randomize_field(bs, f) 399 | end 400 | end 401 | 402 | def randomize_field(bs, field) 403 | case field 404 | when BitStruct::SignedField 405 | divisor = field.options[:fixed] 406 | if divisor 407 | value = (rand(2**field.size) - 2**(field.size-1))/divisor.to_f 408 | else 409 | value = rand(2**field.size) - 2**(field.size-1) 410 | end 411 | bs.send "#{field.name}=", value 412 | last_set_value = value 413 | 414 | when BitStruct::UnsignedField 415 | divisor = field.options[:fixed] 416 | if divisor 417 | value = rand(2**field.size)/divisor.to_f 418 | else 419 | value = rand(2**field.size) 420 | ## should ensure that there are some very low and very high walues 421 | ## esp. 0, 1, 2**n - 1, and so on 422 | end 423 | bs.send "#{field.name}=", value 424 | last_set_value = value 425 | 426 | when BitStruct::HexOctetField 427 | val = (1..field.length/8).map {"%02x" % rand(256)}.join(":") 428 | bs.send "#{field.name}=", val 429 | last_set_value = val 430 | 431 | when BitStruct::OctetField 432 | val = (1..field.length/8).map {"%d" % rand(256)}.join(".") 433 | bs.send "#{field.name}=", val 434 | last_set_value = val 435 | 436 | when BitStruct::CharField 437 | s = (rand(64)+32).chr 438 | value = s * (field.length/8) 439 | bs.send "#{field.name}=", value 440 | last_set_value = value 441 | 442 | when BitStruct::TextField 443 | s = (rand(64)+32).chr 444 | value = s * rand(field.length*2/8) 445 | bs.send "#{field.name}=", value 446 | last_set_value = s * [field.length/8, value.length].min 447 | 448 | when BitStruct::FloatField 449 | value = rand(2**30) 450 | bs.send "#{field.name}=", value 451 | last_set_value = value 452 | 453 | else raise 454 | end 455 | 456 | return last_set_value 457 | end 458 | end 459 | --------------------------------------------------------------------------------