File: json2.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2025 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 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29 go build -ldflags "-s -w" -trimpath json2.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "encoding/json"
  37     "errors"
  38     "io"
  39     "os"
  40 )
  41 
  42 const info = `
  43 json2 [filepath...]
  44 
  45 JSON-2 indents valid JSON input into multi-line JSON which uses 2 spaces for
  46 each indentation level.
  47 `
  48 
  49 // errNoMoreOutput is a generic dummy output-error, which is meant to be
  50 // ultimately ignored, being just an excuse to quit the app immediately
  51 // and successfully
  52 var errNoMoreOutput = errors.New(`no more output`)
  53 
  54 func main() {
  55     if len(os.Args) > 1 {
  56         switch os.Args[1] {
  57         case `-h`, `--h`, `-help`, `--help`:
  58             os.Stderr.WriteString(info[1:])
  59             return
  60         }
  61     }
  62 
  63     if len(os.Args) > 2 {
  64         const msg = "\x1b[31mmultiple inputs not allowed\x1b[0m\n"
  65         os.Stderr.WriteString(msg)
  66         os.Exit(1)
  67     }
  68 
  69     // figure out whether input should come from a named file or from stdin
  70     path := `-`
  71     if len(os.Args) > 1 {
  72         path = os.Args[1]
  73     }
  74 
  75     err := handleInput(os.Stdout, path)
  76     if err != nil && err != io.EOF && err != errNoMoreOutput {
  77         os.Stderr.WriteString("\x1b[31m")
  78         os.Stderr.WriteString(err.Error())
  79         os.Stderr.WriteString("\x1b[0m\n")
  80         os.Exit(1)
  81     }
  82 }
  83 
  84 // handleInput simplifies control-flow for func main
  85 func handleInput(w io.Writer, path string) error {
  86     if path == `-` {
  87         return convert(w, os.Stdin)
  88     }
  89 
  90     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
  91     //  resp, err := http.Get(path)
  92     //  if err != nil {
  93     //      return err
  94     //  }
  95     //  defer resp.Body.Close()
  96     //  return convert(w, resp.Body)
  97     // }
  98 
  99     f, err := os.Open(path)
 100     if err != nil {
 101         // on windows, file-not-found error messages may mention `CreateFile`,
 102         // even when trying to open files in read-only mode
 103         return errors.New(`can't open file named ` + path)
 104     }
 105     defer f.Close()
 106     return convert(w, f)
 107 }
 108 
 109 // convert simplifies control-flow for func handleInput
 110 func convert(w io.Writer, r io.Reader) error {
 111     bw := bufio.NewWriter(w)
 112     defer bw.Flush()
 113     return json2(bw, r)
 114 }
 115 
 116 // escapedStringBytes helps func handleString treat all string bytes quickly
 117 // and correctly, using their officially-supported JSON escape sequences
 118 //
 119 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 120 var escapedStringBytes = [256][]byte{
 121     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 122     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 123     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 124     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 125     {'\\', 'b'}, {'\\', 't'},
 126     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 127     {'\\', 'f'}, {'\\', 'r'},
 128     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 129     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 130     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 131     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 132     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 133     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 134     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 135     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 136     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 137     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 138     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 139     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 140     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 141     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 142     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 143     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 144     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 145     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 146     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 147     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 148     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 149     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 150     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 151     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 152     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 153     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 154     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 155     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 156     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 157     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 158     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 159     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 160     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 161     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 162     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 163     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 164     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 165 }
 166 
 167 // writeSpaces does what it says, minimizing calls to write-like funcs
 168 func writeSpaces(w *bufio.Writer, n int) {
 169     const spaces = `                                `
 170     if n < 1 {
 171         return
 172     }
 173 
 174     for n >= len(spaces) {
 175         w.WriteString(spaces)
 176         n -= len(spaces)
 177     }
 178     w.WriteString(spaces[:n])
 179 }
 180 
 181 // json2 does it all, given a reader and a writer
 182 func json2(w *bufio.Writer, r io.Reader) error {
 183     dec := json.NewDecoder(r)
 184     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 185     // even if JSON parsers aren't required to guarantee such input-fidelity
 186     // for numbers
 187     dec.UseNumber()
 188 
 189     t, err := dec.Token()
 190     if err == io.EOF {
 191         return errors.New(`input has no JSON values`)
 192     }
 193 
 194     if err = handleToken(w, dec, t, 0, 0); err != nil {
 195         return err
 196     }
 197     // don't forget ending the last line for the last value
 198     w.WriteByte('\n')
 199 
 200     _, err = dec.Token()
 201     if err == io.EOF {
 202         // input is over, so it's a success
 203         return nil
 204     }
 205 
 206     if err == nil {
 207         // a successful `read` is a failure, as it means there are
 208         // trailing JSON tokens
 209         return errors.New(`unexpected trailing data`)
 210     }
 211 
 212     // any other error, perhaps some invalid-JSON-syntax-type error
 213     return err
 214 }
 215 
 216 // handleToken handles recursion for func json2
 217 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 218     switch t := t.(type) {
 219     case json.Delim:
 220         switch t {
 221         case json.Delim('['):
 222             return handleArray(w, dec, pre, level)
 223         case json.Delim('{'):
 224             return handleObject(w, dec, pre, level)
 225         default:
 226             return errors.New(`unsupported JSON syntax ` + string(t))
 227         }
 228 
 229     case nil:
 230         writeSpaces(w, 2*pre)
 231         w.WriteString(`null`)
 232         return nil
 233 
 234     case bool:
 235         writeSpaces(w, 2*pre)
 236         if t {
 237             w.WriteString(`true`)
 238         } else {
 239             w.WriteString(`false`)
 240         }
 241         return nil
 242 
 243     case json.Number:
 244         writeSpaces(w, 2*pre)
 245         w.WriteString(t.String())
 246         return nil
 247 
 248     case string:
 249         return handleString(w, t, pre)
 250 
 251     default:
 252         // return fmt.Errorf(`unsupported token type %T`, t)
 253         return errors.New(`invalid JSON token`)
 254     }
 255 }
 256 
 257 // handleArray handles arrays for func handleToken
 258 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 259     for i := 0; true; i++ {
 260         t, err := dec.Token()
 261         if err != nil {
 262             return err
 263         }
 264 
 265         if t == json.Delim(']') {
 266             if i == 0 {
 267                 writeSpaces(w, 2*pre)
 268                 w.WriteByte('[')
 269                 w.WriteByte(']')
 270             } else {
 271                 w.WriteByte('\n')
 272                 writeSpaces(w, 2*level)
 273                 w.WriteByte(']')
 274             }
 275             return nil
 276         }
 277 
 278         if i == 0 {
 279             writeSpaces(w, 2*pre)
 280             w.WriteByte('[')
 281             w.WriteByte('\n')
 282         } else {
 283             w.WriteByte(',')
 284             w.WriteByte('\n')
 285             if err := w.Flush(); err != nil {
 286                 // a write error may be the consequence of stdout being closed,
 287                 // perhaps by another app along a pipe
 288                 return errNoMoreOutput
 289             }
 290         }
 291 
 292         err = handleToken(w, dec, t, level+1, level+1)
 293         if err != nil {
 294             return err
 295         }
 296     }
 297 
 298     // make the compiler happy
 299     return nil
 300 }
 301 
 302 // handleObject handles objects for func handleToken
 303 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 304     for i := 0; true; i++ {
 305         t, err := dec.Token()
 306         if err != nil {
 307             return err
 308         }
 309 
 310         if t == json.Delim('}') {
 311             if i == 0 {
 312                 writeSpaces(w, 2*pre)
 313                 w.WriteByte('{')
 314                 w.WriteByte('}')
 315             } else {
 316                 w.WriteByte('\n')
 317                 writeSpaces(w, 2*level)
 318                 w.WriteByte('}')
 319             }
 320             return nil
 321         }
 322 
 323         if i == 0 {
 324             writeSpaces(w, 2*pre)
 325             w.WriteByte('{')
 326             w.WriteByte('\n')
 327         } else {
 328             w.WriteByte(',')
 329             w.WriteByte('\n')
 330             if err := w.Flush(); err != nil {
 331                 // a write error may be the consequence of stdout being closed,
 332                 // perhaps by another app along a pipe
 333                 return errNoMoreOutput
 334             }
 335         }
 336 
 337         k, ok := t.(string)
 338         if !ok {
 339             return errors.New(`expected a string for a key-value pair`)
 340         }
 341 
 342         err = handleString(w, k, level+1)
 343         if err != nil {
 344             return err
 345         }
 346 
 347         w.WriteString(": ")
 348 
 349         t, err = dec.Token()
 350         if err == io.EOF {
 351             return errors.New(`expected a value for a key-value pair`)
 352         }
 353 
 354         err = handleToken(w, dec, t, 0, level+1)
 355         if err != nil {
 356             return err
 357         }
 358     }
 359 
 360     // make the compiler happy
 361     return nil
 362 }
 363 
 364 // handleString handles strings for func handleToken, and keys for func
 365 // handleObject
 366 func handleString(w *bufio.Writer, s string, level int) error {
 367     writeSpaces(w, 2*level)
 368     w.WriteByte('"')
 369     for i := range s {
 370         w.Write(escapedStringBytes[s[i]])
 371     }
 372     w.WriteByte('"')
 373     return nil
 374 }