diff options
author | Florian Frank <flori@ping.de> | 2010-09-22 22:21:02 +0200 |
---|---|---|
committer | Florian Frank <flori@ping.de> | 2010-09-23 01:16:01 +0200 |
commit | e3fe104e7d5ec184aac36128aed2d217cb655dfc (patch) | |
tree | 3a63dc0152effdb990defcd5c935e38209649a8f /java/src | |
parent | 2c0f8d2c9b15a33b8d10ffcb1959aef54d320b57 (diff) | |
download | json-e3fe104e7d5ec184aac36128aed2d217cb655dfc.tar.gz |
started to build jruby extension with Rakefile
Diffstat (limited to 'java/src')
-rw-r--r-- | java/src/json/ext/ByteListTranscoder.java | 167 | ||||
-rw-r--r-- | java/src/json/ext/Generator.java | 441 | ||||
-rw-r--r-- | java/src/json/ext/GeneratorMethods.java | 231 | ||||
-rw-r--r-- | java/src/json/ext/GeneratorService.java | 42 | ||||
-rw-r--r-- | java/src/json/ext/GeneratorState.java | 473 | ||||
-rw-r--r-- | java/src/json/ext/OptionsReader.java | 108 | ||||
-rw-r--r-- | java/src/json/ext/Parser.java | 2269 | ||||
-rw-r--r-- | java/src/json/ext/Parser.rl | 799 | ||||
-rw-r--r-- | java/src/json/ext/ParserService.java | 34 | ||||
-rw-r--r-- | java/src/json/ext/RuntimeInfo.java | 119 | ||||
-rw-r--r-- | java/src/json/ext/StringDecoder.java | 166 | ||||
-rw-r--r-- | java/src/json/ext/StringEncoder.java | 106 | ||||
-rw-r--r-- | java/src/json/ext/Utils.java | 89 |
13 files changed, 5044 insertions, 0 deletions
diff --git a/java/src/json/ext/ByteListTranscoder.java b/java/src/json/ext/ByteListTranscoder.java new file mode 100644 index 0000000..ed9e54b --- /dev/null +++ b/java/src/json/ext/ByteListTranscoder.java @@ -0,0 +1,167 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.ThreadContext; +import org.jruby.util.ByteList; + +/** + * A class specialized in transcoding a certain String format into another, + * using UTF-8 ByteLists as both input and output. + */ +abstract class ByteListTranscoder { + protected final ThreadContext context; + + protected ByteList src; + protected int srcEnd; + /** Position where the last read character started */ + protected int charStart; + /** Position of the next character to read */ + protected int pos; + + private ByteList out; + /** + * When a character that can be copied straight into the output is found, + * its index is stored on this variable, and copying is delayed until + * the sequence of characters that can be copied ends. + * + * <p>The variable stores -1 when not in a plain sequence. + */ + private int quoteStart = -1; + + protected ByteListTranscoder(ThreadContext context) { + this.context = context; + } + + protected void init(ByteList src, ByteList out) { + this.init(src, 0, src.length(), out); + } + + protected void init(ByteList src, int start, int end, ByteList out) { + this.src = src; + this.pos = start; + this.charStart = start; + this.srcEnd = end; + this.out = out; + } + + /** + * Returns whether there are any characters left to be read. + */ + protected boolean hasNext() { + return pos < srcEnd; + } + + /** + * Returns the next character in the buffer. + */ + private char next() { + return src.charAt(pos++); + } + + /** + * Reads an UTF-8 character from the input and returns its code point, + * while advancing the input position. + * + * <p>Raises an {@link #invalidUtf8()} exception if an invalid byte + * is found. + */ + protected int readUtf8Char() { + charStart = pos; + char head = next(); + if (head <= 0x7f) { // 0b0xxxxxxx (ASCII) + return head; + } + if (head <= 0xbf) { // 0b10xxxxxx + throw invalidUtf8(); // tail byte with no head + } + if (head <= 0xdf) { // 0b110xxxxx + ensureMin(1); + int cp = ((head & 0x1f) << 6) + | nextPart(); + if (cp < 0x0080) throw invalidUtf8(); + return cp; + } + if (head <= 0xef) { // 0b1110xxxx + ensureMin(2); + int cp = ((head & 0x0f) << 12) + | (nextPart() << 6) + | nextPart(); + if (cp < 0x0800) throw invalidUtf8(); + return cp; + } + if (head <= 0xf7) { // 0b11110xxx + ensureMin(3); + int cp = ((head & 0x07) << 18) + | (nextPart() << 12) + | (nextPart() << 6) + | nextPart(); + if (!Character.isValidCodePoint(cp)) throw invalidUtf8(); + return cp; + } + // 0b11111xxx? + throw invalidUtf8(); + } + + /** + * Throws a GeneratorError if the input list doesn't have at least this + * many bytes left. + */ + protected void ensureMin(int n) { + if (pos + n > srcEnd) throw incompleteUtf8(); + } + + /** + * Reads the next byte of a multi-byte UTF-8 character and returns its + * contents (lower 6 bits). + * + * <p>Throws a GeneratorError if the byte is not a valid tail. + */ + private int nextPart() { + char c = next(); + // tail bytes must be 0b10xxxxxx + if ((c & 0xc0) != 0x80) throw invalidUtf8(); + return c & 0x3f; + } + + + protected void quoteStart() { + if (quoteStart == -1) quoteStart = charStart; + } + + /** + * When in a sequence of characters that can be copied directly, + * interrupts the sequence and copies it to the output buffer. + * + * @param endPos The offset until which the direct character quoting should + * occur. You may pass {@link #pos} to quote until the most + * recently read character, or {@link #charStart} to quote + * until the character before it. + */ + protected void quoteStop(int endPos) { + if (quoteStart != -1) { + out.append(src, quoteStart, endPos - quoteStart); + quoteStart = -1; + } + } + + protected void append(int b) { + out.append(b); + } + + protected void append(byte[] origin, int start, int length) { + out.append(origin, start, length); + } + + + protected abstract RaiseException invalidUtf8(); + + protected RaiseException incompleteUtf8() { + return invalidUtf8(); + } +} diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java new file mode 100644 index 0000000..230d68f --- /dev/null +++ b/java/src/json/ext/Generator.java @@ -0,0 +1,441 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyArray; +import org.jruby.RubyBignum; +import org.jruby.RubyBoolean; +import org.jruby.RubyClass; +import org.jruby.RubyFixnum; +import org.jruby.RubyFloat; +import org.jruby.RubyHash; +import org.jruby.RubyNumeric; +import org.jruby.RubyString; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +public final class Generator { + private Generator() { + throw new RuntimeException(); + } + + /** + * Encodes the given object as a JSON string, using the given handler. + */ + static <T extends IRubyObject> RubyString + generateJson(ThreadContext context, T object, + Handler<? super T> handler, IRubyObject[] args) { + Session session = new Session(context, args.length > 0 ? args[0] + : null); + return session.infect(handler.generateNew(session, object)); + } + + /** + * Encodes the given object as a JSON string, detecting the appropriate handler + * for the given object. + */ + static <T extends IRubyObject> RubyString + generateJson(ThreadContext context, T object, IRubyObject[] args) { + Handler<? super T> handler = getHandlerFor(context.getRuntime(), object); + return generateJson(context, object, handler, args); + } + + /** + * Encodes the given object as a JSON string, using the appropriate + * handler if one is found or calling #to_json if not. + */ + public static <T extends IRubyObject> RubyString + generateJson(ThreadContext context, T object, + GeneratorState config) { + Session session = new Session(context, config); + Handler<? super T> handler = getHandlerFor(context.getRuntime(), object); + return handler.generateNew(session, object); + } + + /** + * Returns the best serialization handler for the given object. + */ + // Java's generics can't handle this satisfactorily, so I'll just leave + // the best I could get and ignore the warnings + @SuppressWarnings("unchecked") + private static <T extends IRubyObject> + Handler<? super T> getHandlerFor(Ruby runtime, T object) { + RubyClass metaClass = object.getMetaClass(); + if (metaClass == runtime.getString()) return (Handler)STRING_HANDLER; + if (metaClass == runtime.getFixnum()) return (Handler)FIXNUM_HANDLER; + if (metaClass == runtime.getHash()) return (Handler)HASH_HANDLER; + if (metaClass == runtime.getArray()) return (Handler)ARRAY_HANDLER; + if (object.isNil()) return (Handler)NIL_HANDLER; + if (object == runtime.getTrue()) return (Handler)TRUE_HANDLER; + if (object == runtime.getFalse()) return (Handler)FALSE_HANDLER; + if (metaClass == runtime.getFloat()) return (Handler)FLOAT_HANDLER; + if (metaClass == runtime.getBignum()) return (Handler)BIGNUM_HANDLER; + return GENERIC_HANDLER; + } + + + /* Generator context */ + + /** + * A class that concentrates all the information that is shared by + * generators working on a single session. + * + * <p>A session is defined as the process of serializing a single root + * object; any handler directly called by container handlers (arrays and + * hashes/objects) shares this object with its caller. + * + * <p>Note that anything called indirectly (via {@link GENERIC_HANDLER}) + * won't be part of the session. + */ + static class Session { + private final ThreadContext context; + private GeneratorState state; + private IRubyObject possibleState; + private RuntimeInfo info; + private StringEncoder stringEncoder; + + private boolean tainted = false; + private boolean untrusted = false; + + Session(ThreadContext context, GeneratorState state) { + this.context = context; + this.state = state; + } + + Session(ThreadContext context, IRubyObject possibleState) { + this.context = context; + this.possibleState = possibleState == null || possibleState.isNil() + ? null : possibleState; + } + + public ThreadContext getContext() { + return context; + } + + public Ruby getRuntime() { + return context.getRuntime(); + } + + public GeneratorState getState() { + if (state == null) { + state = GeneratorState.fromState(context, getInfo(), possibleState); + } + return state; + } + + public RuntimeInfo getInfo() { + if (info == null) info = RuntimeInfo.forRuntime(getRuntime()); + return info; + } + + public StringEncoder getStringEncoder() { + if (stringEncoder == null) { + stringEncoder = new StringEncoder(context, getState().asciiOnly()); + } + return stringEncoder; + } + + public void infectBy(IRubyObject object) { + if (object.isTaint()) tainted = true; + if (object.isUntrusted()) untrusted = true; + } + + public <T extends IRubyObject> T infect(T object) { + if (tainted) object.setTaint(true); + if (untrusted) object.setUntrusted(true); + return object; + } + } + + + /* Handler base classes */ + + private static abstract class Handler<T extends IRubyObject> { + /** + * Returns an estimative of how much space the serialization of the + * given object will take. Used for allocating enough buffer space + * before invoking other methods. + */ + int guessSize(Session session, T object) { + return 4; + } + + RubyString generateNew(Session session, T object) { + ByteList buffer = new ByteList(guessSize(session, object)); + generate(session, object, buffer); + return RubyString.newString(session.getRuntime(), buffer); + } + + abstract void generate(Session session, T object, ByteList buffer); + } + + /** + * A handler that returns a fixed keyword regardless of the passed object. + */ + private static class KeywordHandler<T extends IRubyObject> + extends Handler<T> { + private final ByteList keyword; + + private KeywordHandler(String keyword) { + this.keyword = new ByteList(ByteList.plain(keyword), false); + } + + @Override + int guessSize(Session session, T object) { + return keyword.length(); + } + + @Override + RubyString generateNew(Session session, T object) { + return RubyString.newStringShared(session.getRuntime(), keyword); + } + + @Override + void generate(Session session, T object, ByteList buffer) { + buffer.append(keyword); + } + } + + + /* Handlers */ + + static final Handler<RubyBignum> BIGNUM_HANDLER = + new Handler<RubyBignum>() { + @Override + void generate(Session session, RubyBignum object, ByteList buffer) { + // JRUBY-4751: RubyBignum.to_s() returns generic object + // representation (fixed in 1.5, but we maintain backwards + // compatibility; call to_s(IRubyObject[]) then + buffer.append(((RubyString)object.to_s(IRubyObject.NULL_ARRAY)).getByteList()); + } + }; + + static final Handler<RubyFixnum> FIXNUM_HANDLER = + new Handler<RubyFixnum>() { + @Override + void generate(Session session, RubyFixnum object, ByteList buffer) { + buffer.append(object.to_s().getByteList()); + } + }; + + static final Handler<RubyFloat> FLOAT_HANDLER = + new Handler<RubyFloat>() { + @Override + void generate(Session session, RubyFloat object, ByteList buffer) { + double value = RubyFloat.num2dbl(object); + + if (Double.isInfinite(value) || Double.isNaN(value)) { + if (!session.getState().allowNaN()) { + throw Utils.newException(session.getContext(), + Utils.M_GENERATOR_ERROR, + object + " not allowed in JSON"); + } + } + buffer.append(((RubyString)object.to_s()).getByteList()); + } + }; + + static final Handler<RubyArray> ARRAY_HANDLER = + new Handler<RubyArray>() { + @Override + int guessSize(Session session, RubyArray object) { + GeneratorState state = session.getState(); + int depth = state.getDepth(); + int perItem = + 4 // prealloc + + (depth + 1) * state.getIndent().length() // indent + + 1 + state.getArrayNl().length(); // ',' arrayNl + return 2 + object.size() * perItem; + } + + @Override + void generate(Session session, RubyArray object, ByteList buffer) { + ThreadContext context = session.getContext(); + Ruby runtime = context.getRuntime(); + GeneratorState state = session.getState(); + int depth = state.increaseDepth(); + + ByteList indentUnit = state.getIndent(); + byte[] shift = Utils.repeat(indentUnit, depth); + + ByteList arrayNl = state.getArrayNl(); + byte[] delim = new byte[1 + arrayNl.length()]; + delim[0] = ','; + System.arraycopy(arrayNl.unsafeBytes(), arrayNl.begin(), delim, 1, + arrayNl.length()); + + session.infectBy(object); + + buffer.append((byte)'['); + buffer.append(arrayNl); + boolean firstItem = true; + for (int i = 0, t = object.getLength(); i < t; i++) { + IRubyObject element = object.eltInternal(i); + session.infectBy(element); + if (firstItem) { + firstItem = false; + } else { + buffer.append(delim); + } + buffer.append(shift); + Handler<IRubyObject> handler = getHandlerFor(runtime, element); + handler.generate(session, element, buffer); + } + + state.decreaseDepth(); + if (arrayNl.length() != 0) { + buffer.append(arrayNl); + buffer.append(shift, 0, state.getDepth() * indentUnit.length()); + } + + buffer.append((byte)']'); + } + }; + + static final Handler<RubyHash> HASH_HANDLER = + new Handler<RubyHash>() { + @Override + int guessSize(Session session, RubyHash object) { + GeneratorState state = session.getState(); + int perItem = + 12 // key, colon, comma + + (state.getDepth() + 1) * state.getIndent().length() + + state.getSpaceBefore().length() + + state.getSpace().length(); + return 2 + object.size() * perItem; + } + + @Override + void generate(final Session session, RubyHash object, + final ByteList buffer) { + ThreadContext context = session.getContext(); + final Ruby runtime = context.getRuntime(); + final GeneratorState state = session.getState(); + final int depth = state.increaseDepth(); + + final ByteList objectNl = state.getObjectNl(); + final byte[] indent = Utils.repeat(state.getIndent(), depth); + final ByteList spaceBefore = state.getSpaceBefore(); + final ByteList space = state.getSpace(); + + buffer.append((byte)'{'); + buffer.append(objectNl); + object.visitAll(new RubyHash.Visitor() { + private boolean firstPair = true; + + @Override + public void visit(IRubyObject key, IRubyObject value) { + if (firstPair) { + firstPair = false; + } else { + buffer.append((byte)','); + buffer.append(objectNl); + } + if (objectNl.length() != 0) buffer.append(indent); + + STRING_HANDLER.generate(session, key.asString(), buffer); + session.infectBy(key); + + buffer.append(spaceBefore); + buffer.append((byte)':'); + buffer.append(space); + + Handler<IRubyObject> valueHandler = getHandlerFor(runtime, value); + valueHandler.generate(session, value, buffer); + session.infectBy(value); + } + }); + state.decreaseDepth(); + if (objectNl.length() != 0) { + buffer.append(objectNl); + if (indent.length != 0) { + for (int i = 0; i < state.getDepth(); i++) { + buffer.append(indent); + } + } + } + buffer.append((byte)'}'); + } + }; + + static final Handler<RubyString> STRING_HANDLER = + new Handler<RubyString>() { + @Override + int guessSize(Session session, RubyString object) { + // for most applications, most strings will be just a set of + // printable ASCII characters without any escaping, so let's + // just allocate enough space for that + the quotes + return 2 + object.getByteList().length(); + } + + @Override + void generate(Session session, RubyString object, ByteList buffer) { + RuntimeInfo info = session.getInfo(); + RubyString src; + + if (info.encodingsSupported() && + object.encoding(session.getContext()) != info.utf8) { + src = (RubyString)object.encode(session.getContext(), + info.utf8); + } else { + src = object; + } + + session.getStringEncoder().encode(src.getByteList(), buffer); + } + }; + + static final Handler<RubyBoolean> TRUE_HANDLER = + new KeywordHandler<RubyBoolean>("true"); + static final Handler<RubyBoolean> FALSE_HANDLER = + new KeywordHandler<RubyBoolean>("false"); + static final Handler<IRubyObject> NIL_HANDLER = + new KeywordHandler<IRubyObject>("null"); + + /** + * The default handler (<code>Object#to_json</code>): coerces the object + * to string using <code>#to_s</code>, and serializes that string. + */ + static final Handler<IRubyObject> OBJECT_HANDLER = + new Handler<IRubyObject>() { + @Override + RubyString generateNew(Session session, IRubyObject object) { + RubyString str = object.asString(); + return STRING_HANDLER.generateNew(session, str); + } + + @Override + void generate(Session session, IRubyObject object, ByteList buffer) { + RubyString str = object.asString(); + STRING_HANDLER.generate(session, str, buffer); + } + }; + + /** + * A handler that simply calls <code>#to_json(state)</code> on the + * given object. + */ + static final Handler<IRubyObject> GENERIC_HANDLER = + new Handler<IRubyObject>() { + @Override + RubyString generateNew(Session session, IRubyObject object) { + IRubyObject result = + object.callMethod(session.getContext(), "to_json", + new IRubyObject[] {session.getState()}); + if (result instanceof RubyString) return (RubyString)result; + throw session.getRuntime().newTypeError("to_json must return a String"); + } + + @Override + void generate(Session session, IRubyObject object, ByteList buffer) { + RubyString result = generateNew(session, object); + buffer.append(result.getByteList()); + } + }; +} diff --git a/java/src/json/ext/GeneratorMethods.java b/java/src/json/ext/GeneratorMethods.java new file mode 100644 index 0000000..28a612d --- /dev/null +++ b/java/src/json/ext/GeneratorMethods.java @@ -0,0 +1,231 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyArray; +import org.jruby.RubyBoolean; +import org.jruby.RubyFixnum; +import org.jruby.RubyFloat; +import org.jruby.RubyHash; +import org.jruby.RubyInteger; +import org.jruby.RubyModule; +import org.jruby.RubyNumeric; +import org.jruby.RubyString; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +/** + * A class that populates the + * <code>Json::Ext::Generator::GeneratorMethods</code> module. + * + * @author mernen + */ +class GeneratorMethods { + /** + * Populates the given module with all modules and their methods + * @param info + * @param generatorMethodsModule The module to populate + * (normally <code>JSON::Generator::GeneratorMethods</code>) + */ + static void populate(RuntimeInfo info, RubyModule module) { + defineMethods(module, "Array", RbArray.class); + defineMethods(module, "FalseClass", RbFalse.class); + defineMethods(module, "Float", RbFloat.class); + defineMethods(module, "Hash", RbHash.class); + defineMethods(module, "Integer", RbInteger.class); + defineMethods(module, "NilClass", RbNil.class); + defineMethods(module, "Object", RbObject.class); + defineMethods(module, "String", RbString.class); + defineMethods(module, "TrueClass", RbTrue.class); + + info.stringExtendModule = module.defineModuleUnder("String") + .defineModuleUnder("Extend"); + info.stringExtendModule.defineAnnotatedMethods(StringExtend.class); + } + + /** + * Convenience method for defining methods on a submodule. + * @param parentModule + * @param submoduleName + * @param klass + */ + private static void defineMethods(RubyModule parentModule, + String submoduleName, Class klass) { + RubyModule submodule = parentModule.defineModuleUnder(submoduleName); + submodule.defineAnnotatedMethods(klass); + } + + + public static class RbHash { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, (RubyHash)vSelf, + Generator.HASH_HANDLER, args); + } + } + + public static class RbArray { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, (RubyArray)vSelf, + Generator.ARRAY_HANDLER, args); + } + } + + public static class RbInteger { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, vSelf, args); + } + } + + public static class RbFloat { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, (RubyFloat)vSelf, + Generator.FLOAT_HANDLER, args); + } + } + + public static class RbString { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, (RubyString)vSelf, + Generator.STRING_HANDLER, args); + } + + /** + * <code>{@link RubyString String}#to_json_raw(*)</code> + * + * <p>This method creates a JSON text from the result of a call to + * {@link #to_json_raw_object} of this String. + */ + @JRubyMethod(rest=true) + public static IRubyObject to_json_raw(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + RubyHash obj = toJsonRawObject(context, Utils.ensureString(vSelf)); + return Generator.generateJson(context, obj, + Generator.HASH_HANDLER, args); + } + + /** + * <code>{@link RubyString String}#to_json_raw_object(*)</code> + * + * <p>This method creates a raw object Hash, that can be nested into + * other data structures and will be unparsed as a raw string. This + * method should be used if you want to convert raw strings to JSON + * instead of UTF-8 strings, e.g. binary data. + */ + @JRubyMethod(rest=true) + public static IRubyObject to_json_raw_object(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return toJsonRawObject(context, Utils.ensureString(vSelf)); + } + + private static RubyHash toJsonRawObject(ThreadContext context, + RubyString self) { + Ruby runtime = context.getRuntime(); + RubyHash result = RubyHash.newHash(runtime); + + IRubyObject createId = RuntimeInfo.forRuntime(runtime) + .jsonModule.callMethod(context, "create_id"); + result.op_aset(context, createId, self.getMetaClass().to_s()); + + ByteList bl = self.getByteList(); + byte[] uBytes = bl.unsafeBytes(); + RubyArray array = runtime.newArray(bl.length()); + for (int i = bl.begin(), t = bl.begin() + bl.length(); i < t; i++) { + array.store(i, runtime.newFixnum(uBytes[i] & 0xff)); + } + + result.op_aset(context, runtime.newString("raw"), array); + return result; + } + + @JRubyMethod(required=1, module=true) + public static IRubyObject included(ThreadContext context, + IRubyObject vSelf, IRubyObject module) { + RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime()); + return module.callMethod(context, "extend", info.stringExtendModule); + } + } + + public static class StringExtend { + /** + * <code>{@link RubyString String}#json_create(o)</code> + * + * <p>Raw Strings are JSON Objects (the raw bytes are stored in an + * array for the key "raw"). The Ruby String can be created by this + * module method. + */ + @JRubyMethod(required=1) + public static IRubyObject json_create(ThreadContext context, + IRubyObject vSelf, IRubyObject vHash) { + Ruby runtime = context.getRuntime(); + RubyHash o = vHash.convertToHash(); + IRubyObject rawData = o.fastARef(runtime.newString("raw")); + if (rawData == null) { + throw runtime.newArgumentError("\"raw\" value not defined " + + "for encoded String"); + } + RubyArray ary = Utils.ensureArray(rawData); + byte[] bytes = new byte[ary.getLength()]; + for (int i = 0, t = ary.getLength(); i < t; i++) { + IRubyObject element = ary.eltInternal(i); + if (element instanceof RubyFixnum) { + bytes[i] = (byte)RubyNumeric.fix2long(element); + } else { + throw runtime.newTypeError(element, runtime.getFixnum()); + } + } + return runtime.newString(new ByteList(bytes, false)); + } + } + + public static class RbTrue { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, (RubyBoolean)vSelf, + Generator.TRUE_HANDLER, args); + } + } + + public static class RbFalse { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, (RubyBoolean)vSelf, + Generator.FALSE_HANDLER, args); + } + } + + public static class RbNil { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject vSelf, IRubyObject[] args) { + return Generator.generateJson(context, vSelf, + Generator.NIL_HANDLER, args); + } + } + + public static class RbObject { + @JRubyMethod(rest=true) + public static IRubyObject to_json(ThreadContext context, + IRubyObject self, IRubyObject[] args) { + return RbString.to_json(context, self.asString(), args); + } + } +} diff --git a/java/src/json/ext/GeneratorService.java b/java/src/json/ext/GeneratorService.java new file mode 100644 index 0000000..b8deb22 --- /dev/null +++ b/java/src/json/ext/GeneratorService.java @@ -0,0 +1,42 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import java.io.IOException; + +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyModule; +import org.jruby.runtime.load.BasicLibraryService; + +/** + * The service invoked by JRuby's {@link org.jruby.runtime.load.LoadService LoadService}. + * Defines the <code>JSON::Ext::Generator</code> module. + * @author mernen + */ +public class GeneratorService implements BasicLibraryService { + public boolean basicLoad(Ruby runtime) throws IOException { + runtime.getLoadService().require("json/common"); + RuntimeInfo info = RuntimeInfo.initRuntime(runtime); + + info.jsonModule = runtime.defineModule("JSON"); + RubyModule jsonExtModule = info.jsonModule.defineModuleUnder("Ext"); + RubyModule generatorModule = jsonExtModule.defineModuleUnder("Generator"); + + RubyClass stateClass = + generatorModule.defineClassUnder("State", runtime.getObject(), + GeneratorState.ALLOCATOR); + stateClass.defineAnnotatedMethods(GeneratorState.class); + info.generatorStateClass = stateClass; + + RubyModule generatorMethods = + generatorModule.defineModuleUnder("GeneratorMethods"); + GeneratorMethods.populate(info, generatorMethods); + + return true; + } +} diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java new file mode 100644 index 0000000..dc99000 --- /dev/null +++ b/java/src/json/ext/GeneratorState.java @@ -0,0 +1,473 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyBoolean; +import org.jruby.RubyClass; +import org.jruby.RubyHash; +import org.jruby.RubyInteger; +import org.jruby.RubyNumeric; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Block; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.Visibility; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +/** + * The <code>JSON::Ext::Generator::State</code> class. + * + * <p>This class is used to create State instances, that are use to hold data + * while generating a JSON text from a a Ruby data structure. + * + * @author mernen + */ +public class GeneratorState extends RubyObject { + /** + * The indenting unit string. Will be repeated several times for larger + * indenting levels. + */ + private ByteList indent = ByteList.EMPTY_BYTELIST; + /** + * The spacing to be added after a semicolon on a JSON object. + * @see #spaceBefore + */ + private ByteList space = ByteList.EMPTY_BYTELIST; + /** + * The spacing to be added before a semicolon on a JSON object. + * @see #space + */ + private ByteList spaceBefore = ByteList.EMPTY_BYTELIST; + /** + * Any suffix to be added after the comma for each element on a JSON object. + * It is assumed to be a newline, if set. + */ + private ByteList objectNl = ByteList.EMPTY_BYTELIST; + /** + * Any suffix to be added after the comma for each element on a JSON Array. + * It is assumed to be a newline, if set. + */ + private ByteList arrayNl = ByteList.EMPTY_BYTELIST; + + /** + * The maximum level of nesting of structures allowed. + * <code>0</code> means disabled. + */ + private int maxNesting = DEFAULT_MAX_NESTING; + static final int DEFAULT_MAX_NESTING = 19; + /** + * Whether special float values (<code>NaN</code>, <code>Infinity</code>, + * <code>-Infinity</code>) are accepted. + * If set to <code>false</code>, an exception will be thrown upon + * encountering one. + */ + private boolean allowNaN = DEFAULT_ALLOW_NAN; + static final boolean DEFAULT_ALLOW_NAN = false; + /** + * XXX + */ + private boolean asciiOnly = DEFAULT_ASCII_ONLY; + static final boolean DEFAULT_ASCII_ONLY = false; + + /** + * The current depth (inside a #to_json call) + */ + private int depth = 0; + + static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { + public IRubyObject allocate(Ruby runtime, RubyClass klazz) { + return new GeneratorState(runtime, klazz); + } + }; + + public GeneratorState(Ruby runtime, RubyClass metaClass) { + super(runtime, metaClass); + } + + /** + * <code>State.from_state(opts)</code> + * + * <p>Creates a State object from <code>opts</code>, which ought to be + * {@link RubyHash Hash} to create a new <code>State</code> instance + * configured by <codes>opts</code>, something else to create an + * unconfigured instance. If <code>opts</code> is a <code>State</code> + * object, it is just returned. + * @param clazzParam The receiver of the method call + * ({@link RubyClass} <code>State</code>) + * @param opts The object to use as a base for the new <code>State</code> + * @param block The block passed to the method + * @return A <code>GeneratorState</code> as determined above + */ + @JRubyMethod(meta=true) + public static IRubyObject from_state(ThreadContext context, + IRubyObject klass, IRubyObject opts) { + return fromState(context, opts); + } + + static GeneratorState fromState(ThreadContext context, IRubyObject opts) { + return fromState(context, RuntimeInfo.forRuntime(context.getRuntime()), opts); + } + + static GeneratorState fromState(ThreadContext context, RuntimeInfo info, + IRubyObject opts) { + RubyClass klass = info.generatorStateClass; + if (opts != null) { + // if the given parameter is a Generator::State, return itself + if (klass.isInstance(opts)) return (GeneratorState)opts; + + // if the given parameter is a Hash, pass it to the instantiator + if (context.getRuntime().getHash().isInstance(opts)) { + return (GeneratorState)klass.newInstance(context, + new IRubyObject[] {opts}, Block.NULL_BLOCK); + } + } + + // for other values, return the safe prototype + return (GeneratorState)info.getSafeStatePrototype(context).dup(); + } + + /** + * <code>State#initialize(opts = {})</code> + * + * Instantiates a new <code>State</code> object, configured by <code>opts</code>. + * + * <code>opts</code> can have the following keys: + * + * <dl> + * <dt><code>:indent</code> + * <dd>a {@link RubyString String} used to indent levels (default: <code>""</code>) + * <dt><code>:space</code> + * <dd>a String that is put after a <code>':'</code> or <code>','</code> + * delimiter (default: <code>""</code>) + * <dt><code>:space_before</code> + * <dd>a String that is put before a <code>":"</code> pair delimiter + * (default: <code>""</code>) + * <dt><code>:object_nl</code> + * <dd>a String that is put at the end of a JSON object (default: <code>""</code>) + * <dt><code>:array_nl</code> + * <dd>a String that is put at the end of a JSON array (default: <code>""</code>) + * <dt><code>:allow_nan</code> + * <dd><code>true</code> if <code>NaN</code>, <code>Infinity</code>, and + * <code>-Infinity</code> should be generated, otherwise an exception is + * thrown if these values are encountered. + * This options defaults to <code>false</code>. + */ + @JRubyMethod(optional=1, visibility=Visibility.PRIVATE) + public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { + configure(context, args.length > 0 ? args[0] : null); + return this; + } + + @JRubyMethod + public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) { + Ruby runtime = context.getRuntime(); + if (!(vOrig instanceof GeneratorState)) { + throw runtime.newTypeError(vOrig, getType()); + } + GeneratorState orig = (GeneratorState)vOrig; + this.indent = orig.indent; + this.space = orig.space; + this.spaceBefore = orig.spaceBefore; + this.objectNl = orig.objectNl; + this.arrayNl = orig.arrayNl; + this.maxNesting = orig.maxNesting; + this.allowNaN = orig.allowNaN; + this.asciiOnly = orig.asciiOnly; + this.depth = orig.depth; + return this; + } + + /** + * XXX + */ + @JRubyMethod + public IRubyObject generate(ThreadContext context, IRubyObject obj) { + RubyString result = Generator.generateJson(context, obj, this); + if (!objectOrArrayLiteral(result)) { + throw Utils.newException(context, Utils.M_GENERATOR_ERROR, + "only generation of JSON objects or arrays allowed"); + } + return result; + } + + /** + * Ensures the given string is in the form "[...]" or "{...}", being + * possibly surrounded by white space. + * The string's encoding must be ASCII-compatible. + * @param value + * @return + */ + private static boolean objectOrArrayLiteral(RubyString value) { + ByteList bl = value.getByteList(); + int len = bl.length(); + + for (int pos = 0; pos < len - 1; pos++) { + int b = bl.get(pos); + if (Character.isWhitespace(b)) continue; + + // match the opening brace + switch (b) { + case '[': + return matchClosingBrace(bl, pos, len, ']'); + case '{': + return matchClosingBrace(bl, pos, len, '}'); + default: + return false; + } + } + return false; + } + + private static boolean matchClosingBrace(ByteList bl, int pos, int len, + int brace) { + for (int endPos = len - 1; endPos > pos; endPos--) { + int b = bl.get(endPos); + if (Character.isWhitespace(b)) continue; + return b == brace; + } + return false; + } + + @JRubyMethod(name="[]", required=1) + public IRubyObject op_aref(ThreadContext context, IRubyObject vName) { + String name = vName.asJavaString(); + if (getMetaClass().isMethodBound(name, true)) { + return send(context, vName, Block.NULL_BLOCK); + } + return context.getRuntime().getNil(); + } + + public ByteList getIndent() { + return indent; + } + + @JRubyMethod(name="indent") + public RubyString indent_get(ThreadContext context) { + return context.getRuntime().newString(indent); + } + + @JRubyMethod(name="indent=") + public IRubyObject indent_set(ThreadContext context, IRubyObject indent) { + this.indent = prepareByteList(context, indent); + return indent; + } + + public ByteList getSpace() { + return space; + } + + @JRubyMethod(name="space") + public RubyString space_get(ThreadContext context) { + return context.getRuntime().newString(space); + } + + @JRubyMethod(name="space=") + public IRubyObject space_set(ThreadContext context, IRubyObject space) { + this.space = prepareByteList(context, space); + return space; + } + + public ByteList getSpaceBefore() { + return spaceBefore; + } + + @JRubyMethod(name="space_before") + public RubyString space_before_get(ThreadContext context) { + return context.getRuntime().newString(spaceBefore); + } + + @JRubyMethod(name="space_before=") + public IRubyObject space_before_set(ThreadContext context, + IRubyObject spaceBefore) { + this.spaceBefore = prepareByteList(context, spaceBefore); + return spaceBefore; + } + + public ByteList getObjectNl() { + return objectNl; + } + + @JRubyMethod(name="object_nl") + public RubyString object_nl_get(ThreadContext context) { + return context.getRuntime().newString(objectNl); + } + + @JRubyMethod(name="object_nl=") + public IRubyObject object_nl_set(ThreadContext context, + IRubyObject objectNl) { + this.objectNl = prepareByteList(context, objectNl); + return objectNl; + } + + public ByteList getArrayNl() { + return arrayNl; + } + + @JRubyMethod(name="array_nl") + public RubyString array_nl_get(ThreadContext context) { + return context.getRuntime().newString(arrayNl); + } + + @JRubyMethod(name="array_nl=") + public IRubyObject array_nl_set(ThreadContext context, + IRubyObject arrayNl) { + this.arrayNl = prepareByteList(context, arrayNl); + return arrayNl; + } + + @JRubyMethod(name="check_circular?") + public RubyBoolean check_circular_p(ThreadContext context) { + return context.getRuntime().newBoolean(maxNesting != 0); + } + + /** + * Returns the maximum level of nesting configured for this state. + */ + public int getMaxNesting() { + return maxNesting; + } + + @JRubyMethod(name="max_nesting") + public RubyInteger max_nesting_get(ThreadContext context) { + return context.getRuntime().newFixnum(maxNesting); + } + + @JRubyMethod(name="max_nesting=") + public IRubyObject max_nesting_set(IRubyObject max_nesting) { + maxNesting = RubyNumeric.fix2int(max_nesting); + return max_nesting; + } + + public boolean allowNaN() { + return allowNaN; + } + + @JRubyMethod(name="allow_nan?") + public RubyBoolean allow_nan_p(ThreadContext context) { + return context.getRuntime().newBoolean(allowNaN); + } + + public boolean asciiOnly() { + return asciiOnly; + } + + @JRubyMethod(name="ascii_only?") + public RubyBoolean ascii_only_p(ThreadContext context) { + return context.getRuntime().newBoolean(asciiOnly); + } + + public int getDepth() { + return depth; + } + + @JRubyMethod(name="depth") + public RubyInteger depth_get(ThreadContext context) { + return context.getRuntime().newFixnum(depth); + } + + @JRubyMethod(name="depth=") + public IRubyObject depth_set(IRubyObject vDepth) { + depth = RubyNumeric.fix2int(vDepth); + return vDepth; + } + + private ByteList prepareByteList(ThreadContext context, IRubyObject value) { + RubyString str = value.convertToString(); + RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime()); + if (info.encodingsSupported() && str.encoding(context) != info.utf8) { + str = (RubyString)str.encode(context, info.utf8); + } + return str.getByteList().dup(); + } + + /** + * <code>State#configure(opts)</code> + * + * <p>Configures this State instance with the {@link RubyHash Hash} + * <code>opts</code>, and returns itself. + * @param vOpts The options hash + * @return The receiver + */ + @JRubyMethod + public IRubyObject configure(ThreadContext context, IRubyObject vOpts) { + OptionsReader opts = new OptionsReader(context, vOpts); + + ByteList indent = opts.getString("indent"); + if (indent != null) this.indent = indent; + + ByteList space = opts.getString("space"); + if (space != null) this.space = space; + + ByteList spaceBefore = opts.getString("space_before"); + if (spaceBefore != null) this.spaceBefore = spaceBefore; + + ByteList arrayNl = opts.getString("array_nl"); + if (arrayNl != null) this.arrayNl = arrayNl; + + ByteList objectNl = opts.getString("object_nl"); + if (objectNl != null) this.objectNl = objectNl; + + maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); + allowNaN = opts.getBool("allow_nan", DEFAULT_ALLOW_NAN); + asciiOnly = opts.getBool("ascii_only", DEFAULT_ASCII_ONLY); + + depth = opts.getInt("depth", 0); + + return this; + } + + /** + * <code>State#to_h()</code> + * + * <p>Returns the configuration instance variables as a hash, that can be + * passed to the configure method. + * @return + */ + @JRubyMethod + public RubyHash to_h(ThreadContext context) { + Ruby runtime = context.getRuntime(); + RubyHash result = RubyHash.newHash(runtime); + + result.op_aset(context, runtime.newSymbol("indent"), indent_get(context)); + result.op_aset(context, runtime.newSymbol("space"), space_get(context)); + result.op_aset(context, runtime.newSymbol("space_before"), space_before_get(context)); + result.op_aset(context, runtime.newSymbol("object_nl"), object_nl_get(context)); + result.op_aset(context, runtime.newSymbol("array_nl"), array_nl_get(context)); + result.op_aset(context, runtime.newSymbol("allow_nan"), allow_nan_p(context)); + result.op_aset(context, runtime.newSymbol("ascii_only"), ascii_only_p(context)); + result.op_aset(context, runtime.newSymbol("max_nesting"), max_nesting_get(context)); + result.op_aset(context, runtime.newSymbol("depth"), depth_get(context)); + return result; + } + + public int increaseDepth() { + depth++; + checkMaxNesting(); + return depth; + } + + public int decreaseDepth() { + return --depth; + } + + /** + * Checks if the current depth is allowed as per this state's options. + * @param context + * @param depth The corrent depth + */ + private void checkMaxNesting() { + if (maxNesting != 0 && depth > maxNesting) { + depth--; + throw Utils.newException(getRuntime().getCurrentContext(), + Utils.M_NESTING_ERROR, "nesting of " + depth + " is too deep"); + } + } +} diff --git a/java/src/json/ext/OptionsReader.java b/java/src/json/ext/OptionsReader.java new file mode 100644 index 0000000..3bc8d5f --- /dev/null +++ b/java/src/json/ext/OptionsReader.java @@ -0,0 +1,108 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyHash; +import org.jruby.RubyNumeric; +import org.jruby.RubyString; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +final class OptionsReader { + private final ThreadContext context; + private final Ruby runtime; + private final RubyHash opts; + private RuntimeInfo info; + + OptionsReader(ThreadContext context, IRubyObject vOpts) { + this.context = context; + this.runtime = context.getRuntime(); + + if (vOpts == null || vOpts.isNil()) { + opts = null; + } else if (vOpts.respondsTo("to_hash")) { + opts = vOpts.convertToHash(); + } else { + opts = vOpts.callMethod(context, "to_h").convertToHash(); + } + } + + private RuntimeInfo getRuntimeInfo() { + if (info != null) return info; + info = RuntimeInfo.forRuntime(runtime); + return info; + } + + /** + * Efficiently looks up items with a {@link RubySymbol Symbol} key + * @param key The Symbol name to look up for + * @return The item in the {@link RubyHash Hash}, or <code>null</code> + * if not found + */ + IRubyObject get(String key) { + return opts == null ? null : opts.fastARef(runtime.newSymbol(key)); + } + + boolean getBool(String key, boolean defaultValue) { + IRubyObject value = get(key); + return value == null ? defaultValue : value.isTrue(); + } + + int getInt(String key, int defaultValue) { + IRubyObject value = get(key); + if (value == null) return defaultValue; + if (!value.isTrue()) return 0; + return RubyNumeric.fix2int(value); + } + + /** + * Reads the setting from the options hash. If no entry is set for this + * key or if it evaluates to <code>false</code>, returns null; attempts to + * coerce the value to {@link RubyString String} otherwise. + * @param key The Symbol name to look up for + * @return <code>null</code> if the key is not in the Hash or if + * its value evaluates to <code>false</code> + * @throws RaiseException <code>TypeError</code> if the value does not + * evaluate to <code>false</code> and can't be + * converted to string + */ + ByteList getString(String key) { + IRubyObject value = get(key); + if (value == null || !value.isTrue()) return null; + + RubyString str = value.convertToString(); + RuntimeInfo info = getRuntimeInfo(); + if (info.encodingsSupported() && str.encoding(context) != info.utf8) { + str = (RubyString)str.encode(context, info.utf8); + } + return str.getByteList().dup(); + } + + /** + * Reads the setting from the options hash. If it is <code>nil</code> or + * undefined, returns the default value given. + * If not, ensures it is a RubyClass instance and shares the same + * allocator as the default value (i.e. for the basic types which have + * their specific allocators, this ensures the passed value is + * a subclass of them). + */ + RubyClass getClass(String key, RubyClass defaultValue) { + IRubyObject value = get(key); + + if (value == null || value.isNil()) return defaultValue; + + if (value instanceof RubyClass && + ((RubyClass)value).getAllocator() == defaultValue.getAllocator()) { + return (RubyClass)value; + } + throw runtime.newTypeError(key + " option must be a subclass of " + + defaultValue); + } +} diff --git a/java/src/json/ext/Parser.java b/java/src/json/ext/Parser.java new file mode 100644 index 0000000..c8f6f3d --- /dev/null +++ b/java/src/json/ext/Parser.java @@ -0,0 +1,2269 @@ + +// line 1 "Parser.rl" +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyArray; +import org.jruby.RubyClass; +import org.jruby.RubyEncoding; +import org.jruby.RubyFloat; +import org.jruby.RubyHash; +import org.jruby.RubyInteger; +import org.jruby.RubyModule; +import org.jruby.RubyNumeric; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.anno.JRubyMethod; +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.Block; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.Visibility; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +/** + * The <code>JSON::Ext::Parser</code> class. + * + * <p>This is the JSON parser implemented as a Java class. To use it as the + * standard parser, set + * <pre>JSON.parser = JSON::Ext::Parser</pre> + * This is performed for you when you <code>include "json/ext"</code>. + * + * <p>This class does not perform the actual parsing, just acts as an interface + * to Ruby code. When the {@link #parse()} method is invoked, a + * Parser.ParserSession object is instantiated, which handles the process. + * + * @author mernen + */ +public class Parser extends RubyObject { + private final RuntimeInfo info; + private RubyString vSource; + private RubyString createId; + private int maxNesting; + private boolean allowNaN; + private boolean symbolizeNames; + private RubyClass objectClass; + private RubyClass arrayClass; + + private static final int DEFAULT_MAX_NESTING = 19; + + private static final String JSON_MINUS_INFINITY = "-Infinity"; + // constant names in the JSON module containing those values + private static final String CONST_NAN = "NaN"; + private static final String CONST_INFINITY = "Infinity"; + private static final String CONST_MINUS_INFINITY = "MinusInfinity"; + + static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { + public IRubyObject allocate(Ruby runtime, RubyClass klazz) { + return new Parser(runtime, klazz); + } + }; + + /** + * Multiple-value return for internal parser methods. + * + * <p>All the <code>parse<var>Stuff</var></code> methods return instances of + * <code>ParserResult</code> when successful, or <code>null</code> when + * there's a problem with the input data. + */ + static final class ParserResult { + /** + * The result of the successful parsing. Should never be + * <code>null</code>. + */ + final IRubyObject result; + /** + * The point where the parser returned. + */ + final int p; + + ParserResult(IRubyObject result, int p) { + this.result = result; + this.p = p; + } + } + + public Parser(Ruby runtime, RubyClass metaClass) { + super(runtime, metaClass); + info = RuntimeInfo.forRuntime(runtime); + } + + /** + * <code>Parser.new(source, opts = {})</code> + * + * <p>Creates a new <code>JSON::Ext::Parser</code> instance for the string + * <code>source</code>. + * It will be configured by the <code>opts</code> Hash. + * <code>opts</code> can have the following keys: + * + * <dl> + * <dt><code>:max_nesting</code> + * <dd>The maximum depth of nesting allowed in the parsed data + * structures. Disable depth checking with <code>:max_nesting => false|nil|0</code>, + * it defaults to 19. + * + * <dt><code>:allow_nan</code> + * <dd>If set to <code>true</code>, allow <code>NaN</code>, + * <code>Infinity</code> and <code>-Infinity</code> in defiance of RFC 4627 + * to be parsed by the Parser. This option defaults to <code>false</code>. + * + * <dt><code>:symbolize_names</code> + * <dd>If set to <code>true</code>, returns symbols for the names (keys) in + * a JSON object. Otherwise strings are returned, which is also the default. + * + * <dt><code>:create_additions</code> + * <dd>If set to <code>false</code>, the Parser doesn't create additions + * even if a matchin class and <code>create_id</code> was found. This option + * defaults to <code>true</code>. + * + * <dt><code>:object_class</code> + * <dd>Defaults to Hash. + * + * <dt><code>:array_class</code> + * <dd>Defaults to Array. + * </dl> + */ + @JRubyMethod(name = "new", required = 1, optional = 1, meta = true) + public static IRubyObject newInstance(IRubyObject clazz, IRubyObject[] args, Block block) { + Parser parser = (Parser)((RubyClass)clazz).allocate(); + + parser.callInit(args, block); + + return parser; + } + + @JRubyMethod(required = 1, optional = 1, visibility = Visibility.PRIVATE) + public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { + Ruby runtime = context.getRuntime(); + RubyString source = convertEncoding(context, args[0].convertToString()); + + OptionsReader opts = + new OptionsReader(context, args.length > 1 ? args[1] : null); + + this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); + this.allowNaN = opts.getBool("allow_nan", false); + this.symbolizeNames = opts.getBool("symbolize_names", false); + this.createId = + opts.getBool("create_additions", true) ? getCreateId(context) + : null; + this.objectClass = opts.getClass("object_class", runtime.getHash()); + this.arrayClass = opts.getClass("array_class", runtime.getArray()); + + this.vSource = source; + return this; + } + + /** + * Checks the given string's encoding. If a non-UTF-8 encoding is detected, + * a converted copy is returned. + * Returns the source string if no conversion is needed. + */ + private RubyString convertEncoding(ThreadContext context, RubyString source) { + ByteList bl = source.getByteList(); + int len = bl.length(); + if (len < 2) { + throw Utils.newException(context, Utils.M_PARSER_ERROR, + "A JSON text must at least contain two octets!"); + } + + if (info.encodingsSupported()) { + RubyEncoding encoding = (RubyEncoding)source.encoding(context); + if (encoding != info.ascii8bit) { + return (RubyString)source.encode(context, info.utf8); + } + + String sniffedEncoding = sniffByteList(bl); + if (sniffedEncoding == null) return source; // assume UTF-8 + return reinterpretEncoding(context, source, sniffedEncoding); + } + + String sniffedEncoding = sniffByteList(bl); + if (sniffedEncoding == null) return source; // assume UTF-8 + Ruby runtime = context.getRuntime(); + return (RubyString)info.jsonModule. + callMethod(context, "iconv", + new IRubyObject[] { + runtime.newString("utf-8"), + runtime.newString(sniffedEncoding), + source}); + } + + /** + * Checks the first four bytes of the given ByteList to infer its encoding, + * using the principle demonstrated on section 3 of RFC 4627 (JSON). + */ + private static String sniffByteList(ByteList bl) { + if (bl.length() < 4) return null; + if (bl.get(0) == 0 && bl.get(2) == 0) { + return bl.get(1) == 0 ? "utf-32be" : "utf-16be"; + } + if (bl.get(1) == 0 && bl.get(3) == 0) { + return bl.get(2) == 0 ? "utf-32le" : "utf-16le"; + } + return null; + } + + /** + * Assumes the given (binary) RubyString to be in the given encoding, then + * converts it to UTF-8. + */ + private RubyString reinterpretEncoding(ThreadContext context, + RubyString str, String sniffedEncoding) { + RubyEncoding actualEncoding = info.getEncoding(context, sniffedEncoding); + RubyEncoding targetEncoding = info.utf8; + RubyString dup = (RubyString)str.dup(); + dup.force_encoding(context, actualEncoding); + return (RubyString)dup.encode_bang(context, targetEncoding); + } + + /** + * <code>Parser#parse()</code> + * + * <p>Parses the current JSON text <code>source</code> and returns the + * complete data structure as a result. + */ + @JRubyMethod + public IRubyObject parse(ThreadContext context) { + return new ParserSession(this, context).parse(); + } + + /** + * <code>Parser#source()</code> + * + * <p>Returns a copy of the current <code>source</code> string, that was + * used to construct this Parser. + */ + @JRubyMethod(name = "source") + public IRubyObject source_get() { + return vSource.dup(); + } + + /** + * Queries <code>JSON.create_id</code>. Returns <code>null</code> if it is + * set to <code>nil</code> or <code>false</code>, and a String if not. + */ + private RubyString getCreateId(ThreadContext context) { + IRubyObject v = info.jsonModule.callMethod(context, "create_id"); + return v.isTrue() ? v.convertToString() : null; + } + + /** + * A string parsing session. + * + * <p>Once a ParserSession is instantiated, the source string should not + * change until the parsing is complete. The ParserSession object assumes + * the source {@link RubyString} is still associated to its original + * {@link ByteList}, which in turn must still be bound to the same + * <code>byte[]</code> value (and on the same offset). + */ + // Ragel uses lots of fall-through + @SuppressWarnings("fallthrough") + private static class ParserSession { + private final Parser parser; + private final ThreadContext context; + private final ByteList byteList; + private final byte[] data; + private final StringDecoder decoder; + private int currentNesting = 0; + + // initialization value for all state variables. + // no idea about the origins of this value, ask Flori ;) + private static final int EVIL = 0x666; + + private ParserSession(Parser parser, ThreadContext context) { + this.parser = parser; + this.context = context; + this.byteList = parser.vSource.getByteList(); + this.data = byteList.unsafeBytes(); + this.decoder = new StringDecoder(context); + } + + private RaiseException unexpectedToken(int absStart, int absEnd) { + RubyString msg = getRuntime().newString("unexpected token at '") + .cat(data, absStart, absEnd - absStart) + .cat((byte)'\''); + return newException(Utils.M_PARSER_ERROR, msg); + } + + private Ruby getRuntime() { + return context.getRuntime(); + } + + +// line 323 "Parser.rl" + + + +// line 305 "Parser.java" +private static byte[] init__JSON_value_actions_0() +{ + return new byte [] { + 0, 1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, + 5, 1, 6, 1, 7, 1, 8, 1, 9 + }; +} + +private static final byte _JSON_value_actions[] = init__JSON_value_actions_0(); + + +private static byte[] init__JSON_value_key_offsets_0() +{ + return new byte [] { + 0, 0, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 + }; +} + +private static final byte _JSON_value_key_offsets[] = init__JSON_value_key_offsets_0(); + + +private static char[] init__JSON_value_trans_keys_0() +{ + return new char [] { + 34, 45, 73, 78, 91, 102, 110, 116, 123, 48, 57, 110, + 102, 105, 110, 105, 116, 121, 97, 78, 97, 108, 115, 101, + 117, 108, 108, 114, 117, 101, 0 + }; +} + +private static final char _JSON_value_trans_keys[] = init__JSON_value_trans_keys_0(); + + +private static byte[] init__JSON_value_single_lengths_0() +{ + return new byte [] { + 0, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 + }; +} + +private static final byte _JSON_value_single_lengths[] = init__JSON_value_single_lengths_0(); + + +private static byte[] init__JSON_value_range_lengths_0() +{ + return new byte [] { + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; +} + +private static final byte _JSON_value_range_lengths[] = init__JSON_value_range_lengths_0(); + + +private static byte[] init__JSON_value_index_offsets_0() +{ + return new byte [] { + 0, 0, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, + 31, 33, 35, 37, 39, 41, 43, 45, 47, 49 + }; +} + +private static final byte _JSON_value_index_offsets[] = init__JSON_value_index_offsets_0(); + + +private static byte[] init__JSON_value_trans_targs_0() +{ + return new byte [] { + 21, 21, 2, 9, 21, 11, 15, 18, 21, 21, 0, 3, + 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 21, + 0, 10, 0, 21, 0, 12, 0, 13, 0, 14, 0, 21, + 0, 16, 0, 17, 0, 21, 0, 19, 0, 20, 0, 21, + 0, 0, 0 + }; +} + +private static final byte _JSON_value_trans_targs[] = init__JSON_value_trans_targs_0(); + + +private static byte[] init__JSON_value_trans_actions_0() +{ + return new byte [] { + 13, 11, 0, 0, 15, 0, 0, 0, 17, 11, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, + 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 5, + 0, 0, 0 + }; +} + +private static final byte _JSON_value_trans_actions[] = init__JSON_value_trans_actions_0(); + + +private static byte[] init__JSON_value_from_state_actions_0() +{ + return new byte [] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 19 + }; +} + +private static final byte _JSON_value_from_state_actions[] = init__JSON_value_from_state_actions_0(); + + +static final int JSON_value_start = 1; +static final int JSON_value_first_final = 21; +static final int JSON_value_error = 0; + +static final int JSON_value_en_main = 1; + + +// line 429 "Parser.rl" + + + ParserResult parseValue(int p, int pe) { + int cs = EVIL; + IRubyObject result = null; + + +// line 427 "Parser.java" + { + cs = JSON_value_start; + } + +// line 436 "Parser.rl" + +// line 434 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _acts = _JSON_value_from_state_actions[cs]; + _nacts = (int) _JSON_value_actions[_acts++]; + while ( _nacts-- > 0 ) { + switch ( _JSON_value_actions[_acts++] ) { + case 9: +// line 414 "Parser.rl" + { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; +// line 466 "Parser.java" + } + } + + _match: do { + _keys = _JSON_value_key_offsets[cs]; + _trans = _JSON_value_index_offsets[cs]; + _klen = _JSON_value_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_value_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_value_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_value_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_value_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + cs = _JSON_value_trans_targs[_trans]; + + if ( _JSON_value_trans_actions[_trans] != 0 ) { + _acts = _JSON_value_trans_actions[_trans]; + _nacts = (int) _JSON_value_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_value_actions[_acts++] ) + { + case 0: +// line 331 "Parser.rl" + { + result = getRuntime().getNil(); + } + break; + case 1: +// line 334 "Parser.rl" + { + result = getRuntime().getFalse(); + } + break; + case 2: +// line 337 "Parser.rl" + { + result = getRuntime().getTrue(); + } + break; + case 3: +// line 340 "Parser.rl" + { + if (parser.allowNaN) { + result = getConstant(CONST_NAN); + } else { + throw unexpectedToken(p - 2, pe); + } + } + break; + case 4: +// line 347 "Parser.rl" + { + if (parser.allowNaN) { + result = getConstant(CONST_INFINITY); + } else { + throw unexpectedToken(p - 7, pe); + } + } + break; + case 5: +// line 354 "Parser.rl" + { + if (pe > p + 9 && + absSubSequence(p, p + 9).toString().equals(JSON_MINUS_INFINITY)) { + + if (parser.allowNaN) { + result = getConstant(CONST_MINUS_INFINITY); + {p = (( p + 10))-1;} + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + throw unexpectedToken(p, pe); + } + } + ParserResult res = parseFloat(p, pe); + if (res != null) { + result = res.result; + {p = (( res.p))-1;} + } + res = parseInteger(p, pe); + if (res != null) { + result = res.result; + {p = (( res.p))-1;} + } + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; + case 6: +// line 380 "Parser.rl" + { + ParserResult res = parseString(p, pe); + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result = res.result; + {p = (( res.p))-1;} + } + } + break; + case 7: +// line 390 "Parser.rl" + { + currentNesting++; + ParserResult res = parseArray(p, pe); + currentNesting--; + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result = res.result; + {p = (( res.p))-1;} + } + } + break; + case 8: +// line 402 "Parser.rl" + { + currentNesting++; + ParserResult res = parseObject(p, pe); + currentNesting--; + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result = res.result; + {p = (( res.p))-1;} + } + } + break; +// line 638 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 437 "Parser.rl" + + if (cs >= JSON_value_first_final && result != null) { + return new ParserResult(result, p); + } else { + return null; + } + } + + +// line 668 "Parser.java" +private static byte[] init__JSON_integer_actions_0() +{ + return new byte [] { + 0, 1, 0 + }; +} + +private static final byte _JSON_integer_actions[] = init__JSON_integer_actions_0(); + + +private static byte[] init__JSON_integer_key_offsets_0() +{ + return new byte [] { + 0, 0, 4, 7, 9, 11 + }; +} + +private static final byte _JSON_integer_key_offsets[] = init__JSON_integer_key_offsets_0(); + + +private static char[] init__JSON_integer_trans_keys_0() +{ + return new char [] { + 45, 48, 49, 57, 48, 49, 57, 48, 57, 48, 57, 0 + }; +} + +private static final char _JSON_integer_trans_keys[] = init__JSON_integer_trans_keys_0(); + + +private static byte[] init__JSON_integer_single_lengths_0() +{ + return new byte [] { + 0, 2, 1, 0, 0, 0 + }; +} + +private static final byte _JSON_integer_single_lengths[] = init__JSON_integer_single_lengths_0(); + + +private static byte[] init__JSON_integer_range_lengths_0() +{ + return new byte [] { + 0, 1, 1, 1, 1, 0 + }; +} + +private static final byte _JSON_integer_range_lengths[] = init__JSON_integer_range_lengths_0(); + + +private static byte[] init__JSON_integer_index_offsets_0() +{ + return new byte [] { + 0, 0, 4, 7, 9, 11 + }; +} + +private static final byte _JSON_integer_index_offsets[] = init__JSON_integer_index_offsets_0(); + + +private static byte[] init__JSON_integer_indicies_0() +{ + return new byte [] { + 0, 2, 3, 1, 2, 3, 1, 1, 4, 3, 4, 1, + 0 + }; +} + +private static final byte _JSON_integer_indicies[] = init__JSON_integer_indicies_0(); + + +private static byte[] init__JSON_integer_trans_targs_0() +{ + return new byte [] { + 2, 0, 3, 4, 5 + }; +} + +private static final byte _JSON_integer_trans_targs[] = init__JSON_integer_trans_targs_0(); + + +private static byte[] init__JSON_integer_trans_actions_0() +{ + return new byte [] { + 0, 0, 0, 0, 1 + }; +} + +private static final byte _JSON_integer_trans_actions[] = init__JSON_integer_trans_actions_0(); + + +static final int JSON_integer_start = 1; +static final int JSON_integer_first_final = 5; +static final int JSON_integer_error = 0; + +static final int JSON_integer_en_main = 1; + + +// line 456 "Parser.rl" + + + ParserResult parseInteger(int p, int pe) { + int cs = EVIL; + + +// line 774 "Parser.java" + { + cs = JSON_integer_start; + } + +// line 462 "Parser.rl" + int memo = p; + +// line 782 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _match: do { + _keys = _JSON_integer_key_offsets[cs]; + _trans = _JSON_integer_index_offsets[cs]; + _klen = _JSON_integer_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_integer_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_integer_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_integer_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_integer_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_integer_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + _trans = _JSON_integer_indicies[_trans]; + cs = _JSON_integer_trans_targs[_trans]; + + if ( _JSON_integer_trans_actions[_trans] != 0 ) { + _acts = _JSON_integer_trans_actions[_trans]; + _nacts = (int) _JSON_integer_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_integer_actions[_acts++] ) + { + case 0: +// line 450 "Parser.rl" + { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; +// line 869 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 464 "Parser.rl" + + if (cs < JSON_integer_first_final) { + return null; + } + + ByteList num = absSubSequence(memo, p); + // note: this is actually a shared string, but since it is temporary and + // read-only, it doesn't really matter + RubyString expr = RubyString.newStringLight(getRuntime(), num); + RubyInteger number = RubyNumeric.str2inum(getRuntime(), expr, 10, true); + return new ParserResult(number, p + 1); + } + + +// line 904 "Parser.java" +private static byte[] init__JSON_float_actions_0() +{ + return new byte [] { + 0, 1, 0 + }; +} + +private static final byte _JSON_float_actions[] = init__JSON_float_actions_0(); + + +private static byte[] init__JSON_float_key_offsets_0() +{ + return new byte [] { + 0, 0, 4, 7, 10, 12, 18, 22, 24, 30, 35 + }; +} + +private static final byte _JSON_float_key_offsets[] = init__JSON_float_key_offsets_0(); + + +private static char[] init__JSON_float_trans_keys_0() +{ + return new char [] { + 45, 48, 49, 57, 48, 49, 57, 46, 69, 101, 48, 57, + 69, 101, 45, 46, 48, 57, 43, 45, 48, 57, 48, 57, + 69, 101, 45, 46, 48, 57, 46, 69, 101, 48, 57, 0 + }; +} + +private static final char _JSON_float_trans_keys[] = init__JSON_float_trans_keys_0(); + + +private static byte[] init__JSON_float_single_lengths_0() +{ + return new byte [] { + 0, 2, 1, 3, 0, 2, 2, 0, 2, 3, 0 + }; +} + +private static final byte _JSON_float_single_lengths[] = init__JSON_float_single_lengths_0(); + + +private static byte[] init__JSON_float_range_lengths_0() +{ + return new byte [] { + 0, 1, 1, 0, 1, 2, 1, 1, 2, 1, 0 + }; +} + +private static final byte _JSON_float_range_lengths[] = init__JSON_float_range_lengths_0(); + + +private static byte[] init__JSON_float_index_offsets_0() +{ + return new byte [] { + 0, 0, 4, 7, 11, 13, 18, 22, 24, 29, 34 + }; +} + +private static final byte _JSON_float_index_offsets[] = init__JSON_float_index_offsets_0(); + + +private static byte[] init__JSON_float_indicies_0() +{ + return new byte [] { + 0, 2, 3, 1, 2, 3, 1, 4, 5, 5, 1, 6, + 1, 5, 5, 1, 6, 7, 8, 8, 9, 1, 9, 1, + 1, 1, 1, 9, 7, 4, 5, 5, 3, 1, 1, 0 + }; +} + +private static final byte _JSON_float_indicies[] = init__JSON_float_indicies_0(); + + +private static byte[] init__JSON_float_trans_targs_0() +{ + return new byte [] { + 2, 0, 3, 9, 4, 6, 5, 10, 7, 8 + }; +} + +private static final byte _JSON_float_trans_targs[] = init__JSON_float_trans_targs_0(); + + +private static byte[] init__JSON_float_trans_actions_0() +{ + return new byte [] { + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 + }; +} + +private static final byte _JSON_float_trans_actions[] = init__JSON_float_trans_actions_0(); + + +static final int JSON_float_start = 1; +static final int JSON_float_first_final = 10; +static final int JSON_float_error = 0; + +static final int JSON_float_en_main = 1; + + +// line 492 "Parser.rl" + + + ParserResult parseFloat(int p, int pe) { + int cs = EVIL; + + +// line 1013 "Parser.java" + { + cs = JSON_float_start; + } + +// line 498 "Parser.rl" + int memo = p; + +// line 1021 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _match: do { + _keys = _JSON_float_key_offsets[cs]; + _trans = _JSON_float_index_offsets[cs]; + _klen = _JSON_float_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_float_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_float_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_float_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_float_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_float_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + _trans = _JSON_float_indicies[_trans]; + cs = _JSON_float_trans_targs[_trans]; + + if ( _JSON_float_trans_actions[_trans] != 0 ) { + _acts = _JSON_float_trans_actions[_trans]; + _nacts = (int) _JSON_float_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_float_actions[_acts++] ) + { + case 0: +// line 483 "Parser.rl" + { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; +// line 1108 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 500 "Parser.rl" + + if (cs < JSON_float_first_final) { + return null; + } + + ByteList num = absSubSequence(memo, p); + // note: this is actually a shared string, but since it is temporary and + // read-only, it doesn't really matter + RubyString expr = RubyString.newStringLight(getRuntime(), num); + RubyFloat number = RubyNumeric.str2fnum(getRuntime(), expr, true); + return new ParserResult(number, p + 1); + } + + +// line 1143 "Parser.java" +private static byte[] init__JSON_string_actions_0() +{ + return new byte [] { + 0, 2, 0, 1 + }; +} + +private static final byte _JSON_string_actions[] = init__JSON_string_actions_0(); + + +private static byte[] init__JSON_string_key_offsets_0() +{ + return new byte [] { + 0, 0, 1, 5, 8, 14, 20, 26, 32 + }; +} + +private static final byte _JSON_string_key_offsets[] = init__JSON_string_key_offsets_0(); + + +private static char[] init__JSON_string_trans_keys_0() +{ + return new char [] { + 34, 34, 92, 0, 31, 117, 0, 31, 48, 57, 65, 70, + 97, 102, 48, 57, 65, 70, 97, 102, 48, 57, 65, 70, + 97, 102, 48, 57, 65, 70, 97, 102, 0 + }; +} + +private static final char _JSON_string_trans_keys[] = init__JSON_string_trans_keys_0(); + + +private static byte[] init__JSON_string_single_lengths_0() +{ + return new byte [] { + 0, 1, 2, 1, 0, 0, 0, 0, 0 + }; +} + +private static final byte _JSON_string_single_lengths[] = init__JSON_string_single_lengths_0(); + + +private static byte[] init__JSON_string_range_lengths_0() +{ + return new byte [] { + 0, 0, 1, 1, 3, 3, 3, 3, 0 + }; +} + +private static final byte _JSON_string_range_lengths[] = init__JSON_string_range_lengths_0(); + + +private static byte[] init__JSON_string_index_offsets_0() +{ + return new byte [] { + 0, 0, 2, 6, 9, 13, 17, 21, 25 + }; +} + +private static final byte _JSON_string_index_offsets[] = init__JSON_string_index_offsets_0(); + + +private static byte[] init__JSON_string_indicies_0() +{ + return new byte [] { + 0, 1, 2, 3, 1, 0, 4, 1, 0, 5, 5, 5, + 1, 6, 6, 6, 1, 7, 7, 7, 1, 0, 0, 0, + 1, 1, 0 + }; +} + +private static final byte _JSON_string_indicies[] = init__JSON_string_indicies_0(); + + +private static byte[] init__JSON_string_trans_targs_0() +{ + return new byte [] { + 2, 0, 8, 3, 4, 5, 6, 7 + }; +} + +private static final byte _JSON_string_trans_targs[] = init__JSON_string_trans_targs_0(); + + +private static byte[] init__JSON_string_trans_actions_0() +{ + return new byte [] { + 0, 0, 1, 0, 0, 0, 0, 0 + }; +} + +private static final byte _JSON_string_trans_actions[] = init__JSON_string_trans_actions_0(); + + +static final int JSON_string_start = 1; +static final int JSON_string_first_final = 8; +static final int JSON_string_error = 0; + +static final int JSON_string_en_main = 1; + + +// line 544 "Parser.rl" + + + ParserResult parseString(int p, int pe) { + int cs = EVIL; + RubyString result = null; + + +// line 1253 "Parser.java" + { + cs = JSON_string_start; + } + +// line 551 "Parser.rl" + int memo = p; + +// line 1261 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _match: do { + _keys = _JSON_string_key_offsets[cs]; + _trans = _JSON_string_index_offsets[cs]; + _klen = _JSON_string_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_string_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_string_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_string_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_string_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_string_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + _trans = _JSON_string_indicies[_trans]; + cs = _JSON_string_trans_targs[_trans]; + + if ( _JSON_string_trans_actions[_trans] != 0 ) { + _acts = _JSON_string_trans_actions[_trans]; + _nacts = (int) _JSON_string_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_string_actions[_acts++] ) + { + case 0: +// line 519 "Parser.rl" + { + int offset = byteList.begin(); + ByteList decoded = decoder.decode(byteList, memo + 1 - offset, + p - offset); + result = getRuntime().newString(decoded); + if (result == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + {p = (( p + 1))-1;} + } + } + break; + case 1: +// line 532 "Parser.rl" + { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; +// line 1363 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 553 "Parser.rl" + + if (cs >= JSON_string_first_final && result != null) { + return new ParserResult(result, p + 1); + } else { + return null; + } + } + + +// line 1393 "Parser.java" +private static byte[] init__JSON_array_actions_0() +{ + return new byte [] { + 0, 1, 0, 1, 1 + }; +} + +private static final byte _JSON_array_actions[] = init__JSON_array_actions_0(); + + +private static byte[] init__JSON_array_key_offsets_0() +{ + return new byte [] { + 0, 0, 1, 18, 25, 41, 43, 44, 46, 47, 49, 50, + 52, 53, 55, 56, 58, 59 + }; +} + +private static final byte _JSON_array_key_offsets[] = init__JSON_array_key_offsets_0(); + + +private static char[] init__JSON_array_trans_keys_0() +{ + return new char [] { + 91, 13, 32, 34, 45, 47, 73, 78, 91, 93, 102, 110, + 116, 123, 9, 10, 48, 57, 13, 32, 44, 47, 93, 9, + 10, 13, 32, 34, 45, 47, 73, 78, 91, 102, 110, 116, + 123, 9, 10, 48, 57, 42, 47, 42, 42, 47, 10, 42, + 47, 42, 42, 47, 10, 42, 47, 42, 42, 47, 10, 0 + }; +} + +private static final char _JSON_array_trans_keys[] = init__JSON_array_trans_keys_0(); + + +private static byte[] init__JSON_array_single_lengths_0() +{ + return new byte [] { + 0, 1, 13, 5, 12, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 1, 2, 1, 0 + }; +} + +private static final byte _JSON_array_single_lengths[] = init__JSON_array_single_lengths_0(); + + +private static byte[] init__JSON_array_range_lengths_0() +{ + return new byte [] { + 0, 0, 2, 1, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + }; +} + +private static final byte _JSON_array_range_lengths[] = init__JSON_array_range_lengths_0(); + + +private static byte[] init__JSON_array_index_offsets_0() +{ + return new byte [] { + 0, 0, 2, 18, 25, 40, 43, 45, 48, 50, 53, 55, + 58, 60, 63, 65, 68, 70 + }; +} + +private static final byte _JSON_array_index_offsets[] = init__JSON_array_index_offsets_0(); + + +private static byte[] init__JSON_array_indicies_0() +{ + return new byte [] { + 0, 1, 0, 0, 2, 2, 3, 2, 2, 2, 4, 2, + 2, 2, 2, 0, 2, 1, 5, 5, 6, 7, 4, 5, + 1, 6, 6, 2, 2, 8, 2, 2, 2, 2, 2, 2, + 2, 6, 2, 1, 9, 10, 1, 11, 9, 11, 6, 9, + 6, 10, 12, 13, 1, 14, 12, 14, 5, 12, 5, 13, + 15, 16, 1, 17, 15, 17, 0, 15, 0, 16, 1, 0 + }; +} + +private static final byte _JSON_array_indicies[] = init__JSON_array_indicies_0(); + + +private static byte[] init__JSON_array_trans_targs_0() +{ + return new byte [] { + 2, 0, 3, 13, 17, 3, 4, 9, 5, 6, 8, 7, + 10, 12, 11, 14, 16, 15 + }; +} + +private static final byte _JSON_array_trans_targs[] = init__JSON_array_trans_targs_0(); + + +private static byte[] init__JSON_array_trans_actions_0() +{ + return new byte [] { + 0, 0, 1, 0, 3, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + }; +} + +private static final byte _JSON_array_trans_actions[] = init__JSON_array_trans_actions_0(); + + +static final int JSON_array_start = 1; +static final int JSON_array_first_final = 17; +static final int JSON_array_error = 0; + +static final int JSON_array_en_main = 1; + + +// line 594 "Parser.rl" + + + ParserResult parseArray(int p, int pe) { + int cs = EVIL; + + if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) { + throw newException(Utils.M_NESTING_ERROR, + "nesting of " + currentNesting + " is too deep"); + } + + // this is guaranteed to be a RubyArray due to the earlier + // allocator test at OptionsReader#getClass + RubyArray result = + (RubyArray)parser.arrayClass.newInstance(context, + IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); + + +// line 1524 "Parser.java" + { + cs = JSON_array_start; + } + +// line 611 "Parser.rl" + +// line 1531 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _match: do { + _keys = _JSON_array_key_offsets[cs]; + _trans = _JSON_array_index_offsets[cs]; + _klen = _JSON_array_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_array_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_array_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_array_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_array_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_array_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + _trans = _JSON_array_indicies[_trans]; + cs = _JSON_array_trans_targs[_trans]; + + if ( _JSON_array_trans_actions[_trans] != 0 ) { + _acts = _JSON_array_trans_actions[_trans]; + _nacts = (int) _JSON_array_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_array_actions[_acts++] ) + { + case 0: +// line 567 "Parser.rl" + { + ParserResult res = parseValue(p, pe); + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result.append(res.result); + {p = (( res.p))-1;} + } + } + break; + case 1: +// line 578 "Parser.rl" + { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; +// line 1631 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 612 "Parser.rl" + + if (cs >= JSON_array_first_final) { + return new ParserResult(result, p + 1); + } else { + throw unexpectedToken(p, pe); + } + } + + +// line 1661 "Parser.java" +private static byte[] init__JSON_object_actions_0() +{ + return new byte [] { + 0, 1, 0, 1, 1, 1, 2 + }; +} + +private static final byte _JSON_object_actions[] = init__JSON_object_actions_0(); + + +private static byte[] init__JSON_object_key_offsets_0() +{ + return new byte [] { + 0, 0, 1, 8, 14, 16, 17, 19, 20, 36, 43, 49, + 51, 52, 54, 55, 57, 58, 60, 61, 63, 64, 66, 67, + 69, 70, 72, 73 + }; +} + +private static final byte _JSON_object_key_offsets[] = init__JSON_object_key_offsets_0(); + + +private static char[] init__JSON_object_trans_keys_0() +{ + return new char [] { + 123, 13, 32, 34, 47, 125, 9, 10, 13, 32, 47, 58, + 9, 10, 42, 47, 42, 42, 47, 10, 13, 32, 34, 45, + 47, 73, 78, 91, 102, 110, 116, 123, 9, 10, 48, 57, + 13, 32, 44, 47, 125, 9, 10, 13, 32, 34, 47, 9, + 10, 42, 47, 42, 42, 47, 10, 42, 47, 42, 42, 47, + 10, 42, 47, 42, 42, 47, 10, 42, 47, 42, 42, 47, + 10, 0 + }; +} + +private static final char _JSON_object_trans_keys[] = init__JSON_object_trans_keys_0(); + + +private static byte[] init__JSON_object_single_lengths_0() +{ + return new byte [] { + 0, 1, 5, 4, 2, 1, 2, 1, 12, 5, 4, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 1, 0 + }; +} + +private static final byte _JSON_object_single_lengths[] = init__JSON_object_single_lengths_0(); + + +private static byte[] init__JSON_object_range_lengths_0() +{ + return new byte [] { + 0, 0, 1, 1, 0, 0, 0, 0, 2, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0 + }; +} + +private static final byte _JSON_object_range_lengths[] = init__JSON_object_range_lengths_0(); + + +private static byte[] init__JSON_object_index_offsets_0() +{ + return new byte [] { + 0, 0, 2, 9, 15, 18, 20, 23, 25, 40, 47, 53, + 56, 58, 61, 63, 66, 68, 71, 73, 76, 78, 81, 83, + 86, 88, 91, 93 + }; +} + +private static final byte _JSON_object_index_offsets[] = init__JSON_object_index_offsets_0(); + + +private static byte[] init__JSON_object_indicies_0() +{ + return new byte [] { + 0, 1, 0, 0, 2, 3, 4, 0, 1, 5, 5, 6, + 7, 5, 1, 8, 9, 1, 10, 8, 10, 5, 8, 5, + 9, 7, 7, 11, 11, 12, 11, 11, 11, 11, 11, 11, + 11, 7, 11, 1, 13, 13, 14, 15, 4, 13, 1, 14, + 14, 2, 16, 14, 1, 17, 18, 1, 19, 17, 19, 14, + 17, 14, 18, 20, 21, 1, 22, 20, 22, 13, 20, 13, + 21, 23, 24, 1, 25, 23, 25, 7, 23, 7, 24, 26, + 27, 1, 28, 26, 28, 0, 26, 0, 27, 1, 0 + }; +} + +private static final byte _JSON_object_indicies[] = init__JSON_object_indicies_0(); + + +private static byte[] init__JSON_object_trans_targs_0() +{ + return new byte [] { + 2, 0, 3, 23, 27, 3, 4, 8, 5, 7, 6, 9, + 19, 9, 10, 15, 11, 12, 14, 13, 16, 18, 17, 20, + 22, 21, 24, 26, 25 + }; +} + +private static final byte _JSON_object_trans_targs[] = init__JSON_object_trans_targs_0(); + + +private static byte[] init__JSON_object_trans_actions_0() +{ + return new byte [] { + 0, 0, 3, 0, 5, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + }; +} + +private static final byte _JSON_object_trans_actions[] = init__JSON_object_trans_actions_0(); + + +static final int JSON_object_start = 1; +static final int JSON_object_first_final = 27; +static final int JSON_object_error = 0; + +static final int JSON_object_en_main = 1; + + +// line 668 "Parser.rl" + + + ParserResult parseObject(int p, int pe) { + int cs = EVIL; + IRubyObject lastName = null; + + if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) { + throw newException(Utils.M_NESTING_ERROR, + "nesting of " + currentNesting + " is too deep"); + } + + // this is guaranteed to be a RubyHash due to the earlier + // allocator test at OptionsReader#getClass + RubyHash result = + (RubyHash)parser.objectClass.newInstance(context, + IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); + + +// line 1803 "Parser.java" + { + cs = JSON_object_start; + } + +// line 686 "Parser.rl" + +// line 1810 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _match: do { + _keys = _JSON_object_key_offsets[cs]; + _trans = _JSON_object_index_offsets[cs]; + _klen = _JSON_object_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_object_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_object_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_object_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_object_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_object_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + _trans = _JSON_object_indicies[_trans]; + cs = _JSON_object_trans_targs[_trans]; + + if ( _JSON_object_trans_actions[_trans] != 0 ) { + _acts = _JSON_object_trans_actions[_trans]; + _nacts = (int) _JSON_object_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_object_actions[_acts++] ) + { + case 0: +// line 626 "Parser.rl" + { + ParserResult res = parseValue(p, pe); + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result.op_aset(context, lastName, res.result); + {p = (( res.p))-1;} + } + } + break; + case 1: +// line 637 "Parser.rl" + { + ParserResult res = parseString(p, pe); + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + RubyString name = (RubyString)res.result; + if (parser.symbolizeNames) { + lastName = context.getRuntime().is1_9() + ? name.intern19() + : name.intern(); + } else { + lastName = name; + } + {p = (( res.p))-1;} + } + } + break; + case 2: +// line 655 "Parser.rl" + { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } + break; +// line 1930 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 687 "Parser.rl" + + if (cs < JSON_object_first_final) { + return null; + } + + IRubyObject returnedResult = result; + + // attempt to de-serialize object + if (parser.createId != null) { + IRubyObject vKlassName = result.op_aref(context, parser.createId); + if (!vKlassName.isNil()) { + // might throw ArgumentError, we let it propagate + IRubyObject klass = parser.info.jsonModule. + callMethod(context, "deep_const_get", vKlassName); + if (klass.respondsTo("json_creatable?") && + klass.callMethod(context, "json_creatable?").isTrue()) { + + returnedResult = klass.callMethod(context, "json_create", result); + } + } + } + return new ParserResult(returnedResult, p + 1); + } + + +// line 1976 "Parser.java" +private static byte[] init__JSON_actions_0() +{ + return new byte [] { + 0, 1, 0, 1, 1 + }; +} + +private static final byte _JSON_actions[] = init__JSON_actions_0(); + + +private static byte[] init__JSON_key_offsets_0() +{ + return new byte [] { + 0, 0, 7, 9, 10, 12, 13, 15, 16, 18, 19 + }; +} + +private static final byte _JSON_key_offsets[] = init__JSON_key_offsets_0(); + + +private static char[] init__JSON_trans_keys_0() +{ + return new char [] { + 13, 32, 47, 91, 123, 9, 10, 42, 47, 42, 42, 47, + 10, 42, 47, 42, 42, 47, 10, 13, 32, 47, 9, 10, + 0 + }; +} + +private static final char _JSON_trans_keys[] = init__JSON_trans_keys_0(); + + +private static byte[] init__JSON_single_lengths_0() +{ + return new byte [] { + 0, 5, 2, 1, 2, 1, 2, 1, 2, 1, 3 + }; +} + +private static final byte _JSON_single_lengths[] = init__JSON_single_lengths_0(); + + +private static byte[] init__JSON_range_lengths_0() +{ + return new byte [] { + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 + }; +} + +private static final byte _JSON_range_lengths[] = init__JSON_range_lengths_0(); + + +private static byte[] init__JSON_index_offsets_0() +{ + return new byte [] { + 0, 0, 7, 10, 12, 15, 17, 20, 22, 25, 27 + }; +} + +private static final byte _JSON_index_offsets[] = init__JSON_index_offsets_0(); + + +private static byte[] init__JSON_indicies_0() +{ + return new byte [] { + 0, 0, 2, 3, 4, 0, 1, 5, 6, 1, 7, 5, + 7, 0, 5, 0, 6, 8, 9, 1, 10, 8, 10, 11, + 8, 11, 9, 11, 11, 12, 11, 1, 0 + }; +} + +private static final byte _JSON_indicies[] = init__JSON_indicies_0(); + + +private static byte[] init__JSON_trans_targs_0() +{ + return new byte [] { + 1, 0, 2, 10, 10, 3, 5, 4, 7, 9, 8, 10, + 6 + }; +} + +private static final byte _JSON_trans_targs[] = init__JSON_trans_targs_0(); + + +private static byte[] init__JSON_trans_actions_0() +{ + return new byte [] { + 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, + 0 + }; +} + +private static final byte _JSON_trans_actions[] = init__JSON_trans_actions_0(); + + +static final int JSON_start = 1; +static final int JSON_first_final = 10; +static final int JSON_error = 0; + +static final int JSON_en_main = 1; + + +// line 745 "Parser.rl" + + + public IRubyObject parse() { + int cs = EVIL; + int p, pe; + IRubyObject result = null; + + +// line 2089 "Parser.java" + { + cs = JSON_start; + } + +// line 753 "Parser.rl" + p = byteList.begin(); + pe = p + byteList.length(); + +// line 2098 "Parser.java" + { + int _klen; + int _trans = 0; + int _acts; + int _nacts; + int _keys; + int _goto_targ = 0; + + _goto: while (true) { + switch ( _goto_targ ) { + case 0: + if ( p == pe ) { + _goto_targ = 4; + continue _goto; + } + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } +case 1: + _match: do { + _keys = _JSON_key_offsets[cs]; + _trans = _JSON_index_offsets[cs]; + _klen = _JSON_single_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + _klen - 1; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( data[p] < _JSON_trans_keys[_mid] ) + _upper = _mid - 1; + else if ( data[p] > _JSON_trans_keys[_mid] ) + _lower = _mid + 1; + else { + _trans += (_mid - _keys); + break _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _JSON_range_lengths[cs]; + if ( _klen > 0 ) { + int _lower = _keys; + int _mid; + int _upper = _keys + (_klen<<1) - 2; + while (true) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( data[p] < _JSON_trans_keys[_mid] ) + _upper = _mid - 2; + else if ( data[p] > _JSON_trans_keys[_mid+1] ) + _lower = _mid + 2; + else { + _trans += ((_mid - _keys)>>1); + break _match; + } + } + _trans += _klen; + } + } while (false); + + _trans = _JSON_indicies[_trans]; + cs = _JSON_trans_targs[_trans]; + + if ( _JSON_trans_actions[_trans] != 0 ) { + _acts = _JSON_trans_actions[_trans]; + _nacts = (int) _JSON_actions[_acts++]; + while ( _nacts-- > 0 ) + { + switch ( _JSON_actions[_acts++] ) + { + case 0: +// line 717 "Parser.rl" + { + currentNesting = 1; + ParserResult res = parseObject(p, pe); + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result = res.result; + {p = (( res.p))-1;} + } + } + break; + case 1: +// line 729 "Parser.rl" + { + currentNesting = 1; + ParserResult res = parseArray(p, pe); + if (res == null) { + p--; + { p += 1; _goto_targ = 5; if (true) continue _goto;} + } else { + result = res.result; + {p = (( res.p))-1;} + } + } + break; +// line 2206 "Parser.java" + } + } + } + +case 2: + if ( cs == 0 ) { + _goto_targ = 5; + continue _goto; + } + if ( ++p != pe ) { + _goto_targ = 1; + continue _goto; + } +case 4: +case 5: + } + break; } + } + +// line 756 "Parser.rl" + + if (cs >= JSON_first_final && p == pe) { + return result; + } else { + throw unexpectedToken(p, pe); + } + } + + /** + * Returns a subsequence of the source ByteList, based on source + * array byte offsets (i.e., the ByteList's own begin offset is not + * automatically added). + * @param start + * @param end + */ + private ByteList absSubSequence(int absStart, int absEnd) { + int offset = byteList.begin(); + return (ByteList)byteList.subSequence(absStart - offset, + absEnd - offset); + } + + /** + * Retrieves a constant directly descended from the <code>JSON</code> module. + * @param name The constant name + */ + private IRubyObject getConstant(String name) { + return parser.info.jsonModule.getConstant(name); + } + + private RaiseException newException(String className, String message) { + return Utils.newException(context, className, message); + } + + private RaiseException newException(String className, RubyString message) { + return Utils.newException(context, className, message); + } + + private RaiseException newException(String className, + String messageBegin, ByteList messageEnd) { + return newException(className, + getRuntime().newString(messageBegin).cat(messageEnd)); + } + } +} diff --git a/java/src/json/ext/Parser.rl b/java/src/json/ext/Parser.rl new file mode 100644 index 0000000..00badc8 --- /dev/null +++ b/java/src/json/ext/Parser.rl @@ -0,0 +1,799 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyArray; +import org.jruby.RubyClass; +import org.jruby.RubyEncoding; +import org.jruby.RubyFloat; +import org.jruby.RubyHash; +import org.jruby.RubyInteger; +import org.jruby.RubyModule; +import org.jruby.RubyNumeric; +import org.jruby.RubyObject; +import org.jruby.RubyString; +import org.jruby.anno.JRubyMethod; +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.Block; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.Visibility; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +/** + * The <code>JSON::Ext::Parser</code> class. + * + * <p>This is the JSON parser implemented as a Java class. To use it as the + * standard parser, set + * <pre>JSON.parser = JSON::Ext::Parser</pre> + * This is performed for you when you <code>include "json/ext"</code>. + * + * <p>This class does not perform the actual parsing, just acts as an interface + * to Ruby code. When the {@link #parse()} method is invoked, a + * Parser.ParserSession object is instantiated, which handles the process. + * + * @author mernen + */ +public class Parser extends RubyObject { + private final RuntimeInfo info; + private RubyString vSource; + private RubyString createId; + private int maxNesting; + private boolean allowNaN; + private boolean symbolizeNames; + private RubyClass objectClass; + private RubyClass arrayClass; + + private static final int DEFAULT_MAX_NESTING = 19; + + private static final String JSON_MINUS_INFINITY = "-Infinity"; + // constant names in the JSON module containing those values + private static final String CONST_NAN = "NaN"; + private static final String CONST_INFINITY = "Infinity"; + private static final String CONST_MINUS_INFINITY = "MinusInfinity"; + + static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { + public IRubyObject allocate(Ruby runtime, RubyClass klazz) { + return new Parser(runtime, klazz); + } + }; + + /** + * Multiple-value return for internal parser methods. + * + * <p>All the <code>parse<var>Stuff</var></code> methods return instances of + * <code>ParserResult</code> when successful, or <code>null</code> when + * there's a problem with the input data. + */ + static final class ParserResult { + /** + * The result of the successful parsing. Should never be + * <code>null</code>. + */ + final IRubyObject result; + /** + * The point where the parser returned. + */ + final int p; + + ParserResult(IRubyObject result, int p) { + this.result = result; + this.p = p; + } + } + + public Parser(Ruby runtime, RubyClass metaClass) { + super(runtime, metaClass); + info = RuntimeInfo.forRuntime(runtime); + } + + /** + * <code>Parser.new(source, opts = {})</code> + * + * <p>Creates a new <code>JSON::Ext::Parser</code> instance for the string + * <code>source</code>. + * It will be configured by the <code>opts</code> Hash. + * <code>opts</code> can have the following keys: + * + * <dl> + * <dt><code>:max_nesting</code> + * <dd>The maximum depth of nesting allowed in the parsed data + * structures. Disable depth checking with <code>:max_nesting => false|nil|0</code>, + * it defaults to 19. + * + * <dt><code>:allow_nan</code> + * <dd>If set to <code>true</code>, allow <code>NaN</code>, + * <code>Infinity</code> and <code>-Infinity</code> in defiance of RFC 4627 + * to be parsed by the Parser. This option defaults to <code>false</code>. + * + * <dt><code>:symbolize_names</code> + * <dd>If set to <code>true</code>, returns symbols for the names (keys) in + * a JSON object. Otherwise strings are returned, which is also the default. + * + * <dt><code>:create_additions</code> + * <dd>If set to <code>false</code>, the Parser doesn't create additions + * even if a matchin class and <code>create_id</code> was found. This option + * defaults to <code>true</code>. + * + * <dt><code>:object_class</code> + * <dd>Defaults to Hash. + * + * <dt><code>:array_class</code> + * <dd>Defaults to Array. + * </dl> + */ + @JRubyMethod(name = "new", required = 1, optional = 1, meta = true) + public static IRubyObject newInstance(IRubyObject clazz, IRubyObject[] args, Block block) { + Parser parser = (Parser)((RubyClass)clazz).allocate(); + + parser.callInit(args, block); + + return parser; + } + + @JRubyMethod(required = 1, optional = 1, visibility = Visibility.PRIVATE) + public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { + Ruby runtime = context.getRuntime(); + RubyString source = convertEncoding(context, args[0].convertToString()); + + OptionsReader opts = + new OptionsReader(context, args.length > 1 ? args[1] : null); + + this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); + this.allowNaN = opts.getBool("allow_nan", false); + this.symbolizeNames = opts.getBool("symbolize_names", false); + this.createId = + opts.getBool("create_additions", true) ? getCreateId(context) + : null; + this.objectClass = opts.getClass("object_class", runtime.getHash()); + this.arrayClass = opts.getClass("array_class", runtime.getArray()); + + this.vSource = source; + return this; + } + + /** + * Checks the given string's encoding. If a non-UTF-8 encoding is detected, + * a converted copy is returned. + * Returns the source string if no conversion is needed. + */ + private RubyString convertEncoding(ThreadContext context, RubyString source) { + ByteList bl = source.getByteList(); + int len = bl.length(); + if (len < 2) { + throw Utils.newException(context, Utils.M_PARSER_ERROR, + "A JSON text must at least contain two octets!"); + } + + if (info.encodingsSupported()) { + RubyEncoding encoding = (RubyEncoding)source.encoding(context); + if (encoding != info.ascii8bit) { + return (RubyString)source.encode(context, info.utf8); + } + + String sniffedEncoding = sniffByteList(bl); + if (sniffedEncoding == null) return source; // assume UTF-8 + return reinterpretEncoding(context, source, sniffedEncoding); + } + + String sniffedEncoding = sniffByteList(bl); + if (sniffedEncoding == null) return source; // assume UTF-8 + Ruby runtime = context.getRuntime(); + return (RubyString)info.jsonModule. + callMethod(context, "iconv", + new IRubyObject[] { + runtime.newString("utf-8"), + runtime.newString(sniffedEncoding), + source}); + } + + /** + * Checks the first four bytes of the given ByteList to infer its encoding, + * using the principle demonstrated on section 3 of RFC 4627 (JSON). + */ + private static String sniffByteList(ByteList bl) { + if (bl.length() < 4) return null; + if (bl.get(0) == 0 && bl.get(2) == 0) { + return bl.get(1) == 0 ? "utf-32be" : "utf-16be"; + } + if (bl.get(1) == 0 && bl.get(3) == 0) { + return bl.get(2) == 0 ? "utf-32le" : "utf-16le"; + } + return null; + } + + /** + * Assumes the given (binary) RubyString to be in the given encoding, then + * converts it to UTF-8. + */ + private RubyString reinterpretEncoding(ThreadContext context, + RubyString str, String sniffedEncoding) { + RubyEncoding actualEncoding = info.getEncoding(context, sniffedEncoding); + RubyEncoding targetEncoding = info.utf8; + RubyString dup = (RubyString)str.dup(); + dup.force_encoding(context, actualEncoding); + return (RubyString)dup.encode_bang(context, targetEncoding); + } + + /** + * <code>Parser#parse()</code> + * + * <p>Parses the current JSON text <code>source</code> and returns the + * complete data structure as a result. + */ + @JRubyMethod + public IRubyObject parse(ThreadContext context) { + return new ParserSession(this, context).parse(); + } + + /** + * <code>Parser#source()</code> + * + * <p>Returns a copy of the current <code>source</code> string, that was + * used to construct this Parser. + */ + @JRubyMethod(name = "source") + public IRubyObject source_get() { + return vSource.dup(); + } + + /** + * Queries <code>JSON.create_id</code>. Returns <code>null</code> if it is + * set to <code>nil</code> or <code>false</code>, and a String if not. + */ + private RubyString getCreateId(ThreadContext context) { + IRubyObject v = info.jsonModule.callMethod(context, "create_id"); + return v.isTrue() ? v.convertToString() : null; + } + + /** + * A string parsing session. + * + * <p>Once a ParserSession is instantiated, the source string should not + * change until the parsing is complete. The ParserSession object assumes + * the source {@link RubyString} is still associated to its original + * {@link ByteList}, which in turn must still be bound to the same + * <code>byte[]</code> value (and on the same offset). + */ + // Ragel uses lots of fall-through + @SuppressWarnings("fallthrough") + private static class ParserSession { + private final Parser parser; + private final ThreadContext context; + private final ByteList byteList; + private final byte[] data; + private final StringDecoder decoder; + private int currentNesting = 0; + + // initialization value for all state variables. + // no idea about the origins of this value, ask Flori ;) + private static final int EVIL = 0x666; + + private ParserSession(Parser parser, ThreadContext context) { + this.parser = parser; + this.context = context; + this.byteList = parser.vSource.getByteList(); + this.data = byteList.unsafeBytes(); + this.decoder = new StringDecoder(context); + } + + private RaiseException unexpectedToken(int absStart, int absEnd) { + RubyString msg = getRuntime().newString("unexpected token at '") + .cat(data, absStart, absEnd - absStart) + .cat((byte)'\''); + return newException(Utils.M_PARSER_ERROR, msg); + } + + private Ruby getRuntime() { + return context.getRuntime(); + } + + %%{ + machine JSON_common; + + cr = '\n'; + cr_neg = [^\n]; + ws = [ \t\r\n]; + c_comment = '/*' ( any* - (any* '*/' any* ) ) '*/'; + cpp_comment = '//' cr_neg* cr; + comment = c_comment | cpp_comment; + ignore = ws | comment; + name_separator = ':'; + value_separator = ','; + Vnull = 'null'; + Vfalse = 'false'; + Vtrue = 'true'; + VNaN = 'NaN'; + VInfinity = 'Infinity'; + VMinusInfinity = '-Infinity'; + begin_value = [nft"\-[{NI] | digit; + begin_object = '{'; + end_object = '}'; + begin_array = '['; + end_array = ']'; + begin_string = '"'; + begin_name = begin_string; + begin_number = digit | '-'; + }%% + + %%{ + machine JSON_value; + include JSON_common; + + write data; + + action parse_null { + result = getRuntime().getNil(); + } + action parse_false { + result = getRuntime().getFalse(); + } + action parse_true { + result = getRuntime().getTrue(); + } + action parse_nan { + if (parser.allowNaN) { + result = getConstant(CONST_NAN); + } else { + throw unexpectedToken(p - 2, pe); + } + } + action parse_infinity { + if (parser.allowNaN) { + result = getConstant(CONST_INFINITY); + } else { + throw unexpectedToken(p - 7, pe); + } + } + action parse_number { + if (pe > fpc + 9 && + absSubSequence(fpc, fpc + 9).toString().equals(JSON_MINUS_INFINITY)) { + + if (parser.allowNaN) { + result = getConstant(CONST_MINUS_INFINITY); + fexec p + 10; + fhold; + fbreak; + } else { + throw unexpectedToken(p, pe); + } + } + ParserResult res = parseFloat(fpc, pe); + if (res != null) { + result = res.result; + fexec res.p; + } + res = parseInteger(fpc, pe); + if (res != null) { + result = res.result; + fexec res.p; + } + fhold; + fbreak; + } + action parse_string { + ParserResult res = parseString(fpc, pe); + if (res == null) { + fhold; + fbreak; + } else { + result = res.result; + fexec res.p; + } + } + action parse_array { + currentNesting++; + ParserResult res = parseArray(fpc, pe); + currentNesting--; + if (res == null) { + fhold; + fbreak; + } else { + result = res.result; + fexec res.p; + } + } + action parse_object { + currentNesting++; + ParserResult res = parseObject(fpc, pe); + currentNesting--; + if (res == null) { + fhold; + fbreak; + } else { + result = res.result; + fexec res.p; + } + } + action exit { + fhold; + fbreak; + } + + main := ( Vnull @parse_null | + Vfalse @parse_false | + Vtrue @parse_true | + VNaN @parse_nan | + VInfinity @parse_infinity | + begin_number >parse_number | + begin_string >parse_string | + begin_array >parse_array | + begin_object >parse_object + ) %*exit; + }%% + + ParserResult parseValue(int p, int pe) { + int cs = EVIL; + IRubyObject result = null; + + %% write init; + %% write exec; + + if (cs >= JSON_value_first_final && result != null) { + return new ParserResult(result, p); + } else { + return null; + } + } + + %%{ + machine JSON_integer; + + write data; + + action exit { + fhold; + fbreak; + } + + main := '-'? ( '0' | [1-9][0-9]* ) ( ^[0-9] @exit ); + }%% + + ParserResult parseInteger(int p, int pe) { + int cs = EVIL; + + %% write init; + int memo = p; + %% write exec; + + if (cs < JSON_integer_first_final) { + return null; + } + + ByteList num = absSubSequence(memo, p); + // note: this is actually a shared string, but since it is temporary and + // read-only, it doesn't really matter + RubyString expr = RubyString.newStringLight(getRuntime(), num); + RubyInteger number = RubyNumeric.str2inum(getRuntime(), expr, 10, true); + return new ParserResult(number, p + 1); + } + + %%{ + machine JSON_float; + include JSON_common; + + write data; + + action exit { + fhold; + fbreak; + } + + main := '-'? + ( ( ( '0' | [1-9][0-9]* ) '.' [0-9]+ ( [Ee] [+\-]?[0-9]+ )? ) + | ( ( '0' | [1-9][0-9]* ) ( [Ee] [+\-]? [0-9]+ ) ) ) + ( ^[0-9Ee.\-] @exit ); + }%% + + ParserResult parseFloat(int p, int pe) { + int cs = EVIL; + + %% write init; + int memo = p; + %% write exec; + + if (cs < JSON_float_first_final) { + return null; + } + + ByteList num = absSubSequence(memo, p); + // note: this is actually a shared string, but since it is temporary and + // read-only, it doesn't really matter + RubyString expr = RubyString.newStringLight(getRuntime(), num); + RubyFloat number = RubyNumeric.str2fnum(getRuntime(), expr, true); + return new ParserResult(number, p + 1); + } + + %%{ + machine JSON_string; + include JSON_common; + + write data; + + action parse_string { + int offset = byteList.begin(); + ByteList decoded = decoder.decode(byteList, memo + 1 - offset, + p - offset); + result = getRuntime().newString(decoded); + if (result == null) { + fhold; + fbreak; + } else { + fexec p + 1; + } + } + + action exit { + fhold; + fbreak; + } + + main := '"' + ( ( ^(["\\]|0..0x1f) + | '\\'["\\/bfnrt] + | '\\u'[0-9a-fA-F]{4} + | '\\'^(["\\/bfnrtu]|0..0x1f) + )* %parse_string + ) '"' @exit; + }%% + + ParserResult parseString(int p, int pe) { + int cs = EVIL; + RubyString result = null; + + %% write init; + int memo = p; + %% write exec; + + if (cs >= JSON_string_first_final && result != null) { + return new ParserResult(result, p + 1); + } else { + return null; + } + } + + %%{ + machine JSON_array; + include JSON_common; + + write data; + + action parse_value { + ParserResult res = parseValue(fpc, pe); + if (res == null) { + fhold; + fbreak; + } else { + result.append(res.result); + fexec res.p; + } + } + + action exit { + fhold; + fbreak; + } + + next_element = value_separator ignore* begin_value >parse_value; + + main := begin_array + ignore* + ( ( begin_value >parse_value + ignore* ) + ( ignore* + next_element + ignore* )* )? + ignore* + end_array @exit; + }%% + + ParserResult parseArray(int p, int pe) { + int cs = EVIL; + + if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) { + throw newException(Utils.M_NESTING_ERROR, + "nesting of " + currentNesting + " is too deep"); + } + + // this is guaranteed to be a RubyArray due to the earlier + // allocator test at OptionsReader#getClass + RubyArray result = + (RubyArray)parser.arrayClass.newInstance(context, + IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); + + %% write init; + %% write exec; + + if (cs >= JSON_array_first_final) { + return new ParserResult(result, p + 1); + } else { + throw unexpectedToken(p, pe); + } + } + + %%{ + machine JSON_object; + include JSON_common; + + write data; + + action parse_value { + ParserResult res = parseValue(fpc, pe); + if (res == null) { + fhold; + fbreak; + } else { + result.op_aset(context, lastName, res.result); + fexec res.p; + } + } + + action parse_name { + ParserResult res = parseString(fpc, pe); + if (res == null) { + fhold; + fbreak; + } else { + RubyString name = (RubyString)res.result; + if (parser.symbolizeNames) { + lastName = context.getRuntime().is1_9() + ? name.intern19() + : name.intern(); + } else { + lastName = name; + } + fexec res.p; + } + } + + action exit { + fhold; + fbreak; + } + + a_pair = ignore* + begin_name >parse_name + ignore* name_separator ignore* + begin_value >parse_value; + + main := begin_object + (a_pair (ignore* value_separator a_pair)*)? + ignore* end_object @exit; + }%% + + ParserResult parseObject(int p, int pe) { + int cs = EVIL; + IRubyObject lastName = null; + + if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) { + throw newException(Utils.M_NESTING_ERROR, + "nesting of " + currentNesting + " is too deep"); + } + + // this is guaranteed to be a RubyHash due to the earlier + // allocator test at OptionsReader#getClass + RubyHash result = + (RubyHash)parser.objectClass.newInstance(context, + IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); + + %% write init; + %% write exec; + + if (cs < JSON_object_first_final) { + return null; + } + + IRubyObject returnedResult = result; + + // attempt to de-serialize object + if (parser.createId != null) { + IRubyObject vKlassName = result.op_aref(context, parser.createId); + if (!vKlassName.isNil()) { + // might throw ArgumentError, we let it propagate + IRubyObject klass = parser.info.jsonModule. + callMethod(context, "deep_const_get", vKlassName); + if (klass.respondsTo("json_creatable?") && + klass.callMethod(context, "json_creatable?").isTrue()) { + + returnedResult = klass.callMethod(context, "json_create", result); + } + } + } + return new ParserResult(returnedResult, p + 1); + } + + %%{ + machine JSON; + include JSON_common; + + write data; + + action parse_object { + currentNesting = 1; + ParserResult res = parseObject(fpc, pe); + if (res == null) { + fhold; + fbreak; + } else { + result = res.result; + fexec res.p; + } + } + + action parse_array { + currentNesting = 1; + ParserResult res = parseArray(fpc, pe); + if (res == null) { + fhold; + fbreak; + } else { + result = res.result; + fexec res.p; + } + } + + main := ignore* + ( begin_object >parse_object + | begin_array >parse_array ) + ignore*; + }%% + + public IRubyObject parse() { + int cs = EVIL; + int p, pe; + IRubyObject result = null; + + %% write init; + p = byteList.begin(); + pe = p + byteList.length(); + %% write exec; + + if (cs >= JSON_first_final && p == pe) { + return result; + } else { + throw unexpectedToken(p, pe); + } + } + + /** + * Returns a subsequence of the source ByteList, based on source + * array byte offsets (i.e., the ByteList's own begin offset is not + * automatically added). + * @param start + * @param end + */ + private ByteList absSubSequence(int absStart, int absEnd) { + int offset = byteList.begin(); + return (ByteList)byteList.subSequence(absStart - offset, + absEnd - offset); + } + + /** + * Retrieves a constant directly descended from the <code>JSON</code> module. + * @param name The constant name + */ + private IRubyObject getConstant(String name) { + return parser.info.jsonModule.getConstant(name); + } + + private RaiseException newException(String className, String message) { + return Utils.newException(context, className, message); + } + + private RaiseException newException(String className, RubyString message) { + return Utils.newException(context, className, message); + } + + private RaiseException newException(String className, + String messageBegin, ByteList messageEnd) { + return newException(className, + getRuntime().newString(messageBegin).cat(messageEnd)); + } + } +} diff --git a/java/src/json/ext/ParserService.java b/java/src/json/ext/ParserService.java new file mode 100644 index 0000000..e0805a7 --- /dev/null +++ b/java/src/json/ext/ParserService.java @@ -0,0 +1,34 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import java.io.IOException; + +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyModule; +import org.jruby.runtime.load.BasicLibraryService; + +/** + * The service invoked by JRuby's {@link org.jruby.runtime.load.LoadService LoadService}. + * Defines the <code>JSON::Ext::Parser</code> class. + * @author mernen + */ +public class ParserService implements BasicLibraryService { + public boolean basicLoad(Ruby runtime) throws IOException { + runtime.getLoadService().require("json/common"); + RuntimeInfo info = RuntimeInfo.initRuntime(runtime); + + info.jsonModule = runtime.defineModule("JSON"); + RubyModule jsonExtModule = info.jsonModule.defineModuleUnder("Ext"); + RubyClass parserClass = + jsonExtModule.defineClassUnder("Parser", runtime.getObject(), + Parser.ALLOCATOR); + parserClass.defineAnnotatedMethods(Parser.class); + return true; + } +} diff --git a/java/src/json/ext/RuntimeInfo.java b/java/src/json/ext/RuntimeInfo.java new file mode 100644 index 0000000..f446afe --- /dev/null +++ b/java/src/json/ext/RuntimeInfo.java @@ -0,0 +1,119 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyEncoding; +import org.jruby.RubyModule; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; + + +final class RuntimeInfo { + // since the vast majority of cases runs just one runtime, + // we optimize for that + private static WeakReference<Ruby> runtime1 = new WeakReference<Ruby>(null); + private static RuntimeInfo info1; + // store remaining runtimes here (does not include runtime1) + private static Map<Ruby, RuntimeInfo> runtimes; + + // these fields are filled by the service loaders + /** JSON */ + RubyModule jsonModule; + /** JSON::Ext::Generator::GeneratorMethods::String::Extend */ + RubyModule stringExtendModule; + /** JSON::Ext::Generator::State */ + RubyClass generatorStateClass; + /** JSON::SAFE_STATE_PROTOTYPE */ + GeneratorState safeStatePrototype; + + final RubyEncoding utf8; + final RubyEncoding ascii8bit; + // other encodings + private final Map<String, RubyEncoding> encodings; + + private RuntimeInfo(Ruby runtime) { + RubyClass encodingClass = runtime.getEncoding(); + if (encodingClass == null) { // 1.8 mode + utf8 = ascii8bit = null; + encodings = null; + } else { + ThreadContext context = runtime.getCurrentContext(); + + utf8 = (RubyEncoding)RubyEncoding.find(context, + encodingClass, runtime.newString("utf-8")); + ascii8bit = (RubyEncoding)RubyEncoding.find(context, + encodingClass, runtime.newString("ascii-8bit")); + encodings = new HashMap<String, RubyEncoding>(); + } + } + + static RuntimeInfo initRuntime(Ruby runtime) { + synchronized (RuntimeInfo.class) { + if (runtime1.get() == runtime) { + return info1; + } else if (runtime1.get() == null) { + runtime1 = new WeakReference<Ruby>(runtime); + info1 = new RuntimeInfo(runtime); + return info1; + } else { + if (runtimes == null) { + runtimes = new WeakHashMap<Ruby, RuntimeInfo>(1); + } + RuntimeInfo cache = runtimes.get(runtime); + if (cache == null) { + cache = new RuntimeInfo(runtime); + runtimes.put(runtime, cache); + } + return cache; + } + } + } + + public static RuntimeInfo forRuntime(Ruby runtime) { + synchronized (RuntimeInfo.class) { + if (runtime1.get() == runtime) return info1; + RuntimeInfo cache = null; + if (runtimes != null) cache = runtimes.get(runtime); + assert cache != null : "Runtime given has not initialized JSON::Ext"; + return cache; + } + } + + public boolean encodingsSupported() { + return utf8 != null; + } + + public RubyEncoding getEncoding(ThreadContext context, String name) { + synchronized (encodings) { + RubyEncoding encoding = encodings.get(name); + if (encoding == null) { + Ruby runtime = context.getRuntime(); + encoding = (RubyEncoding)RubyEncoding.find(context, + runtime.getEncoding(), runtime.newString(name)); + encodings.put(name, encoding); + } + return encoding; + } + } + + public GeneratorState getSafeStatePrototype(ThreadContext context) { + if (safeStatePrototype == null) { + IRubyObject value = jsonModule.getConstant("SAFE_STATE_PROTOTYPE"); + if (!(value instanceof GeneratorState)) { + throw context.getRuntime().newTypeError(value, generatorStateClass); + } + safeStatePrototype = (GeneratorState)value; + } + return safeStatePrototype; + } +} diff --git a/java/src/json/ext/StringDecoder.java b/java/src/json/ext/StringDecoder.java new file mode 100644 index 0000000..a4ee975 --- /dev/null +++ b/java/src/json/ext/StringDecoder.java @@ -0,0 +1,166 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.ThreadContext; +import org.jruby.util.ByteList; + +/** + * A decoder that reads a JSON-encoded string from the given sources and + * returns its decoded form on a new ByteList. Escaped Unicode characters + * are encoded as UTF-8. + */ +final class StringDecoder extends ByteListTranscoder { + /** + * Stores the offset of the high surrogate when reading a surrogate pair, + * or -1 when not. + */ + private int surrogatePairStart = -1; + + // Array used for writing multi-byte characters into the buffer at once + private final byte[] aux = new byte[4]; + + StringDecoder(ThreadContext context) { + super(context); + } + + ByteList decode(ByteList src, int start, int end) { + ByteList out = new ByteList(end - start); + init(src, start, end, out); + while (hasNext()) { + handleChar(readUtf8Char()); + } + quoteStop(pos); + return out; + } + + private void handleChar(int c) { + if (c == '\\') { + quoteStop(charStart); + handleEscapeSequence(); + } else { + quoteStart(); + } + } + + private void handleEscapeSequence() { + ensureMin(1); + switch (readUtf8Char()) { + case 'b': + append('\b'); + break; + case 'f': + append('\f'); + break; + case 'n': + append('\n'); + break; + case 'r': + append('\r'); + break; + case 't': + append('\t'); + break; + case 'u': + ensureMin(4); + int cp = readHex(); + if (Character.isHighSurrogate((char)cp)) { + handleLowSurrogate((char)cp); + } else if (Character.isLowSurrogate((char)cp)) { + // low surrogate with no high surrogate + throw invalidUtf8(); + } else { + writeUtf8Char(cp); + } + break; + default: // '\\', '"', '/'... + quoteStart(); + } + } + + private void handleLowSurrogate(char highSurrogate) { + surrogatePairStart = charStart; + ensureMin(1); + int lowSurrogate = readUtf8Char(); + + if (lowSurrogate == '\\') { + ensureMin(5); + if (readUtf8Char() != 'u') throw invalidUtf8(); + lowSurrogate = readHex(); + } + + if (Character.isLowSurrogate((char)lowSurrogate)) { + writeUtf8Char(Character.toCodePoint(highSurrogate, + (char)lowSurrogate)); + surrogatePairStart = -1; + } else { + throw invalidUtf8(); + } + } + + private void writeUtf8Char(int codePoint) { + if (codePoint < 0x80) { + append(codePoint); + } else if (codePoint < 0x800) { + aux[0] = (byte)(0xc0 | (codePoint >>> 6)); + aux[1] = tailByte(codePoint & 0x3f); + append(aux, 0, 2); + } else if (codePoint < 0x10000) { + aux[0] = (byte)(0xe0 | (codePoint >>> 12)); + aux[1] = tailByte(codePoint >>> 6); + aux[2] = tailByte(codePoint); + append(aux, 0, 3); + } else { + aux[0] = (byte)(0xf0 | codePoint >>> 18); + aux[1] = tailByte(codePoint >>> 12); + aux[2] = tailByte(codePoint >>> 6); + aux[3] = tailByte(codePoint); + append(aux, 0, 4); + } + } + + private byte tailByte(int value) { + return (byte)(0x80 | (value & 0x3f)); + } + + /** + * Reads a 4-digit unsigned hexadecimal number from the source. + */ + private int readHex() { + int numberStart = pos; + int result = 0; + int length = 4; + for (int i = 0; i < length; i++) { + int digit = readUtf8Char(); + int digitValue; + if (digit >= '0' && digit <= '9') { + digitValue = digit - '0'; + } else if (digit >= 'a' && digit <= 'f') { + digitValue = 10 + digit - 'a'; + } else if (digit >= 'A' && digit <= 'F') { + digitValue = 10 + digit - 'A'; + } else { + throw new NumberFormatException("Invalid base 16 number " + + src.subSequence(numberStart, numberStart + length)); + } + result = result * 16 + digitValue; + } + return result; + } + + @Override + protected RaiseException invalidUtf8() { + ByteList message = new ByteList( + ByteList.plain("partial character in source, " + + "but hit end near ")); + int start = surrogatePairStart != -1 ? surrogatePairStart : charStart; + message.append(src, start, srcEnd - start); + return Utils.newException(context, Utils.M_PARSER_ERROR, + context.getRuntime().newString(message)); + } +} diff --git a/java/src/json/ext/StringEncoder.java b/java/src/json/ext/StringEncoder.java new file mode 100644 index 0000000..57bd19b --- /dev/null +++ b/java/src/json/ext/StringEncoder.java @@ -0,0 +1,106 @@ +package json.ext; + +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.ThreadContext; +import org.jruby.util.ByteList; + +/** + * An encoder that reads from the given source and outputs its representation + * to another ByteList. The source string is fully checked for UTF-8 validity, + * and throws a GeneratorError if any problem is found. + */ +final class StringEncoder extends ByteListTranscoder { + private final boolean asciiOnly; + + // Escaped characters will reuse this array, to avoid new allocations + // or appending them byte-by-byte + private final byte[] aux = + new byte[] {/* First unicode character */ + '\\', 'u', 0, 0, 0, 0, + /* Second unicode character (for surrogate pairs) */ + '\\', 'u', 0, 0, 0, 0, + /* "\X" characters */ + '\\', 0}; + // offsets on the array above + private static final int ESCAPE_UNI1_OFFSET = 0; + private static final int ESCAPE_UNI2_OFFSET = ESCAPE_UNI1_OFFSET + 6; + private static final int ESCAPE_CHAR_OFFSET = ESCAPE_UNI2_OFFSET + 6; + /** Array used for code point decomposition in surrogates */ + private final char[] utf16 = new char[2]; + + private static final byte[] HEX = + new byte[] {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + StringEncoder(ThreadContext context, boolean asciiOnly) { + super(context); + this.asciiOnly = asciiOnly; + } + + void encode(ByteList src, ByteList out) { + init(src, out); + append('"'); + while (hasNext()) { + handleChar(readUtf8Char()); + } + quoteStop(pos); + append('"'); + } + + private void handleChar(int c) { + switch (c) { + case '"': + case '\\': + escapeChar((char)c); + break; + case '\n': + escapeChar('n'); + break; + case '\r': + escapeChar('r'); + break; + case '\t': + escapeChar('t'); + break; + case '\f': + escapeChar('f'); + break; + case '\b': + escapeChar('b'); + break; + default: + if (c >= 0x20 && c <= 0x7f || + (c >= 0x80 && !asciiOnly)) { + quoteStart(); + } else { + quoteStop(charStart); + escapeUtf8Char(c); + } + } + } + + private void escapeChar(char c) { + quoteStop(charStart); + aux[ESCAPE_CHAR_OFFSET + 1] = (byte)c; + append(aux, ESCAPE_CHAR_OFFSET, 2); + } + + private void escapeUtf8Char(int codePoint) { + int numChars = Character.toChars(codePoint, utf16, 0); + escapeCodeUnit(utf16[0], ESCAPE_UNI1_OFFSET + 2); + if (numChars > 1) escapeCodeUnit(utf16[1], ESCAPE_UNI2_OFFSET + 2); + append(aux, ESCAPE_UNI1_OFFSET, 6 * numChars); + } + + private void escapeCodeUnit(char c, int auxOffset) { + for (int i = 0; i < 4; i++) { + aux[auxOffset + i] = HEX[(c >>> (12 - 4 * i)) & 0xf]; + } + } + + @Override + protected RaiseException invalidUtf8() { + return Utils.newException(context, Utils.M_GENERATOR_ERROR, + "source sequence is illegal/malformed utf-8"); + } +} diff --git a/java/src/json/ext/Utils.java b/java/src/json/ext/Utils.java new file mode 100644 index 0000000..7a1dfee --- /dev/null +++ b/java/src/json/ext/Utils.java @@ -0,0 +1,89 @@ +/* + * This code is copyrighted work by Daniel Luz <dev at mernen dot com>. + * + * Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files + * for details. + */ +package json.ext; + +import org.jruby.Ruby; +import org.jruby.RubyArray; +import org.jruby.RubyClass; +import org.jruby.RubyException; +import org.jruby.RubyHash; +import org.jruby.RubyString; +import org.jruby.exceptions.RaiseException; +import org.jruby.runtime.Block; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; + +/** + * Library of miscellaneous utility functions + */ +final class Utils { + public static final String M_GENERATOR_ERROR = "GeneratorError"; + public static final String M_NESTING_ERROR = "NestingError"; + public static final String M_PARSER_ERROR = "ParserError"; + + private Utils() { + throw new RuntimeException(); + } + + /** + * Safe {@link RubyArray} type-checking. + * Returns the given object if it is an <code>Array</code>, + * or throws an exception if not. + * @param object The object to test + * @return The given object if it is an <code>Array</code> + * @throws RaiseException <code>TypeError</code> if the object is not + * of the expected type + */ + static RubyArray ensureArray(IRubyObject object) throws RaiseException { + if (object instanceof RubyArray) return (RubyArray)object; + Ruby runtime = object.getRuntime(); + throw runtime.newTypeError(object, runtime.getArray()); + } + + static RubyHash ensureHash(IRubyObject object) throws RaiseException { + if (object instanceof RubyHash) return (RubyHash)object; + Ruby runtime = object.getRuntime(); + throw runtime.newTypeError(object, runtime.getHash()); + } + + static RubyString ensureString(IRubyObject object) throws RaiseException { + if (object instanceof RubyString) return (RubyString)object; + Ruby runtime = object.getRuntime(); + throw runtime.newTypeError(object, runtime.getString()); + } + + static RaiseException newException(ThreadContext context, + String className, String message) { + return newException(context, className, + context.getRuntime().newString(message)); + } + + static RaiseException newException(ThreadContext context, + String className, RubyString message) { + RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime()); + RubyClass klazz = info.jsonModule.getClass(className); + RubyException excptn = + (RubyException)klazz.newInstance(context, + new IRubyObject[] {message}, Block.NULL_BLOCK); + return new RaiseException(excptn); + } + + static byte[] repeat(ByteList a, int n) { + return repeat(a.unsafeBytes(), a.begin(), a.length(), n); + } + + static byte[] repeat(byte[] a, int begin, int length, int n) { + if (length == 0) return ByteList.NULL_ARRAY; + int resultLen = length * n; + byte[] result = new byte[resultLen]; + for (int pos = 0; pos < resultLen; pos += length) { + System.arraycopy(a, begin, result, pos, length); + } + return result; + } +} |