File: bitdump.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 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 bitdump.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "errors"
  37     "io"
  38     "os"
  39     "strconv"
  40 )
  41 
  42 // Note: the code is avoiding using the fmt package to save hundreds of
  43 // kilobytes on the resulting executable, which is a noticeable difference.
  44 
  45 const info = `
  46 bitdump [options...] [filenames...]
  47 
  48 
  49 Show all bits for all input bytes, starting each output line with the
  50 leading byte's offset.
  51 
  52 All (optional) leading options start with either single or double-dash:
  53 
  54     -h, -help                  show this help message
  55     -no-offset, -no-offsets    don't start lines with current byte offsets
  56     -tab, -tabs                separate items with tabs instead of spaces
  57 `
  58 
  59 const itemsPerLine = 8
  60 
  61 // errNoMoreOutput is a dummy error whose message is ignored, and which
  62 // causes the app to quit immediately and successfully
  63 var errNoMoreOutput = errors.New(`no more output`)
  64 
  65 func main() {
  66     emitOffsets := true
  67     separator := byte(' ')
  68     args := os.Args[1:]
  69 
  70 out:
  71     for len(args) > 0 {
  72         switch args[0] {
  73         case `-h`, `--h`, `-help`, `--help`:
  74             os.Stdout.WriteString(info[1:])
  75             return
  76         case `-no-offset`, `--no-offset`, `-no-offsets`, `--no-offsets`:
  77             emitOffsets = false
  78             args = args[1:]
  79         case `-tab`, `--tab`, `-tabs`, `--tabs`:
  80             separator = '\t'
  81             args = args[1:]
  82         default:
  83             break out
  84         }
  85     }
  86 
  87     if len(args) > 0 && args[0] == `--` {
  88         args = args[1:]
  89     }
  90 
  91     var p params
  92     p.emitOffset = emitNoOffset
  93     if emitOffsets {
  94         p.emitOffset = emitDecimalOffset
  95     }
  96     p.separator = separator
  97 
  98     if err := run(os.Stdout, p, args); isActualError(err) {
  99         os.Stderr.WriteString(err.Error())
 100         os.Stderr.WriteString("\n")
 101         os.Exit(1)
 102     }
 103 }
 104 
 105 type params struct {
 106     offset     *int64
 107     emitOffset func(w *bufio.Writer, offset int64, sep byte)
 108     separator  byte
 109 }
 110 
 111 func run(w io.Writer, p params, args []string) error {
 112     offset := int64(0)
 113     p.offset = &offset
 114     bw := bufio.NewWriter(w)
 115     defer func() {
 116         if offset%itemsPerLine == 0 && offset > 0 {
 117             bw.WriteByte('\n')
 118         }
 119         bw.Flush()
 120     }()
 121 
 122     if len(args) == 0 {
 123         return bitdump(bw, os.Stdin, p)
 124     }
 125 
 126     for _, name := range args {
 127         if err := handleFile(bw, name, p); err != nil {
 128             return err
 129         }
 130     }
 131     return nil
 132 }
 133 
 134 func handleFile(w *bufio.Writer, name string, p params) error {
 135     if name == `` || name == `-` {
 136         return bitdump(w, os.Stdin, p)
 137     }
 138 
 139     f, err := os.Open(name)
 140     if err != nil {
 141         return errors.New(`can't read from file named "` + name + `"`)
 142     }
 143     defer f.Close()
 144 
 145     return bitdump(w, f, p)
 146 }
 147 
 148 // isActualError is to figure out whether not to ignore an error, and thus
 149 // show it as an error message
 150 func isActualError(err error) bool {
 151     return err != nil && err != io.EOF && err != errNoMoreOutput
 152 }
 153 
 154 func bitdump(w *bufio.Writer, r io.Reader, p params) error {
 155     var buf [32 * 1024]byte
 156     defer w.Flush()
 157 
 158     for {
 159         n, err := r.Read(buf[:])
 160         if n < 1 && err == io.EOF {
 161             return nil
 162         }
 163 
 164         if err != nil && err != io.EOF {
 165             return err
 166         }
 167 
 168         chunk := buf[:n]
 169 
 170         for len(chunk) >= itemsPerLine {
 171             if err := emitChunk(w, chunk[:itemsPerLine], p); err != nil {
 172                 return err
 173             }
 174 
 175             chunk = chunk[itemsPerLine:]
 176             *p.offset += itemsPerLine
 177         }
 178 
 179         if len(chunk) > 0 {
 180             if err := emitChunk(w, chunk, p); err != nil {
 181                 return err
 182             }
 183 
 184             *p.offset += int64(len(chunk))
 185         }
 186     }
 187 }
 188 
 189 func emitDecimalOffset(w *bufio.Writer, offset int64, sep byte) {
 190     const pad = `00000000`
 191     var str [24]byte
 192 
 193     s := strconv.AppendInt(str[:0], offset, 10)
 194     // pad small offsets with leading zeros
 195     if len(s) < len(pad) {
 196         w.WriteString(pad[len(s):])
 197     }
 198     w.Write(s)
 199     w.WriteByte(sep)
 200 }
 201 
 202 func emitNoOffset(w *bufio.Writer, offset int64, sep byte) {
 203     // deliberately does nothing
 204 }
 205 
 206 func emitChunk(w *bufio.Writer, chunk []byte, p params) error {
 207     p.emitOffset(w, *p.offset, p.separator)
 208     for i, b := range chunk {
 209         if i > 0 {
 210             w.WriteByte(p.separator)
 211         }
 212         w.WriteString(bits[b])
 213     }
 214 
 215     if err := w.WriteByte('\n'); err != nil {
 216         // assume a write error is the consequence of stdout
 217         // being closed, perhaps by another app along a pipe
 218         return errNoMoreOutput
 219     }
 220     return nil
 221 }
 222 
 223 /*
 224 tlp = '(f"{bin(v)[2:]:>08}" for v in range(256))' | lineup 6 |
 225 gsub '\t' '`, `' | tlp 'f"\t`{l}`,"'
 226 */
 227 var bits = [256]string{
 228     `00000000`, `00000001`, `00000010`, `00000011`, `00000100`, `00000101`,
 229     `00000110`, `00000111`, `00001000`, `00001001`, `00001010`, `00001011`,
 230     `00001100`, `00001101`, `00001110`, `00001111`, `00010000`, `00010001`,
 231     `00010010`, `00010011`, `00010100`, `00010101`, `00010110`, `00010111`,
 232     `00011000`, `00011001`, `00011010`, `00011011`, `00011100`, `00011101`,
 233     `00011110`, `00011111`, `00100000`, `00100001`, `00100010`, `00100011`,
 234     `00100100`, `00100101`, `00100110`, `00100111`, `00101000`, `00101001`,
 235     `00101010`, `00101011`, `00101100`, `00101101`, `00101110`, `00101111`,
 236     `00110000`, `00110001`, `00110010`, `00110011`, `00110100`, `00110101`,
 237     `00110110`, `00110111`, `00111000`, `00111001`, `00111010`, `00111011`,
 238     `00111100`, `00111101`, `00111110`, `00111111`, `01000000`, `01000001`,
 239     `01000010`, `01000011`, `01000100`, `01000101`, `01000110`, `01000111`,
 240     `01001000`, `01001001`, `01001010`, `01001011`, `01001100`, `01001101`,
 241     `01001110`, `01001111`, `01010000`, `01010001`, `01010010`, `01010011`,
 242     `01010100`, `01010101`, `01010110`, `01010111`, `01011000`, `01011001`,
 243     `01011010`, `01011011`, `01011100`, `01011101`, `01011110`, `01011111`,
 244     `01100000`, `01100001`, `01100010`, `01100011`, `01100100`, `01100101`,
 245     `01100110`, `01100111`, `01101000`, `01101001`, `01101010`, `01101011`,
 246     `01101100`, `01101101`, `01101110`, `01101111`, `01110000`, `01110001`,
 247     `01110010`, `01110011`, `01110100`, `01110101`, `01110110`, `01110111`,
 248     `01111000`, `01111001`, `01111010`, `01111011`, `01111100`, `01111101`,
 249     `01111110`, `01111111`, `10000000`, `10000001`, `10000010`, `10000011`,
 250     `10000100`, `10000101`, `10000110`, `10000111`, `10001000`, `10001001`,
 251     `10001010`, `10001011`, `10001100`, `10001101`, `10001110`, `10001111`,
 252     `10010000`, `10010001`, `10010010`, `10010011`, `10010100`, `10010101`,
 253     `10010110`, `10010111`, `10011000`, `10011001`, `10011010`, `10011011`,
 254     `10011100`, `10011101`, `10011110`, `10011111`, `10100000`, `10100001`,
 255     `10100010`, `10100011`, `10100100`, `10100101`, `10100110`, `10100111`,
 256     `10101000`, `10101001`, `10101010`, `10101011`, `10101100`, `10101101`,
 257     `10101110`, `10101111`, `10110000`, `10110001`, `10110010`, `10110011`,
 258     `10110100`, `10110101`, `10110110`, `10110111`, `10111000`, `10111001`,
 259     `10111010`, `10111011`, `10111100`, `10111101`, `10111110`, `10111111`,
 260     `11000000`, `11000001`, `11000010`, `11000011`, `11000100`, `11000101`,
 261     `11000110`, `11000111`, `11001000`, `11001001`, `11001010`, `11001011`,
 262     `11001100`, `11001101`, `11001110`, `11001111`, `11010000`, `11010001`,
 263     `11010010`, `11010011`, `11010100`, `11010101`, `11010110`, `11010111`,
 264     `11011000`, `11011001`, `11011010`, `11011011`, `11011100`, `11011101`,
 265     `11011110`, `11011111`, `11100000`, `11100001`, `11100010`, `11100011`,
 266     `11100100`, `11100101`, `11100110`, `11100111`, `11101000`, `11101001`,
 267     `11101010`, `11101011`, `11101100`, `11101101`, `11101110`, `11101111`,
 268     `11110000`, `11110001`, `11110010`, `11110011`, `11110100`, `11110101`,
 269     `11110110`, `11110111`, `11111000`, `11111001`, `11111010`, `11111011`,
 270     `11111100`, `11111101`, `11111110`, `11111111`,
 271 }