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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Cisco
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ module CTypes
7
+ # Used to export CTypes
8
+ # @api private
9
+ class Exporter
10
+ def initialize(output = "".dup)
11
+ @output = output
12
+ @indent = 0
13
+ @indented = false
14
+ @type_lookup = nil
15
+ end
16
+ attr_writer :type_lookup
17
+ attr_reader :output
18
+
19
+ def nest(indent, &block)
20
+ @indent += indent
21
+ yield
22
+ ensure
23
+ @indent -= indent
24
+ end
25
+
26
+ def break
27
+ self << "\n"
28
+ end
29
+
30
+ def <<(arg)
31
+ case arg
32
+ when CTypes::Type
33
+ if buf = @type_lookup&.call(arg)
34
+ self << buf
35
+ else
36
+ nest(2) { arg.export_type(self) }
37
+ end
38
+ when ::String
39
+ unless @indented
40
+ @output << " " * @indent
41
+ @indented = true
42
+ end
43
+ @output << arg
44
+ @indented = !arg.end_with?("\n")
45
+ else
46
+ raise Error, "not supported"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Cisco
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require "dry-types"
7
+
8
+ module CTypes
9
+ module Helpers
10
+ extend self
11
+
12
+ # define integer types
13
+ [8, 16, 32, 64, 128].each do |bits|
14
+ define_method("uint%d" % bits) { CTypes.const_get("UInt#{bits}") }
15
+ define_method("int%d" % bits) { CTypes.const_get("Int#{bits}") }
16
+ end
17
+
18
+ # create an {Enum} type
19
+ # @param type [Type] integer type to encode as; default uint32
20
+ # @param values [Array, Hash] value names, or name-value pairs
21
+ #
22
+ # @example 8-bit enum with two known values
23
+ # t = enum(uint8, [:a, :b])
24
+ # t.pack(:a) # => "\x00"
25
+ # t.pack(:b) # => "\x01"
26
+ # t.pack(:c) # Dry::Types::ConstraintError
27
+ #
28
+ # @example sparse 32-bit enum
29
+ # t = enum(uint32, {none: 0, a: 0x1000, b: 0x20})
30
+ # t.pack(:none) # => "\0\0\0\0"
31
+ # t.pack(:a) # => "\x00\x10\x00\x00" (little endian)
32
+ # t.pack(:b) # => "\x20\x00\x00\x00" (little endian)
33
+ #
34
+ # @example sparse 32-bit enum using builder
35
+ # t = enum(uint16) do |e|
36
+ # e << %i{a b c} # a = 0, b = 1, c = 2
37
+ # e << {d: 16} # d = 16
38
+ # e << :e # e = 17
39
+ # end
40
+ # t.pack(:e) # => "\x11\x00" (little endian)
41
+ def enum(type = nil, values = nil, &)
42
+ Enum.new(type, values, &)
43
+ end
44
+
45
+ # create a {Bitmap} type
46
+ # @param type [Type] integer type to encode as; default min bytes required
47
+ # @param bits [Hash, Enum] map of names to bit position
48
+ #
49
+ # @example 32-bit bitmap
50
+ # bitmap({a: 0, b: 1, c: 2}) # => #<Bitmap ...>
51
+ #
52
+ # @example 32-bit bitmap using block syntax; same as [Enum]
53
+ # bitmap do |b|
54
+ # b << :a
55
+ # b << :b
56
+ # end # => #<Bitmap a: 0, b: 1>
57
+ #
58
+ # @example 8-bit bitmap
59
+ # bitmap(uint8, {a: 0, b: 1}) # => #<Bitmap a: 0, b: 1>
60
+ #
61
+ def bitmap(type = nil, bits = nil, &)
62
+ if bits.nil?
63
+ bits = type
64
+ type = uint32
65
+ end
66
+
67
+ bits = enum(bits, &) unless bits.is_a?(Enum)
68
+ Bitmap.new(type: type, bits: bits)
69
+ end
70
+
71
+ # create a {String} type
72
+ # @param size [Integer] optional string size in bytes
73
+ # @param trim [Boolean] set to false to preserve trailing null bytes when
74
+ # unpacking
75
+ #
76
+ # @example 5 byte string
77
+ # s = string(5)
78
+ # s.unpack("hello world") # => "hello")
79
+ def string(size = nil, trim: true)
80
+ String.new(size:, trim:)
81
+ end
82
+
83
+ # create a {Struct} type
84
+ # @param attributes [Hash] name/type attribute pairs
85
+ # @yield block passed to {Struct::Builder}
86
+ #
87
+ # @example hash syntax
88
+ # t = struct(id: uint32, name: string.terminated)
89
+ # t.pack({id: 1, name: "Karlach"}) # => "\1\0\0\0Karlach\0"
90
+ #
91
+ # @example block syntax
92
+ # t = struct do
93
+ # attribute :id, uint32
94
+ # attrubite :name, string.terminated
95
+ # end
96
+ # t.pack({id: 1, name: "Karlach"}) # => "\1\0\0\0Karlach\0"
97
+ def struct(attributes = nil, &block)
98
+ Class.new(Struct) do
99
+ if attributes
100
+ layout do
101
+ attributes.each do |name, type|
102
+ attribute name, type
103
+ end
104
+ end
105
+ else
106
+ layout(&block)
107
+ end
108
+ end
109
+ end
110
+
111
+ # create a {Union} type
112
+ # @param members [Hash] name/type member pairs
113
+ # @yield block bassed to {Union::Builder}
114
+ #
115
+ # @example hash syntax
116
+ # t = union(word: uint32, halfword: uint16, byte: uint8)
117
+ # t.pack({byte: 3}) # => "\x03\x00\x00\x00"
118
+ #
119
+ # @example block syntax
120
+ # t = union do
121
+ # member :word, uint32
122
+ # member :halfword, uint16
123
+ # member :byte, uint8
124
+ # end
125
+ # t.pack({byte: 3}) # => "\x03\x00\x00\x00"
126
+ def union(members = nil, &block)
127
+ Class.new(Union) do
128
+ if members
129
+ layout do
130
+ members.each do |name, type|
131
+ member name, type
132
+ end
133
+ end
134
+ else
135
+ layout(&block)
136
+ end
137
+ end
138
+ end
139
+
140
+ # create an {Array} type
141
+ # @param type [Type] data type contained within the array
142
+ # @param size [Integer] optional array size; no size implies greedy array
143
+ # @param terminator optional unpacked value that represents the array
144
+ # terminator
145
+ #
146
+ # @example
147
+ # # greedy array of uint32 values
148
+ # array(uint32)
149
+ # # array of 4 uint8 values
150
+ # array(uint8, 4)
151
+ # # array of signed 32-bit integers terminated with a -1
152
+ # array(int32, terminator: -1)
153
+ def array(type, size = nil, terminator: nil)
154
+ Array.new(type:, size:, terminator:)
155
+ end
156
+
157
+ # create a {Bitfield} type
158
+ # @param type [Type] type to use for packed representation
159
+ # @param bits [Hash] map of name to bit count
160
+ # @yield block passed to {Bitfield::Builder}
161
+ #
162
+ # @example dynamically sized
163
+ # t = bitfield(a: 1, b: 2, c: 3)
164
+ # t.pack({c: 0b111}) # => "\x38" (0b00111000)
165
+ #
166
+ # @example fixed size to pad to 16 bits.
167
+ # t = bitfield(uint16, a: 1, b: 2, c: 3)
168
+ # t.pack({c: 0b111}) # => "\x38\x00" (0b00111000_00000000)
169
+ #
170
+ def bitfield(type = nil, bits = nil, &block)
171
+ if bits.nil? && !block
172
+ bits = type
173
+ type = nil
174
+ end
175
+
176
+ Class.new(Bitfield) do
177
+ if bits
178
+ layout do
179
+ bytes(type.size) if type
180
+ bits.each do |name, size|
181
+ unsigned name, size
182
+ end
183
+ end
184
+ else
185
+ layout(&block)
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,150 @@
1
+ # SPDX-FileCopyrightText: 2025 Cisco
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ require "nokogiri"
5
+
6
+ module CTypes
7
+ class Importers::CastXML::Loader
8
+ include CTypes::Helpers
9
+
10
+ def initialize(io)
11
+ @doc = Nokogiri.parse(io)
12
+ @xml = @doc.xpath("//castxml[1]")&.first or
13
+ raise Error, "<castxml> node not found"
14
+ @nodes = @xml.xpath("./*[@id]").each_with_object({}) { |n, o|
15
+ o[n[:id]] = n
16
+ }
17
+ @ctypes = {}
18
+ end
19
+ attr_reader :xml
20
+
21
+ def load
22
+ m = Module.new
23
+ load_into(m)
24
+ end
25
+
26
+ def load_into(namespace)
27
+ @xml.children.each do |node|
28
+ next unless node.element?
29
+
30
+ case node.name
31
+ when "typedef", "struct", "union", "array", "enumeration"
32
+ # skip builtin types
33
+ next if node[:file] == "f0"
34
+
35
+ name, type = ctype(node[:id])
36
+ next if name.empty?
37
+
38
+ namespace.define_singleton_method(name) { type }
39
+ end
40
+ end
41
+
42
+ namespace
43
+ end
44
+
45
+ private
46
+
47
+ def ctype(id)
48
+ return @ctypes[id] if @ctypes.has_key?(id)
49
+ node = @nodes[id] or raise Error, "node not found: id=\"#{id}\""
50
+
51
+ return node[:name], nil if node[:incomplete] == "1"
52
+
53
+ type = case node.name
54
+ when "fundamentaltype"
55
+ unsigned = node[:name].include?("unsigned")
56
+ case node[:size]
57
+ when "0"
58
+ nil
59
+ when "128"
60
+ array(unsigned ? uint64 : int64, 2)
61
+ when "64"
62
+ unsigned ? uint64 : int64
63
+ when "32"
64
+ unsigned ? uint32 : int32
65
+ when "16"
66
+ unsigned ? uint16 : int16
67
+ when "8"
68
+ unsigned ? uint8 : int8
69
+ else
70
+ raise Error, "unknown FundamentalType: %s" % node.pretty_inspect
71
+ end
72
+ when "typedef", "field", "elaboratedtype", "cvqualifiedtype"
73
+ _, t = ctype(node[:type])
74
+ t
75
+ when "struct"
76
+ if node.has_attribute?("members")
77
+ pos = 0
78
+ members = node[:members].split.each_with_object({}) do |mid, o|
79
+ # to support member alignment, we need to do some extra work here
80
+ # to add padding members to structures when there are gaps. Note
81
+ # that for some reason, anonymous structs are not counted towards
82
+ # the offset of following struct fields; this may be a bug in llvm,
83
+ # as castxml just prints what was provided.
84
+ mem = @nodes[mid] or raise Error, "node not found: id=\"#{mid}\""
85
+ if mem.has_attribute?("offset")
86
+
87
+ # add a padding member to the struct if needed
88
+ offset = mem[:offset].to_i
89
+ if pos < offset
90
+ o[:"__pad_#{pos / 8}"] =
91
+ string((offset - pos) / 8, trim: false)
92
+ end
93
+
94
+ # always set pos to the current offset; this handles the case
95
+ # where we added the size of a nested anonymous struct, but llvm
96
+ # does not appear to.
97
+ pos = offset
98
+ end
99
+
100
+ name, mtype = ctype(mid)
101
+ o[name.to_sym] = mtype unless name.empty?
102
+ pos += mtype.size * 8
103
+ end
104
+ else
105
+ members = {unknown: array(uint8, node[:size].to_i)}
106
+ end
107
+ struct(members)
108
+ when "union"
109
+ members = node[:members].split.each_with_object({}) do |mid, o|
110
+ name, mtype = ctype(mid)
111
+ o[name.to_sym] = mtype
112
+ end
113
+ union(members)
114
+ when "arraytype"
115
+ n, t = ctype(node[:type])
116
+ if n == "char"
117
+ string(node[:max].to_i + 1)
118
+ else
119
+ array(t, node[:max].to_i + 1)
120
+ end
121
+ when "enumeration"
122
+ values = node.children.each_with_object({}) do |v, o|
123
+ o[v[:name].downcase] = v[:init].to_i if v.name == "enumvalue"
124
+ end
125
+ type = if node[:type]
126
+ _, t = ctype(node[:type])
127
+ t
128
+ elsif node[:size] == "32"
129
+ uint32
130
+ else
131
+ raise Error, "unsupported enum node: %p" % node
132
+ end
133
+ enum(type, values)
134
+ when "pointertype"
135
+ case node[:size]
136
+ when "64"
137
+ uint64
138
+ when "32"
139
+ uint32
140
+ else
141
+ raise Error, "unknown PointerType size: %s" % node.pretty_inspect
142
+ end
143
+ else
144
+ raise "unsupported node: %s" % node.pretty_inspect
145
+ end
146
+
147
+ @ctypes[id] = [node[:name], type]
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,59 @@
1
+ # SPDX-FileCopyrightText: 2025 Cisco
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ require "open3"
5
+ begin
6
+ require "nokogiri"
7
+ rescue LoadError
8
+ puts <<~ERR
9
+ WARNING: Failed to require `nokogiri` gem.
10
+
11
+ `nokogiri` is required to parse CastXML output. To use the CastXML
12
+ importer, please install the gem.
13
+ ERR
14
+ end
15
+
16
+ module CTypes
17
+ module Importers
18
+ module CastXML
19
+ class CompilerError < CTypes::Error; end
20
+
21
+ def self.load_xml(xml)
22
+ io = case xml
23
+ when IO
24
+ xml
25
+ when ::String
26
+ StringIO.new(xml)
27
+ else
28
+ raise Error, "arg must be IO or String: %p" % xml
29
+ end
30
+
31
+ l = Loader.new(io)
32
+ l.load
33
+ end
34
+
35
+ def self.load_xml_file(path)
36
+ File.open(path) do |f|
37
+ load_xml(f)
38
+ end
39
+ end
40
+
41
+ def self.load_source(src)
42
+ Tempfile.open(["", ".c"]) do |f|
43
+ f.write(src)
44
+ f.flush
45
+ load_source_file(f.path)
46
+ end
47
+ end
48
+
49
+ def self.load_source_file(path)
50
+ stdout, stderr, status = Open3
51
+ .capture3("castxml --castxml-output=1 #{path} -o -")
52
+ raise CompilerError, stderr unless status.success?
53
+ load_xml(stdout)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ require_relative "castxml/loader"
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Cisco
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ module CTypes
5
+ # Tools for importing types from other sources to declare ctypes
6
+ module Importers; end
7
+ end
data/lib/ctypes/int.rb ADDED
@@ -0,0 +1,147 @@
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
+ # Handles packing and unpacking of integer types of various lengths and
9
+ # signed-ness. The most common integer sizes have been declared as
10
+ # constants:
11
+ # - unsigned: {UInt64}, {UInt32}, {UInt16}, {UInt8}
12
+ # - signed: {Int64}, {Int32}, {Int16}, {Int8}
13
+ # or their respective helpers in {Helpers}.
14
+ class Int
15
+ include Type
16
+
17
+ # initialize an {Int} type
18
+ #
19
+ # @param [Integer] bits number of bits in the integer
20
+ # @param [Boolean] signed set to true if integer is signed
21
+ # @param [String] format {::Array#pack} format for integer
22
+ # @param [String] desc human-readable description of type
23
+ #
24
+ def initialize(bits:, signed:, format:, desc:)
25
+ type = Dry::Types["integer"].default(0)
26
+ @signed = !!signed
27
+ if @signed
28
+ @min = 0 - (1 << (bits - 1))
29
+ @max = 1 << (bits - 1) - 1
30
+ else
31
+ @min = 0
32
+ @max = (1 << bits) - 1
33
+ end
34
+ @dry_type = type.constrained(gteq: @min, lteq: @max)
35
+ @size = bits / 8
36
+ if @size > 1
37
+ @format_big = "#{format}>"
38
+ @format_little = "#{format}<"
39
+ else
40
+ @format_big = @format_little = format.to_s
41
+ end
42
+ @fmt = (@size > 1) ? {big: "#{format}>", little: "#{format}<"} :
43
+ {big: format.to_s, little: format.to_s}
44
+ @desc = desc
45
+ end
46
+ attr_reader :size, :min, :max
47
+
48
+ # convert an Integer into a String containing the binary representation of
49
+ # that number for the given type.
50
+ #
51
+ # @param value [Integer] number to pack
52
+ # @param endian [Symbol] byte order
53
+ # @param validate [Boolean] set to false to disable bounds checking
54
+ # @return [String] binary encoding for value
55
+ #
56
+ # @example pack a uint32_t using the native endian
57
+ # CTypes::UInt32.pack(0x12345678) # => "\x78\x56\x34\x12"
58
+ #
59
+ # @example pack a big endian uint32_t
60
+ # CTypes::UInt32.pack(endian: :big) # => "\x12\x34\x56\x78"
61
+ #
62
+ # @example pack a fixed-endian (big) uint32_t
63
+ # t = UInt32.with_endian(:big)
64
+ # t.pack(0x12345678) # => "\x12\x34\x56\x78"
65
+ #
66
+ # @see CTypes::Type#pack
67
+ def pack(value, endian: default_endian, validate: true)
68
+ value = (value.nil? ? @dry_type[] : @dry_type[value]) if validate
69
+ endian ||= default_endian
70
+ [value].pack(@fmt[endian])
71
+ end
72
+
73
+ # decode an Integer from the byte String provided, returning both the
74
+ # Integer and any unused bytes in the String
75
+ #
76
+ # @param buf [String] bytes to be unpacked
77
+ # @param endian [Symbol] endian of data within buf
78
+ # @return [Integer, String] decoded Integer, and remaining bytes
79
+ #
80
+ # @example pack a uint32_t using the native endian
81
+ # CTypes::UInt32.pack(0x12345678) # => "\x78\x56\x34\x12"
82
+ #
83
+ # @example pack a big endian uint32_t
84
+ # CTypes::UInt32.pack(endian: :big) # => "\x12\x34\x56\x78"
85
+ #
86
+ # @example pack a fixed-endian (big) uint32_t
87
+ # t = UInt32.with_endian(:big)
88
+ # t.pack(0x12345678) # => "\x12\x34\x56\x78"
89
+ #
90
+ # @see CTypes::Type#unpack
91
+ # @see CTypes::Type#unpack_one
92
+ def unpack_one(buf, endian: default_endian)
93
+ endian ||= default_endian # override nil
94
+ value = buf.unpack1(@fmt[endian]) or
95
+ raise missing_bytes_error(input: buf, need: @size)
96
+ [value, buf.byteslice(@size..)]
97
+ end
98
+
99
+ # @api private
100
+ def greedy?
101
+ false
102
+ end
103
+
104
+ # @api private
105
+ def signed?
106
+ @signed
107
+ end
108
+
109
+ # @api private
110
+ def pretty_print(q) # :nodoc:
111
+ if @endian
112
+ q.text(@desc + ".with_endian(%p)" % @endian)
113
+ else
114
+ q.text(@desc)
115
+ end
116
+ end
117
+ alias_method :inspect, :pretty_inspect # :nodoc:
118
+
119
+ # @api private
120
+ def export_type(q) # :nodoc:
121
+ q << @desc
122
+ q << ".with_endian(%p)" % [@endian] if @endian
123
+ end
124
+
125
+ # @api private
126
+ def type_name
127
+ "#{@desc}_t"
128
+ end
129
+ end
130
+
131
+ # base type for unsiged 8-bit integers
132
+ UInt8 = Int.new(bits: 8, signed: false, format: "C", desc: "uint8")
133
+ # base type for unsiged 16-bit integers
134
+ UInt16 = Int.new(bits: 16, signed: false, format: "S", desc: "uint16")
135
+ # base type for unsiged 32-bit integers
136
+ UInt32 = Int.new(bits: 32, signed: false, format: "L", desc: "uint32")
137
+ # base type for unsiged 64-bit integers
138
+ UInt64 = Int.new(bits: 64, signed: false, format: "Q", desc: "uint64")
139
+ # base type for siged 8-bit integers
140
+ Int8 = Int.new(bits: 8, signed: true, format: "c", desc: "int8")
141
+ # base type for siged 16-bit integers
142
+ Int16 = Int.new(bits: 16, signed: true, format: "s", desc: "int16")
143
+ # base type for siged 32-bit integers
144
+ Int32 = Int.new(bits: 32, signed: true, format: "l", desc: "int32")
145
+ # base type for siged 64-bit integers
146
+ Int64 = Int.new(bits: 64, signed: true, format: "q", desc: "int64")
147
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Cisco
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ module CTypes
7
+ # Exception raised when attempting to unpack a {Type} that requires more
8
+ # bytes than were provided in the input.
9
+ class MissingBytesError < Error
10
+ def initialize(type:, input:, need:)
11
+ @type = type
12
+ @input = input
13
+ @need = need
14
+ super("insufficent input to unpack %s; missing %d bytes" %
15
+ [@type, missing])
16
+ end
17
+ attr_reader :type, :input, :need
18
+
19
+ # get the number of additional bytes required to unpack this type
20
+ def missing
21
+ @need - @input.size
22
+ end
23
+ end
24
+ end
data/lib/ctypes/pad.rb ADDED
@@ -0,0 +1,56 @@
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
+ # Generic type to represent a gap in the data structure. Unpacking this
9
+ # type will consume the pad size and return nil. Packing this type will
10
+ # return a string of null bytes of the appropriate size.
11
+ #
12
+ # @example
13
+ # t = Pad.new(4)
14
+ # t.unpack_one("hello_world) # => [nil, "o_world"]
15
+ # t.pack("blahblahblah") # => "\0\0\0\0"
16
+ class Pad
17
+ include Type
18
+
19
+ def initialize(size)
20
+ @size = size
21
+ @dry_type = Dry::Types::Any.default(nil)
22
+ end
23
+ attr_reader :size
24
+
25
+ def pack(value, endian: default_endian, validate: true)
26
+ "\0" * @size
27
+ end
28
+
29
+ def unpack_one(buf, endian: default_endian)
30
+ raise missing_bytes_error(input: buf, need: @size) if
31
+ @size && buf.size < @size
32
+ [nil, buf.byteslice(@size..)]
33
+ end
34
+
35
+ def greedy?
36
+ false
37
+ end
38
+
39
+ def to_s
40
+ "pad(%d)" % [@size]
41
+ end
42
+
43
+ def pretty_print(q)
44
+ q.text("pad(%d)" % @size)
45
+ end
46
+ alias_method :inspect, :pretty_inspect # :nodoc:
47
+
48
+ def export_type(q)
49
+ q << ".pad(%d)" % [@size]
50
+ end
51
+
52
+ def ==(other)
53
+ other.is_a?(self.class) && other.size == size
54
+ end
55
+ end
56
+ end