File: j0.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2024 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 j0: this version has no http(s) support. Even
  27 the unit-tests from the original j0 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 j0.go
  33 */
  34 
  35 package main
  36 
  37 import (
  38     "bufio"
  39     "bytes"
  40     "errors"
  41     "io"
  42     "os"
  43     "strconv"
  44 )
  45 
  46 const info = `
  47 j0 [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 both rest-of-line and multi-line comments
  57     - ignores extra/trailing commas in arrays and objects
  58     - turns single-quoted strings/keys into double-quoted strings
  59     - double-quotes unquoted object keys
  60     - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
  61 
  62 The only option available can either start with a single or a double-dash
  63 
  64     -h   -help       show this help message
  65 `
  66 
  67 const (
  68     bufSize    = 32 * 1024
  69     errorStyle = "\x1b[31m"
  70 )
  71 
  72 func main() {
  73     if len(os.Args) > 1 {
  74         switch os.Args[1] {
  75         case `-h`, `--h`, `-help`, `--help`:
  76             os.Stderr.WriteString(info[1:])
  77             return
  78         }
  79     }
  80 
  81     if len(os.Args) > 2 {
  82         const msg = `only 1 (optional) named input is supported`
  83         os.Stderr.WriteString(errorStyle + msg + "\x1b[0m\n")
  84         os.Exit(1)
  85     }
  86 
  87     name := `-`
  88     if len(os.Args) > 1 {
  89         name = os.Args[1]
  90     }
  91 
  92     if err := run(os.Stdout, name); isActualError(err) {
  93         os.Stderr.WriteString(errorStyle)
  94         os.Stderr.WriteString(err.Error())
  95         os.Stderr.WriteString("\x1b[0m\n")
  96         os.Exit(1)
  97     }
  98 }
  99 
 100 func run(w io.Writer, name string) error {
 101     if name == `` || name == `-` {
 102         return convertPseudoJSON(w, os.Stdin)
 103     }
 104 
 105     f, err := os.Open(name)
 106     if err != nil {
 107         return errors.New(`can't read from file named "` + name + `"`)
 108     }
 109     defer f.Close()
 110 
 111     return convertPseudoJSON(w, f)
 112 }
 113 
 114 var (
 115     errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
 116     errInputEarlyEnd   = errors.New(`expected end of input data`)
 117     errInvalidComment  = errors.New(`expected / or *`)
 118     errInvalidHex      = errors.New(`expected a base-16 digit`)
 119     errInvalidToken    = errors.New(`invalid JSON token`)
 120     errNoDigits        = errors.New(`expected numeric digits`)
 121     errNoStringQuote   = errors.New(`expected " or '`)
 122     errNoArrayComma    = errors.New(`missing comma between array values`)
 123     errNoObjectComma   = errors.New(`missing comma between key-value pairs`)
 124     errStringEarlyEnd  = errors.New(`unexpected early-end of string`)
 125     errExtraBytes      = errors.New(`unexpected extra input bytes`)
 126 
 127     // errNoMoreOutput is a generic dummy output-error, which is meant to be
 128     // ultimately ignored, being just an excuse to quit the app immediately
 129     // and successfully
 130     errNoMoreOutput = errors.New(`no more output`)
 131 )
 132 
 133 // isActualError is to figure out whether not to ignore an error, and thus
 134 // show it as an error message
 135 func isActualError(err error) bool {
 136     return err != nil && err != io.EOF && err != errNoMoreOutput
 137 }
 138 
 139 // linePosError is a more descriptive kind of error, showing the source of
 140 // the input-related problem, as 1-based a line/pos number pair in front
 141 // of the error message
 142 type linePosError struct {
 143     // line is the 1-based line count from the input
 144     line int
 145 
 146     // pos is the 1-based `horizontal` position in its line
 147     pos int
 148 
 149     // err is the error message to `decorate` with the position info
 150     err error
 151 }
 152 
 153 // Error satisfies the error interface
 154 func (lpe linePosError) Error() string {
 155     where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
 156     return where + `: ` + lpe.err.Error()
 157 }
 158 
 159 // convertPseudoJSON handles regular JSON and pseudo-JSON input
 160 func convertPseudoJSON(w io.Writer, r io.Reader) error {
 161     bw := bufio.NewWriterSize(w, bufSize)
 162     br := bufio.NewReaderSize(r, bufSize)
 163     defer bw.Flush()
 164 
 165     if err := json0(bw, br); err != nil {
 166         return err
 167     }
 168     // end the only output-line with a line-feed; this also avoids showing
 169     // error messages on the same line as the main output, since JSON-0
 170     // output has no line-feeds before its last byte
 171     return outputByte(bw, '\n')
 172 }
 173 
 174 // isIdentifier improves control-flow of func handleKey, when it handles
 175 // unquoted object keys
 176 var isIdentifier = [256]bool{
 177     '_': true,
 178 
 179     '0': true, '1': true, '2': true, '3': true, '4': true,
 180     '5': true, '6': true, '7': true, '8': true, '9': true,
 181 
 182     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
 183     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
 184     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
 185     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
 186     'Y': true, 'Z': true,
 187 
 188     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
 189     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
 190     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
 191     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
 192     'y': true, 'z': true,
 193 }
 194 
 195 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
 196 // being 0, and normalizes letter-case for the hex letters
 197 var matchHex = [256]byte{
 198     '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
 199     '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
 200     'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
 201     'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
 202 }
 203 
 204 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; this func
 205 // avoids writing a trailing line-feed, leaving that up to its caller
 206 func json0(w *bufio.Writer, r *bufio.Reader) error {
 207     jr := jsonReader{r, 1, 1}
 208 
 209     // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
 210     // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
 211     // about byte-order by design
 212     jr.skipUTF8BOM()
 213 
 214     // ignore leading whitespace and/or comments
 215     if err := jr.seekNext(); err != nil {
 216         return err
 217     }
 218 
 219     // handle a single top-level JSON value
 220     if err := handleValue(w, &jr); err != nil {
 221         return err
 222     }
 223 
 224     // ignore trailing whitespace and/or comments
 225     if err := jr.seekNext(); err != nil {
 226         return err
 227     }
 228 
 229     // beyond trailing whitespace and/or comments, any more bytes
 230     // make the whole input data invalid JSON
 231     if _, ok := jr.peekByte(); ok {
 232         return jr.improveError(errExtraBytes)
 233     }
 234     return nil
 235 }
 236 
 237 // jsonReader reads data via a buffer, keeping track of the input position:
 238 // this in turn allows showing much more useful errors, when these happen
 239 type jsonReader struct {
 240     // r is the actual reader
 241     r *bufio.Reader
 242 
 243     // line is the 1-based line-counter for input bytes, and gives errors
 244     // useful position info
 245     line int
 246 
 247     // pos is the 1-based `horizontal` position in its line, and gives
 248     // errors useful position info
 249     pos int
 250 }
 251 
 252 // improveError makes any error more useful, by giving it info about the
 253 // current input-position, as a 1-based line/within-line-position pair
 254 func (jr jsonReader) improveError(err error) error {
 255     if _, ok := err.(linePosError); ok {
 256         return err
 257     }
 258 
 259     if err == io.EOF {
 260         return linePosError{jr.line, jr.pos, errInputEarlyEnd}
 261     }
 262     if err != nil {
 263         return linePosError{jr.line, jr.pos, err}
 264     }
 265     return nil
 266 }
 267 
 268 // demandSyntax fails with an error when the next byte isn't the one given;
 269 // when it is, the byte is then read/skipped, and a nil error is returned
 270 func (jr *jsonReader) demandSyntax(syntax byte) error {
 271     chunk, err := jr.r.Peek(1)
 272     if err == io.EOF {
 273         return jr.improveError(errInputEarlyEnd)
 274     }
 275     if err != nil {
 276         return jr.improveError(err)
 277     }
 278 
 279     if len(chunk) < 1 || chunk[0] != syntax {
 280         msg := `expected ` + string(rune(syntax))
 281         return jr.improveError(errors.New(msg))
 282     }
 283 
 284     jr.readByte()
 285     return nil
 286 }
 287 
 288 // updatePosInfo does what it says, given the byte just read separately
 289 func (jr *jsonReader) updatePosInfo(b byte) {
 290     if b == '\n' {
 291         jr.line += 1
 292         jr.pos = 1
 293     } else {
 294         jr.pos++
 295     }
 296 }
 297 
 298 // peekByte simplifies control-flow for various other funcs
 299 func (jr jsonReader) peekByte() (b byte, ok bool) {
 300     chunk, err := jr.r.Peek(1)
 301     if err == nil && len(chunk) >= 1 {
 302         return chunk[0], true
 303     }
 304     return 0, false
 305 }
 306 
 307 // readByte does what it says, updating the reader's position info
 308 func (jr *jsonReader) readByte() (b byte, err error) {
 309     b, err = jr.r.ReadByte()
 310     if err == nil {
 311         jr.updatePosInfo(b)
 312         return b, nil
 313     }
 314     return b, jr.improveError(err)
 315 }
 316 
 317 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
 318 // and comments, either single-line (starting with //) or general (starting
 319 // with /* and ending with */)
 320 func (jr *jsonReader) seekNext() error {
 321     for {
 322         b, ok := jr.peekByte()
 323         if !ok {
 324             return nil
 325         }
 326 
 327         // case ' ', '\t', '\f', '\v', '\r', '\n':
 328         if b <= 32 {
 329             // keep skipping whitespace bytes
 330             b, _ := jr.readByte()
 331             jr.updatePosInfo(b)
 332             continue
 333         }
 334 
 335         if b != '/' {
 336             // reached the next token
 337             return nil
 338         }
 339 
 340         if err := jr.skipComment(); err != nil {
 341             return err
 342         }
 343 
 344         // after comments, keep looking for more whitespace and/or comments
 345     }
 346 }
 347 
 348 // skipComment helps func seekNext skip over comments, simplifying the latter
 349 // func's control-flow
 350 func (jr *jsonReader) skipComment() error {
 351     err := jr.demandSyntax('/')
 352     if err != nil {
 353         return err
 354     }
 355 
 356     b, ok := jr.peekByte()
 357     if !ok {
 358         return jr.improveError(errInputEarlyEnd)
 359     }
 360 
 361     switch b {
 362     case '/':
 363         // handle single-line comments
 364         return jr.skipLine()
 365 
 366     case '*':
 367         // handle (potentially) multi-line comments
 368         return jr.skipGeneralComment()
 369 
 370     default:
 371         return jr.improveError(errInvalidComment)
 372     }
 373 }
 374 
 375 // skipLine handles single-line comments for func skipComment
 376 func (jr *jsonReader) skipLine() error {
 377     for {
 378         b, err := jr.r.ReadByte()
 379         if err == io.EOF {
 380             // end of input is fine in this case
 381             return nil
 382         }
 383         if err != nil {
 384             return err
 385         }
 386 
 387         jr.updatePosInfo(b)
 388         if b == '\n' {
 389             jr.line++
 390             return nil
 391         }
 392     }
 393 }
 394 
 395 // skipGeneralComment handles (potentially) multi-line comments for func
 396 // skipComment
 397 func (jr *jsonReader) skipGeneralComment() error {
 398     var prev byte
 399     for {
 400         b, err := jr.readByte()
 401         if err != nil {
 402             return jr.improveError(errCommentEarlyEnd)
 403         }
 404 
 405         if prev == '*' && b == '/' {
 406             return nil
 407         }
 408         if b == '\n' {
 409             jr.line++
 410         }
 411         prev = b
 412     }
 413 }
 414 
 415 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
 416 func (jr *jsonReader) skipUTF8BOM() {
 417     lead, err := jr.r.Peek(3)
 418     if err == nil && bytes.HasPrefix(lead, []byte{0xef, 0xbb, 0xbf}) {
 419         jr.readByte()
 420         jr.readByte()
 421         jr.readByte()
 422         jr.pos += 3
 423     }
 424 }
 425 
 426 // outputByte is a small wrapper on func WriteByte, which adapts any error
 427 // into a custom dummy output-error, which is in turn meant to be ignored,
 428 // being just an excuse to quit the app immediately and successfully
 429 func outputByte(w *bufio.Writer, b byte) error {
 430     err := w.WriteByte(b)
 431     if err == nil {
 432         return nil
 433     }
 434     return errNoMoreOutput
 435 }
 436 
 437 // handleArray handles arrays for func handleValue
 438 func handleArray(w *bufio.Writer, jr *jsonReader) error {
 439     if err := jr.demandSyntax('['); err != nil {
 440         return err
 441     }
 442     w.WriteByte('[')
 443 
 444     for n := 0; true; n++ {
 445         // there may be whitespace/comments before the next comma
 446         if err := jr.seekNext(); err != nil {
 447             return err
 448         }
 449 
 450         // handle commas between values, as well as trailing ones
 451         comma := false
 452         b, _ := jr.peekByte()
 453         if b == ',' {
 454             jr.readByte()
 455             comma = true
 456 
 457             // there may be whitespace/comments before an ending ']'
 458             if err := jr.seekNext(); err != nil {
 459                 return err
 460             }
 461             b, _ = jr.peekByte()
 462         }
 463 
 464         // handle end of array
 465         if b == ']' {
 466             jr.readByte()
 467             w.WriteByte(']')
 468             return nil
 469         }
 470 
 471         // don't forget commas between adjacent values
 472         if n > 0 {
 473             if !comma {
 474                 return errNoArrayComma
 475             }
 476             if err := outputByte(w, ','); err != nil {
 477                 return err
 478             }
 479         }
 480 
 481         // handle the next value
 482         if err := jr.seekNext(); err != nil {
 483             return err
 484         }
 485         if err := handleValue(w, jr); err != nil {
 486             return err
 487         }
 488     }
 489 
 490     // make the compiler happy
 491     return nil
 492 }
 493 
 494 // handleDigits helps various number-handling funcs do their job
 495 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
 496     for n := 0; true; n++ {
 497         b, _ := jr.peekByte()
 498 
 499         // support `nice` long numbers by ignoring their underscores
 500         if b == '_' {
 501             jr.readByte()
 502             continue
 503         }
 504 
 505         if '0' <= b && b <= '9' {
 506             jr.readByte()
 507             w.WriteByte(b)
 508             continue
 509         }
 510 
 511         if n == 0 {
 512             return errNoDigits
 513         }
 514         return nil
 515     }
 516 
 517     // make the compiler happy
 518     return nil
 519 }
 520 
 521 // handleDot handles pseudo-JSON numbers which start with a decimal dot
 522 func handleDot(w *bufio.Writer, jr *jsonReader) error {
 523     if err := jr.demandSyntax('.'); err != nil {
 524         return err
 525     }
 526     w.Write([]byte{'0', '.'})
 527     return handleDigits(w, jr)
 528 }
 529 
 530 // handleKey is used by func handleObjects and generalizes func handleString,
 531 // by allowing unquoted object keys; it's not used anywhere else, as allowing
 532 // unquoted string values is ambiguous with actual JSON-keyword values null,
 533 // false, and true.
 534 func handleKey(w *bufio.Writer, jr *jsonReader) error {
 535     quote, ok := jr.peekByte()
 536     if quote == '"' || quote == '\'' {
 537         return handleString(w, jr)
 538     }
 539     if !ok {
 540         return jr.improveError(errStringEarlyEnd)
 541     }
 542 
 543     w.WriteByte('"')
 544     for {
 545         if b, _ := jr.peekByte(); isIdentifier[b] {
 546             jr.readByte()
 547             w.WriteByte(b)
 548             continue
 549         }
 550 
 551         w.WriteByte('"')
 552         return nil
 553     }
 554 }
 555 
 556 // trySimpleInner tries to handle (more quickly) inner-strings where all bytes
 557 // are unescaped ASCII symbols: this is a very common case for strings, and is
 558 // almost always the case for object keys; returns whether it succeeded, so
 559 // this func's caller knows knows if it needs to do anything, the slower way
 560 func trySimpleInner(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
 561     chunk, _ := jr.r.Peek(64)
 562 
 563     for i, b := range chunk {
 564         if b < 32 || b > 127 || b == '\\' {
 565             return false
 566         }
 567         if b != quote {
 568             continue
 569         }
 570 
 571         // bulk-writing the chunk is this func's whole point
 572         w.WriteByte('"')
 573         w.Write(chunk[:i])
 574         w.WriteByte('"')
 575 
 576         jr.r.Discard(i + 1)
 577         return true
 578     }
 579 
 580     // maybe the inner-string is ok, but it's just longer than the chunk
 581     return false
 582 }
 583 
 584 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
 585 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
 586     for rest := kw; len(rest) > 0; rest = rest[1:] {
 587         b, err := jr.readByte()
 588         if err == nil && b == rest[0] {
 589             // keywords given to this func have no line-feeds
 590             jr.pos++
 591             continue
 592         }
 593 
 594         msg := `expected JSON value ` + string(kw)
 595         return jr.improveError(errors.New(msg))
 596     }
 597 
 598     w.Write(kw)
 599     return nil
 600 }
 601 
 602 // handleNegative handles numbers starting with a negative sign for func
 603 // handleValue
 604 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
 605     if err := jr.demandSyntax('-'); err != nil {
 606         return err
 607     }
 608 
 609     w.WriteByte('-')
 610     if b, _ := jr.peekByte(); b == '.' {
 611         jr.readByte()
 612         w.Write([]byte{'0', '.'})
 613         return handleDigits(w, jr)
 614     }
 615     return handleNumber(w, jr)
 616 }
 617 
 618 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
 619 // such as values starting with a decimal dot
 620 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
 621     // handle integer digits
 622     if err := handleDigits(w, jr); err != nil {
 623         return err
 624     }
 625 
 626     // handle optional decimal digits, starting with a leading dot
 627     if b, _ := jr.peekByte(); b == '.' {
 628         jr.readByte()
 629         w.WriteByte('.')
 630         return handleDigits(w, jr)
 631     }
 632 
 633     // handle optional exponent digits
 634     if b, _ := jr.peekByte(); b == 'e' || b == 'E' {
 635         jr.readByte()
 636         w.WriteByte(b)
 637         b, _ = jr.peekByte()
 638         if b == '+' {
 639             jr.readByte()
 640         } else if b == '-' {
 641             w.WriteByte('-')
 642             jr.readByte()
 643         }
 644         return handleDigits(w, jr)
 645     }
 646 
 647     return nil
 648 }
 649 
 650 // handleObject handles objects for func handleValue
 651 func handleObject(w *bufio.Writer, jr *jsonReader) error {
 652     if err := jr.demandSyntax('{'); err != nil {
 653         return err
 654     }
 655     w.WriteByte('{')
 656 
 657     for npairs := 0; true; npairs++ {
 658         // there may be whitespace/comments before the next comma
 659         if err := jr.seekNext(); err != nil {
 660             return err
 661         }
 662 
 663         // handle commas between key-value pairs, as well as trailing ones
 664         comma := false
 665         b, _ := jr.peekByte()
 666         if b == ',' {
 667             jr.readByte()
 668             comma = true
 669 
 670             // there may be whitespace/comments before an ending '}'
 671             if err := jr.seekNext(); err != nil {
 672                 return err
 673             }
 674             b, _ = jr.peekByte()
 675         }
 676 
 677         // handle end of object
 678         if b == '}' {
 679             jr.readByte()
 680             w.WriteByte('}')
 681             return nil
 682         }
 683 
 684         // don't forget commas between adjacent key-value pairs
 685         if npairs > 0 {
 686             if !comma {
 687                 return errNoObjectComma
 688             }
 689             if err := outputByte(w, ','); err != nil {
 690                 return err
 691             }
 692         }
 693 
 694         // handle the next pair's key
 695         if err := jr.seekNext(); err != nil {
 696             return err
 697         }
 698         if err := handleKey(w, jr); err != nil {
 699             return err
 700         }
 701 
 702         // demand a colon right after the key
 703         if err := jr.seekNext(); err != nil {
 704             return err
 705         }
 706         if err := jr.demandSyntax(':'); err != nil {
 707             return err
 708         }
 709         w.WriteByte(':')
 710 
 711         // handle the next pair's value
 712         if err := jr.seekNext(); err != nil {
 713             return err
 714         }
 715         if err := handleValue(w, jr); err != nil {
 716             return err
 717         }
 718     }
 719 
 720     // make the compiler happy
 721     return nil
 722 }
 723 
 724 // handlePositive handles numbers starting with a positive sign for func
 725 // handleValue
 726 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
 727     if err := jr.demandSyntax('+'); err != nil {
 728         return err
 729     }
 730 
 731     // valid JSON isn't supposed to have leading pluses on numbers, so
 732     // emit nothing for it, unlike for negative numbers
 733 
 734     if b, _ := jr.peekByte(); b == '.' {
 735         jr.readByte()
 736         w.Write([]byte{'0', '.'})
 737         return handleDigits(w, jr)
 738     }
 739     return handleNumber(w, jr)
 740 }
 741 
 742 // handleString handles strings for funcs handleValue and handleObject, and
 743 // supports both single-quotes and double-quotes, always emitting the latter
 744 // in the output, of course
 745 func handleString(w *bufio.Writer, jr *jsonReader) error {
 746     quote, ok := jr.peekByte()
 747     if !ok || (quote != '"' && quote != '\'') {
 748         return errNoStringQuote
 749     }
 750 
 751     jr.readByte()
 752     // try the quicker all-unescaped-ASCII handler
 753     if trySimpleInner(w, jr, quote) {
 754         return nil
 755     }
 756 
 757     // it's a non-trivial inner-string, so handle it byte-by-byte
 758     w.WriteByte('"')
 759     escaped := false
 760 
 761     for {
 762         b, err := jr.r.ReadByte()
 763         if err != nil {
 764             if err == io.EOF {
 765                 return jr.improveError(errStringEarlyEnd)
 766             }
 767             return jr.improveError(err)
 768         }
 769 
 770         if !escaped {
 771             if b == '\\' {
 772                 escaped = true
 773                 continue
 774             }
 775 
 776             // handle end of string
 777             if b == quote {
 778                 return outputByte(w, '"')
 779             }
 780 
 781             w.Write(escapedStringBytes[b])
 782             jr.updatePosInfo(b)
 783             continue
 784         }
 785 
 786         // handle escaped items
 787         escaped = false
 788 
 789         switch b {
 790         case 'u':
 791             // \u needs exactly 4 hex-digits to follow it
 792             w.Write([]byte{'\\', 'u'})
 793             if err := copyHex(w, 4, jr); err != nil {
 794                 return jr.improveError(err)
 795             }
 796 
 797         case 'x':
 798             // JSON only supports 4 escaped hex-digits, so pad the 2
 799             // expected hex-digits with 2 zeros
 800             w.Write([]byte{'\\', 'u', '0', '0'})
 801             if err := copyHex(w, 2, jr); err != nil {
 802                 return jr.improveError(err)
 803             }
 804 
 805         case 't', 'f', 'r', 'n', 'b', '\\', '"':
 806             // handle valid-JSON escaped string sequences
 807             w.WriteByte('\\')
 808             w.WriteByte(b)
 809 
 810         // case '\'':
 811         //  // escaped single-quotes aren't standard JSON, but they can
 812         //  // be handy when the input uses non-standard single-quoted
 813         //  // strings
 814         //  w.WriteByte('\'')
 815 
 816         default:
 817             // return jr.decorateError(unexpectedByte{b})
 818             w.Write(escapedStringBytes[b])
 819         }
 820     }
 821 }
 822 
 823 // copyHex handles a run of hex-digits for func handleString, starting right
 824 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
 825 // errors with position info: that's up to the caller
 826 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
 827     for i := 0; i < n; i++ {
 828         b, err := jr.r.ReadByte()
 829         if err == io.EOF {
 830             return errStringEarlyEnd
 831         }
 832         if err != nil {
 833             return err
 834         }
 835 
 836         jr.updatePosInfo(b)
 837 
 838         if b := matchHex[b]; b != 0 {
 839             w.WriteByte(b)
 840             continue
 841         }
 842 
 843         return errInvalidHex
 844     }
 845 
 846     return nil
 847 }
 848 
 849 // handleValue is a generic JSON-token handler, which allows the recursive
 850 // behavior to handle any kind of JSON/pseudo-JSON input
 851 func handleValue(w *bufio.Writer, jr *jsonReader) error {
 852     chunk, err := jr.r.Peek(1)
 853     if err == nil && len(chunk) >= 1 {
 854         return handleValueDispatch(w, jr, chunk[0])
 855     }
 856 
 857     if err == io.EOF {
 858         return jr.improveError(errInputEarlyEnd)
 859     }
 860     return jr.improveError(errInputEarlyEnd)
 861 }
 862 
 863 // handleValueDispatch simplifies control-flow for func handleValue
 864 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
 865     switch b {
 866     case 'f':
 867         return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
 868     case 'n':
 869         return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
 870     case 't':
 871         return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
 872     case '.':
 873         return handleDot(w, jr)
 874     case '+':
 875         return handlePositive(w, jr)
 876     case '-':
 877         return handleNegative(w, jr)
 878     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 879         return handleNumber(w, jr)
 880     case '\'', '"':
 881         return handleString(w, jr)
 882     case '[':
 883         return handleArray(w, jr)
 884     case '{':
 885         return handleObject(w, jr)
 886     default:
 887         return jr.improveError(errInvalidToken)
 888     }
 889 }
 890 
 891 // escapedStringBytes helps func handleString treat all string bytes quickly
 892 // and correctly, using their officially-supported JSON escape sequences
 893 //
 894 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 895 var escapedStringBytes = [256][]byte{
 896     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 897     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 898     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 899     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 900     {'\\', 'b'}, {'\\', 't'},
 901     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 902     {'\\', 'f'}, {'\\', 'r'},
 903     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 904     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 905     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 906     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 907     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 908     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 909     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 910     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 911     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 912     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 913     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 914     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 915     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 916     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 917     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 918     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 919     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 920     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 921     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 922     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 923     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 924     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 925     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 926     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 927     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 928     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 929     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 930     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 931     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 932     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 933     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 934     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 935     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 936     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 937     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 938     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 939     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 940 }