| // 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 $V23_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 |
| } |
| fmt.Fprintln(env.Stdout, vom.Dump(binbytes)) |
| return nil |
| } |
| // 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 |
| } |