commit a2f357eb3330a282e36f740beaf3a189fc6fec48 Author: gutmet Date: Sun Apr 26 22:57:19 2020 +0200 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1392b88 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +LaymansHex: A layman's hex editor. +Copyright (C) 2020 Alexander Weinhold + +Unless explicitly stated otherwise, the following applies: + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as published by +the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README b/README new file mode 100644 index 0000000..d6523f5 --- /dev/null +++ b/README @@ -0,0 +1,50 @@ +LaymansHex +========== + +LaymansHex takes a (partial) file description and allows tinkering with +the values in a binary file. It thus serves as a sort of layman's +hex editor. + + +File description format +----------------------- + +In EBNF: +``` +description = + { comment } + endianness + definition + { definition } +. + +comment = '#' { LETTER | DIGIT } '\n'. + +endianness = ("little endian" | "big endian") '\n'. + +definition = name ':' type '\n'. + +name = { LETTER | DIGIT }. + +type = ( "byte[" INTEGER "]" ) + | int8 | int16 | int32 | int64 + | uint8 | uint16 | uint32 | uint64 + | float32 | float64 +``` + +The name of a definition line can be left empty to ignore that particular value. +Portions of the binary file not covered be the file description are ignored. + + +Usage +----- + +To get values: laymanshex FORMATFILE BINARY +To set values: laymanshex -set "key123=value123" FORMATFILE BINARY + + +TODO +---- + +There is no way to express offset values inside a file yet. Since this is +relatively common, I will add it at some point. diff --git a/laymanshex.go b/laymanshex.go new file mode 100644 index 0000000..d44d5fc --- /dev/null +++ b/laymanshex.go @@ -0,0 +1,380 @@ +package main + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "errors" + "flag" + "fmt" + "io" + "os" + "strconv" + "strings" + "time" +) + +func optPanic(msg string, err error) { + if err != nil { + panic(errors.New(msg + ": " + err.Error())) + } +} + +type handling struct { + bytes int64 + readFrom func(io.Reader, binary.ByteOrder) string + writeTo func(io.Writer, binary.ByteOrder, string) +} + +func binaryRead(r io.Reader, order binary.ByteOrder, pointerTo interface{}) { + err := binary.Read(r, order, pointerTo) + optPanic(fmt.Sprintf("binary conversion failed (%T)", pointerTo), err) +} + +func binaryWrite(w io.Writer, order binary.ByteOrder, pointerTo interface{}) { + err := binary.Write(w, order, pointerTo) + optPanic(fmt.Sprintf("binary write failed (%T)", pointerTo), err) +} + +var handlings = map[string]handling{ + "int8": handling{1, + func(r io.Reader, bo binary.ByteOrder) string { + var i int8 + binaryRead(r, bo, &i) + return strconv.FormatInt(int64(i), 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseInt(s, 10, 8) + optPanic("conversion of "+s+"to int8 failed", err) + binaryWrite(w, bo, int8(i)) + }}, + "uint8": handling{1, + func(r io.Reader, bo binary.ByteOrder) string { + var i uint8 + binaryRead(r, bo, &i) + return strconv.FormatUint(uint64(i), 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseUint(s, 10, 8) + optPanic("conversion of "+s+"to uint8 failed", err) + binaryWrite(w, bo, uint8(i)) + }}, + "int16": handling{2, + func(r io.Reader, bo binary.ByteOrder) string { + var i int16 + binaryRead(r, bo, &i) + return strconv.FormatInt(int64(i), 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseInt(s, 10, 16) + optPanic("conversion of "+s+"to int16 failed", err) + binaryWrite(w, bo, int16(i)) + }}, + "uint16": handling{2, + func(r io.Reader, bo binary.ByteOrder) string { + var i uint16 + binaryRead(r, bo, &i) + return strconv.FormatUint(uint64(i), 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseUint(s, 10, 16) + optPanic("conversion of "+s+"to uint16 failed", err) + binaryWrite(w, bo, uint16(i)) + }}, + "int32": handling{4, + func(r io.Reader, bo binary.ByteOrder) string { + var i int32 + binaryRead(r, bo, &i) + return strconv.FormatInt(int64(i), 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseInt(s, 10, 32) + optPanic("conversion of "+s+"to int32 failed", err) + binaryWrite(w, bo, int32(i)) + }}, + "uint32": handling{4, + func(r io.Reader, bo binary.ByteOrder) string { + var i uint32 + binaryRead(r, bo, &i) + return strconv.FormatUint(uint64(i), 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseUint(s, 10, 32) + optPanic("conversion of "+s+"to uint32 failed", err) + binaryWrite(w, bo, uint32(i)) + }}, + "int64": handling{8, + func(r io.Reader, bo binary.ByteOrder) string { + var i int64 + binaryRead(r, bo, &i) + return strconv.FormatInt(i, 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseInt(s, 10, 64) + optPanic("conversion of "+s+"to int64 failed", err) + binaryWrite(w, bo, i) + }}, + "uint64": handling{8, + func(r io.Reader, bo binary.ByteOrder) string { + var i uint64 + binaryRead(r, bo, &i) + return strconv.FormatUint(i, 10) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + i, err := strconv.ParseUint(s, 10, 64) + optPanic("conversion of "+s+"to uint64 failed", err) + binaryWrite(w, bo, i) + }}, + "float32": handling{4, + func(r io.Reader, bo binary.ByteOrder) string { + var f float32 + binaryRead(r, bo, &f) + return strconv.FormatFloat(float64(f), 'f', -1, 64) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + f, err := strconv.ParseFloat(s, 32) + optPanic("conversion of "+s+"to float32 failed", err) + binaryWrite(w, bo, float32(f)) + }}, + "float64": handling{8, + func(r io.Reader, bo binary.ByteOrder) string { + var f float64 + binaryRead(r, bo, &f) + return strconv.FormatFloat(f, 'f', -1, 64) + }, + func(w io.Writer, bo binary.ByteOrder, s string) { + f, err := strconv.ParseFloat(s, 64) + optPanic("conversion of "+s+"to float64 failed", err) + binaryWrite(w, bo, float64(f)) + }}, +} + +type filePart struct { + name string + parttype string + handling handling +} + +func (part *filePart) Line() string { + return part.name + " : " + part.parttype +} + +func (part *filePart) setHandling() { + if strings.HasPrefix(part.parttype, "byte") { + tmp := strings.TrimRight(strings.TrimLeft(part.parttype, "byte["), "]") + bytes, err := strconv.ParseInt(tmp, 10, 64) + optPanic(part.name, err) + readFrom := func(r io.Reader, bo binary.ByteOrder) string { + buf := make([]byte, bytes) + var i int64 + for i = 0; i < bytes; i++ { + binaryRead(r, bo, &buf[i]) + } + return hex.EncodeToString(buf) + } + writeTo := func(w io.Writer, bo binary.ByteOrder, s string) { + buf, err := hex.DecodeString(s) + optPanic("invalid string of hex values: "+s, err) + binaryWrite(w, bo, buf) + } + part.handling = handling{bytes, readFrom, writeTo} + + } else { + if handling, ok := handlings[part.parttype]; !ok { + panic(part.Line() + ": Don't know type") + } else { + part.handling = handling + } + } +} + +type fileDescription struct { + byteOrder binary.ByteOrder + parts []filePart +} + +func trim(s string) string { + return strings.TrimSpace(s) +} + +var debug bool + +func info(i string, s string) { + if debug { + fmt.Fprintln(os.Stderr, i, "-->", s) + } +} + +func readPart(line string) *filePart { + part := &filePart{} + splitline := strings.Split(line, ":") + if len(splitline) != 2 { + panic("invalid definition: " + line) + } + part.name = trim(splitline[0]) + part.parttype = trim(splitline[1]) + info("definition", part.Line()) + part.setHandling() + return part +} + +func readFileDescription(path string) fileDescription { + errmsg := "read format file" + descr := fileDescription{} + f, err := os.Open(path) + defer f.Close() + optPanic("open format file", err) + buf := bufio.NewReader(f) + /*----------------------------------*/ + line, err := buf.ReadString('\n') + optPanic(errmsg, err) + line = trim(line) + for strings.HasPrefix(line, "#") { + info("comment", line) + line, err = buf.ReadString('\n') + optPanic(errmsg, err) + line = trim(line) + } + /*----------------------------------*/ + line = trim(line) + if line == "big endian" { + descr.byteOrder = binary.BigEndian + } else if line == "little endian" { + descr.byteOrder = binary.LittleEndian + } else { + panic(errmsg + ": expected endianness ('little endian' or 'big endian'), got :" + line) + } + info("endianness", line) + /*----------------------------------*/ + line, err = buf.ReadString('\n') + optPanic(errmsg, err) + line = trim(line) + for err != io.EOF && line != "" { + part := readPart(line) + descr.parts = append(descr.parts, *part) + line, err = buf.ReadString('\n') + line = trim(line) + } + line = trim(line) + if err == io.EOF && line != "" { + descr.parts = append(descr.parts, *readPart(line)) + } else if err != io.EOF { + optPanic(errmsg, err) + } + return descr +} + +func readAssignments(descr fileDescription, s string) map[string]string { + assignments := strings.Split(s, ",") + m := make(map[string]string) + for _, assignment := range assignments { + kv := strings.Split(assignment, "=") + if len(kv) != 2 { + panic("weird parameters in set: " + assignment + "\nWant list of comma-separated key=value assignments") + } + key := trim(kv[0]) + value := trim(kv[1]) + found := false + for _, part := range descr.parts { + if key == part.name { + found = true + } + } + if found { + m[key] = value + } else { + panic("unknown key: " + key) + } + } + return m +} + +func backup(binpath string) { + bin, err := os.Open(binpath) + defer bin.Close() + optPanic("failed to open binary", err) + timestamp := time.Now().Format("20060102150405") + fi, err := bin.Stat() + optPanic("failed to get permissions of binary", err) + backuppath := binpath + ".bak" + timestamp + backup, err := os.OpenFile(backuppath, os.O_RDWR|os.O_CREATE, fi.Mode()) + defer backup.Close() + _, err = io.Copy(backup, bin) + optPanic("failed to create backup", err) + backup.Chmod(fi.Mode()) + fmt.Println("Created backup " + backuppath + "\n") +} + +func setValues(descr fileDescription, binpath string, vals map[string]string) { + backup(binpath) + f, err := os.OpenFile(binpath, os.O_RDWR, 0666) + optPanic("failed to write-open binary", err) + defer f.Close() + for _, part := range descr.parts { + needSeek := true + if part.name != "" { + if val, ok := vals[part.name]; ok { + part.handling.writeTo(f, descr.byteOrder, val) + needSeek = false + } + } + if needSeek { + f.Seek(part.handling.bytes, 1) + } + } +} + +func getValues(descr fileDescription, binpath string) map[string]string { + vals := make(map[string]string) + f, err := os.Open(binpath) + defer f.Close() + optPanic("failed to open binary", err) + for _, part := range descr.parts { + if part.name == "" { + f.Seek(part.handling.bytes, 1) + } else { + vals[part.name] = part.handling.readFrom(f, descr.byteOrder) + } + } + return vals +} + +func padding(descr fileDescription) string { + pad := 0 + for _, part := range descr.parts { + if n := len(part.name); n > pad { + pad = n + } + } + return strconv.Itoa(pad) +} + +func printValues(descr fileDescription, vals map[string]string) { + for _, part := range descr.parts { + if part.name != "" { + fmt.Printf("%"+padding(descr)+"s = %s\n", part.name, vals[part.name]) + } + } +} + +func printUsage() { + fmt.Fprintf(os.Stderr, "Usage: %s [Options] FORMATFILE BINARY\n", os.Args[0]) + flag.PrintDefaults() +} + +func main() { + var assignments string + flag.StringVar(&assignments, "set", "", "key=value pairs (e.g. \"key1=value1,key2=value2\")") + flag.BoolVar(&debug, "debug", false, "print recognized parts of the format file") + flag.Parse() + args := flag.Args() + if len(args) != 2 { + printUsage() + os.Exit(-1) + } + descr := readFileDescription(args[0]) + if assignments != "" { + setValues(descr, args[1], readAssignments(descr, assignments)) + } + printValues(descr, getValues(descr, args[1])) +}