File: j0/errors.go
   1 package main
   2 
   3 import (
   4     "errors"
   5     "io"
   6     "strconv"
   7 )
   8 
   9 var (
  10     errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
  11     errInputEarlyEnd   = errors.New(`expected end of input data`)
  12     errInvalidComment  = errors.New(`expected / or *`)
  13     errInvalidHex      = errors.New(`expected a base-16 digit`)
  14     errInvalidToken    = errors.New(`invalid JSON token`)
  15     errNoDigits        = errors.New(`expected numeric digits`)
  16     errNoStringQuote   = errors.New(`expected " or '`)
  17     errNoArrayComma    = errors.New(`missing comma between array values`)
  18     errNoObjectComma   = errors.New(`missing comma between key-value pairs`)
  19     errStringEarlyEnd  = errors.New(`unexpected early-end of string`)
  20     errExtraBytes      = errors.New(`unexpected extra input bytes`)
  21 
  22     errMultipleInputs = errors.New(`multiple inputs not allowed`)
  23 
  24     // errNoMoreOutput is a generic dummy output-error, which is meant to be
  25     // ultimately ignored, being just an excuse to quit the app immediately
  26     // and successfully
  27     errNoMoreOutput = errors.New(`no more output`)
  28 )
  29 
  30 // isActualError is to figure out whether not to ignore an error, and thus
  31 // show it as an error message
  32 func isActualError(err error) bool {
  33     return err != nil && err != io.EOF && err != errNoMoreOutput
  34 }
  35 
  36 // linePosError is a more descriptive kind of error, showing the source of
  37 // the input-related problem, as 1-based a line/pos number pair in front
  38 // of the error message
  39 type linePosError struct {
  40     // line is the 1-based line count from the input
  41     line int
  42 
  43     // pos is the 1-based `horizontal` position in its line
  44     pos int
  45 
  46     // err is the error message to `decorate` with the position info
  47     err error
  48 }
  49 
  50 // Error satisfies the error interface
  51 func (lpe linePosError) Error() string {
  52     where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
  53     return where + `: ` + lpe.err.Error()
  54 }

     File: j0/info.txt
   1 j0 [options...] [filepath...]
   2 
   3 
   4 Json-0 converts/fixes JSON/pseudo-JSON input into valid minimal JSON output.
   5 
   6 Besides minimizing bytes, this tool also adapts almost-JSON input into valid
   7 JSON, since it ignores things like comments and trailing commas, neither of
   8 which are supported in JSON, but which are still commonly used.
   9 
  10 Output is always a single line, which ends with a line-feed. When not given
  11 a filepath, input is read from the standard input.
  12 
  13 
  14 When the input deviates from strictly-valid JSON, it can also use
  15 
  16     - leading + signs in numbers
  17     - leading . decimal-markers in numbers
  18     - unquoted object keys
  19     - single-quoted strings
  20     - \x in strings, followed by hexadecimal pairs
  21     - trailing commas in arrays and objects
  22     - rest-of-line //-style comments
  23     - /*-style comments, which end with */ and can span multiple lines
  24 
  25 
  26 All options can either start with a single or a double-dash; some of the
  27 options available, and their aliases, are
  28 
  29     -help    show this help message
  30     -jsonl   read input as lines, each of which is (pseudo)-JSON
  31     -bytes   turn input bytes into an array of numbers
  32     -text    read input as the unquoted/inner-part of a string
  33 
  34     -b       same as option -bytes
  35     -h       same as option -help
  36     -jl      same as option -jsonl
  37     -s       same as option -text
  38     -str     same as option -text
  39     -string  same as option -text
  40     -t       same as option -text

     File: j0/json.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "errors"
   7     "io"
   8 )
   9 
  10 // isIdentifier improves control-flow of func handleKey, when it handles
  11 // unquoted object keys
  12 var isIdentifier = [256]bool{
  13     '_': true,
  14 
  15     '0': true, '1': true, '2': true, '3': true, '4': true,
  16     '5': true, '6': true, '7': true, '8': true, '9': true,
  17 
  18     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
  19     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
  20     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
  21     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
  22     'Y': true, 'Z': true,
  23 
  24     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
  25     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
  26     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
  27     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
  28     'y': true, 'z': true,
  29 }
  30 
  31 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
  32 // being 0, and normalizes letter-case for the hex letters
  33 var matchHex = [256]byte{
  34     '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
  35     '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
  36     'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
  37     'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
  38 }
  39 
  40 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; this func
  41 // avoids writing a trailing line-feed, leaving that up to its caller
  42 func json0(w *bufio.Writer, r *bufio.Reader) error {
  43     jr := jsonReader{r, 1, 1}
  44 
  45     // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
  46     // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
  47     // about byte-order by design
  48     jr.skipUTF8BOM()
  49 
  50     // ignore leading whitespace and/or comments
  51     if err := jr.seekNext(); err != nil {
  52         return err
  53     }
  54 
  55     // handle a single top-level JSON value
  56     if err := handleValue(w, &jr); err != nil {
  57         return err
  58     }
  59 
  60     // ignore trailing whitespace and/or comments
  61     if err := jr.seekNext(); err != nil {
  62         return err
  63     }
  64 
  65     // beyond trailing whitespace and/or comments, any more bytes
  66     // make the whole input data invalid JSON
  67     if _, ok := jr.peekByte(); ok {
  68         return jr.improveError(errExtraBytes)
  69     }
  70     return nil
  71 }
  72 
  73 // jsonReader reads data via a buffer, keeping track of the input position:
  74 // this in turn allows showing much more useful errors, when these happen
  75 type jsonReader struct {
  76     // r is the actual reader
  77     r *bufio.Reader
  78 
  79     // line is the 1-based line-counter for input bytes, and gives errors
  80     // useful position info
  81     line int
  82 
  83     // pos is the 1-based `horizontal` position in its line, and gives
  84     // errors useful position info
  85     pos int
  86 }
  87 
  88 // improveError makes any error more useful, by giving it info about the
  89 // current input-position, as a 1-based line/within-line-position pair
  90 func (jr jsonReader) improveError(err error) error {
  91     if _, ok := err.(linePosError); ok {
  92         return err
  93     }
  94 
  95     if err == io.EOF {
  96         return linePosError{jr.line, jr.pos, errInputEarlyEnd}
  97     }
  98     if err != nil {
  99         return linePosError{jr.line, jr.pos, err}
 100     }
 101     return nil
 102 }
 103 
 104 // demandSyntax fails with an error when the next byte isn't the one given;
 105 // when it is, the byte is then read/skipped, and a nil error is returned
 106 func (jr *jsonReader) demandSyntax(syntax byte) error {
 107     chunk, err := jr.r.Peek(1)
 108     if err == io.EOF {
 109         return jr.improveError(errInputEarlyEnd)
 110     }
 111     if err != nil {
 112         return jr.improveError(err)
 113     }
 114 
 115     if len(chunk) < 1 || chunk[0] != syntax {
 116         msg := `expected ` + string(rune(syntax))
 117         return jr.improveError(errors.New(msg))
 118     }
 119 
 120     jr.readByte()
 121     return nil
 122 }
 123 
 124 // updatePosInfo does what it says, given the byte just read separately
 125 func (jr *jsonReader) updatePosInfo(b byte) {
 126     if b == '\n' {
 127         jr.line += 1
 128         jr.pos = 1
 129     } else {
 130         jr.pos++
 131     }
 132 }
 133 
 134 // peekByte simplifies control-flow for various other funcs
 135 func (jr jsonReader) peekByte() (b byte, ok bool) {
 136     chunk, err := jr.r.Peek(1)
 137     if err == nil && len(chunk) >= 1 {
 138         return chunk[0], true
 139     }
 140     return 0, false
 141 }
 142 
 143 // readByte does what it says, updating the reader's position info
 144 func (jr *jsonReader) readByte() (b byte, err error) {
 145     b, err = jr.r.ReadByte()
 146     if err == nil {
 147         jr.updatePosInfo(b)
 148         return b, nil
 149     }
 150     return b, jr.improveError(err)
 151 }
 152 
 153 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
 154 // and comments, either single-line (starting with //) or general (starting
 155 // with /* and ending with */)
 156 func (jr *jsonReader) seekNext() error {
 157     for {
 158         b, ok := jr.peekByte()
 159         if !ok {
 160             return nil
 161         }
 162 
 163         // case ' ', '\t', '\f', '\v', '\r', '\n':
 164         if b <= 32 {
 165             // keep skipping whitespace bytes
 166             b, _ := jr.readByte()
 167             jr.updatePosInfo(b)
 168             continue
 169         }
 170 
 171         if b != '/' {
 172             // reached the next token
 173             return nil
 174         }
 175 
 176         if err := jr.skipComment(); err != nil {
 177             return err
 178         }
 179 
 180         // after comments, keep looking for more whitespace and/or comments
 181     }
 182 }
 183 
 184 // skipComment helps func seekNext skip over comments, simplifying the latter
 185 // func's control-flow
 186 func (jr *jsonReader) skipComment() error {
 187     err := jr.demandSyntax('/')
 188     if err != nil {
 189         return err
 190     }
 191 
 192     b, ok := jr.peekByte()
 193     if !ok {
 194         return jr.improveError(errInputEarlyEnd)
 195     }
 196 
 197     switch b {
 198     case '/':
 199         // handle single-line comments
 200         return jr.skipLine()
 201 
 202     case '*':
 203         // handle (potentially) multi-line comments
 204         return jr.skipGeneralComment()
 205 
 206     default:
 207         return jr.improveError(errInvalidComment)
 208     }
 209 }
 210 
 211 // skipLine handles single-line comments for func skipComment
 212 func (jr *jsonReader) skipLine() error {
 213     for {
 214         b, err := jr.r.ReadByte()
 215         if err == io.EOF {
 216             // end of input is fine in this case
 217             return nil
 218         }
 219         if err != nil {
 220             return err
 221         }
 222 
 223         jr.updatePosInfo(b)
 224         if b == '\n' {
 225             jr.line++
 226             return nil
 227         }
 228     }
 229 }
 230 
 231 // skipGeneralComment handles (potentially) multi-line comments for func
 232 // skipComment
 233 func (jr *jsonReader) skipGeneralComment() error {
 234     var prev byte
 235     for {
 236         b, err := jr.readByte()
 237         if err != nil {
 238             return jr.improveError(errCommentEarlyEnd)
 239         }
 240 
 241         if prev == '*' && b == '/' {
 242             return nil
 243         }
 244         if b == '\n' {
 245             jr.line++
 246         }
 247         prev = b
 248     }
 249 }
 250 
 251 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
 252 func (jr *jsonReader) skipUTF8BOM() {
 253     lead, err := jr.r.Peek(3)
 254     if err == nil && bytes.HasPrefix(lead, []byte{0xef, 0xbb, 0xbf}) {
 255         jr.readByte()
 256         jr.readByte()
 257         jr.readByte()
 258         jr.pos += 3
 259     }
 260 }
 261 
 262 // outputByte is a small wrapper on func WriteByte, which adapts any error
 263 // into a custom dummy output-error, which is in turn meant to be ignored,
 264 // being just an excuse to quit the app immediately and successfully
 265 func outputByte(w *bufio.Writer, b byte) error {
 266     err := w.WriteByte(b)
 267     if err == nil {
 268         return nil
 269     }
 270     return errNoMoreOutput
 271 }
 272 
 273 // handleArray handles arrays for func handleValue
 274 func handleArray(w *bufio.Writer, jr *jsonReader) error {
 275     if err := jr.demandSyntax('['); err != nil {
 276         return err
 277     }
 278     w.WriteByte('[')
 279 
 280     for n := 0; true; n++ {
 281         // there may be whitespace/comments before the next comma
 282         if err := jr.seekNext(); err != nil {
 283             return err
 284         }
 285 
 286         // handle commas between values, as well as trailing ones
 287         comma := false
 288         b, _ := jr.peekByte()
 289         if b == ',' {
 290             jr.readByte()
 291             comma = true
 292 
 293             // there may be whitespace/comments before an ending ']'
 294             if err := jr.seekNext(); err != nil {
 295                 return err
 296             }
 297             b, _ = jr.peekByte()
 298         }
 299 
 300         // handle end of array
 301         if b == ']' {
 302             jr.readByte()
 303             w.WriteByte(']')
 304             return nil
 305         }
 306 
 307         // don't forget commas between adjacent values
 308         if n > 0 {
 309             if !comma {
 310                 return errNoArrayComma
 311             }
 312             if err := outputByte(w, ','); err != nil {
 313                 return err
 314             }
 315         }
 316 
 317         // handle the next value
 318         if err := jr.seekNext(); err != nil {
 319             return err
 320         }
 321         if err := handleValue(w, jr); err != nil {
 322             return err
 323         }
 324     }
 325 
 326     // make the compiler happy
 327     return nil
 328 }
 329 
 330 // handleDigits helps various number-handling funcs do their job
 331 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
 332     for n := 0; true; n++ {
 333         b, _ := jr.peekByte()
 334 
 335         // support `nice` long numbers by ignoring their underscores
 336         if b == '_' {
 337             jr.readByte()
 338             continue
 339         }
 340 
 341         if '0' <= b && b <= '9' {
 342             jr.readByte()
 343             w.WriteByte(b)
 344             continue
 345         }
 346 
 347         if n == 0 {
 348             return errNoDigits
 349         }
 350         return nil
 351     }
 352 
 353     // make the compiler happy
 354     return nil
 355 }
 356 
 357 // handleDot handles pseudo-JSON numbers which start with a decimal dot
 358 func handleDot(w *bufio.Writer, jr *jsonReader) error {
 359     if err := jr.demandSyntax('.'); err != nil {
 360         return err
 361     }
 362     w.Write([]byte{'0', '.'})
 363     return handleDigits(w, jr)
 364 }
 365 
 366 // handleKey is used by func handleObjects and generalizes func handleString,
 367 // by allowing unquoted object keys; it's not used anywhere else, as allowing
 368 // unquoted string values is ambiguous with actual JSON-keyword values null,
 369 // false, and true.
 370 func handleKey(w *bufio.Writer, jr *jsonReader) error {
 371     quote, ok := jr.peekByte()
 372     if quote == '"' || quote == '\'' {
 373         return handleString(w, jr)
 374     }
 375     if !ok {
 376         return jr.improveError(errStringEarlyEnd)
 377     }
 378 
 379     w.WriteByte('"')
 380     for {
 381         if b, _ := jr.peekByte(); isIdentifier[b] {
 382             jr.readByte()
 383             w.WriteByte(b)
 384             continue
 385         }
 386 
 387         w.WriteByte('"')
 388         return nil
 389     }
 390 }
 391 
 392 // trySimpleInner tries to handle (more quickly) inner-strings where all bytes
 393 // are unescaped ASCII symbols: this is a very common case for strings, and is
 394 // almost always the case for object keys; returns whether it succeeded, so
 395 // this func's caller knows knows if it needs to do anything, the slower way
 396 func trySimpleInner(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
 397     chunk, _ := jr.r.Peek(64)
 398 
 399     for i, b := range chunk {
 400         if b < 32 || b > 127 || b == '\\' {
 401             return false
 402         }
 403         if b != quote {
 404             continue
 405         }
 406 
 407         // bulk-writing the chunk is this func's whole point
 408         w.WriteByte('"')
 409         w.Write(chunk[:i])
 410         w.WriteByte('"')
 411 
 412         jr.r.Discard(i + 1)
 413         return true
 414     }
 415 
 416     // maybe the inner-string is ok, but it's just longer than the chunk
 417     return false
 418 }
 419 
 420 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
 421 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
 422     for rest := kw; len(rest) > 0; rest = rest[1:] {
 423         b, err := jr.readByte()
 424         if err == nil && b == rest[0] {
 425             // keywords given to this func have no line-feeds
 426             jr.pos++
 427             continue
 428         }
 429 
 430         msg := `expected JSON value ` + string(kw)
 431         return jr.improveError(errors.New(msg))
 432     }
 433 
 434     w.Write(kw)
 435     return nil
 436 }
 437 
 438 // handleNegative handles numbers starting with a negative sign for func
 439 // handleValue
 440 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
 441     if err := jr.demandSyntax('-'); err != nil {
 442         return err
 443     }
 444 
 445     w.WriteByte('-')
 446     if b, _ := jr.peekByte(); b == '.' {
 447         jr.readByte()
 448         w.Write([]byte{'0', '.'})
 449         return handleDigits(w, jr)
 450     }
 451     return handleNumber(w, jr)
 452 }
 453 
 454 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
 455 // such as values starting with a decimal dot
 456 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
 457     // handle integer digits
 458     if err := handleDigits(w, jr); err != nil {
 459         return err
 460     }
 461 
 462     // handle optional decimal digits, starting with a leading dot
 463     if b, _ := jr.peekByte(); b == '.' {
 464         jr.readByte()
 465         w.WriteByte('.')
 466         return handleDigits(w, jr)
 467     }
 468     return nil
 469 }
 470 
 471 // handleObject handles objects for func handleValue
 472 func handleObject(w *bufio.Writer, jr *jsonReader) error {
 473     if err := jr.demandSyntax('{'); err != nil {
 474         return err
 475     }
 476     w.WriteByte('{')
 477 
 478     for npairs := 0; true; npairs++ {
 479         // there may be whitespace/comments before the next comma
 480         if err := jr.seekNext(); err != nil {
 481             return err
 482         }
 483 
 484         // handle commas between key-value pairs, as well as trailing ones
 485         comma := false
 486         b, _ := jr.peekByte()
 487         if b == ',' {
 488             jr.readByte()
 489             comma = true
 490 
 491             // there may be whitespace/comments before an ending '}'
 492             if err := jr.seekNext(); err != nil {
 493                 return err
 494             }
 495             b, _ = jr.peekByte()
 496         }
 497 
 498         // handle end of object
 499         if b == '}' {
 500             jr.readByte()
 501             w.WriteByte('}')
 502             return nil
 503         }
 504 
 505         // don't forget commas between adjacent key-value pairs
 506         if npairs > 0 {
 507             if !comma {
 508                 return errNoObjectComma
 509             }
 510             if err := outputByte(w, ','); err != nil {
 511                 return err
 512             }
 513         }
 514 
 515         // handle the next pair's key
 516         if err := jr.seekNext(); err != nil {
 517             return err
 518         }
 519         if err := handleKey(w, jr); err != nil {
 520             return err
 521         }
 522 
 523         // demand a colon right after the key
 524         if err := jr.seekNext(); err != nil {
 525             return err
 526         }
 527         if err := jr.demandSyntax(':'); err != nil {
 528             return err
 529         }
 530         w.WriteByte(':')
 531 
 532         // handle the next pair's value
 533         if err := jr.seekNext(); err != nil {
 534             return err
 535         }
 536         if err := handleValue(w, jr); err != nil {
 537             return err
 538         }
 539     }
 540 
 541     // make the compiler happy
 542     return nil
 543 }
 544 
 545 // handlePositive handles numbers starting with a positive sign for func
 546 // handleValue
 547 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
 548     if err := jr.demandSyntax('+'); err != nil {
 549         return err
 550     }
 551 
 552     // valid JSON isn't supposed to have leading pluses on numbers, so
 553     // emit nothing for it, unlike for negative numbers
 554 
 555     if b, _ := jr.peekByte(); b == '.' {
 556         jr.readByte()
 557         w.Write([]byte{'0', '.'})
 558         return handleDigits(w, jr)
 559     }
 560     return handleNumber(w, jr)
 561 }
 562 
 563 // handleString handles strings for funcs handleValue and handleObject, and
 564 // supports both single-quotes and double-quotes, always emitting the latter
 565 // in the output, of course
 566 func handleString(w *bufio.Writer, jr *jsonReader) error {
 567     quote, ok := jr.peekByte()
 568     if !ok || (quote != '"' && quote != '\'') {
 569         return errNoStringQuote
 570     }
 571 
 572     jr.readByte()
 573     // try the quicker all-unescaped-ASCII handler
 574     if trySimpleInner(w, jr, quote) {
 575         return nil
 576     }
 577 
 578     // it's a non-trivial inner-string, so handle it byte-by-byte
 579     w.WriteByte('"')
 580     escaped := false
 581 
 582     for {
 583         b, err := jr.r.ReadByte()
 584         if err != nil {
 585             if err == io.EOF {
 586                 return jr.improveError(errStringEarlyEnd)
 587             }
 588             return jr.improveError(err)
 589         }
 590 
 591         if !escaped {
 592             if b == '\\' {
 593                 escaped = true
 594                 continue
 595             }
 596 
 597             // handle end of string
 598             if b == quote {
 599                 return outputByte(w, '"')
 600             }
 601 
 602             w.Write(escapedStringBytes[b])
 603             jr.updatePosInfo(b)
 604             continue
 605         }
 606 
 607         // handle escaped items
 608         escaped = false
 609 
 610         switch b {
 611         case 'u':
 612             // \u needs exactly 4 hex-digits to follow it
 613             w.Write([]byte{'\\', 'u'})
 614             if err := copyHex(w, 4, jr); err != nil {
 615                 return jr.improveError(err)
 616             }
 617 
 618         case 'x':
 619             // JSON only supports 4 escaped hex-digits, so pad the 2
 620             // expected hex-digits with 2 zeros
 621             w.Write([]byte{'\\', 'u', '0', '0'})
 622             if err := copyHex(w, 2, jr); err != nil {
 623                 return jr.improveError(err)
 624             }
 625 
 626         case 't', 'f', 'r', 'n', 'b', '\\', '"':
 627             // handle valid-JSON escaped string sequences
 628             w.WriteByte('\\')
 629             w.WriteByte(b)
 630 
 631         // case '\'':
 632         //  // escaped single-quotes aren't standard JSON, but they can
 633         //  // be handy when the input uses non-standard single-quoted
 634         //  // strings
 635         //  w.WriteByte('\'')
 636 
 637         default:
 638             // return jr.decorateError(unexpectedByte{b})
 639             w.Write(escapedStringBytes[b])
 640         }
 641     }
 642 }
 643 
 644 // copyHex handles a run of hex-digits for func handleString, starting right
 645 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
 646 // errors with position info: that's up to the caller
 647 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
 648     for i := 0; i < n; i++ {
 649         b, err := jr.r.ReadByte()
 650         if err == io.EOF {
 651             return errStringEarlyEnd
 652         }
 653         if err != nil {
 654             return err
 655         }
 656 
 657         jr.updatePosInfo(b)
 658 
 659         if b := matchHex[b]; b != 0 {
 660             w.WriteByte(b)
 661             continue
 662         }
 663 
 664         return errInvalidHex
 665     }
 666 
 667     return nil
 668 }
 669 
 670 // handleValue is a generic JSON-token handler, which allows the recursive
 671 // behavior to handle any kind of JSON/pseudo-JSON input
 672 func handleValue(w *bufio.Writer, jr *jsonReader) error {
 673     chunk, err := jr.r.Peek(1)
 674     if err == nil && len(chunk) >= 1 {
 675         return handleValueDispatch(w, jr, chunk[0])
 676     }
 677 
 678     if err == io.EOF {
 679         return jr.improveError(errInputEarlyEnd)
 680     }
 681     return jr.improveError(errInputEarlyEnd)
 682 }
 683 
 684 // handleValueDispatch simplifies control-flow for func handleValue
 685 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
 686     switch b {
 687     case 'f':
 688         return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
 689     case 'n':
 690         return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
 691     case 't':
 692         return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
 693     case '.':
 694         return handleDot(w, jr)
 695     case '+':
 696         return handlePositive(w, jr)
 697     case '-':
 698         return handleNegative(w, jr)
 699     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 700         return handleNumber(w, jr)
 701     case '\'', '"':
 702         return handleString(w, jr)
 703     case '[':
 704         return handleArray(w, jr)
 705     case '{':
 706         return handleObject(w, jr)
 707     default:
 708         return jr.improveError(errInvalidToken)
 709     }
 710 }

     File: j0/json_test.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "strings"
   7     "testing"
   8 )
   9 
  10 func TestJSON0(t *testing.T) {
  11     var tests = []struct {
  12         Input    string
  13         Expected string
  14     }{
  15         {`false`, `false`},
  16         {`null`, `null`},
  17         {`  true  `, `true`},
  18 
  19         {`0`, `0`},
  20         {`1`, `1`},
  21         {`2`, `2`},
  22         {`3`, `3`},
  23         {`4`, `4`},
  24         {`5`, `5`},
  25         {`6`, `6`},
  26         {`7`, `7`},
  27         {`8`, `8`},
  28         {`9`, `9`},
  29 
  30         {`  .345`, `0.345`},
  31         {` -.345`, `-0.345`},
  32         {` +.345`, `0.345`},
  33         {` +123.345`, `123.345`},
  34         {` +.345`, `0.345`},
  35         {` 123.34523`, `123.34523`},
  36         {` 123.34_523`, `123.34523`},
  37         {` 123_456.123`, `123456.123`},
  38 
  39         {`""`, `""`},
  40         {`''`, `""`},
  41         {`"\""`, `"\""`},
  42         {`'\"'`, `"\""`},
  43         {`'\''`, `"'"`},
  44         {`'abc\u0e9A'`, `"abc\u0E9A"`},
  45         {`'abc\x1f[0m'`, `"abc\u001F[0m"`},
  46 
  47         {`[  ]`, `[]`},
  48         {`[ , ]`, `[]`},
  49         {`[.345, false,null , ]`, `[0.345,false,null]`},
  50 
  51         {`{  }`, `{}`},
  52         {`{ , }`, `{}`},
  53 
  54         {
  55             `{ 'abc': .345, "def"  : false, 'xyz':null , }`,
  56             `{"abc":0.345,"def":false,"xyz":null}`,
  57         },
  58 
  59         {`{0problems:123,}`, `{"0problems":123}`},
  60         {`{0_problems:123}`, `{"0_problems":123}`},
  61     }
  62 
  63     for _, tc := range tests {
  64         t.Run(tc.Input, func(t *testing.T) {
  65             var out strings.Builder
  66             w := bufio.NewWriter(&out)
  67             r := bufio.NewReader(strings.NewReader(tc.Input))
  68             if err := json0(w, r); isActualError(err) {
  69                 t.Fatal(err)
  70                 return
  71             }
  72             // don't forget to flush the buffer, or output will be empty
  73             w.Flush()
  74 
  75             s := out.String()
  76             if s != tc.Expected {
  77                 t.Fatalf("<got>\n%s\n<expected>\n%s", s, tc.Expected)
  78                 return
  79             }
  80         })
  81     }
  82 }
  83 
  84 func TestEscapedStringBytes(t *testing.T) {
  85     var escaped = map[rune][]byte{
  86         '\x00': {'\\', 'u', '0', '0', '0', '0'},
  87         '\x01': {'\\', 'u', '0', '0', '0', '1'},
  88         '\x02': {'\\', 'u', '0', '0', '0', '2'},
  89         '\x03': {'\\', 'u', '0', '0', '0', '3'},
  90         '\x04': {'\\', 'u', '0', '0', '0', '4'},
  91         '\x05': {'\\', 'u', '0', '0', '0', '5'},
  92         '\x06': {'\\', 'u', '0', '0', '0', '6'},
  93         '\x07': {'\\', 'u', '0', '0', '0', '7'},
  94         '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
  95         '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
  96         '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
  97         '\x10': {'\\', 'u', '0', '0', '1', '0'},
  98         '\x11': {'\\', 'u', '0', '0', '1', '1'},
  99         '\x12': {'\\', 'u', '0', '0', '1', '2'},
 100         '\x13': {'\\', 'u', '0', '0', '1', '3'},
 101         '\x14': {'\\', 'u', '0', '0', '1', '4'},
 102         '\x15': {'\\', 'u', '0', '0', '1', '5'},
 103         '\x16': {'\\', 'u', '0', '0', '1', '6'},
 104         '\x17': {'\\', 'u', '0', '0', '1', '7'},
 105         '\x18': {'\\', 'u', '0', '0', '1', '8'},
 106         '\x19': {'\\', 'u', '0', '0', '1', '9'},
 107         '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
 108         '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
 109         '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
 110         '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
 111         '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
 112         '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
 113 
 114         '\t': {'\\', 't'},
 115         '\f': {'\\', 'f'},
 116         '\b': {'\\', 'b'},
 117         '\r': {'\\', 'r'},
 118         '\n': {'\\', 'n'},
 119         '\\': {'\\', '\\'},
 120         '"':  {'\\', '"'},
 121     }
 122 
 123     if n := len(escapedStringBytes); n != 256 {
 124         t.Fatalf(`expected 256 entries, instead of %d`, n)
 125         return
 126     }
 127 
 128     for i, v := range escapedStringBytes {
 129         exp := []byte{byte(i)}
 130         if esc, ok := escaped[rune(i)]; ok {
 131             exp = esc
 132         }
 133 
 134         if !bytes.Equal(v, exp) {
 135             t.Fatalf("%d: expected %#v, got %#v", i, exp, v)
 136             return
 137         }
 138     }
 139 }

     File: j0/loading.go
   1 package main
   2 
   3 import (
   4     "errors"
   5     "io"
   6     "os"
   7 )
   8 
   9 func open(path string) (io.ReadCloser, error) {
  10     if path == `-` {
  11         // return io.NopCloser(os.Stdin), nil
  12         return os.Stdin, nil
  13     }
  14 
  15     f, err := os.Open(path)
  16     if err != nil {
  17         // on windows, file-not-found error messages may mention `CreateFile`,
  18         // even when trying to open files in read-only mode
  19         return nil, errors.New(`can't open file named ` + path)
  20     }
  21     return f, nil
  22 }

     File: j0/loading_web.go.txt
   1 //go:build web
   2 
   3 package main
   4 
   5 import (
   6     "errors"
   7     "io"
   8     "net/http"
   9     "os"
  10     "strings"
  11 )
  12 
  13 func open(path string) (io.ReadCloser, error) {
  14     if path == `-` {
  15         return io.NopCloser(os.Stdin), nil
  16     }
  17 
  18     if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
  19         resp, err := http.Get(path)
  20         if err != nil {
  21             return nil, err
  22         }
  23         return resp.Body, nil
  24     }
  25 
  26     f, err := os.Open(path)
  27     if err != nil {
  28         // on windows, file-not-found error messages may mention `CreateFile`,
  29         // even when trying to open files in read-only mode
  30         return nil, errors.New(`can't open file named ` + path)
  31     }
  32     return f, nil
  33 }

     File: j0/main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "io"
   7     "os"
   8 
   9     _ "embed"
  10 )
  11 
  12 /*
  13 You can build smaller executables using either of these 2 commands:
  14 
  15 go build -ldflags "-s -w" -trimpath
  16 go build -compiler gccgo -gccgoflags "-s -O2" -trimpath
  17 
  18 The latter command will make a much smaller file than the first, but it
  19 requires gccgo to be installed beside the standard go(lang) compiler.
  20 */
  21 
  22 // Note: the code is avoiding using the fmt package to save hundreds of
  23 // kilobytes on the resulting executable, which is a noticeable difference.
  24 
  25 //go:embed info.txt
  26 var info string
  27 
  28 // bufSize is trying to be a good buffer-size for modern CPU cores
  29 const bufSize = 16 * 1024
  30 
  31 func main() {
  32     if len(os.Args) > 1 {
  33         switch os.Args[1] {
  34         case `-h`, `--h`, `-help`, `--help`:
  35             os.Stderr.WriteString(info)
  36             return
  37         }
  38     }
  39 
  40     if err := run(os.Args[1:]); isActualError(err) {
  41         os.Stderr.WriteString("\x1b[31m")
  42         os.Stderr.WriteString(err.Error())
  43         os.Stderr.WriteString("\x1b[0m\n")
  44         os.Exit(1)
  45     }
  46 }
  47 
  48 func run(args []string) error {
  49     convert := convertPseudoJSON
  50     if len(args) > 0 {
  51         switch args[0] {
  52         case `-jl`, `--jl`, `-jsonl`, `--jsonl`:
  53             args = args[1:]
  54             convert = convertJSONL
  55         case `-t`, `--t`, `-text`, `--text`, `-s`, `--s`, `-str`, `--str`,
  56             `-string`, `--string`:
  57             args = args[1:]
  58             convert = convertString
  59         case `-b`, `--b`, `-bytes`, `--bytes`:
  60             args = args[1:]
  61             convert = convertBytes
  62         }
  63     }
  64 
  65     if len(args) > 1 {
  66         return errMultipleInputs
  67     }
  68 
  69     path := `-`
  70     if len(args) > 0 {
  71         path = args[0]
  72     }
  73     return handleInput(os.Stdout, path, convert)
  74 }
  75 
  76 type converter func(w io.Writer, r io.Reader) error
  77 
  78 // handleInput simplifies control-flow for func main
  79 func handleInput(w io.Writer, path string, convert converter) error {
  80     // pro, _ := os.Create(`j0.prof`)
  81     // defer pro.Close()
  82     // pprof.StartCPUProfile(pro)
  83     // defer pprof.StopCPUProfile()
  84 
  85     if path == `-` {
  86         return convert(w, os.Stdin)
  87     }
  88 
  89     f, err := open(path)
  90     if err != nil {
  91         return err
  92     }
  93     defer f.Close()
  94 
  95     return convert(w, f)
  96 }
  97 
  98 // convertPseudoJSON handles regular JSON and pseudo-JSON input
  99 func convertPseudoJSON(w io.Writer, r io.Reader) error {
 100     bw := bufio.NewWriterSize(w, bufSize)
 101     br := bufio.NewReaderSize(r, bufSize)
 102     defer bw.Flush()
 103 
 104     if err := json0(bw, br); err != nil {
 105         return err
 106     }
 107 
 108     // end the only output-line with a line-feed; this also avoids showing
 109     // error messages on the same line as the main output, since JSON-0
 110     // output has no line-feeds before its last byte
 111     return outputByte(bw, '\n')
 112 }
 113 
 114 // convertBytes handles all input bytes, emitting a JSON array of numbers
 115 func convertBytes(w io.Writer, r io.Reader) error {
 116     bw := bufio.NewWriterSize(w, bufSize)
 117     defer bw.Flush()
 118 
 119     var buf [bufSize]byte
 120     bw.WriteByte('[')
 121 
 122     for i := 0; true; {
 123         n, err := r.Read(buf[:])
 124         if err == io.EOF {
 125             if n == 0 {
 126                 break
 127             }
 128             err = nil
 129         }
 130 
 131         if err != nil {
 132             return err
 133         }
 134 
 135         for _, b := range buf[:n] {
 136             if i > 0 {
 137                 bw.WriteByte(',')
 138             }
 139             i++
 140 
 141             _, err := bw.Write(bytes2digits[b])
 142             if err != nil {
 143                 return errNoMoreOutput
 144             }
 145         }
 146     }
 147 
 148     bw.WriteByte(']')
 149     return outputByte(bw, '\n')
 150 }
 151 
 152 // convertString handles all input as the unquote/inner-part of a string
 153 func convertString(w io.Writer, r io.Reader) error {
 154     bw := bufio.NewWriterSize(w, bufSize)
 155     br := bufio.NewReaderSize(r, bufSize)
 156     defer bw.Flush()
 157 
 158     bw.WriteByte('"')
 159 
 160     for {
 161         r, size, err := br.ReadRune()
 162         if err == io.EOF {
 163             if size == 0 {
 164                 break
 165             }
 166             err = nil
 167         }
 168 
 169         if err != nil {
 170             return err
 171         }
 172 
 173         if r < 128 {
 174             _, err = bw.Write(escapedStringBytes[r])
 175         } else {
 176             _, err = bw.WriteRune(r)
 177         }
 178 
 179         if err != nil {
 180             return errNoMoreOutput
 181         }
 182     }
 183 
 184     bw.WriteByte('"')
 185     return outputByte(bw, '\n')
 186 }
 187 
 188 // convertJSONL handles JSON lines and pseudo-JSON lines
 189 func convertJSONL(w io.Writer, r io.Reader) error {
 190     bw := bufio.NewWriterSize(w, bufSize)
 191     defer bw.Flush()
 192 
 193     items := 0
 194     sc := bufio.NewScanner(r)
 195     lr := bytes.NewReader(nil)
 196     blr := bufio.NewReaderSize(nil, bufSize)
 197 
 198     bw.WriteByte('[')
 199 
 200     for sc.Scan() {
 201         line := sc.Bytes()
 202         trimmed := bytes.TrimSpace(line)
 203         if len(trimmed) == 0 || bytes.HasPrefix(trimmed, []byte{'/', '/'}) {
 204             continue
 205         }
 206 
 207         if items > 0 {
 208             if err := outputByte(bw, ','); err != nil {
 209                 return err
 210             }
 211         }
 212         items++
 213 
 214         lr.Reset(line)
 215         blr.Reset(lr)
 216         if err := json0(bw, blr); err != nil {
 217             return err
 218         }
 219     }
 220 
 221     if err := sc.Err(); err != nil {
 222         return err
 223     }
 224 
 225     bw.WriteByte(']')
 226     return outputByte(bw, '\n')
 227 }

     File: j0/mit-license.txt
   1 The MIT License (MIT)
   2 
   3 Copyright © 2024 pacman64
   4 
   5 Permission is hereby granted, free of charge, to any person obtaining a copy of
   6 this software and associated documentation files (the “Software”), to deal
   7 in the Software without restriction, including without limitation the rights to
   8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
   9 of the Software, and to permit persons to whom the Software is furnished to do
  10 so, subject to the following conditions:
  11 
  12 The above copyright notice and this permission notice shall be included in all
  13 copies or substantial portions of the Software.
  14 
  15 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 SOFTWARE.

     File: j0/tables.go
   1 package main
   2 
   3 // escapedStringBytes helps func handleString treat all string bytes quickly
   4 // and correctly, using their officially-supported JSON escape sequences
   5 //
   6 // https://www.rfc-editor.org/rfc/rfc8259#section-7
   7 var escapedStringBytes = [256][]byte{
   8     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
   9     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
  10     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
  11     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
  12     {'\\', 'b'}, {'\\', 't'},
  13     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
  14     {'\\', 'f'}, {'\\', 'r'},
  15     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
  16     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
  17     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
  18     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
  19     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
  20     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
  21     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
  22     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
  23     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
  24     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
  25     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
  26     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
  27     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
  28     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
  29     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
  30     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
  31     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
  32     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
  33     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
  34     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
  35     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
  36     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
  37     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
  38     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
  39     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
  40     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
  41     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
  42     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
  43     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
  44     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
  45     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
  46     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
  47     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
  48     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
  49     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
  50     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
  51     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
  52 }
  53 
  54 // bytes2digits has all bytes `pre-rendered` as numeric ASCII strings
  55 var bytes2digits = [256][]byte{
  56     {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'},
  57     {'8'}, {'9'}, {'1', '0'}, {'1', '1'}, {'1', '2'},
  58     {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'},
  59     {'1', '7'}, {'1', '8'}, {'1', '9'}, {'2', '0'},
  60     {'2', '1'}, {'2', '2'}, {'2', '3'}, {'2', '4'},
  61     {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'},
  62     {'2', '9'}, {'3', '0'}, {'3', '1'}, {'3', '2'},
  63     {'3', '3'}, {'3', '4'}, {'3', '5'}, {'3', '6'},
  64     {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'},
  65     {'4', '1'}, {'4', '2'}, {'4', '3'}, {'4', '4'},
  66     {'4', '5'}, {'4', '6'}, {'4', '7'}, {'4', '8'},
  67     {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'},
  68     {'5', '3'}, {'5', '4'}, {'5', '5'}, {'5', '6'},
  69     {'5', '7'}, {'5', '8'}, {'5', '9'}, {'6', '0'},
  70     {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'},
  71     {'6', '5'}, {'6', '6'}, {'6', '7'}, {'6', '8'},
  72     {'6', '9'}, {'7', '0'}, {'7', '1'}, {'7', '2'},
  73     {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'},
  74     {'7', '7'}, {'7', '8'}, {'7', '9'}, {'8', '0'},
  75     {'8', '1'}, {'8', '2'}, {'8', '3'}, {'8', '4'},
  76     {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'},
  77     {'8', '9'}, {'9', '0'}, {'9', '1'}, {'9', '2'},
  78     {'9', '3'}, {'9', '4'}, {'9', '5'}, {'9', '6'},
  79     {'9', '7'}, {'9', '8'}, {'9', '9'}, {'1', '0', '0'},
  80     {'1', '0', '1'}, {'1', '0', '2'}, {'1', '0', '3'}, {'1', '0', '4'},
  81     {'1', '0', '5'}, {'1', '0', '6'}, {'1', '0', '7'}, {'1', '0', '8'},
  82     {'1', '0', '9'}, {'1', '1', '0'}, {'1', '1', '1'}, {'1', '1', '2'},
  83     {'1', '1', '3'}, {'1', '1', '4'}, {'1', '1', '5'}, {'1', '1', '6'},
  84     {'1', '1', '7'}, {'1', '1', '8'}, {'1', '1', '9'}, {'1', '2', '0'},
  85     {'1', '2', '1'}, {'1', '2', '2'}, {'1', '2', '3'}, {'1', '2', '4'},
  86     {'1', '2', '5'}, {'1', '2', '6'}, {'1', '2', '7'}, {'1', '2', '8'},
  87     {'1', '2', '9'}, {'1', '3', '0'}, {'1', '3', '1'}, {'1', '3', '2'},
  88     {'1', '3', '3'}, {'1', '3', '4'}, {'1', '3', '5'}, {'1', '3', '6'},
  89     {'1', '3', '7'}, {'1', '3', '8'}, {'1', '3', '9'}, {'1', '4', '0'},
  90     {'1', '4', '1'}, {'1', '4', '2'}, {'1', '4', '3'}, {'1', '4', '4'},
  91     {'1', '4', '5'}, {'1', '4', '6'}, {'1', '4', '7'}, {'1', '4', '8'},
  92     {'1', '4', '9'}, {'1', '5', '0'}, {'1', '5', '1'}, {'1', '5', '2'},
  93     {'1', '5', '3'}, {'1', '5', '4'}, {'1', '5', '5'}, {'1', '5', '6'},
  94     {'1', '5', '7'}, {'1', '5', '8'}, {'1', '5', '9'}, {'1', '6', '0'},
  95     {'1', '6', '1'}, {'1', '6', '2'}, {'1', '6', '3'}, {'1', '6', '4'},
  96     {'1', '6', '5'}, {'1', '6', '6'}, {'1', '6', '7'}, {'1', '6', '8'},
  97     {'1', '6', '9'}, {'1', '7', '0'}, {'1', '7', '1'}, {'1', '7', '2'},
  98     {'1', '7', '3'}, {'1', '7', '4'}, {'1', '7', '5'}, {'1', '7', '6'},
  99     {'1', '7', '7'}, {'1', '7', '8'}, {'1', '7', '9'}, {'1', '8', '0'},
 100     {'1', '8', '1'}, {'1', '8', '2'}, {'1', '8', '3'}, {'1', '8', '4'},
 101     {'1', '8', '5'}, {'1', '8', '6'}, {'1', '8', '7'}, {'1', '8', '8'},
 102     {'1', '8', '9'}, {'1', '9', '0'}, {'1', '9', '1'}, {'1', '9', '2'},
 103     {'1', '9', '3'}, {'1', '9', '4'}, {'1', '9', '5'}, {'1', '9', '6'},
 104     {'1', '9', '7'}, {'1', '9', '8'}, {'1', '9', '9'}, {'2', '0', '0'},
 105     {'2', '0', '1'}, {'2', '0', '2'}, {'2', '0', '3'}, {'2', '0', '4'},
 106     {'2', '0', '5'}, {'2', '0', '6'}, {'2', '0', '7'}, {'2', '0', '8'},
 107     {'2', '0', '9'}, {'2', '1', '0'}, {'2', '1', '1'}, {'2', '1', '2'},
 108     {'2', '1', '3'}, {'2', '1', '4'}, {'2', '1', '5'}, {'2', '1', '6'},
 109     {'2', '1', '7'}, {'2', '1', '8'}, {'2', '1', '9'}, {'2', '2', '0'},
 110     {'2', '2', '1'}, {'2', '2', '2'}, {'2', '2', '3'}, {'2', '2', '4'},
 111     {'2', '2', '5'}, {'2', '2', '6'}, {'2', '2', '7'}, {'2', '2', '8'},
 112     {'2', '2', '9'}, {'2', '3', '0'}, {'2', '3', '1'}, {'2', '3', '2'},
 113     {'2', '3', '3'}, {'2', '3', '4'}, {'2', '3', '5'}, {'2', '3', '6'},
 114     {'2', '3', '7'}, {'2', '3', '8'}, {'2', '3', '9'}, {'2', '4', '0'},
 115     {'2', '4', '1'}, {'2', '4', '2'}, {'2', '4', '3'}, {'2', '4', '4'},
 116     {'2', '4', '5'}, {'2', '4', '6'}, {'2', '4', '7'}, {'2', '4', '8'},
 117     {'2', '4', '9'}, {'2', '5', '0'}, {'2', '5', '1'}, {'2', '5', '2'},
 118     {'2', '5', '3'}, {'2', '5', '4'}, {'2', '5', '5'},
 119 }

     File: j0/tables_test.go
   1 package main
   2 
   3 import (
   4     "bytes"
   5     "fmt"
   6     "testing"
   7 )
   8 
   9 func TestTables(t *testing.T) {
  10     if l := len(bytes2digits); l != 256 {
  11         t.Fatalf(`bytes-table has %d entries, instead of 256`, l)
  12         return
  13     }
  14 
  15     for i := 0; i < len(bytes2digits); i++ {
  16         exp := bytes2digits[i]
  17         got := fmt.Sprintf(`%d`, byte(i))
  18         if !bytes.Equal([]byte(got), exp) {
  19             const fs = `bytes-table entry for %d is %s, instead of %s`
  20             t.Fatalf(fs, i, got, string(exp))
  21             return
  22         }
  23     }
  24 }