diff --git a/Rakefile b/Rakefile index 2c7010479..4427cece2 100644 --- a/Rakefile +++ b/Rakefile @@ -49,7 +49,6 @@ end require_relative "perf/bench" unless jruby? -RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new(:rspec) if jruby? @@ -74,8 +73,7 @@ task :clean_all => :clean do end end -task :ext_spec => :compile do - ENV["WITH_EXT"] = "C" +task :spec => :compile do Rake::Task["rspec"].invoke end @@ -110,12 +108,6 @@ namespace :benchmark do require "bson" benchmark! end - - task :profile => :compile do - puts "Profiling with native extensions..." - require "bson" - profile! - end end -task :default => [ :clean_all, :spec, :ext_spec ] +task :default => [ :clean_all, :spec ] diff --git a/ext/bson/native-endian.h b/ext/bson/native-endian.h new file mode 100644 index 000000000..61f533264 --- /dev/null +++ b/ext/bson/native-endian.h @@ -0,0 +1,118 @@ +// "License": Public Domain +// I, Mathias PanzenbГ¶ck, place this file hereby into the public domain. Use it at your own risk for whatever you like. +// In case there are jurisdictions that don't support putting things in the public domain you can also consider it to +// be "dual licensed" under the BSD, MIT and Apache licenses, if you want to. This code is trivial anyway. Consider it +// an example on how to get the endian conversion functions on different platforms. + +#ifndef PORTABLE_ENDIAN_H__ +#define PORTABLE_ENDIAN_H__ + +#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__) + +# define __WINDOWS__ + +#endif + +#if defined(__linux__) || defined(__CYGWIN__) + +# include + +#elif defined(__APPLE__) + +# include + +# define htobe16(x) OSSwapHostToBigInt16(x) +# define htole16(x) OSSwapHostToLittleInt16(x) +# define be16toh(x) OSSwapBigToHostInt16(x) +# define le16toh(x) OSSwapLittleToHostInt16(x) + +# define htobe32(x) OSSwapHostToBigInt32(x) +# define htole32(x) OSSwapHostToLittleInt32(x) +# define be32toh(x) OSSwapBigToHostInt32(x) +# define le32toh(x) OSSwapLittleToHostInt32(x) + +# define htobe64(x) OSSwapHostToBigInt64(x) +# define htole64(x) OSSwapHostToLittleInt64(x) +# define be64toh(x) OSSwapBigToHostInt64(x) +# define le64toh(x) OSSwapLittleToHostInt64(x) + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__OpenBSD__) + +# include + +#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) + +# include + +# define be16toh(x) betoh16(x) +# define le16toh(x) letoh16(x) + +# define be32toh(x) betoh32(x) +# define le32toh(x) letoh32(x) + +# define be64toh(x) betoh64(x) +# define le64toh(x) letoh64(x) + +#elif defined(__WINDOWS__) + +# include +# include + +# if BYTE_ORDER == LITTLE_ENDIAN + +# define htobe16(x) htons(x) +# define htole16(x) (x) +# define be16toh(x) ntohs(x) +# define le16toh(x) (x) + +# define htobe32(x) htonl(x) +# define htole32(x) (x) +# define be32toh(x) ntohl(x) +# define le32toh(x) (x) + +# define htobe64(x) htonll(x) +# define htole64(x) (x) +# define be64toh(x) ntohll(x) +# define le64toh(x) (x) + +# elif BYTE_ORDER == BIG_ENDIAN + + /* that would be xbox 360 */ +# define htobe16(x) (x) +# define htole16(x) __builtin_bswap16(x) +# define be16toh(x) (x) +# define le16toh(x) __builtin_bswap16(x) + +# define htobe32(x) (x) +# define htole32(x) __builtin_bswap32(x) +# define be32toh(x) (x) +# define le32toh(x) __builtin_bswap32(x) + +# define htobe64(x) (x) +# define htole64(x) __builtin_bswap64(x) +# define be64toh(x) (x) +# define le64toh(x) __builtin_bswap64(x) + +# else + +# error byte order not supported + +# endif + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#else + +# error platform not supported + +#endif + +#endif diff --git a/ext/bson/native.c b/ext/bson/native.c index 1b60365c3..cc0fe6098 100644 --- a/ext/bson/native.c +++ b/ext/bson/native.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2013 MongoDB Inc. + * Copyright (C) 2009-2015 MongoDB Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,733 +13,692 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifdef _WIN32 -#include -#else -#include -#include -#endif - -#include -#include -#include #include +#include +#include +#include +#include "native-endian.h" -/** - * For 64 byte systems we convert to longs, for 32 byte systems we convert - * to a long long. - * - * @since 2.0.0 - */ -#if SIZEOF_LONG == 8 -#define NUM2INT64(v) NUM2LONG(v) -#define INT642NUM(v) LONG2NUM(v) -#else -#define NUM2INT64(v) NUM2LL(v) -#define INT642NUM(v) LL2NUM(v) -#endif - -/** - * Ruby 1.8.7 does not define DBL2NUM, so we define it if it's not there. - * - * @since 2.0.0 - */ -#ifndef DBL2NUM -#define DBL2NUM(dbl) rb_float_new(dbl) -#endif +#define BSON_BYTE_BUFFER_SIZE 512 -/** - * Define the max hostname hash length constant if nonexistant. - * - * @since 3.2.0 - */ #ifndef HOST_NAME_HASH_MAX #define HOST_NAME_HASH_MAX 256 #endif -/** - * Define index sizes for array serialization. - * - * @since 2.0.0 - */ -#define BSON_INDEX_SIZE 1024 -#define BSON_INDEX_CHAR_SIZE 5 -#define INTEGER_CHAR_SIZE 22 +typedef struct { + size_t size; + size_t write_position; + size_t read_position; + char buffer[BSON_BYTE_BUFFER_SIZE]; + char *b_ptr; +} byte_buffer_t; + +#define READ_PTR(byte_buffer_ptr) \ + (byte_buffer_ptr->b_ptr + byte_buffer_ptr->read_position) + +#define READ_SIZE(byte_buffer_ptr) \ + (byte_buffer_ptr->write_position - byte_buffer_ptr->read_position) + +#define WRITE_PTR(byte_buffer_ptr) \ + (byte_buffer_ptr->b_ptr + byte_buffer_ptr->write_position) + +#define ENSURE_BSON_WRITE(buffer_ptr, length) \ + { if (buffer_ptr->write_position + length > buffer_ptr->size) rb_bson_expand_buffer(buffer_ptr, length); } + +#define ENSURE_BSON_READ(buffer_ptr, length) \ + { if (buffer_ptr->read_position + length > buffer_ptr->write_position) \ + rb_raise(rb_eRangeError, "Attempted to read %zu bytes, but only %zu bytes remain", (size_t)length, READ_SIZE(buffer_ptr)); } + +static VALUE rb_bson_byte_buffer_allocate(VALUE klass); +static VALUE rb_bson_byte_buffer_initialize(int argc, VALUE *argv, VALUE self); +static VALUE rb_bson_byte_buffer_length(VALUE self); +static VALUE rb_bson_byte_buffer_get_byte(VALUE self); +static VALUE rb_bson_byte_buffer_get_bytes(VALUE self, VALUE i); +static VALUE rb_bson_byte_buffer_get_cstring(VALUE self); +static VALUE rb_bson_byte_buffer_get_double(VALUE self); +static VALUE rb_bson_byte_buffer_get_int32(VALUE self); +static VALUE rb_bson_byte_buffer_get_int64(VALUE self); +static VALUE rb_bson_byte_buffer_get_string(VALUE self); +static VALUE rb_bson_byte_buffer_put_byte(VALUE self, VALUE byte); +static VALUE rb_bson_byte_buffer_put_bytes(VALUE self, VALUE bytes); +static VALUE rb_bson_byte_buffer_put_cstring(VALUE self, VALUE string); +static VALUE rb_bson_byte_buffer_put_double(VALUE self, VALUE f); +static VALUE rb_bson_byte_buffer_put_int32(VALUE self, VALUE i); +static VALUE rb_bson_byte_buffer_put_int64(VALUE self, VALUE i); +static VALUE rb_bson_byte_buffer_put_string(VALUE self, VALUE string); +static VALUE rb_bson_byte_buffer_read_position(VALUE self); +static VALUE rb_bson_byte_buffer_replace_int32(VALUE self, VALUE index, VALUE i); +static VALUE rb_bson_byte_buffer_write_position(VALUE self); +static VALUE rb_bson_byte_buffer_to_s(VALUE self); +static VALUE rb_bson_object_id_generator_next(int argc, VALUE* args, VALUE self); + +static size_t rb_bson_byte_buffer_memsize(const void *ptr); +static void rb_bson_byte_buffer_free(void *ptr); +static void rb_bson_expand_buffer(byte_buffer_t* buffer_ptr, size_t length); +static bool rb_bson_utf8_validate(const char *utf8, size_t utf8_len, bool allow_null); + +static const rb_data_type_t rb_byte_buffer_data_type = { + "bson/byte_buffer", + { NULL, rb_bson_byte_buffer_free, rb_bson_byte_buffer_memsize }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY +}; /** - * Constant for the intetger array indexes. - * - * @since 2.0.0 + * Holds the machine id hash for object id generation. */ -static char rb_bson_array_indexes[BSON_INDEX_SIZE][BSON_INDEX_CHAR_SIZE]; +static char rb_bson_machine_id_hash[HOST_NAME_HASH_MAX]; /** - * BSON::UTF8 - * - * @since 2.0.0 + * The counter for incrementing object ids. */ -static VALUE rb_bson_utf8_string; +static unsigned int rb_bson_object_id_counter = 0; /** - * Set the UTC string method for reference at load. - * - * @since 2.0.0 + * Initialize the native extension. */ -static VALUE rb_utc_method; - -#include - -#if __BYTE_ORDER == __BIG_ENDIAN - typedef union doublebyte +void Init_native() { - double d; - unsigned char b[sizeof(double)]; -} doublebytet; -#endif + VALUE rb_bson_module = rb_define_module("BSON"); + VALUE rb_byte_buffer_class = rb_define_class_under(rb_bson_module, "ByteBuffer", rb_cObject); + VALUE rb_bson_object_id_class = rb_const_get(rb_bson_module, rb_intern("ObjectId")); + VALUE rb_bson_object_id_generator_class = rb_const_get(rb_bson_object_id_class, rb_intern("Generator")); + + rb_define_alloc_func(rb_byte_buffer_class, rb_bson_byte_buffer_allocate); + rb_define_method(rb_byte_buffer_class, "initialize", rb_bson_byte_buffer_initialize, -1); + rb_define_method(rb_byte_buffer_class, "length", rb_bson_byte_buffer_length, 0); + rb_define_method(rb_byte_buffer_class, "get_byte", rb_bson_byte_buffer_get_byte, 0); + rb_define_method(rb_byte_buffer_class, "get_bytes", rb_bson_byte_buffer_get_bytes, 1); + rb_define_method(rb_byte_buffer_class, "get_cstring", rb_bson_byte_buffer_get_cstring, 0); + rb_define_method(rb_byte_buffer_class, "get_double", rb_bson_byte_buffer_get_double, 0); + rb_define_method(rb_byte_buffer_class, "get_int32", rb_bson_byte_buffer_get_int32, 0); + rb_define_method(rb_byte_buffer_class, "get_int64", rb_bson_byte_buffer_get_int64, 0); + rb_define_method(rb_byte_buffer_class, "get_string", rb_bson_byte_buffer_get_string, 0); + rb_define_method(rb_byte_buffer_class, "put_byte", rb_bson_byte_buffer_put_byte, 1); + rb_define_method(rb_byte_buffer_class, "put_bytes", rb_bson_byte_buffer_put_bytes, 1); + rb_define_method(rb_byte_buffer_class, "put_cstring", rb_bson_byte_buffer_put_cstring, 1); + rb_define_method(rb_byte_buffer_class, "put_double", rb_bson_byte_buffer_put_double, 1); + rb_define_method(rb_byte_buffer_class, "put_int32", rb_bson_byte_buffer_put_int32, 1); + rb_define_method(rb_byte_buffer_class, "put_int64", rb_bson_byte_buffer_put_int64, 1); + rb_define_method(rb_byte_buffer_class, "put_string", rb_bson_byte_buffer_put_string, 1); + rb_define_method(rb_byte_buffer_class, "read_position", rb_bson_byte_buffer_read_position, 0); + rb_define_method(rb_byte_buffer_class, "replace_int32", rb_bson_byte_buffer_replace_int32, 2); + rb_define_method(rb_byte_buffer_class, "write_position", rb_bson_byte_buffer_write_position, 0); + rb_define_method(rb_byte_buffer_class, "to_s", rb_bson_byte_buffer_to_s, 0); + rb_define_method(rb_bson_object_id_generator_class, "next_object_id", rb_bson_object_id_generator_next, -1); + + // Get the object id machine id and hash it. + rb_require("digest/md5"); + VALUE rb_digest_class = rb_const_get(rb_cObject, rb_intern("Digest")); + VALUE rb_md5_class = rb_const_get(rb_digest_class, rb_intern("MD5")); + char rb_bson_machine_id[256]; + gethostname(rb_bson_machine_id, sizeof(rb_bson_machine_id)); + rb_bson_machine_id[255] = '\0'; + 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)); +} /** - * Convert the binary string to a ruby utf8 string. - * - * @example Convert the string to binary. - * rb_bson_from_bson_string("test"); - * - * @param [ String ] string The ruby string. - * - * @return [ String ] The encoded string. - * - * @since 2.0.0 + * Allocates a bson byte buffer that wraps a byte_buffer_t. */ -static VALUE rb_bson_from_bson_string(VALUE string) +VALUE rb_bson_byte_buffer_allocate(VALUE klass) { - return rb_enc_associate(string, rb_utf8_encoding()); + byte_buffer_t *b; + VALUE obj = TypedData_Make_Struct(klass, byte_buffer_t, &rb_byte_buffer_data_type, b); + b->b_ptr = b->buffer; + b->size = BSON_BYTE_BUFFER_SIZE; + return obj; } /** - * Provide default new string with binary encoding. - * - * @example Check encoded and provide default new binary encoded string. - * if (NIL_P(encoded)) encoded = rb_str_new_encoded_binary(); - * - * @return [ String ] The new string with binary encoding. - * - * @since 2.0.0 + * Initialize a byte buffer. */ -static VALUE rb_str_new_encoded_binary(void) +VALUE rb_bson_byte_buffer_initialize(int argc, VALUE *argv, VALUE self) { - return rb_enc_str_new("", 0, rb_ascii8bit_encoding()); + VALUE bytes; + rb_scan_args(argc, argv, "01", &bytes); + + if (!NIL_P(bytes)) { + rb_bson_byte_buffer_put_bytes(self, bytes); + } + + return self; } /** - * Constant for a null byte. - * - * @since 2.0.0 + * Get the length of the buffer. */ -static const char rb_bson_null_byte = 0; +VALUE rb_bson_byte_buffer_length(VALUE self) +{ + byte_buffer_t *b; + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + return UINT2NUM(READ_SIZE(b)); +} /** - * Constant for a true byte. - * - * @since 2.0.0 + * Get a single byte from the buffer. */ -static const char rb_bson_true_byte = 1; +VALUE rb_bson_byte_buffer_get_byte(VALUE self) +{ + byte_buffer_t *b; + VALUE byte; + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_READ(b, 1); + byte = rb_str_new(READ_PTR(b), 1); + b->read_position += 1; + return byte; +} /** - * Holds the machine id hash for object id generation. - * - * @since 3.2.0 - * + * Get bytes from the buffer. */ -static char rb_bson_machine_id_hash[HOST_NAME_HASH_MAX]; +VALUE rb_bson_byte_buffer_get_bytes(VALUE self, VALUE i) +{ + byte_buffer_t *b; + VALUE bytes; + const long length = FIX2LONG(i); + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_READ(b, length); + bytes = rb_str_new(READ_PTR(b), length); + b->read_position += length; + return bytes; +} /** - * The counter for incrementing object ids. - * - * @since 2.0.0 + * Get a cstring from the buffer. */ -static unsigned int rb_bson_object_id_counter = 0; +VALUE rb_bson_byte_buffer_get_cstring(VALUE self) +{ + byte_buffer_t *b; + VALUE string; + int length; + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + length = (int)strlen(READ_PTR(b)); + ENSURE_BSON_READ(b, length); + string = rb_enc_str_new(READ_PTR(b), length, rb_utf8_encoding()); + b->read_position += length + 1; + return string; +} /** - * Take the provided params and return the encoded bytes or a default one. - * - * @example Get the default encoded bytes. - * rb_get_default_encoded(1, bytes); - * - * @param [ int ] argc The number of arguments. - * @param [ Object ] argv The arguments. - * - * @return [ String ] The encoded string. - * - * @since 2.0.0 + * Get a double from the buffer. */ -static VALUE rb_get_default_encoded(int argc, VALUE *argv) +VALUE rb_bson_byte_buffer_get_double(VALUE self) { - VALUE encoded; - rb_scan_args(argc, argv, "01", &encoded); - if (NIL_P(encoded)) encoded = rb_str_new_encoded_binary(); - return encoded; + byte_buffer_t *b; + union { uint64_t i64; double d; } ucast; + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_READ(b, 8); + ucast.i64 = le64toh(*(uint64_t*)READ_PTR(b)); + b->read_position += 8; + return DBL2NUM(ucast.d); } /** - * Append the ruby float as 8-byte double value to buffer. - * - * @example Convert float to double and append. - * rb_float_to_bson(..., 1.2311); - * - * @param [ String] encoded Optional string buffer, default provided by rb_str_encoded_binary - * @param [ Float ] self The ruby float value. - * - * @return [ String ] The encoded bytes with double value appended. - * - * @since 2.0.0 + * Get a int32 from the buffer. */ -static VALUE rb_float_to_bson(int argc, VALUE *argv, VALUE self) +VALUE rb_bson_byte_buffer_get_int32(VALUE self) { - const double v = NUM2DBL(self); - VALUE encoded = rb_get_default_encoded(argc, argv); - # if __BYTE_ORDER == __LITTLE_ENDIAN - rb_str_cat(encoded, (char*) &v, 8); - #elif __BYTE_ORDER == __BIG_ENDIAN - doublebytet swap; - unsigned char b; - swap.d = v; - for (int i=0; i < sizeof(double)/2; i++) { - b=swap.b[i]; - swap.b[i] = swap.b[((sizeof(double)-1)-i)]; - swap.b[((sizeof(double)-1)-i)]=b; - } - rb_str_cat(encoded, (char*)&swap.d, 8); - #endif - return encoded; + byte_buffer_t *b; + int32_t i32; + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_READ(b, 4); + i32 = le32toh(*((int32_t*)READ_PTR(b))); + b->read_position += 4; + return INT2NUM(i32); } /** - * Convert the bytes for the double into a Ruby float. - * - * @example Convert the bytes to a float. - * rb_float_from_bson_double(class, bytes); - * - * @param [ Class ] The float class. - * @param [ String ] The double bytes. - * - * @return [ Float ] The ruby float value. - * - * @since 2.0.0 + * Get a int64 from the buffer. */ -static VALUE rb_float_from_bson_double(VALUE self, VALUE value) +VALUE rb_bson_byte_buffer_get_int64(VALUE self) { - const char * bytes; - double v; - bytes = StringValuePtr(value); -#if __BYTE_ORDER == __LITTLE_ENDIAN - memcpy(&v, bytes, RSTRING_LEN(value)); -#else - doublebytet swap; - unsigned char b; - memcpy(&swap.d, bytes, RSTRING_LEN(value)); - for (int i=0; i < sizeof(double)/2; i++) { - b=swap.b[i]; - swap.b[i] = swap.b[((sizeof(double)-1)-i)]; - swap.b[((sizeof(double)-1)-i)]=b; - } - memcpy(&v, swap.b, RSTRING_LEN(value)); -#endif - - return DBL2NUM(v); + byte_buffer_t *b; + int64_t i64; + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_READ(b, 8); + i64 = le64toh(*((int64_t*)READ_PTR(b))); + b->read_position += 8; + return LONG2NUM(i64); } /** - * Generate the data for the next object id. - * - * @example Generate the data for the next object id. - * rb_object_id_generator_next(0, NULL, object_id); - * - * @param [ int ] argc The argument count. - * @param [ Time ] time The optional Ruby time. - * @param [ BSON::ObjectId ] self The object id. - * - * @return [ String ] The raw bytes for the id. - * - * @since 2.0.0 + * Get a string from the buffer. */ -static VALUE rb_object_id_generator_next(int argc, VALUE* args, VALUE self) +VALUE rb_bson_byte_buffer_get_string(VALUE self) { - char bytes[12]; - unsigned long t; - unsigned short pid = htons(getpid()); - - if (argc == 0 || (argc == 1 && *args == Qnil)) { - t = htonl((int) time(NULL)); - } - else { - t = htonl(NUM2UINT(rb_funcall(*args, rb_intern("to_i"), 0))); - } - - unsigned long c; - c = htonl(rb_bson_object_id_counter << 8); - -# if __BYTE_ORDER == __LITTLE_ENDIAN - memcpy(&bytes, &t, 4); - memcpy(&bytes[4], rb_bson_machine_id_hash, 3); - memcpy(&bytes[7], &pid, 2); - memcpy(&bytes[9], (unsigned char*) &c, 3); -#elif __BYTE_ORDER == __BIG_ENDIAN - memcpy(&bytes, ((unsigned char*) &t) + 4, 4); - memcpy(&bytes[4], rb_bson_machine_id_hash, 3); - memcpy(&bytes[7], &pid, 2); - memcpy(&bytes[9], ((unsigned char*) &c) + 4, 3); -#endif - rb_bson_object_id_counter++; - return rb_str_new(bytes, 12); + byte_buffer_t *b; + int32_t length; + VALUE string; + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_READ(b, 4); + length = le32toh(*((int32_t*)READ_PTR(b))); + b->read_position += 4; + ENSURE_BSON_READ(b, length); + string = rb_enc_str_new(READ_PTR(b), length - 1, rb_utf8_encoding()); + b->read_position += length; + return string; } /** - * Check if the integer is a 32 bit integer. - * - * @example Check if the integer is 32 bit. - * rb_integer_is_bson_int32(integer); - * - * @param [ Integer ] self The ruby integer. - * - * @return [ true, false ] If the integer is 32 bit. - * - * @since 2.0.0 + * Writes a byte to the byte buffer. */ -static VALUE rb_integer_is_bson_int32(VALUE self) +VALUE rb_bson_byte_buffer_put_byte(VALUE self, VALUE byte) { - const int64_t v = NUM2INT64(self); - if (INT_MIN <= v && v <= INT_MAX) { - return Qtrue; - } - else { - return Qfalse; - } + byte_buffer_t *b; + const char *str = RSTRING_PTR(byte); + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, 1); + memcpy(WRITE_PTR(b), str, 1); + b->write_position += 1; + + return self; } /** - * Convert the Ruby integer into a BSON as per the 32 bit specification, - * which is 4 bytes. - * - * @example Convert the integer to 32bit BSON. - * rb_integer_to_bson_int32(128, encoded); - * - * @param [ Integer ] self The Ruby integer. - * @param [ String ] encoded The Ruby binary string to append to. - * - * @return [ String ] encoded Ruby binary string with BSON raw bytes appended. - * - * @since 2.0.0 + * Writes bytes to the byte buffer. */ -static VALUE rb_integer_to_bson_int32(VALUE self, VALUE encoded) +VALUE rb_bson_byte_buffer_put_bytes(VALUE self, VALUE bytes) { - const int32_t v = NUM2INT(self); - const char bytes[4] = { - v & 255, - (v >> 8) & 255, - (v >> 16) & 255, - (v >> 24) & 255 - }; - return rb_str_cat(encoded, bytes, 4); + byte_buffer_t *b; + const char *str = RSTRING_PTR(bytes); + const size_t length = RSTRING_LEN(bytes); + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, length); + memcpy(WRITE_PTR(b), str, length); + b->write_position += length; + return self; } /** - * Initialize the bson array index for integers. - * - * @example Initialize the array. - * rb_bson_init_integer_bson_array_indexes(); - * - * @since 2.0.0 + * Writes a cstring to the byte buffer. */ -static void rb_bson_init_integer_bson_array_indexes(void) +VALUE rb_bson_byte_buffer_put_cstring(VALUE self, VALUE string) { - int i; - for (i = 0; i < BSON_INDEX_SIZE; i++) { - snprintf(rb_bson_array_indexes[i], BSON_INDEX_CHAR_SIZE, "%d", i); + byte_buffer_t *b; + char *c_str = RSTRING_PTR(string); + size_t length = RSTRING_LEN(string) + 1; + + if (!rb_bson_utf8_validate(c_str, length - 1, false)) { + rb_raise(rb_eArgError, "String %s is not a valid UTF-8 CString.", c_str); } + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, length); + memcpy(WRITE_PTR(b), c_str, length); + b->write_position += length; + return self; } /** - * Convert the Ruby integer into a character string and append with nullchar to encoded BSON. - * - * @example Convert the integer to string and append with nullchar. - * rb_integer_to_bson_key(128, encoded); - * - * @param [ Integer ] self The Ruby integer. - * @param [ String ] encoded The Ruby binary string to append to. - * - * @return [ String ] encoded Ruby binary string with BSON raw bytes appended. - * - * @since 2.0.0 + * Writes a 64 bit double to the buffer. */ -static VALUE rb_integer_to_bson_key(int argc, VALUE *argv, VALUE self) +VALUE rb_bson_byte_buffer_put_double(VALUE self, VALUE f) { - char bytes[INTEGER_CHAR_SIZE]; - const int64_t v = NUM2INT64(self); - VALUE encoded = rb_get_default_encoded(argc, argv); - int length; - if (v < BSON_INDEX_SIZE) - return rb_str_cat(encoded, rb_bson_array_indexes[v], strlen(rb_bson_array_indexes[v]) + 1); - length = snprintf(bytes, INTEGER_CHAR_SIZE, "%ld", (long)v); - return rb_str_cat(encoded, bytes, length + 1); + byte_buffer_t *b; + union {double d; uint64_t i64;} ucast; + + ucast.d = NUM2DBL(f); + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, 8); + ucast.i64 = htole64(ucast.i64); + *(int64_t*)WRITE_PTR(b) = ucast.i64; + b->write_position += 8; + + return self; } /** - * Convert the provided raw bytes into a 32bit Ruby integer. - * - * @example Convert the bytes to an Integer. - * rb_integer_from_bson_int32(Int32, bytes); - * - * @param [ BSON::Int32 ] self The Int32 eigenclass. - * @param [ String ] bytes The raw bytes. - * - * @return [ Integer ] The Ruby integer. - * - * @since 2.0.0 + * Writes a 32 bit integer to the byte buffer. */ -static VALUE rb_integer_from_bson_int32(VALUE self, VALUE bson) +VALUE rb_bson_byte_buffer_put_int32(VALUE self, VALUE i) { - const uint8_t *v = (const uint8_t*) StringValuePtr(bson); - const int32_t integer = v[0] + (v[1] << 8) + (v[2] << 16) + (v[3] << 24); - return INT2NUM(integer); + byte_buffer_t *b; + const int32_t i32 = NUM2INT(i); + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, 4); + *((int32_t*)WRITE_PTR(b)) = htole32(i32); + b->write_position += 4; + + return self; } /** - * Convert the raw BSON bytes into an int64_t type. - * - * @example Convert the bytes into an int64_t. - * rb_bson_to_int64_t(bson); - * - * @param [ String ] bson The raw bytes. - * - * @return [ int64_t ] The int64_t. - * - * @since 2.0.0 + * Writes a 64 bit integer to the byte buffer. */ -static int64_t rb_bson_to_int64_t(VALUE bson) +VALUE rb_bson_byte_buffer_put_int64(VALUE self, VALUE i) { - uint8_t *v; - uint32_t byte_0, byte_1; - int64_t byte_2, byte_3; - int64_t lower, upper; - v = (uint8_t*) StringValuePtr(bson); - byte_0 = v[0]; - byte_1 = v[1]; - byte_2 = v[2]; - byte_3 = v[3]; - lower = byte_0 + (byte_1 << 8) + (byte_2 << 16) + (byte_3 << 24); - byte_0 = v[4]; - byte_1 = v[5]; - byte_2 = v[6]; - byte_3 = v[7]; - upper = byte_0 + (byte_1 << 8) + (byte_2 << 16) + (byte_3 << 24); - return lower + (upper << 32); + byte_buffer_t *b; + const int64_t i64 = NUM2LONG(i); + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, 8); + *((int64_t*)WRITE_PTR(b)) = htole64(i64); + b->write_position += 8; + + return self; } /** - * Convert the provided raw bytes into a 64bit Ruby integer. - * - * @example Convert the bytes to an Integer. - * rb_integer_from_bson_int64(Int64, bytes); - * - * @param [ BSON::Int64 ] self The Int64 eigenclass. - * @param [ String ] bytes The raw bytes. - * - * @return [ Integer ] The Ruby integer. - * - * @since 2.0.0 + * Writes a string to the byte buffer. */ -static VALUE rb_integer_from_bson_int64(VALUE self, VALUE bson) +VALUE rb_bson_byte_buffer_put_string(VALUE self, VALUE string) { - return INT642NUM(rb_bson_to_int64_t(bson)); + byte_buffer_t *b; + + char *str = RSTRING_PTR(string); + const size_t length = RSTRING_LEN(string) + 1; + + if (!rb_bson_utf8_validate(str, length - 1, true)) { + rb_raise(rb_eArgError, "String %s is not valid UTF-8.", str); + } + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + ENSURE_BSON_WRITE(b, length + 4); + *((int32_t*)WRITE_PTR(b)) = htole32(length); + b->write_position += 4; + memcpy(WRITE_PTR(b), str, length); + b->write_position += length; + + return self; } /** - * Append the 64-bit integer to encoded BSON Ruby binary string. - * - * @example Append the 64-bit integer to encoded BSON. - * int64_t_to_bson(128, encoded); - * - * @param [ int64_t ] self The 64-bit integer. - * @param [ String ] encoded The BSON Ruby binary string to append to. - * - * @return [ String ] encoded Ruby binary string with BSON raw bytes appended. - * - * @since 2.0.0 + * Get the read position. */ -static VALUE int64_t_to_bson(int64_t v, VALUE encoded) +VALUE rb_bson_byte_buffer_read_position(VALUE self) { - const char bytes[8] = { - v & 255, - (v >> 8) & 255, - (v >> 16) & 255, - (v >> 24) & 255, - (v >> 32) & 255, - (v >> 40) & 255, - (v >> 48) & 255, - (v >> 56) & 255 - }; - return rb_str_cat(encoded, bytes, 8); + byte_buffer_t *b; + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + return INT2NUM(b->read_position); } /** - * Convert the Ruby integer into a BSON as per the 64 bit specification, - * which is 8 bytes. - * - * @example Convert the integer to 64bit BSON. - * rb_integer_to_bson_int64(128, encoded); - * - * @param [ Integer ] self The Ruby integer. - * @param [ String ] encoded The Ruby binary string to append to. - * - * @return [ String ] encoded Ruby binary string with BSON raw bytes appended. - * - * @since 2.0.0 + * Replace a 32 bit integer int the byte buffer. */ -static VALUE rb_integer_to_bson_int64(VALUE self, VALUE encoded) +VALUE rb_bson_byte_buffer_replace_int32(VALUE self, VALUE index, VALUE i) { - return int64_t_to_bson(NUM2INT64(self), StringValue(encoded)); + byte_buffer_t *b; + const int32_t position = NUM2INT(index); + const int32_t i32 = htole32(NUM2INT(i)); + + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + + memcpy(READ_PTR(b) + position, &i32, 4); + + return self; } /** - * Converts the milliseconds time to the raw BSON bytes. We need to - * explicitly convert using 64 bit here. - * - * @example Convert the milliseconds value to BSON bytes. - * rb_time_to_bson(time, 2124132340000, encoded); - * - * @param [ Time ] self The Ruby Time object. - * @param [ Integer ] milliseconds The milliseconds pre/post epoch. - * @param [ String ] encoded The Ruby binary string to append to. - * - * @return [ String ] encoded Ruby binary string with time BSON raw bytes appended. - * - * @since 2.0.0 + * Get the write position. */ -static VALUE rb_time_to_bson(int argc, VALUE *argv, VALUE self) +VALUE rb_bson_byte_buffer_write_position(VALUE self) { - int64_t t = NUM2INT64(rb_funcall(self, rb_intern("to_i"), 0)); - int64_t milliseconds = (int64_t)(t * 1000); - int32_t micro = NUM2INT(rb_funcall(self, rb_intern("usec"), 0)); - int64_t time = milliseconds + (micro / 1000); - VALUE encoded = rb_get_default_encoded(argc, argv); - return int64_t_to_bson(time, encoded); + byte_buffer_t *b; + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + return INT2NUM(b->write_position); } /** - * Converts the raw BSON bytes into a UTC Ruby time. - * - * @example Convert the bytes to a Ruby time. - * rb_time_from_bson(time, bytes); - * - * @param [ Class ] self The Ruby Time class. - * @param [ String ] bytes The raw BSON bytes. - * - * @return [ Time ] The UTC time. - * - * @since 2.0.0 + * Convert the buffer to a string. */ -static VALUE rb_time_from_bson(VALUE self, VALUE bytes) +VALUE rb_bson_byte_buffer_to_s(VALUE self) { - const int64_t millis = rb_bson_to_int64_t(bytes); - const VALUE time = rb_time_new(millis / 1000, (millis % 1000) * 1000); - return rb_funcall(time, rb_utc_method, 0); + byte_buffer_t *b; + TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b); + return rb_str_new(READ_PTR(b), READ_SIZE(b)); } /** - * Set four bytes for int32 in a binary string and return it. - * - * @example Set int32 in a BSON string. - * rb_string_set_int32(self, pos, int32) - * - * @param [ String ] self The Ruby binary string. - * @param [ Fixnum ] The position to set. - * @param [ Fixnum ] The int32 value. - * - * @return [ String ] The binary string. - * - * @since 2.0.0 + * Get the size of the byte_buffer_t in memory. */ -static VALUE rb_string_set_int32(VALUE str, VALUE pos, VALUE an_int32) +size_t rb_bson_byte_buffer_memsize(const void *ptr) { - const int32_t offset = NUM2INT(pos); - const int32_t v = NUM2INT(an_int32); - const char bytes[4] = { - v & 255, - (v >> 8) & 255, - (v >> 16) & 255, - (v >> 24) & 255 - }; - rb_str_modify(str); - if (offset < 0 || offset + 4 > RSTRING_LEN(str)) { - rb_raise(rb_eArgError, "invalid position"); - } - memcpy(RSTRING_PTR(str) + offset, bytes, 4); - return str; + return ptr ? sizeof(byte_buffer_t) : 0; } /** - * Check for illegal characters in string. - * - * @example Check for illegal characters. - * rb_string_check_for_illegal_characters("test"); - * - * @param [ String ] self The string value. - * - * @since 2.0.0 + * Free the memory for the byte buffer. */ -static VALUE rb_string_check_for_illegal_characters(VALUE self) +void rb_bson_byte_buffer_free(void *ptr) { - if (strlen(RSTRING_PTR(self)) != (size_t) RSTRING_LEN(self)) - rb_raise(rb_eArgError, "Illegal C-String contains a null byte."); - return self; + byte_buffer_t *b = ptr; + if (b->b_ptr != b->buffer) { + xfree(b->b_ptr); + } + xfree(b); } /** - * Encode a false value to bson. - * - * @example Encode the false value. - * rb_false_class_to_bson(0, false); - * - * @param [ int ] argc The number or arguments. - * @param [ Array ] argv The arguments. - * @param [ TrueClass ] self The true value. - * - * @return [ String ] The encoded string. - * - * @since 2.0.0 + * Expand the byte buffer linearly. */ -static VALUE rb_false_class_to_bson(int argc, VALUE *argv, VALUE self) +void rb_bson_expand_buffer(byte_buffer_t* buffer_ptr, size_t length) { - VALUE encoded = rb_get_default_encoded(argc, argv); - rb_str_cat(encoded, &rb_bson_null_byte, 1); - return encoded; + const size_t required_size = buffer_ptr->write_position - buffer_ptr->read_position + length; + if (required_size <= buffer_ptr->size) { + memmove(buffer_ptr->b_ptr, READ_PTR(buffer_ptr), READ_SIZE(buffer_ptr)); + buffer_ptr->write_position -= buffer_ptr->read_position; + buffer_ptr->read_position = 0; + } else { + char *new_b_ptr; + const size_t new_size = required_size + BSON_BYTE_BUFFER_SIZE; + new_b_ptr = ALLOC_N(char, new_size); + memcpy(new_b_ptr, READ_PTR(buffer_ptr), READ_SIZE(buffer_ptr)); + if (buffer_ptr->b_ptr != buffer_ptr->buffer) { + xfree(buffer_ptr->b_ptr); + } + buffer_ptr->b_ptr = new_b_ptr; + buffer_ptr->size = new_size; + buffer_ptr->write_position -= buffer_ptr->read_position; + buffer_ptr->read_position = 0; + } } /** - * Encode a true value to bson. - * - * @example Encode the true value. - * rb_true_class_to_bson(0, true); - * - * @param [ int ] argc The number or arguments. - * @param [ Array ] argv The arguments. - * @param [ TrueClass ] self The true value. - * - * @return [ String ] The encoded string. - * - * @since 2.0.0 + * Generate the next object id. */ -static VALUE rb_true_class_to_bson(int argc, VALUE *argv, VALUE self) +VALUE rb_bson_object_id_generator_next(int argc, VALUE* args, VALUE self) { - VALUE encoded = rb_get_default_encoded(argc, argv); - rb_str_cat(encoded, &rb_bson_true_byte, 1); - return encoded; + char bytes[12]; + unsigned long t; + unsigned short pid = htons(getpid()); + + if (argc == 0 || (argc == 1 && *args == Qnil)) { + t = htonl((int) time(NULL)); + } + else { + t = htonl(NUM2UINT(rb_funcall(*args, rb_intern("to_i"), 0))); + } + + unsigned long c; + c = htonl(rb_bson_object_id_counter << 8); + +# if __BYTE_ORDER == __LITTLE_ENDIAN + memcpy(&bytes, &t, 4); + memcpy(&bytes[4], rb_bson_machine_id_hash, 3); + memcpy(&bytes[7], &pid, 2); + memcpy(&bytes[9], (unsigned char*) &c, 3); +#elif __BYTE_ORDER == __BIG_ENDIAN + memcpy(&bytes, ((unsigned char*) &t) + 4, 4); + memcpy(&bytes[4], rb_bson_machine_id_hash, 3); + memcpy(&bytes[7], &pid, 2); + memcpy(&bytes[9], ((unsigned char*) &c) + 4, 3); +#endif + rb_bson_object_id_counter++; + return rb_str_new(bytes, 12); } /** - * Decode a string from bson. - * - * @example Decode a string. - * rb_bson_string_from_bson(string, io); - * - * @param [ String ] self The string class. - * @param [ IO ] bson The io stream of BSON. - * - * @return [ String ] The decoded string. - * - * @since 3.2.5 + * Taken from libbson. */ -static VALUE rb_bson_string_from_bson(VALUE self, VALUE bson) +static void _bson_utf8_get_sequence(const char *utf8, uint8_t *seq_length, uint8_t *first_mask) { - ID read_method = rb_intern("read"); - VALUE int_bytes = rb_funcall(bson, read_method, 1, 4); - VALUE size = rb_integer_from_bson_int32(self, int_bytes); - VALUE string_bytes = rb_funcall(bson, read_method, 1, size - 1); - return rb_bson_from_bson_string(string_bytes); + unsigned char c = *(const unsigned char *)utf8; + uint8_t m; + uint8_t n; + + /* + * See the following[1] for a description of what the given multi-byte + * sequences will be based on the bits set of the first byte. We also need + * to mask the first byte based on that. All subsequent bytes are masked + * against 0x3F. + * + * [1] http://www.joelonsoftware.com/articles/Unicode.html + */ + + if ((c & 0x80) == 0) { + n = 1; + m = 0x7F; + } else if ((c & 0xE0) == 0xC0) { + n = 2; + m = 0x1F; + } else if ((c & 0xF0) == 0xE0) { + n = 3; + m = 0x0F; + } else if ((c & 0xF8) == 0xF0) { + n = 4; + m = 0x07; + } else if ((c & 0xFC) == 0xF8) { + n = 5; + m = 0x03; + } else if ((c & 0xFE) == 0xFC) { + n = 6; + m = 0x01; + } else { + n = 0; + m = 0; + } + + *seq_length = n; + *first_mask = m; } /** - * Initialize the bson c extension. - * - * @since 2.0.0 + * Taken from libbson. */ -void Init_native() +bool rb_bson_utf8_validate(const char *utf8, size_t utf8_len, bool allow_null) { - // Get all the constants to be used in the extensions. - VALUE bson = rb_const_get(rb_cObject, rb_intern("BSON")); - VALUE integer = rb_const_get(bson, rb_intern("Integer")); - VALUE floats = rb_const_get(bson, rb_intern("Float")); - VALUE float_class = rb_const_get(floats, rb_intern("ClassMethods")); - VALUE time = rb_const_get(bson, rb_intern("Time")); - VALUE time_class = rb_singleton_class(time); - VALUE int32 = rb_const_get(bson, rb_intern("Int32")); - VALUE int32_class = rb_singleton_class(int32); - VALUE int64 = rb_const_get(bson, rb_intern("Int64")); - VALUE int64_class = rb_singleton_class(int64); - VALUE object_id = rb_const_get(bson, rb_intern("ObjectId")); - VALUE generator = rb_const_get(object_id, rb_intern("Generator")); - VALUE string = rb_const_get(bson, rb_intern("String")); - VALUE string_class = rb_singleton_class(string); - VALUE true_class = rb_const_get(bson, rb_intern("TrueClass")); - VALUE false_class = rb_const_get(bson, rb_intern("FalseClass")); - // needed to hash the machine id - rb_require("digest/md5"); - VALUE digest_class = rb_const_get(rb_cObject, rb_intern("Digest")); - VALUE md5_class = rb_const_get(digest_class, rb_intern("MD5")); - rb_bson_utf8_string = rb_const_get(bson, rb_intern("UTF8")); - rb_utc_method = rb_intern("utc"); + uint32_t c; + uint8_t first_mask; + uint8_t seq_length; + unsigned i; + unsigned j; + + if (!utf8) { + return false; + } - // Get the object id machine id and hash it. - char rb_bson_machine_id[256]; - gethostname(rb_bson_machine_id, sizeof rb_bson_machine_id); - rb_bson_machine_id[255] = '\0'; - VALUE digest = rb_funcall(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)); + for (i = 0; i < utf8_len; i += seq_length) { + _bson_utf8_get_sequence(&utf8[i], &seq_length, &first_mask); + + /* + * Ensure we have a valid multi-byte sequence length. + */ + if (!seq_length) { + return false; + } + + /* + * Ensure we have enough bytes left. + */ + if ((utf8_len - i) < seq_length) { + return false; + } + + /* + * Also calculate the next char as a unichar so we can + * check code ranges for non-shortest form. + */ + c = utf8 [i] & first_mask; + + /* + * Check the high-bits for each additional sequence byte. + */ + for (j = i + 1; j < (i + seq_length); j++) { + c = (c << 6) | (utf8 [j] & 0x3F); + if ((utf8[j] & 0xC0) != 0x80) { + return false; + } + } + + /* + * Check for NULL bytes afterwards. + * + * Hint: if you want to optimize this function, starting here to do + * this in the same pass as the data above would probably be a good + * idea. You would add a branch into the inner loop, but save possibly + * on cache-line bouncing on larger strings. Just a thought. + */ + if (!allow_null) { + for (j = 0; j < seq_length; j++) { + if (((i + j) > utf8_len) || !utf8[i + j]) { + return false; + } + } + } + + /* + * Code point wont fit in utf-16, not allowed. + */ + if (c > 0x0010FFFF) { + return false; + } + + /* + * Byte is in reserved range for UTF-16 high-marks + * for surrogate pairs. + */ + if ((c & 0xFFFFF800) == 0xD800) { + return false; + } + + /* + * Check non-shortest form unicode. + */ + switch (seq_length) { + case 1: + if (c <= 0x007F) { + continue; + } + return false; + + case 2: + if ((c >= 0x0080) && (c <= 0x07FF)) { + continue; + } else if (c == 0) { + /* Two-byte representation for NULL. */ + continue; + } + return false; + + case 3: + if (((c >= 0x0800) && (c <= 0x0FFF)) || + ((c >= 0x1000) && (c <= 0xFFFF))) { + continue; + } + return false; + + case 4: + if (((c >= 0x10000) && (c <= 0x3FFFF)) || + ((c >= 0x40000) && (c <= 0xFFFFF)) || + ((c >= 0x100000) && (c <= 0x10FFFF))) { + continue; + } + return false; + + default: + return false; + } + } - // Integer optimizations. - rb_undef_method(integer, "to_bson_int32"); - rb_define_method(integer, "to_bson_int32", rb_integer_to_bson_int32, 1); - rb_undef_method(integer, "to_bson_int64"); - rb_define_method(integer, "to_bson_int64", rb_integer_to_bson_int64, 1); - rb_undef_method(integer, "bson_int32?"); - rb_define_method(integer, "bson_int32?", rb_integer_is_bson_int32, 0); - rb_bson_init_integer_bson_array_indexes(); - rb_undef_method(integer, "to_bson_key"); - rb_define_method(integer, "to_bson_key", rb_integer_to_bson_key, -1); - rb_undef_method(int32_class, "from_bson_int32"); - rb_define_private_method(int32_class, "from_bson_int32", rb_integer_from_bson_int32, 1); - rb_undef_method(int64_class, "from_bson_int64"); - rb_define_private_method(int64_class, "from_bson_int64", rb_integer_from_bson_int64, 1); - - // Float optimizations. - rb_undef_method(floats, "to_bson"); - rb_define_method(floats, "to_bson", rb_float_to_bson, -1); - rb_undef_method(float_class, "from_bson_double"); - rb_define_private_method(float_class, "from_bson_double", rb_float_from_bson_double, 1); - - // Boolean optimizations - deserialization has no benefit so we provide - // no extensions there. - rb_undef_method(true_class, "to_bson"); - rb_define_method(true_class, "to_bson", rb_true_class_to_bson, -1); - rb_undef_method(false_class, "to_bson"); - rb_define_method(false_class, "to_bson", rb_false_class_to_bson, -1); - - // Optimizations around time serialization and deserialization. - rb_undef_method(time, "to_bson"); - rb_define_method(time, "to_bson", rb_time_to_bson, -1); - rb_undef_method(time_class, "from_bson"); - rb_define_method(time_class, "from_bson", rb_time_from_bson, 1); - - // String optimizations. - rb_undef_method(string, "set_int32"); - rb_define_method(string, "set_int32", rb_string_set_int32, 2); - rb_undef_method(string, "from_bson_string"); - rb_define_method(string, "from_bson_string", rb_bson_from_bson_string, 0); - rb_undef_method(string_class, "from_bson"); - rb_define_method(string_class, "from_bson", rb_bson_string_from_bson, 1); - rb_undef_method(string, "check_for_illegal_characters!"); - rb_define_private_method(string, "check_for_illegal_characters!", rb_string_check_for_illegal_characters, 0); - - // Redefine the next method on the object id generator. - rb_undef_method(generator, "next_object_id"); - rb_define_method(generator, "next_object_id", rb_object_id_generator_next, -1); + return true; } diff --git a/lib/bson.rb b/lib/bson.rb index 4566e62b0..1e773974a 100644 --- a/lib/bson.rb +++ b/lib/bson.rb @@ -62,7 +62,6 @@ def self.ObjectId(string) require "bson/int32" require "bson/int64" require "bson/integer" -require "bson/encodable" require "bson/array" require "bson/binary" require "bson/boolean" diff --git a/lib/bson/array.rb b/lib/bson/array.rb index c2b085739..4f0a82cd2 100644 --- a/lib/bson/array.rb +++ b/lib/bson/array.rb @@ -21,7 +21,6 @@ module BSON # # @since 2.0.0 module Array - include Encodable # An array is type 0x04 in the BSON spec. # @@ -41,14 +40,16 @@ module Array # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded| - each_with_index do |value, index| - encoded << value.bson_type - index.to_bson_key(encoded) - value.to_bson(encoded) - end + def to_bson(buffer = ByteBuffer.new) + position = buffer.length + buffer.put_int32(0) + each_with_index do |value, index| + buffer.put_byte(value.bson_type) + buffer.put_cstring(index.to_s) + value.to_bson(buffer) end + buffer.put_byte(NULL_BYTE) + buffer.replace_int32(position, buffer.length - position) end # Convert the array to an object id. This will only work for arrays of size @@ -84,19 +85,19 @@ module ClassMethods # Deserialize the array from BSON. # - # @param [ BSON ] bson The bson representing an array. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Array ] The decoded array. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) + def from_bson(buffer) array = new - bson.read(4) # throw away the length - while (type = bson.readbyte.chr) != NULL_BYTE - bson.gets(NULL_BYTE) - array << BSON::Registry.get(type).from_bson(bson) + buffer.get_int32 # throw away the length + while (type = buffer.get_byte) != NULL_BYTE + buffer.get_cstring + array << BSON::Registry.get(type).from_bson(buffer) end array end diff --git a/lib/bson/binary.rb b/lib/bson/binary.rb index 7aa92a78b..4af77023a 100644 --- a/lib/bson/binary.rb +++ b/lib/bson/binary.rb @@ -21,7 +21,6 @@ module BSON # @since 2.0.0 class Binary include JSON - include Encodable # A binary is type 0x05 in the BSON spec. # @@ -130,28 +129,29 @@ def inspect # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encode_binary_data_with_placeholder(encoded) do |encoded| - encoded << SUBTYPES.fetch(type) - encoded << data.bytesize.to_bson if type == :old - encoded << data.force_encoding(BINARY) - end + def to_bson(buffer = ByteBuffer.new) + position = buffer.length + buffer.put_int32(0) + buffer.put_byte(SUBTYPES.fetch(type)) + buffer.put_int32(data.bytesize) if type == :old + buffer.put_bytes(data.force_encoding(BINARY)) + buffer.replace_int32(position, buffer.length - position - 5) end # Deserialize the binary data from BSON. # - # @param [ BSON ] bson The bson representing binary data. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Binary ] The decoded binary data. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - length = Int32.from_bson(bson) - type = TYPES[bson.read(1)] - length = Int32.from_bson(bson) if type == :old - data = bson.read(length) + def self.from_bson(buffer) + length = buffer.get_int32 + type = TYPES[buffer.get_byte] + length = buffer.get_int32 if type == :old + data = buffer.get_bytes(length) new(data, type) end diff --git a/lib/bson/boolean.rb b/lib/bson/boolean.rb index 0f85562f3..5576cc520 100644 --- a/lib/bson/boolean.rb +++ b/lib/bson/boolean.rb @@ -29,15 +29,15 @@ class Boolean # Deserialize a boolean from BSON. # - # @param [ BSON ] bson The encoded boolean. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ TrueClass, FalseClass ] The decoded boolean. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - bson.readbyte.chr == TrueClass::TRUE_BYTE + def self.from_bson(buffer) + buffer.get_byte == TrueClass::TRUE_BYTE end # Register this type when the module is loaded. diff --git a/lib/bson/code.rb b/lib/bson/code.rb index bd6b8c7c6..b188f9c2c 100644 --- a/lib/bson/code.rb +++ b/lib/bson/code.rb @@ -21,7 +21,6 @@ module BSON # @since 2.0.0 class Code include JSON - include Encodable # A code is type 0x0D in the BSON spec. # @@ -82,23 +81,21 @@ def initialize(javascript = "") # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encode_with_placeholder_and_null(STRING_ADJUST, encoded) do |encoded| - javascript.to_bson_string(encoded) - end + def to_bson(buffer = ByteBuffer.new) + buffer.put_string(javascript) # @todo: was formerly to_bson_string end # Deserialize code from BSON. # - # @param [ BSON ] bson The encoded code. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ TrueClass, FalseClass ] The decoded code. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - new(bson.read(Int32.from_bson(bson)).from_bson_string.chop!) + def self.from_bson(buffer) + new(buffer.get_string) end # Register this type when the module is loaded. diff --git a/lib/bson/code_with_scope.rb b/lib/bson/code_with_scope.rb index 1680a83c5..3abb61939 100644 --- a/lib/bson/code_with_scope.rb +++ b/lib/bson/code_with_scope.rb @@ -21,7 +21,6 @@ module BSON # # @since 2.0.0 class CodeWithScope - include Encodable include JSON # A code with scope is type 0x0F in the BSON spec. @@ -88,28 +87,26 @@ def initialize(javascript = "", scope = {}) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - # -1 because we are removing an extra byte - out = encode_with_placeholder_and_null(BSON_ADJUST - 1, encoded) do |encoded| - javascript.to_bson(encoded) - scope.to_bson(encoded) - end - # an extra null byte has been added; we must remove it - out.chop! + def to_bson(buffer = ByteBuffer.new) + position = buffer.length + buffer.put_int32(0) + buffer.put_string(javascript) + scope.to_bson(buffer) + buffer.replace_int32(position, buffer.length - position) end # Deserialize a code with scope from BSON. # - # @param [ BSON ] bson The encoded code with scope. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ TrueClass, FalseClass ] The decoded code with scope. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - bson.read(4) # Throw away the total length. - new(bson.read(Int32.from_bson(bson)).from_bson_string.chop!, ::Hash.from_bson(bson)) + def self.from_bson(buffer) + buffer.get_int32 # Throw away the total length. + new(buffer.get_string, ::Hash.from_bson(buffer)) end # Register this type when the module is loaded. diff --git a/lib/bson/date.rb b/lib/bson/date.rb index 232ea6eb5..782368c2b 100644 --- a/lib/bson/date.rb +++ b/lib/bson/date.rb @@ -34,8 +34,8 @@ module Date # @see http://bsonspec.org/#/specification # # @since 2.1.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - ::Time.utc(year, month, day).to_bson(encoded) + def to_bson(buffer = ByteBuffer.new) + ::Time.utc(year, month, day).to_bson(buffer) end # Get the BSON type for the date. diff --git a/lib/bson/date_time.rb b/lib/bson/date_time.rb index 45a3fa222..ee01e2229 100644 --- a/lib/bson/date_time.rb +++ b/lib/bson/date_time.rb @@ -34,8 +34,8 @@ module DateTime # @see http://bsonspec.org/#/specification # # @since 2.1.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - to_time.to_bson(encoded) + def to_bson(buffer = ByteBuffer.new) + to_time.to_bson(buffer) end end diff --git a/lib/bson/encodable.rb b/lib/bson/encodable.rb deleted file mode 100644 index bf727552b..000000000 --- a/lib/bson/encodable.rb +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (C) 2009-2014 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. - -module BSON - - # Defines behaviour around objects that can be encoded. - # - # @since 2.0.0 - module Encodable - - # A 4 byte placeholder that would be replaced by a length at a later point. - # - # @since 2.0.0 - PLACEHOLDER = 0.to_bson.freeze - - # Adjustment value for total number of document bytes. - # - # @since 2.0.0 - BSON_ADJUST = 0.freeze - - # Adjustment value for total number of string bytes. - # - # @since 2.0.0 - STRING_ADJUST = -4.freeze - - # Encodes BSON to raw bytes, for types that require the length of the - # entire bytes to be present as the first word of the encoded string. This - # includes Hash, CodeWithScope. - # - # @example Encode the BSON with placeholder bytes. - # hash.encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded| - # each do |field, value| - # value.to_bson(encoded) - # end - # end - # - # @param [ Integer ] adjust The number of bytes to adjust with. - # @param [ String ] encoded The string to encode. - # - # @return [ String ] The encoded string. - # - # @since 2.0.0 - def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BINARY)) - pos = encoded.bytesize - encoded << PLACEHOLDER - yield(encoded) - encoded << NULL_BYTE - encoded.set_int32(pos, encoded.bytesize - pos + adjust) - encoded - end - - # Encodes binary data with a generic placeholder value to be written later - # once all bytes have been written. - # - # @example Encode the BSON with placeholder bytes. - # string.encode_binary_data_with_placeholder(encoded) do |encoded| - # each do |field, value| - # value.to_bson(encoded) - # end - # end - # - # @param [ String ] encoded The string to encode. - # - # @return [ String ] The encoded string. - # - # @since 2.0.0 - def encode_binary_data_with_placeholder(encoded = ''.force_encoding(BINARY)) - pos = encoded.bytesize - encoded << PLACEHOLDER - yield(encoded) - encoded.set_int32(pos, encoded.bytesize - pos - 5) - encoded - end - end -end diff --git a/lib/bson/false_class.rb b/lib/bson/false_class.rb index 9a2bd61e2..c4940a1cb 100644 --- a/lib/bson/false_class.rb +++ b/lib/bson/false_class.rb @@ -49,8 +49,8 @@ def bson_type # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded << FALSE_BYTE + def to_bson(buffer = ByteBuffer.new) + buffer.put_byte(FALSE_BYTE) end end diff --git a/lib/bson/float.rb b/lib/bson/float.rb index 1e2e3aa22..fa1b70e89 100644 --- a/lib/bson/float.rb +++ b/lib/bson/float.rb @@ -42,29 +42,23 @@ module Float # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded << [ self ].pack(PACK) + def to_bson(buffer = ByteBuffer.new) + buffer.put_double(self) end module ClassMethods # Deserialize an instance of a Float from a BSON double. # - # @param [ BSON ] bson The encoded double. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Float ] The decoded Float. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) - from_bson_double(bson.read(8)) - end - - private - - def from_bson_double(double) - double.unpack(PACK).first + def from_bson(buffer) + buffer.get_double end end diff --git a/lib/bson/hash.rb b/lib/bson/hash.rb index 5feac584e..088bc22c5 100644 --- a/lib/bson/hash.rb +++ b/lib/bson/hash.rb @@ -21,7 +21,6 @@ module BSON # # @since 2.0.0 module Hash - include Encodable # An hash (embedded document) is type 0x03 in the BSON spec. # @@ -38,14 +37,16 @@ module Hash # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded| - each do |field, value| - encoded << value.bson_type - field.to_bson_key(encoded) - value.to_bson(encoded) - end + def to_bson(buffer = ByteBuffer.new) + position = buffer.length + buffer.put_int32(0) + each do |field, value| + buffer.put_byte(value.bson_type) + buffer.put_cstring(field.to_bson_key) + value.to_bson(buffer) end + buffer.put_byte(NULL_BYTE) + buffer.replace_int32(position, buffer.length - position) end # Converts the hash to a normalized value in a BSON document. @@ -64,19 +65,19 @@ module ClassMethods # Deserialize the hash from BSON. # - # @param [ IO ] bson The bson representing a hash. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Array ] The decoded hash. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) + def from_bson(buffer) hash = Document.allocate - bson.read(4) # Swallow the first four bytes. - while (type = bson.readbyte.chr) != NULL_BYTE - field = bson.gets(NULL_BYTE).from_bson_string.chop! - hash.store(field, BSON::Registry.get(type).from_bson(bson)) + buffer.get_int32 # Throw away the size - todo: just move read position? + while (type = buffer.get_byte) != NULL_BYTE + field = buffer.get_cstring + hash.store(field, BSON::Registry.get(type).from_bson(buffer)) end hash end diff --git a/lib/bson/int32.rb b/lib/bson/int32.rb index f2efd3aad..27a68ec5d 100644 --- a/lib/bson/int32.rb +++ b/lib/bson/int32.rb @@ -27,6 +27,11 @@ class Int32 # @since 2.0.0 BSON_TYPE = 16.chr.force_encoding(BINARY).freeze + # The number of bytes constant. + # + # @since 4.0.0 + BYTES_LENGTH = 4 + # Constant for the int 32 pack directive. # # @since 2.0.0 @@ -34,21 +39,15 @@ class Int32 # Deserialize an Integer from BSON. # - # @param [ BSON ] bson The encoded int32. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Integer ] The decoded Integer. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - from_bson_int32(bson.read(4)) - end - - private - - def self.from_bson_int32(bytes) - bytes.unpack(PACK).first + def self.from_bson(buffer) + buffer.get_int32 end # Register this type when the module is loaded. diff --git a/lib/bson/int64.rb b/lib/bson/int64.rb index cbb89f5ac..012a136ae 100644 --- a/lib/bson/int64.rb +++ b/lib/bson/int64.rb @@ -34,21 +34,15 @@ class Int64 # Deserialize an Integer from BSON. # - # @param [ BSON ] bson The encoded int64. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Integer ] The decoded Integer. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - from_bson_int64(bson.read(8)) - end - - private - - def self.from_bson_int64(bytes) - bytes.unpack(PACK).first + def self.from_bson(buffer) + buffer.get_int64 end # Register this type when the module is loaded. diff --git a/lib/bson/integer.rb b/lib/bson/integer.rb index 6e451909a..563ee14d5 100644 --- a/lib/bson/integer.rb +++ b/lib/bson/integer.rb @@ -22,16 +22,6 @@ module BSON # @since 2.0.0 module Integer - # A 32bit integer is type 0x10 in the BSON spec. - # - # @since 2.0.0 - INT32_TYPE = 16.chr.force_encoding(BINARY).freeze - - # A 64bit integer is type 0x12 in the BSON spec. - # - # @since 2.0.0 - INT64_TYPE = 18.chr.force_encoding(BINARY).freeze - # The maximum 32 bit integer value. # # @since 2.0.0 @@ -100,7 +90,7 @@ def bson_int64? # # @since 2.0.0 def bson_type - bson_int32? ? INT32_TYPE : (bson_int64? ? INT64_TYPE : out_of_range!) + bson_int32? ? Int32::BSON_TYPE : (bson_int64? ? Int64::BSON_TYPE : out_of_range!) end # Get the integer as encoded BSON. @@ -113,11 +103,11 @@ def bson_type # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) + def to_bson(buffer = ByteBuffer.new) if bson_int32? - to_bson_int32(encoded) + buffer.put_int32(self) elsif bson_int64? - to_bson_int64(encoded) + buffer.put_int64(self) else out_of_range! end @@ -155,12 +145,8 @@ def to_bson_int64(encoded) encoded << ((self >> 56) & 255) end - def to_bson_key(encoded = ''.force_encoding(BINARY)) - if self < BSON_INDEX_SIZE - encoded << BSON_ARRAY_INDEXES[self] - else - self.to_s.to_bson_cstring(encoded) - end + def to_bson_key(buffer = ByteBuffer.new) + buffer.put_cstring(to_s) end private diff --git a/lib/bson/nil_class.rb b/lib/bson/nil_class.rb index 3ed2603ba..afb806ce9 100644 --- a/lib/bson/nil_class.rb +++ b/lib/bson/nil_class.rb @@ -21,37 +21,25 @@ module BSON # # @since 2.0.0 module NilClass + include Specialized # A nil is type 0x0A in the BSON spec. # # @since 2.0.0 BSON_TYPE = 10.chr.force_encoding(BINARY).freeze - # Get the nil as encoded BSON. - # - # @example Get the nil as encoded BSON. - # nil.to_bson - # - # @return [ String ] An empty string. - # - # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded - end - module ClassMethods + # Deserialize NilClass from BSON. # - # @param [ BSON ] bson The encoded Null value. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ nil ] The decoded nil value. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) + def from_bson(buffer) nil end end diff --git a/lib/bson/object.rb b/lib/bson/object.rb index 7d6874c5e..bde31f223 100644 --- a/lib/bson/object.rb +++ b/lib/bson/object.rb @@ -31,7 +31,7 @@ module Object # @see http://bsonspec.org/#/specification # # @since 2.2.4 - def to_bson_key(encoded = ''.force_encoding(BINARY)) + def to_bson_key(buffer = ByteBuffer.new) raise InvalidKey.new(self) end diff --git a/lib/bson/object_id.rb b/lib/bson/object_id.rb index 0b8c041e7..fc52f8300 100644 --- a/lib/bson/object_id.rb +++ b/lib/bson/object_id.rb @@ -44,7 +44,7 @@ class ObjectId # @since 2.0.0 def ==(other) return false unless other.is_a?(ObjectId) - to_bson == other.to_bson + generate_data == other.send(:generate_data) end alias :eql? :== @@ -86,7 +86,7 @@ def as_json(*args) # # @since 2.0.0 def <=>(other) - to_bson <=> other.to_bson + generate_data <=> other.send(:generate_data) end # Return the UTC time at which this ObjectId was generated. This may @@ -100,7 +100,7 @@ def <=>(other) # # @since 2.0.0 def generation_time - ::Time.at(to_bson.unpack("N")[0]).utc + ::Time.at(generate_data.unpack("N")[0]).utc end # Get the hash value for the object id. @@ -112,7 +112,7 @@ def generation_time # # @since 2.0.0 def hash - to_bson.hash + generate_data.hash end # Get a nice string for use with object inspection. @@ -136,7 +136,7 @@ def inspect # # @since 2.0.0 def marshal_dump - to_bson + generate_data end # Unmarshal the data into an object id. @@ -168,10 +168,8 @@ def marshal_load(data) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - repair if defined?(@data) - @raw_data ||= @@generator.next_object_id - encoded << @raw_data + def to_bson(buffer = ByteBuffer.new) + buffer.put_bytes(generate_data) end # Get the string representation of the object id. @@ -183,7 +181,7 @@ def to_bson(encoded = ''.force_encoding(BINARY)) # # @since 2.0.0 def to_s - to_bson.to_hex_string.force_encoding(UTF8) + generate_data.to_hex_string.force_encoding(UTF8) end alias :to_str :to_s @@ -194,6 +192,11 @@ class Invalid < RuntimeError; end private + def generate_data + repair if defined?(@data) + @raw_data ||= @@generator.next_object_id + end + def repair @raw_data = @data.to_bson_object_id remove_instance_variable(:@data) @@ -206,13 +209,13 @@ class << self # @example Get the object id from BSON. # ObjectId.from_bson(bson) # - # @param [ String ] bson The raw BSON bytes. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ BSON::ObjectId ] The object id. # # @since 2.0.0 - def from_bson(bson) - from_data(bson.read(12)) + def from_bson(buffer) + from_data(buffer.get_bytes(12)) end # Create a new object id from raw bytes. diff --git a/lib/bson/regexp.rb b/lib/bson/regexp.rb index bf7f742ae..93da0ad5c 100644 --- a/lib/bson/regexp.rb +++ b/lib/bson/regexp.rb @@ -84,9 +84,9 @@ def as_json(*args) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - source.to_bson_cstring(encoded) - bson_options.to_bson_cstring(encoded) + def to_bson(buffer = ByteBuffer.new) + buffer.put_cstring(source) + buffer.put_cstring(bson_options) end private @@ -168,15 +168,15 @@ module ClassMethods # Deserialize the regular expression from BSON. # - # @param [ BSON ] bson The bson representing a regular expression. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Regexp ] The decoded regular expression. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) - pattern = bson.gets(NULL_BYTE).from_bson_string.chop! + def from_bson(buffer) + pattern = buffer.get_cstring options = 0 while (option = bson.readbyte.chr) != NULL_BYTE case option diff --git a/lib/bson/specialized.rb b/lib/bson/specialized.rb index 1afb73dc2..03df6a2df 100644 --- a/lib/bson/specialized.rb +++ b/lib/bson/specialized.rb @@ -45,8 +45,8 @@ def ==(other) # @return [ String ] An empty string. # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded + def to_bson(buffer = ByteBuffer.new) + buffer end private @@ -57,16 +57,16 @@ def self.included(klass) module ClassMethods - # Deserialize MinKey from BSON. + # Deserialize from BSON. # - # @param [ BSON ] bson The encoded MinKey. + # @param [ ByteBuffer ] buffer The byte buffer. # - # @return [ MinKey ] The decoded MinKey. + # @return [ Specialized ] The decoded specialized class. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) + def from_bson(buffer) new end end diff --git a/lib/bson/string.rb b/lib/bson/string.rb index e08b0267f..587b8fc45 100644 --- a/lib/bson/string.rb +++ b/lib/bson/string.rb @@ -22,7 +22,6 @@ module BSON # # @since 2.0.0 module String - include Encodable # A string is type 0x02 in the BSON spec. # @@ -41,10 +40,8 @@ module String # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encode_with_placeholder_and_null(STRING_ADJUST, encoded) do |encoded| - to_bson_string(encoded) - end + def to_bson(buffer = ByteBuffer.new) + buffer.put_string(self) end # Get the string as a BSON key name encoded C string with checking for special characters. @@ -59,25 +56,8 @@ def to_bson(encoded = ''.force_encoding(BINARY)) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson_key(encoded = ''.force_encoding(BINARY)) - to_bson_cstring(encoded) - end - - # Get the string as an encoded C string. - # - # @example Get the string as an encoded C string. - # "test".to_bson_cstring - # - # @raise [ EncodingError ] If the string is not UTF-8. - # - # @return [ String ] The encoded string. - # - # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 - def to_bson_cstring(encoded = ''.force_encoding(BINARY)) - check_for_illegal_characters! - to_bson_string(encoded) << NULL_BYTE + def to_bson_key + self end # Convert the string to an object id. This will only work for strings of size @@ -97,27 +77,6 @@ def to_bson_object_id ObjectId.repair(self) end - # Convert the string to a UTF-8 string then force to binary. This is so - # we get errors for strings that are not UTF-8 encoded. - # - # @example Convert to valid BSON string. - # "Straße".to_bson_string - # - # @raise [ EncodingError ] If the string is not UTF-8. - # - # @return [ String ] The binary string. - # - # @since 2.0.0 - def to_bson_string(encoded = ''.force_encoding(BINARY)) - begin - to_utf8_binary(encoded) - rescue EncodingError - data = dup.force_encoding(UTF8) - raise unless data.valid_encoding? - encoded << data.force_encoding(BINARY) - end - end - # Convert the string to a hexidecimal representation. # # @example Convert the string to hex. @@ -130,62 +89,19 @@ def to_hex_string unpack("H*")[0] end - # Take the binary string and return a UTF-8 encoded string. - # - # @example Convert from a BSON string. - # "\x00".from_bson_string - # - # @raise [ EncodingError ] If the string is not UTF-8. - # - # @return [ String ] The UTF-8 string. - # - # @since 2.0.0 - def from_bson_string - force_encoding(UTF8) - end - - # Set four bytes for int32 in a binary string and return it. - # - # @example Set int32 in a BSON string. - # "".set_int32(pos, int32) - # - # @param [ Fixnum ] The position to set. - # @param [ Fixnum ] The int32 value. - # - # @return [ String ] The binary string. - # - # @since 2.0.0 - def set_int32(pos, int32) - self[pos, 4] = [ int32 ].pack(Int32::PACK) - end - - private - - def to_utf8_binary(encoded) - encoded << encode(UTF8).force_encoding(BINARY) - end - module ClassMethods # Deserialize a string from BSON. # - # @param [ BSON ] bson The bson representing a string. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Regexp ] The decoded string. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) - bson.read(Int32.from_bson(bson)).from_bson_string.chop! - end - end - - private - - def check_for_illegal_characters! - if include?(NULL_BYTE) - raise(ArgumentError, "Illegal C-String '#{self}' contains a null byte.") + def from_bson(buffer) + buffer.get_string end end diff --git a/lib/bson/symbol.rb b/lib/bson/symbol.rb index fa7dd7edc..c7c9851a2 100644 --- a/lib/bson/symbol.rb +++ b/lib/bson/symbol.rb @@ -40,8 +40,8 @@ module Symbol # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - to_s.to_bson(encoded) + def to_bson(buffer = ByteBuffer.new) + to_s.to_bson(buffer) end # Get the symbol as a BSON key name encoded C symbol. @@ -54,8 +54,8 @@ def to_bson(encoded = ''.force_encoding(BINARY)) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson_key(encoded = ''.force_encoding(BINARY)) - to_s.to_bson_key(encoded) + def to_bson_key + to_s.to_bson_key end # Converts the symbol to a normalized key in a BSON document. @@ -71,17 +71,18 @@ def to_bson_normalized_key end module ClassMethods + # Deserialize a symbol from BSON. # - # @param [ BSON ] bson The bson representing a symbol. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Regexp ] The decoded symbol. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) - bson.read(Int32.from_bson(bson)).from_bson_string.chop!.intern + def from_bson(buffer) + buffer.get_string.intern end end diff --git a/lib/bson/time.rb b/lib/bson/time.rb index 8bd8fc439..076b0b49c 100644 --- a/lib/bson/time.rb +++ b/lib/bson/time.rb @@ -37,23 +37,23 @@ module Time # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded << [ (to_i * 1000) + (usec / 1000) ].pack(Int64::PACK) + def to_bson(buffer = ByteBuffer.new) + buffer.put_int64((to_i * 1000) + (usec / 1000)) end module ClassMethods # Deserialize UTC datetime from BSON. # - # @param [ BSON ] bson The bson representing UTC datetime. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Time ] The decoded UTC datetime. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def from_bson(bson) - seconds, fragment = Int64.from_bson(bson).divmod(1000) + def from_bson(buffer) + seconds, fragment = Int64.from_bson(buffer).divmod(1000) at(seconds, fragment * 1000).utc end end diff --git a/lib/bson/timestamp.rb b/lib/bson/timestamp.rb index 826611d21..1fdda8896 100644 --- a/lib/bson/timestamp.rb +++ b/lib/bson/timestamp.rb @@ -87,22 +87,24 @@ def initialize(seconds, increment) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - increment.to_bson_int32(encoded) - seconds.to_bson_int32(encoded) + def to_bson(buffer = ByteBuffer.new) + buffer.put_int32(increment) + buffer.put_int32(seconds) end # Deserialize timestamp from BSON. # - # @param [ BSON ] bson The bson representing a timestamp. + # @param [ ByteBuffer ] buffer The byte buffer. # # @return [ Timestamp ] The decoded timestamp. # # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(bson) - new(*bson.read(8).unpack(Int32::PACK * 2).reverse) + def self.from_bson(buffer) + increment = buffer.get_int32 + seconds = buffer.get_int32 + new(seconds, increment) end # Register this type when the module is loaded. diff --git a/lib/bson/true_class.rb b/lib/bson/true_class.rb index c3094a084..59bbc2ce6 100644 --- a/lib/bson/true_class.rb +++ b/lib/bson/true_class.rb @@ -49,8 +49,8 @@ def bson_type # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded << TRUE_BYTE + def to_bson(buffer = ByteBuffer.new) + buffer.put_byte(TRUE_BYTE) end end diff --git a/lib/bson/undefined.rb b/lib/bson/undefined.rb index 5fbc2c184..e09659632 100644 --- a/lib/bson/undefined.rb +++ b/lib/bson/undefined.rb @@ -20,6 +20,7 @@ module BSON # # @since 2.0.0 class Undefined + include Specialized # Undefined is type 0x06 in the BSON spec. # @@ -40,32 +41,6 @@ def ==(other) self.class == other.class end - # Encode the Undefined field - has no value since it only needs the type - # and field name when being encoded. - # - # @example Encode the undefined value. - # Undefined.to_bson - # - # @return [ String ] An empty string. - # - # @since 2.0.0 - def to_bson(encoded = ''.force_encoding(BINARY)) - encoded - end - - # Deserialize undefined BSON type from BSON. - # - # @param [ BSON ] bson The encoded undefined value. - # - # @return [ Undefined ] The decoded undefined value. - # - # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 - def self.from_bson(bson) - new - end - # Register this type when the module is loaded. # # @since 2.0.0 diff --git a/lib/bson/version.rb b/lib/bson/version.rb index 3cb191739..de86407ea 100644 --- a/lib/bson/version.rb +++ b/lib/bson/version.rb @@ -13,5 +13,5 @@ # limitations under the License. module BSON - VERSION = "3.2.6" + VERSION = "4.0.0" end diff --git a/perf/bench.rb b/perf/bench.rb index f3ee3ec1f..0bdfb18c9 100644 --- a/perf/bench.rb +++ b/perf/bench.rb @@ -14,7 +14,6 @@ $:.unshift File.join(File.dirname(__FILE__), "..", "lib") require "benchmark" -require "ruby-prof" def benchmark! count = 1_000_000 @@ -30,158 +29,136 @@ def benchmark! count.times { document.to_bson } end - bench.report("Binary#to_bson -------->") do - count.times { BSON::Binary.new("test", :generic).to_bson } - end - - bench.report("Code#to_bson ---------->") do - count.times { BSON::Code.new("this.value = 1").to_bson } - end - - bench.report("FalseClass#to_bson ---->") do - count.times { false.to_bson } - end - - bench.report("Float#to_bson --------->") do - count.times { 1.131312.to_bson } - end - - bench.report("Integer#to_bson ------->") do - count.times { 1024.to_bson } - end - - bench.report("MaxKey#to_bson -------->") do - count.times { BSON::MaxKey.new.to_bson } - end - - bench.report("MinKey#to_bson -------->") do - count.times { BSON::MinKey.new.to_bson } - end - - bench.report("ObjectId#to_bson ------>") do - count.times { BSON::ObjectId.new.to_bson } - end - - bench.report("ObjectId#to_s --------->") do - object_id = BSON::ObjectId.new - count.times { object_id.to_s } - end - - bench.report("Regexp#to_bson -------->") do - count.times { %r{\d+}.to_bson } - end - - bench.report("String#to_bson -------->") do - count.times { "testing".to_bson } - end - - bench.report("Symbol#to_bson -------->") do - count.times { "testing".to_bson } - end - - bench.report("Time#to_bson ---------->") do - count.times { Time.new.to_bson } - end - - bench.report("TrueClass#to_bson ----->") do - count.times { true.to_bson } - end - - boolean_bytes = true.to_bson - bench.report("Boolean#from_bson ----->") do - count.times { BSON::Boolean.from_bson(StringIO.new(boolean_bytes)) } - end - - int32_bytes = 1024.to_bson - bench.report("Int32#from_bson ------->") do - count.times { BSON::Int32.from_bson(StringIO.new(int32_bytes)) } - end - - int64_bytes = (BSON::Integer::MAX_32BIT + 1).to_bson - bench.report("Int64#from_bson ------->") do - count.times { BSON::Int64.from_bson(StringIO.new(int64_bytes)) } - end - - float_bytes = 1.23131.to_bson - bench.report("Float#from_bson ------->") do - count.times { Float.from_bson(StringIO.new(float_bytes)) } - end - - binary_bytes = BSON::Binary.new("test", :generic).to_bson - bench.report("Binary#from_bson ------>") do - count.times { BSON::Binary.from_bson(StringIO.new(binary_bytes)) } - end - - code_bytes = BSON::Code.new("this.value = 1").to_bson - bench.report("Code#from_bson -------->") do - count.times { BSON::Code.from_bson(StringIO.new(code_bytes)) } - end - - false_bytes = false.to_bson - bench.report("Boolean#from_bson ----->") do - count.times { BSON::Boolean.from_bson(StringIO.new(false_bytes)) } - end - - max_key_bytes = BSON::MaxKey.new.to_bson - bench.report("MaxKey#from_bson ------>") do - count.times { BSON::MaxKey.from_bson(StringIO.new(max_key_bytes)) } - end - - min_key_bytes = BSON::MinKey.new.to_bson - bench.report("MinKey#from_bson ------>") do - count.times { BSON::MinKey.from_bson(StringIO.new(min_key_bytes)) } - end - - object_id_bytes = BSON::ObjectId.new.to_bson - bench.report("ObjectId#from_bson ---->") do - count.times { BSON::ObjectId.from_bson(StringIO.new(object_id_bytes)) } - end - - regex_bytes = %r{\d+}.to_bson - bench.report("Regexp#from_bson ------>") do - count.times { Regexp.from_bson(StringIO.new(regex_bytes)) } - end - - string_bytes = "testing".to_bson - bench.report("String#from_bson ------>") do - count.times { String.from_bson(StringIO.new(string_bytes)) } - end - - symbol_bytes = "testing".to_bson - bench.report("Symbol#from_bson ------>") do - count.times { Symbol.from_bson(StringIO.new(symbol_bytes)) } - end - - time_bytes = Time.new.to_bson - bench.report("Time#from_bson -------->") do - count.times { Time.from_bson(StringIO.new(time_bytes)) } - end - - doc_bytes = document.to_bson + # bench.report("Binary#to_bson -------->") do + # count.times { BSON::Binary.new("test", :generic).to_bson } + # end + + # bench.report("Code#to_bson ---------->") do + # count.times { BSON::Code.new("this.value = 1").to_bson } + # end + + # bench.report("FalseClass#to_bson ---->") do + # count.times { false.to_bson } + # end + + # bench.report("Float#to_bson --------->") do + # count.times { 1.131312.to_bson } + # end + + # bench.report("Integer#to_bson ------->") do + # count.times { 1024.to_bson } + # end + + # bench.report("MaxKey#to_bson -------->") do + # count.times { BSON::MaxKey.new.to_bson } + # end + + # bench.report("MinKey#to_bson -------->") do + # count.times { BSON::MinKey.new.to_bson } + # end + + # bench.report("ObjectId#to_bson ------>") do + # count.times { BSON::ObjectId.new.to_bson } + # end + + # bench.report("ObjectId#to_s --------->") do + # object_id = BSON::ObjectId.new + # count.times { object_id.to_s } + # end + + # bench.report("Regexp#to_bson -------->") do + # count.times { %r{\d+}.to_bson } + # end + + # bench.report("String#to_bson -------->") do + # count.times { "testing".to_bson } + # end + + # bench.report("Symbol#to_bson -------->") do + # count.times { "testing".to_bson } + # end + + # bench.report("Time#to_bson ---------->") do + # count.times { Time.new.to_bson } + # end + + # bench.report("TrueClass#to_bson ----->") do + # count.times { true.to_bson } + # end + + # boolean_bytes = true.to_bson + # bench.report("Boolean#from_bson ----->") do + # count.times { BSON::Boolean.from_bson(StringIO.new(boolean_bytes)) } + # end + + # int32_bytes = 1024.to_bson + # bench.report("Int32#from_bson ------->") do + # count.times { BSON::Int32.from_bson(StringIO.new(int32_bytes)) } + # end + + # int64_bytes = (BSON::Integer::MAX_32BIT + 1).to_bson + # bench.report("Int64#from_bson ------->") do + # count.times { BSON::Int64.from_bson(StringIO.new(int64_bytes)) } + # end + + # float_bytes = 1.23131.to_bson + # bench.report("Float#from_bson ------->") do + # count.times { Float.from_bson(StringIO.new(float_bytes)) } + # end + + # binary_bytes = BSON::Binary.new("test", :generic).to_bson + # bench.report("Binary#from_bson ------>") do + # count.times { BSON::Binary.from_bson(StringIO.new(binary_bytes)) } + # end + + # code_bytes = BSON::Code.new("this.value = 1").to_bson + # bench.report("Code#from_bson -------->") do + # count.times { BSON::Code.from_bson(StringIO.new(code_bytes)) } + # end + + # false_bytes = false.to_bson + # bench.report("Boolean#from_bson ----->") do + # count.times { BSON::Boolean.from_bson(StringIO.new(false_bytes)) } + # end + + # max_key_bytes = BSON::MaxKey.new.to_bson + # bench.report("MaxKey#from_bson ------>") do + # count.times { BSON::MaxKey.from_bson(StringIO.new(max_key_bytes)) } + # end + + # min_key_bytes = BSON::MinKey.new.to_bson + # bench.report("MinKey#from_bson ------>") do + # count.times { BSON::MinKey.from_bson(StringIO.new(min_key_bytes)) } + # end + + # object_id_bytes = BSON::ObjectId.new.to_bson + # bench.report("ObjectId#from_bson ---->") do + # count.times { BSON::ObjectId.from_bson(StringIO.new(object_id_bytes)) } + # end + + # regex_bytes = %r{\d+}.to_bson + # bench.report("Regexp#from_bson ------>") do + # count.times { Regexp.from_bson(StringIO.new(regex_bytes)) } + # end + + # string_bytes = "testing".to_bson + # bench.report("String#from_bson ------>") do + # count.times { String.from_bson(StringIO.new(string_bytes)) } + # end + + # symbol_bytes = "testing".to_bson + # bench.report("Symbol#from_bson ------>") do + # count.times { Symbol.from_bson(StringIO.new(symbol_bytes)) } + # end + + # time_bytes = Time.new.to_bson + # bench.report("Time#from_bson -------->") do + # count.times { Time.from_bson(StringIO.new(time_bytes)) } + # end + + doc_bytes = document.to_bson.to_s bench.report("Document#from_bson ---->") do - count.times { BSON::Document.from_bson(StringIO.new(doc_bytes)) } + count.times { BSON::Document.from_bson(BSON::ByteBuffer.new(doc_bytes)) } end end end - -def profile! - count = 1_000 - - document = BSON::Document.new(field1: 'testing', field2: 'testing') - embedded = 5.times.map do |i| - BSON::Document.new(field1: 10, field2: 'test') - end - document[:embedded] = embedded - - document_serialization = RubyProf.profile do - count.times { document.to_bson } - end - - doc_bytes = document.to_bson - document_deserialization = RubyProf.profile do - count.times { BSON::Document.from_bson(StringIO.new(doc_bytes)) } - end - - RubyProf::GraphPrinter.new(document_serialization).print($stdout) - RubyProf::GraphPrinter.new(document_deserialization).print($stdout) -end diff --git a/spec/bson/array_spec.rb b/spec/bson/array_spec.rb index fdd055e42..592b69cea 100644 --- a/spec/bson/array_spec.rb +++ b/spec/bson/array_spec.rb @@ -21,7 +21,7 @@ let(:type) { 4.chr } let(:obj) {[ "one", "two" ]} let(:bson) do - BSON::Document["0", "one", "1", "two"].to_bson + BSON::Document["0", "one", "1", "two"].to_bson.to_s end it_behaves_like "a bson element" diff --git a/spec/bson/byte_buffer_spec.rb b/spec/bson/byte_buffer_spec.rb new file mode 100644 index 000000000..a211d51be --- /dev/null +++ b/spec/bson/byte_buffer_spec.rb @@ -0,0 +1,445 @@ +require 'spec_helper' + +describe BSON::ByteBuffer do + + describe '#allocate' do + + let(:buffer) do + described_class.allocate + end + + it 'allocates a buffer' do + expect(buffer).to be_a(BSON::ByteBuffer) + end + end + + describe '#get_byte' do + + let(:buffer) do + described_class.new(BSON::Int32::BSON_TYPE) + end + + let!(:byte) do + buffer.get_byte + end + + it 'gets the byte from the buffer' do + expect(byte).to eq(BSON::Int32::BSON_TYPE) + end + + it 'increments the read position by 1' do + expect(buffer.read_position).to eq(1) + end + end + + describe '#get_bytes' do + + let(:string) do + "#{BSON::Int32::BSON_TYPE}#{BSON::Int32::BSON_TYPE}" + end + + let(:buffer) do + described_class.new(string) + end + + let!(:bytes) do + buffer.get_bytes(2) + end + + it 'gets the bytes from the buffer' do + expect(bytes).to eq(string) + end + + it 'increments the position by the length' do + expect(buffer.read_position).to eq(string.bytesize) + end + end + + describe '#get_cstring' do + + let(:buffer) do + described_class.new("testing#{BSON::NULL_BYTE}") + end + + let!(:string) do + buffer.get_cstring + end + + it 'gets the cstring from the buffer' do + expect(string).to eq("testing") + end + + it 'increments the position by string length + 1' do + expect(buffer.read_position).to eq(8) + end + end + + describe '#get_double' do + + let(:buffer) do + described_class.new("#{12.5.to_bson.to_s}") + end + + let!(:double) do + buffer.get_double + end + + it 'gets the double from the buffer' do + expect(double).to eq(12.5) + end + + it 'increments the read position by 8' do + expect(buffer.read_position).to eq(8) + end + end + + describe '#get_int32' do + + let(:buffer) do + described_class.new("#{12.to_bson.to_s}") + end + + let!(:int32) do + buffer.get_int32 + end + + it 'gets the int32 from the buffer' do + expect(int32).to eq(12) + end + + it 'increments the position by 4' do + expect(buffer.read_position).to eq(4) + end + end + + describe '#get_int64' do + + let(:buffer) do + described_class.new("#{(Integer::MAX_64BIT - 1).to_bson.to_s}") + end + + let!(:int64) do + buffer.get_int64 + end + + it 'gets the int64 from the buffer' do + expect(int64).to eq(Integer::MAX_64BIT - 1) + end + + it 'increments the position by 8' do + expect(buffer.read_position).to eq(8) + end + end + + describe '#get_string' do + + let(:buffer) do + described_class.new("#{8.to_bson.to_s}testing#{BSON::NULL_BYTE}") + end + + let!(:string) do + buffer.get_string + end + + it 'gets the string from the buffer' do + expect(string).to eq("testing") + end + + it 'increments the position by string length + 5' do + expect(buffer.read_position).to eq(12) + end + end + + describe '#length' do + + let(:buffer) do + described_class.new + end + + before do + buffer.put_int32(5) + end + + it 'returns the length of the buffer' do + expect(buffer.length).to eq(4) + end + end + + describe '#put_byte' do + + let(:buffer) do + described_class.new + end + + let!(:modified) do + buffer.put_byte(BSON::Int32::BSON_TYPE) + end + + it 'appends the byte to the byte buffer' do + expect(modified.to_s).to eq(BSON::Int32::BSON_TYPE.chr) + end + + it 'increments the write position by 1' do + expect(modified.write_position).to eq(1) + end + end + + describe '#put_cstring' do + + let(:buffer) do + described_class.new + end + + context 'when the string is valid' do + + let!(:modified) do + buffer.put_cstring('testing') + end + + it 'appends the string plus null byte to the byte buffer' do + expect(modified.to_s).to eq("testing#{BSON::NULL_BYTE}") + end + + it 'increments the write position by the length + 1' do + expect(modified.write_position).to eq(8) + end + end + + context "when the string contains a null byte" do + + let(:string) do + "test#{BSON::NULL_BYTE}ing" + end + + it "raises an error" do + expect { + buffer.put_cstring(string) + }.to raise_error(ArgumentError) + end + end + end + + describe '#put_double' do + + let(:buffer) do + described_class.new + end + + let!(:modified) do + buffer.put_double(1.2332) + end + + it 'appends the double to the buffer' do + expect(modified.to_s).to eq([ 1.2332 ].pack(Float::PACK)) + end + + it 'increments the write position by 8' do + expect(modified.write_position).to eq(8) + end + end + + describe '#put_int32' do + + let(:buffer) do + described_class.new + end + + context 'when the integer is 32 bit' do + + context 'when the integer is positive' do + + let!(:modified) do + buffer.put_int32(Integer::MAX_32BIT - 1) + end + + let(:expected) do + [ Integer::MAX_32BIT - 1 ].pack(BSON::Int32::PACK) + end + + it 'appends the int32 to the byte buffer' do + expect(modified.to_s).to eq(expected) + end + + it 'increments the write position by 4' do + expect(modified.write_position).to eq(4) + end + end + + context 'when the integer is negative' do + + let!(:modified) do + buffer.put_int32(Integer::MIN_32BIT + 1) + end + + let(:expected) do + [ Integer::MIN_32BIT + 1 ].pack(BSON::Int32::PACK) + end + + it 'appends the int32 to the byte buffer' do + expect(modified.to_s).to eq(expected) + end + + it 'increments the write position by 4' do + expect(modified.write_position).to eq(4) + end + end + + context 'when the integer is not 32 bit' do + + it 'raises an exception' do + expect { + buffer.put_int32(Integer::MAX_64BIT - 1) + }.to raise_error(RangeError) + end + end + end + end + + describe '#put_int64' do + + let(:buffer) do + described_class.new + end + + context 'when the integer is 64 bit' do + + context 'when the integer is positive' do + + let!(:modified) do + buffer.put_int64(Integer::MAX_64BIT - 1) + end + + let(:expected) do + [ Integer::MAX_64BIT - 1 ].pack(BSON::Int64::PACK) + end + + it 'appends the int64 to the byte buffer' do + expect(modified.to_s).to eq(expected) + end + + it 'increments the write position by 8' do + expect(modified.write_position).to eq(8) + end + end + + context 'when the integer is negative' do + + let!(:modified) do + buffer.put_int64(Integer::MIN_64BIT + 1) + end + + let(:expected) do + [ Integer::MIN_64BIT + 1 ].pack(BSON::Int64::PACK) + end + + it 'appends the int64 to the byte buffer' do + expect(modified.to_s).to eq(expected) + end + + it 'increments the write position by 8' do + expect(modified.write_position).to eq(8) + end + end + + context 'when the integer is larger than 64 bit' do + + it 'raises an exception' do + expect { + buffer.put_int64(Integer::MAX_64BIT + 1) + }.to raise_error(RangeError) + end + end + end + end + + describe '#put_string' do + + context 'when the buffer does not need to be expanded' do + + let(:buffer) do + described_class.new + end + + context 'when the string is UTF-8' do + + let!(:modified) do + buffer.put_string('testing') + end + + it 'appends the string to the byte buffer' do + expect(modified.to_s).to eq("#{8.to_bson.to_s}testing#{BSON::NULL_BYTE}") + end + + it 'increments the write position by length + 5' do + expect(modified.write_position).to eq(12) + end + end + end + + context 'when the buffer needs to be expanded' do + + let(:buffer) do + described_class.new + end + + let(:string) do + 300.times.inject(""){ |s, i| s << "#{i}" } + end + + context 'when no bytes exist in the buffer' do + + let!(:modified) do + buffer.put_string(string) + end + + it 'appends the string to the byte buffer' do + expect(modified.to_s).to eq("#{(string.bytesize + 1).to_bson.to_s}#{string}#{BSON::NULL_BYTE}") + end + + it 'increments the write position by length + 5' do + expect(modified.write_position).to eq(string.bytesize + 5) + end + end + + context 'when bytes exist in the buffer' do + + let!(:modified) do + buffer.put_int32(4).put_string(string) + end + + it 'appends the string to the byte buffer' do + expect(modified.to_s).to eq( + "#{[ 4 ].pack(BSON::Int32::PACK)}#{(string.bytesize + 1).to_bson.to_s}#{string}#{BSON::NULL_BYTE}" + ) + end + + it 'increments the write position by length + 5' do + expect(modified.write_position).to eq(string.bytesize + 9) + end + end + end + end + + describe '#replace_int32' do + + let(:buffer) do + described_class.new + end + + let(:exp_first) do + [ 5 ].pack(BSON::Int32::PACK) + end + + let(:exp_second) do + [ 4 ].pack(BSON::Int32::PACK) + end + + let(:modified) do + buffer.put_int32(0).put_int32(4).replace_int32(0, 5) + end + + it 'replaces the int32 at the location' do + expect(modified.to_s).to eq("#{exp_first}#{exp_second}") + end + end +end diff --git a/spec/bson/code_with_scope_spec.rb b/spec/bson/code_with_scope_spec.rb index f668427d2..e4d535419 100644 --- a/spec/bson/code_with_scope_spec.rb +++ b/spec/bson/code_with_scope_spec.rb @@ -63,8 +63,8 @@ end let(:obj) { described_class.new(code, scope) } let(:bson) do - "#{47.to_bson}#{(code.length + 1).to_bson}#{code}#{BSON::NULL_BYTE}" + - "#{scope.to_bson}" + "#{47.to_bson.to_s}#{(code.length + 1).to_bson.to_s}#{code}#{BSON::NULL_BYTE}" + + "#{scope.to_bson.to_s}" end it_behaves_like "a bson element" @@ -79,7 +79,7 @@ { "name" => "test" } end let(:obj) { described_class.new(code, scope) } - let(:bson) { StringIO.new(obj.to_bson) } + let(:bson) { BSON::ByteBuffer.new(obj.to_bson.to_s) } let!(:deserialized) { described_class.from_bson(bson) } it "deserializes the javascript" do @@ -89,9 +89,5 @@ it "deserializes the scope" do expect(deserialized.scope).to eq(scope) end - - it "does not leave any extra bytes" do - expect(bson.read(1)).to be_nil - end end end diff --git a/spec/bson/document_spec.rb b/spec/bson/document_spec.rb index ec314066f..c111cdc17 100644 --- a/spec/bson/document_spec.rb +++ b/spec/bson/document_spec.rb @@ -653,11 +653,11 @@ end let(:serialized) do - document.to_bson + document.to_bson.to_s end let(:deserialized) do - described_class.from_bson(StringIO.new(serialized)) + described_class.from_bson(BSON::ByteBuffer.new(serialized)) end it 'deserializes the documents' do @@ -688,7 +688,7 @@ end it "properly serializes the symbol" do - expect(obj.to_bson).to eq(bson) + expect(obj.to_bson.to_s).to eq(bson) end end @@ -723,7 +723,7 @@ it_behaves_like "a deserializable bson element" let(:raw) do - StringIO.new(bson) + BSON::ByteBuffer.new(bson) end it "returns an instance of a BSON::Document" do @@ -768,7 +768,7 @@ end let(:deserialized) do - described_class.from_bson(StringIO.new(document.to_bson)) + described_class.from_bson(BSON::ByteBuffer.new(document.to_bson.to_s)) end it "serializes and deserializes properly" do @@ -812,30 +812,50 @@ it_behaves_like "a document able to handle utf-8" end - context "when non utf-8 values exist" do + context "when utf-8 values exist in wrong encoding" do let(:string) { "gültig" } let(:document) do described_class["type", string.encode("iso-8859-1")] end + it "raises an exception", unless: BSON::Environment.jruby? do + expect { + document.to_bson + }.to raise_error(ArgumentError) + end + + it 'converts the values', if: BSON::Environment.jruby? do + expect( + BSON::Document.from_bson(BSON::ByteBuffer.new(document.to_bson.to_s)) + ).to eq({ "type" => string }) + end + end + + context "when binary strings with utf-8 values exist", if: BSON::Environment.jruby? do + + let(:string) { "europäisch" } + let(:document) do + described_class["type", string.encode("binary")] + end + it "encodes and decodes the document properly" do expect( - BSON::Document.from_bson(StringIO.new(document.to_bson)) + BSON::Document.from_bson(BSON::ByteBuffer.new(document.to_bson.to_s)) ).to eq({ "type" => string }) end end - context "when binary strings with utf-8 values exist" do + context "when binary strings with utf-8 values exist", unless: BSON::Environment.jruby? do - let(:string) { "europäischen" } + let(:string) { "europäisch" } let(:document) do described_class["type", string.encode("binary", "binary")] end it "encodes and decodes the document properly" do expect( - BSON::Document.from_bson(StringIO.new(document.to_bson)) + BSON::Document.from_bson(BSON::ByteBuffer.new(document.to_bson.to_s)) ).to eq({ "type" => string }) end end diff --git a/spec/bson/hash_spec.rb b/spec/bson/hash_spec.rb index dee6f426d..22fccc594 100644 --- a/spec/bson/hash_spec.rb +++ b/spec/bson/hash_spec.rb @@ -29,8 +29,8 @@ end let(:bson) do - "#{20.to_bson}#{String::BSON_TYPE}key#{BSON::NULL_BYTE}" + - "#{6.to_bson}value#{BSON::NULL_BYTE}#{BSON::NULL_BYTE}" + "#{20.to_bson.to_s}#{String::BSON_TYPE}key#{BSON::NULL_BYTE}" + + "#{6.to_bson.to_s}value#{BSON::NULL_BYTE}#{BSON::NULL_BYTE}" end it_behaves_like "a serializable bson element" @@ -44,9 +44,9 @@ end let(:bson) do - "#{32.to_bson}#{Hash::BSON_TYPE}field#{BSON::NULL_BYTE}" + - "#{20.to_bson}#{String::BSON_TYPE}key#{BSON::NULL_BYTE}" + - "#{6.to_bson}value#{BSON::NULL_BYTE}#{BSON::NULL_BYTE}#{BSON::NULL_BYTE}" + "#{32.to_bson.to_s}#{Hash::BSON_TYPE}field#{BSON::NULL_BYTE}" + + "#{20.to_bson.to_s}#{String::BSON_TYPE}key#{BSON::NULL_BYTE}" + + "#{6.to_bson.to_s}value#{BSON::NULL_BYTE}#{BSON::NULL_BYTE}#{BSON::NULL_BYTE}" end it_behaves_like "a serializable bson element" diff --git a/spec/bson/int32_spec.rb b/spec/bson/int32_spec.rb index c99fa1552..361d46405 100644 --- a/spec/bson/int32_spec.rb +++ b/spec/bson/int32_spec.rb @@ -27,16 +27,18 @@ end describe "when the integer is negative" do + let(:decoded) { -1 } - let(:encoded) {StringIO.new([ -1 ].pack(BSON::Int32::PACK))} + let(:encoded) { BSON::ByteBuffer.new([ -1 ].pack(BSON::Int32::PACK)) } let(:decoded_2) { -50 } - let(:encoded_2) {StringIO.new([ -50 ].pack(BSON::Int32::PACK))} + let(:encoded_2) { BSON::ByteBuffer.new([ -50 ].pack(BSON::Int32::PACK)) } + it "decodes a -1 correctly" do expect(BSON::Int32.from_bson(encoded)).to eq(decoded) - end + end + it "decodes a -50 correctly" do expect(BSON::Int32.from_bson(encoded_2)).to eq(decoded_2) - end + end end - end diff --git a/spec/bson/integer_spec.rb b/spec/bson/integer_spec.rb index 21fa6fa04..aeb28c6e9 100644 --- a/spec/bson/integer_spec.rb +++ b/spec/bson/integer_spec.rb @@ -63,14 +63,9 @@ let(:obj) { Integer::MAX_32BIT - 1 } let(:encoded) { obj.to_s + BSON::NULL_BYTE } - let(:previous_content) { 'previous_content'.force_encoding(BSON::BINARY) } it "returns the encoded string" do - expect(obj.to_bson_key).to eq(encoded) - end - - it "appends to optional previous content" do - expect(obj.to_bson_key(previous_content)).to eq(previous_content << encoded) + expect(obj.to_bson_key.to_s).to eq(encoded) end end end diff --git a/spec/bson/object_id_spec.rb b/spec/bson/object_id_spec.rb index 9adffed53..f2b5235be 100644 --- a/spec/bson/object_id_spec.rb +++ b/spec/bson/object_id_spec.rb @@ -379,7 +379,7 @@ end it "returns a hash of the raw bytes" do - expect(object_id.hash).to eq(object_id.to_bson.hash) + expect(object_id.hash).to eq(object_id.to_bson.to_s.hash) end end @@ -488,7 +488,7 @@ let(:time) { Time.utc(2013, 1, 1) } let(:type) { 7.chr } let(:obj) { described_class.from_time(time) } - let(:bson) { obj.to_bson } + let(:bson) { obj.to_bson.to_s } it_behaves_like "a bson element" it_behaves_like "a serializable bson element" diff --git a/spec/bson/regexp_spec.rb b/spec/bson/regexp_spec.rb index d60840459..43d2b3f75 100644 --- a/spec/bson/regexp_spec.rb +++ b/spec/bson/regexp_spec.rb @@ -37,7 +37,7 @@ let(:obj) { /test/ } let(:io) do - StringIO.new(bson) + BSON::ByteBuffer.new(bson) end let(:regex) do diff --git a/spec/bson/string_spec.rb b/spec/bson/string_spec.rb index 56e78efd8..a219b57ae 100644 --- a/spec/bson/string_spec.rb +++ b/spec/bson/string_spec.rb @@ -22,109 +22,13 @@ let(:type) { 2.chr } let(:obj) { "test" } - let(:bson) { "#{5.to_bson}test#{BSON::NULL_BYTE}" } + let(:bson) { "#{5.to_bson.to_s}test#{BSON::NULL_BYTE}" } it_behaves_like "a bson element" it_behaves_like "a serializable bson element" it_behaves_like "a deserializable bson element" end - describe "#to_bson_cstring" do - - context "when the string is valid" do - - let(:string) do - "test" - end - - let(:encoded) do - string.to_bson_cstring - end - - let(:previous_content) do - 'previous_content'.force_encoding(BSON::BINARY) - end - - it "returns the encoded string" do - expect(encoded).to eq("test#{BSON::NULL_BYTE}") - end - - it_behaves_like "a binary encoded string" - - it "appends to optional previous content" do - expect(string.to_bson_cstring(previous_content)).to eq(previous_content << encoded) - end - end - - context "when the string contains a null byte" do - - let(:string) do - "test#{BSON::NULL_BYTE}ing" - end - - it "raises an error" do - expect { - string.to_bson_cstring - }.to raise_error(ArgumentError) - end - end - - context "when the string contains utf-8 characters" do - - let(:string) do - "Straße" - end - - let(:encoded) do - string.to_bson_cstring - end - - let(:char) do - "ß".chr.force_encoding(BSON::BINARY) - end - - it "returns the encoded string" do - expect(encoded).to eq("Stra#{char}e#{BSON::NULL_BYTE}") - end - - it_behaves_like "a binary encoded string" - end - - context "when the string is encoded in non utf-8" do - - let(:string) do - "Straße".encode("iso-8859-1") - end - - let(:encoded) do - string.to_bson_cstring - end - - let(:char) do - "ß".chr.force_encoding(BSON::BINARY) - end - - it "returns the encoded string" do - expect(encoded).to eq("Stra#{char}e#{BSON::NULL_BYTE}") - end - - it_behaves_like "a binary encoded string" - end - - context "when the string contains non utf-8 characters" do - - let(:string) do - 255.chr - end - - it "raises an error" do - expect { - string.to_bson_cstring - }.to raise_error(EncodingError) - end - end - end - describe "#to_bson_object_id" do context "when the string has 12 characters" do @@ -152,107 +56,6 @@ end end - describe "#to_bson_string" do - - context "when the string is valid" do - - let(:string) do - "test" - end - - let(:encoded) do - string.to_bson_string - end - - let(:previous_content) do - 'previous_content'.force_encoding(BSON::BINARY) - end - - it "returns the string" do - expect(encoded).to eq(string) - end - - it_behaves_like "a binary encoded string" - - it "appends to optional previous content" do - expect(string.to_bson_string(previous_content)).to eq(previous_content << encoded) - end - - end - - context "when the string contains a null byte" do - - let(:string) do - "test#{BSON::NULL_BYTE}ing" - end - - let(:encoded) do - string.to_bson_string - end - - it "retains the null byte" do - expect(encoded).to eq(string) - end - - it_behaves_like "a binary encoded string" - end - - context "when the string contains utf-8 characters" do - - let(:string) do - "Straße" - end - - let(:encoded) do - string.to_bson_string - end - - let(:char) do - "ß".chr.force_encoding(BSON::BINARY) - end - - it "returns the encoded string" do - expect(encoded).to eq("Stra#{char}e") - end - - it_behaves_like "a binary encoded string" - end - - context "when the string is encoded in non utf-8" do - - let(:string) do - "Straße".encode("iso-8859-1") - end - - let(:encoded) do - string.to_bson_string - end - - let(:char) do - "ß".chr.force_encoding(BSON::BINARY) - end - - it "returns the encoded string" do - expect(encoded).to eq("Stra#{char}e") - end - - it_behaves_like "a binary encoded string" - end - - context "when the string contains non utf-8 characters" do - - let(:string) do - 255.chr - end - - it "raises an error" do - expect { - string.to_bson_string - }.to raise_error(EncodingError) - end - end - end - context "when the class is loaded" do let(:registered) do @@ -267,16 +70,11 @@ describe "#to_bson_key" do let(:string) { "test" } - let(:encoded) { string.to_s + BSON::NULL_BYTE } - let(:previous_content) { 'previous_content'.force_encoding(BSON::BINARY) } + let(:encoded) { string.to_s } it "returns the encoded string" do expect(string.to_bson_key).to eq(encoded) end - - it "appends to optional previous content" do - expect(string.to_bson_key(previous_content)).to eq(previous_content << encoded) - end end describe "#to_hex_string" do diff --git a/spec/bson/symbol_spec.rb b/spec/bson/symbol_spec.rb index 33632197b..c315dd01d 100644 --- a/spec/bson/symbol_spec.rb +++ b/spec/bson/symbol_spec.rb @@ -20,7 +20,7 @@ let(:type) { 14.chr } let(:obj) { :test } - let(:bson) { "#{5.to_bson}test#{BSON::NULL_BYTE}" } + let(:bson) { "#{5.to_bson.to_s}test#{BSON::NULL_BYTE}" } it_behaves_like "a bson element" it_behaves_like "a serializable bson element" @@ -31,25 +31,10 @@ describe "#to_bson_key" do let(:symbol) { :test } - let(:encoded) { symbol.to_s + BSON::NULL_BYTE } - let(:previous_content) { 'previous_content'.force_encoding(BSON::BINARY) } + let(:encoded) { symbol.to_s } it "returns the encoded string" do expect(symbol.to_bson_key).to eq(encoded) end - - it "appends to optional previous content" do - expect(symbol.to_bson_key(previous_content)).to eq(previous_content << encoded) - end - - context 'when the symbol contains a null byte' do - let(:symbol) { :"test#{BSON::NULL_BYTE}ing" } - - it 'raises an error' do - expect { - symbol.to_bson_key - }.to raise_error(ArgumentError) - end - end end end diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb index ad8913093..81538c2a9 100644 --- a/spec/support/shared_examples.rb +++ b/spec/support/shared_examples.rb @@ -38,23 +38,15 @@ shared_examples_for "a serializable bson element" do - let(:previous_content) do - 'previous_content'.force_encoding(BSON::BINARY) - end - it "serializes to bson" do - expect(obj.to_bson).to eq(bson) - end - - it "serializes to bson by appending" do - expect(obj.to_bson(previous_content)).to eq(previous_content << bson) + expect(obj.to_bson.to_s).to eq(bson) end end shared_examples_for "a deserializable bson element" do let(:io) do - StringIO.new(bson) + BSON::ByteBuffer.new(bson) end let(:result) do @@ -64,21 +56,6 @@ it "deserializes from bson" do expect(result).to eq(obj) end - - context 'when io#readbyte returns a String' do - - let(:io) do - AlternateIO.new(bson) - end - - let(:result) do - described_class.from_bson(io) - end - - it "deserializes from bson" do - expect(result).to eq(obj) - end - end end shared_examples_for "a JSON serializable object" do @@ -108,7 +85,7 @@ it "serializes and deserializes properly" do expect( - BSON::Document.from_bson(StringIO.new(document.to_bson)) + BSON::Document.from_bson(BSON::ByteBuffer.new(document.to_bson.to_s)) ).to eq(document) end end diff --git a/src/main/org/bson/BooleanExtension.java b/src/main/org/bson/BooleanExtension.java deleted file mode 100644 index b0cf2c5fe..000000000 --- a/src/main/org/bson/BooleanExtension.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2009-2013 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. - */ - -package org.bson; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import org.jruby.Ruby; -import org.jruby.RubyBoolean; -import org.jruby.RubyModule; -import org.jruby.RubyString; -import org.jruby.anno.JRubyMethod; -import org.jruby.runtime.builtin.IRubyObject; - -/** - * Provides native extensions around boolean operations. - * - * @since 2.0.0 - */ -public class BooleanExtension { - - /** - * Constant for the FalseClass module name. - * - * @since 2.0.0 - */ - private static final String FALSE_CLASS = "FalseClass".intern(); - - /** - * Constant for the TrueClass module name. - * - * @since 2.0.0 - */ - private static final String TRUE_CLASS = "TrueClass".intern(); - - /** - * Constant for a single false byte. - * - * @since 2.0.0 - */ - private static final byte FALSE_BYTE = 0; - - /** - * Constant for a single true byte. - * - * @since 2.0.0 - */ - private static final byte TRUE_BYTE = 1; - - /** - * Constant for the array of 1 false byte. - * - * @since 2.0.0 - */ - private static final byte[] FALSE_BYTES = new byte[] { FALSE_BYTE }; - - /** - * Constant for the array of 1 true byte. - * - * @since 2.0.0 - */ - private static final byte[] TRUE_BYTES = new byte[] { TRUE_BYTE }; - - /** - * Load the method definitions into the boolean module. - * - * @param bson The bson module to define the methods under. - * - * @since 2.0.0 - */ - public static void extend(final RubyModule bson) { - RubyModule falseMod = bson.defineOrGetModuleUnder(FALSE_CLASS); - RubyModule trueMod = bson.defineOrGetModuleUnder(TRUE_CLASS); - falseMod.defineAnnotatedMethods(BooleanExtension.class); - trueMod.defineAnnotatedMethods(BooleanExtension.class); - } - - /** - * Encodes the boolean to the raw BSON bytes. - * - * @param bool The instance of the boolean object. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject bool) { - return toBsonBoolean(bool); - } - - /** - * Encodes the boolean to the raw BSON bytes. - * - * @param bool The instance of the boolean object. - * @param bytes The bytes to encode to. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject bool, final IRubyObject bytes) { - return ((RubyString) bytes).append(toBsonBoolean(bool)); - } - - /** - * Take the boolean value and convert it to its bytes. - * - * @param bool The Ruby boolean value. - * - * @return The byte array. - * - * @since 2.0.0 - */ - private static RubyString toBsonBoolean(final IRubyObject bool) { - final Ruby runtime = bool.getRuntime(); - if (bool == runtime.getTrue()) { - return RubyString.newString(runtime, TRUE_BYTES); - } - else { - return RubyString.newString(runtime, FALSE_BYTES); - } - } -} diff --git a/src/main/org/bson/ByteBuf.java b/src/main/org/bson/ByteBuf.java new file mode 100644 index 000000000..8ea9795af --- /dev/null +++ b/src/main/org/bson/ByteBuf.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2015 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. + */ + +package org.bson; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +import org.jcodings.Encoding; +import org.jcodings.EncodingDB; + +import org.jruby.Ruby; +import org.jruby.RubyBignum; +import org.jruby.RubyClass; +import org.jruby.RubyFloat; +import org.jruby.RubyFixnum; +import org.jruby.RubyNumeric; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +import static java.lang.String.format; + +/** + * Provides native extensions around boolean operations. + * + * @since 4.0.0 + */ +public class ByteBuf extends RubyObject { + + /** + * Constant for a null byte. + */ + private static byte NULL_BYTE = 0x00; + + /** + * The default size of the buffer. + */ + private static int DEFAULT_SIZE = 512; + + /** + * The UTF-8 String. + */ + private static String UTF8 = "UTF-8".intern(); + + /** + * Constant for UTF-8 encoding. + */ + private static Encoding UTF_8 = EncodingDB.getEncodings().get(UTF8.getBytes()).getEncoding(); + + /** + * The modes for the buffer. + */ + private enum Mode { READ, WRITE } + + /** + * The wrapped byte buffer. + */ + private ByteBuffer buffer; + + /** + * The current buffer mode. + */ + private Mode mode; + + /** + * The current position while reading. + */ + private int readPosition = 0; + + /** + * The current position while writing. + */ + private int writePosition = 0; + + /** + * Instantiate the ByteBuf - this is #allocate in Ruby. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + public ByteBuf(final Ruby runtime, final RubyClass rubyClass) { + super(runtime, rubyClass); + } + + /** + * Initialize an empty buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "initialize") + public IRubyObject intialize() { + this.buffer = ByteBuffer.allocate(DEFAULT_SIZE).order(ByteOrder.LITTLE_ENDIAN); + this.mode = Mode.WRITE; + return null; + } + + /** + * Instantiate the buffer with bytes. + * + * @param value The bytes to instantiate with. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "initialize") + public IRubyObject initialize(final RubyString value) { + this.buffer = ByteBuffer.wrap(value.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + this.mode = Mode.READ; + return null; + } + + /** + * Get a single byte from the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_byte") + public RubyString getByte() { + ensureBsonRead(); + RubyString string = RubyString.newString(getRuntime(), new byte[] { this.buffer.get() }); + this.readPosition += 1; + return string; + } + + /** + * Get the supplied number of bytes from the buffer. + * + * @param value The number of bytes to read. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_bytes") + public RubyString getBytes(final IRubyObject value) { + ensureBsonRead(); + int length = RubyNumeric.fix2int((RubyFixnum) value); + byte[] bytes = new byte[length]; + ByteBuffer buff = this.buffer.get(bytes); + RubyString string = RubyString.newString(getRuntime(), bytes); + this.readPosition += length; + return string; + } + + /** + * Get a cstring from the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_cstring") + public RubyString getCString() { + ensureBsonRead(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte next = NULL_BYTE; + while((next = this.buffer.get()) != NULL_BYTE) { + bytes.write(next); + } + RubyString string = getUTF8String(bytes.toByteArray()); + this.readPosition += (bytes.size() + 1); + return string; + } + + /** + * Get a double from the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_double") + public RubyFloat getDouble() { + ensureBsonRead(); + RubyFloat doubl = new RubyFloat(getRuntime(), this.buffer.getDouble()); + this.readPosition += 8; + return doubl; + } + + /** + * Get a 32 bit integer from the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_int32") + public RubyFixnum getInt32() { + ensureBsonRead(); + RubyFixnum int32 = new RubyFixnum(getRuntime(), this.buffer.getInt()); + this.readPosition += 4; + return int32; + } + + /** + * Get a UTF-8 string from the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_string") + public RubyString getString() { + ensureBsonRead(); + int length = this.buffer.getInt(); + this.readPosition += 4; + byte[] stringBytes = new byte[length]; + this.buffer.get(stringBytes); + byte[] bytes = Arrays.copyOfRange(stringBytes, 0, stringBytes.length - 1); + RubyString string = getUTF8String(bytes); + this.readPosition += length; + return string; + } + + /** + * Get a 64 bit integer from the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "get_int64") + public RubyBignum getInt64() { + ensureBsonRead(); + RubyBignum int64 = new RubyBignum(getRuntime(), RubyBignum.long2big(this.buffer.getLong())); + this.readPosition += 8; + return int64; + } + + /** + * Put a single byte onto the buffer. + * + * @param value The byte to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_byte") + public ByteBuf putByte(final IRubyObject value) { + ensureBsonWrite(1); + this.buffer.put(((RubyString) value).getBytes()[0]); + this.writePosition += 1; + return this; + } + + /** + * Put raw bytes onto the buffer. + * + * @param value The bytes to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_bytes") + public ByteBuf putBytes(final IRubyObject value) { + byte[] bytes = ((RubyString) value).getBytes(); + ensureBsonWrite(bytes.length); + this.buffer.put(bytes); + this.writePosition += bytes.length; + return this; + } + + /** + * Put a cstring onto the buffer. + * + * @param value The cstring to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_cstring") + public ByteBuf putCString(final IRubyObject value) throws UnsupportedEncodingException { + String string = ((RubyString) value).asJavaString(); + this.writePosition += writeCharacters(string, true); + return this; + } + + /** + * Put a double onto the buffer. + * + * @param value the double to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_double") + public ByteBuf putDouble(final IRubyObject value) { + ensureBsonWrite(8); + this.buffer.putDouble(((RubyFloat) value).getDoubleValue()); + this.writePosition += 8; + return this; + } + + /** + * Put a 32 bit integer onto the buffer. + * + * @param value The integer to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_int32") + public ByteBuf putInt32(final IRubyObject value) { + ensureBsonWrite(4); + this.buffer.putInt(RubyNumeric.fix2int((RubyFixnum) value)); + this.writePosition += 4; + return this; + } + + /** + * Put a 64 bit integer onto the buffer. + * + * @param value The integer to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_int64") + public ByteBuf putInt64(final IRubyObject value) { + if (value instanceof RubyBignum) { + throw getRuntime().newRangeError("Value is too large for a 64bit integer"); + } + ensureBsonWrite(8); + this.buffer.putLong(RubyNumeric.fix2long((RubyFixnum) value)); + this.writePosition += 8; + return this; + } + + /** + * Put a UTF-8 string onto the buffer. + * + * @param value The UTF-8 string to write. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "put_string") + public ByteBuf putString(final IRubyObject value) throws UnsupportedEncodingException { + String string = ((RubyString) value).asJavaString(); + this.buffer.putInt(0); + int length = writeCharacters(string, false); + this.buffer.putInt(this.buffer.position() - length - 4, length); + this.writePosition += (length + 4); + return this; + } + + /** + * Replace a 32 bit integer at the provided index in the buffer. + * + * @param index The index to replace at. + * @param value The value to replace with. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "replace_int32") + public ByteBuf replaceInt32(final IRubyObject index, final IRubyObject value) { + int i = RubyNumeric.fix2int((RubyFixnum) index); + int int32 = RubyNumeric.fix2int((RubyFixnum) value); + this.buffer.putInt(i, int32); + return this; + } + + /** + * Get the total length of the buffer. + * + * @author Durran Jordan + * @since 2015.09.29 + * @version 4.0.0 + */ + @JRubyMethod(name = "length") + public RubyFixnum getLength() { + return getWritePosition(); + } + + /** + * Get the read position of the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "read_position") + public RubyFixnum getReadPosition() { + return new RubyFixnum(getRuntime(), this.readPosition); + } + + /** + * Get the write position of the buffer. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "write_position") + public RubyFixnum getWritePosition() { + return new RubyFixnum(getRuntime(), this.writePosition); + } + + /** + * Convert the byte buffer to a string of the bytes. + * + * @author Durran Jordan + * @since 2015.09.26 + * @version 4.0.0 + */ + @JRubyMethod(name = "to_s") + public RubyString toRubyString() { + ensureBsonRead(); + byte[] bytes = new byte[this.writePosition]; + this.buffer.get(bytes, 0, this.writePosition); + return RubyString.newString(getRuntime(), bytes); + } + + private RubyString getUTF8String(final byte[] bytes) { + return RubyString.newString(getRuntime(), new ByteList(bytes, UTF_8)); + } + + private void ensureBsonRead() { + if (this.mode == Mode.WRITE) { + this.buffer.flip(); + } + } + + private void ensureBsonWrite(int length) { + if (this.mode == Mode.READ) { + this.buffer.flip(); + } + if (length > this.buffer.remaining()) { + int size = this.buffer.position() + length + DEFAULT_SIZE; + ByteBuffer newBuffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + if (this.buffer.position() > 0) { + byte [] existing = new byte[this.buffer.position()]; + this.buffer.rewind(); + this.buffer.get(existing); + newBuffer.put(existing); + } + this.buffer = newBuffer; + } + } + + private void write(byte b) { + ensureBsonWrite(1); + this.buffer.put(b); + } + + private int writeCharacters(final String string, final boolean checkForNull) { + int len = string.length(); + int total = 0; + + for (int i = 0; i < len;) { + int c = Character.codePointAt(string, i); + + if (checkForNull && c == 0x0) { + throw getRuntime().newArgumentError(format("String %s is not a valid UTF-8 CString.", string)); + } + + if (c < 0x80) { + write((byte) c); + total += 1; + } else if (c < 0x800) { + write((byte) (0xc0 + (c >> 6))); + write((byte) (0x80 + (c & 0x3f))); + total += 2; + } else if (c < 0x10000) { + write((byte) (0xe0 + (c >> 12))); + write((byte) (0x80 + ((c >> 6) & 0x3f))); + write((byte) (0x80 + (c & 0x3f))); + total += 3; + } else { + write((byte) (0xf0 + (c >> 18))); + write((byte) (0x80 + ((c >> 12) & 0x3f))); + write((byte) (0x80 + ((c >> 6) & 0x3f))); + write((byte) (0x80 + (c & 0x3f))); + total += 4; + } + + i += Character.charCount(c); + } + + write((byte) 0); + total++; + return total; + } +} diff --git a/src/main/org/bson/FloatExtension.java b/src/main/org/bson/FloatExtension.java deleted file mode 100644 index e68f1878e..000000000 --- a/src/main/org/bson/FloatExtension.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2009-2013 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. - */ - -package org.bson; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import org.jruby.Ruby; -import org.jruby.RubyFloat; -import org.jruby.RubyModule; -import org.jruby.RubyString; -import org.jruby.anno.JRubyMethod; -import org.jruby.runtime.builtin.IRubyObject; - -/** - * Provides native extensions around float operations. - * - * @since 2.0.0 - */ -public class FloatExtension { - - /** - * Constant for the Float module name. - * - * @since 2.0.0 - */ - private static final String FLOAT = "Float".intern(); - - /** - * Load the method definitions into the float module. - * - * @param bson The bson module to define the methods under. - * - * @since 2.0.0 - */ - public static void extend(final RubyModule bson) { - RubyModule floatMod = bson.defineOrGetModuleUnder(FLOAT); - floatMod.defineAnnotatedMethods(FloatExtension.class); - } - - /** - * Encodes the float to the raw BSON bytes. - * - * @param float The instance of the float object. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject number) { - final double value = ((RubyFloat) number).getDoubleValue(); - return toBsonDouble(number.getRuntime(), value); - } - - /** - * Encodes the float to the raw BSON bytes. - * - * @param float The instance of the float object. - * @param bytes The bytes to encode to. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject number, final IRubyObject bytes) { - final double value = ((RubyFloat) number).getDoubleValue(); - return ((RubyString) bytes).append(toBsonDouble(number.getRuntime(), value)); - } - - /** - * Take the double value and convert it to it's little endian bytes. - * - * @param runtime The JRuby runtime. - * @param value The value to encode. - * - * @return The byte array. - * - * @since 2.0.0 - */ - private static RubyString toBsonDouble(final Ruby runtime, final double value) { - final ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); - buffer.putDouble(value); - return RubyString.newString(runtime, buffer.array()); - } -} diff --git a/src/main/org/bson/IntegerExtension.java b/src/main/org/bson/IntegerExtension.java deleted file mode 100644 index 15592624b..000000000 --- a/src/main/org/bson/IntegerExtension.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2009-2013 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. - */ - -package org.bson; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import org.jruby.Ruby; -import org.jruby.RubyModule; -import org.jruby.RubyInteger; -import org.jruby.RubyString; -import org.jruby.anno.JRubyMethod; -import org.jruby.runtime.builtin.IRubyObject; - -/** - * Provides native extensions around integer operations. - * - * @since 2.0.0 - */ -public class IntegerExtension { - - /** - * Constant for the Integer module name. - * - * @since 2.0.0 - */ - private static final String INTEGER = "Integer".intern(); - - /** - * Load the method definitions into the integer module. - * - * @param bson The bson module to define the methods under. - * - * @since 2.0.0 - */ - public static void extend(final RubyModule bson) { - RubyModule integer = bson.defineOrGetModuleUnder(INTEGER); - integer.defineAnnotatedMethods(IntegerExtension.class); - } - - /** - * Encodes the integer to the raw BSON bytes. - * - * @param integer The instance of the integer object. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject integer) { - final long value = ((RubyInteger) integer).getLongValue(); - return toBsonInt(integer.getRuntime(), value); - } - - /** - * Encodes the integer to the raw BSON bytes. - * - * @param integer The instance of the integer object. - * @param bytes The bytes to encode to. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject integer, final IRubyObject bytes) { - final long value = ((RubyInteger) integer).getLongValue(); - return ((RubyString) bytes).append(toBsonInt(integer.getRuntime(), value)); - } - - /** - * Convert the integer to the raw bson. - * - * @param runtime The JRuby runtime. - * @param value The integer value. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - private static RubyString toBsonInt(final Ruby runtime, final long value) { - return isInt32(value) ? toBsonInt32(runtime, value) : toBsonInt64(runtime, value); - } - - /** - * Determine if the integer is 32bit. - * - * @param value The integer value. - * - * @return If the integer is in 32bit range. - * - * @since 2.0.0 - */ - private static boolean isInt32(final long value) { - return (Integer.MIN_VALUE <= value && value <= Integer.MAX_VALUE); - } - - /** - * Take the 32bit value and convert it to it's little endian bytes. - * - * @param runtime The JRuby runtime. - * @param value The value to encode. - * - * @return The byte array. - * - * @since 2.0.0 - */ - private static RubyString toBsonInt32(final Ruby runtime, final long value) { - final ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN); - buffer.putInt((int) value); - return RubyString.newString(runtime, buffer.array()); - } - - /** - * Take the 64bit value and convert it to it's little endian bytes. - * - * @param runtime The JRuby runtime. - * @param value The value to encode. - * - * @return The byte array. - * - * @since 2.0.0 - */ - private static RubyString toBsonInt64(final Ruby runtime, final long value) { - final ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); - buffer.putLong(value); - return RubyString.newString(runtime, buffer.array()); - } -} diff --git a/src/main/org/bson/NativeService.java b/src/main/org/bson/NativeService.java index a760f64d3..27ebc0149 100644 --- a/src/main/org/bson/NativeService.java +++ b/src/main/org/bson/NativeService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2013 MongoDB, Inc. + * Copyright (C) 2009-2015 MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,11 @@ import java.io.IOException; import org.jruby.Ruby; +import org.jruby.RubyClass; import org.jruby.RubyModule; import org.jruby.runtime.load.BasicLibraryService; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.builtin.IRubyObject; /** * The native implementation of various extensions. @@ -36,6 +39,13 @@ public class NativeService implements BasicLibraryService { */ private final String BSON = "BSON".intern(); + /** + * Constant for the BSON module name. + * + * @since 2.0.0 + */ + private final String BYTE_BUF = "ByteBuff".intern(); + /** * Loads the native extension into the JRuby runtime. * @@ -47,11 +57,15 @@ public class NativeService implements BasicLibraryService { */ public boolean basicLoad(final Ruby runtime) throws IOException { RubyModule bson = runtime.fastGetModule(BSON); - BooleanExtension.extend(bson); - FloatExtension.extend(bson); GeneratorExtension.extend(bson); - IntegerExtension.extend(bson); - TimeExtension.extend(bson); + + RubyClass byteBuffer = bson.defineClassUnder("ByteBuffer", runtime.getObject(), new ObjectAllocator() { + public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { + return new ByteBuf(runtime, rubyClass); + } + }); + + byteBuffer.defineAnnotatedMethods(ByteBuf.class); return true; } } diff --git a/src/main/org/bson/TimeExtension.java b/src/main/org/bson/TimeExtension.java deleted file mode 100644 index a409776bb..000000000 --- a/src/main/org/bson/TimeExtension.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2009-2013 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. - */ - -package org.bson; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import org.jruby.Ruby; -import org.jruby.RubyModule; -import org.jruby.RubyString; -import org.jruby.RubyTime; -import org.jruby.anno.JRubyMethod; -import org.jruby.runtime.builtin.IRubyObject; - -/** - * Provides native extensions around time operations. - * - * @since 2.0.0 - */ -public class TimeExtension { - - /** - * Constant for the time module name. - * - * @since 2.0.0 - */ - private static final String TIME = "Time".intern(); - - /** - * Load the method definitions into the time module. - * - * @param bson The bson module to define the methods under. - * - * @since 2.0.0 - */ - public static void extend(final RubyModule bson) { - RubyModule time = bson.defineOrGetModuleUnder(TIME); - time.defineAnnotatedMethods(TimeExtension.class); - } - - /** - * Encodes the time to the raw BSON bytes. - * - * @param time The instance of the time object. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject time) { - final long millis = ((RubyTime) time).getJavaDate().getTime(); - return toBsonTime(time.getRuntime(), millis); - } - - /** - * Encodes the time to the raw BSON bytes. - * - * @param time The instance of the time object. - * @param bytes The bytes to encode to. - * - * @return The encoded bytes. - * - * @since 2.0.0 - */ - @JRubyMethod(name = "to_bson") - public static IRubyObject toBson(final IRubyObject time, final IRubyObject bytes) { - final long millis = ((RubyTime) time).getJavaDate().getTime(); - return ((RubyString) bytes).append(toBsonTime(time.getRuntime(), millis)); - } - - /** - * Take the 64bit milliseconds and convert it to it's little endian bytes. - * - * @param runtime The JRuby runtime. - * @param millis The milliseconds to encode. - * - * @return The byte array. - * - * @since 2.0.0 - */ - private static IRubyObject toBsonTime(final Ruby runtime, final long millis) { - final ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); - buffer.putLong(millis); - return RubyString.newString(runtime, buffer.array()); - } -}