mongodb/bson-ruby

View on GitHub
ext/bson/util.c

Summary

Maintainability
Test Coverage
/*
 * Copyright (C) 2009-2020 MongoDB Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "bson-native.h"

/**
 * Holds the machine id hash for object id generation.
 */
static char rb_bson_machine_id_hash[HOST_NAME_HASH_MAX];

/**
 * Holds a reference to the SecureRandom module, or Qnil if the modle is
 * not available.
 */
static VALUE pvt_SecureRandom = Qnil;

/**
 * Indicates whether or not the SecureRandom module responds to the
 * `random_number` method (depends on Ruby version).
 */
static int pvt_has_random_number = 0;

void rb_bson_generate_machine_id(VALUE rb_md5_class, char *rb_bson_machine_id)
{
  VALUE digest = rb_funcall(rb_md5_class, rb_intern("digest"), 1, rb_str_new2(rb_bson_machine_id));
  memcpy(rb_bson_machine_id_hash, RSTRING_PTR(digest), RSTRING_LEN(digest));
}

/**
 * Generate the next object id.
 *
 * Specification:
 * https://github.com/mongodb/specifications/blob/master/source/objectid.rst
 *
 * The ObjectID BSON type is a 12-byte value consisting of three different portions (fields):
 *   * a 4-byte value representing the seconds since the Unix epoch in the highest order bytes,
 *   * a 5-byte random number unique to a machine and process,
 *   * a 3-byte counter, starting with a random value.
 */
VALUE rb_bson_object_id_generator_next(int argc, VALUE* args, VALUE self)
{
  char bytes[12];
  uint32_t time_component;
  uint8_t* random_component;
  uint32_t counter_component;
  VALUE timestamp;
  VALUE rb_bson_object_id_class;

  rb_bson_object_id_class = pvt_const_get_2("BSON", "ObjectId");

  /* "Drivers SHOULD have an accessor method on an ObjectID class for
   * obtaining the timestamp value." */

  timestamp = rb_funcall(rb_bson_object_id_class, rb_intern("timestamp"), 0);
  time_component = BSON_UINT32_TO_BE(NUM2UINT(timestamp));

  /* "A 5-byte field consisting of a random value generated once per process.
   * This random value is unique to the machine and process.
   *
   * "Drivers MUST NOT have an accessor method on an ObjectID class for
   * obtaining this value."
   */

  random_component = pvt_get_object_id_random_value();

  /* shift left 8 bits, so that the first three bytes of the result are
   * the meaningful ones */
  counter_component = BSON_UINT32_TO_BE(rb_bson_object_id_counter << 8);

  memcpy(&bytes, &time_component, 4);
  memcpy(&bytes[4], random_component, 5);
  memcpy(&bytes[9], &counter_component, 3);

  rb_bson_object_id_counter = (rb_bson_object_id_counter + 1) % 0x1000000;

  return rb_str_new(bytes, 12);
}

/**
 * Reset the counter. This is purely as an aid for testing.
 *
 * @param [ Integer ] i the value to set the counter to (default is 0)
 */
VALUE rb_bson_object_id_generator_reset_counter(int argc, VALUE* args, VALUE self) {
  switch(argc) {
    case 0: rb_bson_object_id_counter = 0; break;
    case 1: rb_bson_object_id_counter = FIX2INT(args[0]); break;
    default: rb_raise(rb_eArgError, "Expected 0 or 1 arguments, got %d", argc);
  }

  return T_NIL;
}

/**
 * Returns a Ruby constant nested one level, e.g. BSON::Document.
 */
VALUE pvt_const_get_2(const char *c1, const char *c2) {
  return rb_const_get(rb_const_get(rb_cObject, rb_intern(c1)), rb_intern(c2));
}

/**
 * Returns a Ruby constant nested two levels, e.g. BSON::Regexp::Raw.
 */
VALUE pvt_const_get_3(const char *c1, const char *c2, const char *c3) {
  return rb_const_get(pvt_const_get_2(c1, c2), rb_intern(c3));
}

/**
 * Returns the value of the :mode option, or the default if the option is not
 * specified. Raises ArgumentError if the value is not one of nil or :bson.
 * A future version of bson-ruby is expected to also support :ruby and :ruby!
 * values. Returns one of the BSON_MODE_* values.
 */
