1 /* 2 * Protocol Buffers - Google's data interchange format 3 * Copyright 2014 Google Inc. All rights reserved. 4 * https://developers.google.com/protocol-buffers/ 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are 8 * met: 9 * 10 * * Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * * Redistributions in binary form must reproduce the above 13 * copyright notice, this list of conditions and the following disclaimer 14 * in the documentation and/or other materials provided with the 15 * distribution. 16 * * Neither the name of Google Inc. nor the names of its 17 * contributors may be used to endorse or promote products derived from 18 * this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.google.protobuf.jruby; 34 35 import com.google.protobuf.ByteString; 36 import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; 37 import com.google.protobuf.Descriptors.FieldDescriptor; 38 import java.math.BigInteger; 39 import org.jcodings.Encoding; 40 import org.jcodings.specific.ASCIIEncoding; 41 import org.jcodings.specific.UTF8Encoding; 42 import org.jruby.*; 43 import org.jruby.common.RubyWarnings; 44 import org.jruby.exceptions.RaiseException; 45 import org.jruby.ext.bigdecimal.RubyBigDecimal; 46 import org.jruby.runtime.Block; 47 import org.jruby.runtime.Helpers; 48 import org.jruby.runtime.ThreadContext; 49 import org.jruby.runtime.builtin.IRubyObject; 50 import org.jruby.util.ByteList; 51 52 public class Utils { rubyToFieldType(IRubyObject typeClass)53 public static FieldDescriptor.Type rubyToFieldType(IRubyObject typeClass) { 54 return FieldDescriptor.Type.valueOf(typeClass.asJavaString().toUpperCase()); 55 } 56 fieldTypeToRuby(ThreadContext context, FieldDescriptor.Type type)57 public static IRubyObject fieldTypeToRuby(ThreadContext context, FieldDescriptor.Type type) { 58 return fieldTypeToRuby(context, type.name()); 59 } 60 fieldTypeToRuby(ThreadContext context, FieldDescriptorProto.Type type)61 public static IRubyObject fieldTypeToRuby(ThreadContext context, FieldDescriptorProto.Type type) { 62 return fieldTypeToRuby(context, type.name()); 63 } 64 fieldTypeToRuby(ThreadContext context, String typeName)65 private static IRubyObject fieldTypeToRuby(ThreadContext context, String typeName) { 66 67 return context.runtime.newSymbol(typeName.replace("TYPE_", "").toLowerCase()); 68 } 69 checkType( ThreadContext context, FieldDescriptor.Type fieldType, String fieldName, IRubyObject value, RubyModule typeClass)70 public static IRubyObject checkType( 71 ThreadContext context, 72 FieldDescriptor.Type fieldType, 73 String fieldName, 74 IRubyObject value, 75 RubyModule typeClass) { 76 Ruby runtime = context.runtime; 77 78 switch (fieldType) { 79 case SFIXED32: 80 case SFIXED64: 81 case FIXED64: 82 case SINT64: 83 case SINT32: 84 case FIXED32: 85 case INT32: 86 case INT64: 87 case UINT32: 88 case UINT64: 89 if (!isRubyNum(value)) 90 throw createExpectedTypeError(context, "number", "integral", fieldName, value); 91 92 if (value instanceof RubyFloat) { 93 double doubleVal = RubyNumeric.num2dbl(value); 94 if (Math.floor(doubleVal) != doubleVal) { 95 throw runtime.newRangeError( 96 "Non-integral floating point value assigned to integer field '" 97 + fieldName 98 + "' (given " 99 + value.getMetaClass() 100 + ")."); 101 } 102 } 103 if (fieldType == FieldDescriptor.Type.UINT32 104 || fieldType == FieldDescriptor.Type.UINT64 105 || fieldType == FieldDescriptor.Type.FIXED32 106 || fieldType == FieldDescriptor.Type.FIXED64) { 107 if (((RubyNumeric) value).isNegative()) { 108 throw runtime.newRangeError( 109 "Assigning negative value to unsigned integer field '" 110 + fieldName 111 + "' (given " 112 + value.getMetaClass() 113 + ")."); 114 } 115 } 116 117 switch (fieldType) { 118 case INT32: 119 RubyNumeric.num2int(value); 120 break; 121 case UINT32: 122 case FIXED32: 123 num2uint(value); 124 break; 125 case UINT64: 126 case FIXED64: 127 num2ulong(context.runtime, value); 128 break; 129 default: 130 RubyNumeric.num2long(value); 131 break; 132 } 133 break; 134 case FLOAT: 135 if (!isRubyNum(value)) 136 throw createExpectedTypeError(context, "number", "float", fieldName, value); 137 break; 138 case DOUBLE: 139 if (!isRubyNum(value)) 140 throw createExpectedTypeError(context, "number", "double", fieldName, value); 141 break; 142 case BOOL: 143 if (!(value instanceof RubyBoolean)) 144 throw createInvalidTypeError(context, "boolean", fieldName, value); 145 break; 146 case BYTES: 147 value = validateAndEncodeString(context, "bytes", fieldName, value, ASCIIEncoding.INSTANCE); 148 break; 149 case STRING: 150 value = 151 validateAndEncodeString( 152 context, "string", fieldName, symToString(value), UTF8Encoding.INSTANCE); 153 break; 154 case MESSAGE: 155 if (value.getMetaClass() != typeClass) { 156 // See if we can convert the value before flagging it as invalid 157 String className = typeClass.getName(); 158 159 if (className.equals("Google::Protobuf::Timestamp") && value instanceof RubyTime) { 160 RubyTime rt = (RubyTime) value; 161 RubyHash timestampArgs = 162 Helpers.constructHash( 163 runtime, 164 runtime.newString("nanos"), 165 rt.nsec(), 166 false, 167 runtime.newString("seconds"), 168 rt.to_i(), 169 false); 170 return ((RubyClass) typeClass).newInstance(context, timestampArgs, Block.NULL_BLOCK); 171 172 } else if (className.equals("Google::Protobuf::Duration") 173 && value instanceof RubyNumeric) { 174 IRubyObject seconds; 175 if (value instanceof RubyFloat) { 176 seconds = ((RubyFloat) value).truncate(context); 177 } else if (value instanceof RubyRational) { 178 seconds = ((RubyRational) value).to_i(context); 179 } else if (value instanceof RubyBigDecimal) { 180 seconds = ((RubyBigDecimal) value).to_int(context); 181 } else { 182 seconds = ((RubyInteger) value).to_i(); 183 } 184 185 IRubyObject nanos = ((RubyNumeric) value).remainder(context, RubyFixnum.one(runtime)); 186 if (nanos instanceof RubyFloat) { 187 nanos = ((RubyFloat) nanos).op_mul(context, 1000000000); 188 } else if (nanos instanceof RubyRational) { 189 nanos = ((RubyRational) nanos).op_mul(context, runtime.newFixnum(1000000000)); 190 } else if (nanos instanceof RubyBigDecimal) { 191 nanos = ((RubyBigDecimal) nanos).op_mul(context, runtime.newFixnum(1000000000)); 192 } else { 193 nanos = ((RubyInteger) nanos).op_mul(context, 1000000000); 194 } 195 196 RubyHash durationArgs = 197 Helpers.constructHash( 198 runtime, 199 runtime.newString("nanos"), 200 ((RubyNumeric) nanos).round(context), 201 false, 202 runtime.newString("seconds"), 203 seconds, 204 false); 205 return ((RubyClass) typeClass).newInstance(context, durationArgs, Block.NULL_BLOCK); 206 } 207 208 // Not able to convert so flag as invalid 209 throw createTypeError( 210 context, 211 "Invalid type " 212 + value.getMetaClass() 213 + " to assign to submessage field '" 214 + fieldName 215 + "'."); 216 } 217 218 break; 219 case ENUM: 220 boolean isValid = 221 ((RubyEnumDescriptor) typeClass.getInstanceVariable(DESCRIPTOR_INSTANCE_VAR)) 222 .isValidValue(context, value); 223 if (!isValid) { 224 throw runtime.newRangeError("Unknown symbol value for enum field '" + fieldName + "'."); 225 } 226 break; 227 default: 228 break; 229 } 230 return value; 231 } 232 wrapPrimaryValue( ThreadContext context, FieldDescriptor.Type fieldType, Object value)233 public static IRubyObject wrapPrimaryValue( 234 ThreadContext context, FieldDescriptor.Type fieldType, Object value) { 235 return wrapPrimaryValue(context, fieldType, value, false); 236 } 237 wrapPrimaryValue( ThreadContext context, FieldDescriptor.Type fieldType, Object value, boolean encodeBytes)238 public static IRubyObject wrapPrimaryValue( 239 ThreadContext context, FieldDescriptor.Type fieldType, Object value, boolean encodeBytes) { 240 Ruby runtime = context.runtime; 241 switch (fieldType) { 242 case INT32: 243 case SFIXED32: 244 case SINT32: 245 return runtime.newFixnum((Integer) value); 246 case SFIXED64: 247 case SINT64: 248 case INT64: 249 return runtime.newFixnum((Long) value); 250 case FIXED32: 251 case UINT32: 252 return runtime.newFixnum(((Integer) value) & (-1l >>> 32)); 253 case FIXED64: 254 case UINT64: 255 long ret = (Long) value; 256 return ret >= 0 257 ? runtime.newFixnum(ret) 258 : RubyBignum.newBignum(runtime, UINT64_COMPLEMENTARY.add(new BigInteger(ret + ""))); 259 case FLOAT: 260 return runtime.newFloat((Float) value); 261 case DOUBLE: 262 return runtime.newFloat((Double) value); 263 case BOOL: 264 return (Boolean) value ? runtime.getTrue() : runtime.getFalse(); 265 case BYTES: 266 { 267 IRubyObject wrapped = 268 encodeBytes 269 ? RubyString.newString( 270 runtime, 271 new ByteList(((ByteString) value).toByteArray()), 272 ASCIIEncoding.INSTANCE) 273 : RubyString.newString(runtime, ((ByteString) value).toByteArray()); 274 wrapped.setFrozen(true); 275 return wrapped; 276 } 277 case STRING: 278 { 279 IRubyObject wrapped = runtime.newString(value.toString()); 280 wrapped.setFrozen(true); 281 return wrapped; 282 } 283 default: 284 return runtime.getNil(); 285 } 286 } 287 num2uint(IRubyObject value)288 public static int num2uint(IRubyObject value) { 289 long longVal = RubyNumeric.num2long(value); 290 if (longVal > UINT_MAX) 291 throw value 292 .getRuntime() 293 .newRangeError("Integer " + longVal + " too big to convert to 'unsigned int'"); 294 long num = longVal; 295 if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE) 296 // encode to UINT32 297 num = (-longVal ^ (-1l >>> 32)) + 1; 298 RubyNumeric.checkInt(value, num); 299 return (int) num; 300 } 301 num2ulong(Ruby runtime, IRubyObject value)302 public static long num2ulong(Ruby runtime, IRubyObject value) { 303 if (value instanceof RubyFloat) { 304 RubyBignum bignum = RubyBignum.newBignum(runtime, ((RubyFloat) value).getDoubleValue()); 305 return RubyBignum.big2ulong(bignum); 306 } else if (value instanceof RubyBignum) { 307 return RubyBignum.big2ulong((RubyBignum) value); 308 } else { 309 return RubyNumeric.num2long(value); 310 } 311 } 312 313 /* 314 * Helper to make it easier to support symbols being passed instead of strings 315 */ symToString(IRubyObject sym)316 public static IRubyObject symToString(IRubyObject sym) { 317 if (sym instanceof RubySymbol) { 318 return ((RubySymbol) sym).id2name(); 319 } 320 return sym; 321 } 322 checkNameAvailability(ThreadContext context, String name)323 public static void checkNameAvailability(ThreadContext context, String name) { 324 if (context.runtime.getObject().getConstantAt(name) != null) 325 throw context.runtime.newNameError(name + " is already defined", name); 326 } 327 isMapEntry(FieldDescriptor fieldDescriptor)328 public static boolean isMapEntry(FieldDescriptor fieldDescriptor) { 329 return fieldDescriptor.getType() == FieldDescriptor.Type.MESSAGE 330 && fieldDescriptor.isRepeated() 331 && fieldDescriptor.getMessageType().getOptions().getMapEntry(); 332 } 333 createTypeError(ThreadContext context, String message)334 public static RaiseException createTypeError(ThreadContext context, String message) { 335 if (cTypeError == null) { 336 cTypeError = (RubyClass) context.runtime.getClassFromPath("Google::Protobuf::TypeError"); 337 } 338 return RaiseException.from(context.runtime, cTypeError, message); 339 } 340 createExpectedTypeError( ThreadContext context, String type, String fieldType, String fieldName, IRubyObject value)341 public static RaiseException createExpectedTypeError( 342 ThreadContext context, String type, String fieldType, String fieldName, IRubyObject value) { 343 return createTypeError( 344 context, 345 String.format( 346 EXPECTED_TYPE_ERROR_FORMAT, type, fieldType, fieldName, value.getMetaClass())); 347 } 348 createInvalidTypeError( ThreadContext context, String fieldType, String fieldName, IRubyObject value)349 public static RaiseException createInvalidTypeError( 350 ThreadContext context, String fieldType, String fieldName, IRubyObject value) { 351 return createTypeError( 352 context, 353 String.format(INVALID_TYPE_ERROR_FORMAT, fieldType, fieldName, value.getMetaClass())); 354 } 355 isRubyNum(Object value)356 protected static boolean isRubyNum(Object value) { 357 return value instanceof RubyFixnum || value instanceof RubyFloat || value instanceof RubyBignum; 358 } 359 validateTypeClass( ThreadContext context, FieldDescriptor.Type type, IRubyObject value)360 protected static void validateTypeClass( 361 ThreadContext context, FieldDescriptor.Type type, IRubyObject value) { 362 Ruby runtime = context.runtime; 363 if (!(value instanceof RubyModule)) { 364 throw runtime.newArgumentError("TypeClass has incorrect type"); 365 } 366 RubyModule klass = (RubyModule) value; 367 IRubyObject descriptor = klass.getInstanceVariable(DESCRIPTOR_INSTANCE_VAR); 368 if (descriptor.isNil()) { 369 throw runtime.newArgumentError( 370 "Type class has no descriptor. Please pass a " 371 + "class or enum as returned by the DescriptorPool."); 372 } 373 if (type == FieldDescriptor.Type.MESSAGE) { 374 if (!(descriptor instanceof RubyDescriptor)) { 375 throw runtime.newArgumentError("Descriptor has an incorrect type"); 376 } 377 } else if (type == FieldDescriptor.Type.ENUM) { 378 if (!(descriptor instanceof RubyEnumDescriptor)) { 379 throw runtime.newArgumentError("Descriptor has an incorrect type"); 380 } 381 } 382 } 383 validateAndEncodeString( ThreadContext context, String fieldType, String fieldName, IRubyObject value, Encoding encoding)384 private static IRubyObject validateAndEncodeString( 385 ThreadContext context, 386 String fieldType, 387 String fieldName, 388 IRubyObject value, 389 Encoding encoding) { 390 if (!(value instanceof RubyString)) 391 throw createInvalidTypeError(context, fieldType, fieldName, value); 392 393 RubyString string = (RubyString) value; 394 if (encoding == UTF8Encoding.INSTANCE && string.getEncoding().isUTF8()) { 395 if (string.isCodeRangeBroken()) { 396 // TODO: For now we only warn for 397 // this case. We will remove the warning and throw an exception in the 30.x release 398 context 399 .runtime 400 .getWarnings() 401 .warn("String is invalid UTF-8. This will be an error in a future version."); 402 } 403 } 404 405 value = 406 string.encode( 407 context, context.runtime.getEncodingService().convertEncodingToRubyEncoding(encoding)); 408 value.setFrozen(true); 409 return value; 410 } 411 412 public static final String DESCRIPTOR_INSTANCE_VAR = "@descriptor"; 413 414 public static final String EQUAL_SIGN = "="; 415 416 private static final BigInteger UINT64_COMPLEMENTARY = 417 new BigInteger("18446744073709551616"); // Math.pow(2, 64) 418 419 private static final String EXPECTED_TYPE_ERROR_FORMAT = 420 "Expected %s type for %s field '%s' (given %s)."; 421 private static final String INVALID_TYPE_ERROR_FORMAT = 422 "Invalid argument for %s field '%s' (given %s)."; 423 424 private static final long UINT_MAX = 0xffffffffl; 425 426 private static RubyClass cTypeError; 427 } 428