Skip to content

Commit ac5296c

Browse files
committed
Add C API specs for digest plugins.
1 parent d1d94d8 commit ac5296c

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-0
lines changed

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

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

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

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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 * get_metadata_ptr(VALUE obj) {
100+
rb_digest_metadata_t *algo;
101+
102+
#ifdef DIGEST_USE_RB_EXT_RESOLVE_SYMBOL
103+
// In the digest gem there is an additional data type check performed before reading the value out.
104+
// Since the type definition isn't public, we can't use it as part of a type check here so we omit it.
105+
// This is safe to do because this code is intended to only load digest plugins written as part of this test suite.
106+
algo = RTYPEDDATA_DATA(obj);
107+
#else
108+
# undef RUBY_UNTYPED_DATA_WARNING
109+
# define RUBY_UNTYPED_DATA_WARNING 0
110+
Data_Get_Struct(obj, rb_digest_metadata_t, algo);
111+
#endif
112+
113+
return algo;
114+
}
115+
116+
VALUE digest_spec_rb_digest_make_metadata(VALUE self) {
117+
return rb_digest_make_metadata(&metadata);
118+
}
119+
120+
VALUE digest_spec_block_length(VALUE self, VALUE meta) {
121+
rb_digest_metadata_t* algo = get_metadata_ptr(meta);
122+
123+
return SIZET2NUM(algo->block_len);
124+
}
125+
126+
VALUE digest_spec_digest_length(VALUE self, VALUE meta) {
127+
rb_digest_metadata_t* algo = get_metadata_ptr(meta);
128+
129+
return SIZET2NUM(algo->digest_len);
130+
}
131+
132+
VALUE digest_spec_context_size(VALUE self, VALUE meta) {
133+
rb_digest_metadata_t* algo = get_metadata_ptr(meta);
134+
135+
return SIZET2NUM(algo->ctx_size);
136+
}
137+
138+
#define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x)))
139+
140+
VALUE digest_spec_context(VALUE self, VALUE digest) {
141+
return PTR2NUM(context);
142+
}
143+
144+
void Init_digest_spec(void) {
145+
VALUE cls;
146+
147+
cls = rb_define_class("CApiDigestSpecs", rb_cObject);
148+
rb_define_method(cls, "rb_digest_make_metadata", digest_spec_rb_digest_make_metadata, 0);
149+
rb_define_method(cls, "block_length", digest_spec_block_length, 1);
150+
rb_define_method(cls, "digest_length", digest_spec_digest_length, 1);
151+
rb_define_method(cls, "context_size", digest_spec_context_size, 1);
152+
rb_define_method(cls, "context", digest_spec_context, 1);
153+
154+
VALUE mDigest, cDigest_Base, cDigest;
155+
156+
mDigest = rb_define_module("Digest");
157+
mDigest = rb_digest_namespace();
158+
cDigest_Base = rb_const_get(mDigest, rb_intern_const("Base"));
159+
160+
cDigest = rb_define_class_under(mDigest, "TestDigest", cDigest_Base);
161+
rb_iv_set(cDigest, "metadata", rb_digest_make_metadata(&metadata));
162+
}
163+
164+
#ifdef __cplusplus
165+
}
166+
#endif

0 commit comments

Comments
 (0)