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
data/lib/ctypes/type.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
module CTypes
|
7
|
+
# interface for all supported types
|
8
|
+
module Type
|
9
|
+
# @api private
|
10
|
+
# Dry::Type used for constraint checking & defaults
|
11
|
+
attr_reader :dry_type
|
12
|
+
|
13
|
+
# endian to use when packing/unpacking.
|
14
|
+
# nil means {CTypes.default_endian} will be used.
|
15
|
+
# @see #with_endian #with_endian to create fixed-endian types
|
16
|
+
attr_reader :endian
|
17
|
+
|
18
|
+
# encode a ruby type into a String containing the binary representation of
|
19
|
+
# the c type
|
20
|
+
#
|
21
|
+
# @param value value to be encoded
|
22
|
+
# @param endian [Symbol] endian to pack with
|
23
|
+
# @param validate [Boolean] set to false to disable value validation
|
24
|
+
# @return [::String] binary encoding for value
|
25
|
+
# @see Int#pack
|
26
|
+
# @see Struct#pack
|
27
|
+
# @see Union#pack
|
28
|
+
# @see Array#pack
|
29
|
+
# @see String#pack
|
30
|
+
# @see Terminated#pack
|
31
|
+
def pack(value, endian: default_endian, validate: true)
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
# convert a String containing the binary represention of a c type into the
|
36
|
+
# equivalent ruby type
|
37
|
+
#
|
38
|
+
# @param buf [::String] bytes that make up the type
|
39
|
+
# @param endian [Symbol] endian of data within buf
|
40
|
+
# @return decoded type
|
41
|
+
#
|
42
|
+
# @see Type#unpack_one
|
43
|
+
# @see Int#unpack_one
|
44
|
+
# @see Struct#unpack_one
|
45
|
+
# @see Union#unpack_one
|
46
|
+
# @see Array#unpack_one
|
47
|
+
# @see String#unpack_one
|
48
|
+
# @see Terminated#unpack_one
|
49
|
+
def unpack(buf, endian: default_endian)
|
50
|
+
o, = unpack_one(buf, endian:)
|
51
|
+
o
|
52
|
+
end
|
53
|
+
|
54
|
+
# convert a String containing the binary represention of a c type into the
|
55
|
+
# equivalent ruby type
|
56
|
+
#
|
57
|
+
# @param buf [String] bytes that make up the type
|
58
|
+
# @param endian [Symbol] endian of data within buf
|
59
|
+
# @return [Array(Object, ::String)] decoded type, and remaining bytes
|
60
|
+
#
|
61
|
+
# @see Type#unpack
|
62
|
+
# @see Int#unpack_one
|
63
|
+
# @see Struct#unpack_one
|
64
|
+
# @see Union#unpack_one
|
65
|
+
# @see Array#unpack_one
|
66
|
+
# @see String#unpack_one
|
67
|
+
# @see Terminated#unpack_one
|
68
|
+
def unpack_one(buf, endian: default_endian)
|
69
|
+
raise NotImplementedError
|
70
|
+
end
|
71
|
+
|
72
|
+
# unpack as many instances of Type are present in the supplied string
|
73
|
+
#
|
74
|
+
# @param buf [String] bytes that make up the type
|
75
|
+
# @param endian [Symbol] endian of data within buf
|
76
|
+
# @return Array(Object) decoded types, and remaining
|
77
|
+
def unpack_all(buf, endian: default_endian)
|
78
|
+
out = []
|
79
|
+
until buf.empty?
|
80
|
+
t, buf = unpack_one(buf, endian:)
|
81
|
+
out << t
|
82
|
+
end
|
83
|
+
out
|
84
|
+
end
|
85
|
+
|
86
|
+
# read a fixed-sized type from an IO instance and unpack it
|
87
|
+
#
|
88
|
+
# @param buf [::String] bytes that make up the type
|
89
|
+
# @param endian [Symbol] endian of data within buf
|
90
|
+
# @return decoded type
|
91
|
+
def read(io, endian: default_endian)
|
92
|
+
unless fixed_size?
|
93
|
+
raise NotImplementedError,
|
94
|
+
"read() does not support variable-length types"
|
95
|
+
end
|
96
|
+
|
97
|
+
unpack(io.read(@size), endian: default_endian)
|
98
|
+
end
|
99
|
+
|
100
|
+
# read a fixed-sized type from an IO instance at a specific offset and
|
101
|
+
# unpack it
|
102
|
+
#
|
103
|
+
# @param buf [::String] bytes that make up the type
|
104
|
+
# @param pos [::Integer] seek position
|
105
|
+
# @param endian [Symbol] endian of data within buf
|
106
|
+
# @return decoded type
|
107
|
+
def pread(io, pos, endian: default_endian)
|
108
|
+
unless fixed_size?
|
109
|
+
raise NotImplementedError,
|
110
|
+
"pread() does not support variable-length types"
|
111
|
+
end
|
112
|
+
|
113
|
+
unpack(io.pread(@size, pos), endian: default_endian)
|
114
|
+
end
|
115
|
+
|
116
|
+
# get a fixed-endian instance of this type.
|
117
|
+
#
|
118
|
+
# If a type has a fixed endian, it will override the default endian set
|
119
|
+
# with {CTypes.default_endian=}.
|
120
|
+
#
|
121
|
+
# @param value [Symbol] endian; `:big` or `:little`
|
122
|
+
# @return [Type] fixed-endian {Type}
|
123
|
+
#
|
124
|
+
# @example uint32_t
|
125
|
+
# t = CTypes::UInt32
|
126
|
+
# t.pack(1) # => "\1\0\0\0"
|
127
|
+
# b = t.with_endian(:big)
|
128
|
+
# b.pack(1) # => "\0\0\0\1"
|
129
|
+
# l = t.with_endian(:little)
|
130
|
+
# l.pack(1) # => "\1\0\0\0"
|
131
|
+
#
|
132
|
+
# @example array
|
133
|
+
# include Ctype::Helpers
|
134
|
+
# t = array(uint32, 2)
|
135
|
+
# t.pack([1,2]) # => "\1\0\0\0\2\0\0\0"
|
136
|
+
# b = t.with_endian(:big)
|
137
|
+
# b.pack([1,2]) # => "\0\0\0\1\0\0\0\2"
|
138
|
+
# l = t.with_endian(:little)
|
139
|
+
# l.pack([1,2]) # => "\1\0\0\0\2\0\0\0"
|
140
|
+
#
|
141
|
+
# @example struct with mixed endian fields
|
142
|
+
# include Ctype::Helpers
|
143
|
+
# t = struct do
|
144
|
+
# attribute native: uint32
|
145
|
+
# attribute big: uint32.with_endian(:big)
|
146
|
+
# attribute little: uint32.with_endian(:little)
|
147
|
+
# end
|
148
|
+
# t.pack({native: 1, big: 2, little: 3}) # => "\1\0\0\0\0\0\0\2\3\0\0\0"
|
149
|
+
def with_endian(value)
|
150
|
+
return self if value == @endian
|
151
|
+
|
152
|
+
endian = Endian[value]
|
153
|
+
@with_endian ||= {}
|
154
|
+
@with_endian[endian] ||= begin
|
155
|
+
o = clone
|
156
|
+
o.instance_variable_set(:@without_endian, self) unless @endian
|
157
|
+
o.instance_variable_set(:@endian, endian)
|
158
|
+
o
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def without_endian
|
163
|
+
@without_endian ||= begin
|
164
|
+
o = clone
|
165
|
+
o.remove_instance_variable(:@endian)
|
166
|
+
o
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def greedy?
|
171
|
+
raise NotImplementedError, "Type must implement `.greedy?`: %p" % [self]
|
172
|
+
end
|
173
|
+
|
174
|
+
# check if this is a fixed-size type
|
175
|
+
def fixed_size?
|
176
|
+
!!@size&.is_a?(Integer)
|
177
|
+
end
|
178
|
+
|
179
|
+
# @api private
|
180
|
+
def default_value
|
181
|
+
dry_type[]
|
182
|
+
end
|
183
|
+
|
184
|
+
# @api private
|
185
|
+
def default_endian
|
186
|
+
@endian || CTypes.default_endian
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def missing_bytes_error(input:, need:)
|
192
|
+
MissingBytesError.new(type: self, input:, need:)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
module CTypes
|
7
|
+
# {Union} layout builder
|
8
|
+
#
|
9
|
+
# This class is used to describe the memory layout of a {Union} type. There
|
10
|
+
# are two approaches available for defining the layout, the declaritive
|
11
|
+
# approach used in ruby source files, or a programmatic approach that enables
|
12
|
+
# the construction of {Union} types from data.
|
13
|
+
#
|
14
|
+
# @example declaritive approach using CTypes::Union
|
15
|
+
# class MyUnion < CTypes::Union
|
16
|
+
# layout do
|
17
|
+
# # this message uses network-byte order
|
18
|
+
# endian :big
|
19
|
+
#
|
20
|
+
# # TLV message header with some fixed types
|
21
|
+
# header = struct(
|
22
|
+
# msg_type: enum(uint8, {invalid: 0, hello: 1, read: 2}),
|
23
|
+
# len: uint16)
|
24
|
+
#
|
25
|
+
# # add header as an unnamed field; adds MyUnion#type, MyUnion#len
|
26
|
+
# member header
|
27
|
+
#
|
28
|
+
# member :hello, struct(header:, version: string)
|
29
|
+
# member :read, struct(header:, offset: uint64, size: uint64)
|
30
|
+
# member :raw, string(trim: false)
|
31
|
+
#
|
32
|
+
# # dynamic size based on the len in header
|
33
|
+
# size { |union| header.size + union[:len] }
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @example programmatic approach building a union from data
|
38
|
+
# # include helpers for `uint32`, `string`, and `array` methods
|
39
|
+
# include CTypes::Helpers
|
40
|
+
#
|
41
|
+
# # data loaded from elsewhere
|
42
|
+
# fields = [
|
43
|
+
# {name: :id, type: uin32},
|
44
|
+
# {name: :name, type: string(256)},
|
45
|
+
# ]
|
46
|
+
#
|
47
|
+
# # create a builder instance
|
48
|
+
# b = CTypes::Union.builder # => #<CTypes::Union::Builder ...>
|
49
|
+
#
|
50
|
+
# # populate the fields in the builder
|
51
|
+
# fields.each do |field|
|
52
|
+
# b.member(field[:name], field[:type])
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# # build the Union type
|
56
|
+
# t = b.build # => #<CTypes::Union ...>
|
57
|
+
class Union::Builder
|
58
|
+
include Helpers
|
59
|
+
|
60
|
+
def initialize(type_lookup: CTypes.type_lookup)
|
61
|
+
@type_lookup = type_lookup
|
62
|
+
@fields = []
|
63
|
+
@field_names = Set.new
|
64
|
+
@schema = []
|
65
|
+
@size = 0
|
66
|
+
@fixed_size = true
|
67
|
+
end
|
68
|
+
|
69
|
+
# build a {Union} instance with the layout configured in this builder
|
70
|
+
# @return [Union] bitfield with the layout defined in this builder
|
71
|
+
def build
|
72
|
+
k = Class.new(Union)
|
73
|
+
k.send(:apply_layout, self)
|
74
|
+
k
|
75
|
+
end
|
76
|
+
|
77
|
+
# @api private
|
78
|
+
def result
|
79
|
+
dry_type = Dry::Types["coercible.hash"]
|
80
|
+
.schema(@schema)
|
81
|
+
.strict
|
82
|
+
.default(@default.freeze)
|
83
|
+
[@name, @fields.freeze, dry_type, @size, @fixed_size, @endian]
|
84
|
+
end
|
85
|
+
|
86
|
+
# set the name of this union for use in pretty-printing
|
87
|
+
def name(value)
|
88
|
+
@name = value.dup.freeze
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# set the endian of this union
|
93
|
+
def endian(value)
|
94
|
+
@endian = Endian[value]
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
# declare a member in the union
|
99
|
+
# @param name name of the member
|
100
|
+
# @param type [CTypes::Type] type of the field
|
101
|
+
#
|
102
|
+
# This function supports the use of {Struct} and {Union} types for
|
103
|
+
# declaring unnamed fields (ISO C11). See example below for more details.
|
104
|
+
#
|
105
|
+
# @example declare named union members
|
106
|
+
# member(:word, uint32)
|
107
|
+
# member(:bytes, array(uint8, 4))
|
108
|
+
# member(:half_words, array(uint16, 2))
|
109
|
+
# member(:header, struct(type: uint16, len: uint16))
|
110
|
+
#
|
111
|
+
# @example add an unnamed field (ISO C11)
|
112
|
+
# include CTypes::Helpers
|
113
|
+
#
|
114
|
+
# # declare the type to be used in the unnamed field
|
115
|
+
# header = struct(id: uint32, len: uint32)
|
116
|
+
#
|
117
|
+
# # create our union type with an unnamed field
|
118
|
+
# t = union do
|
119
|
+
# # add the unnamed field, in this case the header type
|
120
|
+
# member header
|
121
|
+
# member :raw, string
|
122
|
+
# size { |union| header.size + union.len }
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# # now unpack an instance of the union type
|
126
|
+
# packet = t.unpack("\x01\0\0\0\x13\0\0\0hello worldXXX")
|
127
|
+
#
|
128
|
+
# # access the unnamed field attributes
|
129
|
+
# p.id # => 1
|
130
|
+
# p.len # => 19
|
131
|
+
def member(name, type = nil)
|
132
|
+
# named field
|
133
|
+
if type
|
134
|
+
name = name.to_sym
|
135
|
+
@fields << [name, type].freeze
|
136
|
+
@field_names << name
|
137
|
+
@schema << Dry::Types::Schema::Key
|
138
|
+
.new(type.dry_type.type, name, required: false)
|
139
|
+
@default ||= {name => type.default_value}
|
140
|
+
|
141
|
+
# unnamed field
|
142
|
+
else
|
143
|
+
type = name
|
144
|
+
dry_keys = type.dry_type.keys or
|
145
|
+
raise Error, "unsupported type for unnamed field: %p" % [type]
|
146
|
+
names = dry_keys.map(&:name)
|
147
|
+
|
148
|
+
if (duplicate = names.any? { |n| @field_names.include?(n) })
|
149
|
+
raise Error, "duplicate field name %p in unnamed field: %p" %
|
150
|
+
[duplicate, type]
|
151
|
+
end
|
152
|
+
|
153
|
+
@fields << [names, type].freeze
|
154
|
+
@field_names += names
|
155
|
+
@schema += dry_keys.map do |key|
|
156
|
+
# for all of the keys in the type, we need to create an equivalent
|
157
|
+
# Key where the key is omittable.
|
158
|
+
#
|
159
|
+
# note: we strip the default value off the dry type here when defining
|
160
|
+
# the schema for our own dry type. If we do not do this, `dry_type[{}]`
|
161
|
+
# has every member in it, resulting in "only one member" error being
|
162
|
+
# raised in Union.pack when the union is nested within a struct.
|
163
|
+
#
|
164
|
+
# Example:
|
165
|
+
# struct(id: uint8, value: union(byte: uint8, word: uint32))
|
166
|
+
# .pack({value: byte: 1})
|
167
|
+
Dry::Types::Schema::Key.new(key.type.type, key.name, required: false)
|
168
|
+
end
|
169
|
+
|
170
|
+
@default ||= type.default_value
|
171
|
+
end
|
172
|
+
|
173
|
+
# fix up the size
|
174
|
+
@size = type.size if @size.is_a?(Integer) && type.size > @size
|
175
|
+
@fixed_size &&= type.fixed_size?
|
176
|
+
self
|
177
|
+
end
|
178
|
+
|
179
|
+
# Add a proc for determining Union size based on decoded bytes
|
180
|
+
# @param block block will be called to determine struct size in bytes
|
181
|
+
#
|
182
|
+
# When unpacking a variable length {Union}, the size proc is passed a
|
183
|
+
# frozen {Union} instance with the entire input buffer. The size proc will
|
184
|
+
# then unpack only those members it needs to calculate the total union
|
185
|
+
# size, and return the union size in bytes.
|
186
|
+
#
|
187
|
+
# @example variable length type-length-value (TLV) union
|
188
|
+
# CTypes::Helpers.struct do
|
189
|
+
# # TLV message header with some fixed types
|
190
|
+
# header = struct(
|
191
|
+
# msg_type: enum(uint8, {invalid: 0, hello: 1, read: 2}),
|
192
|
+
# len: uint16)
|
193
|
+
#
|
194
|
+
# member :header, header
|
195
|
+
# member :hello, struct(header:, version: string)
|
196
|
+
# member :read, struct(header:, offset: uint64, size: uint64)
|
197
|
+
# member :raw, string(trim: false)
|
198
|
+
#
|
199
|
+
# # the header#len field contains the length of the remaining union
|
200
|
+
# # bytes after the header. So we add the header size, and the value
|
201
|
+
# # in header.len to get the total union size.
|
202
|
+
# size { |union| header.size + union.header.len }
|
203
|
+
# end
|
204
|
+
def size(&block)
|
205
|
+
@fixed_size = false
|
206
|
+
@size = block
|
207
|
+
self
|
208
|
+
end
|
209
|
+
|
210
|
+
# used for custom type resolution
|
211
|
+
# @see CTypes.using_type_lookup
|
212
|
+
def method_missing(name, *args, &block)
|
213
|
+
if @type_lookup && args.empty? && block.nil?
|
214
|
+
type = @type_lookup.call(name)
|
215
|
+
return type if type
|
216
|
+
end
|
217
|
+
super
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|