File: json0.go 1 /* 2 The MIT License (MIT) 3 4 Copyright (c) 2026 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the "Software"), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 Single-file source-code for json0: this version has no http(s) support. Even 27 the unit-tests from the original json0 are omitted. 28 29 To compile a smaller-sized command-line app, you can use the `go` command as 30 follows: 31 32 go build -ldflags "-s -w" -trimpath json0.go 33 */ 34 35 package main 36 37 import ( 38 "bufio" 39 "errors" 40 "io" 41 "os" 42 "strconv" 43 "unicode" 44 ) 45 46 const info = ` 47 json0 [options...] [file...] 48 49 50 JSON-0 converts/fixes JSON/pseudo-JSON input into minimal JSON output. 51 Its output is always a single line, which ends with a line-feed. 52 53 Besides minimizing bytes, this tool also adapts almost-JSON input into 54 valid JSON, since it 55 56 - ignores extra/trailing commas in arrays and objects 57 - ignores rest-of-line, multi-line, and even unix-style comments 58 - ignores final 'n' in javascript bigint literals 59 - ignores final trailing semicolon 60 - turns single-quoted strings/keys into double-quoted strings 61 - turns parentheses into proper square-bracket array delimiters 62 - turns PYON (PYthon Object Notation) into its valid JSON counterpart 63 - double-quotes unquoted object keys 64 - changes \x 2-hex-digit into \u 4-hex-digit string-escapes 65 66 All options available can either start with a single or a double-dash 67 68 -h, -help show this help message 69 -jsonl emit JSON Lines, when top-level value is an array 70 ` 71 72 const ( 73 bufSize = 32 * 1024 74 chunkPeekSize = 32 75 ) 76 77 // escapedStringBytes helps funcs like handleString treat all strings 78 // quickly and correctly, using their officially-supported JSON escape 79 // sequences 80 // 81 // https://www.rfc-editor.org/rfc/rfc8259#section-7 82 var escapedStringBytes = [128][]byte{ 83 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, 84 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, 85 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, 86 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, 87 {'\\', 'b'}, {'\\', 't'}, 88 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, 89 {'\\', 'f'}, {'\\', 'r'}, 90 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, 91 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, 92 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, 93 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, 94 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, 95 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, 96 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, 97 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, 98 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, 99 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, 100 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, 101 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, 102 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, 103 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, 104 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, 105 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, 106 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, 107 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, 108 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, 109 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, 110 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, 111 } 112 113 func main() { 114 args := os.Args[1:] 115 buffered := false 116 handler := json0 117 118 for len(args) > 0 { 119 switch args[0] { 120 case `-b`, `--b`, `-buffered`, `--buffered`: 121 buffered = true 122 args = args[1:] 123 continue 124 125 case `-h`, `--h`, `-help`, `--help`: 126 os.Stdout.WriteString(info[1:]) 127 return 128 129 case `-jsonl`, `--jsonl`: 130 handler = jsonl 131 args = args[1:] 132 continue 133 } 134 135 break 136 } 137 138 if len(args) > 0 && args[0] == `--` { 139 args = args[1:] 140 } 141 142 if len(args) > 1 { 143 const msg = "multiple inputs aren't allowed\n" 144 os.Stderr.WriteString(msg) 145 os.Exit(1) 146 return 147 } 148 149 liveLines := !buffered 150 if !buffered { 151 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil { 152 liveLines = false 153 } 154 } 155 156 name := `-` 157 if len(args) == 1 { 158 name = args[0] 159 } 160 161 if err := run(os.Stdout, name, handler, liveLines); err != nil && err != io.EOF { 162 os.Stderr.WriteString(err.Error()) 163 os.Stderr.WriteString("\n") 164 os.Exit(1) 165 return 166 } 167 } 168 169 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, live bool) error 170 171 func run(w io.Writer, name string, handler handlerFunc, live bool) error { 172 // f, _ := os.Create(`json0.prof`) 173 // defer f.Close() 174 // pprof.StartCPUProfile(f) 175 // defer pprof.StopCPUProfile() 176 177 if name == `` || name == `-` { 178 bw := bufio.NewWriterSize(w, bufSize) 179 br := bufio.NewReaderSize(os.Stdin, bufSize) 180 defer bw.Flush() 181 return handler(bw, br, live) 182 } 183 184 f, err := os.Open(name) 185 if err != nil { 186 return errors.New(`can't read from file named "` + name + `"`) 187 } 188 defer f.Close() 189 190 bw := bufio.NewWriterSize(w, bufSize) 191 br := bufio.NewReaderSize(f, bufSize) 192 defer bw.Flush() 193 return handler(bw, br, live) 194 } 195 196 var ( 197 errCommentEarlyEnd = errors.New(`unexpected early-end of comment`) 198 errInputEarlyEnd = errors.New(`expected end of input data`) 199 errInvalidComment = errors.New(`expected / or *`) 200 errInvalidHex = errors.New(`expected a base-16 digit`) 201 errInvalidRune = errors.New(`invalid UTF-8 bytes`) 202 errInvalidToken = errors.New(`invalid JSON token`) 203 errNoDigits = errors.New(`expected numeric digits`) 204 errNoStringQuote = errors.New(`expected " or '`) 205 errNoArrayComma = errors.New(`missing comma between array values`) 206 errNoObjectComma = errors.New(`missing comma between key-value pairs`) 207 errStringEarlyEnd = errors.New(`unexpected early-end of string`) 208 errExtraBytes = errors.New(`unexpected extra input bytes`) 209 ) 210 211 // linePosError is a more descriptive kind of error, showing the source of 212 // the input-related problem, as 1-based a line/pos number pair in front 213 // of the error message 214 type linePosError struct { 215 // line is the 1-based line count from the input 216 line int 217 218 // pos is the 1-based `horizontal` position in its line 219 pos int 220 221 // err is the error message to `decorate` with the position info 222 err error 223 } 224 225 // Error satisfies the error interface 226 func (lpe linePosError) Error() string { 227 where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos) 228 return where + `: ` + lpe.err.Error() 229 } 230 231 // isIdentifier improves control-flow of func handleKey, when it handles 232 // unquoted object keys 233 var isIdentifier = [256]bool{ 234 '_': true, 235 236 '0': true, '1': true, '2': true, '3': true, '4': true, 237 '5': true, '6': true, '7': true, '8': true, '9': true, 238 239 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 240 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 241 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 242 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 243 'Y': true, 'Z': true, 244 245 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 246 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 247 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 248 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 249 'y': true, 'z': true, 250 } 251 252 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not 253 // being 0, and normalizes letter-case for the hex letters 254 var matchHex = [256]byte{ 255 '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', 256 '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 257 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 258 'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 259 } 260 261 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean 262 // value isn't used, and is just there to match the signature of func jsonl 263 func json0(w *bufio.Writer, r *bufio.Reader, live bool) error { 264 jr := jsonReader{r, 1, 1} 265 defer w.Flush() 266 267 if err := jr.handleLeadingJunk(); err != nil { 268 return err 269 } 270 271 // handle a single top-level JSON value 272 err := handleValue(w, &jr) 273 274 // end the only output-line with a line-feed; this also avoids showing 275 // error messages on the same line as the main output, since JSON-0 276 // output has no line-feeds before its last byte 277 outputByte(w, '\n') 278 279 if err != nil { 280 return err 281 } 282 return jr.handleTrailingJunk() 283 } 284 285 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func 286 // avoids writing a trailing line-feed, leaving that up to its caller 287 func jsonl(w *bufio.Writer, r *bufio.Reader, live bool) error { 288 jr := jsonReader{r, 1, 1} 289 290 if err := jr.handleLeadingJunk(); err != nil { 291 return err 292 } 293 294 chunk, err := jr.r.Peek(1) 295 if err == nil && len(chunk) >= 1 { 296 switch b := chunk[0]; b { 297 case '[', '(': 298 return handleArrayJSONL(w, &jr, b, live) 299 } 300 } 301 302 // handle a single top-level JSON value 303 err = handleValue(w, &jr) 304 305 // end the only output-line with a line-feed; this also avoids showing 306 // error messages on the same line as the main output, since JSON-0 307 // output has no line-feeds before its last byte 308 outputByte(w, '\n') 309 310 if err != nil { 311 return err 312 } 313 return jr.handleTrailingJunk() 314 } 315 316 // handleArrayJSONL handles top-level arrays for func jsonl 317 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error { 318 if err := jr.demandSyntax(start); err != nil { 319 return err 320 } 321 322 var end byte = ']' 323 if start == '(' { 324 end = ')' 325 } 326 327 for n := 0; true; n++ { 328 // there may be whitespace/comments before the next comma 329 if err := jr.seekNext(); err != nil { 330 return err 331 } 332 333 // handle commas between values, as well as trailing ones 334 comma := false 335 b, _ := jr.peekByte() 336 if b == ',' { 337 jr.discardSyntaxByte() 338 comma = true 339 340 // there may be whitespace/comments before an ending ']' 341 if err := jr.seekNext(); err != nil { 342 return err 343 } 344 b, _ = jr.peekByte() 345 } 346 347 // handle end of array 348 if b == end { 349 jr.discardSyntaxByte() 350 if n > 0 { 351 err := outputByte(w, '\n') 352 if live { 353 w.Flush() 354 } 355 return err 356 } 357 return nil 358 } 359 360 // turn commas between adjacent values into line-feeds, as the 361 // output for this custom func is supposed to be JSON Lines 362 if n > 0 { 363 if !comma { 364 return errNoArrayComma 365 } 366 if err := outputByte(w, '\n'); err != nil { 367 return err 368 } 369 if live { 370 w.Flush() 371 } 372 } 373 374 // handle the next value 375 if err := jr.seekNext(); err != nil { 376 return err 377 } 378 if err := handleValue(w, jr); err != nil { 379 return err 380 } 381 } 382 383 // make the compiler happy 384 return nil 385 } 386 387 // jsonReader reads data via a buffer, keeping track of the input position: 388 // this in turn allows showing much more useful errors, when these happen 389 type jsonReader struct { 390 // r is the actual reader 391 r *bufio.Reader 392 393 // line is the 1-based line-counter for input bytes, and gives errors 394 // useful position info 395 line int 396 397 // pos is the 1-based `horizontal` position in its line, and gives 398 // errors useful position info 399 pos int 400 } 401 402 // improveError makes any error more useful, by giving it info about the 403 // current input-position, as a 1-based line/within-line-position pair 404 func (jr jsonReader) improveError(err error) error { 405 if _, ok := err.(linePosError); ok { 406 return err 407 } 408 409 if err == io.EOF { 410 return linePosError{jr.line, jr.pos, errInputEarlyEnd} 411 } 412 if err != nil { 413 return linePosError{jr.line, jr.pos, err} 414 } 415 return nil 416 } 417 418 func (jr *jsonReader) handleLeadingJunk() error { 419 // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order 420 // mark) gives no useful info if present, as UTF-8 leaves no ambiguity 421 // about byte-order by design 422 jr.skipUTF8BOM() 423 424 // ignore leading whitespace and/or comments 425 return jr.seekNext() 426 } 427 428 func (jr *jsonReader) handleTrailingJunk() error { 429 // ignore trailing whitespace and/or comments 430 if err := jr.seekNext(); err != nil { 431 return err 432 } 433 434 // ignore trailing semicolons 435 for { 436 if b, ok := jr.peekByte(); !ok || b != ';' { 437 break 438 } 439 440 jr.discardSyntaxByte() 441 // ignore trailing whitespace and/or comments 442 if err := jr.seekNext(); err != nil { 443 return err 444 } 445 } 446 447 // beyond trailing whitespace and/or comments, any more bytes 448 // make the whole input data invalid JSON 449 if _, ok := jr.peekByte(); ok { 450 return jr.improveError(errExtraBytes) 451 } 452 return nil 453 } 454 455 // demandSyntax fails with an error when the next byte isn't the one given; 456 // when it is, the byte is then read/skipped, and a nil error is returned 457 func (jr *jsonReader) demandSyntax(syntax byte) error { 458 chunk, err := jr.r.Peek(1) 459 if err == io.EOF { 460 return jr.improveError(errInputEarlyEnd) 461 } 462 if err != nil { 463 return jr.improveError(err) 464 } 465 466 if len(chunk) < 1 || chunk[0] != syntax { 467 msg := `expected ` + string(rune(syntax)) 468 return jr.improveError(errors.New(msg)) 469 } 470 471 jr.discardSyntaxByte() 472 return nil 473 } 474 475 // discardSyntaxByte discards 1 byte, updating the reader's position info 476 func (jr *jsonReader) discardSyntaxByte() { 477 jr.readByte() 478 } 479 480 // peekByte simplifies control-flow for various other funcs 481 func (jr jsonReader) peekByte() (b byte, ok bool) { 482 chunk, err := jr.r.Peek(1) 483 if err == nil && len(chunk) >= 1 { 484 return chunk[0], true 485 } 486 return 0, false 487 } 488 489 // readByte does what it says, updating the reader's position info 490 func (jr *jsonReader) readByte() (b byte, err error) { 491 b, err = jr.r.ReadByte() 492 if err == nil { 493 if b == '\n' { 494 jr.line += 1 495 jr.pos = 1 496 } else { 497 jr.pos++ 498 } 499 return b, nil 500 } 501 return b, jr.improveError(err) 502 } 503 504 // readRune does what it says, updating the reader's position info 505 func (jr *jsonReader) readRune() (r rune, err error) { 506 r, _, err = jr.r.ReadRune() 507 if err == nil { 508 if r == '\n' { 509 jr.line += 1 510 jr.pos = 1 511 } else { 512 jr.pos++ 513 } 514 return r, nil 515 } 516 return r, jr.improveError(err) 517 } 518 519 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols 520 // and comments, either single-line (starting with //) or general (starting 521 // with /* and ending with */) 522 func (jr *jsonReader) seekNext() error { 523 for { 524 b, err := jr.r.ReadByte() 525 if err != nil { 526 return nil 527 } 528 529 switch b { 530 case '\n': 531 jr.line++ 532 jr.pos = 1 533 534 case '#': 535 jr.pos++ 536 if err := jr.skipLine(); err != nil { 537 return err 538 } 539 540 case '/': 541 jr.r.UnreadByte() 542 if err := jr.skipComment(); err != nil { 543 return err 544 } 545 546 default: 547 if b <= 32 { 548 // keep skipping whitespace bytes 549 jr.pos++ 550 } else { 551 // reached the next token 552 jr.r.UnreadByte() 553 return nil 554 } 555 } 556 557 // after comments, keep looking for more whitespace and/or comments 558 } 559 } 560 561 // skipComment helps func seekNext skip over comments, simplifying the latter 562 // func's control-flow 563 func (jr *jsonReader) skipComment() error { 564 err := jr.demandSyntax('/') 565 if err != nil { 566 return err 567 } 568 569 b, ok := jr.peekByte() 570 if !ok { 571 return nil 572 } 573 574 switch b { 575 case '/': 576 // handle single-line comments 577 return jr.skipLine() 578 579 case '*': 580 // handle (potentially) multi-line comments 581 return jr.skipGeneralComment() 582 583 default: 584 return jr.improveError(errInvalidComment) 585 } 586 } 587 588 // skipLine handles single-line comments for func skipComment 589 func (jr *jsonReader) skipLine() error { 590 for { 591 b, err := jr.readByte() 592 if err == io.EOF { 593 // end of input is fine in this case 594 return nil 595 } 596 if err != nil { 597 return err 598 } 599 600 if b == '\n' { 601 return nil 602 } 603 } 604 } 605 606 // skipGeneralComment handles (potentially) multi-line comments for func 607 // skipComment 608 func (jr *jsonReader) skipGeneralComment() error { 609 var prev byte 610 for { 611 b, err := jr.readByte() 612 if err != nil { 613 return jr.improveError(errCommentEarlyEnd) 614 } 615 616 if prev == '*' && b == '/' { 617 return nil 618 } 619 if b == '\n' { 620 jr.line++ 621 jr.pos = 1 622 } 623 prev = b 624 } 625 } 626 627 // skipUTF8BOM does what it says, if a UTF-8 BOM is present 628 func (jr *jsonReader) skipUTF8BOM() { 629 lead, err := jr.r.Peek(3) 630 if err != nil { 631 return 632 } 633 634 if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf { 635 jr.r.Discard(3) 636 jr.pos += 1 637 } 638 } 639 640 // outputByte is a small wrapper on func WriteByte, which adapts any error 641 // into a custom dummy output-error, which is in turn meant to be ignored, 642 // being just an excuse to quit the app immediately and successfully 643 func outputByte(w *bufio.Writer, b byte) error { 644 err := w.WriteByte(b) 645 if err == nil { 646 return nil 647 } 648 return io.EOF 649 } 650 651 // handleArray handles arrays for func handleValue 652 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error { 653 if err := jr.demandSyntax(start); err != nil { 654 return err 655 } 656 657 var end byte = ']' 658 if start == '(' { 659 end = ')' 660 } 661 662 w.WriteByte('[') 663 664 for n := 0; true; n++ { 665 // there may be whitespace/comments before the next comma 666 if err := jr.seekNext(); err != nil { 667 return err 668 } 669 670 // handle commas between values, as well as trailing ones 671 comma := false 672 b, _ := jr.peekByte() 673 if b == ',' { 674 jr.discardSyntaxByte() 675 comma = true 676 677 // there may be whitespace/comments before an ending ']' 678 if err := jr.seekNext(); err != nil { 679 return err 680 } 681 b, _ = jr.peekByte() 682 } 683 684 // handle end of array 685 if b == end { 686 jr.discardSyntaxByte() 687 w.WriteByte(']') 688 return nil 689 } 690 691 // don't forget commas between adjacent values 692 if n > 0 { 693 if !comma { 694 return errNoArrayComma 695 } 696 if err := outputByte(w, ','); err != nil { 697 return err 698 } 699 } 700 701 // handle the next value 702 if err := jr.seekNext(); err != nil { 703 return err 704 } 705 if err := handleValue(w, jr); err != nil { 706 return err 707 } 708 } 709 710 // make the compiler happy 711 return nil 712 } 713 714 // handleDigits helps various number-handling funcs do their job 715 func handleDigits(w *bufio.Writer, jr *jsonReader) error { 716 if trySimpleDigits(w, jr) { 717 return nil 718 } 719 720 for n := 0; true; n++ { 721 b, _ := jr.peekByte() 722 723 // support `nice` long numbers by ignoring their underscores 724 if b == '_' { 725 jr.discardSyntaxByte() 726 continue 727 } 728 729 if '0' <= b && b <= '9' { 730 jr.discardSyntaxByte() 731 w.WriteByte(b) 732 continue 733 } 734 735 if n == 0 { 736 return errNoDigits 737 } 738 return nil 739 } 740 741 // make the compiler happy 742 return nil 743 } 744 745 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes 746 // are just digits: this is a very common case for numbers; returns whether 747 // it succeeded, so this func's caller knows knows if it needs to do anything, 748 // the slower way 749 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) { 750 chunk, _ := jr.r.Peek(chunkPeekSize) 751 752 for i, b := range chunk { 753 if '0' <= b && b <= '9' { 754 continue 755 } 756 757 if i == 0 || b == '_' { 758 return false 759 } 760 761 // bulk-writing the chunk is this func's whole point 762 w.Write(chunk[:i]) 763 764 jr.r.Discard(i) 765 jr.pos += i 766 return true 767 } 768 769 // maybe the digits-run is ok, but it's just longer than the chunk 770 return false 771 } 772 773 // handleDot handles pseudo-JSON numbers which start with a decimal dot 774 func handleDot(w *bufio.Writer, jr *jsonReader) error { 775 if err := jr.demandSyntax('.'); err != nil { 776 return err 777 } 778 w.Write([]byte{'0', '.'}) 779 return handleDigits(w, jr) 780 } 781 782 // handleKey is used by func handleObjects and generalizes func handleString, 783 // by allowing unquoted object keys; it's not used anywhere else, as allowing 784 // unquoted string values is ambiguous with actual JSON-keyword values null, 785 // false, and true. 786 func handleKey(w *bufio.Writer, jr *jsonReader) error { 787 quote, ok := jr.peekByte() 788 if !ok { 789 return jr.improveError(errStringEarlyEnd) 790 } 791 792 if quote == '"' || quote == '\'' { 793 return handleString(w, jr, quote) 794 } 795 796 w.WriteByte('"') 797 for { 798 if b, _ := jr.peekByte(); isIdentifier[b] { 799 jr.discardSyntaxByte() 800 w.WriteByte(b) 801 continue 802 } 803 804 w.WriteByte('"') 805 return nil 806 } 807 } 808 809 // trySimpleString tries to handle (more quickly) inner-strings where all bytes 810 // are unescaped ASCII symbols: this is a very common case for strings, and is 811 // almost always the case for object keys; returns whether it succeeded, so 812 // this func's caller knows knows if it needs to do anything, the slower way 813 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) { 814 end := -1 815 chunk, _ := jr.r.Peek(chunkPeekSize) 816 817 for i, b := range chunk { 818 if b == byte(quote) { 819 end = i 820 break 821 } 822 823 if b < ' ' || b > 127 || b == '\\' || b == '"' { 824 return false 825 } 826 } 827 828 if end < 0 { 829 return false 830 } 831 832 // bulk-writing the chunk is this func's whole point 833 w.WriteByte('"') 834 w.Write(chunk[:end]) 835 w.WriteByte('"') 836 837 jr.r.Discard(end + 1) 838 jr.pos += end + 1 839 return true 840 } 841 842 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue 843 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error { 844 for rest := kw; len(rest) > 0; rest = rest[1:] { 845 b, err := jr.readByte() 846 if err == nil && b == rest[0] { 847 // keywords given to this func have no line-feeds 848 jr.pos++ 849 continue 850 } 851 852 msg := `expected JSON value ` + string(kw) 853 return jr.improveError(errors.New(msg)) 854 } 855 856 w.Write(kw) 857 return nil 858 } 859 860 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error { 861 for rest := kw; len(rest) > 0; rest = rest[1:] { 862 b, err := jr.readByte() 863 if err == nil && b == rest[0] { 864 // keywords given to this func have no line-feeds 865 jr.pos++ 866 continue 867 } 868 869 msg := `expected JSON value ` + string(kw) 870 return jr.improveError(errors.New(msg)) 871 } 872 873 w.Write(with) 874 return nil 875 } 876 877 // handleNegative handles numbers starting with a negative sign for func 878 // handleValue 879 func handleNegative(w *bufio.Writer, jr *jsonReader) error { 880 if err := jr.demandSyntax('-'); err != nil { 881 return err 882 } 883 884 w.WriteByte('-') 885 if b, _ := jr.peekByte(); b == '.' { 886 jr.discardSyntaxByte() 887 w.Write([]byte{'0', '.'}) 888 return handleDigits(w, jr) 889 } 890 return handleNumber(w, jr) 891 } 892 893 // handleNumber handles numeric values/tokens, including invalid-JSON cases, 894 // such as values starting with a decimal dot 895 func handleNumber(w *bufio.Writer, jr *jsonReader) error { 896 // handle integer digits 897 if err := handleDigits(w, jr); err != nil { 898 return err 899 } 900 901 switch b, _ := jr.peekByte(); b { 902 case 'n': 903 // handle optional trailing 'n', used in javascript's bigint literals 904 jr.discardSyntaxByte() 905 return nil 906 907 case '.': 908 // handle optional decimal digits, starting with a leading dot 909 jr.discardSyntaxByte() 910 w.WriteByte('.') 911 return handleDigits(w, jr) 912 913 case 'e', 'E': 914 // handle optional exponent digits 915 jr.discardSyntaxByte() 916 w.WriteByte(b) 917 b, _ = jr.peekByte() 918 if b == '+' { 919 jr.discardSyntaxByte() 920 } else if b == '-' { 921 w.WriteByte('-') 922 jr.discardSyntaxByte() 923 } 924 return handleDigits(w, jr) 925 926 default: 927 // nothing special after some digits 928 return nil 929 } 930 } 931 932 // handleObject handles objects for func handleValue 933 func handleObject(w *bufio.Writer, jr *jsonReader) error { 934 if err := jr.demandSyntax('{'); err != nil { 935 return err 936 } 937 w.WriteByte('{') 938 939 for npairs := 0; true; npairs++ { 940 // there may be whitespace/comments before the next comma 941 if err := jr.seekNext(); err != nil { 942 return err 943 } 944 945 // handle commas between key-value pairs, as well as trailing ones 946 comma := false 947 b, _ := jr.peekByte() 948 if b == ',' { 949 jr.discardSyntaxByte() 950 comma = true 951 952 // there may be whitespace/comments before an ending '}' 953 if err := jr.seekNext(); err != nil { 954 return err 955 } 956 b, _ = jr.peekByte() 957 } 958 959 // handle end of object 960 if b == '}' { 961 jr.discardSyntaxByte() 962 w.WriteByte('}') 963 return nil 964 } 965 966 // don't forget commas between adjacent key-value pairs 967 if npairs > 0 { 968 if !comma { 969 return errNoObjectComma 970 } 971 if err := outputByte(w, ','); err != nil { 972 return err 973 } 974 } 975 976 // handle the next pair's key 977 if err := jr.seekNext(); err != nil { 978 return err 979 } 980 if err := handleKey(w, jr); err != nil { 981 return err 982 } 983 984 // demand a colon right after the key 985 if err := jr.seekNext(); err != nil { 986 return err 987 } 988 if err := jr.demandSyntax(':'); err != nil { 989 return err 990 } 991 w.WriteByte(':') 992 993 // handle the next pair's value 994 if err := jr.seekNext(); err != nil { 995 return err 996 } 997 if err := handleValue(w, jr); err != nil { 998 return err 999 } 1000 } 1001 1002 // make the compiler happy 1003 return nil 1004 } 1005 1006 // handlePositive handles numbers starting with a positive sign for func 1007 // handleValue 1008 func handlePositive(w *bufio.Writer, jr *jsonReader) error { 1009 if err := jr.demandSyntax('+'); err != nil { 1010 return err 1011 } 1012 1013 // valid JSON isn't supposed to have leading pluses on numbers, so 1014 // emit nothing for it, unlike for negative numbers 1015 1016 if b, _ := jr.peekByte(); b == '.' { 1017 jr.discardSyntaxByte() 1018 w.Write([]byte{'0', '.'}) 1019 return handleDigits(w, jr) 1020 } 1021 return handleNumber(w, jr) 1022 } 1023 1024 // handleString handles strings for funcs handleValue and handleObject, and 1025 // supports both single-quotes and double-quotes, always emitting the latter 1026 // in the output, of course 1027 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error { 1028 if quote != '"' && quote != '\'' { 1029 return errNoStringQuote 1030 } 1031 1032 jr.discardSyntaxByte() 1033 1034 // try the quicker no-escapes ASCII handler 1035 if trySimpleString(w, jr, quote) { 1036 return nil 1037 } 1038 1039 // it's a non-trivial inner-string, so handle it byte-by-byte 1040 w.WriteByte('"') 1041 escaped := false 1042 1043 for quote := rune(quote); true; { 1044 r, err := jr.readRune() 1045 if r == unicode.ReplacementChar { 1046 return jr.improveError(errInvalidRune) 1047 } 1048 if err != nil { 1049 if err == io.EOF { 1050 return jr.improveError(errStringEarlyEnd) 1051 } 1052 return jr.improveError(err) 1053 } 1054 1055 if !escaped { 1056 if r == '\\' { 1057 escaped = true 1058 continue 1059 } 1060 1061 // handle end of string 1062 if r == quote { 1063 return outputByte(w, '"') 1064 } 1065 1066 if int(r) <= len(escapedStringBytes) { 1067 w.Write(escapedStringBytes[r]) 1068 } else { 1069 w.WriteRune(r) 1070 } 1071 continue 1072 } 1073 1074 // handle escaped items 1075 escaped = false 1076 1077 switch r { 1078 case 'u': 1079 // \u needs exactly 4 hex-digits to follow it 1080 w.Write([]byte{'\\', 'u'}) 1081 if err := copyHex(w, 4, jr); err != nil { 1082 return jr.improveError(err) 1083 } 1084 1085 case 'x': 1086 // JSON only supports 4 escaped hex-digits, so pad the 2 1087 // expected hex-digits with 2 zeros 1088 w.Write([]byte{'\\', 'u', '0', '0'}) 1089 if err := copyHex(w, 2, jr); err != nil { 1090 return jr.improveError(err) 1091 } 1092 1093 case 't', 'f', 'r', 'n', 'b', '\\', '"': 1094 // handle valid-JSON escaped string sequences 1095 w.WriteByte('\\') 1096 w.WriteByte(byte(r)) 1097 1098 case '\'': 1099 // escaped single-quotes aren't standard JSON, but are needed 1100 // when the input uses non-standard single-quoted strings 1101 w.WriteByte('\'') 1102 1103 default: 1104 if int(r) < len(escapedStringBytes) { 1105 w.Write(escapedStringBytes[r]) 1106 } else { 1107 w.WriteRune(r) 1108 } 1109 } 1110 } 1111 1112 if escaped { 1113 return jr.improveError(errStringEarlyEnd) 1114 } 1115 return nil 1116 } 1117 1118 // copyHex handles a run of hex-digits for func handleString, starting right 1119 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its 1120 // errors with position info: that's up to the caller 1121 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error { 1122 for i := 0; i < n; i++ { 1123 b, err := jr.readByte() 1124 if err == io.EOF { 1125 return errStringEarlyEnd 1126 } 1127 if err != nil { 1128 return err 1129 } 1130 1131 if b >= 128 { 1132 return errInvalidHex 1133 } 1134 1135 if b := matchHex[b]; b != 0 { 1136 w.WriteByte(b) 1137 continue 1138 } 1139 1140 return errInvalidHex 1141 } 1142 1143 return nil 1144 } 1145 1146 // handleValue is a generic JSON-token handler, which allows the recursive 1147 // behavior to handle any kind of JSON/pseudo-JSON input 1148 func handleValue(w *bufio.Writer, jr *jsonReader) error { 1149 chunk, err := jr.r.Peek(1) 1150 if err == nil && len(chunk) >= 1 { 1151 return handleValueDispatch(w, jr, chunk[0]) 1152 } 1153 1154 if err == io.EOF { 1155 return jr.improveError(errInputEarlyEnd) 1156 } 1157 return jr.improveError(errInputEarlyEnd) 1158 } 1159 1160 // handleValueDispatch simplifies control-flow for func handleValue 1161 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error { 1162 switch b { 1163 case '#': 1164 return jr.skipLine() 1165 case 'f': 1166 return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'}) 1167 case 'n': 1168 return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'}) 1169 case 't': 1170 return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'}) 1171 case 'F': 1172 return replaceKeyword(w, jr, []byte(`False`), []byte(`false`)) 1173 case 'N': 1174 return replaceKeyword(w, jr, []byte(`None`), []byte(`null`)) 1175 case 'T': 1176 return replaceKeyword(w, jr, []byte(`True`), []byte(`true`)) 1177 case '.': 1178 return handleDot(w, jr) 1179 case '+': 1180 return handlePositive(w, jr) 1181 case '-': 1182 return handleNegative(w, jr) 1183 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 1184 return handleNumber(w, jr) 1185 case '\'', '"': 1186 return handleString(w, jr, b) 1187 case '[', '(': 1188 return handleArray(w, jr, b) 1189 case '{': 1190 return handleObject(w, jr) 1191 default: 1192 return jr.improveError(errInvalidToken) 1193 } 1194 }