/* * 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; }