├── .gitignore ├── COPYING ├── README.md ├── examples ├── header_example.bas ├── lib_example.bas ├── objectAssembly.bas └── test.bas ├── fbJson.bas ├── fbJson.bi └── fbJson ├── JsonBase.bas ├── JsonBase.bi ├── JsonDatatype.bi ├── JsonItem.bas ├── JsonItem.bi ├── StringFunctions.bi └── fbJsonInternals.bas /.gitignore: -------------------------------------------------------------------------------- 1 | examples/files/read_complex 2 | examples/files/read_simple 3 | examples/strings/array_flat 4 | examples/strings/array_nested 5 | examples/strings/object_flat 6 | examples/strings/object_singlechild 7 | examples/strings/object_singlechild.bas 8 | examples/generated/array_flat 9 | examples/generated/object_flat 10 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fbJson 2 | 3 | A small JSON library written in FreeBASIC. 4 | 5 | Latest stable is 1.0.1 6 | 7 | ## License 8 | 9 | fbJson is licensed under the [MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/) from version 0.14.1 onwards. 10 | 11 | ## Roadmap 12 | 13 | Past 1.0 / nice to have: 14 | 15 | * [ ] More quality of life functionality 16 | * [ ] Datatype specific properties 17 | * [ ] Write properly escaped json on toString() call. 18 | * [ ] Make toString() fast(er) 19 | 20 | ## fbJson specifics 21 | 22 | Unfortunately, the JSON spec (RFC 8259) leaves some aspects up for interpretation. You have to watch for the following: 23 | 24 | * **UTF-8 forgiveness**: The RFC says that all valid JSON must be UTF-8. fbJSON will reject all input that's not UTF-8 or contains 25 | bytesquences that are not valid in UTF. Further, fbJSON will reject certain inputs that are valid in principle, but not (currently) valid 26 | unicode. This includes escaped values in the \uXXXX notation. 27 | * **Nesting**: The RFC says: "An implementation may set limits on the maximum depth ofnesting". fbJSON doesn't. It will happily 28 | parse nested elements until it runs out of memory. **Beware.** 29 | * **String length**: Same situation. In theory, the upper limit is whatever FreeBasics limit is, minus 2 byte. 30 | * **Duplicate keys**: Parse() will use the newer value for any key. Meaning ```{"a": 1, "a": 2}``` is treated like ```{"a": 2}```. 31 | 32 | Note: jsonItem.AddItem() will not override existing keys, since I think the programmer should have more control when 33 | manipulating the JSON this way. 34 | 35 | ## Usage 36 | 37 | For hassle free use as-is, I suggest simply throwing the entire fbJson/ folder and the fbJson.bi file into your 38 | repository to include the code at compile time. 39 | 40 | If you don't want to litter your repository with my source code, you can also compile fbJson.bas with the "-lib" or "-dll" 41 | flag. Then you only need the fbJsonBase.bi and fbJsonItem.bi along with the DLL. 42 | 43 | 44 | ## Parsing stuff 45 | 46 | You can either give the JSON input via the constructor: 47 | 48 | ``` 49 | dim item as jsonItem = JsonItem("{}") 50 | ``` 51 | 52 | Or you can create the instance first and then use .Parse(). 53 | 54 | ``` 55 | dim item as jsonItem 56 | ' some code ... 57 | item.Parse("{}") 58 | ``` 59 | 60 | You could also overwrite instances with the constructor like below, but that comes with some overhead. 61 | 62 | ``` 63 | dim item as jsonItem 64 | ' some code ... 65 | item = JsonItem("{}") 66 | ``` 67 | 68 | ## Accessing elements 69 | 70 | You can access any child-element via the square brackets, either by using the index of the element, 71 | or in case of json-Objects, using the key. 72 | 73 | `jsonItem[string]` 74 | Returns the child element with the corresponding key. 75 | 76 | Keys are case senstive. 77 | 78 | `jsonItem[integer]` 79 | Returns the nTh child of the item. 80 | 81 | Keep in mind that the index starts at 0. 82 | 83 | ## Other properties and methods 84 | 85 | #### JsonItem 86 | 87 | `jsonItem.Count` 88 | Gets the total number of children. 89 | 90 | `jsonItem.DataType` 91 | Gets the datatype of the item. 92 | 93 | `jsonItem.Key` 94 | Gets or sets the key of the item. Setting the key will _silently_ fail when the new key is already in use. 95 | 96 | `jsonItem.Value as string` 97 | Gets or sets the value of the item (as string). Also sets the datatype. Returns an emptry string on objects and arrays. 98 | 99 | `jsonItem.AddItem(string, jsonItem) as boolean` 100 | Adds an item with a key (only on jsonObjects). 101 | If the item is of type null, it's converted into an object. 102 | 103 | `jsonItem.AddItem(string, string) as boolean` 104 | Adds a value with a key (only on jsonObjects). 105 | If the item is of type null, it's converted into an object. 106 | 107 | `jsonItem.AddItem(string) as boolean` 108 | Adds an item (only on jsonArrays). Returns true if successful. 109 | If the item is of type null, it's converted into an array. 110 | 111 | `jsonItem.AddItem(string) as boolean` 112 | Adds a value (only on jsonArrays). Returns true if successful. 113 | If the item is of type null, it's converted into an array. 114 | 115 | 116 | `jsonItem.RemoveItem(string) as boolean` 117 | Removes the child with the given key. Returns true if successful. 118 | 119 | `jsonItem.RemoveItem(integer) as boolean` 120 | Removes the Nth child. Returns true if successful. 121 | 122 | `jsonItem.ContainsKey(string) as boolean` 123 | Returns true if the item is an object that contains the given key. 124 | 125 | `jsonItem.ToString() as string` 126 | Creates a string representation of the Item and all it's children. 127 | 128 | ## Performance and compiler options 129 | 130 | I unscientifically tested how fast fbJson parses through this monstrosity of a JSON file: 131 | 132 | https://github.com/zemirco/sf-city-lots-json/blob/master/citylots.json (181 mb) 133 | 134 | On my desktop machine (AMD Ryzen 7 1700X), I get the following results: 135 | 136 | | Compiler options | parsing time (s) | 137 | | ------------- | ------------- | 138 | | fbc "%f" | 6.33 | 139 | | fbc "%f" -gen GCC -Wc -O1 | ~4.01 | 140 | 141 | Test-setup is just loading the file with open and get# and this loop: 142 | 143 | ``` 144 | dim as double start = timer 145 | for i as uinteger = 1 to 30 146 | dim item as jsonItem = jsonItem(jsonFile) 147 | next 148 | print (timer - start) / 30 149 | ``` 150 | 151 | Setup: 152 | 153 | * AMD Ryzen 7 1700X 154 | * FreeBASIC Compiler - Version 1.06.0 (11-18-2017) (64bit) 155 | * glibc 2.28 156 | * Linux 4.20.4 (64bit) 157 | * fbJson commit 6ef899d296 158 | 159 | ## Design 160 | 161 | The basic principle behind fbJson is to handle all parsing of JSON in one go. There is no AST, no tokenization, etc. 162 | JsonBase.Parse() will do all the heavy lifting while iterating over the input byte for byte. Ideally, fbJson would not 163 | do a single string comparison and only ever look at each byte of the input once. Didn't quite get there ;-) 164 | 165 | fbJson is not the fastest parser in the world, but I tried my best to optimize it. As a result, a lot of the code 166 | is rather ugly and convuluted. isValidDouble() is the most prominent example. Keep that in mind when exploring the codebase. 167 | -------------------------------------------------------------------------------- /examples/header_example.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #define fbJSON_debug 8 | 9 | ' This example includes the whole source of fbJson into the programm, 10 | ' instead of just using the headers and a dll / shared object. 11 | 12 | #include once "../fbJson.bas" 13 | 14 | dim as jsonItem foo = jsonItem("[1,2,3,4,5,6,7,8,9,10]") 15 | 16 | print foo.toString() 17 | -------------------------------------------------------------------------------- /examples/lib_example.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #define fbJSON_debug 8 | 9 | ' This example imports just the header files into the test program and loads the actual code of the library as a dll / shared object. 10 | 11 | ' For this example to work, you need to compile the file "fbJson.bas" in the root directory with the "-lib" parameter: 12 | ' # fbc -lib fbJson.bas 13 | 14 | ' Libpath tells the linker to search the libfbJson.a in the root dir of the repository 15 | #libpath "../" 16 | #include once "../fbJson.bi" 17 | 18 | dim as jsonItem foo = jsonItem("[1,2,3,4,5,6,7,8,9,10]") 19 | 20 | print foo.toString() 21 | -------------------------------------------------------------------------------- /examples/objectAssembly.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #define fbJSON_debug 8 | 9 | #include once "../fbJson.bas" 10 | 11 | dim as jsonItem root = jsonItem() 12 | 13 | root.addItem("Name", "fbJson") 14 | root.addItem("Version", "0.17.2") 15 | root.addItem("Double", "11.1e10") 16 | root.addItem("String", """11.1e10""") 17 | root.addItem("Purpose", "To chew bubblegum and parse JSON.") 18 | root.addItem("Status", "All out of bubblegum.") 19 | root.addItem("array", jsonItem()) 20 | 21 | for i as integer = 1 to 10 22 | root["array"].addItem(str(i)) 23 | next 24 | 25 | print root.toString() 26 | -------------------------------------------------------------------------------- /examples/test.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | sub AssertEqual overload (expected as integer, result as integer) 8 | if ( expected <> result ) then 9 | print "Assert failed: Expected "& expected &" but got: "& result 10 | end -1 11 | end if 12 | end sub 13 | 14 | sub AssertEqual(expected as string, result as string) 15 | if ( expected <> result ) then 16 | print "Assert failed: Expected "& expected &" but got: "& result 17 | end -1 18 | end if 19 | end sub 20 | 21 | 22 | #define fbJson_Debug 23 | #include once "../fbJson.bas" 24 | 25 | dim as jsonItem item 26 | 27 | Print "#0 - Test special keys" 28 | 29 | item = jsonItem(chr(255) + "{"""": true}") 30 | assertEqual(malformed, item.Datatype) 31 | 32 | item = jsonItem("{""\u2665"":""Foo""}") 33 | assertequal("♥", item[0].key) 34 | item = jsonItem("{""\uD83E\uDDC0"":""Foo""}") 35 | assertequal("🧀", item[0].key) 36 | 37 | item = jsonItem("{"""":""Foo""}") 38 | assertequal("", item[0].key) 39 | 40 | item = jsonItem("{""\\c"":""Foo""}") 41 | assertequal("\c", item[0].key) 42 | 43 | 44 | print "[OK]" 45 | 46 | print "Test escaping." 47 | 48 | item = jsonItem("{""\\c\\"":""\\foo\\bar\/baz\u2665""}") 49 | assertequal("\c\", item[0].key) 50 | assertequal("\foo\bar/baz♥", item[0].value) 51 | 52 | item = jsonItem("""\""""") 53 | assertequal("""", item.value) 54 | 55 | item = jsonItem("""\\\\\""""") 56 | assertequal("\\""", item.value) 57 | 58 | item = jsonItem("""\\""") 59 | assertequal("\", item.value) 60 | 61 | item = jsonItem("""\\\\\\""") 62 | assertequal("\\\", item.value) 63 | item = jsonItem("""\\\\\u2665\\""") 64 | assertequal("\\♥\", item.value) 65 | 66 | print "[OK]" 67 | 68 | print "Test empty object" 69 | item = jsonItem("{}") 70 | AssertEqual(jsonObject, item.Datatype) 71 | print "[OK]" 72 | print 73 | 74 | print "Test empty array" 75 | item = jsonItem("[]") 76 | 77 | AssertEqual(jsonArray, item.Datatype) 78 | print "[OK]" 79 | print 80 | 81 | print "Test simple key value object" 82 | item = jsonItem("{""key"": ""value""}") 83 | 84 | AssertEqual(jsonObject, item.Datatype) 85 | AssertEqual("key", item[0].key) 86 | AssertEqual("value", item["key"].value) 87 | print "[OK]" 88 | print 89 | 90 | print "Test simple array, all datatypes" 91 | item = jsonItem("[ 1 , true , false , null , ""string"", [], {} ]") 92 | 93 | AssertEqual(jsonArray, item.Datatype) 94 | AssertEqual(7, item.Count) 95 | AssertEqual(jsonNumber, item[0].Datatype) 96 | AssertEqual(jsonBool, item[1].Datatype) 97 | AssertEqual(jsonBool, item[2].Datatype) 98 | AssertEqual(jsonNull, item[3].Datatype) 99 | AssertEqual(jsonString, item[4].Datatype) 100 | AssertEqual(jsonArray, item[5].Datatype) 101 | AssertEqual(jsonObject, item[6].Datatype) 102 | 103 | print "[OK]" 104 | print 105 | 106 | print "Test simple object, all datatypes" 107 | 108 | item = jsonItem("{ ""number"":1 , ""bool1"": true , ""bool2"": false , ""null"": null , ""string"": ""string"", ""object"": {}, ""array"": [] }") 109 | 110 | AssertEqual(jsonObject, item.Datatype) 111 | AssertEqual(7, item.Count) 112 | AssertEqual(jsonNumber, item["number"].Datatype) 113 | AssertEqual(jsonBool, item["bool1"].Datatype) 114 | AssertEqual(jsonBool, item["bool2"].Datatype) 115 | AssertEqual(jsonNull, item["null"].Datatype) 116 | AssertEqual(jsonString, item["string"].Datatype) 117 | AssertEqual(jsonObject, item["object"].Datatype) 118 | AssertEqual(jsonArray, item["array"].Datatype) 119 | 120 | print "[OK]" 121 | print 122 | 123 | print "Test nested object with arrays" 124 | 125 | item = jsonItem("{ ""object"": { ""array"": [1,2,3], ""array2"": [3,4,5], ""array3"": [6,7,8]}, ""string"": ""string""}") 126 | 127 | AssertEqual(jsonObject, item.Datatype) 128 | AssertEqual(2, item.Count) 129 | AssertEqual(jsonObject, item["object"].Datatype) 130 | AssertEqual(3, item["object"].Count) 131 | AssertEqual(jsonString, item["string"].Datatype) 132 | AssertEqual("string", item["string"].value) 133 | 134 | print "[OK]" 135 | print 136 | 137 | 138 | 139 | print "Test nested object with objects" 140 | 141 | item = jsonItem("{ ""object"": { ""nested1"": { ""string"": ""string""}, ""nested2"": { ""string"": ""string""}, ""nested3"": { ""string"": ""string""}} }") 142 | 143 | AssertEqual(jsonObject, item.Datatype) 144 | AssertEqual(1, item.Count) 145 | AssertEqual(jsonObject, item["object"].Datatype) 146 | AssertEqual(3, item["object"].Count) 147 | AssertEqual(jsonObject, item["object"]["nested1"].Datatype) 148 | AssertEqual(jsonObject, item["object"]["nested2"].Datatype) 149 | AssertEqual(jsonObject, item["object"]["nested3"].Datatype) 150 | 151 | 152 | print "[OK]" 153 | print 154 | 155 | print "Test malformed object: {""foo"":bar}" 156 | item = jsonItem("{""foo"":bar}") 157 | AssertEqual(malformed, item.DataType) 158 | print "[OK]" 159 | print 160 | 161 | print "Test nested array" 162 | item = jsonItem("[[1,2,3,4],[4,3,2,1],[5,6,7,8]]") 163 | 164 | AssertEqual(jsonArray, item.Datatype) 165 | AssertEqual(3, item.Count) 166 | AssertEqual(4, item[0].Count) 167 | AssertEqual(4, item[1].Count) 168 | AssertEqual(4, item[2].Count) 169 | AssertEqual("8", item[2][3].value) 170 | 171 | print "[OK]" 172 | print 173 | 174 | print "Test nested structures" 175 | 176 | item = jsonItem("{""foo"": { }, ""bar"": [ ],""objects"": [{},{},{}],""arrays"":[[],[],[]]}") 177 | 178 | assertEqual(jsonObject, item.Datatype) 179 | assertEqual(4, item.Count) 180 | assertEqual(jsonObject, item["foo"].datatype) 181 | assertEqual(jsonArray, item["bar"].datatype) 182 | assertEqual(jsonArray, item["objects"].datatype) 183 | assertEqual(3, item["objects"].Count) 184 | assertEqual(3, item["arrays"].Count) 185 | assertEqual(jsonObject, item["objects"][0].datatype) 186 | assertEqual(jsonObject, item["objects"][1].datatype) 187 | assertEqual(jsonObject, item["objects"][2].datatype) 188 | 189 | assertEqual(jsonArray, item["arrays"][0].datatype) 190 | assertEqual(jsonArray, item["arrays"][1].datatype) 191 | assertEqual(jsonArray, item["arrays"][2].datatype) 192 | 193 | print "[OK]" 194 | print 195 | 196 | print "Test malformed object: {""foo"":}" 197 | 198 | 199 | item = jsonItem("{""foo"": }") 200 | AssertEqual(malformed, item.DataType) 201 | print "[OK]" 202 | print 203 | 204 | print "#2 - test malformed string: {""key"":""}" 205 | 206 | item = jsonItem("{""key"":""}") 207 | assertEqual(malformed, item.Datatype) 208 | print "[OK]" 209 | 210 | print "#2 - test malformed string 2: {""key"":""one value\n}" 211 | 212 | item = jsonItem("{""key"":""value\n}") 213 | assertEqual(malformed, item.Datatype) 214 | print "[OK]" 215 | 216 | 217 | print "#1 - Testing positive signed number : {""key"": +4.44}" 218 | 219 | item = jsonItem("{""key"":+4.44}") 220 | assertEqual(malformed, item.Datatype) 221 | 222 | print "[OK]" 223 | 224 | print "#1 - Testing negative signed number : {""key"":-4.44}" 225 | 226 | item = jsonItem("{""key"":-4.44}") 227 | assertEqual(jsonObject, item.Datatype) 228 | assertEqual(jsonNumber, item["key"].Datatype) 229 | assertEqual("-4.44", item["key"].Value) 230 | 231 | print "[OK]" 232 | 233 | print "#5 - Testing flat value - string : ""value""" 234 | 235 | item = jsonItem("""value""") 236 | assertEqual(jsonString, item.Datatype) 237 | assertEqual("value", item.Value) 238 | 239 | print "#5 - Testing strings - \uXXXX and surrogate pairs" 240 | item = jsonItem("""\u2665123456""") 241 | assertEqual("♥123456", item.value) 242 | item = jsonItem("""\uD83E\uDDC0123456""") 243 | assertEqual("🧀123456", item.value) 244 | 245 | print "[OK]" 246 | 247 | print "#5 - Testing flat value - boolean" 248 | 249 | item = jsonItem("true") 250 | assertEqual(jsonBool, item.Datatype) 251 | assertEqual("true", item.Value) 252 | 253 | item = jsonItem("false") 254 | assertEqual(jsonBool, item.Datatype) 255 | assertEqual("false", item.Value) 256 | 257 | print "[OK]" 258 | 259 | print "#5 - Testing flat value - null" 260 | 261 | item = jsonItem("null") 262 | assertEqual(jsonNull, item.Datatype) 263 | assertEqual("null", item.Value) 264 | 265 | print "[OK]" 266 | 267 | 268 | print "#5 - Testing flat value - numbers" 269 | 270 | item = jsonItem("1000000") 271 | assertEqual(jsonNumber, item.Datatype) 272 | assertEqual("1000000", item.Value) 273 | 274 | item = jsonItem("10.000001") 275 | assertEqual(jsonNumber, item.Datatype) 276 | assertEqual("10.000001", item.Value) 277 | 278 | item = jsonItem("12.3456789") 279 | assertEqual(jsonNumber, item.Datatype) 280 | assertEqual("12.3456789", item.Value) 281 | 282 | item = jsonItem("-12.3456789") 283 | assertEqual(jsonNumber, item.Datatype) 284 | assertEqual("-12.3456789", item.Value) 285 | 286 | item = jsonItem("[12.00000000]") 287 | assertEqual("12.00000000", item[0].value) 288 | assertEqual(jsonNumber, item[0].Datatype) 289 | 290 | item = jsonItem("0.01") 291 | assertEqual(jsonNumber, item.Datatype) 292 | assertEqual("0.01", item.value) 293 | 294 | item = jsonItem("-0.01") 295 | assertEqual(jsonNumber, item.Datatype) 296 | assertEqual("-0.01", item.value) 297 | 298 | 299 | item = jsonItem("[+12.3456789]") 300 | assertEqual(malformed, item.Datatype) 301 | 302 | item = jsonItem("{""foo"": NaN}") 303 | assertEqual(malformed, item.Datatype) 304 | 305 | item = jsonItem("[-NaN]") 306 | assertEqual(malformed, item.Datatype) 307 | '/ 308 | 309 | item = jsonItem("{""foo"": [Infinity]}") 310 | assertEqual(malformed, item.Datatype) 311 | assertEqual(malformed, item[0].Datatype) 312 | 313 | item = jsonItem("4.44") 314 | assertEqual(jsonNumber, item.Datatype) 315 | assertEqual("4.44", item.value) 316 | 317 | item = jsonItem("4e1") 318 | assertEqual(jsonNumber, item.Datatype) 319 | assertEqual("40", item.value) 320 | 321 | item = jsonItem("4e-1") 322 | assertEqual(jsonNumber, item.Datatype) 323 | assertEqual("0.4", item.value) 324 | 325 | item = jsonItem("-4e-1") 326 | assertEqual(jsonNumber, item.Datatype) 327 | assertEqual("-0.4", item.value) 328 | 329 | item = jsonItem("-4e-1.2") 330 | assertEqual(malformed, item.Datatype) 331 | 332 | item = jsonItem("-4e+1") 333 | assertEqual(jsonNumber, item.Datatype) 334 | assertEqual("-40", item.value) 335 | 336 | item = jsonItem("{""foo"": [Infinity]}") 337 | assertEqual(malformed, item.Datatype) 338 | assertEqual(malformed, item[0].Datatype) 339 | 340 | item = jsonItem("[-Infinity]") 341 | assertEqual(malformed, item.Datatype) 342 | 343 | item = jsonItem("[0E+]") 344 | assertEqual(malformed, item.Datatype) 345 | 346 | item = jsonItem("[.2e-3]") 347 | assertEqual(malformed, item.Datatype) 348 | 349 | print "[OK]" 350 | 351 | print "#6 - Array structure" 352 | 353 | item = jsonItem("[,1]") 354 | assertEqual(malformed, item.Datatype) 355 | 356 | item = jsonItem("["": 1]]") 357 | assertEqual(malformed, item.Datatype) 358 | 359 | item = jsonItem("[1,1,1") 360 | assertEqual(malformed, item.Datatype) 361 | 362 | print "#7 - Objects" 363 | 364 | item = jsonItem("{"""":""a""}") 365 | assertEqual(jsonObject, item.Datatype) 366 | assertEqual("a", item[0].value) 367 | assertEqual("a", item[""].value) 368 | print "[OK]" 369 | 370 | print "#8 - Strings" 371 | 372 | item = jsonItem("""\""\\/\b\f\n\r\t""") 373 | assertEqual(jsonString, item.Datatype) 374 | 375 | item = jsonItem("[""\") 376 | assertEqual(malformed, item.Datatype) 377 | 378 | item = jsonItem("") 379 | assertEqual(malformed, item.Datatype) 380 | 381 | print "[OK]" 382 | 383 | dim as string invalidUTF8 (0 to 6) = { _ 384 | !"\195\28", _ 385 | !"\160\161", _ 386 | !"\226\28\161", _ 387 | !"\226\82\28", _ 388 | !"\240\28\140\188", _ 389 | !"\240\90\28\188", _ 390 | !"\240\28\140\28" _ 391 | } 392 | 393 | 394 | for i as integer = 0 to 6 395 | print "Testing invalid strings #"& i 396 | item = jsonItem(""""& invalidUTF8(i) & """") 397 | assertEqual(malformed, item.Datatype) 398 | next 399 | 400 | dim as string validUTF8 (0 to 4) = { _ 401 | !"\195\177", _ 402 | !"\226\130\161", _ 403 | !"\240\144\140\188"_ 404 | } 405 | 406 | for i as integer = 0 to 4 407 | print "Testing valid string #"& i 408 | item = jsonItem(""""& validUTF8(i) & """") 409 | assertEqual(jsonString, item.Datatype) 410 | next 411 | 412 | item = jsonItem("[ ""1"", ""2"", ""3"", ""4"",""5""]") 413 | assertEqual(item.count, 5) 414 | 415 | 416 | 417 | -------------------------------------------------------------------------------- /fbJson.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #define fbJson_HeaderLib 8 | 9 | #include once "fbJson/JsonItem.bas" 10 | -------------------------------------------------------------------------------- /fbJson.bi: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #include once "fbJson/JsonItem.bi" 8 | -------------------------------------------------------------------------------- /fbJson/JsonBase.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #include once "JsonBase.bi" 8 | #include once "StringFunctions.bi" 9 | 10 | sub JsonBase.setErrorMessage(errorCode as fbJsonInternal.jsonError, jsonstring as byte ptr, position as uinteger) 11 | using fbJsonInternal 12 | dim as integer lineNumber = 1 13 | dim as integer linePosition = 0 14 | dim as integer lastBreak = 0 15 | dim as string lastLine 16 | 17 | for j as integer = 0 to position 18 | if ( jsonString[j] = 10 ) then 19 | lineNumber +=1 20 | linePosition = 0 21 | lastBreak = j 22 | end if 23 | linePosition +=1 24 | next 25 | 26 | select case as const errorCode 27 | case arrayNotClosed: 28 | this._error = "Array was not properly closed. Expected ']' at position "& linePosition &" on line "& lineNumber &", found '"& chr(jsonstring[position]) &"' instead." 29 | case objectNotClosed: 30 | this._error = "Object was not properly closed. Expected '}' at position "& linePosition &" on line "& lineNumber &", found '"& chr(jsonstring[position]) &"' instead." 31 | case stringNotClosed: 32 | this._error = "String value or key was not closed. Expected '""', found "& chr(jsonstring[position]) &" at position "& linePosition &" on line "& lineNumber &"." 33 | case invalidValue: 34 | this._error = "Invalid value '"& this._value &"' encountered at position "& linePosition &" on line "& lineNumber &"." 35 | case invalidEscapeSequence: 36 | this._error = "Could not de-escape '"& this._value &"' encountered at position "& linePosition &" on line "& lineNumber &"." 37 | case invalidNumber: 38 | this._error = "Invalid number '"& this._value &"' encountered at position "& linePosition &" on line "& lineNumber &"." 39 | case expectedKey: 40 | this._error = "Expected a key at position "& linePosition &" on line "& lineNumber &", found '"& chr(jsonstring[position]) &"' instead." 41 | case expectedValue: 42 | this._error = "Expected a value at position "& linePosition &" on line "& lineNumber &", found '"& chr(jsonstring[position]) &"' instead." 43 | case unexpectedToken: 44 | this._error = "Unexpected token '"& chr(jsonstring[position]) &"' at "& linePosition &" on line "& lineNumber &"." 45 | case invalidCodepoint 46 | this._error = "Invalid UTF-8 bytesequence encountered: "& hex(jsonstring[position]) &"' at "& linePosition &" on line "& lineNumber &"." 47 | end select 48 | 49 | this._value = "" 50 | this.SetMalformed() 51 | #ifdef fbJSON_debug 52 | print "fbJSON Error: "& this._error 53 | 'end -1 54 | #endif 55 | end sub 56 | 57 | constructor JsonBase() 58 | ' Nothing to do 59 | end constructor 60 | 61 | constructor JsonBase(byref jsonString as string) 62 | fbJsonInternal.FastTrimWhitespace(jsonstring) 63 | this.Parse(strptr(jsonString), len(jsonstring)-1) 64 | end constructor 65 | 66 | destructor JsonBase() 67 | if (this._count >= 0 and this._children <> 0) then 68 | for i as integer = 0 to this._count 69 | delete this._children[i] 70 | next 71 | this._count = -1 72 | deallocate(this._children) 73 | end if 74 | end destructor 75 | 76 | operator JsonBase.LET(copy as JsonBase) 77 | this.destructor() 78 | fbJsonInternal.FastCopy(this._key, copy._key) 79 | fbJsonInternal.FastCopy(this._value, copy._value) 80 | 81 | this._dataType = copy._dataType 82 | this._error = copy._error 83 | this._count = copy._count 84 | 85 | if ( copy._count >= 0) then 86 | this._children = allocate(sizeOf(JsonBase ptr) * (copy._count+1)) 87 | for i as integer = 0 to copy._count 88 | this._children[i] = callocate(sizeOf(JsonBase)) 89 | *this._children[i] = *copy._children[i] 90 | next 91 | end if 92 | end operator 93 | 94 | property JsonBase.Parent() byref as JsonBase 95 | if ( this._parent <> 0 ) then 96 | return *this._parent 97 | end if 98 | 99 | return *new JsonBase() 100 | end property 101 | 102 | property JsonBase.Count() as integer 103 | return this._count + 1 104 | end property 105 | 106 | property JsonBase.DataType() as jsonDataType 107 | return this._datatype 108 | end property 109 | 110 | function JsonBase.AppendChild(newChild as JsonBase ptr, override as boolean = false) as boolean 111 | if ( newChild = 0 ) then return false 112 | if ( this._datatype = jsonObject ) then 113 | for i as integer = 0 to this._count 114 | if ( fbJsonInternal.AreEqual(this._children[i]->_key, newChild->_key) ) then 115 | if (override) then 116 | delete this._children[i] 117 | newChild->_parent = @this 118 | this._children[i] = newChild 119 | return true 120 | else 121 | return false 122 | end if 123 | end if 124 | next 125 | end if 126 | 127 | newChild->_parent = @this 128 | this._count += 1 129 | 130 | ' I think allocating 2 elements at a time is a decent compromise between memory and speed. 131 | ' And it does cut the number of reallocations in half. 132 | if (this._count mod 2 = 0) then 133 | if this._children = 0 then 134 | this._children = allocate(sizeof(JsonBase ptr) * (this._count+2)) 135 | else 136 | this._children = reallocate(this._children, sizeof(JsonBase ptr) * (this._count+2)) 137 | end if 138 | 139 | if this._children = 0 then 140 | this.setMalformed() 141 | return false 142 | end if 143 | end if 144 | this._children[this._count] = newChild 145 | 146 | if ( newChild->_datatype = malformed ) then 147 | this.SetMalformed() 148 | end if 149 | return true 150 | end function 151 | 152 | function JsonBase.ContainsKey(byref newKey as string) as boolean 153 | if ( this._datatype <> jsonObject ) then return false 154 | 155 | for i as integer = 0 to this._count 156 | if ( fbJsonInternal.areEqual(this._children[i]->_key, newKey ) ) then 157 | return true 158 | end if 159 | next 160 | return false 161 | end function 162 | 163 | sub JsonBase.SetMalformed() 164 | this._datatype = malformed 165 | if (this._parent <> 0) then 166 | dim item as JsonBase ptr = this._parent 167 | item->_error = this._error 168 | do 169 | if (item->_parent <> 0) then 170 | item->_parent->_error = item->_error 171 | end if 172 | item->_datatype = malformed 173 | item = item->_parent 174 | 175 | loop until item = 0 176 | end if 177 | end sub 178 | 179 | sub JsonBase.Parse(jsonString as ubyte ptr, endIndex as integer) 180 | using fbJsonInternal 181 | if (endIndex < 0) then 182 | this.setMalformed() 183 | this._error = "No data!" 184 | return 185 | end if 186 | 187 | dim currentType as jsonDataType = -2 188 | 189 | ' Objects we will work with: 190 | dim currentItem as JsonBase ptr = @this 191 | dim as JsonBase ptr child 192 | 193 | ' key states and variables for the main parsing: 194 | dim as uinteger parseStart = 1, parseEnd = endIndex -1 195 | dim as integer valueStart 196 | dim as parserState state 197 | dim as boolean isStringOpen 198 | dim as boolean stringIsEscaped = false 199 | dim as byte unicodeSequence 200 | dim as boolean isEscaped = false 201 | 202 | ' To handle trimming, we use these: 203 | dim as integer valueEnd 204 | dim as boolean trimLeftActive = false 205 | 206 | if ( jsonstring[0] = jsonToken.CurlyOpen ) then 207 | if ( jsonString[endIndex] = jsonToken.CurlyClose ) then 208 | currentItem->_datatype = jsonObject 209 | state = parserState.none 210 | else 211 | currentItem->setErrorMessage(objectNotClosed, jsonstring, endIndex) 212 | return 213 | end if 214 | elseif ( jsonstring[0] = jsonToken.SquareOpen ) then 215 | if (jsonString[endIndex] = jsonToken.SquareClose ) then 216 | currentItem->_dataType = jsonArray 217 | valueStart = 1 218 | trimLeftActive = true 219 | state = valueToken 220 | currentType = -2 221 | else 222 | currentItem->setErrorMessage(arrayNotClosed, jsonstring, endIndex) 223 | return 224 | end if 225 | else 226 | parseStart = 0 227 | parseEnd = endIndex 228 | state = valueToken 229 | end if 230 | 231 | ' Skipping the opening and closing brackets makes things a bit easier. 232 | for i as integer = parseStart to parseEnd 233 | select case as const jsonstring[i] 234 | ' These codepoints are straight up invalid no matter what: 235 | case 192, 193, 245 to 255: 236 | currentItem->setErrorMessage(invalidCodepoint, jsonstring, i) 237 | goto cleanup 238 | case 237 239 | ' TODO Validate against surrogate pairs, which are invalid in UTF-8. 240 | currentItem->setErrorMessage(invalidCodepoint, jsonstring, i) 241 | goto cleanup 242 | 243 | case jsonToken.Quote 244 | if (isEscaped = false) then 245 | isStringOpen = not(isStringOpen) 246 | if ( currentItem->_datatype = jsonObject ) then 247 | if state = none then 248 | state = keyToken 249 | valueStart = i+1 250 | elseif state = keyToken then 251 | child = new JsonBase() 252 | fastmid(child->_key, jsonString, valuestart, i - valueStart) 253 | if ( isInString(child->_key, jsonToken.backslash) <> 0 ) then 254 | if ( DeEscapeString(child->_key) = false ) then 255 | child->setErrorMessage(invalidEscapeSequence, jsonstring, i) 256 | end if 257 | end if 258 | state = keyTokenClosed 259 | end if 260 | end if 261 | end if 262 | goto utf8Validation ' Fall-through to the else-case: 263 | case else 264 | utf8Validation: 265 | ' UTF-8 length validation: 266 | if ( jsonstring[i] > 127 andAlso jsonstring[i] SHR 6 = &b10 ) then 267 | unicodeSequence -= 1 268 | if (unicodeSequence < 0 ) then 269 | currentItem->setErrorMessage(invalidCodepoint, jsonstring, i) 270 | goto cleanup 271 | end if 272 | else 273 | if (unicodeSequence > 0) then 274 | currentItem->setErrorMessage(invalidCodepoint, jsonstring, i) 275 | goto cleanup 276 | end if 277 | select case as const jsonString[i] ' shr 4 278 | case 192 to 223 279 | unicodeSequence = 1 280 | case 224 to 239 281 | unicodeSequence = 2 282 | case 240 to 247 283 | unicodeSequence = 3 284 | case else 285 | unicodeSequence = 0 286 | end select 287 | end if 288 | end select 289 | ' When not in a string, we can handle the complicated suff: 290 | if ( isStringOpen = false ) then 291 | ' Note: Not a single string-comparison in here. 292 | select case as const jsonstring[i] 293 | case jsonToken.BackSlash 294 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 295 | goto cleanup 296 | 297 | case jsonToken.Colon: 298 | if ( state = keyTokenClosed ) then 299 | state = valueToken 300 | currentType = -2 301 | trimLeftActive = true 302 | valueStart = i+1 303 | else 304 | currentItem->setErrorMessage(expectedKey, jsonstring, i) 305 | goto cleanup 306 | end if 307 | 308 | case jsonToken.Comma: 309 | if (i = parseEnd) then 310 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 311 | goto cleanup 312 | end if 313 | 314 | if ( state = valueToken ) then 315 | state = valueTokenClosed 316 | if valueEnd = 0 then valueEnd = i 317 | elseif ( state = nestEnd ) then 318 | state = resetState 319 | else 320 | currentItem->setErrorMessage(expectedKey, jsonstring, i) 321 | goto cleanup 322 | end if 323 | 324 | case jsonToken.CurlyOpen: 325 | if ( state = valueToken ) then 326 | if (child = 0) then child = new JsonBase() 327 | child->_datatype = jsonobject 328 | currentItem->AppendChild( child , true) 329 | currentItem = child 330 | state = resetState 331 | else 332 | currentItem->setErrorMessage(expectedKey, jsonstring, i) 333 | goto cleanup 334 | end if 335 | 336 | case jsonToken.SquareOpen: 337 | if ( state = valueToken andAlso valueStart = i ) then 338 | if (child = 0) then child = new JsonBase() 339 | child->_datatype = jsonArray 340 | currentItem->AppendChild( child , true) 341 | currentItem = child 342 | state = resetState 343 | else 344 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 345 | goto cleanup 346 | end if 347 | 348 | case jsonToken.CurlyClose: 349 | if (currentItem->_datatype = jsonObject) then 350 | if state = valueToken andAlso valueEnd = 0 andAlso valueStart <> i then 351 | valueEnd = i 352 | end if 353 | 354 | if (currentItem = 0 or currentItem->_parent = 0) then 355 | this.setMalformed() 356 | goto cleanup 357 | end if 358 | state = nestEnd 359 | currentItem->AppendChild(child, true) 360 | 361 | currentItem = currentItem->_parent 362 | else 363 | currentItem->setErrorMessage(arrayNotClosed, jsonstring, i) 364 | goto cleanup 365 | end if 366 | 367 | case jsonToken.SquareClose: 368 | if (currentItem->_datatype = jsonArray ) then 369 | if state = valueToken andAlso valueStart <> i then 370 | if (valueEnd = 0) then valueEnd = i 371 | if (child = 0) then 372 | child = new jsonBase() 373 | end if 374 | end if 375 | if (currentItem = 0 or currentItem->_parent = 0) then 376 | this.setMalformed() 377 | goto cleanup 378 | end if 379 | state = nestEnd 380 | 381 | currentItem->AppendChild(child, true) 382 | currentItem = currentItem->_parent 383 | else 384 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 385 | goto cleanup 386 | end if 387 | 388 | case jsonToken.Space, jsonToken.Tab, jsonToken.NewLine, 13 389 | ' Here, we count the left trim we need. This is faster than using TRIM() even for a single space in front of the value 390 | ' And most important: It's not slower if we have no whitespaces. 391 | if ( state = valueToken ) then 392 | if( trimLeftActive) then 393 | valueStart = i+1 394 | else 395 | if valueEnd = 0 then valueEnd = i 396 | end if 397 | end if 398 | 399 | case jsonToken.Quote 400 | ' The closing quote get's through to here. We treat is as part of a value, but without throwing errors. 401 | if ( state = valueToken ) then 402 | trimLeftActive = false 403 | valueEnd = i 404 | end if 405 | case asc("n"),asc("-"),asc("e"),asc("t"),asc("r"),asc("u"),asc("l"),asc("f"),asc("a"),asc("s"), 48 to 57, asc("E"), asc("+"), asc(".") 406 | ' If we are currently parsing values, add up the length and abort the trim-counting. 407 | if ( state = valueToken andAlso valueEnd = 0 ) then 408 | trimLeftActive = false 409 | else 410 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 411 | goto cleanup 412 | end if 413 | select case currentType 414 | case -2: 415 | select case as const jsonString[i]: 416 | case asc("n"): 417 | if (jsonString[i+1] = asc("u") _ 418 | andAlso jsonString[i+2] = asc("l") _ 419 | andAlso jsonstring[i+3] = asc("l") ) then 420 | currentType = jsonNull 421 | i+=3 422 | else 423 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 424 | goto cleanup 425 | end if 426 | 427 | case asc("t") 428 | if (jsonString[i+1] = asc("r") _ 429 | andAlso jsonString[i+2] = asc("u") _ 430 | andAlso jsonstring[i+3] = asc("e") ) then 431 | currentType = jsonBool 432 | i+=3 433 | else 434 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 435 | goto cleanup 436 | end if 437 | 438 | case asc("f"): 439 | if (jsonString[i+1] = asc("a") _ 440 | andAlso jsonString[i+2] = asc("l") _ 441 | andAlso jsonString[i+3] = asc("s") _ 442 | andAlso jsonstring[i+4] = asc("e") ) then 443 | currentType = jsonBool 444 | i+=4 445 | else 446 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 447 | goto cleanup 448 | end if 449 | case jsonToken.minus, 48 to 57: 450 | currentType = jsonNumber 451 | case else 452 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 453 | goto cleanup 454 | end select 455 | case jsonNull, jsonBool 456 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 457 | goto cleanup 458 | end select 459 | 460 | case else: 461 | 462 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 463 | goto cleanup 464 | end select 465 | else 466 | if (state = valueToken) then 467 | currentType = jsonDataType.jsonString 468 | end if 469 | select case as const jsonString[i] 470 | case 0 to 31 471 | currentItem->setErrorMessage(unexpectedToken, jsonstring, i) 472 | goto cleanup 473 | case 92 474 | stringIsEscaped = true 475 | if (isEscaped = false) then 476 | isEscaped = true 477 | else 478 | isEscaped = false 479 | end if 480 | case else 481 | if (isEscaped) then isEscaped = false 482 | end select 483 | continue for 484 | end if 485 | 486 | if ( i = parseEnd andAlso state = valueToken) then 487 | if valueEnd = 0 then valueEnd = i +1 488 | state = valueTokenClosed 489 | end if 490 | 491 | select case as const state 492 | case valueTokenClosed, nestEnd 493 | ' because we already know how long the string we are going to parse is, we can skip if it's 0. 494 | if ( valueEnd > 0 andAlso (child <> 0 or state = valueTokenClosed)) then 495 | if (child = 0) then child = new JsonBase() 496 | ' The time saved with this is miniscule, but reliably measurable. 497 | select case as const currentType 498 | case jsonDataType.jsonString 499 | FastMid(child->_value, jsonString, valuestart+1, valueEnd - valueStart-1) 500 | child->_dataType = jsonDataType.jsonString 501 | if ( stringIsEscaped ) then 502 | if ( len(child->_value) > 0 andAlso DeEscapeString(child->_value) = false ) then 503 | child->setErrorMessage(invalidEscapeSequence, jsonstring, i) 504 | end if 505 | stringIsEscaped = false 506 | end if 507 | 508 | case jsonNull 509 | child->_dataType = jsonNull 510 | FastMid(child->_value, jsonString, valuestart, valueEnd - valueStart) 511 | case jsonBool 512 | ' Nesting "select-case" isn't pretty, but fast. Saw this first in the .net compiler. 513 | FastMid(child->_value, jsonString, valuestart, valueEnd - valueStart) 514 | 515 | child->_datatype = jsonBool 516 | 517 | case jsonNumber 518 | fastMid(child->_value, jsonString, valuestart, valueEnd - valueStart) 519 | if ( isValidDouble(child->_value) ) then 520 | child->_dataType = jsonDataType.jsonNumber 521 | else 522 | child->setErrorMessage(invalidNumber, jsonstring, i) 523 | end if 524 | 525 | case else 526 | child->setErrorMessage(invalidValue, jsonstring, i) 527 | end select 528 | 529 | if (currentItem->_datatype = jsonNull ) then 530 | if (i = parseEnd andAlso child->_datatype <> malformed) then 531 | FastCopy(this._value, child->_value) 532 | this._datatype = child->_datatype 533 | this._error = child->_error 534 | delete child 535 | child = 0 536 | return 537 | else 538 | currentItem->setErrorMessage(0, jsonstring, i+1) 539 | goto cleanup 540 | end if 541 | elseif (state = valueTokenClosed) then 542 | if (child->_datatype = malformed) then 543 | currentItem->SetMalformed() 544 | end if 545 | currentItem->AppendChild(child, true) 546 | else 547 | if (child->_parent = 0) then 548 | delete child 549 | end if 550 | end if 551 | valueEnd = 0 552 | child = 0 553 | end if 554 | 555 | if state <> nestEnd then 556 | goto resetStateJump 557 | end if 558 | 559 | case resetState: 560 | resetStateJump: 561 | child = 0 562 | if ( currentItem->_datatype = jsonArray ) then 563 | state = valueToken 564 | currentType = -2 565 | valueStart = i+1 566 | valueEnd = 0 567 | trimLeftActive = true 568 | else 569 | currentType = -2 570 | state = none 571 | end if 572 | end select 573 | next 574 | 575 | if (isStringOpen) then 576 | currentItem->setErrorMessage(stringNotClosed, jsonstring, endIndex) 577 | goto cleanup 578 | end if 579 | if (state = keyTokenClosed) then 580 | currentItem->setErrorMessage(expectedKey, jsonstring, endIndex) 581 | goto cleanup 582 | end if 583 | 584 | return 585 | cleanup: 586 | if (child <> 0) then 587 | delete child 588 | end if 589 | end sub 590 | 591 | sub JsonBase.Parse(byref inputString as string) 592 | this.destructor() 593 | this.constructor() 594 | fbJsonInternal.FastTrimWhitespace(inputString) 595 | this.Parse( cast (byte ptr, strptr(inputstring)), len(inputString)-1) 596 | end sub 597 | 598 | function JsonBase.getError() as string 599 | return this._error 600 | end function 601 | 602 | -------------------------------------------------------------------------------- /fbJson/JsonBase.bi: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #include once "JsonDatatype.bi" 8 | #include once "fbJsonInternals.bas" 9 | 10 | type JsonBase extends object 11 | protected: 12 | _dataType as jsonDataType = jsonNull 13 | _value as string 14 | _error as string 15 | _children as JsonBase ptr ptr = 0 16 | _parent as JsonBase ptr = 0 17 | _key as string 18 | _count as integer = -1 19 | 20 | declare sub Parse(jsonString as ubyte ptr, endIndex as integer) 21 | declare sub SetMalformed() 22 | declare function AppendChild(newChild as JsonBase ptr, override as boolean = false) as boolean 23 | declare sub setErrorMessage(errorCode as fbJsonInternal.jsonError, jsonstring as byte ptr, position as uinteger) 24 | public: 25 | declare static function ParseJson(inputString as string) byref as JsonBase 26 | declare constructor() 27 | declare constructor(byref jsonString as string) 28 | 29 | declare destructor() 30 | 31 | declare property Count() as integer 32 | declare property DataType() as jsonDataType 33 | 34 | declare property Parent() byref as JsonBase 35 | 36 | declare operator LET(A as JsonBase) 37 | 38 | declare sub Parse(byref jsonString as string) 39 | 40 | declare function ContainsKey(byref key as string) as boolean 41 | 42 | declare function getError() as string 43 | end type 44 | -------------------------------------------------------------------------------- /fbJson/JsonDatatype.bi: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | enum jsonDataType 8 | malformed = -1 9 | jsonNull = 0 10 | jsonObject 11 | jsonArray 12 | jsonNumber 13 | jsonString 14 | jsonBool 15 | end enum 16 | -------------------------------------------------------------------------------- /fbJson/JsonItem.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #include once "JsonItem.bi" 8 | #include once "JsonBase.bas" 9 | #include once "StringFunctions.bi" 10 | 11 | constructor JsonItem() 12 | base() 13 | ' Nothing to do 14 | end constructor 15 | 16 | constructor JsonItem(byref jsonString as string) 17 | base(jsonstring) 18 | end constructor 19 | 20 | destructor JsonItem() 21 | base.destructor() 22 | end destructor 23 | 24 | operator JsonItem.[](newKey as string) byref as JsonItem 25 | if ( this._datatype = jsonObject and this._count > -1 ) then 26 | for i as integer = 0 to this._count 27 | if ( this._children[i]->_key = newkey ) then 28 | return *cast(JsonItem ptr,this._children[i]) 29 | end if 30 | next 31 | end if 32 | 33 | #ifdef fbJSON_debug 34 | print "fbJSON Error: Key '"& key & "' not found in object "& this.key 35 | end -1 36 | #endif 37 | return *new JsonItem() 38 | end operator 39 | 40 | operator JsonItem.[](index as integer) byref as JsonItem 41 | if ( index <= this._count ) then 42 | return *cast(JsonItem ptr,this._children[index]) 43 | end if 44 | 45 | #ifdef fbJSON_debug 46 | print "fbJSON Error: "& index & " out of bounds in "& this.key &". Actual size is "& this.count 47 | end -1 48 | #else 49 | return *new JsonItem() 50 | #endif 51 | end operator 52 | 53 | property JsonItem.Key() as string 54 | return this._key 55 | end property 56 | 57 | property JsonItem.Key(newkey as string) 58 | if ( this._key = newKey ) then return 59 | 60 | if ( this.Parent.ContainsKey(newKey) ) then return 61 | 62 | this._key = newKey 63 | end property 64 | 65 | property JsonItem.datatype() as jsonDataType 66 | return this._dataType 67 | end property 68 | 69 | property JsonItem.Count() as integer 70 | return this._count + 1 71 | end property 72 | 73 | property JsonItem.Value( byref newValue as string) 74 | using fbJsonInternal 75 | 76 | ' TODO: Optimize this, according to the parser optimizations 77 | 78 | ' First, handle strings in quotes: 79 | select case as const newValue[0] 80 | case jsonToken.Quote 81 | if ( newValue[len(newValue)-1] = jsonToken.Quote ) then 82 | this._dataType = jsonString 83 | this._value = mid(newValue,2, len(newValue)-2) 84 | if ( DeEscapeString(this._value) = false ) then 85 | this.setMalformed() 86 | end if 87 | else 88 | this.setMalformed() 89 | end if 90 | case 45, 48,49,50,51,52,53,54,55,56,57 '-, 0 - 9 91 | if (isValidDouble(newValue) ) then 92 | this._datatype = jsonNumber 93 | this._value = str(cdbl(newValue)) 94 | else 95 | this._datatype = jsonString 96 | this._value = newValue 97 | 98 | if ( DeEscapeString(this._value) = false ) then 99 | this.setMalformed() 100 | end if 101 | end if 102 | case 110,78, 102,70, 116,84 ' n, f, t 103 | select case lcase(newValue) 104 | case "null" 105 | this._value = newValue 106 | this._dataType = jsonNull 107 | case "true", "false" 108 | this._value = newValue 109 | this._dataType = jsonBool 110 | case else: 111 | ' strict vs. nonstrict mode? 112 | this._datatype = jsonString 113 | this._value = newValue 114 | 115 | if ( DeEscapeString(this._value) = false ) then 116 | this.setMalformed() 117 | end if 118 | end select 119 | case else 120 | this._dataType = jsonString 121 | this._value = newValue 122 | if ( DeEscapeString(this._value) = false ) then 123 | this.setMalformed() 124 | end if 125 | end select 126 | end property 127 | 128 | property JsonItem.Value() as string 129 | return this._value 130 | end property 131 | 132 | function JsonItem.AddItem(newKey as string, newValue as string) as boolean 133 | if ( len(newKey) = 0 orElse this.containsKey(newKey) ) then 134 | return false 135 | end if 136 | 137 | if ( this._datatype = jsonNull ) then 138 | this._datatype = jsonObject 139 | end if 140 | 141 | if ( this._datatype = jsonObject ) then 142 | dim child as JsonItem ptr = new JsonItem 143 | child->Value = newValue 144 | child->_key = newKey 145 | return this.AppendChild(child) 146 | end if 147 | return false 148 | end function 149 | 150 | function JsonItem.AddItem(newKey as string, item as JsonItem) as boolean 151 | if ( len(newKey) = 0 orElse this.containsKey(newKey) ) then 152 | return false 153 | end if 154 | 155 | if ( this._datatype = jsonNull ) then 156 | this._datatype = jsonObject 157 | end if 158 | 159 | if ( this._datatype = jsonObject ) then 160 | dim child as JsonItem ptr = callocate(sizeof(JsonItem)) 161 | *child = item 162 | child->_key = newKey 163 | return this.AppendChild(child) 164 | end if 165 | return false 166 | end function 167 | 168 | function JsonItem.AddItem(newValue as string) as boolean 169 | if ( this._datatype = jsonArray or this._datatype = jsonNull ) then 170 | this._datatype = jsonArray 171 | dim child as JsonItem ptr = new JsonItem 172 | child->value = newValue 173 | return this.AppendChild(child) 174 | end if 175 | return false 176 | end function 177 | 178 | function JsonItem.AddItem(item as JsonItem) as boolean 179 | if ( this._datatype = jsonArray or this._datatype = jsonNull ) then 180 | this._datatype = jsonArray 181 | dim child as JsonItem ptr = callocate(sizeof(JsonItem)) 182 | *child = item 183 | return this.AppendChild(child) 184 | end if 185 | return false 186 | end function 187 | 188 | function JsonItem.RemoveItem(newKey as string) as boolean 189 | dim as integer index = -1 190 | 191 | if ( this._datatype = jsonObject ) then 192 | for i as integer = 0 to this._count 193 | if ( this._children[i]->_key = newkey ) then 194 | index = i 195 | exit for 196 | end if 197 | next 198 | end if 199 | 200 | return this.RemoveItem(index) 201 | end function 202 | 203 | function JsonItem.RemoveItem(index as integer) as boolean 204 | if ( index <= this._count andAlso index > -1 ) then 205 | delete this._children[index] 206 | if ( index < this.Count -1 ) then 207 | for i as integer = index to this._count 208 | this._children[i] = this._children[i+1] 209 | next 210 | end if 211 | 212 | this._count -= 1 213 | this._children = reallocate(this._children, sizeof(jsonItem ptr) * this._count) 214 | 215 | return true 216 | end if 217 | return false 218 | end function 219 | 220 | function JsonItem.ToString(level as integer = 0) as string 221 | dim as string result 222 | 223 | ' TODO: Clean up this mess. 224 | if ( this.datatype = jsonObject ) then 225 | result = "{" 226 | elseif ( this.datatype = jsonArray ) then 227 | result = "[" 228 | elseif ( level = 0 ) then 229 | return this._value 230 | end if 231 | 232 | for i as integer = 0 to this._count 233 | result += chr(10) + string((level +1) * 2, " ") 234 | if ( this.datatype = jsonObject ) then 235 | result += """" & this[i]._key & """ : " 236 | end if 237 | 238 | if ( this[i].Count >= 1 ) then 239 | result += this[i].toString(level+1) 240 | else 241 | if ( this[i].datatype = jsonString) then 242 | result += """" & this[i]._value & """" 243 | else 244 | result += this[i]._value 245 | end if 246 | end if 247 | if ( i < this.Count - 1 ) then 248 | result += "," 249 | else 250 | level -= 1 251 | result += chr(10) 252 | end if 253 | 254 | next 255 | 256 | 257 | if this.datatype = jsonObject then 258 | result += string((level +1) * 2, " ") + "}" 259 | elseif ( this.datatype = jsonArray ) then 260 | result += string((level +1) * 2, " ") +"]" 261 | end if 262 | 263 | return result 264 | end function 265 | -------------------------------------------------------------------------------- /fbJson/JsonItem.bi: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #include once "JsonBase.bi" 8 | #ifndef fbJson_HeaderLib 9 | #inclib "fbJson" 10 | #endif 11 | 12 | type JsonItem extends JsonBase 13 | public: 14 | declare constructor() 15 | declare constructor(byref jsonString as string) 16 | 17 | declare destructor() 18 | 19 | declare property Key () as string 20 | declare property Key (value as string) 21 | 22 | declare property Value(byref newValue as string) 23 | declare property Value() as string 24 | 25 | declare property Count() as integer 26 | declare property DataType() as jsonDataType 27 | 28 | declare operator [](key as string) byref as JsonItem 29 | declare operator [](index as integer) byref as JsonItem 30 | 31 | 'declare operator LET(A as JsonItem) 32 | 33 | declare function ToString(level as integer = 0) as string 34 | 35 | declare function AddItem(key as string, value as string) as boolean 36 | declare function AddItem(key as string, item as JsonItem) as boolean 37 | 38 | declare function AddItem(value as string) as boolean 39 | declare function AddItem(item as JsonItem) as boolean 40 | 41 | declare function RemoveItem(key as string) as boolean 42 | declare function RemoveItem(index as integer) as boolean 43 | end type 44 | -------------------------------------------------------------------------------- /fbJson/StringFunctions.bi: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | #include "crt.bi" 8 | 9 | namespace fbJsonInternal 10 | 11 | ' Allows us to interact directly with the FB-Internal string-structure. 12 | ' Don't use it, unless you know what you're doing. 13 | type fbString 14 | dim as byte ptr stringData 15 | dim as integer length 16 | dim as integer size 17 | end type 18 | 19 | const replacementChar as string = "�" 20 | 21 | 'declare function validateCodepoint(byref codepoint as ubyte) as boolean 22 | declare sub FastSpace(byref destination as string, length as uinteger) 23 | declare sub FastLeft(byref destination as string, length as uinteger) 24 | declare sub FastMid(byref destination as string, byref source as byte ptr, start as uinteger, length as uinteger) 25 | declare function isInString(byref target as string, query as byte) as boolean 26 | declare sub LongToUft8(byref codepoint as long, byref result as string) 27 | declare sub SurrogateToUtf8(surrogateA as long, surrogateB as long, byref result as string) 28 | declare function areEqual(byref stringA as string, byref stringB as string) as boolean 29 | declare function DeEscapeString(byref escapedString as string) as boolean 30 | 31 | sub FastSpace(byref destination as string, length as uinteger) 32 | dim as fbString ptr destinationPtr = cast(fbString ptr, @destination) 33 | if ( destinationPtr->size < length ) then 34 | deallocate destinationptr->stringdata 35 | destinationPtr->stringData = allocate(length+1) 36 | end if 37 | memset(destinationPtr->stringData, 32, length) 38 | destinationPtr->length = length 39 | end sub 40 | 41 | sub FastCopy(byref destination as string, byref source as string) 42 | dim as fbString ptr destinationPtr = cast(fbString ptr, @destination) 43 | dim as fbString ptr sourcePtr = cast(fbString ptr, @source) 44 | if (sourcePtr->length = 0 and destinationPtr->length = 0) then return 45 | if ( destinationPtr->size < sourcePtr->size ) then 46 | deallocate destinationptr->stringdata 47 | destinationPtr->length = sourcePtr->length 48 | destinationPtr->size = sourcePtr->length 49 | destinationPtr->stringData = allocate(sourcePtr->length+1) 50 | destinationPtr->stringData[sourcePtr->length] = 0 51 | end if 52 | 53 | ' We allocate an extra byte here because FB tries to write into that extra byte when doing string copies. 54 | ' The more "correct" mitigation would be to allocate up to the next blocksize (32 bytes), but that's slow. 55 | memcpy( destinationPtr->stringData, sourcePtr->stringData, destinationPtr->size) 56 | 57 | end sub 58 | 59 | 60 | sub FastLeft(byref destination as string, length as uinteger) 61 | dim as fbString ptr destinationPtr = cast(fbString ptr, @destination) 62 | dim as any ptr oldPtr = destinationPtr->stringData 63 | destinationPtr->length = IIF(length < destinationPtr->length, length, destinationPtr->length) 64 | 'destinationPtr->size = destinationPtr->length 65 | end sub 66 | 67 | sub FastMid(byref destination as string, byref source as byte ptr, start as uinteger, length as uinteger) 68 | dim as fbString ptr destinationPtr = cast(fbString ptr, @destination) 69 | 70 | ' Setting the length and size of the string, so the runtime knows how to handle it properly. 71 | if ( destinationPtr->size < length ) then 72 | if ( destinationPtr->size ) then deallocate destinationPtr->stringData 73 | 74 | ' We allocate an extra byte here because FB tries to write into that extra byte when doing string copies. 75 | ' The more "correct" mitigation would be to allocate up to the next blocksize (32 bytes), but that's slow. 76 | destinationPtr->stringData = allocate(length+1) 77 | destinationPtr->stringData[length] = 0 78 | end if 79 | destinationPtr->length = length 80 | destinationPtr->size = length 81 | memcpy( destinationPtr->stringData, source+start, destinationPtr->size ) 82 | end sub 83 | 84 | function isInString(byref target as string, query as byte) as boolean 85 | dim as fbstring ptr targetPtr = cast(fbstring ptr, @target) 86 | if ( targetPtr->size = 0 ) then return false 87 | 88 | return memchr( targetPtr->stringData, query, targetPtr->size ) <> 0 89 | end function 90 | 91 | sub LongToUft8(byref codepoint as long, byref result as string) 92 | if codePoint <= &h7F then 93 | fastSpace(result, 1) 94 | result[0] = codePoint 95 | endif 96 | 97 | if (&hD800 <= codepoint AND codepoint <= &hDFFF) OR _ 98 | (codepoint > &h10FFFD) then 99 | result = replacementChar 100 | return 101 | end if 102 | 103 | if (codepoint <= &h7FF) then 104 | fastSpace(result, 2) 105 | result[0] = &hC0 OR (codepoint SHR 6) AND &h1F 106 | result[1] = &h80 OR codepoint AND &h3F 107 | return 108 | end if 109 | if (codepoint <= &hFFFF) then 110 | fastSpace(result, 3) 111 | result[0] = &hE0 OR codepoint SHR 12 AND &hF 112 | result[1] = &h80 OR codepoint SHR 6 AND &h3F 113 | result[2] = &h80 OR codepoint AND &h3F 114 | return 115 | end if 116 | 117 | fastSpace(result, 4) 118 | result[0] = &hF0 OR codepoint SHR 18 AND &h7 119 | result[1] = &h80 OR codepoint SHR 12 AND &h3F 120 | result[2] = &h80 OR codepoint SHR 6 AND &h3F 121 | result[3] = &h80 OR codepoint AND &h3F 122 | 123 | return 124 | end sub 125 | 126 | sub SurrogateToUtf8(surrogateA as long, surrogateB as long, byref result as string) 127 | dim as long codepoint = 0 128 | if (&hD800 <= surrogateA and surrogateA <= &hDBFF) then 129 | if (&hDC00 <= surrogateB and surrogateB <= &hDFFF) then 130 | codepoint = &h10000 131 | codepoint += (surrogateA and &h03FF) shl 10 132 | codepoint += (surrogateB and &h03FF) 133 | end if 134 | end if 135 | 136 | 137 | if ( codePoint = 0 ) then 138 | result = replacementChar 139 | end if 140 | FastSpace(result, 4) 141 | result[0] = &hF0 OR codepoint SHR 18 AND &h7 142 | result[1] = &h80 OR codepoint SHR 12 AND &h3F 143 | result[2] = &h80 OR codepoint SHR 6 AND &h3F 144 | result[3] = &h80 OR codepoint AND &h3F 145 | end sub 146 | 147 | function areEqual(byref stringA as string, byref stringB as string) as boolean 148 | dim as fbString ptr A = cast(fbString ptr, @stringA) 149 | dim as fbString ptr B = cast(fbString ptr, @stringB) 150 | 151 | if (A->length <> B->length) then 152 | return false 153 | end if 154 | 155 | if (A = B) then 156 | return true 157 | end if 158 | return strcmp(A->stringData, B->stringData) = 0 159 | end function 160 | 161 | function DeEscapeString(byref escapedString as string) as boolean 162 | dim as uinteger length = len(escapedString)-1 163 | 164 | dim as uinteger trimSize = 0 165 | dim as boolean isEscaped 166 | for i as uinteger = 0 to length 167 | ' 92 is backslash 168 | 169 | if ( escapedString[i] = 92 andAlso isEscaped = false) then 170 | isEscaped = true 171 | if ( i < length ) then 172 | select case as const escapedString[i+1] 173 | case 34, 92, 47: ' " \ / 174 | ' Nothing to do here. 175 | case 98 ' b 176 | escapedString[i+1] = 8 ' backspace 177 | case 102 ' f 178 | escapedString[i+1] = 12 179 | case 110 ' n 180 | escapedString[i+1] = 10 181 | case 114 ' r 182 | escapedString[i+1] = 13 183 | case 116 ' t 184 | escapedString[i+1] = 9 ' tab 185 | case 117 ' u 186 | 'magic number '6': 2 for "\u" and 4 digit. 187 | if (i+5 > length) then 188 | return false 189 | end if 190 | dim sequence as string = mid(escapedString, i+3, 4) 191 | 192 | dim pad as integer 193 | dim as string glyph 194 | dim as long codepoint = strtoull(sequence, 0, 16) 195 | if (&hD800 <= codepoint and codepoint <= &hDFFF) then 196 | dim secondSurrogate as string = mid(escapedString, i+7+2, 4) 197 | if (len(secondSurrogate) = 4) then 198 | SurrogateToUtf8(codepoint, strtoull(secondSurrogate, 0, 16), glyph) 199 | pad = 12 - len(glyph) 200 | else 201 | return false 202 | end if 203 | elseif (codepoint > 0 orElse sequence = "0000") then 204 | LongToUft8(codepoint, glyph) 205 | pad = 6 - len(glyph) 206 | end if 207 | 208 | if (len(glyph) = 0) then 209 | return false 210 | end if 211 | 212 | for j as integer = 0 to len(glyph)-1 213 | escapedString[i+j+pad] = glyph[j] 214 | next 215 | i += pad -1 216 | trimSize += pad -1 217 | case else 218 | return false 219 | end select 220 | trimSize+=1 221 | end if 222 | elseif ( trimSize > 0 ) then 223 | isEscaped = false 224 | escapedString[i-trimsize] = escapedString[i] 225 | end if 226 | next 227 | if ( trimSize > 0 ) then 228 | fastleft(escapedString, length - trimSize+1) 229 | end if 230 | return true 231 | end function 232 | 233 | function isValidDouble(byref value as string) as boolean 234 | ' Note to reader: 235 | ' This function is strictly for validation as far as the IETF rfc7159 is concerned. 236 | ' This might be more restrictive than you need it to be outside JSON use. 237 | 238 | ' It's also a bit nuts. Callgrind is such a fascinating thing. 239 | 240 | 241 | dim as fbString ptr valuePtr = cast(fbString ptr, @value) 242 | dim as integer period = 0, exponent = 0, sign = 0 243 | 244 | ' Yay for manual loop-unrolling. 245 | select case as const value[0] 246 | case 48: ' 0. No leading zeroes allowed. 247 | if ( valuePtr->length > 2) then 248 | if (value[1] <> asc(".")) then 249 | select case value 250 | ' Shorthands for "0" that won't pass this validation otherwise. 251 | case "0e1","0e+1","0E1", "0E+1", "0e-1", "0E-1" 252 | value = "0" 253 | return true 254 | case else 255 | return false 256 | end select 257 | end if 258 | elseif ( valuePtr->length = 1) then 259 | return true 260 | elseif (valuePtr->length > 1 and value[1] <> 101 and value[1] <> 69 and value[1] <> 46 ) then 261 | return false 262 | end if 263 | case 49 to 57 ' 1 - 9 264 | if (valuePtr->length = 1) then 265 | return true 266 | end if 267 | case 101, 69: 'e, E 268 | return false 269 | case 46: ' . 270 | return false 271 | case 45: ' - 272 | sign += 1 273 | if (valuePtr->length = 1) then 274 | return false 275 | elseif (valuePtr->length >= 3) then 276 | if (value[1] = 48 andAlso (value[2] >= 48 and value[2] <= 57)) then 277 | return false 278 | end if 279 | end if 280 | case else 281 | return false 282 | end select 283 | 284 | 285 | for i as integer = 1 to valuePtr->length-1 286 | select case as const value[i] 287 | case 48 to 57 ' 0 - 9 288 | ' do nothing 289 | case 101, 69: 'e, E 290 | if (i = valuePtr->length-1) then 291 | return false 292 | end if 293 | if (exponent > 0) then 294 | return false 295 | end if 296 | if (value[i-1] = 46) then 297 | return false 298 | end if 299 | exponent += 1 300 | case 46: ' . 301 | if (i = valuePtr->length-1) then 302 | return false 303 | end if 304 | if (period > 0 or exponent > 0 ) then 305 | return false 306 | end if 307 | if ( value[i-1] = 45) then return false 308 | period += 1 309 | case 45, 43 ' -, + 310 | if ((value[i-1] <> 101 and value[i-1] <> 69)) then 311 | return false 312 | end if 313 | if (i = valuePtr->length-1) then 314 | return false 315 | end if 316 | 317 | case else 318 | return false 319 | end select 320 | next 321 | 322 | if (exponent = 0 and valuePtr->length < 307) then 323 | if (period = 0 and sign = 0) then 324 | return true 325 | end if 326 | ' >=3 because the smalles possible is "0.0" 327 | if (period = 1 and sign = 0 and valuePtr->length >= 3) then 328 | return true 329 | end if 330 | ' >=4 because the smallest possible is "-0.0" 331 | if (period = 1 and sign = 1 and valuePtr->length >= 4) then 332 | return true 333 | end if 334 | end if 335 | value = str(cdbl(value)) 336 | return not(valuePtr->length = 1 andAlso (value = "0")) 337 | end function 338 | 339 | 340 | sub FastTrimWhitespace(byref destination as string) 341 | dim as uinteger start, i, strLen = len(destination) 342 | if (strLen = 0) then return 343 | 344 | while start < strLen-1 andAlso destination[start] = 32 orElse destination[start] = 9 orElse destination[start] = 10 orElse destination[start] = 13 345 | start += 1 346 | wend 347 | if start = strLen -1 then 348 | destination = "" 349 | return 350 | end if 351 | 352 | i = strLen -1 353 | while i > 1 andAlso destination[i] = 32 orElse destination[i] = 9 orElse destination[i] = 10 orElse destination[i] = 13 354 | i -= 1 355 | wend 356 | if (start = 0 and i = strLen-1) then return 357 | 358 | fastMid(destination, cast(byte ptr,strptr(destination)), start, i - start + 1) 359 | end sub 360 | 361 | end namespace 362 | -------------------------------------------------------------------------------- /fbJson/fbJsonInternals.bas: -------------------------------------------------------------------------------- 1 | /' 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | '/ 6 | 7 | namespace fbJsonInternal 8 | 9 | enum jsonError 10 | arrayNotClosed 11 | objectNotClosed 12 | stringNotClosed 13 | invalidValue 14 | invalidEscapeSequence 15 | invalidNumber 16 | expectedKey 17 | expectedValue 18 | unexpectedToken 19 | invalidCodepoint 20 | end enum 21 | 22 | enum jsonToken 23 | tab = 9 24 | newLine = 10 25 | space = 32 26 | quote = 34 27 | comma = 44 28 | colon = 58 29 | squareOpen = 91 30 | backSlash = 92 31 | forwardSlash = 47 32 | squareClose = 93 33 | curlyOpen = 123 34 | curlyClose = 125 35 | minus = 45 36 | plus = 43 37 | end enum 38 | 39 | enum parserState 40 | none = 0 41 | keyToken = 1 42 | keyTokenClosed = 2 43 | valueToken = 3 44 | valueTokenClosed = 4 45 | nestEnd = 5 46 | resetState 47 | end enum 48 | 49 | end namespace 50 | --------------------------------------------------------------------------------