Skip to content

Commit 1bebc28

Browse files
Added documentation for new structure dsl
Also, updated name of binary and char string types. Now the default :string is not null padded and the :padding option is used to specify what type of padding is used.
1 parent d31379e commit 1bebc28

File tree

6 files changed

+104
-23
lines changed

6 files changed

+104
-23
lines changed

docs/machostructure-dsl.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Internal MachOStructure DSL
2+
## Documentation
3+
The MachOStructure class makes it easy to describe binary chunks by using the #field method. This method generates the byte size and format strings necessary to parse a chunk of binary data. It also automatically generates the constructor and readers for all fields as well.
4+
5+
The fields are created in order so you will be expected to pass those arguements to the constructor in the same order. Fields with no arguments should be defined last and fields with default arguments should be defined right before them.
6+
7+
The type and options of inherited fields can be changed but their argument position and the number of arguments (used to calculate min_args) will also not change.
8+
9+
Usually, endianness is handled by the Utils#specialize_format method but occasionally a field needs to specifiy that beforehand. That is what the :endian option is for. If not specified, a placeholder is used so that can be specified later.
10+
11+
## Syntax
12+
```ruby
13+
field [field name], [field type], [option1 => value1], [option2 => value2], ...
14+
```
15+
16+
## Example
17+
```ruby
18+
class AllFields < MachO::MachOStructure
19+
field :name1, :string, :size => 16
20+
field :name3, :int32
21+
field :name4, :uint32
22+
field :name5, :uint64
23+
field :name6, :view
24+
field :name7, :lcstr
25+
field :name8, :two_level_hints_table
26+
field :name9, :tool_entries
27+
end
28+
```
29+
30+
## Field Types
31+
- `:string` [requires `:size` option] [optional `:padding` option]
32+
- a string
33+
- `:int32 `
34+
- a signed 32 bit integer
35+
- `:uint32 `
36+
- an unsigned 32 bit integer
37+
- `:uint64 `
38+
- an unsigned 64 bit integer
39+
- `:view` [initalized]
40+
- an instance of the MachOView class (lib/macho/view.rb)
41+
- `:lcstr` [NOT initalized]
42+
- an instance of the LCStr class (lib/macho/load_commands.rb)
43+
- `:two_level_hints_table` [NOT initalized] [NO argument]
44+
- an instance of the TwoLevelHintsTable class (lib/macho/load_commands.rb)
45+
- `:tool_entries` [NOT initalized]
46+
- an instance of the ToolEntries class (lib/macho/load_commands.rb)
47+
48+
## Option Types
49+
- Exclusive (only one can be used at a time)
50+
- `:mask` [Integer] bitmask to be applied to field
51+
- `:unpack` [String] binary unpack string used for further unpacking of :string
52+
- `:default` [Value] default field value
53+
- Inclusive (can be used with other options)
54+
- `:to_s` [Boolean] generate `#to_s` method based on field
55+
- Used with Integer field types
56+
- `:endian` [Symbol] optionally specify `:big` or `:little` endian
57+
- Used with `:string` field type
58+
- `:size` [Integer] size in bytes
59+
- `:padding` [Symbol] optionally specify `:null` padding
60+
61+
## More Infomation
62+
Hop over to lib/macho/structure.rb to see the class itself.

lib/macho/headers.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -751,13 +751,13 @@ class PrelinkedKernelHeader < MachOStructure
751751
field :prelink_version, :uint32, :endian => :big
752752

753753
# @return [void]
754-
field :reserved, :binary, :size => 40, :unpack => "L>10"
754+
field :reserved, :string, :size => 40, :unpack => "L>10"
755755

756756
# @return [void]
757-
field :platform_name, :binary, :size => 64
757+
field :platform_name, :string, :size => 64
758758

759759
# @return [void]
760-
field :root_path, :binary, :size => 256
760+
field :root_path, :string, :size => 256
761761

762762
# @return [Boolean] whether this prelinked kernel supports KASLR
763763
def kaslr?

