git-subtree-dir: libs/protobuf git-subtree-split: fcd3b9a85ef36e46643dc30176cea1a7ad62e02b
408 lines
15 KiB
Java
408 lines
15 KiB
Java
/*
|
|
* Protocol Buffers - Google's data interchange format
|
|
* Copyright 2014 Google Inc. All rights reserved.
|
|
* https://developers.google.com/protocol-buffers/
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
package com.google.protobuf.jruby;
|
|
|
|
import com.google.protobuf.ByteString;
|
|
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
|
|
import com.google.protobuf.Descriptors.FieldDescriptor;
|
|
import java.math.BigInteger;
|
|
import org.jcodings.specific.ASCIIEncoding;
|
|
import org.jruby.*;
|
|
import org.jruby.exceptions.RaiseException;
|
|
import org.jruby.ext.bigdecimal.RubyBigDecimal;
|
|
import org.jruby.runtime.Block;
|
|
import org.jruby.runtime.Helpers;
|
|
import org.jruby.runtime.ThreadContext;
|
|
import org.jruby.runtime.builtin.IRubyObject;
|
|
|
|
public class Utils {
|
|
public static FieldDescriptor.Type rubyToFieldType(IRubyObject typeClass) {
|
|
return FieldDescriptor.Type.valueOf(typeClass.asJavaString().toUpperCase());
|
|
}
|
|
|
|
public static IRubyObject fieldTypeToRuby(ThreadContext context, FieldDescriptor.Type type) {
|
|
return fieldTypeToRuby(context, type.name());
|
|
}
|
|
|
|
public static IRubyObject fieldTypeToRuby(ThreadContext context, FieldDescriptorProto.Type type) {
|
|
return fieldTypeToRuby(context, type.name());
|
|
}
|
|
|
|
private static IRubyObject fieldTypeToRuby(ThreadContext context, String typeName) {
|
|
|
|
return context.runtime.newSymbol(typeName.replace("TYPE_", "").toLowerCase());
|
|
}
|
|
|
|
public static IRubyObject checkType(
|
|
ThreadContext context,
|
|
FieldDescriptor.Type fieldType,
|
|
String fieldName,
|
|
IRubyObject value,
|
|
RubyModule typeClass) {
|
|
Ruby runtime = context.runtime;
|
|
|
|
switch (fieldType) {
|
|
case SFIXED32:
|
|
case SFIXED64:
|
|
case FIXED64:
|
|
case SINT64:
|
|
case SINT32:
|
|
case FIXED32:
|
|
case INT32:
|
|
case INT64:
|
|
case UINT32:
|
|
case UINT64:
|
|
if (!isRubyNum(value))
|
|
throw createExpectedTypeError(context, "number", "integral", fieldName, value);
|
|
|
|
if (value instanceof RubyFloat) {
|
|
double doubleVal = RubyNumeric.num2dbl(value);
|
|
if (Math.floor(doubleVal) != doubleVal) {
|
|
throw runtime.newRangeError(
|
|
"Non-integral floating point value assigned to integer field '"
|
|
+ fieldName
|
|
+ "' (given "
|
|
+ value.getMetaClass()
|
|
+ ").");
|
|
}
|
|
}
|
|
if (fieldType == FieldDescriptor.Type.UINT32
|
|
|| fieldType == FieldDescriptor.Type.UINT64
|
|
|| fieldType == FieldDescriptor.Type.FIXED32
|
|
|| fieldType == FieldDescriptor.Type.FIXED64) {
|
|
if (((RubyNumeric) value).isNegative()) {
|
|
throw runtime.newRangeError(
|
|
"Assigning negative value to unsigned integer field '"
|
|
+ fieldName
|
|
+ "' (given "
|
|
+ value.getMetaClass()
|
|
+ ").");
|
|
}
|
|
}
|
|
|
|
switch (fieldType) {
|
|
case INT32:
|
|
RubyNumeric.num2int(value);
|
|
break;
|
|
case UINT32:
|
|
case FIXED32:
|
|
num2uint(value);
|
|
break;
|
|
case UINT64:
|
|
case FIXED64:
|
|
num2ulong(context.runtime, value);
|
|
break;
|
|
default:
|
|
RubyNumeric.num2long(value);
|
|
break;
|
|
}
|
|
break;
|
|
case FLOAT:
|
|
if (!isRubyNum(value))
|
|
throw createExpectedTypeError(context, "number", "float", fieldName, value);
|
|
break;
|
|
case DOUBLE:
|
|
if (!isRubyNum(value))
|
|
throw createExpectedTypeError(context, "number", "double", fieldName, value);
|
|
break;
|
|
case BOOL:
|
|
if (!(value instanceof RubyBoolean))
|
|
throw createInvalidTypeError(context, "boolean", fieldName, value);
|
|
break;
|
|
case BYTES:
|
|
value = validateAndEncodeString(context, "bytes", fieldName, value, "Encoding::ASCII_8BIT");
|
|
break;
|
|
case STRING:
|
|
value =
|
|
validateAndEncodeString(
|
|
context, "string", fieldName, symToString(value), "Encoding::UTF_8");
|
|
break;
|
|
case MESSAGE:
|
|
if (value.getMetaClass() != typeClass) {
|
|
// See if we can convert the value before flagging it as invalid
|
|
String className = typeClass.getName();
|
|
|
|
if (className.equals("Google::Protobuf::Timestamp") && value instanceof RubyTime) {
|
|
RubyTime rt = (RubyTime) value;
|
|
RubyHash timestampArgs =
|
|
Helpers.constructHash(
|
|
runtime,
|
|
runtime.newString("nanos"),
|
|
rt.nsec(),
|
|
false,
|
|
runtime.newString("seconds"),
|
|
rt.to_i(),
|
|
false);
|
|
return ((RubyClass) typeClass).newInstance(context, timestampArgs, Block.NULL_BLOCK);
|
|
|
|
} else if (className.equals("Google::Protobuf::Duration")
|
|
&& value instanceof RubyNumeric) {
|
|
IRubyObject seconds;
|
|
if (value instanceof RubyFloat) {
|
|
seconds = ((RubyFloat) value).truncate(context);
|
|
} else if (value instanceof RubyRational) {
|
|
seconds = ((RubyRational) value).to_i(context);
|
|
} else if (value instanceof RubyBigDecimal) {
|
|
seconds = ((RubyBigDecimal) value).to_int(context);
|
|
} else {
|
|
seconds = ((RubyInteger) value).to_i();
|
|
}
|
|
|
|
IRubyObject nanos = ((RubyNumeric) value).remainder(context, RubyFixnum.one(runtime));
|
|
if (nanos instanceof RubyFloat) {
|
|
nanos = ((RubyFloat) nanos).op_mul(context, 1000000000);
|
|
} else if (nanos instanceof RubyRational) {
|
|
nanos = ((RubyRational) nanos).op_mul(context, runtime.newFixnum(1000000000));
|
|
} else if (nanos instanceof RubyBigDecimal) {
|
|
nanos = ((RubyBigDecimal) nanos).op_mul(context, runtime.newFixnum(1000000000));
|
|
} else {
|
|
nanos = ((RubyInteger) nanos).op_mul(context, 1000000000);
|
|
}
|
|
|
|
RubyHash durationArgs =
|
|
Helpers.constructHash(
|
|
runtime,
|
|
runtime.newString("nanos"),
|
|
((RubyNumeric) nanos).round(context),
|
|
false,
|
|
runtime.newString("seconds"),
|
|
seconds,
|
|
false);
|
|
return ((RubyClass) typeClass).newInstance(context, durationArgs, Block.NULL_BLOCK);
|
|
}
|
|
|
|
// Not able to convert so flag as invalid
|
|
throw createTypeError(
|
|
context,
|
|
"Invalid type "
|
|
+ value.getMetaClass()
|
|
+ " to assign to submessage field '"
|
|
+ fieldName
|
|
+ "'.");
|
|
}
|
|
|
|
break;
|
|
case ENUM:
|
|
boolean isValid =
|
|
((RubyEnumDescriptor) typeClass.getInstanceVariable(DESCRIPTOR_INSTANCE_VAR))
|
|
.isValidValue(context, value);
|
|
if (!isValid) {
|
|
throw runtime.newRangeError("Unknown symbol value for enum field '" + fieldName + "'.");
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public static IRubyObject wrapPrimaryValue(
|
|
ThreadContext context, FieldDescriptor.Type fieldType, Object value) {
|
|
return wrapPrimaryValue(context, fieldType, value, false);
|
|
}
|
|
|
|
public static IRubyObject wrapPrimaryValue(
|
|
ThreadContext context, FieldDescriptor.Type fieldType, Object value, boolean encodeBytes) {
|
|
Ruby runtime = context.runtime;
|
|
switch (fieldType) {
|
|
case INT32:
|
|
case SFIXED32:
|
|
case SINT32:
|
|
return runtime.newFixnum((Integer) value);
|
|
case SFIXED64:
|
|
case SINT64:
|
|
case INT64:
|
|
return runtime.newFixnum((Long) value);
|
|
case FIXED32:
|
|
case UINT32:
|
|
return runtime.newFixnum(((Integer) value) & (-1l >>> 32));
|
|
case FIXED64:
|
|
case UINT64:
|
|
long ret = (Long) value;
|
|
return ret >= 0
|
|
? runtime.newFixnum(ret)
|
|
: RubyBignum.newBignum(runtime, UINT64_COMPLEMENTARY.add(new BigInteger(ret + "")));
|
|
case FLOAT:
|
|
return runtime.newFloat((Float) value);
|
|
case DOUBLE:
|
|
return runtime.newFloat((Double) value);
|
|
case BOOL:
|
|
return (Boolean) value ? runtime.getTrue() : runtime.getFalse();
|
|
case BYTES:
|
|
{
|
|
IRubyObject wrapped =
|
|
encodeBytes
|
|
? RubyString.newString(
|
|
runtime, ((ByteString) value).toStringUtf8(), ASCIIEncoding.INSTANCE)
|
|
: RubyString.newString(runtime, ((ByteString) value).toByteArray());
|
|
wrapped.setFrozen(true);
|
|
return wrapped;
|
|
}
|
|
case STRING:
|
|
{
|
|
IRubyObject wrapped = runtime.newString(value.toString());
|
|
wrapped.setFrozen(true);
|
|
return wrapped;
|
|
}
|
|
default:
|
|
return runtime.getNil();
|
|
}
|
|
}
|
|
|
|
public static int num2uint(IRubyObject value) {
|
|
long longVal = RubyNumeric.num2long(value);
|
|
if (longVal > UINT_MAX)
|
|
throw value
|
|
.getRuntime()
|
|
.newRangeError("Integer " + longVal + " too big to convert to 'unsigned int'");
|
|
long num = longVal;
|
|
if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE)
|
|
// encode to UINT32
|
|
num = (-longVal ^ (-1l >>> 32)) + 1;
|
|
RubyNumeric.checkInt(value, num);
|
|
return (int) num;
|
|
}
|
|
|
|
public static long num2ulong(Ruby runtime, IRubyObject value) {
|
|
if (value instanceof RubyFloat) {
|
|
RubyBignum bignum = RubyBignum.newBignum(runtime, ((RubyFloat) value).getDoubleValue());
|
|
return RubyBignum.big2ulong(bignum);
|
|
} else if (value instanceof RubyBignum) {
|
|
return RubyBignum.big2ulong((RubyBignum) value);
|
|
} else {
|
|
return RubyNumeric.num2long(value);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper to make it easier to support symbols being passed instead of strings
|
|
*/
|
|
public static IRubyObject symToString(IRubyObject sym) {
|
|
if (sym instanceof RubySymbol) {
|
|
return ((RubySymbol) sym).id2name();
|
|
}
|
|
return sym;
|
|
}
|
|
|
|
public static void checkNameAvailability(ThreadContext context, String name) {
|
|
if (context.runtime.getObject().getConstantAt(name) != null)
|
|
throw context.runtime.newNameError(name + " is already defined", name);
|
|
}
|
|
|
|
public static boolean isMapEntry(FieldDescriptor fieldDescriptor) {
|
|
return fieldDescriptor.getType() == FieldDescriptor.Type.MESSAGE
|
|
&& fieldDescriptor.isRepeated()
|
|
&& fieldDescriptor.getMessageType().getOptions().getMapEntry();
|
|
}
|
|
|
|
public static RaiseException createTypeError(ThreadContext context, String message) {
|
|
if (cTypeError == null) {
|
|
cTypeError = (RubyClass) context.runtime.getClassFromPath("Google::Protobuf::TypeError");
|
|
}
|
|
return RaiseException.from(context.runtime, cTypeError, message);
|
|
}
|
|
|
|
public static RaiseException createExpectedTypeError(
|
|
ThreadContext context, String type, String fieldType, String fieldName, IRubyObject value) {
|
|
return createTypeError(
|
|
context,
|
|
String.format(
|
|
EXPECTED_TYPE_ERROR_FORMAT, type, fieldType, fieldName, value.getMetaClass()));
|
|
}
|
|
|
|
public static RaiseException createInvalidTypeError(
|
|
ThreadContext context, String fieldType, String fieldName, IRubyObject value) {
|
|
return createTypeError(
|
|
context,
|
|
String.format(INVALID_TYPE_ERROR_FORMAT, fieldType, fieldName, value.getMetaClass()));
|
|
}
|
|
|
|
protected static boolean isRubyNum(Object value) {
|
|
return value instanceof RubyFixnum || value instanceof RubyFloat || value instanceof RubyBignum;
|
|
}
|
|
|
|
protected static void validateTypeClass(
|
|
ThreadContext context, FieldDescriptor.Type type, IRubyObject value) {
|
|
Ruby runtime = context.runtime;
|
|
if (!(value instanceof RubyModule)) {
|
|
throw runtime.newArgumentError("TypeClass has incorrect type");
|
|
}
|
|
RubyModule klass = (RubyModule) value;
|
|
IRubyObject descriptor = klass.getInstanceVariable(DESCRIPTOR_INSTANCE_VAR);
|
|
if (descriptor.isNil()) {
|
|
throw runtime.newArgumentError(
|
|
"Type class has no descriptor. Please pass a "
|
|
+ "class or enum as returned by the DescriptorPool.");
|
|
}
|
|
if (type == FieldDescriptor.Type.MESSAGE) {
|
|
if (!(descriptor instanceof RubyDescriptor)) {
|
|
throw runtime.newArgumentError("Descriptor has an incorrect type");
|
|
}
|
|
} else if (type == FieldDescriptor.Type.ENUM) {
|
|
if (!(descriptor instanceof RubyEnumDescriptor)) {
|
|
throw runtime.newArgumentError("Descriptor has an incorrect type");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IRubyObject validateAndEncodeString(
|
|
ThreadContext context,
|
|
String fieldType,
|
|
String fieldName,
|
|
IRubyObject value,
|
|
String encoding) {
|
|
if (!(value instanceof RubyString))
|
|
throw createInvalidTypeError(context, fieldType, fieldName, value);
|
|
|
|
value = ((RubyString) value).encode(context, context.runtime.evalScriptlet(encoding));
|
|
value.setFrozen(true);
|
|
return value;
|
|
}
|
|
|
|
public static final String DESCRIPTOR_INSTANCE_VAR = "@descriptor";
|
|
|
|
public static final String EQUAL_SIGN = "=";
|
|
|
|
private static final BigInteger UINT64_COMPLEMENTARY =
|
|
new BigInteger("18446744073709551616"); // Math.pow(2, 64)
|
|
|
|
private static final String EXPECTED_TYPE_ERROR_FORMAT =
|
|
"Expected %s type for %s field '%s' (given %s).";
|
|
private static final String INVALID_TYPE_ERROR_FORMAT =
|
|
"Invalid argument for %s field '%s' (given %s).";
|
|
|
|
private static final long UINT_MAX = 0xffffffffl;
|
|
|
|
private static RubyClass cTypeError;
|
|
}
|