Skip to content

Commit ed2974c

Browse files
committed
[GR-19220] Support digest plugins (#3822)
PullRequest: truffleruby/4507
2 parents ad29b9e + 0b996c8 commit ed2974c

File tree

10 files changed

+424
-2
lines changed

10 files changed

+424
-2
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Compatibility:
1515
* Implement `ObjectSpace::WeakKeyMap` (#3681, @andrykonchin).
1616
* Fix `String#slice` called with negative offset (@andrykonchin).
1717
* Fix explicitly inherited `Struct` subclasses and don't provide `#members` method (#3802, @andrykonchin).
18+
* Support Digest plugins (#1390, @nirvdrum).
1819

1920
Performance:
2021

Diff for: lib/cext/include/ruby/digest.h

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/************************************************
2+
3+
digest.h - header file for ruby digest modules
4+
5+
$Author$
6+
created at: Fri May 25 08:54:56 JST 2001
7+
8+
9+
Copyright (C) 2001-2006 Akinori MUSHA
10+
11+
$RoughId: digest.h,v 1.3 2001/07/13 15:38:27 knu Exp $
12+
$Id$
13+
14+
************************************************/
15+
16+
#include "ruby.h"
17+
18+
#define RUBY_DIGEST_API_VERSION 3
19+
20+
typedef int (*rb_digest_hash_init_func_t)(void *);
21+
typedef void (*rb_digest_hash_update_func_t)(void *, unsigned char *, size_t);
22+
typedef int (*rb_digest_hash_finish_func_t)(void *, unsigned char *);
23+
24+
typedef struct {
25+
int api_version;
26+
size_t digest_len;
27+
size_t block_len;
28+
size_t ctx_size;
29+
rb_digest_hash_init_func_t init_func;
30+
rb_digest_hash_update_func_t update_func;
31+
rb_digest_hash_finish_func_t finish_func;
32+
} rb_digest_metadata_t;
33+
34+
#define DEFINE_UPDATE_FUNC_FOR_UINT(name) \
35+
void \
36+
rb_digest_##name##_update(void *ctx, unsigned char *ptr, size_t size) \
37+
{ \
38+
const unsigned int stride = 16384; \
39+
\
40+
for (; size > stride; size -= stride, ptr += stride) { \
41+
name##_Update(ctx, ptr, stride); \
42+
} \
43+
/* Since size <= stride, size should fit into an unsigned int */ \
44+
if (size > 0) name##_Update(ctx, ptr, (unsigned int)size); \
45+
}
46+
47+
#define DEFINE_FINISH_FUNC_FROM_FINAL(name) \
48+
int \
49+
rb_digest_##name##_finish(void *ctx, unsigned char *ptr) \
50+
{ \
51+
return name##_Final(ptr, ctx); \
52+
}
53+
54+
static inline VALUE
55+
rb_digest_namespace(void)
56+
{
57+
rb_require("digest");
58+
return rb_path2class("Digest");
59+
}
60+
61+
static inline ID
62+
rb_id_metadata(void)
63+
{
64+
return rb_intern_const("metadata");
65+
}
66+
67+
static inline VALUE
68+
rb_digest_make_metadata(const rb_digest_metadata_t *meta)
69+
{
70+
#undef RUBY_UNTYPED_DATA_WARNING
71+
#define RUBY_UNTYPED_DATA_WARNING 0
72+
return rb_obj_freeze(Data_Wrap_Struct(0, 0, 0, (void *)meta));
73+
}

Diff for: lib/cext/include/truffleruby/truffleruby-abi-version.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
// $RUBY_VERSION must be the same as TruffleRuby.LANGUAGE_VERSION.
2121
// $ABI_NUMBER starts at 1 and is incremented for every ABI-incompatible change.
2222

23-
#define TRUFFLERUBY_ABI_VERSION "3.3.7.2"
23+
#define TRUFFLERUBY_ABI_VERSION "3.3.7.3"
2424

2525
#endif

Diff for: lib/truffle/digest.rb

+80
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@
1313
# under LICENSE.RUBY as it is derived from lib/ruby/stdlib/digest.rb.
1414

1515
require_relative 'digest/version'
16+
require_relative 'ffi'
17+
18+
module Truffle
19+
class Digest
20+
module Foreign
21+
class RbDigestMetadata < ::FFI::Struct
22+
layout :api_version, :int,
23+
:digest_len, :size_t,
24+
:block_len, :size_t,
25+
:ctx_size, :size_t,
26+
:init_func, ::FFI::FunctionType.new(:int, [:pointer]),
27+
:update_func, ::FFI::FunctionType.new(:void, [:pointer, :pointer, :size_t]),
28+
:finish_func, ::FFI::FunctionType.new(:int, [:pointer, :pointer])
29+
end
30+
end
31+
end
32+
end
33+
1634

1735
module Digest
1836

@@ -170,6 +188,12 @@ def self.hexdigest(str, *args)
170188
end
171189

172190
class Base < ::Digest::Class
191+
def self.inherited(klass)
192+
unless %w[Digest::MD5 Digest::SHA1 Digest::SHA2 Digest::SHA256 Digest::SHA384 Digest::SHA512].include?(klass.name)
193+
klass.include(Digest::Plugin)
194+
end
195+
end
196+
173197
def block_length
174198
Truffle::Digest.digest_block_length @digest
175199
end
@@ -186,9 +210,65 @@ def reset
186210
def update(str)
187211
str = StringValue(str)
188212
Truffle::Digest.update(@digest, str)
213+
214+
self
215+
end
216+
alias_method :<<, :update
217+
end
218+
219+
module Plugin
220+
attr_reader :metadata
221+
222+
def initialize
223+
super
224+
metadata_wrapped = Primitive.object_ivar_get(Primitive.class(self), :metadata)
225+
metadata_pointer = Primitive.object_hidden_var_get(metadata_wrapped, Truffle::CExt::DATA_STRUCT).data
226+
@metadata = Truffle::Digest::Foreign::RbDigestMetadata.new(FFI::Pointer.new(metadata_pointer))
227+
228+
reset
229+
end
230+
231+
def initialize_copy(from)
232+
@metadata = from.metadata
233+
@context = from.context.clone
234+
235+
self
236+
end
237+
238+
def context
239+
@context ||= Truffle::FFI::MemoryPointer.new(:uchar, metadata[:ctx_size])
240+
end
241+
242+
def block_length
243+
metadata[:block_len]
244+
end
245+
246+
def digest_length
247+
metadata[:digest_len]
248+
end
249+
250+
def update(str)
251+
metadata[:update_func].call(context, Truffle::CExt.string_to_ffi_pointer_inplace(str), str.bytesize)
252+
189253
self
190254
end
191255
alias_method :<<, :update
256+
257+
def finish
258+
str = Truffle::FFI::MemoryPointer.new(:uchar, metadata[:digest_len], false)
259+
metadata[:finish_func].call(context, str)
260+
261+
reset
262+
263+
Primitive.pointer_read_bytes str.address, metadata[:digest_len]
264+
end
265+
266+
def reset
267+
@context = nil
268+
if metadata[:init_func].call(context) != 1
269+
raise RuntimeError, 'Digest initialization failed'
270+
end
271+
end
192272
end
193273

194274
class MD5 < Base

Diff for: lib/truffle/truffle/cext_ruby.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module Truffle::CExt
1313
# Methods defined in this file are not considered as Ruby code implementing MRI C parts,
1414
# see org.truffleruby.cext.CExtNodes.BlockProcNode
1515

16-
# methods defined with rb_define_method are normal Ruby methods therefore they cannot be defined in the cext.rb file
16+
# methods defined with rb_define_method are normal Ruby methods therefore they cannot be defined in the cext.rb
1717
# file because blocks passed as arguments would be skipped by org.truffleruby.cext.CExtNodes.BlockProcNode
1818
def rb_define_method(mod, name, function, argc)
1919
if argc < -2 or 15 < argc

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

0 commit comments

Comments
 (0)