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,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