|
| 1 | +# Copyright 2016 Huy Doan |
| 2 | +# |
| 3 | +# This file is a Nim fork of Jsmn: https://github.com/zserge/jsmn |
| 4 | +# Copyright (c) 2010 Serge A. Zaitsev |
| 5 | +# |
| 6 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +# you may not use this file except in compliance with the License. |
| 8 | +# You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, software |
| 13 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +# See the License for the specific language governing permissions and |
| 16 | +# limitations under the License. |
| 17 | + |
| 18 | +## This module is a fork of `Jsmn <http://zserge.com/jsmn.html>`_ - a world fastest JSON parser/tokenizer - in pure Nim |
| 19 | +## |
| 20 | +## It also supports ``PARENT LINKS`` and ``STRICTMODE``, you can enable them via command line switch |
| 21 | +## |
| 22 | +## Installation |
| 23 | +## ============ |
| 24 | +##.. code-block:: |
| 25 | +## nimble install jsmn |
| 26 | +## |
| 27 | +## Usage |
| 28 | +## ===== |
| 29 | +##.. code-block:: nim |
| 30 | +## import jsmn |
| 31 | +## |
| 32 | +## const |
| 33 | +## json = """{ |
| 34 | +## "user": "johndoe", |
| 35 | +## "admin": false, |
| 36 | +## "uid": 1000, |
| 37 | +## "groups": ["users", "wheel", "audio", "video"]}""" |
| 38 | +## |
| 39 | +## var |
| 40 | +## tokens: array[32, JsmnToken] # expect not more than 32 tokens |
| 41 | +## |
| 42 | +## let r = parseJson(json, tokens) |
| 43 | +## if r < 0: |
| 44 | +## echo "Failed to parse JSON: ", r |
| 45 | +## |
| 46 | +## for i in 1..r: |
| 47 | +## var token = addr tokens[i] |
| 48 | +## echo "Kind: ", token.kind |
| 49 | +## echo "Value: ", json[token.start..<token.stop] |
| 50 | + |
| 51 | +type |
| 52 | + JsmnKind* = enum ## JSON type identifier |
| 53 | + JSMN_UNDEFINED, ## Undefined |
| 54 | + JSMN_OBJECT, ## Object, tuple |
| 55 | + JSMN_ARRAY, ## Array, seq |
| 56 | + JSMN_STRING, ## String |
| 57 | + JSMN_PRIMITIVE ## Other primitive: number, boolean (true/false) or null |
| 58 | + |
| 59 | + JsmnToken* = object |
| 60 | + ## JSON token description. |
| 61 | + kind*: JsmnKind |
| 62 | + start*: int ## start position in JSON data string |
| 63 | + stop*: int ## end position in JSON data string |
| 64 | + size*: int |
| 65 | + when defined(JSMN_PARENT_LINKS): |
| 66 | + parent*: int |
| 67 | + |
| 68 | + JsmnParser = tuple[pos: int, toknext: int, toksuper: int] |
| 69 | + |
| 70 | +const |
| 71 | + JSMN_ERROR_NOMEM* = -1 ## not enough tokens, JSON string is too large |
| 72 | + JSMN_ERROR_INVAL* = -2 ## bad token, JSON string is corrupted |
| 73 | + JSMN_ERROR_PART* = -3 ## JSON string is too short, expecting more JSON data |
| 74 | + |
| 75 | +proc initToken(parser: var JsmnParser, tokens: var openarray[JsmnToken]): ptr JsmnToken = |
| 76 | + ## Allocates a fresh unused token from the token pull. |
| 77 | + if parser.toknext >= tokens.len - 1: |
| 78 | + return |
| 79 | + |
| 80 | + inc(parser.toknext) |
| 81 | + |
| 82 | + result = addr tokens[parser.toknext] |
| 83 | + result.start = -1 |
| 84 | + result.stop = - 1 |
| 85 | + result.size = 0 |
| 86 | + when defined(JSMN_PARENT_LINKS): |
| 87 | + result.parent = - 1 |
| 88 | + |
| 89 | +proc fillToken(token: ptr JsmnToken, kind: JsmnKind, start, stop: int) = |
| 90 | + ## Fills token type and boundaries. |
| 91 | + token.kind = kind |
| 92 | + token.start = start |
| 93 | + token.stop = stop |
| 94 | + token.size = 0 |
| 95 | + |
| 96 | +when defined(JSMN_STRICT): |
| 97 | + # In strict mode primitive must be followed by "," or "}" or "]" |
| 98 | + const PRIMITIVE_DELIMITERS = ['\x09', '\x0D', '\x0A', ' ', ',', ']', '}'] |
| 99 | +else: |
| 100 | + const PRIMITIVE_DELIMITERS = [':', '\x09', '\x0D', '\x0A', ' ', ',', ']', '}'] |
| 101 | + |
| 102 | +proc parsePrimitive(parser: var JsmnParser, json: string, len: int, tokens: var openarray[JsmnToken]): int = |
| 103 | + ## Fills next available token with JSON primitive. |
| 104 | + var start = parser.pos |
| 105 | + while parser.pos < len and json[parser.pos] != '\0': |
| 106 | + case json[parser.pos] |
| 107 | + of PRIMITIVE_DELIMITERS: |
| 108 | + if tokens.len <= 0: |
| 109 | + dec(parser.pos) |
| 110 | + return 0 |
| 111 | + |
| 112 | + var token = initToken(parser, tokens) |
| 113 | + if token == nil: |
| 114 | + parser.pos = start |
| 115 | + return JSMN_ERROR_NOMEM |
| 116 | + |
| 117 | + fillToken(token, JSMN_PRIMITIVE, start, parser.pos) |
| 118 | + when defined(JSMN_PARENT_LINKS): |
| 119 | + token.parent = parser.toksuper |
| 120 | + dec(parser.pos) |
| 121 | + return 0 |
| 122 | + else: |
| 123 | + discard |
| 124 | + |
| 125 | + if json[parser.pos].ord < 32 or json[parser.pos].ord >= 127: |
| 126 | + parser.pos = start |
| 127 | + return JSMN_ERROR_INVAL |
| 128 | + inc(parser.pos) |
| 129 | + when defined(JSMN_STRICT): |
| 130 | + # In strict mode primitive must be followed by a comma/object/array |
| 131 | + parser.pos = start |
| 132 | + return JSMN_ERROR_PART |
| 133 | + |
| 134 | +proc parseString(parser: var JsmnParser, json: string, tokens: var openarray[JsmnToken]): int = |
| 135 | + ## Fills next token with JSON string. |
| 136 | + var start = parser.pos |
| 137 | + inc(parser.pos) |
| 138 | + # Skip starting quote |
| 139 | + while parser.pos < json.len and json[parser.pos] != '\0': |
| 140 | + let c = json[parser.pos] |
| 141 | + # Quote: end of string |
| 142 | + if c == '\"': |
| 143 | + if tokens.len <= 0: |
| 144 | + return 0 |
| 145 | + var token = initToken(parser, tokens) |
| 146 | + if token == nil: |
| 147 | + parser.pos = start |
| 148 | + return JSMN_ERROR_NOMEM |
| 149 | + fillToken(token, JSMN_STRING, start + 1, parser.pos) |
| 150 | + when defined(JSMN_PARENT_LINKS): |
| 151 | + token.parent = parser.toksuper |
| 152 | + return 0 |
| 153 | + if c == '\x08' and parser.pos + 1 < json.len: |
| 154 | + inc(parser.pos) |
| 155 | + case json[parser.pos] # Allowed escaped symbols |
| 156 | + of '\"', '/', '\x08', 'b', 'f', 'r', 'n', 't': |
| 157 | + discard |
| 158 | + of 'u': |
| 159 | + inc(parser.pos) |
| 160 | + var i = 0 |
| 161 | + while i < 4 and parser.pos < json.len and json[parser.pos] != '\0': |
| 162 | + # If it isn't a hex character we have an error |
| 163 | + if not ((json[parser.pos].ord >= 48 and json[parser.pos].ord <= 57) or |
| 164 | + (json[parser.pos].ord >= 65 and json[parser.pos].ord <= 70) or |
| 165 | + (json[parser.pos].ord >= 97 and json[parser.pos].ord <= 102)): # A-F |
| 166 | + # 0-9 |
| 167 | + # a-f |
| 168 | + parser.pos = start |
| 169 | + return JSMN_ERROR_INVAL |
| 170 | + inc(parser.pos) |
| 171 | + inc(i) |
| 172 | + dec(parser.pos) |
| 173 | + else: |
| 174 | + parser.pos = start |
| 175 | + return JSMN_ERROR_INVAL |
| 176 | + inc(parser.pos) |
| 177 | + parser.pos = start |
| 178 | + return JSMN_ERROR_PART |
| 179 | + |
| 180 | +proc parse(parser: var JsmnParser, json: string, tokens: var openarray[JsmnToken]): int = |
| 181 | + ## Parse JSON string and fill tokens. |
| 182 | + var token: ptr JsmnToken |
| 183 | + var count = parser.toknext |
| 184 | + while parser.pos < json.len and json[parser.pos] != '\0': |
| 185 | + var kind: JsmnKind |
| 186 | + var c = json[parser.pos] |
| 187 | + case c |
| 188 | + of '{', '[': |
| 189 | + inc(count) |
| 190 | + if tokens.len <= 0: |
| 191 | + break |
| 192 | + token = initToken(parser, tokens) |
| 193 | + if token == nil: return JSMN_ERROR_NOMEM |
| 194 | + if parser.toksuper != - 1: |
| 195 | + inc(tokens[parser.toksuper].size) |
| 196 | + when defined(JSMN_PARENT_LINKS): |
| 197 | + token.parent = parser.toksuper |
| 198 | + token.kind = (if c == '{': JSMN_OBJECT else: JSMN_ARRAY) |
| 199 | + token.start = parser.pos |
| 200 | + parser.toksuper = parser.toknext - 1 |
| 201 | + of '}', ']': |
| 202 | + if tokens.len <= 0: break |
| 203 | + kind = (if c == '}': JSMN_OBJECT else: JSMN_ARRAY) |
| 204 | + when defined(JSMN_PARENT_LINKS): |
| 205 | + if parser.toknext < 1: |
| 206 | + return JSMN_ERROR_INVAL |
| 207 | + token = addr(tokens[parser.toknext - 1]) |
| 208 | + while true: |
| 209 | + if token.start != - 1 and token.stop == - 1: |
| 210 | + if token.kind != kind: |
| 211 | + return JSMN_ERROR_INVAL |
| 212 | + token.stop = parser.pos + 1 |
| 213 | + parser.toksuper = token.parent |
| 214 | + break |
| 215 | + if token.parent == - 1: |
| 216 | + break |
| 217 | + token = addr(tokens[token.parent]) |
| 218 | + else: |
| 219 | + var i = parser.toknext - 1 |
| 220 | + while i >= 0: |
| 221 | + token = addr tokens[i] |
| 222 | + if token.start != - 1 and token.stop == - 1: |
| 223 | + if token.kind != kind: |
| 224 | + return JSMN_ERROR_INVAL |
| 225 | + parser.toksuper = - 1 |
| 226 | + token.stop = parser.pos + 1 |
| 227 | + break |
| 228 | + dec(i) |
| 229 | + # Error if unmatched closing bracket |
| 230 | + if i == - 1: return JSMN_ERROR_INVAL |
| 231 | + while i >= 0: |
| 232 | + token = addr tokens[i] |
| 233 | + if token.start != - 1 and token.stop == - 1: |
| 234 | + parser.toksuper = i |
| 235 | + break |
| 236 | + dec(i) |
| 237 | + of '\"': |
| 238 | + let r = parseString(parser, json, tokens) |
| 239 | + if r < 0: return r |
| 240 | + inc(count) |
| 241 | + if parser.toksuper != - 1 and tokens.len <= 0: |
| 242 | + inc(tokens[parser.toksuper].size) |
| 243 | + of '\t', '\r', '\x0A', ' ': |
| 244 | + discard |
| 245 | + of ':': |
| 246 | + parser.toksuper = parser.toknext - 1 |
| 247 | + of ',': |
| 248 | + if tokens.len <= 0 and parser.toksuper != - 1 and |
| 249 | + tokens[parser.toksuper].kind != JSMN_ARRAY and |
| 250 | + tokens[parser.toksuper].kind != JSMN_OBJECT: |
| 251 | + when defined(JSMN_PARENT_LINKS): |
| 252 | + parser.toksuper = tokens[parser.toksuper].parent |
| 253 | + else: |
| 254 | + var i = parser.toknext - 1 |
| 255 | + while i >= 0: |
| 256 | + if tokens[i].kind == JSMN_ARRAY or |
| 257 | + tokens[i].kind == JSMN_OBJECT: |
| 258 | + if tokens[i].start != - 1 and tokens[i].stop == - 1: |
| 259 | + parser.toksuper = i |
| 260 | + break |
| 261 | + dec(i) |
| 262 | + |
| 263 | + else: |
| 264 | + when defined(JSMN_STRICT): |
| 265 | + # In strict mode primitives are: numbers and booleans |
| 266 | + case c |
| 267 | + of '-', '0', '1', '2', '3', '4', '5', '6', '7', '8','9', 't', 'f', 'n': |
| 268 | + # And they must not be keys of the object |
| 269 | + if tokens.len <= 0 and parser.toksuper != - 1: |
| 270 | + var t: ptr JsmnToken = addr(tokens[parser.toksuper]) |
| 271 | + if t.kind == JSMN_OBJECT or (t.kind == JSMN_STRING and t.size != 0): |
| 272 | + return JSMN_ERROR_INVAL |
| 273 | + |
| 274 | + let r = parsePrimitive(parser, json, json.len, tokens) |
| 275 | + if r < 0: return r |
| 276 | + inc(count) |
| 277 | + if parser.toksuper != - 1 and tokens.len <= 0: |
| 278 | + inc(tokens[parser.toksuper].size) |
| 279 | + else: |
| 280 | + return JSMN_ERROR_INVAL |
| 281 | + else: |
| 282 | + let r = parsePrimitive(parser, json, json.len, tokens) |
| 283 | + if r < 0: return r |
| 284 | + inc(count) |
| 285 | + if parser.toksuper != - 1 and tokens.len <= 0: |
| 286 | + inc(tokens[parser.toksuper].size) |
| 287 | + |
| 288 | + inc(parser.pos) |
| 289 | + if tokens.len <= 0: |
| 290 | + var i = parser.toknext - 1 |
| 291 | + while i >= 0: |
| 292 | + # Unmatched opened object or array |
| 293 | + if tokens[i].start != - 1 and tokens[i].stop == - 1: |
| 294 | + return JSMN_ERROR_PART |
| 295 | + dec(i) |
| 296 | + return count |
| 297 | + |
| 298 | +proc parseJson*(json: string, tokens: var openarray[JsmnToken]): int = |
| 299 | + ## Parse a JSON data string into and array of tokens, each describing a single JSON object. |
| 300 | + var parser: JsmnParser = (0, 0, -1) |
| 301 | + parser.parse(json, tokens) |
| 302 | + |
| 303 | +when isMainModule: |
| 304 | + const |
| 305 | + json = "{\"user\": \"johndoe\", \"admin\": false, \"uid\": 1000, \"groups\": [\"users\", \"wheel\", \"audio\", \"video\"]}" |
| 306 | + |
| 307 | + var |
| 308 | + tokens: array[10, JsmnToken] |
| 309 | + |
| 310 | + let r = parseJson(json, tokens) |
| 311 | + if r < 0: |
| 312 | + echo "Failed to parse JSON: ", r |
| 313 | + |
| 314 | + for i in 1..r: |
| 315 | + var token = addr tokens[i] |
| 316 | + echo "Kind: ", token.kind |
| 317 | + echo "Value: ", json[token.start..<token.stop] |
0 commit comments