ctypes 0.2.0
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.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +55 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/MAINTAINERS.md +3 -0
- data/README.md +390 -0
- data/Rakefile +10 -0
- data/SECURITY.md +57 -0
- data/ctypes.gemspec +40 -0
- data/lib/ctypes/array.rb +180 -0
- data/lib/ctypes/bitfield/builder.rb +246 -0
- data/lib/ctypes/bitfield.rb +278 -0
- data/lib/ctypes/bitmap.rb +154 -0
- data/lib/ctypes/enum/builder.rb +85 -0
- data/lib/ctypes/enum.rb +201 -0
- data/lib/ctypes/exporter.rb +50 -0
- data/lib/ctypes/helpers.rb +190 -0
- data/lib/ctypes/importers/castxml/loader.rb +150 -0
- data/lib/ctypes/importers/castxml.rb +59 -0
- data/lib/ctypes/importers.rb +7 -0
- data/lib/ctypes/int.rb +147 -0
- data/lib/ctypes/missing_bytes_error.rb +24 -0
- data/lib/ctypes/pad.rb +56 -0
- data/lib/ctypes/pretty_print_helpers.rb +31 -0
- data/lib/ctypes/string.rb +154 -0
- data/lib/ctypes/struct/builder.rb +242 -0
- data/lib/ctypes/struct.rb +529 -0
- data/lib/ctypes/terminated.rb +65 -0
- data/lib/ctypes/type.rb +195 -0
- data/lib/ctypes/union/builder.rb +220 -0
- data/lib/ctypes/union.rb +637 -0
- data/lib/ctypes/version.rb +8 -0
- data/lib/ctypes.rb +102 -0
- data/sig/ctypes.rbs +4 -0
- metadata +92 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
|
4
|
+
module CTypes
|
5
|
+
# helpers for use when pretty printing ctypes
|
6
|
+
# @api private
|
7
|
+
module PrettyPrintHelpers
|
8
|
+
refine PrettyPrint do
|
9
|
+
def ctype(type, endian = nil)
|
10
|
+
text "#{type} {"
|
11
|
+
group_sub do
|
12
|
+
nest(4) do
|
13
|
+
current_group.break
|
14
|
+
breakable
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
current_group.break
|
19
|
+
breakable
|
20
|
+
text "}"
|
21
|
+
text ".with_endian(%p)" % [endian] if endian
|
22
|
+
end
|
23
|
+
|
24
|
+
def line(buf)
|
25
|
+
text buf
|
26
|
+
current_group.break
|
27
|
+
breakable
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
5
|
+
# SPDX-License-Identifier: MIT
|
6
|
+
|
7
|
+
module CTypes
|
8
|
+
# Type used to unpack binary strings into Ruby {::String} instances
|
9
|
+
#
|
10
|
+
# @example greedy string
|
11
|
+
# t = CTypes::String.new
|
12
|
+
# t.unpack("hello world\0bye") # => "hello world"
|
13
|
+
#
|
14
|
+
# # greedy string will consume all bytes, but only return string up to the
|
15
|
+
# # first null-terminator
|
16
|
+
# t.unpack_one("hello world\0bye") # => ["hello world", ""]
|
17
|
+
#
|
18
|
+
# t.pack("test") # => "test"
|
19
|
+
# t.pack("test\0") # => "test\0"
|
20
|
+
#
|
21
|
+
# @example null-terminated string
|
22
|
+
# t = CTypes::String.terminated
|
23
|
+
# t.unpack("hello world\0bye") # => "hello world"
|
24
|
+
#
|
25
|
+
# # terminated string will consume bytes up to and including the
|
26
|
+
# # terminator
|
27
|
+
# t.unpack_one("hello world\0bye") # => ["hello world", "bye"]
|
28
|
+
#
|
29
|
+
# t.pack("test") # => "test\0"
|
30
|
+
#
|
31
|
+
# @example fixed-size string
|
32
|
+
# t = CTypes::String.new(size: 5)
|
33
|
+
# t.unpack("hello world\0bye") # => "hello"
|
34
|
+
# t.unpack_one("hello world\0bye") # => ["hello", " world\0bye"]
|
35
|
+
# t.pack("hi") # => "hi\0\0\0"
|
36
|
+
#
|
37
|
+
# @example fixed-size string, preserving null bytes
|
38
|
+
# t = CTypes::String.new(size: 8, trim: false)
|
39
|
+
# t.unpack("abc\0\0xyzXXXX") # => "abc\0\0xyz"
|
40
|
+
# t.pack("hello") # => "hello\0\0\0"
|
41
|
+
class String
|
42
|
+
include Type
|
43
|
+
|
44
|
+
# Return a {String} type that is terminated by the supplied sequence
|
45
|
+
# @param terminator [::String] byte sequence to terminate the string
|
46
|
+
#
|
47
|
+
# @example null-terminated string
|
48
|
+
# t = CTypes::String.terminated
|
49
|
+
# t.unpack("hello world\0bye") # => "hello world"
|
50
|
+
#
|
51
|
+
# @example string terminated string
|
52
|
+
# t = CTypes::String.terminated("STOP")
|
53
|
+
# t.unpack("test 1STOPtest 2STOP") # => "test 1"
|
54
|
+
def self.terminated(terminator = "\0")
|
55
|
+
size = terminator.size
|
56
|
+
Terminated.new(type: new,
|
57
|
+
locate: proc { |b, _| [b.index(terminator), size] },
|
58
|
+
terminate: terminator)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param size [Integer] number of bytes
|
62
|
+
# @param trim [Boolean] set to false to preserve null bytes when unpacking
|
63
|
+
def initialize(size: nil, trim: true)
|
64
|
+
@size = size
|
65
|
+
@trim = trim
|
66
|
+
@dry_type = Dry::Types["coercible.string"].default("")
|
67
|
+
@dry_type = @dry_type.constrained(max_size: size) if size.is_a?(Integer)
|
68
|
+
size ||= "*"
|
69
|
+
|
70
|
+
@fmt_pack = "a%s" % size
|
71
|
+
@fmt_unpack = (trim ? "Z%s" : "a%s") % size
|
72
|
+
end
|
73
|
+
attr_reader :trim
|
74
|
+
|
75
|
+
# pack a ruby String into a binary string, applying any required padding
|
76
|
+
#
|
77
|
+
# @param value [::String] string to pack
|
78
|
+
# @param endian [Symbol] endian to use when packing; ignored
|
79
|
+
# @param validate [Boolean] set to false to disable validation
|
80
|
+
# @return [::String] binary encoding for value
|
81
|
+
def pack(value, endian: default_endian, validate: true)
|
82
|
+
value = @dry_type[value] if validate
|
83
|
+
[value].pack(@fmt_pack)
|
84
|
+
end
|
85
|
+
|
86
|
+
# unpack a ruby String from binary string data
|
87
|
+
# @param buf [::String] bytes that make up the type
|
88
|
+
# @param endian [Symbol] endian of data within buf
|
89
|
+
# @return [::Array(::String, ::String)] unpacked string, and unused bytes
|
90
|
+
def unpack_one(buf, endian: default_endian)
|
91
|
+
raise missing_bytes_error(input: buf, need: @size) if
|
92
|
+
@size && buf.size < @size
|
93
|
+
value = buf.unpack1(@fmt_unpack)
|
94
|
+
[value, @size ? buf.byteslice(@size..) : ""]
|
95
|
+
end
|
96
|
+
|
97
|
+
# @api private
|
98
|
+
def greedy?
|
99
|
+
@size.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
# get the size in bytes of the string; returns 0 for greedy strings
|
103
|
+
def size
|
104
|
+
@size || 0
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_s
|
108
|
+
"string[#{size}]"
|
109
|
+
end
|
110
|
+
|
111
|
+
def pretty_print(q)
|
112
|
+
if size && size > 0
|
113
|
+
if trim
|
114
|
+
q.text("string(%d)" % @size)
|
115
|
+
else
|
116
|
+
q.text("string(%d, trim: false)" % @size)
|
117
|
+
end
|
118
|
+
else
|
119
|
+
q.text "string"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
123
|
+
|
124
|
+
# @api private
|
125
|
+
def export_type(q)
|
126
|
+
q << if size && size > 0
|
127
|
+
if trim
|
128
|
+
"string(%d)" % [@size]
|
129
|
+
else
|
130
|
+
"string(%d, trim: false)" % [@size]
|
131
|
+
end
|
132
|
+
else
|
133
|
+
"string"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# @api private
|
138
|
+
def type_name
|
139
|
+
@size ? "char[#{@size}]" : "char[]"
|
140
|
+
end
|
141
|
+
|
142
|
+
# This function is provided as a helper to {Helpers#string} to enable
|
143
|
+
# `string.terminated` as a type.
|
144
|
+
#
|
145
|
+
# @see String.terminated
|
146
|
+
def terminated(terminator = "\0")
|
147
|
+
String.terminated(terminator)
|
148
|
+
end
|
149
|
+
|
150
|
+
def ==(other)
|
151
|
+
other.is_a?(self.class) && other.size == size && other.trim == trim
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
module CTypes
|
7
|
+
# {Struct} layout builder
|
8
|
+
#
|
9
|
+
# This class is used to describe the memory layout of a {Struct} type.
|
10
|
+
# There are two approaches available for defining the layout, the
|
11
|
+
# declaritive approach used in ruby source files, or a programmatic
|
12
|
+
# approach that enables the construction of {Struct} types from data.
|
13
|
+
#
|
14
|
+
# @example declaritive approach using CTypes::Struct
|
15
|
+
# class MyStruct < CTypes::Struct
|
16
|
+
# layout do
|
17
|
+
# # the code in this block is evaluated within a Builder instance
|
18
|
+
#
|
19
|
+
# # add a struct name for use in pretty-printing
|
20
|
+
# name "struct my_struct"
|
21
|
+
#
|
22
|
+
# # set the endian if needed
|
23
|
+
# endian :big
|
24
|
+
#
|
25
|
+
# # add attributes
|
26
|
+
# attribute :id, uint32 # 32-bit unsigned int identifier
|
27
|
+
# attribute :name, string(256) # name that takes up to 256 bytes
|
28
|
+
#
|
29
|
+
# # add an array of four 64-bit unsigned integers
|
30
|
+
# attribute :values, array(uint64, 4)
|
31
|
+
#
|
32
|
+
# # Append a variable length member to the end of the structure.
|
33
|
+
# attribute :tail_len, uint32
|
34
|
+
# attribute :tail, string
|
35
|
+
# size { |struct| offsetof(:tail) + struct[:tail_len] }
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @example programmatic approach
|
40
|
+
# # include helpers for `uint32`, `string`, and `array` methods
|
41
|
+
# include CTypes::Helpers
|
42
|
+
#
|
43
|
+
# # data loaded from elsewhere
|
44
|
+
# fields = [
|
45
|
+
# {name: :id, type: uin32},
|
46
|
+
# {name: :name, type: string(256)},
|
47
|
+
# ]
|
48
|
+
#
|
49
|
+
# # create a builder instance
|
50
|
+
# b = CTypes::Struct.builder # => #<CTypes::Struct::Builder ...>
|
51
|
+
#
|
52
|
+
# # populate the fields in the builder
|
53
|
+
# fields.each do |field|
|
54
|
+
# b.attribute(field[:name], field[:type])
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# # build the Struct type
|
58
|
+
# t = b.build # => #<CTypes::Struct ...>
|
59
|
+
#
|
60
|
+
class Struct::Builder
|
61
|
+
include Helpers
|
62
|
+
|
63
|
+
def initialize(type_lookup: CTypes.type_lookup)
|
64
|
+
@type_lookup = type_lookup
|
65
|
+
@fields = []
|
66
|
+
@schema = []
|
67
|
+
@default = {}
|
68
|
+
@bytes = 0
|
69
|
+
end
|
70
|
+
|
71
|
+
# build a {Struct} instance with the layout configured in this builder
|
72
|
+
# @return [Struct] bitfield with the layout defined in this builder
|
73
|
+
def build
|
74
|
+
k = Class.new(Struct)
|
75
|
+
k.send(:apply_layout, self)
|
76
|
+
k
|
77
|
+
end
|
78
|
+
|
79
|
+
# get the layout description for internal use in {Struct}
|
80
|
+
# @api private
|
81
|
+
def result
|
82
|
+
dry_type = Dry::Types["coercible.hash"]
|
83
|
+
.schema(@schema)
|
84
|
+
.strict
|
85
|
+
.default(@default.freeze)
|
86
|
+
[@name, @fields.freeze, dry_type, @size || @bytes, @endian]
|
87
|
+
end
|
88
|
+
|
89
|
+
# set the name of this structure for use in pretty-printing
|
90
|
+
def name(value)
|
91
|
+
@name = value.dup.freeze
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# set the endian of this structure
|
96
|
+
def endian(value)
|
97
|
+
@endian = Endian[value]
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
# declare an attribute in the structure
|
102
|
+
# @param name name of the attribute, optional for unnamed fields
|
103
|
+
# @param type [CTypes::Type] type of the field
|
104
|
+
#
|
105
|
+
# This function supports the use of {Struct} and {Union} types for
|
106
|
+
# declaring unnamed fields (ISO C11). See example below for more details.
|
107
|
+
#
|
108
|
+
# @example
|
109
|
+
# attribute(:name, string)
|
110
|
+
#
|
111
|
+
# @example add an attribute with a struct type
|
112
|
+
# include CTypes::Helpers
|
113
|
+
# attribute(:header, struct(id: uint32, len: uint32))
|
114
|
+
#
|
115
|
+
# @example add an unnamed field (ISO C11)
|
116
|
+
# include CTypes::Helpers
|
117
|
+
#
|
118
|
+
# # declare the type to be used in the unnamed field
|
119
|
+
# header = struct(id: uint32, len: uint32)
|
120
|
+
#
|
121
|
+
# # create our struct type with an unnamed field
|
122
|
+
# t = struct do
|
123
|
+
# # add the unnamed field, in this case the header type
|
124
|
+
# attribute(header)
|
125
|
+
#
|
126
|
+
# # add any other field needed in the struct
|
127
|
+
# attribute(:body, string)
|
128
|
+
# size { |struct| struct[:len] }
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# # now unpack an instance of the struct type
|
132
|
+
# packet = t.unpack("\x01\0\0\0\x13\0\0\0hello worldXXX")
|
133
|
+
#
|
134
|
+
# # access the unnamed struct fields directly by the inner names
|
135
|
+
# p.id # => 1
|
136
|
+
# p.len # => 19
|
137
|
+
#
|
138
|
+
# # access the body
|
139
|
+
# p.body # => "hello world"
|
140
|
+
#
|
141
|
+
def attribute(name, type = nil)
|
142
|
+
# handle a named field
|
143
|
+
if type
|
144
|
+
name = name.to_sym
|
145
|
+
if @default.has_key?(name)
|
146
|
+
raise Error, "duplicate field name: %p" % name
|
147
|
+
end
|
148
|
+
|
149
|
+
@fields << [name, type]
|
150
|
+
@schema << Dry::Types::Schema::Key.new(type.dry_type, name)
|
151
|
+
@default[name] = type.default_value
|
152
|
+
|
153
|
+
# handle the unnamed field by adding the child fields to our type
|
154
|
+
else
|
155
|
+
type = name
|
156
|
+
dry_keys = type.dry_type.keys or
|
157
|
+
raise Error, "unsupported type for unnamed field: %p" % [type]
|
158
|
+
names = dry_keys.map(&:name)
|
159
|
+
|
160
|
+
if (duplicate = names.any? { |n| @default.has_key?(n) })
|
161
|
+
raise Error, "duplicate field name %p in unnamed field: %p" %
|
162
|
+
[duplicate, type]
|
163
|
+
end
|
164
|
+
|
165
|
+
@fields << [names, type]
|
166
|
+
@schema += dry_keys
|
167
|
+
@default.merge!(type.default_value)
|
168
|
+
end
|
169
|
+
|
170
|
+
# adjust the byte count for this type
|
171
|
+
if @bytes && type.fixed_size?
|
172
|
+
@bytes += type.size
|
173
|
+
else
|
174
|
+
@bytes = nil
|
175
|
+
end
|
176
|
+
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
# Add a proc for determining struct size based on decoded bytes
|
181
|
+
# @param block block will be called to determine struct size in bytes
|
182
|
+
#
|
183
|
+
# When unpacking variable length {Struct}s, we unpack each attribute in
|
184
|
+
# order of declaration until we encounter a field that has a
|
185
|
+
# variable-length type. At that point, we call the block provided to
|
186
|
+
# {Builder#size} do determine the total size of the struct. The block
|
187
|
+
# will receive a single argument, which is an incomplete unpacking of the
|
188
|
+
# {Struct}, containing only those fixed-length fields that have been
|
189
|
+
# unpacked so far. The block can access unpacked fields using
|
190
|
+
# {Struct#[]}. Using the unpacked fields, the block must return the total
|
191
|
+
# size of the struct in bytes.
|
192
|
+
#
|
193
|
+
# @example type-length-value (TLV) struct with variable length
|
194
|
+
# CTypes::Helpers.struct do
|
195
|
+
# attribute :type, enum(uint8, %i[hello read write bye])
|
196
|
+
# attribute :len, uint16.with_endian(:big)
|
197
|
+
# attribute :value, string
|
198
|
+
#
|
199
|
+
# # The :len field contains the length of :value. So we add a size
|
200
|
+
# # proc that takes the offset of :value within the struct (3 bytes),
|
201
|
+
# # and adds the value of the :len field. Note that
|
202
|
+
# size { |struct| offsetof(:value) + struct[:len] }
|
203
|
+
# end
|
204
|
+
def size(&block)
|
205
|
+
@size = block
|
206
|
+
self
|
207
|
+
end
|
208
|
+
|
209
|
+
# allocate unused bytes in the {Struct}
|
210
|
+
# @param bytes [Integer] number of bytes to pad
|
211
|
+
#
|
212
|
+
# This method is used to enforce alignment of other fields, or accurately
|
213
|
+
# mimic padding added in C structs by the compiler.
|
214
|
+
#
|
215
|
+
# @example
|
216
|
+
# CTypes::Helpers.struct do
|
217
|
+
# attribute :id, uint16 # 16-bit integer taking up two bytes
|
218
|
+
# pad 2 # pad two bytes (16-bits) to align value
|
219
|
+
# attribute :value, uint32 # value aligned at 4-byte boundary
|
220
|
+
# end
|
221
|
+
def pad(bytes)
|
222
|
+
pad = Pad.new(bytes)
|
223
|
+
|
224
|
+
# we use the Pad instance as the name of the field so Struct knows to
|
225
|
+
# treat the field as padding
|
226
|
+
@fields << [pad, pad]
|
227
|
+
@bytes += bytes if @bytes
|
228
|
+
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
# used for custom type resolution
|
233
|
+
# @see CTypes.using_type_lookup
|
234
|
+
def method_missing(name, *args, &block)
|
235
|
+
if @type_lookup && args.empty? && block.nil?
|
236
|
+
type = @type_lookup.call(name)
|
237
|
+
return type if type
|
238
|
+
end
|
239
|
+
super
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|