Skip to content

Commit 11d4abe

Browse files
committed
Add C API specs for digest plugins.
1 parent 23308ff commit 11d4abe

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed

Diff for: spec/ruby/optional/capi/digest_spec.rb

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
require_relative 'spec_helper'
2+
3+
require 'ffi'
4+
5+
load_extension('digest')
6+
7+
class RbDigestMetadata < ::FFI::Struct
8+
layout :api_version, :int,
9+
:digest_len, :size_t,
10+
:block_len, :size_t,
11+
:ctx_size, :size_t,
12+
:init_func, ::FFI::FunctionType.new(:int, [:pointer]),
13+
:update_func, ::FFI::FunctionType.new(:void, [:pointer, :pointer, :size_t]),
14+
:finish_func, ::FFI::FunctionType.new(:int, [:pointer, :pointer])
15+
end
16+
17+
# Here is the list of digest C functions that the Blake3 plugin expects:
18+
# * rb_digest_hash_init_func_t
19+
# * rb_digest_hash_update_func_t
20+
# * rb_digest_hash_finish_func_t
21+
#
22+
# It also expects to access the `RUBY_DIGEST_API_VERSION` #define value
23+
24+
describe "C-API Digest functions" do
25+
before :each do
26+
@s = CApiDigestSpecs.new
27+
end
28+
29+
describe "rb_digest_make_metadata" do
30+
before :each do
31+
@metadata = @s.rb_digest_make_metadata
32+
end
33+
34+
it "should store the block length" do
35+
@s.block_length(@metadata).should == 40
36+
end
37+
38+
it "should store the digest length" do
39+
@s.digest_length(@metadata).should == 20
40+
end
41+
42+
it "should store the context size" do
43+
@s.context_size(@metadata).should == 129
44+
end
45+
end
46+
47+
describe "digest plugin" do
48+
before :each do
49+
@s = CApiDigestSpecs.new
50+
@digest = Digest::TestDigest.new
51+
52+
# A pointer to the CTX type defined in the extension for this spec. Digest does not make the context directly
53+
# accessible as part of its API. However, to ensure we are properly loading the plugin, it's useful to have
54+
# direct access to the context pointer to verify its contents.
55+
@context = FFI::Pointer.new(@s.context(@digest))
56+
end
57+
58+
it "should report the block length" do
59+
@digest.block_length.should == 40
60+
end
61+
62+
it "should report the digest length" do
63+
@digest.digest_length.should == 20
64+
end
65+
66+
it "should initialize the context" do
67+
# Our test plugin always writes the string "Initialized\n" when its init function is called.
68+
verify_context("Initialized\n")
69+
end
70+
71+
it "should update the digest" do
72+
@digest.update("hello world")
73+
74+
# Our test plugin always writes the string "Updated: <data>\n" when its update function is called.
75+
current = "Initialized\nUpdated: hello world"
76+
verify_context(current)
77+
78+
@digest << "blah"
79+
80+
current = "Initialized\nUpdated: hello worldUpdated: blah"
81+
verify_context(current)
82+
end
83+
84+
it "should finalize the digest" do
85+
@digest.update("")
86+
87+
finish_string = @digest.instance_eval { finish }
88+
89+
# We expect the plugin to write out the last `@digest.digest_length` bytes, followed by the string "Finished\n".
90+
#
91+
finish_string.should == "d\nUpdated: Finished\n"
92+
finish_string.encoding.should == Encoding::ASCII_8BIT
93+
end
94+
95+
it "should reset the context" do
96+
@digest.update("foo")
97+
verify_context("Initialized\nUpdated: foo")
98+
99+
@digest.reset
100+
verify_context("Initialized\n")
101+
end
102+
103+
def verify_context(current_body)
104+
# In the CTX type, the length of the current context contents is stored in the first byte.
105+
@context.read_char.should == current_body.bytesize
106+
107+
# After the size byte follows a NULL-terminated string.
108+
@context.get_string(1).should == current_body
109+
end
110+
end
111+
end

