bit-struct 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/History.txt +102 -0
- data/README.txt +189 -0
- data/Rakefile +34 -0
- data/TODO +23 -0
- data/TODO-ALSO +71 -0
- data/examples/ara-player-data.rb +82 -0
- data/examples/bignum.rb +18 -0
- data/examples/bits.rb +19 -0
- data/examples/byte-bdy.rb +30 -0
- data/examples/field-ripper.rb +22 -0
- data/examples/fixed-point.rb +17 -0
- data/examples/ip.rb +81 -0
- data/examples/longlong.rb +30 -0
- data/examples/md.rb +23 -0
- data/examples/modular-def.rb +38 -0
- data/examples/native.rb +31 -0
- data/examples/nested.rb +33 -0
- data/examples/pad.rb +14 -0
- data/examples/ping-recv.rb +25 -0
- data/examples/ping.rb +73 -0
- data/examples/player-data.rb +75 -0
- data/examples/raw.rb +62 -0
- data/examples/rest.rb +30 -0
- data/examples/switch-endian.rb +49 -0
- data/examples/vector.rb +98 -0
- data/lib/bit-struct.rb +6 -0
- data/lib/bit-struct/bit-struct.rb +549 -0
- data/lib/bit-struct/char-field.rb +48 -0
- data/lib/bit-struct/fields.rb +273 -0
- data/lib/bit-struct/float-field.rb +61 -0
- data/lib/bit-struct/hex-octet-field.rb +20 -0
- data/lib/bit-struct/nested-field.rb +76 -0
- data/lib/bit-struct/octet-field.rb +45 -0
- data/lib/bit-struct/pad-field.rb +15 -0
- data/lib/bit-struct/signed-field.rb +258 -0
- data/lib/bit-struct/text-field.rb +44 -0
- data/lib/bit-struct/unsigned-field.rb +248 -0
- data/lib/bit-struct/vector-field.rb +77 -0
- data/lib/bit-struct/vector.rb +173 -0
- data/lib/bit-struct/yaml.rb +69 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- data/test/test-endian.rb +39 -0
- data/test/test-vector.rb +38 -0
- data/test/test.rb +433 -0
- metadata +126 -0
@@ -0,0 +1,75 @@
|
|
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
|
+
#<PlayerData pid=400, x_position=1, y_position=2, z_position=3, foobar=42>
|
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
|
data/examples/raw.rb
ADDED
@@ -0,0 +1,62 @@
|
|
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
|
data/examples/rest.rb
ADDED
@@ -0,0 +1,30 @@
|
|
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
|
@@ -0,0 +1,49 @@
|
|
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
|
+
[#<LittleEndian x=256>, "\000\001"]
|
49
|
+
[#<BigEndian x=256>, "\001\000"]
|
data/examples/vector.rb
ADDED
@@ -0,0 +1,98 @@
|
|
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", :length => 5
|
78
|
+
|
79
|
+
# equivalently, using an anonymous subclass of BitStruct::Vector
|
80
|
+
vector :v2, "a vector 2", :length => 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)
|
data/lib/bit-struct.rb
ADDED
@@ -0,0 +1,549 @@
|
|
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.13.1"
|
13
|
+
|
14
|
+
class Field
|
15
|
+
# Offset of field in bits.
|
16
|
+
attr_reader :offset
|
17
|
+
|
18
|
+
# Length of field in bits.
|
19
|
+
attr_reader :length
|
20
|
+
alias size length
|
21
|
+
|
22
|
+
# Name of field (used for its accessors).
|
23
|
+
attr_reader :name
|
24
|
+
|
25
|
+
# Options, such as :default (varies for each field subclass).
|
26
|
+
# In general, options can be provided as strings or as symbols.
|
27
|
+
attr_reader :options
|
28
|
+
|
29
|
+
# Display name of field (used for printing).
|
30
|
+
attr_reader :display_name
|
31
|
+
|
32
|
+
# Default value.
|
33
|
+
attr_reader :default
|
34
|
+
|
35
|
+
# Format for printed value of field.
|
36
|
+
attr_reader :format
|
37
|
+
|
38
|
+
# Subclasses can override this to define a default for all fields of this
|
39
|
+
# class, not just the one currently being added to a BitStruct class, a
|
40
|
+
# "default default" if you will. The global default, if #default returns
|
41
|
+
# nil, is to fill the field with zero. Most field classes just let this
|
42
|
+
# default stand. The default can be overridden per-field when a BitStruct
|
43
|
+
# class is defined.
|
44
|
+
def self.default; nil; end
|
45
|
+
|
46
|
+
# Used in describe.
|
47
|
+
def self.class_name
|
48
|
+
@class_name ||= name[/\w+$/]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Used in describe. Can be overridden per-subclass, as in NestedField.
|
52
|
+
def class_name
|
53
|
+
self.class.class_name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Yield the description of this field, as an array of 5 strings: byte
|
57
|
+
# offset, type, name, size, and description. The opts hash may have:
|
58
|
+
#
|
59
|
+
# :expand :: if the value is true, expand complex fields
|
60
|
+
#
|
61
|
+
# (Subclass implementations may yield more than once for complex fields.)
|
62
|
+
#
|
63
|
+
def describe opts
|
64
|
+
bits = size
|
65
|
+
if bits > 32 and bits % 8 == 0
|
66
|
+
len_str = "%dB" % (bits/8)
|
67
|
+
else
|
68
|
+
len_str = "%db" % bits
|
69
|
+
end
|
70
|
+
|
71
|
+
byte_offset = offset / 8 + (opts[:byte_offset] || 0)
|
72
|
+
|
73
|
+
yield ["@%d" % byte_offset, class_name, name, len_str, display_name]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Options are _display_name_, _default_, and _format_ (subclasses of Field
|
77
|
+
# may add other options).
|
78
|
+
def initialize(offset, length, name, opts = {})
|
79
|
+
@offset, @length, @name, @options =
|
80
|
+
offset, length, name, opts
|
81
|
+
|
82
|
+
@display_name = opts[:display_name] || opts["display_name"]
|
83
|
+
@default = opts[:default] || opts["default"] || self.class.default
|
84
|
+
@format = opts[:format] || opts["format"]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Inspect the value of this field in the specified _obj_.
|
88
|
+
def inspect_in_object(obj, opts)
|
89
|
+
val = obj.send(name)
|
90
|
+
str =
|
91
|
+
begin
|
92
|
+
val.inspect(opts)
|
93
|
+
rescue ArgumentError # assume: "wrong number of arguments (1 for 0)"
|
94
|
+
val.inspect
|
95
|
+
end
|
96
|
+
(f=@format) ? (f % str) : str
|
97
|
+
end
|
98
|
+
|
99
|
+
# Normally, all fields show up in inspect, but some, such as padding,
|
100
|
+
# should not.
|
101
|
+
def inspectable?; true; end
|
102
|
+
end
|
103
|
+
|
104
|
+
NULL_FIELD = Field.new(0, 0, :null, :display_name => "null field")
|
105
|
+
|
106
|
+
# Raised when a field is added after an instance has been created. Fields
|
107
|
+
# cannot be added after this point.
|
108
|
+
class ClosedClassError < StandardError; end
|
109
|
+
|
110
|
+
# Raised if the chosen field name is not allowed, either because another
|
111
|
+
# field by that name exists, or because a method by that name exists.
|
112
|
+
class FieldNameError < StandardError; end
|
113
|
+
|
114
|
+
@default_options = {}
|
115
|
+
|
116
|
+
@initial_value = nil
|
117
|
+
@closed = nil
|
118
|
+
@rest_field = nil
|
119
|
+
@note = nil
|
120
|
+
|
121
|
+
class << self
|
122
|
+
def inherited cl
|
123
|
+
cl.instance_eval do
|
124
|
+
@initial_value = nil
|
125
|
+
@closed = nil
|
126
|
+
@rest_field = nil
|
127
|
+
@note = nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# ------------------------
|
132
|
+
# :section: field access methods
|
133
|
+
#
|
134
|
+
# For introspection and metaprogramming.
|
135
|
+
#
|
136
|
+
# ------------------------
|
137
|
+
|
138
|
+
# Return the list of fields for this class.
|
139
|
+
def fields
|
140
|
+
@fields ||= self == BitStruct ? [] : superclass.fields.dup
|
141
|
+
end
|
142
|
+
|
143
|
+
# Return the list of fields defined by this class, not inherited
|
144
|
+
# from the superclass.
|
145
|
+
def own_fields
|
146
|
+
@own_fields ||= []
|
147
|
+
end
|
148
|
+
|
149
|
+
# Add a field to the BitStruct (usually, this is only used internally).
|
150
|
+
def add_field(name, length, opts = {})
|
151
|
+
round_byte_length ## just to make sure this has been calculated
|
152
|
+
## before adding anything
|
153
|
+
|
154
|
+
name = name.to_sym
|
155
|
+
|
156
|
+
if @closed
|
157
|
+
raise ClosedClassError, "Cannot add field #{name}: " +
|
158
|
+
"The definition of the #{self.inspect} BitStruct class is closed."
|
159
|
+
end
|
160
|
+
|
161
|
+
if fields.find {|f|f.name == name}
|
162
|
+
raise FieldNameError, "Field #{name} is already defined as a field."
|
163
|
+
end
|
164
|
+
|
165
|
+
if instance_methods(true).find {|m| m == name}
|
166
|
+
if opts[:allow_method_conflict] || opts["allow_method_conflict"]
|
167
|
+
warn "Field #{name} is already defined as a method."
|
168
|
+
else
|
169
|
+
raise FieldNameError,"Field #{name} is already defined as a method."
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
field_class = opts[:field_class]
|
174
|
+
|
175
|
+
prev = fields[-1] || NULL_FIELD
|
176
|
+
offset = prev.offset + prev.length
|
177
|
+
field = field_class.new(offset, length, name, opts)
|
178
|
+
field.add_accessors_to(self)
|
179
|
+
fields << field
|
180
|
+
own_fields << field
|
181
|
+
@bit_length += field.length
|
182
|
+
@round_byte_length = (bit_length/8.0).ceil
|
183
|
+
|
184
|
+
if @initial_value
|
185
|
+
diff = @round_byte_length - @initial_value.length
|
186
|
+
if diff > 0
|
187
|
+
@initial_value << "\0" * diff
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
field
|
192
|
+
end
|
193
|
+
|
194
|
+
def parse_options(ary, default_name, default_field_class) # :nodoc:
|
195
|
+
opts = ary.grep(Hash).first || {}
|
196
|
+
opts = default_options.merge(opts)
|
197
|
+
|
198
|
+
opts[:display_name] = ary.grep(String).first || default_name
|
199
|
+
opts[:field_class] = ary.grep(Class).first || default_field_class
|
200
|
+
|
201
|
+
opts
|
202
|
+
end
|
203
|
+
|
204
|
+
# Get or set the hash of default options for the class, which apply to all
|
205
|
+
# fields. Changes take effect immediately, so can be used alternatingly with
|
206
|
+
# blocks of field declarations. If +h+ is provided, update the default
|
207
|
+
# options with that hash. Default options are inherited.
|
208
|
+
#
|
209
|
+
# This is especially useful with the <tt>:endian => val</tt> option.
|
210
|
+
def default_options h = nil
|
211
|
+
@default_options ||= superclass.default_options.dup
|
212
|
+
if h
|
213
|
+
@default_options.merge! h
|
214
|
+
end
|
215
|
+
@default_options
|
216
|
+
end
|
217
|
+
|
218
|
+
# Length, in bits, of this object.
|
219
|
+
def bit_length
|
220
|
+
@bit_length ||= fields.inject(0) {|a, f| a + f.length}
|
221
|
+
end
|
222
|
+
|
223
|
+
# Length, in bytes (rounded up), of this object.
|
224
|
+
def round_byte_length
|
225
|
+
@round_byte_length ||= (bit_length/8.0).ceil
|
226
|
+
end
|
227
|
+
|
228
|
+
def closed! # :nodoc:
|
229
|
+
@closed = true
|
230
|
+
end
|
231
|
+
|
232
|
+
def field_by_name name
|
233
|
+
@field_by_name ||= {}
|
234
|
+
field = @field_by_name[name]
|
235
|
+
unless field
|
236
|
+
field = fields.find {|f| f.name == name}
|
237
|
+
@field_by_name[name] = field if field
|
238
|
+
end
|
239
|
+
field
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Return the list of fields for this class.
|
244
|
+
def fields
|
245
|
+
self.class.fields
|
246
|
+
end
|
247
|
+
|
248
|
+
# Return the field with the given name.
|
249
|
+
def field_by_name name
|
250
|
+
self.class.field_by_name name
|
251
|
+
end
|
252
|
+
|
253
|
+
# ------------------------
|
254
|
+
# :section: metadata inspection methods
|
255
|
+
#
|
256
|
+
# Methods to textually describe the format of a BitStruct subclass.
|
257
|
+
#
|
258
|
+
# ------------------------
|
259
|
+
|
260
|
+
class << self
|
261
|
+
# Default format for describe. Fields are byte, type, name, size,
|
262
|
+
# and description.
|
263
|
+
DESCRIBE_FORMAT = "%8s: %-12s %-14s[%4s] %s"
|
264
|
+
|
265
|
+
# Can be overridden to use a different format.
|
266
|
+
def describe_format
|
267
|
+
DESCRIBE_FORMAT
|
268
|
+
end
|
269
|
+
|
270
|
+
# Textually describe the fields of this class of BitStructs.
|
271
|
+
# Returns a printable table (array of line strings), based on +fmt+,
|
272
|
+
# which defaults to #describe_format, which defaults to +DESCRIBE_FORMAT+.
|
273
|
+
def describe(fmt = nil, opts = {})
|
274
|
+
if fmt.kind_of? Hash
|
275
|
+
opts = fmt; fmt = nil
|
276
|
+
end
|
277
|
+
|
278
|
+
if block_given?
|
279
|
+
fields.each do |field|
|
280
|
+
field.describe(opts) do |desc|
|
281
|
+
yield desc
|
282
|
+
end
|
283
|
+
end
|
284
|
+
nil
|
285
|
+
|
286
|
+
else
|
287
|
+
fmt ||= describe_format
|
288
|
+
|
289
|
+
result = []
|
290
|
+
|
291
|
+
unless opts[:omit_header]
|
292
|
+
result << fmt % ["byte", "type", "name", "size", "description"]
|
293
|
+
result << "-"*70
|
294
|
+
end
|
295
|
+
|
296
|
+
fields.each do |field|
|
297
|
+
field.describe(opts) do |desc|
|
298
|
+
result << fmt % desc
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
unless opts[:omit_footer]
|
303
|
+
result << @note if @note
|
304
|
+
end
|
305
|
+
|
306
|
+
result
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Subclasses can use this to append a string (or several) to the #describe
|
311
|
+
# output. Notes are not cumulative with inheritance. When used with no
|
312
|
+
# arguments simply returns the note string
|
313
|
+
def note(*str)
|
314
|
+
@note = str unless str.empty?
|
315
|
+
@note
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# ------------------------
|
320
|
+
# :section: initialization and conversion methods
|
321
|
+
#
|
322
|
+
# ------------------------
|
323
|
+
|
324
|
+
# Initialize the string with the given string or bitstruct, or with a hash of
|
325
|
+
# field=>value pairs, or with the defaults for the BitStruct subclass, or
|
326
|
+
# with an IO or other object with a #read method. Fields can be strings or
|
327
|
+
# symbols. Finally, if a block is given, yield the instance for modification
|
328
|
+
# using accessors.
|
329
|
+
def initialize(value = nil) # :yields: instance
|
330
|
+
self << self.class.initial_value
|
331
|
+
|
332
|
+
case value
|
333
|
+
when Hash
|
334
|
+
value.each do |k, v|
|
335
|
+
send "#{k}=", v
|
336
|
+
end
|
337
|
+
|
338
|
+
when nil
|
339
|
+
|
340
|
+
else
|
341
|
+
if value.respond_to?(:read)
|
342
|
+
value = value.read(self.class.round_byte_length)
|
343
|
+
end
|
344
|
+
|
345
|
+
self[0, value.length] = value
|
346
|
+
end
|
347
|
+
|
348
|
+
self.class.closed!
|
349
|
+
yield self if block_given?
|
350
|
+
end
|
351
|
+
|
352
|
+
DEFAULT_TO_H_OPTS = {
|
353
|
+
:convert_keys => :to_sym,
|
354
|
+
:include_rest => true
|
355
|
+
}
|
356
|
+
|
357
|
+
# Returns a hash of {name=>value,...} for each field. By default, include
|
358
|
+
# the rest field.
|
359
|
+
# Keys are symbols derived from field names using +to_sym+, unless
|
360
|
+
# <tt>opts[:convert_keys]<\tt> is set to some other method name.
|
361
|
+
def to_h(opts = DEFAULT_TO_H_OPTS)
|
362
|
+
converter = opts[:convert_keys] || :to_sym
|
363
|
+
|
364
|
+
fields_for_to_h = fields
|
365
|
+
if opts[:include_rest] and (rest_field = self.class.rest_field)
|
366
|
+
fields_for_to_h += [rest_field]
|
367
|
+
end
|
368
|
+
|
369
|
+
fields_for_to_h.inject({}) do |h,f|
|
370
|
+
h[f.name.send(converter)] = send(f.name)
|
371
|
+
h
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Returns an array of values of the fields of the BitStruct. By default,
|
376
|
+
# include the rest field.
|
377
|
+
def to_a(include_rest = true)
|
378
|
+
ary =
|
379
|
+
fields.map do |f|
|
380
|
+
send(f.name)
|
381
|
+
end
|
382
|
+
|
383
|
+
if include_rest and (rest_field = self.class.rest_field)
|
384
|
+
ary << send(rest_field.name)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
class << self
|
389
|
+
# The unique "prototype" object from which new instances are copied.
|
390
|
+
# The fields of this instance can be modified in the class definition
|
391
|
+
# to set default values for the fields in that class. (Otherwise, defaults
|
392
|
+
# defined by the fields themselves are used.) A copy of this object is
|
393
|
+
# inherited in subclasses, which they may override using defaults and
|
394
|
+
# by writing to the initial_value object itself.
|
395
|
+
#
|
396
|
+
# If called with a block, yield the initial value object before returning
|
397
|
+
# it. Useful for customization within a class definition.
|
398
|
+
#
|
399
|
+
def initial_value # :yields: the initial value
|
400
|
+
unless @initial_value
|
401
|
+
iv = defined?(superclass.initial_value) ?
|
402
|
+
superclass.initial_value.dup : ""
|
403
|
+
if iv.length < round_byte_length
|
404
|
+
iv << "\0" * (round_byte_length - iv.length)
|
405
|
+
end
|
406
|
+
|
407
|
+
@initial_value = "" # Serves as initval while the real initval is inited
|
408
|
+
@initial_value = new(iv)
|
409
|
+
@closed = false # only creating the first _real_ instance closes.
|
410
|
+
|
411
|
+
fields.each do |field|
|
412
|
+
@initial_value.send("#{field.name}=", field.default) if field.default
|
413
|
+
end
|
414
|
+
end
|
415
|
+
yield @initial_value if block_given?
|
416
|
+
@initial_value
|
417
|
+
end
|
418
|
+
|
419
|
+
# Take +data+ (a string or BitStruct) and parse it into instances of
|
420
|
+
# the +classes+, returning them in an array. The classes can be given
|
421
|
+
# as an array or a separate arguments. (For parsing a string into a _single_
|
422
|
+
# BitStruct instance, just use the #new method with the string as an arg.)
|
423
|
+
def parse(data, *classes)
|
424
|
+
classes.flatten.map do |c|
|
425
|
+
c.new(data.slice!(0...c.round_byte_length))
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# Join the given structs (array or multiple args) as a string.
|
430
|
+
# Actually, the inherited String#+ instance method is the same, as is using
|
431
|
+
# Array#join.
|
432
|
+
def join(*structs)
|
433
|
+
structs.flatten.map {|struct| struct.to_s}.join("")
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
# ------------------------
|
438
|
+
# :section: inspection methods
|
439
|
+
#
|
440
|
+
# ------------------------
|
441
|
+
|
442
|
+
DEFAULT_INSPECT_OPTS = {
|
443
|
+
:format => "#<%s %s>",
|
444
|
+
:field_format => "%s=%s",
|
445
|
+
:separator => ", ",
|
446
|
+
:field_name_meth => :name,
|
447
|
+
:include_rest => true,
|
448
|
+
:brackets => ["[", "]"],
|
449
|
+
:include_class => true,
|
450
|
+
:simple_format => "<%s>"
|
451
|
+
}
|
452
|
+
|
453
|
+
DETAILED_INSPECT_OPTS = {
|
454
|
+
:format => "%s:\n%s",
|
455
|
+
:field_format => "%30s = %s",
|
456
|
+
:separator => "\n",
|
457
|
+
:field_name_meth => :display_name,
|
458
|
+
:include_rest => true,
|
459
|
+
:brackets => [nil, "\n"],
|
460
|
+
:include_class => true,
|
461
|
+
:simple_format => "\n%s"
|
462
|
+
}
|
463
|
+
|
464
|
+
# A standard inspect method which does not add newlines.
|
465
|
+
def inspect(opts = DEFAULT_INSPECT_OPTS)
|
466
|
+
field_format = opts[:field_format]
|
467
|
+
field_name_meth = opts[:field_name_meth]
|
468
|
+
|
469
|
+
fields_for_inspect = fields.select {|field| field.inspectable?}
|
470
|
+
if opts[:include_rest] and (rest_field = self.class.rest_field)
|
471
|
+
fields_for_inspect << rest_field
|
472
|
+
end
|
473
|
+
|
474
|
+
ary = fields_for_inspect.map do |field|
|
475
|
+
field_format %
|
476
|
+
[field.send(field_name_meth),
|
477
|
+
field.inspect_in_object(self, opts)]
|
478
|
+
end
|
479
|
+
|
480
|
+
body = ary.join(opts[:separator])
|
481
|
+
|
482
|
+
if opts[:include_class]
|
483
|
+
opts[:format] % [self.class, body]
|
484
|
+
else
|
485
|
+
opts[:simple_format] % body
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# A more visually appealing inspect method that puts each field/value on
|
490
|
+
# a separate line. Very useful when output is scrolling by on a screen.
|
491
|
+
#
|
492
|
+
# (This is actually a convenience method to call #inspect with the
|
493
|
+
# DETAILED_INSPECT_OPTS opts.)
|
494
|
+
def inspect_detailed
|
495
|
+
inspect(DETAILED_INSPECT_OPTS)
|
496
|
+
end
|
497
|
+
|
498
|
+
# ------------------------
|
499
|
+
# :section: field declaration methods
|
500
|
+
#
|
501
|
+
# ------------------------
|
502
|
+
|
503
|
+
# Define accessors for a variable length substring from the end of
|
504
|
+
# the defined fields to the end of the BitStruct. The _rest_ may behave as
|
505
|
+
# a String or as some other String or BitStruct subclass.
|
506
|
+
#
|
507
|
+
# This does not add a field, which is useful because a superclass can have
|
508
|
+
# a rest method which accesses subclass data. In particular, #rest does
|
509
|
+
# not affect the #round_byte_length class method. Of course, any data
|
510
|
+
# in rest does add to the #length of the BitStruct, calculated as a string.
|
511
|
+
# Also, _rest_ is not inherited.
|
512
|
+
#
|
513
|
+
# The +ary+ argument(s) work as follows:
|
514
|
+
#
|
515
|
+
# If a class is provided, use it for the Field class (String by default).
|
516
|
+
# If a string is provided, use it for the display_name (+name+ by default).
|
517
|
+
# If a hash is provided, use it for options.
|
518
|
+
#
|
519
|
+
# *Warning*: the rest reader method returns a copy of the field, so
|
520
|
+
# accessors on that returned value do not affect the original rest field.
|
521
|
+
#
|
522
|
+
def self.rest(name, *ary)
|
523
|
+
if @rest_field
|
524
|
+
raise ArgumentError, "Duplicate rest field: #{name.inspect}."
|
525
|
+
end
|
526
|
+
|
527
|
+
opts = parse_options(ary, name, String)
|
528
|
+
offset = round_byte_length
|
529
|
+
byte_range = offset..-1
|
530
|
+
class_eval do
|
531
|
+
field_class = opts[:field_class]
|
532
|
+
define_method name do ||
|
533
|
+
field_class.new(self[byte_range])
|
534
|
+
end
|
535
|
+
|
536
|
+
define_method "#{name}=" do |val|
|
537
|
+
self[byte_range] = val
|
538
|
+
end
|
539
|
+
|
540
|
+
@rest_field = Field.new(offset, -1, name, {
|
541
|
+
:display_name => opts[:display_name],
|
542
|
+
:rest_class => field_class
|
543
|
+
})
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
# Not included with the other fields, but accessible separately.
|
548
|
+
def self.rest_field; @rest_field; end
|
549
|
+
end
|