lib/macho/load_commands.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def initialize(endianness, alignment)
367367
# LC_UUID.
368368
class UUIDCommand < LoadCommand
369369
# @return [Array<Integer>] the UUID
370-
field :uuid, :binary, :size => 16, :unpack => "C16"
370+
field :uuid, :string, :size => 16, :unpack => "C16"
371371

372372
# @return [String] a string representation of the UUID
373373
def uuid_string
@@ -398,7 +398,7 @@ def to_h
398398
# the task's address space. Corresponds to LC_SEGMENT.
399399
class SegmentCommand < LoadCommand
400400
# @return [String] the name of the segment
401-
field :segname, :string, :size => 16, :to_s => true
401+
field :segname, :string, :padding => :null, :size => 16, :to_s => true
402402

403403
# @return [Integer] the memory address of the segment
404404
field :vmaddr, :uint32
@@ -1325,7 +1325,7 @@ def to_h
13251325
# Corresponds to LC_NOTE.
13261326
class NoteCommand < LoadCommand
13271327
# @return [String] the name of the owner for this note
1328-
field :data_owner, :string, :size => 16, :to_s => true
1328+
field :data_owner, :string, :padding => :null, :size => 16, :to_s => true
13291329

13301330
# @return [Integer] the offset, within the file, of the note
13311331
field :offset, :uint64

lib/macho/sections.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ module Sections
8989
# Represents a section of a segment for 32-bit architectures.
9090
class Section < MachOStructure
9191
# @return [String] the name of the section, including null pad bytes
92-
field :sectname, :string, :size => 16
92+
field :sectname, :string, :padding => :null, :size => 16
9393

9494
# @return [String] the name of the segment's section, including null
9595
# pad bytes
96-
field :segname, :string, :size => 16
96+
field :segname, :string, :padding => :null, :size => 16
9797

9898
# @return [Integer] the memory address of the section
9999
field :addr, :uint32

lib/macho/structure.rb

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
# frozen_string_literal: true
22

33
module MachO
4-
# A general purpose pseudo-structure.
4+
# A general purpose pseudo-structure. Described in detail in docs/machostructure-dsl.md.
55
# @abstract
66
class MachOStructure
77
# Constants used for parsing MachOStructure fields
88
module Fields
99
# 1. All fields with empty strings and zeros aren't used
1010
# to calculate the format and sizeof variables.
1111
# 2. All fields with nil should provide those values manually
12-
# via the :size and :fmt parameters.
12+
# via the :size parameter.
1313

