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.
data/SECURITY.md ADDED
@@ -0,0 +1,57 @@
1
+ # Security Policies and Procedures
2
+
3
+ This document outlines security procedures and general policies for the
4
+ `ctypes` project.
5
+
6
+ - [Disclosing a security issue](#disclosing-a-security-issue)
7
+ - [Vulnerability management](#vulnerability-management)
8
+ - [Suggesting changes](#suggesting-changes)
9
+
10
+ ## Disclosing a security issue
11
+
12
+ The `ctypes` maintainers take all security issues in the project
13
+ seriously. Thank you for improving the security of `ctypes`. We
14
+ appreciate your dedication to responsible disclosure and will make every effort
15
+ to acknowledge your contributions.
16
+
17
+ `ctypes` leverages GitHub's private vulnerability reporting.
18
+
19
+ To learn more about this feature and how to submit a vulnerability report,
20
+ review [GitHub's documentation on private reporting](https://docs.github.com/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability).
21
+
22
+ Here are some helpful details to include in your report:
23
+
24
+ - a detailed description of the issue
25
+ - the steps required to reproduce the issue
26
+ - versions of the project that may be affected by the issue
27
+ - if known, any mitigations for the issue
28
+
29
+ A maintainer will acknowledge the report within three (3) business days, and
30
+ will send a more detailed response within an additional three (3) business days
31
+ indicating the next steps in handling your report.
32
+
33
+ If you've been unable to successfully draft a vulnerability report via GitHub
34
+ or have not received a response during the alloted response window, please
35
+ reach out via the [Cisco Open security contact email](mailto:oss-security@cisco.com).
36
+
37
+ After the initial reply to your report, the maintainers will endeavor to keep
38
+ you informed of the progress towards a fix and full announcement, and may ask
39
+ for additional information or guidance.
40
+
41
+ ## Vulnerability management
42
+
43
+ When the maintainers receive a disclosure report, they will assign it to a
44
+ primary handler.
45
+
46
+ This person will coordinate the fix and release process, which involves the
47
+ following steps:
48
+
49
+ - confirming the issue
50
+ - determining affected versions of the project
51
+ - auditing code to find any potential similar problems
52
+ - preparing fixes for all releases under maintenance
53
+
54
+ ## Suggesting changes
55
+
56
+ If you have suggestions on how this process could be improved please submit an
57
+ issue or pull request.
data/ctypes.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ctypes/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ctypes"
7
+ spec.version = CTypes::VERSION
8
+ spec.authors = ["David M. Lary"]
9
+ spec.email = ["dmlary@gmail.com"]
10
+
11
+ spec.summary = "Manipulate common C types in Ruby"
12
+ # spec.description = "TODO: Write a longer description or delete this line."
13
+ spec.homepage = "https://github.com/cisco-open/ruby-ctypes"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ # spec.metadata["homepage_uri"] = spec.homepage
20
+ # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
21
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+ spec.add_dependency("dry-struct", "~> 1.0")
37
+
38
+ # For more information and examples about making a new gem, check out our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end
@@ -0,0 +1,180 @@
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
+ # @example array of unsigned 32-bit integers
9
+ # t = CTypes::Array.new(type: CTypes::Helpers.uint32)
10
+ # t.unpack("\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb")
11
+ # # => [0xaaaaaaaa, 0xbbbbbbbb]
12
+ # t.pack([0,0xffffffff]) # => "\0\0\0\0\xff\xff\xff\xff"
13
+ #
14
+ # @example array of signed 8-bit integers
15
+ # t = CTypes::Array.new(type: CTypes::Helpers.int8)
16
+ # t.unpack("\x01\x02\x03\x04") # => [1, 2, 3, 4]
17
+ # t.pack([1, 2, 3, 4]) # => "\x01\x02\x03\x04"
18
+ #
19
+ # @example fixed-size array of 8-bit integers
20
+ # t = CTypes::Array.new(type: CTypes::Helpers.int8, size: 2)
21
+ # t.unpack("\x01\x02\x03\x04") # => [1, 2]
22
+ # t.pack([1, 2]) # => "\x01\x02"
23
+ #
24
+ # @example terminated array of 8-bit integers
25
+ # t = CTypes::Array.new(type: CTypes::Helpers.int8, terminator: -1)
26
+ # t.unpack("\x01\xff\x03\x04") # => [1]
27
+ # t.pack([1]) # => "\x01\xff"
28
+ #
29
+ # @example array of structures
30
+ # include CTypes::Helpers
31
+ # s = struct do
32
+ # attribute :type, uint8
33
+ # attribute :value, uint8
34
+ # end
35
+ # t = array(s)
36
+ # t.unpack("\x01\x02\x03\x04") # => [ { .type = 1, value = 2 },
37
+ # # { .type = 3, value = 4 } ]
38
+ # t.pack([{type: 1, value: 2}, {type: 3, value: 4}])
39
+ # # => "\x01\x02\x03\x04"
40
+ class Array
41
+ include Type
42
+
43
+ # TODO Add support for a pre-unpack terminator that checks against the
44
+ # remaining buffer before calling unpack on inner type. This allows easier
45
+ # support for DWARF-type types (.debug_line file_names)
46
+
47
+ # declare a new Array type
48
+ # @param type [CTypes::Type] type contained within the array
49
+ # @param size [Integer] number of elements in the array; nil means greedy
50
+ # unpack
51
+ # @param terminator array value that denotes the end of the array; the
52
+ # value will not be appended in `unpack` results, but will be appended
53
+ # during `pack`
54
+ def initialize(type:, size: nil, terminator: nil)
55
+ raise Error, "cannot use terminator with fixed size array" if
56
+ size && terminator
57
+ raise Error, "cannot make an Array of variable-length Unions" if
58
+ type.is_a?(Class) && type < Union && !type.fixed_size?
59
+
60
+ @type = type
61
+ @size = size
62
+ if terminator
63
+ @terminator = terminator
64
+ @term_packed = @type.pack(terminator)
65
+ @term_unpacked = @type.unpack(@term_packed)
66
+ end
67
+
68
+ @dry_type = Dry::Types["coercible.array"].of(type.dry_type)
69
+ @dry_type = if size
70
+ @dry_type.constrained(size:)
71
+ .default { ::Array.new(size, type.dry_type[]) }
72
+ else
73
+ @dry_type.default([].freeze)
74
+ end
75
+ end
76
+ attr_reader :type, :terminator
77
+
78
+ # pack a ruby array into a binary string
79
+ # @param value [::Array] array value to pack
80
+ # @param endian [Symbol] optional endian override
81
+ # @param validate [Boolean] set to false to disable value validation
82
+ # @return [::String] binary string
83
+ def pack(value, endian: default_endian, validate: true)
84
+ value = @dry_type[value] if validate
85
+ out = value.inject(::String.new) do |o, v|
86
+ o << @type.pack(v, endian: @type.endian || endian)
87
+ end
88
+ out << @term_packed if @term_packed
89
+ out
90
+ rescue Dry::Types::ConstraintError
91
+ raise unless @size && @size > value.size
92
+
93
+ # value is short some elements; fill them in and retry
94
+ value += ::Array.new(@size - value.size, @type.default_value)
95
+ retry
96
+ end
97
+
98
+ # unpack an instance of an array from the beginning of the supplied binary
99
+ # string
100
+ # @param buf [::String] binary string
101
+ # @param endian [Symbol] optional endian override
102
+ # @return [Array(Object, ::String)] unpacked Array, unused bytes fron buf
103
+ def unpack_one(buf, endian: default_endian)
104
+ rest = buf
105
+ if @size
106
+ value = @size.times.map do |i|
107
+ o, rest = @type.unpack_one(rest, endian: @type.endian || endian)
108
+ o or raise missing_bytes_error(input: value,
109
+ need: @size * @type.size)
110
+ end
111
+ else
112
+ # handle variable-length array; both greedy and terminated
113
+ value = []
114
+ loop do
115
+ if rest.empty?
116
+ if @term_packed
117
+ raise TerminatorNotFoundError,
118
+ "terminator not found in: %p" % buf
119
+ end
120
+ break
121
+ end
122
+
123
+ v, rest = @type.unpack_one(rest, endian: @type.endian || endian)
124
+ break if v === @term_unpacked
125
+ value << v
126
+ end
127
+ end
128
+ [value, rest]
129
+ end
130
+
131
+ # check if this Array is greedy
132
+ def greedy?
133
+ !@size && !@terminator
134
+ end
135
+
136
+ # return the size of the array if one is defined
137
+ def size
138
+ s = @size ? @size * @type.size : 0
139
+ s += @term_packed.size if @term_packed
140
+ s
141
+ end
142
+
143
+ def pretty_print(q) # :nodoc:
144
+ q.group(1, "array(", ")") do
145
+ q.pp(@type)
146
+ if @size
147
+ q.comma_breakable
148
+ q.text(@size.to_s)
149
+ end
150
+ if @terminator
151
+ q.comma_breakable
152
+ q.text("terminator: #{@terminator}")
153
+ end
154
+ end
155
+ end
156
+ alias_method :inspect, :pretty_inspect # :nodoc:
157
+
158
+ def export_type(q) # :nodoc:
159
+ q << "array("
160
+ q << @type
161
+ q << ", #{@size}" if @size
162
+ q << ", terminator: #{@terminator}" if @terminator
163
+ q << ")"
164
+ q << ".with_endian(%p)" % [@endian] if @endian
165
+ end
166
+
167
+ def type_name
168
+ @size ?
169
+ "%s[%s]" % [@type.type_name, @size] :
170
+ "%s[]" % [@type.type_name]
171
+ end
172
+
173
+ def ==(other)
174
+ return false unless other.is_a?(Array)
175
+ other.type == @type &&
176
+ other.size == size &&
177
+ other.terminator == terminator
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Cisco
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ module CTypes
7
+ # {Bitfield} layout builder
8
+ #
9
+ # This class is used to describe the memory layout of a {Bitfield} type.
10
+ # There are two approaches provided here for describing the layout, the first
11
+ # is a constructive interface using {Builder#unsigned}, {Builder#signed},
12
+ # {Builder#align}, and {Builder#skip}, to build the bitfield from
13
+ # right-to-left. These methods track how many bits have been used, and
14
+ # automatically determine the offset of fields as they're declared.
15
+ #
16
+ # The second interface is the programmatic interface that can be used to
17
+ # generate the {Bitfield} layout from data. This uses {Builder#field} to
18
+ # explicitly declare fields using bit size, offset, and signedness.
19
+ #
20
+ # @example using the constructive interface via {CTypes::Bitfield#layout}
21
+ # class MyBits < CTypes::Bitfield
22
+ # layout do
23
+ # # the body of this block is evaluated within a Builder instance
24
+ # unsigned :bit # single bit for a at offset 0
25
+ # unsigned :two, 2 # two bits for this field at offset 1
26
+ # signed :nibble, 4 # four bit nibble as a signed int, offset 3
27
+ # end
28
+ # end
29
+ #
30
+ # @example using the programmatic interface via {CTypes::Bitfield#layout}
31
+ # class MyBits < CTypes::Bitfield
32
+ # layout do
33
+ # # the body of this block is evaluated within a Builder instance
34
+ # field :bit, size: 1, offset: 0 # single bit at offset 0
35
+ # field :two, size: 2, offset: 1 # two bits at offset 1
36
+ # field :nibble, size: 4, offset: 3 # four bits at offset 3
37
+ # end
38
+ # end
39
+ #
40
+ # @example construct {CTypes::Bitfield} programmatically
41
+ # b = CTypes::Bitfield.builder # => #<CTypes::Bitfield::Builder>
42
+ # b.field(:one, bits: 1, offset: 0) # one bit at offset 0, named :one
43
+ #
44
+ # # Create additional fields from data loaded from elsewhere
45
+ # extra_fields = [
46
+ # [:two, 2, 1], # two bits for this field named :two
47
+ # [:nibble, 4, 3] # four bits for the :nibble field
48
+ # ]
49
+ # extra_fields.each do |name, bits, offset|
50
+ # b.field(name, bits:, offset:)
51
+ # end
52
+ #
53
+ # # build the type
54
+ # t = b.build # => #<Bitfield ...>
55
+ class Bitfield::Builder
56
+ include Helpers
57
+
58
+ def initialize
59
+ @fields = []
60
+ @schema = {}
61
+ @default = {}
62
+ @offset = 0
63
+ @layout = []
64
+ @max = 0
65
+ end
66
+
67
+ # get the offset of the next unused bit in the bitfield
68
+ attr_reader :offset
69
+
70
+ # build a {Bitfield} instance with the layout configured in this builder
71
+ # @return [Bitfield] bitfield with the layout defined in this builder
72
+ def build
73
+ k = Class.new(Bitfield)
74
+ k.send(:apply_layout, self)
75
+ k
76
+ end
77
+
78
+ # get the layout description for internal use in {Bitfield}
79
+ # @api private
80
+ def result
81
+ dry_type = Dry::Types["coercible.hash"]
82
+ .schema(@schema)
83
+ .strict
84
+ .default(@default.freeze)
85
+
86
+ type = case @max
87
+ when 0..8
88
+ UInt8
89
+ when 9..16
90
+ UInt16
91
+ when 17..32
92
+ UInt32
93
+ when 32..64
94
+ UInt64
95
+ else
96
+ raise Error, "bitfields greater than 64 bits not supported"
97
+ end
98
+
99
+ [type, @fields, dry_type, @endian, @layout]
100
+ end
101
+
102
+ # set the endian for this {Bitfield}
103
+ # @param value [Symbol] `:big` or `:little`
104
+ def endian(value)
105
+ @endian = Endian[value]
106
+ self
107
+ end
108
+
109
+ # skip `bits` bits in the layout of this bitfield
110
+ # @param bits [Integer] number of bits to skip
111
+ def skip(bits)
112
+ raise Error, "cannot mix `#skip` and `#field` in Bitfield layout" unless
113
+ @offset
114
+ @offset += bits
115
+ @max = @offset if @offset > @max
116
+ @layout << "skip #{bits}"
117
+ self
118
+ end
119
+
120
+ # set the alignment of the next field declared using {Builder#signed} or
121
+ # {Builder#unsigned}
122
+ # @param bits [Integer] bit alignment of the next field
123
+ # @note {Builder#align} cannot be mixed with calls to {Builder#field}
124
+ #
125
+ # @example
126
+ # class MyBits < CTypes::Bitfield
127
+ # layout do
128
+ # unsigned :a # single bit at offset 0
129
+ # align 4
130
+ # unsigned :b, 2 # two bits at offset 4
131
+ # align 4
132
+ # unsigned :c # single bit at offset 8
133
+ # end
134
+ # end
135
+ #
136
+ def align(bits)
137
+ raise Error, "cannot mix `#align` and `#field` in Bitfield layout" unless
138
+ @offset
139
+ @offset += bits - (@offset % bits)
140
+ @layout << "align #{bits}"
141
+ self
142
+ end
143
+
144
+ # append a new unsigned field to the bitfield
145
+ # @param name [String, Symbol] name of the field
146
+ # @param bits [Integer] number of bits
147
+ def unsigned(name, bits = 1)
148
+ unless @offset
149
+ raise Error,
150
+ "cannot mix `#unsigned` and `#field` in Bitfield layout"
151
+ end
152
+
153
+ name = name.to_sym
154
+ raise Error, "duplicate field: %p" % [name] if
155
+ @fields.any? { |(n, _)| n == name }
156
+
157
+ @layout << ((bits == 1) ?
158
+ "unsigned %p" % [name] :
159
+ "unsigned %p, %d" % [name, bits])
160
+
161
+ __field_impl(name:, bits:, offset: @offset, signed: false)
162
+ @offset += bits
163
+ self
164
+ end
165
+
166
+ # append a new signed field to the bitfield
167
+ # @param name [String, Symbol] name of the field
168
+ # @param bits [Integer] number of bits
169
+ def signed(name, bits = 1)
170
+ unless @offset
171
+ raise Error,
172
+ "cannot mix `#signed` and `#field` in Bitfield layout"
173
+ end
174
+
175
+ name = name.to_sym
176
+ raise Error, "duplicate field: %p" % [name] if
177
+ @fields.any? { |(n, _)| n == name }
178
+
179
+ @layout << ((bits == 1) ?
180
+ "signed %p" % [name] :
181
+ "signed %p, %d" % [name, bits])
182
+
183
+ __field_impl(name:, bits:, offset: @offset, signed: true)
184
+ @offset += bits
185
+ self
186
+ end
187
+
188
+ # set the size of the {Bitfield} in bytes
189
+ #
190
+ # Once the size is set, the Bitfield cannot grow past that size. Any calls
191
+ # to {Builder#signed} or {Builder#unsigned} that go beyond the size will
192
+ # raise errors.
193
+ #
194
+ # @param n [Integer] size in bytes
195
+ def bytes(n)
196
+ @layout << "bytes #{n}"
197
+ @max = n * 8
198
+ self
199
+ end
200
+
201
+ # declare a bit field at a specific offset
202
+ # @param name [String, Symbol] name of the field
203
+ # @param offset [Integer] right to left bit offset, where 0 is the least
204
+ # significant bit in a byte
205
+ # @param bits [Integer] number of bits used by this bitfield
206
+ # @param signed [Boolean] set to true to unpack as a signed integer
207
+ #
208
+ # This method is an alternative the construtive interface provided by
209
+ # {Builder#skip}, {Builder#align},
210
+ # {Builder#unsigned}, and {Builder#signed}. This is a programmatic
211
+ # interface for explicitly declaring fields using offset & bitcount.
212
+ #
213
+ # @note This method should not be used in combination with
214
+ # {Builder#skip}, {Builder#align}, {Builder#unsigned}, {Builder#signed}.
215
+ def field(name, offset:, bits:, signed: false)
216
+ name = name.to_sym
217
+ raise Error, "duplicate field: %p" % [name] if
218
+ @fields.any? { |(n, _)| n == name }
219
+
220
+ @layout << "field %p, offset: %d, bits: %d, signed: %p" %
221
+ [name, offset, bits, signed]
222
+ @offset = nil
223
+
224
+ __field_impl(name:, offset:, bits:, signed:)
225
+ self
226
+ end
227
+
228
+ private
229
+
230
+ # @api private
231
+ def __field_impl(name:, offset:, bits:, signed:) # :nodoc:
232
+ mask = (1 << bits) - 1
233
+
234
+ schema = Dry::Types["integer"].default(0)
235
+ @schema[name] = if signed
236
+ schema.constrained(gteq: 0 - (1 << (bits - 1)), lt: 1 << (bits - 1))
237
+ else
238
+ schema.constrained(gteq: 0, lt: 1 << bits)
239
+ end
240
+ @default[name] = 0
241
+
242
+ @fields << [name, offset, mask, signed ? bits : nil, :"@#{name}"]
243
+ @max = offset + bits if offset + bits > @max
244
+ end
245
+ end
246
+ end