Skip to content

Commit

Permalink
Merge pull request #2372 from herwinw/securerandom_random_bytes
Browse files Browse the repository at this point in the history
Implement Securerandom.random_bytes
  • Loading branch information
herwinw committed Dec 4, 2024
2 parents f0a682e + d1b5ce7 commit aa784ad
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 4 deletions.
79 changes: 79 additions & 0 deletions lib/securerandom.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include "natalie.hpp"
#include <random>

using namespace Natalie;

static Value generate_random(double min, double max) {
std::random_device rd;
std::uniform_real_distribution<double> random_number(min, max);
return new FloatObject { random_number(rd) };
}

static Value generate_random(nat_int_t min, nat_int_t max) {
std::random_device rd;
std::uniform_int_distribution<nat_int_t> random_number(min, max);
return Value::integer(random_number(rd));
}

Value SecureRandom_random_number(Env *env, Value self, Args &&args, Block *) {
args.ensure_argc_between(env, 0, 1);
auto arg = args.at(0, NilObject::the());
if (arg->is_nil()) {
return generate_random(0.0, 1.0);
} else {
if (arg->is_float()) {
double max = arg->as_float()->to_double();
if (max <= 0) {
env->raise("ArgumentError", "invalid argument - {}", arg->inspect_str(env));
}
return generate_random(0.0, max);
} else if (arg->is_range()) {
Value min = arg->as_range()->begin();
Value max = arg->as_range()->end();
// TODO: There can be different types of objects that respond to + and - (according to the docs)
// I'm not sure how we should handle those though (coerce via to_int or to_f?)
if (min->is_numeric() && max->is_numeric()) {
if (min.send(env, ">"_s, { max })->is_true()) {
env->raise("ArgumentError", "invalid argument - {}", arg->inspect_str(env));
}

if (min->is_float() || max->is_float()) {
double min_rand, max_rand;
if (min->is_float()) {
min_rand = min->as_float()->to_double();
} else {
min_rand = static_cast<double>(IntegerObject::convert_to_native_type<nat_int_t>(env, min));
}

if (max->is_float()) {
max_rand = max->as_float()->to_double();
} else {
max_rand = static_cast<double>(IntegerObject::convert_to_native_type<nat_int_t>(env, max));
}

return generate_random(min_rand, max_rand);
} else {
nat_int_t min_rand = IntegerObject::convert_to_native_type<nat_int_t>(env, min);
nat_int_t max_rand = IntegerObject::convert_to_native_type<nat_int_t>(env, max);

if (arg->as_range()->exclude_end()) {
max_rand -= 1;
}

return generate_random(min_rand, max_rand);
}
}
env->raise("ArgumentError", "bad value for range");
}

nat_int_t max = IntegerObject::convert_to_nat_int_t(env, arg);
if (max <= 0) {
env->raise("ArgumentError", "invalid argument - {}", arg->inspect_str(env));
}
return generate_random(0, max - 1);
}
}

Value init_securerandom(Env *env, Value self) {
return NilObject::the();
}
5 changes: 5 additions & 0 deletions lib/securerandom.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
require 'natalie/inline'
require 'securerandom.cpp'

require 'random/formatter'

class SecureRandom
extend Random::Formatter

__bind_static_method__ :random_number, :SecureRandom_random_number

def self.bytes(n)
Random.urandom(n)
end
Expand Down
107 changes: 107 additions & 0 deletions spec/library/securerandom/random_number_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
require_relative '../../spec_helper'
require_relative '../../core/random/shared/rand'

require 'securerandom'

describe "SecureRandom.random_number" do
it_behaves_like :random_number, :rand, SecureRandom
it_behaves_like :random_number, :random_number, SecureRandom

it "generates a random positive number smaller then the positive integer argument" do
(1..64).each do |idx|
num = SecureRandom.random_number(idx)
num.should be_kind_of(Integer)
0.should <= num
num.should < idx
end
end

it "generates a random (potentially bignum) integer value for bignum argument" do
max = 12345678901234567890
NATFIXME 'it generates a random (potentially bignum) integer value for bignum argument', exception: RangeError, message: "bignum too big to convert into `long'" do
11.times do
num = SecureRandom.random_number max
num.should be_kind_of(Integer)
0.should <= num
num.should < max
end
end
end

it "generates a random float number between 0.0 and 1.0 if no argument provided" do
64.times do
num = SecureRandom.random_number
num.should be_kind_of(Float)
0.0.should <= num
num.should < 1.0
end
end

it "generates a random value in given (integer) range limits" do
64.times do
num = SecureRandom.random_number 11...13
num.should be_kind_of(Integer)
11.should <= num
num.should < 13
end
end

it "generates a random value in given big (integer) range limits" do
lower = 12345678901234567890
upper = 12345678901234567890 + 5
NATFIXME 'it generates a random value in given big (integer) range limits', exception: RangeError, message: "bignum too big to convert into `long long int'" do
32.times do
num = SecureRandom.random_number lower..upper
num.should be_kind_of(Integer)
lower.should <= num
num.should <= upper
end
end
end

it "generates a random value in given (float) range limits" do
64.times do
num = SecureRandom.random_number 0.6..0.9
num.should be_kind_of(Float)
0.6.should <= num
num.should <= 0.9
end
end

it "generates a random float number between 0.0 and 1.0 if argument is negative" do
NATFIXME 'it generates a random float number between 0.0 and 1.0 if argument is negative', exception: ArgumentError, message: 'invalid argument - -10' do
num = SecureRandom.random_number(-10)
num.should be_kind_of(Float)
0.0.should <= num
num.should < 1.0
end
end

it "generates a random float number between 0.0 and 1.0 if argument is negative float" do
NATFIXME 'it generates a random float number between 0.0 and 1.0 if argument is negative float', exception: ArgumentError, message: 'invalid argument - -11.1' do
num = SecureRandom.random_number(-11.1)
num.should be_kind_of(Float)
0.0.should <= num
num.should < 1.0
end
end

it "generates different float numbers with subsequent invocations" do
# quick and dirty check, but good enough
values = []
256.times do
val = SecureRandom.random_number
# make sure the random values are not repeating
values.should_not include(val)
values << val
end
end

it "raises ArgumentError if the argument is non-numeric" do
NATFIXME 'it raises ArgumentError if the argument is non-numeric', exception: SpecFailedException do
-> {
SecureRandom.random_number(Object.new)
}.should raise_error(ArgumentError)
end
end
end
8 changes: 4 additions & 4 deletions src/random_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ Value RandomObject::rand(Env *env, Value arg) {
if (min->is_float()) {
min_rand = min->as_float()->to_double();
} else {
min_rand = (double)min->as_integer()->to_nat_int_t();
min_rand = static_cast<double>(IntegerObject::convert_to_native_type<nat_int_t>(env, min));
}

if (max->is_float()) {
max_rand = max->as_float()->to_double();
} else {
max_rand = (double)max->as_integer()->to_nat_int_t();
max_rand = static_cast<double>(IntegerObject::convert_to_native_type<nat_int_t>(env, max));
}

return generate_random(min_rand, max_rand);
} else {
nat_int_t min_rand = min->as_integer()->to_nat_int_t();
nat_int_t max_rand = max->as_integer()->to_nat_int_t();
auto min_rand = IntegerObject::convert_to_native_type<nat_int_t>(env, min);
auto max_rand = IntegerObject::convert_to_native_type<nat_int_t>(env, max);

if (arg->as_range()->exclude_end()) {
max_rand -= 1;
Expand Down

0 comments on commit aa784ad

Please sign in to comment.