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.
@@ -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