int pvt_get_mode_option(int argc, VALUE *argv) {
  VALUE opts;
  VALUE mode;

  rb_scan_args(argc, argv, ":", &opts);
  if (NIL_P(opts)) {
    return BSON_MODE_DEFAULT;
  } else {
    mode = rb_hash_lookup(opts, ID2SYM(rb_intern("mode")));
    if (mode == Qnil) {
      return BSON_MODE_DEFAULT;
    } else if (mode == ID2SYM(rb_intern("bson"))) {
      return BSON_MODE_BSON;
    } else {
      rb_raise(rb_eArgError, "Invalid value for :mode option: %s",
        RSTRING_PTR(rb_funcall(mode, rb_intern("inspect"), 0)));
    }
  }
}

/**
 * Returns the random number associated with this host and process. If the
 * process ID changes (e.g. via fork), this will detect the change and
 * generate another random number.
 */
uint8_t* pvt_get_object_id_random_value() {
  static pid_t remembered_pid = 0;
  static uint8_t remembered_value[BSON_OBJECT_ID_RANDOM_VALUE_LENGTH] = {0};
  pid_t pid = getpid();

  if (remembered_pid != pid) {
    remembered_pid = pid;
    pvt_rand_buf(remembered_value, BSON_OBJECT_ID_RANDOM_VALUE_LENGTH, pid);
  }

  return remembered_value;
}

/**
 * Attempts to load the SecureRandom module
 */
VALUE pvt_load_secure_random(VALUE _arg) {
  rb_require("securerandom");
  pvt_SecureRandom = rb_const_get(rb_cObject, rb_intern("SecureRandom"));
  pvt_has_random_number = rb_respond_to(pvt_SecureRandom, rb_intern("random_number"));

  return Qnil;
}

/**
 * The fallback, if loading `securerandom` fails.
 */
VALUE pvt_rescue_load_secure_random(VALUE _arg, VALUE _exception) {
  pvt_SecureRandom = Qnil;

  return Qnil;
}

/**
 * Initializes the RNG.
 */
void pvt_init_rand() {
  // SecureRandom may fail to load because it's not present (LoadError), or
  // because it can't find a random device (NotImplementedError).
  rb_rescue2(pvt_load_secure_random, Qnil, pvt_rescue_load_secure_random, Qnil,
    rb_eLoadError, rb_eNotImpError, 0);
}

/**
 * Fills the buffer with random bytes. It prefers to use SecureRandom for
 * this, but in the very unlikely event that SecureRandom is not available,
 * it will fall back to a much-less-ideal generator using srand/rand.
 *
 * The `pid` argument is only used by the fallback, if SecureRandom is not
 * available.
 */
void pvt_rand_buf(uint8_t* bytes, int len, int pid) {
  if (pvt_SecureRandom != Qnil) {
    VALUE rb_bytes = rb_funcall(pvt_SecureRandom, rb_intern("bytes"), 1, INT2NUM(len));
    memcpy(bytes, StringValuePtr(rb_bytes), len);

  } else {
    time_t t;
    uint32_t seed;
    int ofs = 0;

    t = time(NULL);
    seed = ((uint32_t)t << 16) + ((uint32_t)pid % 0xFFFF);
    srand(seed);

    while (ofs < len) {
      int n = rand();
      unsigned remaining = len - ofs;

      if (remaining > sizeof(n)) remaining = sizeof(n);
      memcpy(bytes+ofs, &n, remaining);

      ofs += remaining;
    }
  }
}

/**
 * Returns a random integer between 0 and INT_MAX.
 */
int pvt_rand() {
  if (pvt_has_random_number) {
    VALUE result = rb_funcall(pvt_SecureRandom, rb_intern("random_number"), 1, INT2NUM(INT_MAX));
    return NUM2INT(result);

  } else if (pvt_SecureRandom != Qnil) {
    int result;
    VALUE rb_result = rb_funcall(pvt_SecureRandom, rb_intern("bytes"), 1, INT2NUM(sizeof(result)));
    memcpy(&result, StringValuePtr(rb_result), sizeof(result));
    return result;

  } else {
    srand((unsigned)time(NULL));
    return rand();
  }
}