Diff for: spec/ruby/optional/capi/ext/digest_spec.c

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#include "ruby.h"
2+
#include "rubyspec.h"
3+
4+
#include "ruby/digest.h"
5+
6+
#ifdef __cplusplus
7+
extern "C" {
8+
#endif
9+
10+
#define DIGEST_LENGTH 20
11+
#define BLOCK_LENGTH 40
12+
13+
const char *init_string = "Initialized\n";
14+
const char *update_string = "Updated: ";
15+
const char *finish_string = "Finished\n";
16+
17+
#define PAYLOAD_SIZE 128
18+
19+
typedef struct CTX {
20+
uint8_t pos;
21+
char payload[PAYLOAD_SIZE];
22+
} CTX;
23+
24+
void* context = NULL;
25+
26+
int digest_spec_plugin_init(void *raw_ctx) {
27+
// Make the context accessible to tests. This isn't safe, but there's no way to access the context otherwise.
28+
context = raw_ctx;
29+
30+
struct CTX *ctx = (struct CTX *)raw_ctx;
31+
size_t len = strlen(init_string);
32+
33+
// Clear the payload since this init function will be invoked as part of the `reset` operation.
34+
memset(ctx->payload, 0, PAYLOAD_SIZE);
35+
36+
// Write a simple value we can verify in tests.
37+
// This is not what a real digest would do, but we're using a dummy digest plugin to test interactions.
38+
memcpy(ctx->payload, init_string, len);
39+
ctx->pos = (uint8_t) len;
40+
41+
return 1;
42+
}
43+
44+
void digest_spec_plugin_update(void *raw_ctx, unsigned char *ptr, size_t size) {
45+
struct CTX *ctx = (struct CTX *)raw_ctx;
46+
size_t update_str_len = strlen(update_string);
47+
48+
if (ctx->pos + update_str_len + size >= PAYLOAD_SIZE) {
49+
rb_raise(rb_eRuntimeError, "update size too large; reset the digest and write fewer updates");
50+
}
51+
52+
// Write the supplied value to the payload so it can be easily verified in test.
53+
// This is not what a real digest would do, but we're using a dummy digest plugin to test interactions.
54+
memcpy(ctx->payload + ctx->pos, update_string, update_str_len);
55+
ctx->pos += update_str_len;
56+
57+
memcpy(ctx->payload + ctx->pos, ptr, size);
58+
ctx->pos += size;
59+
60+
return;
61+
}
62+
63+
int digest_spec_plugin_finish(void *raw_ctx, unsigned char *ptr) {
64+
struct CTX *ctx = (struct CTX *)raw_ctx;
65+
size_t finish_string_len = strlen(finish_string);
66+
67+
// We're always going to write DIGEST_LENGTH bytes. In a real plugin, this would be the digest value. Here we
68+
// write out a text string in order to make validation in tests easier.
69+
//
70+
// In order to delineate the output more clearly from an `Digest#update` call, we always write out the
71+
// `finish_string` message. That leaves `DIGEST_LENGTH - finish_string_len` bytes to read out of the context.
72+
size_t context_bytes = DIGEST_LENGTH - finish_string_len;
73+
74+
memcpy(ptr, ctx->payload + (ctx->pos - context_bytes), context_bytes);
75+
memcpy(ptr + context_bytes, finish_string, finish_string_len);
76+
77+
return 1;
78+
}
79+
80+
static const rb_digest_metadata_t metadata = {
81+
// The RUBY_DIGEST_API_VERSION value comes from ruby/digest.h and may vary based on the Ruby being tested. Since
82+
// it isn't publicly exposed in the digest gem, we ignore for these tests. Either the test hard-codes an expected
83+
// value and is subject to breaking depending on the Ruby being run or we publicly expose `RUBY_DIGEST_API_VERSION`,
84+
// in which case the test would pass trivially.
85+
RUBY_DIGEST_API_VERSION,
86+
DIGEST_LENGTH,
87+
BLOCK_LENGTH,
88+
sizeof(CTX),
89+
(rb_digest_hash_init_func_t) digest_spec_plugin_init,
90+
(rb_digest_hash_update_func_t) digest_spec_plugin_update,
91+
(rb_digest_hash_finish_func_t) digest_spec_plugin_finish,
92+
};
93+
94+
// The `get_metadata_ptr` function is not publicly available in the digest gem. However, we need to use
95+
// to extract the `rb_digest_metadata_t*` value set up by the plugin so we reproduce and adjust the
96+
// definition here.
97+
//
98+
// Taken and adapted from https://github.com/ruby/digest/blob/v3.2.0/ext/digest/digest.c#L558-L568
99+
static rb_digest_metadata_t *
100+
get_metadata_ptr(VALUE obj)
101+
{
102+
rb_digest_metadata_t *algo;
103+
104+
#ifdef DIGEST_USE_RB_EXT_RESOLVE_SYMBOL
105+
// In the digest gem there is an additional data type check performed before reading the value out.
106+
// Since the type definition isn't public, we can't use it as part of a type check here so we omit it.
107+
// This is safe to do because this code is intended to only load digest plugins written as part of this test suite.
108+
algo = RTYPEDDATA_DATA(obj);
109+
#else
110+
# undef RUBY_UNTYPED_DATA_WARNING
111+
# define RUBY_UNTYPED_DATA_WARNING 0
112+
Data_Get_Struct(obj, rb_digest_metadata_t, algo);
113+
#endif
114+
115+
return algo;
116+
}
117+
118+
VALUE digest_spec_rb_digest_make_metadata(VALUE self) {
119+
return rb_digest_make_metadata(&metadata);
120+
}
121+
122+
VALUE digest_spec_block_length(VALUE self, VALUE meta) {
123+
rb_digest_metadata_t* algo = get_metadata_ptr(meta);
124+
125+
return SIZET2NUM(algo->block_len);
126+
}
127+
128+
VALUE digest_spec_digest_length(VALUE self, VALUE meta) {
129+
rb_digest_metadata_t* algo = get_metadata_ptr(meta);
130+
131+
return SIZET2NUM(algo->digest_len);
132+
}
133+
134+
VALUE digest_spec_context_size(VALUE self, VALUE meta) {
135+
rb_digest_metadata_t* algo = get_metadata_ptr(meta);
136+
137+
return SIZET2NUM(algo->ctx_size);
138+
}
139+
140+
#define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x)))
141+
142+
VALUE digest_spec_context(VALUE self, VALUE digest) {
143+
return PTR2NUM(context);
144+
}
145+
146+
void Init_digest_spec(void) {
147+
VALUE cls;
148+
149+
cls = rb_define_class("CApiDigestSpecs", rb_cObject);
150+
rb_define_method(cls, "rb_digest_make_metadata", digest_spec_rb_digest_make_metadata, 0);
151+
rb_define_method(cls, "block_length", digest_spec_block_length, 1);
152+
rb_define_method(cls, "digest_length", digest_spec_digest_length, 1);
153+
rb_define_method(cls, "context_size", digest_spec_context_size, 1);
154+
rb_define_method(cls, "context", digest_spec_context, 1);
155+
156+
VALUE mDigest, cDigest_Base, cDigest;
157+
158+
mDigest = rb_define_module("Digest");
159+
mDigest = rb_digest_namespace();
160+
cDigest_Base = rb_const_get(mDigest, rb_intern_const("Base"));
161+
162+
cDigest = rb_define_class_under(mDigest, "TestDigest", cDigest_Base);
163+
rb_iv_set(cDigest, "metadata", rb_digest_make_metadata(&metadata));
164+
}
165+
166+
#ifdef __cplusplus
167+
}
168+
#endif

0 commit comments

Comments
 (0)