5
\$\begingroup\$

In learning golang, I wrote a small CLI utility that will take paths as arguments and list out their md5 hash as hex strings. Included are two flags that alter functionality, --check | -c which takes an additional file, hashes both, and returns whether or not they match (and exits with the proper return code), and --text | -t that takes the path to a text file that is presumed to have exactly the contents of the hexified MD5 sum and checks it (as above).

The repository is here and code is below:

package main

import (
    "bytes"
    "crypto/md5"
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "strings"
)

var checkSum, textFile string

// build Flags
func init() {
    const (
        checkSumDefault = ""
        checkSumUsage   = "File to check against"
        textFileDefault = ""
        textFileUsage   = "File that has the md5 hash as its only content"
    )

    flag.StringVar(&checkSum, "check", checkSumDefault, checkSumUsage)
    flag.StringVar(&checkSum, "c", checkSumDefault, checkSumUsage)
    flag.StringVar(&textFile, "text", textFileDefault, textFileUsage)
    flag.StringVar(&textFile, "t", textFileDefault, textFileUsage)
}

func makeHash(fname string) [16]byte {
    data, err := ioutil.ReadFile(fname)
    if err != nil {
        panic(fmt.Sprintf("Failed to read file %s", fname))
    }
    return md5.Sum(data)
}

func main() {
    flag.Parse()
    var result int

    if checkSum != "" {
        // check mode -- against file
        checkSumHash := makeHash(checkSum)
        argHash := makeHash(flag.Arg(0))
        if checkSumHash == argHash {
            fmt.Print("They match!")
            result = 0
        } else {
            fmt.Print("No match")
            result = 1
        }
    } else if textFile != "" {
        // check mode -- against text

        checkSum, err := ioutil.ReadFile(textFile)
        if err != nil {
            log.Println(err)
            log.Fatalf("Can't read text file %s", textFile)
        }
        checkSum = bytes.TrimSpace(checkSum)
        argHash := makeHash(flag.Arg(0))
        if strings.EqualFold(fmt.Sprintf("%x", argHash), string(checkSum)) {
            fmt.Print("They match!")
            result = 0
        } else {
            fmt.Print("No match")
            result = 1
        }
    } else {
        // print mode
        for _, fname := range flag.Args() {
            hash := makeHash(fname)
            fmt.Printf("%x\n", hash)
        }
        result = 0
    }
    os.Exit(result)
}

I feel like I should refactor out the different functions and have each one return its success code. That's something I'll do when I have a little time free to poke at it.

The biggest question, however, is the best way to compare the [16]byte argHash to the []byte checkSum if --text is specified. Currently I'm using bytes.TrimSpace, casting to string, and comparing that with Sprintf("%x", argHash). Is there a better way? Seems silly to convert both to string rather than comparing bytes, but I don't know a better way to do it.

\$\endgroup\$
1
\$\begingroup\$

To clearly state our problem/task: We want to compare 2 checksums, one supplied as a text being the hexadecimal representation, and the other being an array of the "raw" bytes.

2 forms exist: a hexadecimal representation and raw bytes. To compare them, we need the same representation. So 2 possible paths:

1. Convert the second to hex representation

Let's see your proposed solution. If we want to handle leading/trailing spaces, and if the text input may contain lowercased and uppercased hex digits, your solution is as simple as it can be.

A variation may be to convert the text to lowercased, and then we can simply compare strings without strings.EqualFold(). The lowercase conversion can be done by calling strings.ToLower() or since we already have the input as []byte, by bytes.ToLower():

checkSum = bytes.ToLower(bytes.TrimSpace(checkSum))
argHash := makeHash(flag.Arg(0))
if fmt.Sprintf("%x", argHash) == string(checkSum) {
    // They match
} else {
    // They don't match
}

2. Convert the first to raw bytes

Compare slices ([]byte)

We may choose to convert the text checksum back to a []byte which holds the raw bytes of the checksum (NOT the bytes of the UTF-8 encoded hex representation).

We can parse the hex representation simply with hex.DecodeString(). Or even better: since we have the text checksum as a []byte, we can use hex.Decode() which takes input as a []byte.

As an extra gain, we don't even have to care about lower or upper case: hex.Decode() handles that for us.

And we can convert [16]byte to []byte by simply slicing it. Once we have 2 []byte, we can use bytes.Equal() to compare them (in Go slices are not comparable unlike arrays).

checkSum = bytes.TrimSpace(checkSum)
dst := make([]byte, 16)
if _, err := hex.Decode(dst, checkSum); err != nil {
    // Invalid input, not hex string or not 16 bytes!
} else {
    argHash := makeHash(flag.Arg(0))
    if bytes.Equal(argHash[:], dst) {
        // They match
    } else {
        // They don't match
    }
}

Compare arrays ([16]byte)

As a variation of the previous solution, we will use arrays to do the comparison as arrays are comparable.

Since makeHash() already returns an array [16]byte, we only need to get the raw bytes of the text checksum into an array. The simplest and fastest is to create an array [16]byte, and pass such a slice to hex.Decode() that shares its backing array with our new array. We can obtain such a slice by simply slicing the array:

checkSum = bytes.TrimSpace(checkSum)
dst := [16]byte{}
if _, err := hex.Decode(dst[:], checkSum); err != nil {
    // Invalid input, not hex string or not 16 bytes!
} else {
    if makeHash(flag.Arg(0)) == dst) {
        // They match
    } else {
        // They don't match
    }
}

Compare manually (byte-by-byte)

We can also do the comparison manually, it's relatively easy and straightforward.

But first to do it manually, let's create a simple helper function which tells if a hex digit (the text representation) equals to the raw data:

func match(hex, raw byte) bool {
    if raw < 10 {
        return hex-'0' == raw
    }
    return hex-'a'+10 == raw || hex-'A'+10 == raw
}

And with this the solution:

checkSum = bytes.TrimSpace(checkSum)
argHash := makeHash(flag.Arg(0))
if len(checkSum) != 2*len(argHash) {
    // Quick check: length differ, they don't match!
} else {
    equal := true
    for i, v := range argHash {
        if !match(checkSum[i*2], v >> 4) || !match(checkSum[i*2+1], v & 0x0f) {
            equal = false
            break
        }
    }
    // Now the variable "equal" tells if they are equal
}
\$\endgroup\$
  • \$\begingroup\$ Thank you for your detailed response! I don't have the time to absorb all your suggestions just yet and the details of arrays vs slices are still hazy to me, coming from Python, so it takes a little longer to ponder. I'll dig into this tomorrow and leave a more pointed comment! :) \$\endgroup\$ – Adam Smith Nov 9 '15 at 7:51
  • \$\begingroup\$ @AdamSmith Ok, sure. I also added another solution: "Compare manually". \$\endgroup\$ – icza Nov 9 '15 at 8:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.