diff --git a/lib/cext/include/ruby/digest.h b/lib/cext/include/ruby/digest.h new file mode 100644 index 00000000000..68a3da5dd24 --- /dev/null +++ b/lib/cext/include/ruby/digest.h @@ -0,0 +1,73 @@ +/************************************************ + + digest.h - header file for ruby digest modules + + $Author$ + created at: Fri May 25 08:54:56 JST 2001 + + + Copyright (C) 2001-2006 Akinori MUSHA + + $RoughId: digest.h,v 1.3 2001/07/13 15:38:27 knu Exp $ + $Id$ + +************************************************/ + +#include "ruby.h" + +#define RUBY_DIGEST_API_VERSION 3 + +typedef int (*rb_digest_hash_init_func_t)(void *); +typedef void (*rb_digest_hash_update_func_t)(void *, unsigned char *, size_t); +typedef int (*rb_digest_hash_finish_func_t)(void *, unsigned char *); + +typedef struct { + int api_version; + size_t digest_len; + size_t block_len; + size_t ctx_size; + rb_digest_hash_init_func_t init_func; + rb_digest_hash_update_func_t update_func; + rb_digest_hash_finish_func_t finish_func; +} rb_digest_metadata_t; + +#define DEFINE_UPDATE_FUNC_FOR_UINT(name) \ +void \ +rb_digest_##name##_update(void *ctx, unsigned char *ptr, size_t size) \ +{ \ + const unsigned int stride = 16384; \ + \ + for (; size > stride; size -= stride, ptr += stride) { \ + name##_Update(ctx, ptr, stride); \ + } \ + /* Since size <= stride, size should fit into an unsigned int */ \ + if (size > 0) name##_Update(ctx, ptr, (unsigned int)size); \ +} + +#define DEFINE_FINISH_FUNC_FROM_FINAL(name) \ +int \ +rb_digest_##name##_finish(void *ctx, unsigned char *ptr) \ +{ \ + return name##_Final(ptr, ctx); \ +} + +static inline VALUE +rb_digest_namespace(void) +{ + rb_require("digest"); + return rb_path2class("Digest"); +} + +static inline ID +rb_id_metadata(void) +{ + return rb_intern_const("metadata"); +} + +static inline VALUE +rb_digest_make_metadata(const rb_digest_metadata_t *meta) +{ +#undef RUBY_UNTYPED_DATA_WARNING +#define RUBY_UNTYPED_DATA_WARNING 0 + return rb_obj_freeze(Data_Wrap_Struct(0, 0, 0, (void *)meta)); +} diff --git a/lib/cext/include/truffleruby/truffleruby-abi-version.h b/lib/cext/include/truffleruby/truffleruby-abi-version.h index f9d0d81d425..3a388ca1ca8 100644 --- a/lib/cext/include/truffleruby/truffleruby-abi-version.h +++ b/lib/cext/include/truffleruby/truffleruby-abi-version.h @@ -20,6 +20,6 @@ // $RUBY_VERSION must be the same as TruffleRuby.LANGUAGE_VERSION. // $ABI_NUMBER starts at 1 and is incremented for every ABI-incompatible change. -#define TRUFFLERUBY_ABI_VERSION "3.3.7.2" +#define TRUFFLERUBY_ABI_VERSION "3.3.7.3" #endif diff --git a/lib/truffle/digest.rb b/lib/truffle/digest.rb index 08068151f43..cb21b805ef5 100644 --- a/lib/truffle/digest.rb +++ b/lib/truffle/digest.rb @@ -13,6 +13,24 @@ # under LICENSE.RUBY as it is derived from lib/ruby/stdlib/digest.rb. require_relative 'digest/version' +require_relative 'ffi' + +module Truffle + class Digest + module Foreign + class RbDigestMetadata < ::FFI::Struct + layout :api_version, :int, + :digest_len, :size_t, + :block_len, :size_t, + :ctx_size, :size_t, + :init_func, ::FFI::FunctionType.new(:int, [:pointer]), + :update_func, ::FFI::FunctionType.new(:void, [:pointer, :pointer, :size_t]), + :finish_func, ::FFI::FunctionType.new(:int, [:pointer, :pointer]) + end + end + end +end + module Digest @@ -170,6 +188,12 @@ def self.hexdigest(str, *args) end class Base < ::Digest::Class + def self.inherited(klass) + unless %w[Digest::MD5 Digest::SHA1 Digest::SHA2 Digest::SHA256 Digest::SHA384 Digest::SHA512].include?(klass.name) + klass.include(Digest::Plugin) + end + end + def block_length Truffle::Digest.digest_block_length @digest end @@ -186,9 +210,65 @@ def reset def update(str) str = StringValue(str) Truffle::Digest.update(@digest, str) + + self + end + alias_method :<<, :update + end + + module Plugin + attr_reader :metadata + + def initialize + super + metadata_wrapped = Primitive.object_ivar_get(Primitive.class(self), :metadata) + metadata_pointer = Primitive.object_hidden_var_get(metadata_wrapped, Truffle::CExt::DATA_STRUCT).data + @metadata = Truffle::Digest::Foreign::RbDigestMetadata.new(FFI::Pointer.new(metadata_pointer)) + + reset + end + + def initialize_copy(from) + @metadata = from.metadata + @context = from.context.clone + + self + end + + def context + @context ||= Truffle::FFI::MemoryPointer.new(:uchar, metadata[:ctx_size]) + end + + def block_length + metadata[:block_len] + end + + def digest_length + metadata[:digest_len] + end + + def update(str) + metadata[:update_func].call(context, Truffle::CExt.string_to_ffi_pointer_inplace(str), str.bytesize) + self end alias_method :<<, :update + + def finish + str = Truffle::FFI::MemoryPointer.new(:uchar, metadata[:digest_len], false) + metadata[:finish_func].call(context, str) + + reset + + Primitive.pointer_read_bytes str.address, metadata[:digest_len] + end + + def reset + @context = nil + if metadata[:init_func].call(context) != 1 + raise RuntimeError, 'Digest initialization failed' + end + end end class MD5 < Base diff --git a/lib/truffle/truffle/cext_ruby.rb b/lib/truffle/truffle/cext_ruby.rb index 1cc3b81e03a..f433e7846c9 100644 --- a/lib/truffle/truffle/cext_ruby.rb +++ b/lib/truffle/truffle/cext_ruby.rb @@ -13,7 +13,7 @@ module Truffle::CExt # Methods defined in this file are not considered as Ruby code implementing MRI C parts, # see org.truffleruby.cext.CExtNodes.BlockProcNode - # methods defined with rb_define_method are normal Ruby methods therefore they cannot be defined in the cext.rb file + # methods defined with rb_define_method are normal Ruby methods therefore they cannot be defined in the cext.rb # file because blocks passed as arguments would be skipped by org.truffleruby.cext.CExtNodes.BlockProcNode def rb_define_method(mod, name, function, argc) if argc < -2 or 15 < argc diff --git a/spec/ruby/optional/capi/digest_spec.rb b/spec/ruby/optional/capi/digest_spec.rb new file mode 100644 index 00000000000..58476e4c816 --- /dev/null +++ b/spec/ruby/optional/capi/digest_spec.rb @@ -0,0 +1,99 @@ +require_relative 'spec_helper' + +require 'fiddle' + +load_extension('digest') + +describe "C-API Digest functions" do + before :each do + @s = CApiDigestSpecs.new + end + + describe "rb_digest_make_metadata" do + before :each do + @metadata = @s.rb_digest_make_metadata + end + + it "should store the block length" do + @s.block_length(@metadata).should == 40 + end + + it "should store the digest length" do + @s.digest_length(@metadata).should == 20 + end + + it "should store the context size" do + @s.context_size(@metadata).should == 129 + end + end + + describe "digest plugin" do + before :each do + @s = CApiDigestSpecs.new + @digest = Digest::TestDigest.new + + # A pointer to the CTX type defined in the extension for this spec. Digest does not make the context directly + # accessible as part of its API. However, to ensure we are properly loading the plugin, it's useful to have + # direct access to the context pointer to verify its contents. + @context = Fiddle::Pointer.new(@s.context(@digest)) + end + + it "should report the block length" do + @digest.block_length.should == 40 + end + + it "should report the digest length" do + @digest.digest_length.should == 20 + end + + it "should initialize the context" do + # Our test plugin always writes the string "Initialized\n" when its init function is called. + verify_context("Initialized\n") + end + + it "should update the digest" do + @digest.update("hello world") + + # Our test plugin always writes the string "Updated: \n" when its update function is called. + current = "Initialized\nUpdated: hello world" + verify_context(current) + + @digest << "blah" + + current = "Initialized\nUpdated: hello worldUpdated: blah" + verify_context(current) + end + + it "should finalize the digest" do + @digest.update("") + + finish_string = @digest.instance_eval { finish } + + # We expect the plugin to write out the last `@digest.digest_length` bytes, followed by the string "Finished\n". + # + finish_string.should == "d\nUpdated: Finished\n" + finish_string.encoding.should == Encoding::ASCII_8BIT + end + + it "should reset the context" do + @digest.update("foo") + verify_context("Initialized\nUpdated: foo") + + @digest.reset + + # The context will be recreated as a result of the `reset` so we must fetch the latest context pointer. + @context = Fiddle::Pointer.new(@s.context(@digest)) + + verify_context("Initialized\n") + end + + def verify_context(current_body) + # In the CTX type, the length of the current context contents is stored in the first byte. + byte_count = @context[0] + byte_count.should == current_body.bytesize + + # After the size byte follows a string. + @context[1, byte_count].should == current_body + end + end +end \ No newline at end of file diff --git a/spec/ruby/optional/capi/ext/digest_spec.c b/spec/ruby/optional/capi/ext/digest_spec.c new file mode 100644 index 00000000000..683ec96b674 --- /dev/null +++ b/spec/ruby/optional/capi/ext/digest_spec.c @@ -0,0 +1,166 @@ +#include "ruby.h" +#include "rubyspec.h" + +#include "ruby/digest.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define DIGEST_LENGTH 20 +#define BLOCK_LENGTH 40 + +const char *init_string = "Initialized\n"; +const char *update_string = "Updated: "; +const char *finish_string = "Finished\n"; + +#define PAYLOAD_SIZE 128 + +typedef struct CTX { + uint8_t pos; + char payload[PAYLOAD_SIZE]; +} CTX; + +void* context = NULL; + +int digest_spec_plugin_init(void *raw_ctx) { + // Make the context accessible to tests. This isn't safe, but there's no way to access the context otherwise. + context = raw_ctx; + + struct CTX *ctx = (struct CTX *)raw_ctx; + size_t len = strlen(init_string); + + // Clear the payload since this init function will be invoked as part of the `reset` operation. + memset(ctx->payload, 0, PAYLOAD_SIZE); + + // Write a simple value we can verify in tests. + // This is not what a real digest would do, but we're using a dummy digest plugin to test interactions. + memcpy(ctx->payload, init_string, len); + ctx->pos = (uint8_t) len; + + return 1; +} + +void digest_spec_plugin_update(void *raw_ctx, unsigned char *ptr, size_t size) { + struct CTX *ctx = (struct CTX *)raw_ctx; + size_t update_str_len = strlen(update_string); + + if (ctx->pos + update_str_len + size >= PAYLOAD_SIZE) { + rb_raise(rb_eRuntimeError, "update size too large; reset the digest and write fewer updates"); + } + + // Write the supplied value to the payload so it can be easily verified in test. + // This is not what a real digest would do, but we're using a dummy digest plugin to test interactions. + memcpy(ctx->payload + ctx->pos, update_string, update_str_len); + ctx->pos += update_str_len; + + memcpy(ctx->payload + ctx->pos, ptr, size); + ctx->pos += size; + + return; +} + +int digest_spec_plugin_finish(void *raw_ctx, unsigned char *ptr) { + struct CTX *ctx = (struct CTX *)raw_ctx; + size_t finish_string_len = strlen(finish_string); + + // We're always going to write DIGEST_LENGTH bytes. In a real plugin, this would be the digest value. Here we + // write out a text string in order to make validation in tests easier. + // + // In order to delineate the output more clearly from an `Digest#update` call, we always write out the + // `finish_string` message. That leaves `DIGEST_LENGTH - finish_string_len` bytes to read out of the context. + size_t context_bytes = DIGEST_LENGTH - finish_string_len; + + memcpy(ptr, ctx->payload + (ctx->pos - context_bytes), context_bytes); + memcpy(ptr + context_bytes, finish_string, finish_string_len); + + return 1; +} + +static const rb_digest_metadata_t metadata = { + // The RUBY_DIGEST_API_VERSION value comes from ruby/digest.h and may vary based on the Ruby being tested. Since + // it isn't publicly exposed in the digest gem, we ignore for these tests. Either the test hard-codes an expected + // value and is subject to breaking depending on the Ruby being run or we publicly expose `RUBY_DIGEST_API_VERSION`, + // in which case the test would pass trivially. + RUBY_DIGEST_API_VERSION, + DIGEST_LENGTH, + BLOCK_LENGTH, + sizeof(CTX), + (rb_digest_hash_init_func_t) digest_spec_plugin_init, + (rb_digest_hash_update_func_t) digest_spec_plugin_update, + (rb_digest_hash_finish_func_t) digest_spec_plugin_finish, +}; + +// The `get_metadata_ptr` function is not publicly available in the digest gem. However, we need to use +// to extract the `rb_digest_metadata_t*` value set up by the plugin so we reproduce and adjust the +// definition here. +// +// Taken and adapted from https://github.com/ruby/digest/blob/v3.2.0/ext/digest/digest.c#L558-L568 +static rb_digest_metadata_t * get_metadata_ptr(VALUE obj) { + rb_digest_metadata_t *algo; + +#ifdef DIGEST_USE_RB_EXT_RESOLVE_SYMBOL + // In the digest gem there is an additional data type check performed before reading the value out. + // Since the type definition isn't public, we can't use it as part of a type check here so we omit it. + // This is safe to do because this code is intended to only load digest plugins written as part of this test suite. + algo = RTYPEDDATA_DATA(obj); +#else +# undef RUBY_UNTYPED_DATA_WARNING +# define RUBY_UNTYPED_DATA_WARNING 0 + Data_Get_Struct(obj, rb_digest_metadata_t, algo); +#endif + + return algo; +} + +VALUE digest_spec_rb_digest_make_metadata(VALUE self) { + return rb_digest_make_metadata(&metadata); +} + +VALUE digest_spec_block_length(VALUE self, VALUE meta) { + rb_digest_metadata_t* algo = get_metadata_ptr(meta); + + return SIZET2NUM(algo->block_len); +} + +VALUE digest_spec_digest_length(VALUE self, VALUE meta) { + rb_digest_metadata_t* algo = get_metadata_ptr(meta); + + return SIZET2NUM(algo->digest_len); +} + +VALUE digest_spec_context_size(VALUE self, VALUE meta) { + rb_digest_metadata_t* algo = get_metadata_ptr(meta); + + return SIZET2NUM(algo->ctx_size); +} + +#define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x))) + +VALUE digest_spec_context(VALUE self, VALUE digest) { + return PTR2NUM(context); +} + +void Init_digest_spec(void) { + VALUE cls; + + cls = rb_define_class("CApiDigestSpecs", rb_cObject); + rb_define_method(cls, "rb_digest_make_metadata", digest_spec_rb_digest_make_metadata, 0); + rb_define_method(cls, "block_length", digest_spec_block_length, 1); + rb_define_method(cls, "digest_length", digest_spec_digest_length, 1); + rb_define_method(cls, "context_size", digest_spec_context_size, 1); + rb_define_method(cls, "context", digest_spec_context, 1); + + VALUE mDigest, cDigest_Base, cDigest; + + mDigest = rb_define_module("Digest"); + mDigest = rb_digest_namespace(); + cDigest_Base = rb_const_get(mDigest, rb_intern_const("Base")); + + cDigest = rb_define_class_under(mDigest, "TestDigest", cDigest_Base); + rb_iv_set(cDigest, "metadata", rb_digest_make_metadata(&metadata)); +} + +#ifdef __cplusplus +} +#endif diff --git a/spec/truffle/methods/Digest::Base.singleton_class.txt b/spec/truffle/methods/Digest::Base.singleton_class.txt index e69de29bb2d..ecd377a4f31 100644 --- a/spec/truffle/methods/Digest::Base.singleton_class.txt +++ b/spec/truffle/methods/Digest::Base.singleton_class.txt @@ -0,0 +1 @@ +inherited \ No newline at end of file