Class: Depix::Binary::Structure
- Inherits:
-
Object
- Object
- Depix::Binary::Structure
- Defined in:
- lib/depix/binary/structure.rb
Overview
A basic C structs library (only works by value). Here’s the basic mode of operation:
1) You define a struct, with a number of fields in it. This hould be a subclass of Dict within which you
create Field objects which are saved in a class variable
3) Each created Field instance knows how big it is and how to produce a pattern to get it's value from the byte stream
by using Ruby's "pack/unpack". Each field thus provides an unpack pattern, and patterns are ordered
into a stack, starting with the first unpack pattern
4) When you parse some bytes using the struct:
- An unpack pattern will be compiled from all of the fields composing the struct,
and it will be a single string. The string gets applied to the bytes passed to parse()
- An array of unpacked values returned by unpack is then passed to the struct's consumption engine,
which lets each field take as many items off the stack as it needs. A field might happily produce
4 items for unpacking and then take the same 4 items off the stack of parsed values. Or not.
- A new structure gets created and for every named field it defines an attr_accessor. When consuming,
the values returned by Field objects get set using the accessors (so accessors can be overridden too!)
5) When you save out the struct roughly the same happens but in reverse (readers are called per field,
then it's checked whether the data can be packed and fits into the alloted number of bytes, and then
one big array of values is composed and passed on to Array#pack)
For example
class OneIntegerAndOneFloat < Structure
uint32 :identifier, :description => "This is the important ID", :required => true
real :value, :description => "The value that we store"
end
ready_struct = OneIntegerAndOneFloat.new
ready_struct.identifier = 23 # Plain Ruby assignment
ready_struct.value = 45.0
binary_file.write(OneIntegerAndOneFloat.pack(ready_struct)) # dumps the packed struct with paddings
Direct Known Subclasses
CompactDPX, DPX, FileInfo, FilmInfo, ImageElement, ImageInfo, OrientationInfo, TelevisionInfo, UserInfo
Constant Summary collapse
- DEF_OPTS =
{ :req => false, :desc => nil }
Class Method Summary collapse
-
.apply!(string) ⇒ Object
Apply this structure to data in the string, returning an instance of this structure with fields completed.
-
.apply_le!(string) ⇒ Object
Apply this structure to data in the string, returning an instance of this structure with fields completed assume little-endian fields.
-
.array(name, mapped_to, *extras) {|a.members| ... } ⇒ Object
Define an array of values.
-
.blanking(name, *extras) ⇒ Object
Define a blanking field (it’s return value is always nil).
-
.byteify_string(string) ⇒ Object
Only relevant for 1.9.
-
.char(name, *extras) ⇒ Object
Define a char field.
-
.const_missing(c) ⇒ Object
Allows us to use field names from Fields module.
-
.consume!(stack_of_unpacked_values) ⇒ Object
Consume a stack of unpacked values, letting each field decide how many to consume.
-
.fields ⇒ Object
Get the array of fields defined in this struct.
-
.filler ⇒ Object
Get an opaque struct based on this one, that will consume exactly as many bytes as this structure would occupy, but discard them instead.
-
.inner(name, mapped_to, *extras) ⇒ Object
Define a nested struct.
-
.length ⇒ Object
How many bytes are needed to complete this structure.
-
.only(*field_names) ⇒ Object
Get a class that would parse just the same, preserving only the fields passed in the array.
-
.pack(instance, buffer = nil) ⇒ Object
Pack the instance of this struct.
-
.pattern ⇒ Object
Get the pattern that will be used to unpack this structure and all of it’s descendants.
-
.pattern_le ⇒ Object
Get the pattern that will be used to unpack this structure and all of it’s descendants from a buffer with pieces in little-endian byte order.
-
.r32(name, *extras) ⇒ Object
Define a real number.
-
.u16(name, *extras) ⇒ Object
Define a double-width unsigned integer.
-
.u32(name, *extras) ⇒ Object
Define a 4-byte unsigned integer.
-
.u8(name, *extras) ⇒ Object
Define a small unsigned integer.
-
.validate!(instance) ⇒ Object
Validate a passed instance.
Instance Method Summary collapse
Class Method Details
.apply!(string) ⇒ Object
Apply this structure to data in the string, returning an instance of this structure with fields completed
143 144 145 |
# File 'lib/depix/binary/structure.rb', line 143 def self.apply!(string) consume!(string.unpack(pattern)) end |
.apply_le!(string) ⇒ Object
Apply this structure to data in the string, returning an instance of this structure with fields completed assume little-endian fields
149 150 151 |
# File 'lib/depix/binary/structure.rb', line 149 def self.apply_le!(string) consume!(string.unpack(pattern_le)) end |
.array(name, mapped_to, *extras) {|a.members| ... } ⇒ Object
Define an array of values
89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/depix/binary/structure.rb', line 89 def self.array(name, mapped_to, *extras) count, opts = count_and_opts_from(extras) attr_accessor name a = ArrayField.new({:name => name}.merge(opts)) a.members = if mapped_to.is_a?(Class) # Array of structs [InnerField.new(:cast => mapped_to)] * count else c = Depix::Binary::Fields.const_get("#{mapped_to.to_s.upcase}Field") [c.new] * count end yield a.members if block_given? fields << a end |
.blanking(name, *extras) ⇒ Object
Define a blanking field (it’s return value is always nil)
68 69 70 71 72 |
# File 'lib/depix/binary/structure.rb', line 68 def self.blanking(name, *extras) length, opts = count_and_opts_from(extras) attr_accessor name fields << BlankingField.new( {:name => name, :length => length}.merge(opts) ) end |
.byteify_string(string) ⇒ Object
Only relevant for 1.9
183 184 185 |
# File 'lib/depix/binary/structure.rb', line 183 def self.byteify_string(string) string.force_encoding("ASCII-8BIT") end |
.char(name, *extras) ⇒ Object
Define a char field
111 112 113 114 115 |
# File 'lib/depix/binary/structure.rb', line 111 def self.char(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << CharField.new( {:name => name, :length => count}.merge(opts) ) end |
.const_missing(c) ⇒ Object
Allows us to use field names from Fields module
37 38 39 |
# File 'lib/depix/binary/structure.rb', line 37 def self.const_missing(c) Depix::Binary::Fields.const_get(c) end |
.consume!(stack_of_unpacked_values) ⇒ Object
Consume a stack of unpacked values, letting each field decide how many to consume
134 135 136 137 138 139 140 |
# File 'lib/depix/binary/structure.rb', line 134 def self.consume!(stack_of_unpacked_values) new_item = new @fields.each do | field | new_item.send("#{field.name}=", field.consume!(stack_of_unpacked_values)) unless field.name.nil? end new_item end |
.fields ⇒ Object
Get the array of fields defined in this struct
42 43 44 |
# File 'lib/depix/binary/structure.rb', line 42 def self.fields @fields ||= [] end |
.filler ⇒ Object
Get an opaque struct based on this one, that will consume exactly as many bytes as this structure would occupy, but discard them instead
178 179 180 |
# File 'lib/depix/binary/structure.rb', line 178 def self.filler only([]) end |
.inner(name, mapped_to, *extras) ⇒ Object
Define a nested struct
104 105 106 107 108 |
# File 'lib/depix/binary/structure.rb', line 104 def self.inner(name, mapped_to, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << InnerField.new({:name => name, :cast => mapped_to}.merge(opts)) end |
.length ⇒ Object
How many bytes are needed to complete this structure
129 130 131 |
# File 'lib/depix/binary/structure.rb', line 129 def self.length fields.inject(0){|_, s| _ + s.length.to_i } end |
.only(*field_names) ⇒ Object
Get a class that would parse just the same, preserving only the fields passed in the array. This speeds up parsing because we only extract and conform the fields that we need
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/depix/binary/structure.rb', line 155 def self.only(*field_names) distillate = fields.inject([]) do | m, f | if field_names.include?(f.name) # preserve m.push(f) else # create filler unless m[-1].is_a?(Filler) m.push(Filler.new(:length => f.length)) else m[-1].length += f.length end m end end anon = Class.new(self) anon.fields.replace(distillate) only_items = distillate.map{|n| n.name } anon end |
.pack(instance, buffer = nil) ⇒ Object
Pack the instance of this struct
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/depix/binary/structure.rb', line 188 def self.pack(instance, buffer = nil) # Preallocate a buffer just as big as me since we want everything to remain at fixed offsets buffer ||= (0xFF.chr * length) # We need to enforce ASCII-8bit encoding which in Ruby parlance is actually "bytestream" byteify_string(buffer) unless RUBY_VERSION < '1.9.0' # If the instance is nil return pure padding return buffer if instance.nil? # Now for the important stuff. For each field that we have, replace a piece at offsets in the buffer # with the packed results, skipping fillers fields.each_with_index do | f, i | # Skip blanking, we just dont touch it. TODO - test! next if f.is_a?(Filler) # Where should we put that value? offset = fields[0...i].inject(0){|_, s| _ + s.length } val = instance.send(f.name) # Validate the passed value using the format the field supports f.validate!(val) packed = f.pack(val) # Signal offset violation raise "Improper length for #{f.name} - packed #{packed.length} bytes but #{f.length} is required to fill the slot" if packed.length != f.length # See above, byt we need to do this with the packed string as well byteify_string(packed) unless RUBY_VERSION < '1.9.0' buffer[offset...(offset+f.length)] = packed end raise "Resulting buffer not the same length, expected #{length} bytes but compued #{buffer.length}" if buffer.length != length buffer end |
.pattern ⇒ Object
Get the pattern that will be used to unpack this structure and all of it’s descendants
118 119 120 |
# File 'lib/depix/binary/structure.rb', line 118 def self.pattern fields.map{|f| f.pattern }.join end |
.pattern_le ⇒ Object
Get the pattern that will be used to unpack this structure and all of it’s descendants from a buffer with pieces in little-endian byte order
124 125 126 |
# File 'lib/depix/binary/structure.rb', line 124 def self.pattern_le pattern.tr("gN", "eV") end |
.r32(name, *extras) ⇒ Object
Define a real number
82 83 84 85 86 |
# File 'lib/depix/binary/structure.rb', line 82 def self.r32(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << R32Field.new( {:name => name}.merge(opts) ) end |
.u16(name, *extras) ⇒ Object
Define a double-width unsigned integer
61 62 63 64 65 |
# File 'lib/depix/binary/structure.rb', line 61 def self.u16(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << U16Field.new( {:name => name }.merge(opts) ) end |
.u32(name, *extras) ⇒ Object
Define a 4-byte unsigned integer
54 55 56 57 58 |
# File 'lib/depix/binary/structure.rb', line 54 def self.u32(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << U32Field.new( {:name => name }.merge(opts) ) end |
.u8(name, *extras) ⇒ Object
Define a small unsigned integer
75 76 77 78 79 |
# File 'lib/depix/binary/structure.rb', line 75 def self.u8(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << U8Field.new( {:name => name }.merge(opts) ) end |
.validate!(instance) ⇒ Object
Validate a passed instance
47 48 49 50 51 |
# File 'lib/depix/binary/structure.rb', line 47 def self.validate!(instance) fields.each do | f | f.validate!(instance.send(f.name)) if f.name end end |
Instance Method Details
#[](field) ⇒ Object
241 242 243 |
# File 'lib/depix/binary/structure.rb', line 241 def [](field) send(field) end |
#[]=(field, value) ⇒ Object
237 238 239 |
# File 'lib/depix/binary/structure.rb', line 237 def []=(field, value) send("#{field}=", value) end |