blob: 2da161e248eca81c558b0664876c4311a884e79f [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
package main
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"unicode"
"v.io/v23/vdl"
"v.io/v23/vom"
"v.io/x/lib/cmdline"
)
func main() {
cmdline.Main(cmdVom)
}
var cmdVom = &cmdline.Command{
Name: "vom",
Short: "helps debug the Vanadium Object Marshaling wire protocol",
Long: `
Command vom helps debug the Vanadium Object Marshaling wire protocol.
`,
Children: []*cmdline.Command{cmdDecode, cmdDump},
}
var cmdDecode = &cmdline.Command{
Runner: cmdline.RunnerFunc(runDecode),
Name: "decode",
Short: "Decode data encoded in the vom format",
Long: `
Decode decodes data encoded in the vom format. If no arguments are provided,
decode reads the data from stdin, otherwise the argument is the data.
By default the data is assumed to be represented in hex, with all whitespace
anywhere in the data ignored. Use the -data flag to specify other data
representations.
`,
ArgsName: "[data]",
ArgsLong: "[data] is the data to decode; if not specified, reads from stdin",
}
var cmdDump = &cmdline.Command{
Runner: cmdline.RunnerFunc(runDump),
Name: "dump",
Short: "Dump data encoded in the vom format into formatted output",
Long: `
Dump dumps data encoded in the vom format, generating formatted output
describing each portion of the encoding. If no arguments are provided, dump
reads the data from stdin, otherwise the argument is the data.
By default the data is assumed to be represented in hex, with all whitespace
anywhere in the data ignored. Use the -data flag to specify other data
representations.
Calling "vom dump" with no flags and no arguments combines the default stdin
mode with the default hex mode. This default mode is special; certain non-hex
characters may be input to represent commands:
. (period) Calls Dumper.Status to get the current decoding status.
; (semicolon) Calls Dumper.Flush to flush output and start a new message.
This lets you cut-and-paste hex strings into your terminal, and use the commands
to trigger status or flush calls; i.e. a rudimentary debugging UI.
See v.io/v23/vom.Dumper for details on the dump output.
`,
ArgsName: "[data]",
ArgsLong: "[data] is the data to dump; if not specified, reads from stdin",
}
var (
flagDataRep = dataRepHex
)
func init() {
cmdDecode.Flags.Var(&flagDataRep, "data",
"Data representation, one of "+fmt.Sprint(dataRepAll))
cmdDump.Flags.Var(&flagDataRep, "data",
"Data representation, one of "+fmt.Sprint(dataRepAll))
}
func runDecode(env *cmdline.Env, args []string) error {
// Convert all inputs into a reader over binary bytes.
var data string
switch {
case len(args) > 1:
return env.UsageErrorf("too many args")
case len(args) == 1:
data = args[0]
default:
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return err
}
data = string(bytes)
}
binbytes, err := dataToBinaryBytes(data)
if err != nil {
return err
}
reader := bytes.NewBuffer(binbytes)
// Decode the binary bytes.
// TODO(toddw): Add a flag to set a specific type to decode into.
decoder := vom.NewDecoder(reader)
var result *vdl.Value
if err := decoder.Decode(&result); err != nil {
return err
}
fmt.Fprintln(env.Stdout, result)
if reader.Len() != 0 {
return fmt.Errorf("%d leftover bytes: % x", reader.Len(), reader.String())
}
return nil
}
func runDump(env *cmdline.Env, args []string) error {
// Handle non-streaming cases.
switch {
case len(args) > 1:
return env.UsageErrorf("too many args")
case len(args) == 1:
binbytes, err := dataToBinaryBytes(args[0])
if err != nil {
return err
}
d, err := vom.Dump(binbytes)
fmt.Fprintln(env.Stdout, d)
return err
}
// Handle streaming from stdin.
dumper := vom.NewDumper(vom.NewDumpWriter(env.Stdout))
defer dumper.Close()
// Handle simple non-hex cases.
switch flagDataRep {
case dataRepBinary:
_, err := io.Copy(dumper, os.Stdin)
return err
}
return runDumpHexStream(dumper)
}
// runDumpHexStream handles the hex stdin-streaming special-case, with commands
// for status and flush. This is tricky because we need to strip whitespace,
// handle commands where they appear in the stream, and deal with the fact that
// it takes two hex characters to encode a single byte.
//
// The strategy is to run a ReadLoop that reads into a reasonably-sized buffer.
// Inside the ReadLoop we take the buffer, strip whitespace, and keep looping to
// process all data up to each command, and then process the command. If a
// command appears in the middle of two hex characters representing a byte, we
// send the command first, before sending the byte.
//
// Any leftover non-command single byte is stored in buf and bufStart is set, so
// that the next iteration of ReadLoop can read after those bytes.
func runDumpHexStream(dumper *vom.Dumper) error {
buf := make([]byte, 1024)
bufStart := 0
ReadLoop:
for {
n, err := os.Stdin.Read(buf[bufStart:])
switch {
case n == 0 && err == io.EOF:
return nil
case n == 0 && err != nil:
return err
}
// We may have hex interspersed with spaces and commands. The strategy is
// to strip all whitespace, and process each even-sized chunk of hex bytes
// up to a command or the end of the buffer.
//
// Data that appears before a command is written before the command, and
// data after the command is written after. But if a command appears in the
// middle of two hex characters representing a byte, we send the command
// first, before sending the byte.
hexbytes := bytes.Map(dropWhitespace, buf[:bufStart+n])
for len(hexbytes) > 0 {
end := len(hexbytes)
cmdIndex := bytes.IndexAny(hexbytes, ".;")
if cmdIndex != -1 {
end = cmdIndex
} else if end == 1 {
// We have a single non-command byte left in hexbytes; copy it into buf
// and set bufStart.
copy(buf, hexbytes[0:1])
bufStart = 1
continue ReadLoop
}
if end%2 == 1 {
end -= 1 // Ensure the end is on an even boundary.
}
// Write this even-sized chunk of hex bytes to the dumper.
binbytes, err := hex.DecodeString(string(hexbytes[:end]))
if err != nil {
return err
}
if _, err := dumper.Write(binbytes); err != nil {
return err
}
// Handle commands.
if cmdIndex != -1 {
switch cmd := hexbytes[cmdIndex]; cmd {
case '.':
dumper.Status()
case ';':
dumper.Flush()
default:
return fmt.Errorf("unhandled command %q", cmd)
}
// Move data after the command forward.
copy(hexbytes[cmdIndex:], hexbytes[cmdIndex+1:])
hexbytes = hexbytes[:len(hexbytes)-1]
}
// Move data after the end forward.
copy(hexbytes, hexbytes[end:])
hexbytes = hexbytes[:len(hexbytes)-end]
}
bufStart = 0
}
}
func dataToBinaryBytes(data string) ([]byte, error) {
// Transform all data representations to binary.
switch flagDataRep {
case dataRepHex:
// Remove all whitespace out of the hex string.
binbytes, err := hex.DecodeString(strings.Map(dropWhitespace, data))
if err != nil {
return nil, err
}
return binbytes, nil
}
return []byte(data), nil
}
func dropWhitespace(r rune) rune {
if unicode.IsSpace(r) {
return -1
}
return r
}