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 }