1414
# association of field types to byte size
1515
# @api private
1616
BYTE_SIZE = {
1717
# Binary slices
18-
:binary => nil,
1918
:string => nil,
19+
:null_padded_string => nil,
2020
:int32 => 4,
2121
:uint32 => 4,
2222
:uint64 => 8,
@@ -36,8 +36,8 @@ module Fields
3636
# @api private
3737
FORMAT_CODE = {
3838
# Binary slices
39-
:binary => "a",
40-
:string => "Z",
39+
:string => "a",
40+
:null_padded_string => "Z",
4141
:int32 => "l=",
4242
:uint32 => "L=",
4343
:uint64 => "Q=",
@@ -51,12 +51,12 @@ module Fields
5151
# A list of classes that must get initialized
5252
# To add a new class append it here and add the init method to the def_class_reader method
5353
# @api private
54-
CLASS_LIST = %i[lcstr tool_entries two_level_hints_table].freeze
54+
CLASSES_TO_INIT = %i[lcstr tool_entries two_level_hints_table].freeze
5555

56-
# A list of fields that don't require arguments
56+
# A list of fields that don't require arguments in the initializer
5757
# Used to calculate MachOStructure#min_args
5858
# @api private
59-
NO_ARGS_LIST = %i[two_level_hints_table].freeze
59+
NO_ARG_REQUIRED = %i[two_level_hints_table].freeze
6060
end
6161

6262
# map of field names to indices
@@ -139,6 +139,7 @@ def inherited(subclass) # rubocop:disable Lint/MissingSuper
139139
# :default [Value] default value
140140
# :to_s [Boolean] flag for generating #to_s
141141
# :endian [Symbol] optionally specify :big or :little endian
142+
# :padding [Symbol] optionally specify :null padding
142143
# @api private
143144
def field(name, type, **options)
144145
raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type)
@@ -147,13 +148,16 @@ def field(name, type, **options)
147148
idx = if @field_idxs.key?(name)
148149
@field_idxs[name]
149150
else
150-
@min_args += 1 unless options.key?(:default) || Fields::NO_ARGS_LIST.include?(type)
151+
@min_args += 1 unless options.key?(:default) || Fields::NO_ARG_REQUIRED.include?(type)
151152
@field_idxs[name] = @field_idxs.size
152153
@size_list << nil
153154
@fmt_list << nil
154155
@field_idxs.size - 1
155156
end
156157

158+
# Update string type if padding is specified
159+
type = :null_padded_string if type == :string && options[:padding] == :null
160+
157161
# Add to size_list and fmt_list
158162
@size_list[idx] = Fields::BYTE_SIZE[type] || options[:size]
159163
@fmt_list[idx] = if options[:endian]
@@ -164,7 +168,7 @@ def field(name, type, **options)
164168
@fmt_list[idx] += options[:size].to_s if options.key?(:size)
165169

166170
# Generate methods
167-
if Fields::CLASS_LIST.include?(type)
171+
if Fields::CLASSES_TO_INIT.include?(type)
168172
def_class_reader(name, type, idx)
169173
elsif options.key?(:mask)
170174
def_mask_reader(name, idx, options[:mask])
@@ -184,7 +188,7 @@ def field(name, type, **options)
184188
#
185189

186190
# Generates a reader method for classes that need to be initialized.
187-
# These classes are defined in the Fields::CLASS_LIST array.
191+
# These classes are defined in the Fields::CLASSES_TO_INIT array.
188192
# @param name [Symbol] name of internal field
189193
# @param type [Symbol] type of field in terms of binary size
190194
# @param idx [Integer] the index of the field value in the @values array

test/test_structure_dsl.rb

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ class MachOStructureTest < Minitest::Test
77
# that information is reflected in the bytesize, min_args
88
# and format.
99
class AllFields < MachO::MachOStructure
10-
field :binary, :binary, :size => 16
11-
field :string, :string, :size => 32
10+
field :string, :string, :size => 16
11+
field :null_term_str, :string, :padding => :null, :size => 32
1212
field :int32, :int32
1313
field :uint32, :uint32
1414
field :uint64, :uint64
@@ -19,8 +19,8 @@ class AllFields < MachO::MachOStructure
1919
end
2020

2121
def test_all_field_types
22-
assert_includes AllFields.instance_methods, :binary
2322
assert_includes AllFields.instance_methods, :string
23+
assert_includes AllFields.instance_methods, :null_term_str
2424
assert_includes AllFields.instance_methods, :int32
2525
assert_includes AllFields.instance_methods, :uint32
2626
assert_includes AllFields.instance_methods, :uint64
@@ -66,7 +66,7 @@ def test_mask_option
6666
end
6767

6868
class UnpackCmd < MachO::MachOStructure
69-
field :unpack_field, :binary, :size => 8, :unpack => "L>2"
69+
field :unpack_field, :string, :size => 8, :unpack => "L>2"
7070
end
7171

7272
def test_unpack_option
@@ -106,4 +106,19 @@ class EndianCmd < MachO::MachOStructure
106106
def test_endian_option
107107
assert_equal EndianCmd.format, "L>L<"
108108
end
109+
110+
class PaddingCmd < MachO::MachOStructure
111+
field :str, :string, :size => 12
112+
field :null_term_str, :string, :padding => :null, :size => 12
113+
end
114+
115+
def test_padding_option
116+
assert_equal PaddingCmd.format, "a12Z12"
117+
assert_equal PaddingCmd.bytesize, 24
118+
119+
padded_str = "Hello\x00World!" * 2
120+
padding_cmd = PaddingCmd.new_from_bin(:big, padded_str)
121+
assert_equal padding_cmd.str, "Hello\x00World!"
122+
assert_equal padding_cmd.null_term_str, "Hello"
123+
end
109124
end

0 commit comments

Comments
 (0)