├── .github └── workflows │ ├── oss-fuzz.yml │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── code-of-conduct.md ├── examples └── formatjson5.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── fuzz_parse.rs ├── rustfmt.toml ├── samples └── fuzz_fails_fixed │ ├── clusterfuzz-testcase-minimized-fuzz_parse-4641835596251136 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-4802677486780416 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-4993106563956736 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-5734884822351872 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-6069233958649856 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-6238978431385600 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-6541106597199872 │ ├── clusterfuzz-testcase-minimized-fuzz_parse-6612345919504384 │ └── clusterfuzz-testcase-minimized-fuzz_parse-6642606161920000 ├── src ├── content.rs ├── error.rs ├── formatter.rs ├── lib.rs ├── options.rs └── parser.rs └── tests └── lib.rs /.github/workflows/oss-fuzz.yml: -------------------------------------------------------------------------------- 1 | name: CIFuzz 2 | on: [pull_request] 3 | jobs: 4 | Fuzzing: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | sanitizer: [address] 10 | # options include 1 or more of sanitizer: [address, undefined, memory] 11 | steps: 12 | - name: Build Fuzzers (${{ matrix.sanitizer }}) 13 | id: build 14 | uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master 15 | with: 16 | oss-fuzz-project-name: 'json5format' 17 | language: rust 18 | dry-run: false 19 | sanitizer: ${{ matrix.sanitizer }} 20 | - name: Run Fuzzers (${{ matrix.sanitizer }}) 21 | uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master 22 | with: 23 | oss-fuzz-project-name: 'json5format' 24 | language: rust 25 | fuzz-seconds: 600 26 | dry-run: false 27 | sanitizer: ${{ matrix.sanitizer }} 28 | - name: Upload Crash 29 | uses: actions/upload-artifact@v1 30 | if: failure() && steps.build.outcome == 'success' 31 | with: 32 | name: ${{ matrix.sanitizer }}-artifacts 33 | path: ./out/artifacts 34 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: json5format 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Cancel previous 18 | uses: styfle/cancel-workflow-action@0.8.0 19 | with: 20 | access_token: ${{ github.token }} 21 | - uses: actions/checkout@v2 22 | - name: Run Rustfmt 23 | run: cargo fmt -- --check 24 | - name: Build 25 | run: cargo build --verbose 26 | - name: Clippy 27 | run: cargo clippy --all-targets -- -D warnings 28 | - name: Run tests 29 | run: cargo test --verbose 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | rls*.log 3 | *.swp 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.28" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "0.1.7" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.0.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 51 | 52 | [[package]] 53 | name = "bit-set" 54 | version = "0.5.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "e84c238982c4b1e1ee668d136c510c67a13465279c0cb367ea6baf6310620a80" 57 | dependencies = [ 58 | "bit-vec", 59 | ] 60 | 61 | [[package]] 62 | name = "bit-vec" 63 | version = "0.5.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.2.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 72 | 73 | [[package]] 74 | name = "byteorder" 75 | version = "1.3.4" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "0.1.10" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 84 | 85 | [[package]] 86 | name = "clap" 87 | version = "2.33.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 90 | dependencies = [ 91 | "ansi_term", 92 | "atty", 93 | "bitflags", 94 | "strsim", 95 | "textwrap", 96 | "unicode-width", 97 | "vec_map", 98 | ] 99 | 100 | [[package]] 101 | name = "cloudabi" 102 | version = "0.0.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 105 | dependencies = [ 106 | "bitflags", 107 | ] 108 | 109 | [[package]] 110 | name = "fnv" 111 | version = "1.0.6" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 114 | 115 | [[package]] 116 | name = "fuchsia-cprng" 117 | version = "0.1.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 120 | 121 | [[package]] 122 | name = "getrandom" 123 | version = "0.1.14" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 126 | dependencies = [ 127 | "cfg-if", 128 | "libc", 129 | "wasi", 130 | ] 131 | 132 | [[package]] 133 | name = "heck" 134 | version = "0.3.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 137 | dependencies = [ 138 | "unicode-segmentation", 139 | ] 140 | 141 | [[package]] 142 | name = "hermit-abi" 143 | version = "0.1.11" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "8a0d737e0f947a1864e93d33fdef4af8445a00d1ed8dc0c8ddb73139ea6abf15" 146 | dependencies = [ 147 | "libc", 148 | ] 149 | 150 | [[package]] 151 | name = "json5format" 152 | version = "0.2.6" 153 | dependencies = [ 154 | "anyhow", 155 | "lazy_static", 156 | "maplit", 157 | "proptest", 158 | "regex", 159 | "structopt", 160 | ] 161 | 162 | [[package]] 163 | name = "lazy_static" 164 | version = "1.4.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 167 | 168 | [[package]] 169 | name = "libc" 170 | version = "0.2.69" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 173 | 174 | [[package]] 175 | name = "maplit" 176 | version = "1.0.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 179 | 180 | [[package]] 181 | name = "memchr" 182 | version = "2.5.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 185 | 186 | [[package]] 187 | name = "num-traits" 188 | version = "0.2.11" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 191 | dependencies = [ 192 | "autocfg 1.0.0", 193 | ] 194 | 195 | [[package]] 196 | name = "ppv-lite86" 197 | version = "0.2.6" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 200 | 201 | [[package]] 202 | name = "proc-macro2" 203 | version = "0.4.30" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 206 | dependencies = [ 207 | "unicode-xid", 208 | ] 209 | 210 | [[package]] 211 | name = "proptest" 212 | version = "0.9.6" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "01c477819b845fe023d33583ebf10c9f62518c8d79a0960ba5c36d6ac8a55a5b" 215 | dependencies = [ 216 | "bit-set", 217 | "bitflags", 218 | "byteorder", 219 | "lazy_static", 220 | "num-traits", 221 | "quick-error", 222 | "rand 0.6.5", 223 | "rand_chacha 0.1.1", 224 | "rand_xorshift", 225 | "regex-syntax", 226 | "rusty-fork", 227 | "tempfile", 228 | ] 229 | 230 | [[package]] 231 | name = "quick-error" 232 | version = "1.2.3" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 235 | 236 | [[package]] 237 | name = "quote" 238 | version = "0.6.13" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 241 | dependencies = [ 242 | "proc-macro2", 243 | ] 244 | 245 | [[package]] 246 | name = "rand" 247 | version = "0.6.5" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 250 | dependencies = [ 251 | "autocfg 0.1.7", 252 | "libc", 253 | "rand_chacha 0.1.1", 254 | "rand_core 0.4.2", 255 | "rand_hc 0.1.0", 256 | "rand_isaac", 257 | "rand_jitter", 258 | "rand_os", 259 | "rand_pcg", 260 | "rand_xorshift", 261 | "winapi", 262 | ] 263 | 264 | [[package]] 265 | name = "rand" 266 | version = "0.7.3" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 269 | dependencies = [ 270 | "getrandom", 271 | "libc", 272 | "rand_chacha 0.2.2", 273 | "rand_core 0.5.1", 274 | "rand_hc 0.2.0", 275 | ] 276 | 277 | [[package]] 278 | name = "rand_chacha" 279 | version = "0.1.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 282 | dependencies = [ 283 | "autocfg 0.1.7", 284 | "rand_core 0.3.1", 285 | ] 286 | 287 | [[package]] 288 | name = "rand_chacha" 289 | version = "0.2.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 292 | dependencies = [ 293 | "ppv-lite86", 294 | "rand_core 0.5.1", 295 | ] 296 | 297 | [[package]] 298 | name = "rand_core" 299 | version = "0.3.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 302 | dependencies = [ 303 | "rand_core 0.4.2", 304 | ] 305 | 306 | [[package]] 307 | name = "rand_core" 308 | version = "0.4.2" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 311 | 312 | [[package]] 313 | name = "rand_core" 314 | version = "0.5.1" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 317 | dependencies = [ 318 | "getrandom", 319 | ] 320 | 321 | [[package]] 322 | name = "rand_hc" 323 | version = "0.1.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 326 | dependencies = [ 327 | "rand_core 0.3.1", 328 | ] 329 | 330 | [[package]] 331 | name = "rand_hc" 332 | version = "0.2.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 335 | dependencies = [ 336 | "rand_core 0.5.1", 337 | ] 338 | 339 | [[package]] 340 | name = "rand_isaac" 341 | version = "0.1.1" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 344 | dependencies = [ 345 | "rand_core 0.3.1", 346 | ] 347 | 348 | [[package]] 349 | name = "rand_jitter" 350 | version = "0.1.4" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 353 | dependencies = [ 354 | "libc", 355 | "rand_core 0.4.2", 356 | "winapi", 357 | ] 358 | 359 | [[package]] 360 | name = "rand_os" 361 | version = "0.1.3" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 364 | dependencies = [ 365 | "cloudabi", 366 | "fuchsia-cprng", 367 | "libc", 368 | "rand_core 0.4.2", 369 | "rdrand", 370 | "winapi", 371 | ] 372 | 373 | [[package]] 374 | name = "rand_pcg" 375 | version = "0.1.2" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 378 | dependencies = [ 379 | "autocfg 0.1.7", 380 | "rand_core 0.4.2", 381 | ] 382 | 383 | [[package]] 384 | name = "rand_xorshift" 385 | version = "0.1.1" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 388 | dependencies = [ 389 | "rand_core 0.3.1", 390 | ] 391 | 392 | [[package]] 393 | name = "rdrand" 394 | version = "0.4.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 397 | dependencies = [ 398 | "rand_core 0.3.1", 399 | ] 400 | 401 | [[package]] 402 | name = "redox_syscall" 403 | version = "0.1.56" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 406 | 407 | [[package]] 408 | name = "regex" 409 | version = "1.5.6" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 412 | dependencies = [ 413 | "aho-corasick", 414 | "memchr", 415 | "regex-syntax", 416 | ] 417 | 418 | [[package]] 419 | name = "regex-syntax" 420 | version = "0.6.26" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 423 | 424 | [[package]] 425 | name = "remove_dir_all" 426 | version = "0.5.2" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 429 | dependencies = [ 430 | "winapi", 431 | ] 432 | 433 | [[package]] 434 | name = "rusty-fork" 435 | version = "0.2.2" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "3dd93264e10c577503e926bd1430193eeb5d21b059148910082245309b424fae" 438 | dependencies = [ 439 | "fnv", 440 | "quick-error", 441 | "tempfile", 442 | "wait-timeout", 443 | ] 444 | 445 | [[package]] 446 | name = "strsim" 447 | version = "0.8.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 450 | 451 | [[package]] 452 | name = "structopt" 453 | version = "0.2.18" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" 456 | dependencies = [ 457 | "clap", 458 | "structopt-derive", 459 | ] 460 | 461 | [[package]] 462 | name = "structopt-derive" 463 | version = "0.2.18" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107" 466 | dependencies = [ 467 | "heck", 468 | "proc-macro2", 469 | "quote", 470 | "syn", 471 | ] 472 | 473 | [[package]] 474 | name = "syn" 475 | version = "0.15.44" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 478 | dependencies = [ 479 | "proc-macro2", 480 | "quote", 481 | "unicode-xid", 482 | ] 483 | 484 | [[package]] 485 | name = "tempfile" 486 | version = "3.1.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 489 | dependencies = [ 490 | "cfg-if", 491 | "libc", 492 | "rand 0.7.3", 493 | "redox_syscall", 494 | "remove_dir_all", 495 | "winapi", 496 | ] 497 | 498 | [[package]] 499 | name = "textwrap" 500 | version = "0.11.0" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 503 | dependencies = [ 504 | "unicode-width", 505 | ] 506 | 507 | [[package]] 508 | name = "unicode-segmentation" 509 | version = "1.6.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 512 | 513 | [[package]] 514 | name = "unicode-width" 515 | version = "0.1.7" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 518 | 519 | [[package]] 520 | name = "unicode-xid" 521 | version = "0.1.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 524 | 525 | [[package]] 526 | name = "vec_map" 527 | version = "0.8.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 530 | 531 | [[package]] 532 | name = "wait-timeout" 533 | version = "0.2.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 536 | dependencies = [ 537 | "libc", 538 | ] 539 | 540 | [[package]] 541 | name = "wasi" 542 | version = "0.9.0+wasi-snapshot-preview1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 545 | 546 | [[package]] 547 | name = "winapi" 548 | version = "0.3.8" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 551 | dependencies = [ 552 | "winapi-i686-pc-windows-gnu", 553 | "winapi-x86_64-pc-windows-gnu", 554 | ] 555 | 556 | [[package]] 557 | name = "winapi-i686-pc-windows-gnu" 558 | version = "0.4.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 561 | 562 | [[package]] 563 | name = "winapi-x86_64-pc-windows-gnu" 564 | version = "0.4.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 567 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "json5format" 3 | version = "0.2.6" 4 | authors = [ 5 | "Rich Kadel ", 6 | "David Tamas-Parris ", 7 | ] 8 | edition = "2018" 9 | keywords = ["json", "json5", "style", "formatter", "comments"] 10 | license = "BSD-3-Clause" 11 | description = "Customizable JSON5 document formatter that preserves comments" 12 | repository = "https://github.com/google/json5format" 13 | 14 | [dependencies] 15 | lazy_static = "1.4" 16 | regex = "1.5.6" 17 | 18 | [dev-dependencies] 19 | anyhow = "1.0.25" 20 | maplit = "1.0.1" 21 | proptest = "0.9.3" 22 | structopt = "0.2.14" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Fuchsia Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json5format 2 | 3 | **`json5format` is a general purpose Rust library that formats [JSON5](https://json5.org) (a.k.a., "JSON for Humans"), preserving contextual line and block comments.** 4 | 5 | [![crates.io](https://img.shields.io/crates/v/json5format.svg)](https://crates.io/crates/json5format) 6 | [![license](https://img.shields.io/badge/license-BSD3.0-blue.svg)](https://github.com/google/json5format/LICENSE) 7 | [![docs.rs](https://docs.rs/com/badge.svg)](https://docs.rs/crate/json5format/) 8 | ![json5format](https://github.com/google/json5format/workflows/json5format/badge.svg) 9 | 10 | ## `json5format` Rust library 11 | 12 | The [`json5format` library](https://crates.io/crates/json5format) includes APIs to customize the document format, with style options configurable both globally (affecting the entire document) as well as tailoring specific subsets of a target JSON5 schema. (See the [Rust package documentation](https://docs.rs/json5format/0.1.0/json5format) for more details and examples.) As of version 0.2.0, public APIs allow limited support for accessing the information inside a parsed document, and for injecting or modifying comments. 13 | ## `formatjson5` command line tool 14 | 15 | The `json5format` package also bundles an [example command line tool, `formatjson5`,](https://github.com/google/json5format/blob/master/examples/formatjson5.rs) that formats JSON5 documents using a basic style with some customizations available through command line options: 16 | 17 | ``` 18 | $ cargo build --example formatjson5 19 | $ ./target/debug/examples/formatjson5 --help 20 | 21 | formatjson5 [FLAGS] [OPTIONS] [files]... 22 | 23 | FLAGS: 24 | -h, --help Prints help information 25 | -n, --no_trailing_commas Suppress trailing commas (otherwise added by default) 26 | -o, --one_element_lines Objects or arrays with a single child should collapse to a 27 | single line; no trailing comma 28 | -r, --replace Replace (overwrite) the input file with the formatted result 29 | -s, --sort_arrays Sort arrays of primitive values (string, number, boolean, or 30 | null) lexicographically 31 | -V, --version Prints version information 32 | 33 | OPTIONS: 34 | -i, --indent Indent by the given number of spaces [default: 4] 35 | 36 | ARGS: 37 | ... Files to format (use "-" for stdin) 38 | ``` 39 | 40 | NOTE: This is not an officially supported Google product. 41 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or 19 | physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. 38 | If you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, 46 | who will receive and handle reports of code of conduct violations. In the event 47 | that a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is 55 | taken. The identity of the reporter will be omitted from the details of the 56 | report supplied to the accused. In potentially harmful situations, such as 57 | ongoing harassment or threats to anyone's safety, we may take action without 58 | notice. 59 | 60 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also 61 | be found at .* 62 | 63 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 64 | -------------------------------------------------------------------------------- /examples/formatjson5.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Google LLC All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //! A command line interface (CLI) tool to format [JSON5](https://json5.org) ("JSON for 6 | //! Humans") documents to a consistent style, preserving comments. 7 | //! 8 | //! See [json5format](../json5format/index.html) for more details. 9 | //! 10 | //! # Usage 11 | //! 12 | //! formatjson5 [FLAGS] [OPTIONS] [files]... 13 | //! 14 | //! FLAGS: 15 | //! -h, --help Prints help information 16 | //! -n, --no_trailing_commas Suppress trailing commas (otherwise added by default) 17 | //! -o, --one_element_lines Objects or arrays with a single child should collapse to a 18 | //! single line; no trailing comma 19 | //! -r, --replace Replace (overwrite) the input file with the formatted result 20 | //! -s, --sort_arrays Sort arrays of primitive values (string, number, boolean, or 21 | //! null) lexicographically 22 | //! -V, --version Prints version information 23 | //! 24 | //! OPTIONS: 25 | //! -i, --indent Indent by the given number of spaces [default: 4] 26 | //! 27 | //! ARGS: 28 | //! ... Files to format (use "-" for stdin) 29 | 30 | #![warn(missing_docs)] 31 | 32 | use anyhow::Result; 33 | use json5format::*; 34 | use std::fs; 35 | use std::io; 36 | use std::io::{Read, Write}; 37 | use std::path::PathBuf; 38 | use structopt::StructOpt; 39 | 40 | /// Parses each file in the given `files` vector and returns a parsed object for each JSON5 41 | /// document. If the parser encounters an error in any input file, the command aborts without 42 | /// formatting any of the documents. 43 | fn parse_documents(files: Vec) -> Result, anyhow::Error> { 44 | let mut parsed_documents = Vec::with_capacity(files.len()); 45 | for file in files { 46 | let filename = file.clone().into_os_string().to_string_lossy().to_string(); 47 | let mut buffer = String::new(); 48 | if filename == "-" { 49 | Opt::from_stdin(&mut buffer)?; 50 | } else { 51 | fs::File::open(&file)?.read_to_string(&mut buffer)?; 52 | } 53 | 54 | parsed_documents.push(ParsedDocument::from_string(buffer, Some(filename))?); 55 | } 56 | Ok(parsed_documents) 57 | } 58 | 59 | /// Formats the given parsed documents, applying the given format `options`. If `replace` is true, 60 | /// each input file is overwritten by its formatted version. 61 | fn format_documents( 62 | parsed_documents: Vec, 63 | options: FormatOptions, 64 | replace: bool, 65 | ) -> Result<(), anyhow::Error> { 66 | let format = Json5Format::with_options(options)?; 67 | for (index, parsed_document) in parsed_documents.iter().enumerate() { 68 | let filename = parsed_document.filename().as_ref().unwrap(); 69 | let bytes = format.to_utf8(parsed_document)?; 70 | if replace { 71 | Opt::write_to_file(filename, &bytes)?; 72 | } else { 73 | if index > 0 { 74 | println!(); 75 | } 76 | if parsed_documents.len() > 1 { 77 | println!("{}:", filename); 78 | println!("{}", "=".repeat(filename.len())); 79 | } 80 | print!("{}", std::str::from_utf8(&bytes)?); 81 | } 82 | } 83 | Ok(()) 84 | } 85 | 86 | /// The entry point for the [formatjson5](index.html) command line interface. 87 | fn main() -> Result<()> { 88 | let args = Opt::args(); 89 | 90 | if args.files.is_empty() { 91 | return Err(anyhow::anyhow!("No files to format")); 92 | } 93 | 94 | let parsed_documents = parse_documents(args.files)?; 95 | 96 | let options = FormatOptions { 97 | indent_by: args.indent, 98 | trailing_commas: !args.no_trailing_commas, 99 | collapse_containers_of_one: args.one_element_lines, 100 | sort_array_items: args.sort_arrays, 101 | ..Default::default() 102 | }; 103 | 104 | format_documents(parsed_documents, options, args.replace) 105 | } 106 | 107 | /// Command line options defined via the structopt! macrorule. These definitions generate the 108 | /// option parsing, validation, and [usage documentation](index.html). 109 | #[derive(Debug, StructOpt)] 110 | #[structopt( 111 | name = "json5format", 112 | about = "Format JSON5 documents to a consistent style, preserving comments." 113 | )] 114 | struct Opt { 115 | /// Files to format (use "-" for stdin) 116 | #[structopt(parse(from_os_str))] 117 | files: Vec, 118 | 119 | /// Replace (overwrite) the input file with the formatted result 120 | #[structopt(short, long)] 121 | replace: bool, 122 | 123 | /// Suppress trailing commas (otherwise added by default) 124 | #[structopt(short, long)] 125 | no_trailing_commas: bool, 126 | 127 | /// Objects or arrays with a single child should collapse to a single line; no trailing comma 128 | #[structopt(short, long)] 129 | one_element_lines: bool, 130 | 131 | /// Sort arrays of primitive values (string, number, boolean, or null) lexicographically 132 | #[structopt(short, long)] 133 | sort_arrays: bool, 134 | 135 | /// Indent by the given number of spaces 136 | #[structopt(short, long, default_value = "4")] 137 | indent: usize, 138 | } 139 | 140 | #[cfg(not(test))] 141 | impl Opt { 142 | fn args() -> Self { 143 | Self::from_args() 144 | } 145 | 146 | fn from_stdin(buf: &mut String) -> Result { 147 | io::stdin().read_to_string(buf) 148 | } 149 | 150 | fn write_to_file(filename: &str, bytes: &[u8]) -> Result<(), io::Error> { 151 | fs::OpenOptions::new() 152 | .create(true) 153 | .truncate(true) 154 | .write(true) 155 | .open(filename)? 156 | .write_all(bytes) 157 | } 158 | } 159 | 160 | #[cfg(test)] 161 | impl Opt { 162 | fn args() -> Self { 163 | if let Some(test_args) = unsafe { &self::tests::TEST_ARGS } { 164 | Self::from_clap( 165 | &Self::clap() 166 | .get_matches_from_safe(test_args) 167 | .expect("failed to parse TEST_ARGS command line arguments"), 168 | ) 169 | } else { 170 | Self::from_args() 171 | } 172 | } 173 | 174 | fn from_stdin(mut buf: &mut String) -> Result { 175 | if let Some(test_buffer) = unsafe { &mut self::tests::TEST_BUFFER } { 176 | *buf = test_buffer.clone(); 177 | Ok(buf.as_bytes().len()) 178 | } else { 179 | io::stdin().read_to_string(&mut buf) 180 | } 181 | } 182 | 183 | fn write_to_file(filename: &str, bytes: &[u8]) -> Result<(), io::Error> { 184 | if filename == "-" { 185 | let buf = std::str::from_utf8(&bytes) 186 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 187 | if let Some(test_buffer) = unsafe { &mut self::tests::TEST_BUFFER } { 188 | *test_buffer = buf.to_string(); 189 | } else { 190 | print!("{}", buf); 191 | } 192 | Ok(()) 193 | } else { 194 | fs::OpenOptions::new() 195 | .create(true) 196 | .truncate(true) 197 | .write(true) 198 | .open(filename)? 199 | .write_all(&bytes) 200 | } 201 | } 202 | } 203 | 204 | #[cfg(test)] 205 | mod tests { 206 | 207 | use super::*; 208 | 209 | pub(crate) static mut TEST_ARGS: Option> = None; 210 | pub(crate) static mut TEST_BUFFER: Option = None; 211 | 212 | #[test] 213 | fn test_main() { 214 | let example_json5 = r##"{ 215 | offer: [ 216 | { 217 | runner: "elf", 218 | }, 219 | { 220 | from: "framework", 221 | to: "#elements", 222 | protocol: "/svc/fuchsia.sys2.Realm", 223 | }, 224 | { 225 | to: "#elements", 226 | protocol: [ 227 | "/svc/fuchsia.logger.LogSink", 228 | "/svc/fuchsia.cobalt.LoggerFactory", 229 | ], 230 | from: "realm", 231 | }, 232 | ], 233 | collections: [ 234 | { 235 | name: "elements", 236 | durability: "transient", 237 | } 238 | ], 239 | use: [ 240 | { 241 | runner: "elf", 242 | }, 243 | { 244 | protocol: "/svc/fuchsia.sys2.Realm", 245 | from: "framework", 246 | }, 247 | { 248 | from: "realm", 249 | to: "#elements", 250 | protocol: [ 251 | "/svc/fuchsia.logger.LogSink", 252 | "/svc/fuchsia.cobalt.LoggerFactory", 253 | ], 254 | }, 255 | ], 256 | children: [ 257 | ], 258 | program: { 259 | args: [ "--zarg_first", "zoo_opt", "--arg3", "and_arg3_value" ], 260 | binary: "bin/session_manager", 261 | }, 262 | }"##; 263 | let expected = r##"{ 264 | offer: [ 265 | { runner: "elf" }, 266 | { 267 | from: "framework", 268 | to: "#elements", 269 | protocol: "/svc/fuchsia.sys2.Realm" 270 | }, 271 | { 272 | to: "#elements", 273 | protocol: [ 274 | "/svc/fuchsia.cobalt.LoggerFactory", 275 | "/svc/fuchsia.logger.LogSink" 276 | ], 277 | from: "realm" 278 | } 279 | ], 280 | collections: [ 281 | { 282 | name: "elements", 283 | durability: "transient" 284 | } 285 | ], 286 | use: [ 287 | { runner: "elf" }, 288 | { 289 | protocol: "/svc/fuchsia.sys2.Realm", 290 | from: "framework" 291 | }, 292 | { 293 | from: "realm", 294 | to: "#elements", 295 | protocol: [ 296 | "/svc/fuchsia.cobalt.LoggerFactory", 297 | "/svc/fuchsia.logger.LogSink" 298 | ] 299 | } 300 | ], 301 | children: [], 302 | program: { 303 | args: [ 304 | "--arg3", 305 | "--zarg_first", 306 | "and_arg3_value", 307 | "zoo_opt" 308 | ], 309 | binary: "bin/session_manager" 310 | } 311 | } 312 | "##; 313 | unsafe { 314 | TEST_ARGS = Some(vec![ 315 | "formatjson5", 316 | "--replace", 317 | "--no_trailing_commas", 318 | "--one_element_lines", 319 | "--sort_arrays", 320 | "--indent", 321 | "2", 322 | "-", 323 | ]); 324 | TEST_BUFFER = Some(example_json5.to_string()); 325 | } 326 | main().expect("test failed"); 327 | assert!(unsafe { &TEST_BUFFER }.is_some()); 328 | assert_eq!(unsafe { TEST_BUFFER.as_ref().unwrap() }, expected); 329 | } 330 | 331 | #[test] 332 | fn test_args() { 333 | let args = Opt::from_iter(vec![""].iter()); 334 | assert_eq!(args.files.len(), 0); 335 | assert_eq!(args.replace, false); 336 | assert_eq!(args.no_trailing_commas, false); 337 | assert_eq!(args.one_element_lines, false); 338 | assert_eq!(args.sort_arrays, false); 339 | assert_eq!(args.indent, 4); 340 | 341 | let some_filename = "some_file.json5"; 342 | let args = Opt::from_iter( 343 | vec!["formatjson5", "-r", "-n", "-o", "-s", "-i", "2", some_filename].iter(), 344 | ); 345 | assert_eq!(args.files.len(), 1); 346 | assert_eq!(args.replace, true); 347 | assert_eq!(args.no_trailing_commas, true); 348 | assert_eq!(args.one_element_lines, true); 349 | assert_eq!(args.sort_arrays, true); 350 | assert_eq!(args.indent, 2); 351 | 352 | let filename = args.files[0].clone().into_os_string().to_string_lossy().to_string(); 353 | assert_eq!(filename, some_filename); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "json5format-fuzz" 4 | version = "0.0.0" 5 | authors = ["David Korczynski "] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4.0" 14 | 15 | [dependencies.json5format] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "fuzz_parse" 24 | path = "fuzz_targets/fuzz_parse.rs" 25 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_parse.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use json5format::*; 4 | use std::str; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | if let Ok(utf8) = str::from_utf8(data) { 8 | ParsedDocument::from_str(utf8, None); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Fuchsia Format Style 2 | # last reviewed: Jan 29, 2019 3 | 4 | # Fuchsia uses 2018 edition only 5 | edition = "2018" 6 | 7 | # The "Default" setting has a heuristic which splits lines too aggresively. 8 | # We are willing to revisit this setting in future versions of rustfmt. 9 | # Bugs: 10 | # * https://github.com/rust-lang/rustfmt/issues/3119 11 | # * https://github.com/rust-lang/rustfmt/issues/3120 12 | use_small_heuristics = "Max" 13 | 14 | # Prevent carriage returns 15 | newline_style = "Unix" 16 | -------------------------------------------------------------------------------- /samples/fuzz_fails_fixed/clusterfuzz-testcase-minimized-fuzz_parse-4802677486780416: -------------------------------------------------------------------------------- 1 | ]} -------------------------------------------------------------------------------- /samples/fuzz_fails_fixed/clusterfuzz-testcase-minimized-fuzz_parse-4993106563956736: -------------------------------------------------------------------------------- 1 | { 2 | /*//*//*//*//*//*//*//*//*//@//*174105729,170141183460469231731687303715884105729,170141183460469231731687303715884//*//*//*//*//*//* 3 | 4 | //*//* 5 | 6 | 7 | 8 | //*14118346046923173168730//*/ -------------------------------------------------------------------------------- /samples/fuzz_fails_fixed/clusterfuzz-testcase-minimized-fuzz_parse-5734884822351872: -------------------------------------------------------------------------------- 1 | {//*} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | //{ //* 11 | 12 | 13 | //** 14 | 15 | //* 16 | 17 | //** 18 | 19 | //* 20 | 21 | 22 | //** 23 | 24 | //* 25 | 26 | //** 27 | 28 | //* 29 | 30 | 31 | //** 32 | 33 | //* 34 | 35 | //** 36 | 37 | //* 38 | 39 | 40 | //**Ozƌ 41 | 42 | // 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | //+ [** 51 | 52 | //* 53 | 54 | //** 55 | 56 | //*//* 57 | 58 | 59 | //** 60 | 61 | //* 62 | 63 | //** 64 | 65 | //* 66 | 67 | 68 | //** 69 | 70 | //* 71 | 72 | //** 73 | 74 | //* 75 | 76 | 77 | //** 78 | 79 | // 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | //+.zzz ':{'ƍIzzzzzzzzzz ':{'ƍIz!zzzzz; zzz ':{'ƍIzzRzzz ':{'ƍzzz ':{'ƍIzzz?,! ':{'ƍ zzz ':{'ƍIzzzz ':{'ƍzzz ':{'ƍzzz ':{'ƍIzzzzzz ':{'ƍIzzzzzzzz ':{'ƍIzzzzzzzz?, 17 | 18 | /// The input filename, if any. 19 | filename: Option, 20 | 21 | /// The parsed document model represented as an array of zero or more objects to format. 22 | pub content: Array, 23 | } 24 | 25 | impl ParsedDocument { 26 | /// Parses the JSON5 document represented by `buffer`, and returns a parsed representation of 27 | /// the document that can be formatted by 28 | /// [Json5Format::to_utf8()](struct.Json5Format.html#method.to_utf8). 29 | /// 30 | /// If a filename is also provided, any parsing errors will include the filename with the line 31 | /// number and column where the error was encountered. 32 | pub fn from_str(buffer: &str, filename: Option) -> Result { 33 | Self::from_str_with_nesting_limit(buffer, filename, Parser::DEFAULT_NESTING_LIMIT) 34 | } 35 | 36 | /// Like `from_str()` but also overrides the default nesting limit, used to 37 | /// catch deeply nested JSON5 documents before overflowing the program 38 | /// stack. 39 | pub fn from_str_with_nesting_limit( 40 | buffer: &str, 41 | filename: Option, 42 | nesting_limit: usize, 43 | ) -> Result { 44 | let mut parser = Parser::new(&filename); 45 | parser.set_nesting_limit(nesting_limit); 46 | let content = parser.parse(buffer)?; 47 | 48 | Ok(Self { owned_buffer: None, filename, content }) 49 | } 50 | 51 | /// Parses the JSON5 document represented by `buffer`, and returns a parsed representation of 52 | /// the document that can be formatted by 53 | /// [Json5Format::to_utf8()](struct.Json5Format.html#method.to_utf8). 54 | /// 55 | /// The returned `ParsedDocument` object retains ownership of the input buffer, which can be 56 | /// useful in situations where borrowing the buffer (via 57 | /// [from_str()](struct.ParsedDocument.html#method.from_str) requires burdensome workarounds. 58 | /// 59 | /// If a filename is also provided, any parsing errors will include the filename with the line 60 | /// number and column where the error was encountered. 61 | pub fn from_string(buffer: String, filename: Option) -> Result { 62 | let mut parser = Parser::new(&filename); 63 | let content = parser.parse(&buffer)?; 64 | 65 | Ok(Self { owned_buffer: Some(buffer), filename, content }) 66 | } 67 | 68 | /// Returns the filename, if provided when the object was created. 69 | pub fn filename(&self) -> &Option { 70 | &self.filename 71 | } 72 | 73 | /// Borrows the input buffer owned by this object, if provided by calling 74 | /// [from_string()](struct.ParsedDocument.html#method.from_string). 75 | pub fn input_buffer(&self) -> &Option { 76 | &self.owned_buffer 77 | } 78 | } 79 | 80 | /// Represents the variations of allowable comments. 81 | #[derive(Debug, Clone)] 82 | pub enum Comment { 83 | /// Represents a comment read from a `/* */` pattern. 84 | Block { 85 | /// The content of the block comment, represented as a `String` for each line. 86 | lines: Vec, 87 | /// `align` (if true) indicates that all comment `lines` started in a column after the 88 | /// star's column in the opening `/*`. For each subsequent line in lines, the spaces from 89 | /// column 0 to the star's column will be stripped, allowing the indent spaces to be 90 | /// restored, during format, relative to the block's new horizontal position. Otherwise, the 91 | /// original indentation will not be stripped, and the lines will be restored at their 92 | /// original horizontal position. In either case, lines after the opening `/*` will retain 93 | /// their original horizontal alignment, relative to one another. 94 | align: bool, 95 | }, 96 | 97 | /// Represents a comment read from a line starting with `//`. 98 | Line(String), 99 | 100 | /// Represents a blank line between data. 101 | Break, 102 | } 103 | 104 | impl Comment { 105 | /// Returns `true` if the `Comment` instance is a `Block` variant. 106 | pub fn is_block(&self) -> bool { 107 | matches!(self, Comment::Block { .. }) 108 | } 109 | 110 | /// Returns `true` if the `Comment` instance is a `Line` variant. 111 | #[allow(dead_code)] // for API consistency and tests even though enum is currently not `pub` 112 | pub fn is_line(&self) -> bool { 113 | matches!(self, Comment::Line(..)) 114 | } 115 | 116 | /// Returns `true` if the `Comment` instance is a `Break` variant. 117 | #[allow(dead_code)] // for API consistency and tests even though enum is currently not `pub` 118 | pub fn is_break(&self) -> bool { 119 | matches!(self, Comment::Break) 120 | } 121 | 122 | pub(crate) fn format<'a>( 123 | &self, 124 | formatter: &'a mut Formatter, 125 | ) -> Result<&'a mut Formatter, Error> { 126 | match self { 127 | Comment::Block { lines, align } => { 128 | let len = lines.len(); 129 | for (index, line) in lines.iter().enumerate() { 130 | let is_first = index == 0; 131 | let is_last = index == len - 1; 132 | if is_first { 133 | formatter.append(&format!("/*{}", line))?; 134 | } else if line.len() > 0 { 135 | formatter.append(&line.to_string())?; 136 | } 137 | if !is_last { 138 | if *align { 139 | formatter.start_next_line()?; 140 | } else { 141 | formatter.append_newline()?; 142 | } 143 | } 144 | } 145 | formatter.append("*/") 146 | } 147 | Comment::Line(comment) => formatter.append(&format!("//{}", comment)), 148 | Comment::Break => Ok(&mut *formatter), // inserts blank line only 149 | }?; 150 | formatter.start_next_line() 151 | } 152 | } 153 | 154 | /// A struct containing all comments associated with a specific `Value`. 155 | #[derive(Clone)] 156 | pub struct Comments { 157 | /// Comments applied to the associated value. 158 | before_value: Vec, 159 | 160 | /// A line comment positioned after and on the same line as the last character of the value. The 161 | /// comment may have multiple lines, if parsed as a contiguous group of line comments that are 162 | /// all left-aligned with the initial line comment. 163 | end_of_line_comment: Option, 164 | } 165 | 166 | impl Comments { 167 | /// Retrieves the comments immediately before an associated value. 168 | pub fn before_value(&self) -> &Vec { 169 | &self.before_value 170 | } 171 | 172 | /// Injects text into the end-of-line comment. 173 | pub fn append_end_of_line_comment(&mut self, comment: &str) -> Result<(), Error> { 174 | let updated = match self.end_of_line_comment.take() { 175 | None => comment.to_string(), 176 | Some(current) => current + "\n" + comment, 177 | }; 178 | self.end_of_line_comment = Some(updated); 179 | Ok(()) 180 | } 181 | 182 | /// Retrieves a reference to the end-of-line comment. 183 | pub fn end_of_line(&self) -> &Option { 184 | &self.end_of_line_comment 185 | } 186 | } 187 | 188 | /// A struct used for capturing comments at the end of an JSON5 array or object, which are not 189 | /// associated to any of the Values contained in the array/object. 190 | pub(crate) struct ContainedComments { 191 | /// Parsed comments to be applied to the next Value, when reached. 192 | /// If there are any pending comments after the last item, they are written after the last 193 | /// item when formatting. 194 | pending_comments: Vec, 195 | 196 | /// Immediately after capturing a Value (primitive or start of an object or array block), 197 | /// this is set to the new Value. If a line comment is captured *before* capturing a 198 | /// newline, the line comment is applied to the current_line_value. 199 | current_line_value: Option>>, 200 | 201 | /// If an end-of-line comment is captured after capturing a Value, this saves the column of the 202 | /// first character in the line comment. Successive line comments with the same start column 203 | /// are considered continuations of the end-of-line comment. 204 | end_of_line_comment_start_column: Option, 205 | } 206 | 207 | impl ContainedComments { 208 | fn new() -> Self { 209 | Self { 210 | pending_comments: vec![], 211 | current_line_value: None, 212 | end_of_line_comment_start_column: None, 213 | } 214 | } 215 | 216 | /// After parsing a value, if a newline is encountered before an end-of-line comment, the 217 | /// current line no longer has the value. 218 | fn on_newline(&mut self) -> Result<(), Error> { 219 | if self.end_of_line_comment_start_column.is_none() { 220 | self.current_line_value = None; 221 | } 222 | Ok(()) 223 | } 224 | 225 | /// Adds a standalone line comment to this container, or adds an end_of_line_comment to the 226 | /// current container's current value. 227 | /// 228 | /// # Arguments 229 | /// * `content`: the line comment content (including leading spaces) 230 | /// * `start_column`: the column number of the first character of content. If this line 231 | /// comment was immediately preceded by an end-of-line comment, and both line comments 232 | /// have the same start_column, then this line comment is a continuation of the end-of-line 233 | /// comment (on a new line). Formatting should retain the associated vertical alignment. 234 | /// * `pending_new_line_comment_block` - If true and the comment is not an 235 | /// end_of_line_comment, the container should insert a line_comment_break before inserting 236 | /// the next line comment. This should only be true if this standalone line comment was 237 | /// preceded by one or more standalone line comments and one or more blank lines. 238 | /// (This flag is ignored if the comment is part of an end-of-line comment.) 239 | /// 240 | /// # Returns 241 | /// true if the line comment is standalone, that is, not an end_of_line_comment 242 | fn add_line_comment( 243 | &mut self, 244 | content: &str, 245 | start_column: usize, 246 | pending_new_line_comment_block: bool, 247 | ) -> Result { 248 | if let Some(value_ref) = &mut self.current_line_value { 249 | if start_column == *self.end_of_line_comment_start_column.get_or_insert(start_column) { 250 | (*value_ref.borrow_mut()).comments_mut().append_end_of_line_comment(content)?; 251 | return Ok(false); // the comment is (part of) an end-of-line comment 252 | } 253 | self.current_line_value = None; 254 | } 255 | if pending_new_line_comment_block { 256 | self.pending_comments.push(Comment::Break); 257 | } 258 | self.pending_comments.push(Comment::Line(content.to_string())); 259 | Ok(true) 260 | } 261 | 262 | /// Add a block comment, to be applied to the next contained value, or to the end of the current 263 | /// container. 264 | fn add_block_comment(&mut self, comment: Comment) -> Result<(), Error> { 265 | self.current_line_value = None; 266 | self.pending_comments.push(comment); 267 | Ok(()) 268 | } 269 | 270 | /// There are one or more line and/or block comments to be applied to the next contained value, 271 | /// or to the end of the current container. 272 | fn has_pending_comments(&self) -> bool { 273 | !self.pending_comments.is_empty() 274 | } 275 | 276 | /// When a value is encountered inside the current container, move all pending comments from the 277 | /// container to the new value. 278 | fn take_pending_comments(&mut self) -> Vec { 279 | self.pending_comments.drain(..).collect() 280 | } 281 | } 282 | 283 | /// Represents the possible data types in a JSON5 object. Each variant has a field representing a 284 | /// specialized struct representing the value's data, and a field for comments (possibly including a 285 | /// line comment and comments appearing immediately before the value). For `Object` and `Array`, 286 | /// comments appearing at the end of the the structure are encapsulated inside the appropriate 287 | /// specialized struct. 288 | pub enum Value { 289 | /// Represents a non-recursive data type (string, bool, number, or "null") and its associated 290 | /// comments. 291 | Primitive { 292 | /// The struct containing the associated value. 293 | val: Primitive, 294 | /// The associated comments. 295 | comments: Comments, 296 | }, 297 | /// Represents a JSON5 array and its associated comments. 298 | Array { 299 | /// The struct containing the associated value. 300 | val: Array, 301 | /// The comments associated with the array. 302 | comments: Comments, 303 | }, 304 | /// Represents a JSON5 object and its associated comments. 305 | Object { 306 | /// The struct containing the associated value. 307 | val: Object, 308 | /// The comments associated with the object. 309 | comments: Comments, 310 | }, 311 | } 312 | 313 | impl Value { 314 | /// Returns `true` for an `Array` variant. 315 | pub fn is_array(&self) -> bool { 316 | matches!(self, Value::Array { .. }) 317 | } 318 | 319 | /// Returns `true` for an `Object` variant. 320 | pub fn is_object(&self) -> bool { 321 | matches!(self, Value::Object { .. }) 322 | } 323 | 324 | /// Returns `true` for a `Primitive` variant. 325 | pub fn is_primitive(&self) -> bool { 326 | matches!(self, Value::Primitive { .. }) 327 | } 328 | 329 | /// Instantiates a `Value::Array` with empty data and the provided comments. 330 | pub(crate) fn new_primitive(value_string: String, comments: Vec) -> Self { 331 | Self::Primitive { 332 | val: Primitive { value_string }, 333 | comments: Comments { before_value: comments, end_of_line_comment: None }, 334 | } 335 | } 336 | 337 | /// Instantiates a `Value::Array` with empty data and the provided comments. 338 | pub(crate) fn new_array(comments: Vec) -> Self { 339 | Self::Array { 340 | val: Array { 341 | items: vec![], 342 | is_parsing_value: false, 343 | contained_comments: ContainedComments::new(), 344 | }, 345 | comments: Comments { before_value: comments, end_of_line_comment: None }, 346 | } 347 | } 348 | 349 | /// Instantiates a `Value::Object` with empty data and the provided comments. 350 | pub(crate) fn new_object(comments: Vec) -> Self { 351 | Self::Object { 352 | val: Object { 353 | pending_property_name: None, 354 | properties: vec![], 355 | is_parsing_property: false, 356 | contained_comments: ContainedComments::new(), 357 | }, 358 | comments: Comments { before_value: comments, end_of_line_comment: None }, 359 | } 360 | } 361 | 362 | /// Recursively formats the data inside a `Value`. 363 | pub(crate) fn format<'a>( 364 | &self, 365 | formatter: &'a mut Formatter, 366 | ) -> Result<&'a mut Formatter, Error> { 367 | use Value::*; 368 | match self { 369 | Primitive { val, .. } => val.format(formatter), 370 | Array { val, .. } => val.format(formatter), 371 | Object { val, .. } => val.format(formatter), 372 | } 373 | } 374 | 375 | /// Retrieves an immutable reference to the `comments` attribute of any variant. 376 | pub fn comments(&self) -> &Comments { 377 | use Value::*; 378 | match self { 379 | Primitive { comments, .. } | Array { comments, .. } | Object { comments, .. } => { 380 | comments 381 | } 382 | } 383 | } 384 | /// Returns a mutable reference to the `comments` attribute of any variant. 385 | pub fn comments_mut(&mut self) -> &mut Comments { 386 | use Value::*; 387 | match self { 388 | Primitive { comments, .. } | Array { comments, .. } | Object { comments, .. } => { 389 | comments 390 | } 391 | } 392 | } 393 | 394 | /// Returns true if this value has any block, line, or end-of-line comment(s). 395 | pub fn has_comments(&mut self) -> bool { 396 | let comments = self.comments(); 397 | !comments.before_value().is_empty() || comments.end_of_line().is_some() 398 | } 399 | } 400 | 401 | impl std::fmt::Debug for Value { 402 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 403 | use Value::*; 404 | match self { 405 | Primitive { val, .. } => val.fmt(f), 406 | Array { val, .. } => val.fmt(f), 407 | Object { val, .. } => val.fmt(f), 408 | } 409 | } 410 | } 411 | 412 | /// Represents a primitive value in a JSON5 object property or array item. 413 | /// The parsed value is stored as a formatted string, retaining its original format, 414 | /// and written to the formatted document just as it appeared. 415 | pub struct Primitive { 416 | value_string: String, 417 | } 418 | 419 | impl Primitive { 420 | /// Returns the primitive value, as a formatted string. 421 | #[inline] 422 | pub fn as_str(&self) -> &str { 423 | &self.value_string 424 | } 425 | 426 | fn format<'a>(&self, formatter: &'a mut Formatter) -> Result<&'a mut Formatter, Error> { 427 | formatter.append(&self.value_string) 428 | } 429 | } 430 | 431 | impl std::fmt::Debug for Primitive { 432 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 433 | write!(f, "Primitive: {}", self.value_string) 434 | } 435 | } 436 | 437 | /// An interface that represents the recursive nature of `Object` and `Array`. 438 | pub(crate) trait Container { 439 | /// Called by the `Parser` to add a parsed `Value` to the current `Container`. 440 | fn add_value(&mut self, value: Rc>, parser: &Parser<'_>) -> Result<(), Error>; 441 | 442 | /// The parser encountered a comma, indicating the end of an element declaration. Since 443 | /// commas are optional, close() also indicates the end of a value without a trailing comma. 444 | fn end_value(&mut self, _parser: &Parser<'_>) -> Result<(), Error>; 445 | 446 | /// The parser encountered a closing brace indicating the end of the container's declaration. 447 | fn close(&mut self, _parser: &Parser<'_>) -> Result<(), Error>; 448 | 449 | /// Formats the content of a container (inside the braces) in accordance with the JSON5 syntax 450 | /// and given format options. 451 | fn format_content<'a>(&self, formatter: &'a mut Formatter) -> Result<&'a mut Formatter, Error>; 452 | 453 | /// Retrieves an immutable reference to the `contained_comments` attribute. 454 | fn contained_comments(&self) -> &ContainedComments; 455 | 456 | /// Retrieves a mutable reference to the `contained_comments` attribute. 457 | fn contained_comments_mut(&mut self) -> &mut ContainedComments; 458 | 459 | /// See `ContainedComments::on_newline`. 460 | fn on_newline(&mut self) -> Result<(), Error> { 461 | self.contained_comments_mut().on_newline() 462 | } 463 | 464 | /// See `ContainedComments::add_line_comment`. 465 | fn add_line_comment( 466 | &mut self, 467 | content: &str, 468 | start_column: usize, 469 | pending_new_line_comment_block: bool, 470 | ) -> Result { 471 | self.contained_comments_mut().add_line_comment( 472 | content, 473 | start_column, 474 | pending_new_line_comment_block, 475 | ) 476 | } 477 | 478 | /// See `ContainedComments::add_block_comment`. 479 | fn add_block_comment(&mut self, comment: Comment) -> Result<(), Error> { 480 | self.contained_comments_mut().add_block_comment(comment) 481 | } 482 | 483 | /// See `ContainedComments::has_pending_comments`. 484 | fn has_pending_comments(&self) -> bool { 485 | self.contained_comments().has_pending_comments() 486 | } 487 | 488 | /// See `ContainedComments::take_pending_comments`. 489 | fn take_pending_comments(&mut self) -> Vec { 490 | self.contained_comments_mut().take_pending_comments() 491 | } 492 | } 493 | 494 | /// Represents a JSON5 array of items. During parsing, this object's state changes, as comments and 495 | /// items are encountered. Parsed comments are temporarily stored in contained_comments, to be 496 | /// transferred to the next parsed item. After the last item, if any other comments are encountered, 497 | /// those comments are retained in the contained_comments field, to be restored during formatting, 498 | /// after writing the last item. 499 | pub struct Array { 500 | /// The array items. 501 | items: Vec>>, 502 | 503 | /// Set to true when a value is encountered (parsed primitive, or sub-container in process) 504 | /// and false when a comma or the array's closing brace is encountered. This supports 505 | /// validating that each array item is separated by one and only one comma. 506 | is_parsing_value: bool, 507 | 508 | /// Manages parsed comments inside the array scope, which are either transferred to each array 509 | /// item, or retained for placement after the last array item. 510 | contained_comments: ContainedComments, 511 | } 512 | 513 | impl Array { 514 | /// Returns an iterator over the array items. Items must be dereferenced to access 515 | /// the `Value`. For example: 516 | /// 517 | /// ``` 518 | /// use json5format::*; 519 | /// let parsed_document = ParsedDocument::from_str("{}", None)?; 520 | /// for item in parsed_document.content.items() { 521 | /// assert!(!(*item).is_primitive()); 522 | /// } 523 | /// # Ok::<(),anyhow::Error>(()) 524 | /// ``` 525 | pub fn items(&self) -> impl Iterator> { 526 | self.items.iter().map(|rc| rc.borrow()) 527 | } 528 | 529 | /// As in `Array::items`, returns an iterator over the array items, but with mutable references. 530 | #[inline] 531 | pub fn items_mut(&mut self) -> impl Iterator> { 532 | self.items.iter_mut().map(|rc| rc.borrow_mut()) 533 | } 534 | 535 | /// Returns a reference to the comments at the end of an array not associated with any values. 536 | #[inline] 537 | pub fn trailing_comments(&self) -> &Vec { 538 | &self.contained_comments.pending_comments 539 | } 540 | 541 | /// Returns a mutable reference to the comments at the end of an array not associated with any 542 | /// values. 543 | #[inline] 544 | pub fn trailing_comments_mut(&mut self) -> &mut Vec { 545 | &mut self.contained_comments.pending_comments 546 | } 547 | 548 | /// Returns a cloned vector of item references in sorted order. The items owned by this Array 549 | /// retain their original order. 550 | fn sort_items(&self, options: &FormatOptions) -> Vec>> { 551 | let mut items = self.items.clone(); 552 | if options.sort_array_items { 553 | items.sort_by(|left, right| { 554 | let left: &Value = &left.borrow(); 555 | let right: &Value = &right.borrow(); 556 | if let Value::Primitive { val: left_primitive, .. } = left { 557 | if let Value::Primitive { val: right_primitive, .. } = right { 558 | let mut ordering = left_primitive 559 | .value_string 560 | .to_lowercase() 561 | .cmp(&right_primitive.value_string.to_lowercase()); 562 | // If two values are case-insensitively equal, compare them again with 563 | // case-sensitivity to ensure consistent re-ordering. 564 | if ordering == Ordering::Equal { 565 | ordering = 566 | left_primitive.value_string.cmp(&right_primitive.value_string); 567 | } 568 | ordering 569 | } else { 570 | Ordering::Equal 571 | } 572 | } else { 573 | Ordering::Equal 574 | } 575 | }); 576 | } 577 | items 578 | } 579 | 580 | fn format<'a>(&self, formatter: &'a mut Formatter) -> Result<&'a mut Formatter, Error> { 581 | formatter.format_container("[", "]", |formatter| self.format_content(formatter)) 582 | } 583 | } 584 | 585 | impl Container for Array { 586 | fn add_value(&mut self, value: Rc>, parser: &Parser<'_>) -> Result<(), Error> { 587 | if self.is_parsing_value { 588 | Err(parser.error("Array items must be separated by a comma")) 589 | } else { 590 | self.is_parsing_value = true; 591 | self.contained_comments.current_line_value = Some(value.clone()); 592 | self.contained_comments.end_of_line_comment_start_column = None; 593 | self.items.push(value); 594 | Ok(()) 595 | } 596 | } 597 | 598 | fn end_value(&mut self, parser: &Parser<'_>) -> Result<(), Error> { 599 | if self.is_parsing_value { 600 | self.is_parsing_value = false; 601 | Ok(()) 602 | } else { 603 | Err(parser.error("Unexpected comma without a preceding array item value")) 604 | } 605 | } 606 | 607 | fn close(&mut self, _parser: &Parser<'_>) -> Result<(), Error> { 608 | Ok(()) 609 | } 610 | 611 | fn format_content<'a>(&self, formatter: &'a mut Formatter) -> Result<&'a mut Formatter, Error> { 612 | let sorted_items = self.sort_items(&formatter.options_in_scope()); 613 | let len = sorted_items.len(); 614 | for (index, item) in sorted_items.iter().enumerate() { 615 | let is_first = index == 0; 616 | let is_last = index == len - 1; 617 | formatter.format_item( 618 | item, 619 | is_first, 620 | is_last, 621 | self.contained_comments.has_pending_comments(), 622 | )?; 623 | } 624 | 625 | formatter.format_trailing_comments(&self.contained_comments.pending_comments) 626 | } 627 | 628 | fn contained_comments(&self) -> &ContainedComments { 629 | &self.contained_comments 630 | } 631 | 632 | fn contained_comments_mut(&mut self) -> &mut ContainedComments { 633 | &mut self.contained_comments 634 | } 635 | } 636 | 637 | impl std::fmt::Debug for Array { 638 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 639 | write!( 640 | f, 641 | "Array of {} item{}", 642 | self.items.len(), 643 | if self.items.len() == 1 { "" } else { "s" } 644 | ) 645 | } 646 | } 647 | 648 | /// Represents a name-value pair for a field in a JSON5 object. 649 | #[derive(Clone)] 650 | pub struct Property { 651 | /// An unquoted or quoted property name. If unquoted, the name must match the 652 | /// UNQUOTED_PROPERTY_NAME_PATTERN. 653 | pub(crate) name: String, 654 | 655 | /// The property value. 656 | pub(crate) value: Rc>, 657 | } 658 | 659 | impl Property { 660 | /// Returns a new instance of a `Property` with the name provided as a `String` 661 | /// and value provided as indirection to a `Value`. 662 | pub(crate) fn new(name: String, value: Rc>) -> Self { 663 | Property { name, value } 664 | } 665 | 666 | /// An unquoted or quoted property name. If unquoted, JSON5 property 667 | /// names comply with the ECMAScript 5.1 `IdentifierName` requirements. 668 | #[inline] 669 | pub fn name(&self) -> &str { 670 | &self.name 671 | } 672 | 673 | /// Returns a `Ref` to the property's value, which can be accessed by dereference, 674 | /// for example: `(*some_prop.value()).is_primitive()`. 675 | #[inline] 676 | pub fn value(&self) -> Ref<'_, Value> { 677 | self.value.borrow() 678 | } 679 | 680 | /// Returns a `RefMut` to the property's value, which can be accessed by dereference, 681 | /// for example: `(*some_prop.value()).is_primitive()`. 682 | #[inline] 683 | pub fn value_mut(&mut self) -> RefMut<'_, Value> { 684 | self.value.borrow_mut() 685 | } 686 | } 687 | 688 | impl std::fmt::Debug for Property { 689 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 690 | write!(f, "Property {}: {:?}", self.name, self.value) 691 | } 692 | } 693 | 694 | /// A specialized struct to represent the data of JSON5 object, including any comments placed at 695 | /// the end of the object. 696 | pub struct Object { 697 | /// Parsed property name to be applied to the next upcoming Value. 698 | pending_property_name: Option, 699 | 700 | /// Properties of this object. 701 | properties: Vec, 702 | 703 | /// Set to true when a value is encountered (parsed primitive, or sub-container in process) 704 | /// and false when a comma or the object's closing brace is encountered. This supports 705 | /// validating that each property is separated by one and only one comma. 706 | is_parsing_property: bool, 707 | 708 | /// Manages parsed comments inside the object scope, which are either transferred to each object 709 | /// item, or retained for placement after the last object item. 710 | contained_comments: ContainedComments, 711 | } 712 | 713 | impl Object { 714 | /// Retrieves an iterator from the `properties` field. 715 | #[inline] 716 | pub fn properties(&self) -> impl Iterator { 717 | self.properties.iter() 718 | } 719 | 720 | /// Retrieves an iterator of mutable references from the `properties` field. 721 | #[inline] 722 | pub fn properties_mut(&mut self) -> impl Iterator { 723 | self.properties.iter_mut() 724 | } 725 | 726 | /// Returns a reference to the comments at the end of an object not associated with any values. 727 | #[inline] 728 | pub fn trailing_comments(&self) -> &Vec { 729 | &self.contained_comments.pending_comments 730 | } 731 | 732 | /// Returns a mutable reference to the comments at the end of an object not associated with any 733 | /// values. 734 | #[inline] 735 | pub fn trailing_comments_mut(&mut self) -> &mut Vec { 736 | &mut self.contained_comments.pending_comments 737 | } 738 | /// The given property name was parsed. Once it's value is also parsed, the property will be 739 | /// added to this `Object`. 740 | /// 741 | /// # Arguments 742 | /// * name - the property name, possibly quoted 743 | /// * parser - reference to the current state of the parser 744 | pub(crate) fn set_pending_property( 745 | &mut self, 746 | name: String, 747 | parser: &Parser<'_>, 748 | ) -> Result<(), Error> { 749 | self.contained_comments.current_line_value = None; 750 | if self.is_parsing_property { 751 | Err(parser.error("Properties must be separated by a comma")) 752 | } else { 753 | self.is_parsing_property = true; 754 | match &self.pending_property_name { 755 | Some(property_name) => Err(Error::internal( 756 | parser.location(), 757 | format!( 758 | "Unexpected property '{}' encountered before completing the previous \ 759 | property '{}'", 760 | name, property_name 761 | ), 762 | )), 763 | None => { 764 | self.pending_property_name = Some(name.to_string()); 765 | Ok(()) 766 | } 767 | } 768 | } 769 | } 770 | 771 | /// Returns true if a property name has been parsed, and the parser has not yet reached a value. 772 | pub(crate) fn has_pending_property(&mut self) -> Result { 773 | Ok(self.pending_property_name.is_some()) 774 | } 775 | 776 | /// Returns a cloned vector of property references in sorted order. The properties owned by 777 | /// this Object retain their original order. 778 | fn sort_properties(&self, options: &SubpathOptions) -> Vec { 779 | let mut properties = self.properties.clone(); 780 | properties.sort_by(|left, right| { 781 | options 782 | .get_property_priority(&left.name) 783 | .cmp(&options.get_property_priority(&right.name)) 784 | }); 785 | properties 786 | } 787 | 788 | fn format<'a>(&self, formatter: &'a mut Formatter) -> Result<&'a mut Formatter, Error> { 789 | formatter.format_container("{", "}", |formatter| self.format_content(formatter)) 790 | } 791 | } 792 | 793 | impl Container for Object { 794 | fn add_value(&mut self, value: Rc>, parser: &Parser<'_>) -> Result<(), Error> { 795 | match self.pending_property_name.take() { 796 | Some(name) => { 797 | self.contained_comments.current_line_value = Some(value.clone()); 798 | self.contained_comments.end_of_line_comment_start_column = None; 799 | self.properties.push(Property::new(name, value)); 800 | Ok(()) 801 | } 802 | None => Err(parser.error("Object values require property names")), 803 | } 804 | } 805 | 806 | fn end_value(&mut self, parser: &Parser<'_>) -> Result<(), Error> { 807 | match &self.pending_property_name { 808 | Some(property_name) => Err(parser.error(format!( 809 | "Property '{}' must have a value before the next comma-separated property", 810 | property_name 811 | ))), 812 | None => { 813 | if self.is_parsing_property { 814 | self.is_parsing_property = false; 815 | Ok(()) 816 | } else { 817 | Err(parser.error("Unexpected comma without a preceding property")) 818 | } 819 | } 820 | } 821 | } 822 | 823 | fn close(&mut self, parser: &Parser<'_>) -> Result<(), Error> { 824 | match &self.pending_property_name { 825 | Some(property_name) => Err(parser.error(format!( 826 | "Property '{}' must have a value before closing an object", 827 | property_name 828 | ))), 829 | None => Ok(()), 830 | } 831 | } 832 | 833 | fn format_content<'a>(&self, formatter: &'a mut Formatter) -> Result<&'a mut Formatter, Error> { 834 | let sorted_properties = formatter 835 | .get_current_subpath_options() 836 | .map(|options| self.sort_properties(&options.borrow())); 837 | let properties = match &sorted_properties { 838 | Some(sorted_properties) => sorted_properties, 839 | None => &self.properties, 840 | }; 841 | 842 | let len = properties.len(); 843 | for (index, property) in properties.iter().enumerate() { 844 | let is_first = index == 0; 845 | let is_last = index == len - 1; 846 | formatter.format_property( 847 | property, 848 | is_first, 849 | is_last, 850 | self.contained_comments.has_pending_comments(), 851 | )?; 852 | } 853 | 854 | formatter.format_trailing_comments(&self.contained_comments.pending_comments) 855 | } 856 | 857 | fn contained_comments(&self) -> &ContainedComments { 858 | &self.contained_comments 859 | } 860 | 861 | fn contained_comments_mut(&mut self) -> &mut ContainedComments { 862 | &mut self.contained_comments 863 | } 864 | } 865 | 866 | impl std::fmt::Debug for Object { 867 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 868 | write!( 869 | f, 870 | "Object of {} propert{}", 871 | self.properties.len(), 872 | if self.properties.len() == 1 { "y" } else { "ies" } 873 | ) 874 | } 875 | } 876 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Google LLC All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #![deny(missing_docs)] 6 | 7 | /// A location within a document buffer or document file. This module uses `Location` to identify 8 | /// to refer to locations of JSON5 syntax errors, while parsing) and also to locations in this Rust 9 | /// source file, to improve unit testing output. 10 | pub struct Location { 11 | /// The name of the JSON5 document file being parsed and formatted (if provided). 12 | pub file: Option, 13 | 14 | /// A line number within the JSON5 document. (The first line at the top of the document/file is 15 | /// line 1.) 16 | pub line: usize, 17 | 18 | /// A character column number within the specified line. (The left-most character of the line is 19 | /// column 1). 20 | pub col: usize, 21 | } 22 | 23 | impl Location { 24 | /// Create a new `Location` for the given source document location. 25 | pub fn new(file: Option, line: usize, col: usize) -> Self { 26 | Location { file, line, col } 27 | } 28 | } 29 | 30 | impl std::fmt::Display for Location { 31 | fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | if let Some(file) = &self.file { 33 | write!(formatter, "{}:{}:{}", file, self.line, self.col) 34 | } else { 35 | write!(formatter, "{}:{}", self.line, self.col) 36 | } 37 | } 38 | } 39 | 40 | impl std::fmt::Debug for Location { 41 | fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | write!(formatter, "{}", &self) 43 | } 44 | } 45 | 46 | /// Errors produced by the json5format library. 47 | #[derive(Debug)] 48 | pub enum Error { 49 | /// A formatter configuration option was invalid. 50 | Configuration(String), 51 | 52 | /// A syntax error was encountered while parsing a JSON5 document. 53 | Parse(Option, String), 54 | 55 | /// The parser or formatter entered an unexpected state. An `Error::Internal` likely indicates 56 | /// there is a software bug in the json5format library. 57 | Internal(Option, String), 58 | 59 | /// This error is only produced by internal test functions to indicate a test result did not 60 | /// match expectations. 61 | TestFailure(Option, String), 62 | } 63 | 64 | impl std::error::Error for Error {} 65 | 66 | impl Error { 67 | /// Return a configuration error. 68 | /// # Arguments 69 | /// * err - The error message. 70 | pub fn configuration(err: impl std::fmt::Display) -> Self { 71 | Error::Configuration(err.to_string()) 72 | } 73 | 74 | /// Return a parsing error. 75 | /// # Arguments 76 | /// * location - Optional location in the JSON5 document where the error was detected. 77 | /// * err - The error message. 78 | pub fn parse(location: Option, err: impl std::fmt::Display) -> Self { 79 | Error::Parse(location, err.to_string()) 80 | } 81 | 82 | /// Return an internal error (indicating an error in the software implementation itself). 83 | /// # Arguments 84 | /// * location - Optional location in the JSON5 document where the error was detected, 85 | /// which might be available if the error occurred while parsing the document. 86 | /// * err - The error message. 87 | pub fn internal(location: Option, err: impl Into) -> Self { 88 | Error::Internal(location, err.into()) 89 | } 90 | 91 | /// Return a TestFailure error. 92 | /// # Arguments 93 | /// * location - Optional Rust source code location where the test failed. 94 | /// * err - The error message. 95 | pub fn test_failure(location: Option, err: impl Into) -> Self { 96 | Error::TestFailure(location, err.into()) 97 | } 98 | } 99 | 100 | impl std::fmt::Display for Error { 101 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 | let (prefix, loc, err) = match &self { 103 | Error::Configuration(err) => ("Configuration error", &None, err.to_string()), 104 | Error::Parse(loc, err) => ("Parse error", loc, err.to_string()), 105 | Error::Internal(loc, err) => ("Internal error", loc, err.to_string()), 106 | Error::TestFailure(loc, err) => ("Test failure", loc, err.to_string()), 107 | }; 108 | match loc { 109 | Some(loc) => write!(f, "{}: {}: {}", prefix, loc, err), 110 | None => write!(f, "{}: {}", prefix, err), 111 | } 112 | } 113 | } 114 | 115 | /// Create a `TestFailure` error including the source file location of the macro call. 116 | /// 117 | /// # Example: 118 | /// 119 | /// ```no_run 120 | /// # use json5format::Error; 121 | /// # use json5format::Location; 122 | /// # use json5format::test_error; 123 | /// # fn test() -> std::result::Result<(),Error> { 124 | /// return Err(test_error!("error message")); 125 | /// # } 126 | /// # test(); 127 | /// ``` 128 | #[macro_export] 129 | macro_rules! test_error { 130 | ($err:expr) => { 131 | Error::test_failure( 132 | Some(Location::new(Some(file!().to_string()), line!() as usize, column!() as usize)), 133 | $err, 134 | ) 135 | }; 136 | } 137 | -------------------------------------------------------------------------------- /src/formatter.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Google LLC All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #![deny(missing_docs)] 6 | use { 7 | crate::{content::*, error::*, options::*}, 8 | std::cell::RefCell, 9 | std::collections::HashMap, 10 | std::collections::HashSet, 11 | std::rc::Rc, 12 | }; 13 | 14 | pub(crate) struct SubpathOptions { 15 | /// Options for the matching property name, including subpath-options for nested containers. 16 | /// If matched, these options apply exclusively; the `options_for_next_level` will not apply. 17 | subpath_options_by_name: HashMap>>, 18 | 19 | /// Options for nested containers under any array item, or any property not matching a property 20 | /// name in `subpath_options_by_name`. 21 | unnamed_subpath_options: Option>>, 22 | 23 | /// The options that override the default FormatOptions (those passed to `with_options()`) for 24 | /// the matched path. 25 | pub options: FormatOptions, 26 | 27 | /// A map of property names to priority values, for sorting properties at the matched path. 28 | property_name_priorities: HashMap<&'static str, usize>, 29 | } 30 | 31 | impl SubpathOptions { 32 | /// Properties without an explicit priority will be sorted after prioritized properties and 33 | /// retain their original order with respect to any other unpriorized properties. 34 | const NO_PRIORITY: usize = std::usize::MAX; 35 | 36 | pub fn new(default_options: &FormatOptions) -> Self { 37 | Self { 38 | subpath_options_by_name: HashMap::new(), 39 | unnamed_subpath_options: None, 40 | options: default_options.clone(), 41 | property_name_priorities: HashMap::new(), 42 | } 43 | } 44 | 45 | pub fn override_default_options(&mut self, path_options: &HashSet) { 46 | for path_option in path_options.iter() { 47 | use PathOption::*; 48 | match path_option { 49 | TrailingCommas(path_value) => self.options.trailing_commas = *path_value, 50 | CollapseContainersOfOne(path_value) => { 51 | self.options.collapse_containers_of_one = *path_value 52 | } 53 | SortArrayItems(path_value) => self.options.sort_array_items = *path_value, 54 | PropertyNameOrder(property_names) => { 55 | for (index, property_name) in property_names.iter().enumerate() { 56 | self.property_name_priorities.insert(property_name, index); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | pub fn get_or_create_subpath_options( 64 | &mut self, 65 | path: &[&str], 66 | default_options: &FormatOptions, 67 | ) -> Rc> { 68 | let name_or_star = path[0]; 69 | let remaining_path = &path[1..]; 70 | let subpath_options_ref = if name_or_star == "*" { 71 | self.unnamed_subpath_options.as_ref() 72 | } else { 73 | self.subpath_options_by_name.get(name_or_star) 74 | }; 75 | let subpath_options = match subpath_options_ref { 76 | Some(existing_options) => existing_options.clone(), 77 | None => { 78 | let new_options = Rc::new(RefCell::new(SubpathOptions::new(default_options))); 79 | if name_or_star == "*" { 80 | self.unnamed_subpath_options = Some(new_options.clone()); 81 | } else { 82 | self.subpath_options_by_name 83 | .insert(name_or_star.to_string(), new_options.clone()); 84 | } 85 | new_options 86 | } 87 | }; 88 | if remaining_path.is_empty() { 89 | subpath_options 90 | } else { 91 | (*subpath_options.borrow_mut()) 92 | .get_or_create_subpath_options(remaining_path, default_options) 93 | } 94 | } 95 | 96 | fn get_subpath_options(&self, path: &[&str]) -> Option>> { 97 | let name_or_star = path[0]; 98 | let remaining_path = &path[1..]; 99 | let subpath_options_ref = if name_or_star == "*" { 100 | self.unnamed_subpath_options.as_ref() 101 | } else { 102 | self.subpath_options_by_name.get(name_or_star) 103 | }; 104 | if let Some(subpath_options) = subpath_options_ref { 105 | if remaining_path.is_empty() { 106 | Some(subpath_options.clone()) 107 | } else { 108 | (*subpath_options.borrow()).get_subpath_options(remaining_path) 109 | } 110 | } else { 111 | None 112 | } 113 | } 114 | 115 | fn get_options_for(&self, name_or_star: &str) -> Option>> { 116 | self.get_subpath_options(&[name_or_star]) 117 | } 118 | 119 | pub fn get_property_priority(&self, property_name: &str) -> usize { 120 | match self.property_name_priorities.get(property_name) { 121 | Some(priority) => *priority, 122 | None => SubpathOptions::NO_PRIORITY, 123 | } 124 | } 125 | 126 | fn debug_format( 127 | &self, 128 | formatter: &mut std::fmt::Formatter<'_>, 129 | indent: &str, 130 | ) -> std::fmt::Result { 131 | writeln!(formatter, "{{")?; 132 | let next_indent = indent.to_owned() + " "; 133 | writeln!(formatter, "{}options = {:?}", &next_indent, self.options)?; 134 | writeln!( 135 | formatter, 136 | "{}property_name_priorities = {:?}", 137 | &next_indent, self.property_name_priorities 138 | )?; 139 | if let Some(unnamed_subpath_options) = &self.unnamed_subpath_options { 140 | write!(formatter, "{}* = ", &next_indent)?; 141 | (*unnamed_subpath_options.borrow()).debug_format(formatter, &next_indent)?; 142 | writeln!(formatter)?; 143 | } 144 | for (property_name, subpath_options) in self.subpath_options_by_name.iter() { 145 | write!(formatter, "{}{} = ", &next_indent, property_name)?; 146 | (*subpath_options.borrow()).debug_format(formatter, &next_indent)?; 147 | writeln!(formatter)?; 148 | } 149 | writeln!(formatter, "{}}}", &indent) 150 | } 151 | } 152 | 153 | impl std::fmt::Debug for SubpathOptions { 154 | fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 155 | self.debug_format(formatter, "") 156 | } 157 | } 158 | 159 | /// A JSON5 formatter that produces formatted JSON5 document content from a JSON5 `ParsedDocument`. 160 | pub(crate) struct Formatter { 161 | /// The current depth of the partially-generated document while formatting. Each nexted array or 162 | /// object increases the depth by 1. After the formatted array or object has been generated, the 163 | /// depth decreases by 1. 164 | depth: usize, 165 | 166 | /// The next value to be written should be indented. 167 | pending_indent: bool, 168 | 169 | /// The UTF-8 bytes of the output document as it is being generated. 170 | bytes: Vec, 171 | 172 | /// The 1-based column number of the next character to be appended. 173 | column: usize, 174 | 175 | /// While generating the formatted document, these are the options to be applied at each nesting 176 | /// depth and path, from the document root to the object or array currently being generated. If 177 | /// the current path has no explicit options, the value at the top of the stack is None. 178 | subpath_options_stack: Vec>>>, 179 | 180 | /// Options that alter how the formatter generates the formatted output. This instance of 181 | /// FormatOptions is a subset of the FormatOptions passed to the `with_options` constructor. 182 | /// The `options_by_path` are first removed, and then used to initialize the SubpathOptions 183 | /// hierarchy rooted at the `document_root_options_ref`. 184 | default_options: FormatOptions, 185 | } 186 | 187 | impl Formatter { 188 | /// Create and return a Formatter, with the given options to be applied to the 189 | /// [Json5Format::to_utf8()](struct.Json5Format.html#method.to_utf8) operation. 190 | pub fn new( 191 | default_options: FormatOptions, 192 | document_root_options_ref: Rc>, 193 | ) -> Self { 194 | Formatter { 195 | depth: 0, 196 | pending_indent: false, 197 | bytes: vec![], 198 | column: 1, 199 | subpath_options_stack: vec![Some(document_root_options_ref)], 200 | default_options, 201 | } 202 | } 203 | 204 | pub fn increase_indent(&mut self) -> Result<&mut Formatter, Error> { 205 | self.depth += 1; 206 | Ok(self) 207 | } 208 | 209 | pub fn decrease_indent(&mut self) -> Result<&mut Formatter, Error> { 210 | self.depth -= 1; 211 | Ok(self) 212 | } 213 | 214 | /// Appends the given string, indenting if required. 215 | pub fn append(&mut self, content: &str) -> Result<&mut Formatter, Error> { 216 | if self.pending_indent && !content.starts_with('\n') { 217 | let spaces = self.depth * self.default_options.indent_by; 218 | self.bytes.extend_from_slice(" ".repeat(spaces).as_bytes()); 219 | self.column = spaces + 1; 220 | self.pending_indent = false; 221 | } 222 | if content.ends_with('\n') { 223 | self.column = 1; 224 | self.bytes.extend_from_slice(content.as_bytes()); 225 | } else { 226 | let mut first = true; 227 | for line in content.lines() { 228 | if !first { 229 | self.bytes.extend_from_slice("\n".as_bytes()); 230 | self.column = 1; 231 | } 232 | self.bytes.extend_from_slice(line.as_bytes()); 233 | self.column += line.len(); 234 | first = false; 235 | } 236 | } 237 | Ok(self) 238 | } 239 | 240 | pub fn append_newline(&mut self) -> Result<&mut Formatter, Error> { 241 | self.append("\n") 242 | } 243 | 244 | /// Outputs a newline (unless this is the first line), and sets the `pending_indent` flag to 245 | /// indicate the next non-blank line should be indented. 246 | pub fn start_next_line(&mut self) -> Result<&mut Formatter, Error> { 247 | if !self.bytes.is_empty() { 248 | self.append_newline()?; 249 | } 250 | self.pending_indent = true; 251 | Ok(self) 252 | } 253 | 254 | fn format_content(&mut self, content_fn: F) -> Result<&mut Formatter, Error> 255 | where 256 | F: FnOnce(&mut Formatter) -> Result<&mut Formatter, Error>, 257 | { 258 | content_fn(self) 259 | } 260 | 261 | pub fn format_container( 262 | &mut self, 263 | left_brace: &str, 264 | right_brace: &str, 265 | content_fn: F, 266 | ) -> Result<&mut Formatter, Error> 267 | where 268 | F: FnOnce(&mut Formatter) -> Result<&mut Formatter, Error>, 269 | { 270 | self.append(left_brace)? 271 | .increase_indent()? 272 | .format_content(content_fn)? 273 | .decrease_indent()? 274 | .append(right_brace) 275 | } 276 | 277 | fn format_comments_internal( 278 | &mut self, 279 | comments: &[Comment], 280 | leading_blank_line: bool, 281 | ) -> Result<&mut Formatter, Error> { 282 | let mut previous: Option<&Comment> = None; 283 | for comment in comments.iter() { 284 | match previous { 285 | Some(previous) => { 286 | if comment.is_block() || previous.is_block() { 287 | // Separate block comments and contiguous line comments. 288 | // Use append_newline() instead of start_next_line() because block comment 289 | // lines after the first line append their own indentation spaces. 290 | self.append_newline()?; 291 | } 292 | } 293 | None => { 294 | if leading_blank_line { 295 | self.start_next_line()?; 296 | } 297 | } 298 | } 299 | comment.format(self)?; 300 | previous = Some(comment) 301 | } 302 | Ok(self) 303 | } 304 | 305 | pub fn format_comments( 306 | &mut self, 307 | comments: &[Comment], 308 | is_first: bool, 309 | ) -> Result<&mut Formatter, Error> { 310 | self.format_comments_internal(comments, !is_first) 311 | } 312 | 313 | pub fn format_trailing_comments( 314 | &mut self, 315 | comments: &[Comment], 316 | ) -> Result<&mut Formatter, Error> { 317 | self.format_comments_internal(comments, true) 318 | } 319 | 320 | pub fn get_current_subpath_options(&self) -> Option<&Rc>> { 321 | self.subpath_options_stack.last().unwrap().as_ref() 322 | } 323 | 324 | fn enter_scope(&mut self, name_or_star: &str) { 325 | let mut subpath_options_to_push = None; 326 | if let Some(current_subpath_options_ref) = self.get_current_subpath_options() { 327 | let current_subpath_options = &*current_subpath_options_ref.borrow(); 328 | if let Some(next_subpath_options_ref) = 329 | current_subpath_options.get_options_for(name_or_star) 330 | { 331 | // SubpathOptions were explicitly provided for: 332 | // * the given property name in the current object; or 333 | // * all array items within the current array (as indicated by "*") 334 | subpath_options_to_push = Some(next_subpath_options_ref.clone()); 335 | } else if name_or_star != "*" { 336 | if let Some(next_subpath_options_ref) = current_subpath_options.get_options_for("*") 337 | { 338 | // `name_or_star` was a property name, and SubpathOptions for this path were 339 | // _not_ explicitly defined for this name. In this case, a Subpath defined with 340 | // "*" at this Subpath location, if provided, matches any property name in the 341 | // current object (like a wildcard). 342 | subpath_options_to_push = Some(next_subpath_options_ref.clone()); 343 | } 344 | } 345 | } 346 | self.subpath_options_stack.push(subpath_options_to_push); 347 | } 348 | 349 | fn exit_scope(&mut self) { 350 | self.subpath_options_stack.pop(); 351 | } 352 | 353 | fn format_scoped_value( 354 | &mut self, 355 | name: Option<&str>, 356 | value: &mut Value, 357 | is_first: bool, 358 | is_last: bool, 359 | container_has_pending_comments: bool, 360 | ) -> Result<&mut Formatter, Error> { 361 | let collapsed = is_first 362 | && is_last 363 | && value.is_primitive() 364 | && !value.has_comments() 365 | && !container_has_pending_comments 366 | && self.options_in_scope().collapse_containers_of_one; 367 | match name { 368 | // Above the enter_scope(...), the container's SubpathOptions affect formatting 369 | // and below, formatting is affected by named property or item SubpathOptions. 370 | // vvvvvvvvvvv 371 | Some(name) => self.enter_scope(name), 372 | None => self.enter_scope("*"), 373 | } 374 | if collapsed { 375 | self.append(" ")?; 376 | } else { 377 | if is_first { 378 | self.start_next_line()?; 379 | } 380 | self.format_comments(value.comments().before_value(), is_first)?; 381 | } 382 | if let Some(name) = name { 383 | self.append(&format!("{}: ", name))?; 384 | } 385 | value.format(self)?; 386 | self.exit_scope(); 387 | // ^^^^^^^^^^ 388 | // Named property or item SubpathOptions affect Formatting above exit_scope(...) 389 | // and below, formatting is affected by the container's SubpathOptions. 390 | if collapsed { 391 | self.append(" ")?; 392 | } else { 393 | self.append_comma(is_last)? 394 | .append_end_of_line_comment(value.comments().end_of_line())? 395 | .start_next_line()?; 396 | } 397 | Ok(self) 398 | } 399 | 400 | pub fn format_item( 401 | &mut self, 402 | item: &Rc>, 403 | is_first: bool, 404 | is_last: bool, 405 | container_has_pending_comments: bool, 406 | ) -> Result<&mut Formatter, Error> { 407 | self.format_scoped_value( 408 | None, 409 | &mut item.borrow_mut(), 410 | is_first, 411 | is_last, 412 | container_has_pending_comments, 413 | ) 414 | } 415 | 416 | pub fn format_property( 417 | &mut self, 418 | property: &Property, 419 | is_first: bool, 420 | is_last: bool, 421 | container_has_pending_comments: bool, 422 | ) -> Result<&mut Formatter, Error> { 423 | self.format_scoped_value( 424 | Some(&property.name), 425 | &mut property.value.borrow_mut(), 426 | is_first, 427 | is_last, 428 | container_has_pending_comments, 429 | ) 430 | } 431 | 432 | pub fn options_in_scope(&self) -> FormatOptions { 433 | match self.get_current_subpath_options() { 434 | Some(subpath_options) => subpath_options.borrow().options.clone(), 435 | None => self.default_options.clone(), 436 | } 437 | } 438 | 439 | fn append_comma(&mut self, is_last: bool) -> Result<&mut Formatter, Error> { 440 | if !is_last || self.options_in_scope().trailing_commas { 441 | self.append(",")?; 442 | } 443 | Ok(self) 444 | } 445 | 446 | /// Outputs the value's end-of-line comment. If the comment has multiple lines, the first line 447 | /// is written from the current position and all subsequent lines are written on their own line, 448 | /// left-aligned directly under the first comment. 449 | fn append_end_of_line_comment( 450 | &mut self, 451 | comment: &Option, 452 | ) -> Result<&mut Formatter, Error> { 453 | if let Some(comment) = comment { 454 | let start_column = self.column; 455 | let mut first = true; 456 | for line in comment.lines() { 457 | if !first { 458 | self.append_newline()?; 459 | self.append(&" ".repeat(start_column - 1))?; 460 | } 461 | self.append(&format!(" //{}", line))?; 462 | first = false; 463 | } 464 | } 465 | Ok(self) 466 | } 467 | 468 | /// Formats the given document into the returned UTF8 byte buffer, consuming self. 469 | pub fn format(mut self, parsed_document: &ParsedDocument) -> Result, Error> { 470 | parsed_document.content.format_content(&mut self)?; 471 | Ok(self.bytes) 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Google LLC All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //! A stylized formatter for [JSON5](https://json5.org) ("JSON for Humans") documents. 6 | //! 7 | //! The intent of this formatter is to rewrite a given valid JSON5 document, restructuring the 8 | //! output (if required) to conform to a consistent style. 9 | //! 10 | //! The resulting document should preserve all data precision, data format representations, and 11 | //! semantic intent. Readability should be maintained, if not improved by the consistency within and 12 | //! across documents. 13 | //! 14 | //! Most importantly, all JSON5 comments should be preserved, maintaining the 15 | //! positional relationship with the JSON5 data elements they were intended to document. 16 | //! 17 | //! # Example 18 | //! 19 | //! ```rust 20 | //! use json5format::*; 21 | //! use maplit::hashmap; 22 | //! use maplit::hashset; 23 | //! 24 | //! let json5=r##"{ 25 | //! "name": { 26 | //! "last": "Smith", 27 | //! "first": "John", 28 | //! "middle": "Jacob" 29 | //! }, 30 | //! "children": [ 31 | //! "Buffy", 32 | //! "Biff", 33 | //! "Balto" 34 | //! ], 35 | //! // Consider adding a note field to the `other` contact option 36 | //! "contact_options": [ 37 | //! { 38 | //! "home": { 39 | //! "email": "jj@notreallygmail.com", // This was the original user id. 40 | //! // Now user id's are hash values. 41 | //! "phone": "212-555-4321" 42 | //! }, 43 | //! "other": { 44 | //! "email": "volunteering@serviceprojectsrus.org" 45 | //! }, 46 | //! "work": { 47 | //! "phone": "212-555-1234", 48 | //! "email": "john.j.smith@worksforme.gov" 49 | //! } 50 | //! } 51 | //! ], 52 | //! "address": { 53 | //! "city": "Anytown", 54 | //! "country": "USA", 55 | //! "state": "New York", 56 | //! "street": "101 Main Street" 57 | //! /* Update schema to support multiple addresses: 58 | //! "work": { 59 | //! "city": "Anytown", 60 | //! "country": "USA", 61 | //! "state": "New York", 62 | //! "street": "101 Main Street" 63 | //! } 64 | //! */ 65 | //! } 66 | //! } 67 | //! "##; 68 | //! 69 | //! let options = FormatOptions { 70 | //! indent_by: 2, 71 | //! collapse_containers_of_one: true, 72 | //! options_by_path: hashmap! { 73 | //! "/*" => hashset! { 74 | //! PathOption::PropertyNameOrder(vec![ 75 | //! "name", 76 | //! "address", 77 | //! "contact_options", 78 | //! ]), 79 | //! }, 80 | //! "/*/name" => hashset! { 81 | //! PathOption::PropertyNameOrder(vec![ 82 | //! "first", 83 | //! "middle", 84 | //! "last", 85 | //! "suffix", 86 | //! ]), 87 | //! }, 88 | //! "/*/children" => hashset! { 89 | //! PathOption::SortArrayItems(true), 90 | //! }, 91 | //! "/*/*/*" => hashset! { 92 | //! PathOption::PropertyNameOrder(vec![ 93 | //! "work", 94 | //! "home", 95 | //! "other", 96 | //! ]), 97 | //! }, 98 | //! "/*/*/*/*" => hashset! { 99 | //! PathOption::PropertyNameOrder(vec![ 100 | //! "phone", 101 | //! "email", 102 | //! ]), 103 | //! }, 104 | //! }, 105 | //! ..Default::default() 106 | //! }; 107 | //! 108 | //! let filename = "new_contact.json5".to_string(); 109 | //! 110 | //! let format = Json5Format::with_options(options)?; 111 | //! let parsed_document = ParsedDocument::from_str(&json5, Some(filename))?; 112 | //! let bytes: Vec = format.to_utf8(&parsed_document)?; 113 | //! 114 | //! assert_eq!(std::str::from_utf8(&bytes)?, r##"{ 115 | //! name: { 116 | //! first: "John", 117 | //! middle: "Jacob", 118 | //! last: "Smith", 119 | //! }, 120 | //! address: { 121 | //! city: "Anytown", 122 | //! country: "USA", 123 | //! state: "New York", 124 | //! street: "101 Main Street", 125 | //! 126 | //! /* Update schema to support multiple addresses: 127 | //! "work": { 128 | //! "city": "Anytown", 129 | //! "country": "USA", 130 | //! "state": "New York", 131 | //! "street": "101 Main Street" 132 | //! } 133 | //! */ 134 | //! }, 135 | //! 136 | //! // Consider adding a note field to the `other` contact option 137 | //! contact_options: [ 138 | //! { 139 | //! work: { 140 | //! phone: "212-555-1234", 141 | //! email: "john.j.smith@worksforme.gov", 142 | //! }, 143 | //! home: { 144 | //! phone: "212-555-4321", 145 | //! email: "jj@notreallygmail.com", // This was the original user id. 146 | //! // Now user id's are hash values. 147 | //! }, 148 | //! other: { email: "volunteering@serviceprojectsrus.org" }, 149 | //! }, 150 | //! ], 151 | //! children: [ 152 | //! "Balto", 153 | //! "Biff", 154 | //! "Buffy", 155 | //! ], 156 | //! } 157 | //! "##); 158 | //! # Ok::<(),anyhow::Error>(()) 159 | //! ``` 160 | //! 161 | //! # Formatter Actions 162 | //! 163 | //! When the options above are applied to the input, the formatter will make the following changes: 164 | //! 165 | //! * The formatted document will be indented by 2 spaces. 166 | //! * Quotes are removed from all property names (since they are all legal ECMAScript identifiers) 167 | //! * The top-level properties will be reordered to [`name`, `address`, `contact_options`]. Since 168 | //! property name `children` was not included in the sort order, it will be placed at the end. 169 | //! * The `name` properties will be reordered to [`first`, `middle`, `last`]. 170 | //! * The properties of the unnamed object in array `contact_options` will be reordered to 171 | //! [`work`, `home`, `other`]. 172 | //! * The properties of the `work`, `home`, and `other` objects will be reordered to 173 | //! [`phone`, `email`]. 174 | //! * The `children` names array of string primitives will be sorted. 175 | //! * All elements (except the top-level object, represented by the outermost curly braces) will 176 | //! end with a comma. 177 | //! * Since the `contact_options` descendant element `other` has only one property, the `other` 178 | //! object structure will collapse to a single line, with internal trailing comma suppressed. 179 | //! * The line comment will retain its relative position, above `contact_options`. 180 | //! * The block comment will retain its relative position, inside and at the end of the `address` 181 | //! object. 182 | //! * The end-of-line comment after `home`/`email` will retain its relative location (appended at 183 | //! the end of the `email` value) and any subsequent line comments with the same vertical 184 | //! alignment are also retained, and vertically adjusted to be left-aligned with the new 185 | //! position of the first comment line. 186 | //! 187 | //! # Formatter Behavior Details 188 | //! 189 | //! For reference, the following sections detail how the JSON5 formatter verifies and processes 190 | //! JSON5 content. 191 | //! 192 | //! ## Syntax Validation 193 | //! 194 | //! * Structural syntax is checked, such as validating matching braces, property name-colon-value 195 | //! syntax, enforced separation of values by commas, properly quoted strings, and both block and 196 | //! line comment extraction. 197 | //! * Non-string literal value syntax is checked (null, true, false, and the various legal formats 198 | //! for JSON5 Numbers). 199 | //! * Syntax errors produce error messages with the line and column where the problem 200 | //! was encountered. 201 | //! 202 | //! ## Property Names 203 | //! 204 | //! * Duplicate property names are retained, but may constitute errors in higher-level JSON5 205 | //! parsers or schema-specific deserializers. 206 | //! * All JSON5 unquoted property name characters are supported, including '$' and '_'. Digits are 207 | //! the only valid property name character that cannot be the first character. Property names 208 | //! can also be represented as quoted strings. All valid JSON5 strings, if quoted, are valid 209 | //! property names (including multi-line strings and quoted numbers). 210 | //! 211 | //! Example: 212 | //! ```json 213 | //! $_meta_prop: 'Has "double quotes" and \'single quotes\' and \ 214 | //! multiple lines with escaped \\ backslash', 215 | //! ``` 216 | //! 217 | //! ## Literal Values 218 | //! 219 | //! * JSON5 supports quoting strings (literal values or quoted property names) by either double (") 220 | //! or single (') quote. The formatter does not change the quotes. Double-quoting is 221 | //! conventional, but single quotes may be used when quoting strings containing double-quotes, and 222 | //! leaving the single quotes as-is is preferred. 223 | //! * JSON5 literal values are retained as-is. Strings retain all spacing characters, including 224 | //! escaped newlines. All other literals (unquoted tokens without spaces, such as false, null, 225 | //! 0.234, 1337, or l33t) are _not_ interpreted syntactically. Other schema-based tools and JSON5 226 | //! deserializers may flag these invalid values. 227 | //! 228 | //! ## Optional Sorting 229 | //! 230 | //! * By default, array items and object properties retain their original order. (Some JSON arrays 231 | //! are order-dependent, and sorting them indiscriminantly might change the meaning of the data.) 232 | //! * The formatter can automatically sort array items and object properties if enabled via 233 | //! `FormatOptions`: 234 | //! - To sort all arrays in the document, set 235 | //! [FormatOptions.sort_array_items](struct.FormatOptions.html#structfield.sort_array_items) to 236 | //! `true` 237 | //! - To sort only specific arrays in the target schema, specify the schema location under 238 | //! [FormatOptions.options_by_path](struct.FormatOptions.html#structfield.options_by_path), and 239 | //! set its [SortArrayItems](enum.PathOption.html#variant.SortArrayItems) option. 240 | //! - Properties are sorted based on an explicit user-supplied list of property names in the 241 | //! preferred order, for objects at a specified path. Specify the object's location in the 242 | //! target schema using 243 | //! [FormatOptions.options_by_path](struct.FormatOptions.html#structfield.options_by_path), and 244 | //! provide a vector of property name strings with the 245 | //! [PropertyNameOrder](enum.PathOption.html#variant.PropertyNameOrder) option. Properties not 246 | //! included in this option retain their original order, behind the explicitly ordered 247 | //! properties, if any. 248 | //! * When sorting array items, the formatter only sorts array item literal values (strings, 249 | //! numbers, bools, and null). Child arrays or objects are left in their original order, after 250 | //! sorted literals, if any, within the same array. 251 | //! * Array items are sorted in case-insensitive unicode lexicographic order. **(Note that, since 252 | //! the formatter does not parse unquoted literals, number types cannot be sorted numerically.)** 253 | //! Items that are case-insensitively equal are re-compared and ordered case-sensitively with 254 | //! respect to each other. 255 | //! 256 | //! ## Associated Comments 257 | //! 258 | //! * All comments immediately preceding an element (value or start of an array or object), and 259 | //! trailing line comments (starting on the same line as the element, optionally continued on 260 | //! successive lines if all line comments are left-aligned), are retained and move with the 261 | //! associated item if the item is repositioned during sorting. 262 | //! * All line and block comments are retained. Typically, the comments are re-aligned vertically 263 | //! (indented) with the values with which they were associated. 264 | //! * A single line comment appearing immediately after a JSON value (primitive or closing brace), 265 | //! on the same line, will remain appended to that value on its line after re-formatting. 266 | //! * Spaces separate block comments from blocks of contiguous line comments associated with the 267 | //! same entry. 268 | //! * Comments at the end of a list (after the last property or item) are retained at the end of 269 | //! the same list. 270 | //! * Block comments with lines that extend to the left of the opening "/\*" are not re-aligned. 271 | //! 272 | //! ## Whitespace Handling 273 | //! 274 | //! * Unicode characters are allowed, and unicode space characters should retain their meaning 275 | //! according to unicode standards. 276 | //! * All spaces inside single- or multi-line strings are retained. All spaces in comments are 277 | //! retained *except* trailing spaces at the end of a line. 278 | //! * All other original spaces are removed. 279 | 280 | #![deny(missing_docs)] 281 | #![allow(clippy::len_zero)] 282 | 283 | #[macro_use] 284 | mod error; 285 | 286 | mod content; 287 | mod formatter; 288 | mod options; 289 | mod parser; 290 | 291 | use { 292 | crate::formatter::*, std::cell::RefCell, std::collections::HashMap, std::collections::HashSet, 293 | std::rc::Rc, 294 | }; 295 | 296 | pub use content::Array; 297 | pub use content::Comment; 298 | pub use content::Comments; 299 | pub use content::Object; 300 | pub use content::ParsedDocument; 301 | pub use content::Primitive; 302 | pub use content::Property; 303 | pub use content::Value; 304 | pub use error::Error; 305 | pub use error::Location; 306 | pub use options::FormatOptions; 307 | pub use options::PathOption; 308 | 309 | /// Format a JSON5 document, applying a consistent style, with given options. 310 | /// 311 | /// See [FormatOptions](struct.FormatOptions.html) for style options, and confirm the defaults by 312 | /// reviewing the source of truth via the `src` link for 313 | /// [impl Default for FormatOptions](struct.FormatOptions.html#impl-Default). 314 | /// 315 | /// # Format and Style (Default) 316 | /// 317 | /// Unless FormatOptions are modified, the JSON5 formatter takes a JSON5 document (as a unicode 318 | /// String) and generates a new document with the following formatting: 319 | /// 320 | /// * Indents 4 spaces. 321 | /// * Quotes are removed from property names if they are legal ECMAScript 5.1 identifiers. Property 322 | /// names that do not comply with ECMAScript identifier format requirements will retain their 323 | /// existing (single or double) quotes. 324 | /// * All property and item lists end with a trailing comma. 325 | /// * All property and item lists are broken down; that is, the braces are on separate lines and 326 | /// all values are indented. 327 | /// 328 | /// ```json 329 | /// { 330 | /// key: "value", 331 | /// array: [ 332 | /// 3.145, 333 | /// ] 334 | /// } 335 | /// ``` 336 | /// 337 | /// # Arguments 338 | /// * buffer - A unicode string containing the original JSON5 document. 339 | /// * filename - An optional filename. Parsing errors typically include the filename (if given), 340 | /// and the line number and character column where the error was detected. 341 | /// * options - Format style options to override the default style, if provided. 342 | /// # Returns 343 | /// * The formatted result in UTF-8 encoded bytes. 344 | pub fn format( 345 | buffer: &str, 346 | filename: Option, 347 | options: Option, 348 | ) -> Result, Error> { 349 | let parsed_document = ParsedDocument::from_str(buffer, filename)?; 350 | let options = match options { 351 | Some(options) => options, 352 | None => FormatOptions { ..Default::default() }, 353 | }; 354 | Json5Format::with_options(options)?.to_utf8(&parsed_document) 355 | } 356 | 357 | /// A JSON5 formatter that parses a valid JSON5 input buffer and produces a new, formatted document. 358 | pub struct Json5Format { 359 | /// Options that alter how the formatter generates the formatted output. This instance of 360 | /// FormatOptions is a subset of the FormatOptions passed to the `with_options` constructor. 361 | /// The `options_by_path` are first removed, and then used to initialize the SubpathOptions 362 | /// hierarchy rooted at the `document_root_options_ref`. 363 | default_options: FormatOptions, 364 | 365 | /// Depth-specific options applied at the document root and below. 366 | document_root_options_ref: Rc>, 367 | } 368 | 369 | impl Json5Format { 370 | /// Create and return a Json5Format, with the given options to be applied to the 371 | /// [Json5Format::to_utf8()](struct.Json5Format.html#method.to_utf8) operation. 372 | pub fn with_options(mut options: FormatOptions) -> Result { 373 | let mut document_root_options = SubpathOptions::new(&options); 374 | 375 | // Typical JSON5 documents start and end with curly braces for a top-level unnamed 376 | // object. This is by convention, and the Json5Format represents this 377 | // top-level object as a single child in a conceptual array. The array square braces 378 | // are not rendered, and by convention, the child object should not have a trailing 379 | // comma, even if trailing commas are the default everywhere else in the document. 380 | // 381 | // Set the SubpathOptions for the document array items to prevent trailing commas. 382 | document_root_options.options.trailing_commas = false; 383 | 384 | let mut options_by_path = 385 | options.options_by_path.drain().collect::>>(); 386 | 387 | // Default options remain after draining the `options_by_path` 388 | let default_options = options; 389 | 390 | // Transfer the options_by_path from the given options into the SubpathOptions tree 391 | // rooted at `document_options_root`. 392 | for (path, path_options) in options_by_path.drain() { 393 | let rc; // extend life of temporary 394 | let mut borrowed; // extend life of temporary 395 | let subpath_options = if path == "/" { 396 | &mut document_root_options 397 | } else if let Some(remaining) = path.strip_prefix('/') { 398 | rc = document_root_options.get_or_create_subpath_options( 399 | &remaining.split('/').collect::>(), 400 | &default_options, 401 | ); 402 | borrowed = rc.borrow_mut(); 403 | &mut *borrowed 404 | } else { 405 | return Err(Error::configuration(format!( 406 | "PathOption path '{}' is invalid.", 407 | path 408 | ))); 409 | }; 410 | subpath_options.override_default_options(&path_options); 411 | } 412 | 413 | Ok(Json5Format { 414 | default_options, 415 | document_root_options_ref: Rc::new(RefCell::new(document_root_options)), 416 | }) 417 | } 418 | 419 | /// Create and return a Json5Format, with the default settings. 420 | pub fn new() -> Result { 421 | Self::with_options(FormatOptions { ..Default::default() }) 422 | } 423 | 424 | /// Formats the parsed document into a new Vector of UTF8 bytes. 425 | /// 426 | /// # Arguments 427 | /// * `parsed_document` - The parsed state of the incoming document. 428 | /// 429 | /// # Example 430 | /// 431 | /// ``` 432 | /// # use json5format::*; 433 | /// # let buffer = String::from("{}"); 434 | /// # let filename = String::from("example.json5"); 435 | /// let format = Json5Format::new()?; 436 | /// let parsed_document = ParsedDocument::from_str(&buffer, Some(filename))?; 437 | /// let bytes = format.to_utf8(&parsed_document)?; 438 | /// # assert_eq!("{}\n", std::str::from_utf8(&bytes).unwrap()); 439 | /// # Ok::<(),anyhow::Error>(()) 440 | /// ``` 441 | pub fn to_utf8(&self, parsed_document: &ParsedDocument) -> Result, Error> { 442 | let formatter = 443 | Formatter::new(self.default_options.clone(), self.document_root_options_ref.clone()); 444 | formatter.format(parsed_document) 445 | } 446 | 447 | /// Formats the parsed document into a new String. 448 | /// 449 | /// # Arguments 450 | /// * `parsed_document` - The parsed state of the incoming document. 451 | /// 452 | /// # Example 453 | /// 454 | /// ``` 455 | /// # use json5format::*; 456 | /// # fn main() -> std::result::Result<(), Error> { 457 | /// # let buffer = String::from("{}"); 458 | /// # let filename = String::from("example.json5"); 459 | /// let format = Json5Format::new()?; 460 | /// let parsed_document = ParsedDocument::from_str(&buffer, Some(filename))?; 461 | /// let formatted = format.to_string(&parsed_document)?; 462 | /// # assert_eq!("{}\n", formatted); 463 | /// # Ok(()) 464 | /// # } 465 | /// ``` 466 | pub fn to_string(&self, parsed_document: &ParsedDocument) -> Result { 467 | String::from_utf8(self.to_utf8(parsed_document)?) 468 | .map_err(|e| Error::internal(None, e.to_string())) 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Google LLC All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #![deny(missing_docs)] 6 | use {std::collections::HashMap, std::collections::HashSet, std::hash::Hash, std::hash::Hasher}; 7 | 8 | /// Options that can be applied to specific objects or arrays in the target JSON5 schema, through 9 | /// [FormatOptions.options_by_path](struct.FormatOptions.html#structfield.options_by_path). 10 | /// Each option can be set at most once per unique path. 11 | #[derive(Clone, Debug)] 12 | pub enum PathOption { 13 | /// For matched paths, overrides the FormatOption.trailing_comma provided default. 14 | TrailingCommas(bool), 15 | 16 | /// For matched paths, overrides the FormatOption.collapse_container_of_one provided default. 17 | CollapseContainersOfOne(bool), 18 | 19 | /// For matched paths, overrides the FormatOption.sort_array_items provided default. 20 | SortArrayItems(bool), 21 | 22 | /// Contains a vector of property names. When formatting an object matching the path in 23 | /// `FormatOptions.options_by_path` a specified path, properties of the object will be sorted 24 | /// to match the given order. Any properties not in this list will retain their original order, 25 | /// and placed after the sorted properties. 26 | PropertyNameOrder(Vec<&'static str>), 27 | } 28 | 29 | impl PartialEq for PathOption { 30 | fn eq(&self, other: &Self) -> bool { 31 | use PathOption::*; 32 | matches!( 33 | (self, other), 34 | (&TrailingCommas(..), &TrailingCommas(..)) 35 | | (&CollapseContainersOfOne(..), &CollapseContainersOfOne(..)) 36 | | (&SortArrayItems(..), &SortArrayItems(..)) 37 | | (&PropertyNameOrder(..), &PropertyNameOrder(..)) 38 | ) 39 | } 40 | } 41 | 42 | impl Eq for PathOption {} 43 | 44 | impl Hash for PathOption { 45 | fn hash(&self, state: &mut H) { 46 | use PathOption::*; 47 | state.write_u32(match self { 48 | TrailingCommas(..) => 1, 49 | CollapseContainersOfOne(..) => 2, 50 | SortArrayItems(..) => 3, 51 | PropertyNameOrder(..) => 4, 52 | }); 53 | state.finish(); 54 | } 55 | } 56 | 57 | /// Options that change the style of the formatted JSON5 output. 58 | #[derive(Clone, Debug)] 59 | pub struct FormatOptions { 60 | /// Indent the content of an object or array by this many spaces. 61 | pub indent_by: usize, 62 | 63 | /// Add a trailing comma after the last element in an array or object. 64 | pub trailing_commas: bool, 65 | 66 | /// If an array or object has only one item (or is empty), and no internal comments, collapse 67 | /// the array or object to a single line. 68 | pub collapse_containers_of_one: bool, 69 | 70 | /// If true, sort array primitive values lexicographically. Be aware that the order may not 71 | /// matter in some use cases, but can be very important in others. Consider setting this 72 | /// option for specific property paths only, and otherwise use the default (false). 73 | pub sort_array_items: bool, 74 | 75 | /// A set of "paths", to identify elements of the JSON structure, mapped to a set of one or 76 | /// more [PathOption](enum.PathOption.html) settings. 77 | pub options_by_path: HashMap<&'static str, HashSet>, 78 | } 79 | 80 | impl Default for FormatOptions { 81 | fn default() -> Self { 82 | FormatOptions { 83 | indent_by: 4, 84 | trailing_commas: true, 85 | collapse_containers_of_one: false, 86 | sort_array_items: false, 87 | options_by_path: HashMap::new(), 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Google LLC All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #![deny(missing_docs)] 6 | use { 7 | crate::{test_error, Json5Format}, 8 | json5format::*, 9 | maplit::hashmap, 10 | maplit::hashset, 11 | std::fs::{self, DirEntry}, 12 | std::io::{self, Read}, 13 | std::path::Path, 14 | std::path::PathBuf, 15 | }; 16 | 17 | #[derive(Default)] 18 | struct FormatTest<'a> { 19 | options: Option, 20 | input: &'a str, 21 | error: Option<&'a str>, 22 | expected: &'a str, 23 | } 24 | 25 | fn try_test_format(test: FormatTest<'_>) -> Result<(), Error> { 26 | let result = match ParsedDocument::from_str(test.input, None) { 27 | Ok(parsed_document) => { 28 | let format = match test.options { 29 | Some(options) => Json5Format::with_options(options)?, 30 | None => Json5Format::new()?, 31 | }; 32 | format.to_utf8(&parsed_document) 33 | } 34 | Err(actual_error) => Err(actual_error), 35 | }; 36 | match result { 37 | Ok(bytes) => { 38 | let actual_formatted_document = std::str::from_utf8(&bytes).unwrap(); 39 | match test.error { 40 | Some(expected_error) => { 41 | println!("Unexpected formatted result:"); 42 | println!("==========================="); 43 | println!("{}", actual_formatted_document); 44 | println!("==========================="); 45 | println!("Expected error: {}", expected_error); 46 | Err(test_error!(format!( 47 | "Unexpected 'Ok()' result.\n expected: '{}'", 48 | expected_error 49 | ))) 50 | } 51 | None => { 52 | if actual_formatted_document == test.expected { 53 | Ok(()) 54 | } else { 55 | println!("expected:"); 56 | println!("========"); 57 | println!("{}", test.expected); 58 | println!("========"); 59 | println!("actual:"); 60 | println!("======"); 61 | println!("{}", actual_formatted_document); 62 | println!("======"); 63 | Err(test_error!(format!( 64 | "Actual formatted document did not match expected." 65 | ))) 66 | } 67 | } 68 | } 69 | } 70 | Err(actual_error) => match test.error { 71 | Some(expected_error) => { 72 | let actual_error = format!("{}", actual_error); 73 | if expected_error == actual_error { 74 | Ok(()) 75 | } else { 76 | println!("expected: {}", expected_error); 77 | println!(" actual: {}", actual_error); 78 | Err(test_error!("Actual error did not match expected error.")) 79 | } 80 | } 81 | None => Err(actual_error), 82 | }, 83 | } 84 | } 85 | 86 | fn test_format(test: FormatTest<'_>) -> Result<(), Error> { 87 | try_test_format(test).map_err(|e| { 88 | println!("{}", e); 89 | e 90 | }) 91 | } 92 | 93 | #[test] 94 | fn test_format_simple_objects() { 95 | test_format(FormatTest { 96 | input: r##"{ "program": {} }"##, 97 | expected: r##"{ 98 | program: {}, 99 | } 100 | "##, 101 | ..Default::default() 102 | }) 103 | .unwrap() 104 | } 105 | 106 | #[test] 107 | fn test_format_exponential() { 108 | test_format(FormatTest { 109 | input: r##"{ "exponential": 3.14e-8 }"##, 110 | expected: r##"{ 111 | exponential: 3.14e-8, 112 | } 113 | "##, 114 | ..Default::default() 115 | }) 116 | .unwrap() 117 | } 118 | 119 | #[test] 120 | fn test_last_scope_is_array() { 121 | test_format(FormatTest { 122 | input: r##"{ 123 | program: {}, 124 | expose: [ 125 | { 126 | } 127 | 128 | /* and this */ 129 | ] 130 | } // line comment on primary object 131 | 132 | // line comment at the end of the document 133 | // second line comment 134 | 135 | /* block comment at the end of the document 136 | * block comment continues. 137 | * end of block comment at end of doc */ 138 | "##, 139 | expected: r##"{ 140 | program: {}, 141 | expose: [ 142 | {}, 143 | 144 | /* and this */ 145 | ], 146 | } // line comment on primary object 147 | 148 | // line comment at the end of the document 149 | // second line comment 150 | 151 | /* block comment at the end of the document 152 | * block comment continues. 153 | * end of block comment at end of doc */ 154 | "##, 155 | ..Default::default() 156 | }) 157 | .unwrap() 158 | } 159 | 160 | #[test] 161 | fn test_comment_block() { 162 | test_format(FormatTest { 163 | input: r##"// Copyright or other header 164 | // goes here 165 | { 166 | program: {}, 167 | expose: [ 168 | /* 169 | what happens 170 | with this 1 171 | */ 172 | /* 173 | what happens 174 | with this 2 175 | */ 176 | /* 177 | what happens 178 | with this 3 179 | */ 180 | /* 181 | what happens 182 | with this 4 183 | */ 184 | /* 185 | what happens 186 | 187 | with this 5 188 | */ 189 | /* 190 | 191 | what happens 192 | 193 | with this 6 194 | */ 195 | /* what happens 196 | with this 7 197 | */ 198 | /* what happens 199 | with this 8 200 | and this */ 201 | { 202 | } 203 | 204 | /* and this */ 205 | ] 206 | } 207 | // and end of 208 | // the doc comment"##, 209 | expected: r##"// Copyright or other header 210 | // goes here 211 | { 212 | program: {}, 213 | expose: [ 214 | /* 215 | what happens 216 | with this 1 217 | */ 218 | 219 | /* 220 | what happens 221 | with this 2 222 | */ 223 | 224 | /* 225 | what happens 226 | with this 3 227 | */ 228 | 229 | /* 230 | what happens 231 | with this 4 232 | */ 233 | 234 | /* 235 | what happens 236 | 237 | with this 5 238 | */ 239 | 240 | /* 241 | 242 | what happens 243 | 244 | with this 6 245 | */ 246 | 247 | /* what happens 248 | with this 7 249 | */ 250 | 251 | /* what happens 252 | with this 8 253 | and this */ 254 | {}, 255 | 256 | /* and this */ 257 | ], 258 | } 259 | 260 | // and end of 261 | // the doc comment 262 | "##, 263 | ..Default::default() 264 | }) 265 | .unwrap() 266 | } 267 | 268 | #[test] 269 | fn test_end_of_line_comments() { 270 | test_format(FormatTest { 271 | input: r##" 272 | { // not an end-of-line comment 273 | // because it's not an end of a value 274 | 275 | program: {}, // end of line comment 276 | 277 | expose: [ 278 | "value1",// eol comment 279 | // is here 280 | "value2", // eol comment 2 281 | // 282 | // 283 | // is also here 284 | "value3", // this end of line comment is followed by a comment that is not vertically aligned 285 | // so we assume this line comment is not part of the previous end-of-line comment 286 | /*item4*/"value4", /*item5*/"value5", /*item6*/"value6" // eol comment without comma 287 | // here also 288 | ], 289 | some_object: { 290 | prop1: // eol comment is not here 291 | "value1",// eol comment 292 | // is here 293 | prop2: "value2", // eol comment 2 294 | // 295 | // 296 | // is also here 297 | prop3: "value3", // this end of line comment is followed by a comment that is not vertically aligned 298 | // so we assume this line comment is not part of the previous end-of-line comment 299 | prop4: "value4", prop5: "value5", prop6: "value6" // eol comment without comma 300 | // here also 301 | }, 302 | children: 303 | [ // line comment after open brace for "children" 304 | ], 305 | use: // line comment for "use" 306 | // and "use" line comment's second line 307 | [ 308 | ], 309 | offer: [ 310 | ], // end of line comment for "offer" 311 | collections: [ 312 | ], // not just one line but this 313 | // is a multi-line end of line comment for "collections" 314 | // 315 | // - and should have indentation preserved 316 | // - with multiple bullet points 317 | other: [ 318 | ], /// This doc comment style should still work like any other line 319 | /// or end-of-line comment 320 | /// 321 | /// - and should also have indentation preserved 322 | /// - also with multiple bullet points 323 | } 324 | // not an end-of-line comment because there is a newline; and end of 325 | 326 | // the doc comment was another break, 327 | // and the document ends without the required newline"##, 328 | expected: r##"{ 329 | // not an end-of-line comment 330 | // because it's not an end of a value 331 | program: {}, // end of line comment 332 | expose: [ 333 | "value1", // eol comment 334 | // is here 335 | "value2", // eol comment 2 336 | // 337 | // 338 | // is also here 339 | "value3", // this end of line comment is followed by a comment that is not vertically aligned 340 | 341 | // so we assume this line comment is not part of the previous end-of-line comment 342 | 343 | /*item4*/ 344 | "value4", 345 | 346 | /*item5*/ 347 | "value5", 348 | 349 | /*item6*/ 350 | "value6", // eol comment without comma 351 | // here also 352 | ], 353 | some_object: { 354 | // eol comment is not here 355 | prop1: "value1", // eol comment 356 | // is here 357 | prop2: "value2", // eol comment 2 358 | // 359 | // 360 | // is also here 361 | prop3: "value3", // this end of line comment is followed by a comment that is not vertically aligned 362 | 363 | // so we assume this line comment is not part of the previous end-of-line comment 364 | prop4: "value4", 365 | prop5: "value5", 366 | prop6: "value6", // eol comment without comma 367 | // here also 368 | }, 369 | children: [ 370 | // line comment after open brace for "children" 371 | ], 372 | 373 | // line comment for "use" 374 | // and "use" line comment's second line 375 | use: [], 376 | offer: [], // end of line comment for "offer" 377 | collections: [], // not just one line but this 378 | // is a multi-line end of line comment for "collections" 379 | // 380 | // - and should have indentation preserved 381 | // - with multiple bullet points 382 | other: [], /// This doc comment style should still work like any other line 383 | /// or end-of-line comment 384 | /// 385 | /// - and should also have indentation preserved 386 | /// - also with multiple bullet points 387 | } 388 | 389 | // not an end-of-line comment because there is a newline; and end of 390 | 391 | // the doc comment was another break, 392 | // and the document ends without the required newline 393 | "##, 394 | ..Default::default() 395 | }) 396 | .unwrap() 397 | } 398 | 399 | #[test] 400 | fn test_breaks_between_line_comments() { 401 | test_format(FormatTest { 402 | input: r##"// Copyright or other header 403 | // goes here 404 | 405 | // Another comment block 406 | // separate from the copyright block. 407 | { 408 | 409 | /// doc comment 410 | /// is here 411 | program: {}, 412 | 413 | /// another doc comment 414 | /* and block comment */ 415 | /// and doc comment 416 | 417 | 418 | 419 | /// and multiple blank lines were above this line comment, 420 | /// but replaced by one. 421 | 422 | /// more than 423 | /// two contiguous 424 | /// line comments 425 | /// are 426 | /// here 427 | /// 428 | /// including empty line comments 429 | 430 | expose: [ // inside array so not end of line comment 431 | // comment block 432 | // is here 433 | 434 | //comment block 435 | // is here 2 436 | 437 | //comment block 438 | // is here 3 439 | 440 | // and one more 441 | 442 | /* and a block comment 443 | */ 444 | ], 445 | children: 446 | [ // line comment after open brace for "children" 447 | ], 448 | use: // line comment for "use" 449 | [ 450 | ], 451 | collections: [ 452 | ], // not just one line but this 453 | // is a multi-line end of line comment for "collections" 454 | // 455 | // - and should have indentation preserved 456 | offer: [ 457 | ], // end of line comment for "offer" 458 | } 459 | // and end of 460 | 461 | // the doc comment 462 | // was another break"##, 463 | expected: r##"// Copyright or other header 464 | // goes here 465 | 466 | // Another comment block 467 | // separate from the copyright block. 468 | { 469 | /// doc comment 470 | /// is here 471 | program: {}, 472 | 473 | /// another doc comment 474 | 475 | /* and block comment */ 476 | 477 | /// and doc comment 478 | 479 | /// and multiple blank lines were above this line comment, 480 | /// but replaced by one. 481 | 482 | /// more than 483 | /// two contiguous 484 | /// line comments 485 | /// are 486 | /// here 487 | /// 488 | /// including empty line comments 489 | expose: [ 490 | // inside array so not end of line comment 491 | // comment block 492 | // is here 493 | 494 | //comment block 495 | // is here 2 496 | 497 | //comment block 498 | // is here 3 499 | 500 | // and one more 501 | 502 | /* and a block comment 503 | */ 504 | ], 505 | children: [ 506 | // line comment after open brace for "children" 507 | ], 508 | 509 | // line comment for "use" 510 | use: [], 511 | collections: [], // not just one line but this 512 | // is a multi-line end of line comment for "collections" 513 | // 514 | // - and should have indentation preserved 515 | offer: [], // end of line comment for "offer" 516 | } 517 | 518 | // and end of 519 | 520 | // the doc comment 521 | // was another break 522 | "##, 523 | ..Default::default() 524 | }) 525 | .unwrap() 526 | } 527 | 528 | #[test] 529 | fn test_format_sort_and_align_block_comment() { 530 | test_format(FormatTest { 531 | options: Some(FormatOptions { sort_array_items: true, ..Default::default() }), 532 | input: r##"{ 533 | "program": { 534 | "binary": "bin/session_manager" 535 | }, 536 | "use": [ 537 | { "runner": "elf" }, 538 | { 539 | // The Realm service allows session_manager to start components. 540 | "protocol": "/svc/fuchsia.sys2.Realm", 541 | "from": "framework", 542 | }, 543 | { 544 | /* indented block 545 | comment: 546 | * is here 547 | * ok 548 | */ 549 | "protocol": [ 550 | "/svc/fuchsia.logger.LogSink", 551 | "/svc/fuchsia.cobalt.LoggerFactory", 552 | ], 553 | "from": "realm", 554 | }, 555 | ], 556 | } 557 | "##, 558 | expected: r##"{ 559 | program: { 560 | binary: "bin/session_manager", 561 | }, 562 | use: [ 563 | { 564 | runner: "elf", 565 | }, 566 | { 567 | // The Realm service allows session_manager to start components. 568 | protocol: "/svc/fuchsia.sys2.Realm", 569 | from: "framework", 570 | }, 571 | { 572 | /* indented block 573 | comment: 574 | * is here 575 | * ok 576 | */ 577 | protocol: [ 578 | "/svc/fuchsia.cobalt.LoggerFactory", 579 | "/svc/fuchsia.logger.LogSink", 580 | ], 581 | from: "realm", 582 | }, 583 | ], 584 | } 585 | "##, 586 | ..Default::default() 587 | }) 588 | .unwrap() 589 | } 590 | 591 | #[test] 592 | fn test_property_name_formatting() { 593 | test_format(FormatTest { 594 | input: r##"{ 595 | unquotedName: 1, 596 | $_is_ok_$: 2, 597 | $10million: 3, 598 | _10_9_8___: 4, 599 | "remove_quotes_$_123": 5, 600 | "keep quotes": 6, 601 | "multi \ 602 | line \ 603 | is \ 604 | valid": 7, 605 | "3.14159": "pi", 606 | "with 'quotes'": 9, 607 | 'with "quotes"': 10, 608 | } 609 | "##, 610 | expected: r##"{ 611 | unquotedName: 1, 612 | $_is_ok_$: 2, 613 | $10million: 3, 614 | _10_9_8___: 4, 615 | remove_quotes_$_123: 5, 616 | "keep quotes": 6, 617 | "multi \ 618 | line \ 619 | is \ 620 | valid": 7, 621 | "3.14159": "pi", 622 | "with 'quotes'": 9, 623 | 'with "quotes"': 10, 624 | } 625 | "##, 626 | ..Default::default() 627 | }) 628 | .unwrap() 629 | } 630 | 631 | #[test] 632 | fn test_parse_error_missing_property_value() { 633 | test_format(FormatTest { 634 | input: r##"{ 635 | property: { 636 | sub_property_1: "value", 637 | sub_property_2: , 638 | } 639 | } 640 | "##, 641 | error: Some( 642 | "Parse error: 4:25: Property 'sub_property_2' must have a value before the next \ 643 | comma-separated property: 644 | sub_property_2: , 645 | ^", 646 | ), 647 | ..Default::default() 648 | }) 649 | .unwrap(); 650 | } 651 | 652 | #[test] 653 | fn test_parse_error_missing_property_value_when_closing_object() { 654 | test_format(FormatTest { 655 | input: r##"{ 656 | property: { 657 | sub_property_1: "value", 658 | sub_property_2: 659 | } 660 | } 661 | "##, 662 | error: Some( 663 | "Parse error: 5:5: Property 'sub_property_2' must have a value before closing an \ 664 | object: 665 | } 666 | ^", 667 | ), 668 | ..Default::default() 669 | }) 670 | .unwrap(); 671 | } 672 | 673 | #[test] 674 | fn test_parse_error_incomplete_property() { 675 | test_format(FormatTest { 676 | input: r##"{ 677 | property: { 678 | sub_property_1: "value1" 679 | sub_property_2: "value2", 680 | } 681 | } 682 | "##, 683 | error: Some( 684 | r#"Parse error: 4:9: Properties must be separated by a comma: 685 | sub_property_2: "value2", 686 | ^~~~~~~~~~~~~~~"#, 687 | ), 688 | ..Default::default() 689 | }) 690 | .unwrap(); 691 | 692 | test_format(FormatTest { 693 | input: r##"{ 694 | property: { 695 | sub_property_1: 696 | sub_property_2: "value2", 697 | } 698 | } 699 | "##, 700 | error: Some( 701 | r#"Parse error: 4:9: Properties must be separated by a comma: 702 | sub_property_2: "value2", 703 | ^~~~~~~~~~~~~~~"#, 704 | ), 705 | ..Default::default() 706 | }) 707 | .unwrap(); 708 | 709 | test_format(FormatTest { 710 | input: r##"{ 711 | property: { 712 | sub_property_1: , 713 | sub_property_2: "value2", 714 | } 715 | } 716 | "##, 717 | error: Some( 718 | "Parse error: 3:25: Property 'sub_property_1' must have a value before the next \ 719 | comma-separated property: 720 | sub_property_1: , 721 | ^", 722 | ), 723 | ..Default::default() 724 | }) 725 | .unwrap(); 726 | } 727 | 728 | #[test] 729 | fn test_parse_error_property_name_when_array_value_is_expected() { 730 | test_format(FormatTest { 731 | input: r##"{ 732 | property: [ 733 | "item1", 734 | sub_property_1: "value", 735 | } 736 | } 737 | "##, 738 | error: Some(r#"Parse error: 4:9: Invalid Object token found while parsing an Array of 1 item (mismatched braces?): 739 | sub_property_1: "value", 740 | ^~~~~~~~~~~~~~~"#), 741 | ..Default::default() 742 | }) 743 | .unwrap(); 744 | } 745 | 746 | #[test] 747 | fn test_parse_error_bad_non_string_primitive() { 748 | test_format(FormatTest { 749 | input: r##"{ 750 | non_string_literals: [ 751 | null, 752 | true, 753 | false, 754 | 755 | 0, 756 | 0., 757 | 0.0, 758 | 0.000, 759 | .0, 760 | .000, 761 | 12345, 762 | 12345.00000, 763 | 12345.67890, 764 | 12345., 765 | 0.678900, 766 | .67890, 767 | 1234e5678, 768 | 1234E5678, 769 | 1234e+5678, 770 | 1234E+5678, 771 | 1234e-5678, 772 | 1234E-5678, 773 | 12.34e5678, 774 | 1234.E5678, 775 | .1234e+5678, 776 | 12.34E+5678, 777 | 1234.e-5678, 778 | .1234E-5678, 779 | 0xabc123ef, 780 | 0Xabc123EF, 781 | NaN, 782 | Infinity, 783 | 784 | -12345, 785 | -12345.67890, 786 | -12345., 787 | -.67890, 788 | -1234e5678, 789 | -1234E5678, 790 | -1234e+5678, 791 | -1234E+5678, 792 | -1234e-5678, 793 | -1234E-5678, 794 | -12.34e5678, 795 | -1234.E5678, 796 | -.1234e+5678, 797 | -12.34E+5678, 798 | -1234.e-5678, 799 | -.1234E-5678, 800 | -0xabc123ef, 801 | -0Xabc123EF, 802 | -NaN, 803 | -Infinity, 804 | 805 | +12345, 806 | +12345.67890, 807 | +12345., 808 | +.67890, 809 | +1234e5678, 810 | +1234E5678, 811 | +1234e+5678, 812 | +1234E+5678, 813 | +1234e-5678, 814 | +1234E-5678, 815 | +0xabc123ef, 816 | +0Xabc123EF, 817 | +NaN, 818 | +Infinity, 819 | 820 | 0x123def, 821 | 123def, 822 | ] 823 | } 824 | "##, 825 | error: Some( 826 | "Parse error: 73:9: Unexpected token: 827 | 123def, 828 | ^", 829 | ), 830 | ..Default::default() 831 | }) 832 | .unwrap(); 833 | } 834 | 835 | #[test] 836 | fn test_parse_error_leading_zero() { 837 | test_format(FormatTest { 838 | input: r##"{ 839 | non_string_literals: [ 840 | 0, 841 | 0., 842 | 0.0, 843 | 0.000, 844 | .0, 845 | .000, 846 | +0.678900, 847 | -0.678900, 848 | -01.67890, 849 | ] 850 | } 851 | "##, 852 | error: Some( 853 | "Parse error: 11:9: Unexpected token: 854 | -01.67890, 855 | ^", 856 | ), 857 | ..Default::default() 858 | }) 859 | .unwrap(); 860 | } 861 | 862 | #[test] 863 | fn test_parse_error_expected_object() { 864 | test_format(FormatTest { 865 | input: r##"{ 866 | property: [} 867 | } 868 | "##, 869 | error: Some(r#"Parse error: 2:16: Invalid Object token found while parsing an Array of 0 items (mismatched braces?): 870 | property: [} 871 | ^"#), 872 | ..Default::default() 873 | }) 874 | .unwrap(); 875 | } 876 | 877 | #[test] 878 | fn test_parse_error_expected_array() { 879 | test_format(FormatTest { 880 | input: r##"{ 881 | property: {] 882 | } 883 | "##, 884 | error: Some(r#"Parse error: 2:16: Invalid Array token found while parsing an Object of 0 properties (mismatched braces?): 885 | property: {] 886 | ^"#), 887 | ..Default::default() 888 | }) 889 | .unwrap(); 890 | } 891 | 892 | #[test] 893 | fn test_parse_error_mismatched_braces() { 894 | test_format(FormatTest { 895 | input: r##"{ 896 | property_1: "value1", 897 | property_2: "value2","##, 898 | error: Some( 899 | r#"Parse error: 3:25: Mismatched braces in the document: 900 | property_2: "value2", 901 | ^"#, 902 | ), 903 | ..Default::default() 904 | }) 905 | .unwrap(); 906 | } 907 | 908 | #[test] 909 | fn test_property_name_separator_missing() { 910 | test_format(FormatTest { 911 | input: r##"{ 912 | property_1 "value1", 913 | } 914 | "##, 915 | error: Some( 916 | r#"Parse error: 2:5: Unexpected token: 917 | property_1 "value1", 918 | ^"#, 919 | ), 920 | ..Default::default() 921 | }) 922 | .unwrap(); 923 | } 924 | 925 | #[test] 926 | fn test_parse_error_quoted_property_name_separator_missing() { 927 | test_format(FormatTest { 928 | input: r##"{ 929 | "property_1" "value1", 930 | } 931 | "##, 932 | error: Some( 933 | r#"Parse error: 2:17: Property name separator (:) missing: 934 | "property_1" "value1", 935 | ^"#, 936 | ), 937 | ..Default::default() 938 | }) 939 | .unwrap(); 940 | } 941 | 942 | #[test] 943 | fn test_parse_error_extra_comma_between_properties() { 944 | test_format(FormatTest { 945 | input: r##"{ 946 | property_1: "value1", 947 | , 948 | property_2: "value2", 949 | } 950 | "##, 951 | error: Some( 952 | "Parse error: 3:5: Unexpected comma without a preceding property: 953 | , 954 | ^", 955 | ), 956 | ..Default::default() 957 | }) 958 | .unwrap(); 959 | } 960 | 961 | #[test] 962 | fn test_parse_error_comma_before_first_property() { 963 | test_format(FormatTest { 964 | input: r##"{ 965 | , 966 | property_1: "value1", 967 | property_2: "value2", 968 | } 969 | "##, 970 | error: Some( 971 | "Parse error: 2:5: Unexpected comma without a preceding property: 972 | , 973 | ^", 974 | ), 975 | ..Default::default() 976 | }) 977 | .unwrap(); 978 | } 979 | 980 | #[test] 981 | fn test_parse_error_extra_comma_between_array_items() { 982 | test_format(FormatTest { 983 | input: r##"[ 984 | "value1", 985 | , 986 | "value2", 987 | ]"##, 988 | error: Some( 989 | "Parse error: 3:5: Unexpected comma without a preceding array item value: 990 | , 991 | ^", 992 | ), 993 | ..Default::default() 994 | }) 995 | .unwrap(); 996 | } 997 | 998 | #[test] 999 | fn test_parse_error_comma_before_first_array_item() { 1000 | test_format(FormatTest { 1001 | input: r##"[ 1002 | , 1003 | "value1", 1004 | "value2", 1005 | ]"##, 1006 | error: Some( 1007 | "Parse error: 2:5: Unexpected comma without a preceding array item value: 1008 | , 1009 | ^", 1010 | ), 1011 | ..Default::default() 1012 | }) 1013 | .unwrap(); 1014 | } 1015 | 1016 | #[test] 1017 | fn test_parse_error_quoted_property_name_and_comma_looks_like_a_value() { 1018 | test_format(FormatTest { 1019 | input: r##"{ 1020 | property_1: "value1", 1021 | "value2", 1022 | } 1023 | "##, 1024 | error: Some( 1025 | r#"Parse error: 3:13: Property name separator (:) missing: 1026 | "value2", 1027 | ^"#, 1028 | ), 1029 | ..Default::default() 1030 | }) 1031 | .unwrap(); 1032 | } 1033 | 1034 | #[test] 1035 | fn test_parse_error_value_without_property_name() { 1036 | test_format(FormatTest { 1037 | input: r##"{ 1038 | property_1: "value1", 1039 | false, 1040 | } 1041 | "##, 1042 | error: Some( 1043 | "Parse error: 3:5: Object values require property names: 1044 | false, 1045 | ^~~~~", 1046 | ), 1047 | ..Default::default() 1048 | }) 1049 | .unwrap(); 1050 | } 1051 | 1052 | #[test] 1053 | fn test_parse_error_unclosed_string() { 1054 | test_format(FormatTest { 1055 | input: r##"{ 1056 | property: "bad quotes', 1057 | } 1058 | "##, 1059 | error: Some( 1060 | r#"Parse error: 2:16: Unclosed string: 1061 | property: "bad quotes', 1062 | ^"#, 1063 | ), 1064 | ..Default::default() 1065 | }) 1066 | .unwrap(); 1067 | } 1068 | 1069 | #[test] 1070 | fn test_parse_error_not_json() { 1071 | test_format(FormatTest { 1072 | input: r##" 1073 | # Fuchsia 1074 | 1075 | Pink + Purple == Fuchsia (a new operating system) 1076 | 1077 | ## How can I build and run Fuchsia? 1078 | 1079 | See [Getting Started](https://fuchsia.dev/fuchsia-src/getting_started.md). 1080 | 1081 | ## Where can I learn more about Fuchsia? 1082 | 1083 | See [fuchsia.dev](https://fuchsia.dev). 1084 | "##, 1085 | error: Some( 1086 | r#"Parse error: 2:1: Unexpected token: 1087 | # Fuchsia 1088 | ^"#, 1089 | ), 1090 | ..Default::default() 1091 | }) 1092 | .unwrap(); 1093 | } 1094 | 1095 | #[test] 1096 | fn test_options() { 1097 | let options = FormatOptions { ..Default::default() }; 1098 | assert_eq!(options.indent_by, 4); 1099 | assert!(options.trailing_commas); 1100 | assert!(!options.collapse_containers_of_one); 1101 | assert!(!options.sort_array_items); 1102 | 1103 | let options = FormatOptions { 1104 | indent_by: 2, 1105 | trailing_commas: false, 1106 | collapse_containers_of_one: true, 1107 | sort_array_items: true, 1108 | options_by_path: hashmap! { 1109 | "/*" => hashset! { 1110 | PathOption::PropertyNameOrder(vec![ 1111 | "program", 1112 | "use", 1113 | "expose", 1114 | "offer", 1115 | "children", 1116 | "collections", 1117 | "storage", 1118 | "facets", 1119 | "runners", 1120 | "resolvers", 1121 | "environments", 1122 | ]), 1123 | }, 1124 | "/*/use" => hashset! { 1125 | PathOption::TrailingCommas(false), 1126 | PathOption::CollapseContainersOfOne(false), 1127 | PathOption::SortArrayItems(true), 1128 | PathOption::PropertyNameOrder(vec![ 1129 | "name", 1130 | "url", 1131 | "startup", 1132 | "environment", 1133 | "durability", 1134 | "service", 1135 | "protocol", 1136 | "directory", 1137 | "storage", 1138 | "runner", 1139 | "resolver", 1140 | "to", 1141 | "from", 1142 | "as", 1143 | "rights", 1144 | "subdir", 1145 | "path", 1146 | "dependency", 1147 | ]), 1148 | }, 1149 | "/*/use/service" => hashset! { 1150 | PathOption::SortArrayItems(true), 1151 | }, 1152 | }, 1153 | }; 1154 | 1155 | assert_eq!(options.indent_by, 2); 1156 | assert!(!options.trailing_commas); 1157 | assert!(options.collapse_containers_of_one); 1158 | assert!(options.sort_array_items); 1159 | 1160 | let path_options = options 1161 | .options_by_path 1162 | .get("/*/use") 1163 | .expect("Expected to find path options for the given path"); 1164 | match path_options 1165 | .get(&PathOption::TrailingCommas(true)) 1166 | .expect("Expected to find a PathOption::TrailingCommas setting") 1167 | { 1168 | PathOption::TrailingCommas(trailing_commas) => assert!(!(*trailing_commas)), 1169 | _ => panic!("PathOption enum as key should return a value of the same type"), 1170 | }; 1171 | match path_options 1172 | .get(&PathOption::CollapseContainersOfOne(true)) 1173 | .expect("Expected to find a PathOption::CollapseContainersOfOne setting") 1174 | { 1175 | PathOption::CollapseContainersOfOne(collapsed_container_of_one) => { 1176 | assert!(!(*collapsed_container_of_one)) 1177 | } 1178 | _ => panic!("PathOption enum as key should return a value of the same type"), 1179 | }; 1180 | match path_options 1181 | .get(&PathOption::SortArrayItems(true)) 1182 | .expect("Expected to find a PathOption::SortArrayItems setting") 1183 | { 1184 | PathOption::SortArrayItems(sort_array_items) => assert!(*sort_array_items), 1185 | _ => panic!("PathOption enum as key should return a value of the same type"), 1186 | }; 1187 | match path_options 1188 | .get(&PathOption::PropertyNameOrder(vec![])) 1189 | .expect("Expected to find a PathOption::PropertyNameOrder setting") 1190 | { 1191 | PathOption::PropertyNameOrder(property_names) => assert_eq!(property_names[1], "url"), 1192 | _ => panic!("PathOption enum as key should return a value of the same type"), 1193 | }; 1194 | } 1195 | 1196 | #[test] 1197 | fn test_duplicated_key_in_subpath_options_is_ignored() { 1198 | let options = FormatOptions { 1199 | options_by_path: hashmap! { 1200 | "/*/use" => hashset! { 1201 | PathOption::TrailingCommas(false), 1202 | PathOption::CollapseContainersOfOne(false), 1203 | PathOption::SortArrayItems(true), 1204 | PathOption::PropertyNameOrder(vec![ 1205 | "name", 1206 | "url", 1207 | "startup", 1208 | "environment", 1209 | "durability", 1210 | "service", 1211 | "protocol", 1212 | "directory", 1213 | "storage", 1214 | "runner", 1215 | "resolver", 1216 | "to", 1217 | "from", 1218 | "as", 1219 | "rights", 1220 | "subdir", 1221 | "path", 1222 | "dependency", 1223 | ]), 1224 | PathOption::SortArrayItems(false), 1225 | }, 1226 | }, 1227 | ..Default::default() 1228 | }; 1229 | 1230 | match options.options_by_path.get("/*/use") { 1231 | Some(path_options) => { 1232 | match path_options.get(&PathOption::TrailingCommas(true)) { 1233 | Some(path_option) => match path_option { 1234 | PathOption::TrailingCommas(trailing_commas) => { 1235 | assert!(!(*trailing_commas)); 1236 | } 1237 | _ => panic!("PathOption enum as key should return a value of the same type"), 1238 | }, 1239 | None => panic!("Expected to find a PathOption::TrailingCommas setting"), 1240 | } 1241 | match path_options.get(&PathOption::CollapseContainersOfOne(true)) { 1242 | Some(path_option) => match path_option { 1243 | PathOption::CollapseContainersOfOne(collapsed_container_of_one) => { 1244 | assert!(!(*collapsed_container_of_one)); 1245 | } 1246 | _ => panic!("PathOption enum as key should return a value of the same type"), 1247 | }, 1248 | None => panic!("Expected to find a PathOption::CollapseContainersOfOne setting"), 1249 | } 1250 | match path_options.get(&PathOption::SortArrayItems(true)) { 1251 | Some(path_option) => match path_option { 1252 | PathOption::SortArrayItems(sort_array_items) => { 1253 | assert!(*sort_array_items); 1254 | } 1255 | _ => panic!("PathOption enum as key should return a value of the same type"), 1256 | }, 1257 | None => panic!("Expected to find a PathOption::SortArrayItems setting"), 1258 | } 1259 | match path_options.get(&PathOption::PropertyNameOrder(vec![])) { 1260 | Some(path_option) => match path_option { 1261 | PathOption::PropertyNameOrder(property_names) => { 1262 | assert_eq!(property_names[1], "url"); 1263 | } 1264 | _ => panic!("PathOption enum as key should return a value of the same type"), 1265 | }, 1266 | None => panic!("Expected to find a PathOption::PropertyNamePriorities setting"), 1267 | } 1268 | } 1269 | None => panic!("Expected to find path options for the given path"), 1270 | } 1271 | } 1272 | 1273 | #[test] 1274 | fn test_format_options() { 1275 | test_format(FormatTest { 1276 | options: Some(FormatOptions { 1277 | collapse_containers_of_one: true, 1278 | sort_array_items: true, // but use options_by_path to turn this off for program args 1279 | options_by_path: hashmap! { 1280 | "/*" => hashset! { 1281 | PathOption::PropertyNameOrder(vec![ 1282 | "program", 1283 | "children", 1284 | "collections", 1285 | "use", 1286 | "offer", 1287 | "expose", 1288 | "resolvers", 1289 | "runners", 1290 | "storage", 1291 | "environments", 1292 | "facets", 1293 | ]) 1294 | }, 1295 | "/*/program" => hashset! { 1296 | PathOption::CollapseContainersOfOne(false), 1297 | PathOption::PropertyNameOrder(vec![ 1298 | "binary", 1299 | "args", 1300 | ]) 1301 | }, 1302 | "/*/program/args" => hashset! { 1303 | PathOption::SortArrayItems(false), 1304 | }, 1305 | "/*/*/*" => hashset! { 1306 | PathOption::PropertyNameOrder(vec![ 1307 | "name", 1308 | "url", 1309 | "startup", 1310 | "environment", 1311 | "durability", 1312 | "service", 1313 | "protocol", 1314 | "directory", 1315 | "resolver", 1316 | "runner", 1317 | "storage", 1318 | "from", 1319 | "as", 1320 | "to", 1321 | "rights", 1322 | "path", 1323 | "subdir", 1324 | "event", 1325 | "dependency", 1326 | "extends", 1327 | "resolvers", 1328 | ]) 1329 | }, 1330 | }, 1331 | ..Default::default() 1332 | }), 1333 | input: r##"{ 1334 | offer: [ 1335 | { 1336 | runner: "elf", 1337 | }, 1338 | { 1339 | from: "framework", 1340 | to: "#elements", 1341 | protocol: "/svc/fuchsia.sys2.Realm", 1342 | }, 1343 | { 1344 | to: "#elements", 1345 | protocol: [ 1346 | "/svc/fuchsia.logger.LogSink", 1347 | "/svc/fuchsia.cobalt.LoggerFactory", 1348 | ], 1349 | from: "realm", 1350 | }, 1351 | ], 1352 | collections: [ 1353 | "elements", 1354 | ], 1355 | use: [ 1356 | { 1357 | runner: "elf", 1358 | }, 1359 | { 1360 | protocol: "/svc/fuchsia.sys2.Realm", 1361 | from: "framework", 1362 | }, 1363 | { 1364 | to: "#elements", 1365 | from: "realm", 1366 | protocol: [ 1367 | "/svc/fuchsia.logger.LogSink", 1368 | "/svc/fuchsia.cobalt.LoggerFactory", 1369 | ], 1370 | }, 1371 | ], 1372 | children: [ 1373 | ], 1374 | program: { 1375 | binary: "bin/session_manager", 1376 | }, 1377 | } 1378 | "##, 1379 | expected: r##"{ 1380 | program: { 1381 | binary: "bin/session_manager", 1382 | }, 1383 | children: [], 1384 | collections: [ "elements" ], 1385 | use: [ 1386 | { runner: "elf" }, 1387 | { 1388 | protocol: "/svc/fuchsia.sys2.Realm", 1389 | from: "framework", 1390 | }, 1391 | { 1392 | protocol: [ 1393 | "/svc/fuchsia.cobalt.LoggerFactory", 1394 | "/svc/fuchsia.logger.LogSink", 1395 | ], 1396 | from: "realm", 1397 | to: "#elements", 1398 | }, 1399 | ], 1400 | offer: [ 1401 | { runner: "elf" }, 1402 | { 1403 | protocol: "/svc/fuchsia.sys2.Realm", 1404 | from: "framework", 1405 | to: "#elements", 1406 | }, 1407 | { 1408 | protocol: [ 1409 | "/svc/fuchsia.cobalt.LoggerFactory", 1410 | "/svc/fuchsia.logger.LogSink", 1411 | ], 1412 | from: "realm", 1413 | to: "#elements", 1414 | }, 1415 | ], 1416 | } 1417 | "##, 1418 | ..Default::default() 1419 | }) 1420 | .unwrap(); 1421 | } 1422 | 1423 | #[test] 1424 | fn test_no_trailing_commas() { 1425 | test_format(FormatTest { 1426 | options: Some(FormatOptions { trailing_commas: false, ..Default::default() }), 1427 | input: r##"{ 1428 | offer: [ 1429 | { 1430 | runner: "elf", 1431 | }, 1432 | { 1433 | from: "framework", 1434 | to: "#elements", 1435 | protocol: "/svc/fuchsia.sys2.Realm", 1436 | }, 1437 | { 1438 | to: "#elements", 1439 | protocol: [ 1440 | "/svc/fuchsia.logger.LogSink", 1441 | "/svc/fuchsia.cobalt.LoggerFactory", 1442 | ], 1443 | from: "realm", 1444 | }, 1445 | ], 1446 | collections: [ 1447 | "elements", 1448 | ], 1449 | use: [ 1450 | { 1451 | runner: "elf", 1452 | }, 1453 | { 1454 | protocol: "/svc/fuchsia.sys2.Realm", 1455 | from: "framework", 1456 | }, 1457 | { 1458 | from: "realm", 1459 | to: "#elements", 1460 | protocol: [ 1461 | "/svc/fuchsia.logger.LogSink", 1462 | "/svc/fuchsia.cobalt.LoggerFactory", 1463 | ], 1464 | }, 1465 | ], 1466 | children: [ 1467 | ], 1468 | program: { 1469 | binary: "bin/session_manager", 1470 | }, 1471 | } 1472 | "##, 1473 | expected: r##"{ 1474 | offer: [ 1475 | { 1476 | runner: "elf" 1477 | }, 1478 | { 1479 | from: "framework", 1480 | to: "#elements", 1481 | protocol: "/svc/fuchsia.sys2.Realm" 1482 | }, 1483 | { 1484 | to: "#elements", 1485 | protocol: [ 1486 | "/svc/fuchsia.logger.LogSink", 1487 | "/svc/fuchsia.cobalt.LoggerFactory" 1488 | ], 1489 | from: "realm" 1490 | } 1491 | ], 1492 | collections: [ 1493 | "elements" 1494 | ], 1495 | use: [ 1496 | { 1497 | runner: "elf" 1498 | }, 1499 | { 1500 | protocol: "/svc/fuchsia.sys2.Realm", 1501 | from: "framework" 1502 | }, 1503 | { 1504 | from: "realm", 1505 | to: "#elements", 1506 | protocol: [ 1507 | "/svc/fuchsia.logger.LogSink", 1508 | "/svc/fuchsia.cobalt.LoggerFactory" 1509 | ] 1510 | } 1511 | ], 1512 | children: [], 1513 | program: { 1514 | binary: "bin/session_manager" 1515 | } 1516 | } 1517 | "##, 1518 | ..Default::default() 1519 | }) 1520 | .unwrap(); 1521 | } 1522 | 1523 | #[test] 1524 | fn test_collapse_containers_of_one() { 1525 | test_format(FormatTest { 1526 | options: Some(FormatOptions { collapse_containers_of_one: true, ..Default::default() }), 1527 | input: r##"{ 1528 | offer: [ 1529 | { 1530 | runner: "elf", 1531 | }, 1532 | { 1533 | from: "framework", 1534 | to: "#elements", 1535 | protocol: "/svc/fuchsia.sys2.Realm", 1536 | }, 1537 | { 1538 | to: "#elements", 1539 | protocol: [ 1540 | "/svc/fuchsia.logger.LogSink", 1541 | "/svc/fuchsia.cobalt.LoggerFactory", 1542 | ], 1543 | from: "realm", 1544 | }, 1545 | ], 1546 | collections: [ 1547 | "elements", 1548 | ], 1549 | use: [ 1550 | { 1551 | runner: "elf", 1552 | }, 1553 | { 1554 | protocol: "/svc/fuchsia.sys2.Realm", 1555 | from: "framework", 1556 | }, 1557 | { 1558 | from: "realm", 1559 | to: "#elements", 1560 | protocol: [ 1561 | "/svc/fuchsia.logger.LogSink", 1562 | "/svc/fuchsia.cobalt.LoggerFactory", 1563 | ], 1564 | }, 1565 | ], 1566 | children: [ 1567 | ], 1568 | program: { 1569 | binary: "bin/session_manager", 1570 | }, 1571 | } 1572 | "##, 1573 | expected: r##"{ 1574 | offer: [ 1575 | { runner: "elf" }, 1576 | { 1577 | from: "framework", 1578 | to: "#elements", 1579 | protocol: "/svc/fuchsia.sys2.Realm", 1580 | }, 1581 | { 1582 | to: "#elements", 1583 | protocol: [ 1584 | "/svc/fuchsia.logger.LogSink", 1585 | "/svc/fuchsia.cobalt.LoggerFactory", 1586 | ], 1587 | from: "realm", 1588 | }, 1589 | ], 1590 | collections: [ "elements" ], 1591 | use: [ 1592 | { runner: "elf" }, 1593 | { 1594 | protocol: "/svc/fuchsia.sys2.Realm", 1595 | from: "framework", 1596 | }, 1597 | { 1598 | from: "realm", 1599 | to: "#elements", 1600 | protocol: [ 1601 | "/svc/fuchsia.logger.LogSink", 1602 | "/svc/fuchsia.cobalt.LoggerFactory", 1603 | ], 1604 | }, 1605 | ], 1606 | children: [], 1607 | program: { binary: "bin/session_manager" }, 1608 | } 1609 | "##, 1610 | ..Default::default() 1611 | }) 1612 | .unwrap(); 1613 | } 1614 | 1615 | #[test] 1616 | fn test_validate_example_in_documentation() { 1617 | test_format(FormatTest { 1618 | options: Some(FormatOptions { 1619 | options_by_path: hashmap! { 1620 | "/*" => hashset! { 1621 | PathOption::PropertyNameOrder(vec![ 1622 | "name", 1623 | "address", 1624 | "contact_options", 1625 | ]), 1626 | }, 1627 | "/*/name" => hashset! { 1628 | PathOption::PropertyNameOrder(vec![ 1629 | "first", 1630 | "middle", 1631 | "last", 1632 | "suffix", 1633 | ]), 1634 | }, 1635 | "/*/*/*" => hashset! { 1636 | PathOption::PropertyNameOrder(vec![ 1637 | "work", 1638 | "home", 1639 | "other", 1640 | ]), 1641 | }, 1642 | "/*/*/*/work" => hashset! { 1643 | PathOption::PropertyNameOrder(vec![ 1644 | "phone", 1645 | "email", 1646 | ]), 1647 | }, 1648 | }, 1649 | ..Default::default() 1650 | }), 1651 | input: r##"{ 1652 | name: { 1653 | last: "Smith", 1654 | first: "John", 1655 | middle: "Jacob", 1656 | }, 1657 | address: { 1658 | city: "Anytown", 1659 | country: "USA", 1660 | state: "New York", 1661 | street: "101 Main Street", 1662 | }, 1663 | contact_options: [ 1664 | { 1665 | other: { 1666 | email: "volunteering@serviceprojectsrus.org", 1667 | }, 1668 | home: { 1669 | email: "jj@notreallygmail.com", 1670 | phone: "212-555-4321", 1671 | }, 1672 | }, 1673 | { 1674 | home: { 1675 | email: "john.smith@notreallygmail.com", 1676 | phone: "212-555-2222", 1677 | }, 1678 | work: { 1679 | email: "john.j.smith@worksforme.gov", 1680 | phone: "212-555-1234", 1681 | }, 1682 | }, 1683 | ], 1684 | } 1685 | "##, 1686 | expected: r##"{ 1687 | name: { 1688 | first: "John", 1689 | middle: "Jacob", 1690 | last: "Smith", 1691 | }, 1692 | address: { 1693 | city: "Anytown", 1694 | country: "USA", 1695 | state: "New York", 1696 | street: "101 Main Street", 1697 | }, 1698 | contact_options: [ 1699 | { 1700 | home: { 1701 | email: "jj@notreallygmail.com", 1702 | phone: "212-555-4321", 1703 | }, 1704 | other: { 1705 | email: "volunteering@serviceprojectsrus.org", 1706 | }, 1707 | }, 1708 | { 1709 | work: { 1710 | phone: "212-555-1234", 1711 | email: "john.j.smith@worksforme.gov", 1712 | }, 1713 | home: { 1714 | email: "john.smith@notreallygmail.com", 1715 | phone: "212-555-2222", 1716 | }, 1717 | }, 1718 | ], 1719 | } 1720 | "##, 1721 | ..Default::default() 1722 | }) 1723 | .unwrap(); 1724 | } 1725 | 1726 | #[test] 1727 | fn test_parse_error_block_comment_not_closed() { 1728 | test_format(FormatTest { 1729 | input: r##" 1730 | /* 1731 | Block comment 1 1732 | *//* first line of 1733 | Unclosed block comment 2 1734 | "##, 1735 | error: Some( 1736 | r#"Parse error: 4:13: Block comment started without closing "*/": 1737 | *//* first line of 1738 | ^"#, 1739 | ), 1740 | ..Default::default() 1741 | }) 1742 | .unwrap(); 1743 | } 1744 | 1745 | #[test] 1746 | fn test_parse_error_closing_brace_without_opening_brace() { 1747 | test_format(FormatTest { 1748 | input: r##"]"##, 1749 | error: Some( 1750 | r#"Parse error: 1:1: Closing brace without a matching opening brace: 1751 | ] 1752 | ^"#, 1753 | ), 1754 | ..Default::default() 1755 | }) 1756 | .unwrap(); 1757 | 1758 | test_format(FormatTest { 1759 | input: r##" 1760 | 1761 | ]"##, 1762 | error: Some( 1763 | r#"Parse error: 3:3: Closing brace without a matching opening brace: 1764 | ] 1765 | ^"#, 1766 | ), 1767 | ..Default::default() 1768 | }) 1769 | .unwrap(); 1770 | 1771 | test_format(FormatTest { 1772 | input: r##" 1773 | }"##, 1774 | error: Some( 1775 | r#"Parse error: 2:5: Invalid Object token found while parsing an Array of 0 items (mismatched braces?): 1776 | } 1777 | ^"#, 1778 | ), 1779 | ..Default::default() 1780 | }) 1781 | .unwrap(); 1782 | } 1783 | 1784 | #[test] 1785 | fn test_multibyte_unicode_chars() { 1786 | test_format(FormatTest { 1787 | options: None, 1788 | input: concat!( 1789 | r##"/* 1790 | 1791 | 1792 | # 1793 | "##, 1794 | "\u{0010}", 1795 | r##"*//* 1796 | "##, 1797 | "\u{E006F} ", 1798 | r##" 1799 | */"## 1800 | ), 1801 | expected: concat!( 1802 | r##"/* 1803 | 1804 | 1805 | # 1806 | "##, 1807 | "\u{0010}", 1808 | r##"*/ 1809 | 1810 | /* 1811 | "##, 1812 | "\u{E006F} ", 1813 | r##"*/ 1814 | "## 1815 | ), 1816 | ..Default::default() 1817 | }) 1818 | .unwrap(); 1819 | } 1820 | 1821 | #[test] 1822 | fn test_empty_document() { 1823 | test_format(FormatTest { options: None, input: "", expected: "", ..Default::default() }) 1824 | .unwrap(); 1825 | } 1826 | 1827 | fn visit_dir(dir: &Path, cb: &mut F) -> io::Result<()> 1828 | where 1829 | F: FnMut(&DirEntry) -> Result<(), std::io::Error>, 1830 | { 1831 | if !dir.is_dir() { 1832 | Err(io::Error::new( 1833 | io::ErrorKind::Other, 1834 | format!("visit_dir called with an invalid path: {:?}", dir), 1835 | )) 1836 | } else { 1837 | for entry in fs::read_dir(dir)? { 1838 | let entry = entry?; 1839 | let path = entry.path(); 1840 | if path.is_dir() { 1841 | visit_dir(&path, cb)?; 1842 | } else { 1843 | cb(&entry)?; 1844 | } 1845 | } 1846 | Ok(()) 1847 | } 1848 | } 1849 | 1850 | /// This test is used, for example, to validate fixes to bugs found by oss_fuzz 1851 | /// that may have caused the parser to crash instead of either parsing the input 1852 | /// successfully or returning a more graceful parsing error. 1853 | /// 1854 | /// To manually verify test samples, use: 1855 | /// cargo test test_parsing_samples_does_not_crash -- --nocapture 1856 | /// 1857 | /// To print the full error message (including the line and pointer to the 1858 | /// column), use: 1859 | /// JSON5FORMAT_TEST_FULL_ERRORS=1 cargo test test_parsing_samples_does_not_crash -- --nocapture 1860 | /// To point to a different samples directory: 1861 | /// JSON5FORMAT_TEST_SAMPLES_DIR="/tmp/fuzz_corpus" cargo test test_parsing_samples_does_not_crash 1862 | #[test] 1863 | fn test_parsing_samples_does_not_crash() -> Result<(), std::io::Error> { 1864 | let mut count = 0; 1865 | let pathbuf = if let Some(samples_dir) = option_env!("JSON5FORMAT_TEST_SAMPLES_DIR") { 1866 | PathBuf::from(samples_dir) 1867 | } else { 1868 | let mut manifest_samples = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 1869 | manifest_samples.push("samples"); 1870 | manifest_samples 1871 | }; 1872 | visit_dir(pathbuf.as_path(), &mut |entry| { 1873 | count += 1; 1874 | let filename = entry.path().into_os_string().to_string_lossy().to_string(); 1875 | let mut buffer = String::new(); 1876 | println!("{}. Parsing: {} ...", count, filename); 1877 | if let Err(err) = fs::File::open(entry.path())?.read_to_string(&mut buffer) { 1878 | println!("Ignoring failure to read the file into a string: {:?}", err); 1879 | return Ok(()); 1880 | } 1881 | let result = ParsedDocument::from_string(buffer, Some(filename.clone())); 1882 | match result { 1883 | Ok(_parsed_document) => { 1884 | println!(" ... Success"); 1885 | Ok(()) 1886 | } 1887 | Err(err @ Error::Parse(..)) => { 1888 | if option_env!("JSON5FORMAT_TEST_FULL_ERRORS") == Some("1") { 1889 | println!(" ... Handled input error:\n{}", err); 1890 | } else if let Error::Parse(some_loc, message) = err { 1891 | let loc_string = if let Some(loc) = some_loc { 1892 | format!(" at {}:{}", loc.line, loc.col) 1893 | } else { 1894 | "".to_owned() 1895 | }; 1896 | let mut first_line = message.lines().next().unwrap(); 1897 | // strip the colon off the end of the first line of a parser error message 1898 | first_line = &first_line[0..first_line.len() - 1]; 1899 | println!(" ... Handled input error{}: {}", loc_string, first_line); 1900 | } 1901 | 1902 | // It's OK if the input file is bad, as long as the parser fails 1903 | // gracefully. 1904 | Ok(()) 1905 | } 1906 | Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), 1907 | } 1908 | }) 1909 | } 1910 | --------------------------------------------------------------------------------