]> git.e2factory.org Git - e2factory.git/commitdiff
Add digest, a message digest handling module.
authorTobias Ulmer <tu@emlix.com>
Thu, 13 Dec 2012 18:26:11 +0000 (19:26 +0100)
committerTobias Ulmer <tu@emlix.com>
Tue, 26 Feb 2013 18:07:12 +0000 (19:07 +0100)
Digest allows creation, verification, computation, parsing and writing
message digest as created and read by sha1sum and similar programs.
Currently MD5 support is incomplete (need MD5 module first).

Signed-off-by: Tobias Ulmer <tu@emlix.com>
local/digest.lua [new file with mode: 0644]

diff --git a/local/digest.lua b/local/digest.lua
new file mode 100644 (file)
index 0000000..be8f8e6
--- /dev/null
@@ -0,0 +1,363 @@
+--- Message digest module.
+-- Read, verify and write message digest files like those generated by
+-- sha1sum or md5sum.
+--
+-- @module local.digest
+
+--[[
+   e2factory, the emlix embedded build system
+
+   Copyright (C) 2012 Tobias Ulmer <tu@emlix.com>, emlix GmbH
+
+   For more information have a look at http://www.e2factory.org
+
+   e2factory is a registered trademark by emlix GmbH.
+
+   This file is part of e2factory, the emlix embedded build system.
+
+   e2factory is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   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 <http://www.gnu.org/licenses/>.
+]]
+
+local digest = {}
+
+local e2lib = require("e2lib")
+local hash = require("hash")
+local err = require("err")
+local strict = require("strict")
+
+--- Digest table.
+-- Contains an array of digest entries.
+--
+-- @table dt
+-- @see dt_entry
+
+--- MD5 digest type.
+-- Used to denote the checksum type.
+-- @see dt_entry
+digest.MD5 = 1
+
+--- Length of an alphanumeric MD5 sum string.
+digest.MD5_LEN = 32
+
+--- SHA1 digest type.
+-- Used to denote the checksum type.
+-- @see dt_entry
+digest.SHA1 = 2
+
+--- Length of an alphanumeric SHA1 sum string.
+digest.SHA1_LEN = 40
+
+--- Digest entry table.
+-- Holds the actual fields of a message digest entry.
+--
+-- @table dt_entry
+-- @field digest Digest type of the checksum.
+-- @field checksum The checksum (string).
+-- @field name File name (string). Should be a relative path.
+-- @field name2check File name (string) that should be checked instead of
+--        name by verify(). This field is optional, and not part of the message
+--        digest file. Must be an absolute path.
+-- @see dt
+-- @see verify()
+
+--- Parse a digest file.
+-- Returns a new digest table filled with digest entries.
+--
+-- @param filename The name of the file that should be parsed.
+-- @return A digest table; false when an error is encountered.
+-- @return An error object on failure.
+-- @see dt
+-- @see dt_entry
+function digest.parse(filename)
+    assert(type(filename) == "string")
+
+    local e = err.new("error parsing message digest file '%s'", filename)
+
+    local fd, re = io.open(filename, "r")
+    if not fd then
+        return false, e:append("%s", e)
+    end
+
+    local dt = {}
+    local linenr = 0
+
+    while true do
+        linenr = linenr + 1
+        local line = fd:read("*l")
+        if not line then
+            break
+        end
+
+        -- XXX: This is a pretty naive way to parse the file format.
+        -- Replace it with something more robust.
+        local checksum, filenm = line:match("^([0-9a-z]+)  (%S+)$")
+        if not checksum or not filenm then
+            fd:close()
+            return false, e:append("could not parse file format in line %d",
+            linenr)
+        end
+
+        local entry = {}
+        entry.checksum = checksum
+        entry.name = filenm
+
+        if string.len(checksum) == digest.MD5_LEN then
+            entry.digest = digest.MD5
+        elseif string.len(checksum) == digest.SHA1_LEN then
+            entry.digest = digest.SHA1
+        else
+            fd:close()
+            return false, e:append("unknown digest type in line %d", linenr)
+        end
+
+        table.insert(dt, entry)
+    end
+
+    return dt
+end
+
+--- Create empty digest table.
+-- This is just a helper to improve code clarity.
+-- @return An empty digest table.
+function digest.new()
+    return {}
+end
+
+--- Create a new digest entry.
+-- Adds a new digest entry to the digest table. Returns the new digest entry for
+-- easy modification. This function does not check any arguments except for
+-- "dt". It is legal to set digest entry fields to nil. It does not fail under
+-- normal circumstances.
+-- See dt_entry for a better description of the arguments.
+-- @param dt A digest table
+-- @param digest Digest type
+-- @param checksum Checksum string
+-- @param name Filename string
+-- @param name2check Optional filename string.
+-- return A new digest entry, filled with all the arguments.
+-- @see dt_entry
+function digest.new_entry(dt, digest, checksum, name, name2check)
+    assert(type(dt) == "table")
+
+    local entry = {
+        digest = digest,
+        checksum = checksum,
+        name = name,
+        name2check = name2check,
+    }
+
+    table.insert(dt, entry)
+    return entry
+end
+
+local function compute_checksum_entry(pos, entry, directory, verify)
+    local rc, re
+    local filename, computedcs
+
+    if directory then
+        filename = e2lib.join("/", directory, entry.name)
+    else
+        if not entry.name2check then
+            return false, err.new("internal error: name2check requested but "..
+                "unset")
+        end
+        filename = e2lib.join("/", entry.name2check)
+    end
+
+    if entry.digest == digest.SHA1 then
+        -- XXX: We assume the hash module returns SHA1 checksums. Not nice.
+        local hc, re = hash.hash_start()
+        if not hc then
+            return false, re
+        end
+
+        rc, re = hc:hash_file(filename)
+        if not rc then
+            return false, re
+        end
+
+        computedcs, re = hc:hash_finish()
+        if not computedcs then
+            return false, re
+        end
+    elseif entry.digest.MD5 then
+        -- XXX: Fix this for backwards compat.
+        return false, err.new("Computing MD5 checksums is currently not "..
+            "supported")
+    end
+
+    if verify then
+        if string.lower(entry.checksum) ~= computedcs then
+            return false, err.new("checksum mismatch for file '%s'",
+                entry.name)
+        end
+    else
+        entry.checksum = computedcs
+    end
+
+    return true
+end
+
+--- Compute checksums in a digest table.
+--
+-- @param dt A digest table filled with partial digest entries. Fields "digest",
+--           "name", and optionally "name2check" must be filled in.
+-- @param directory A directory (string) containing the files in "name" which
+--                  should be checksum'ed. Directory may be false, in which case
+--                  the name2check field is used.
+-- @return True on success, false on error.
+-- @return An error object on failure.
+function digest.checksum(dt, directory)
+    assert(type(dt) == "table")
+    assert(type(directory) == "string" or directory == false)
+
+    local e = err.new("checksuming failed")
+    local rc, re
+
+    for pos, entry in ipairs(dt) do
+        rc, re = compute_checksum_entry(pos, entry, directory, false)
+        if not rc then
+            return false, e:cat(re)
+        end
+    end
+
+    return true
+end
+
+--- Verify a digest table.
+--
+-- @param dt A digest table.
+-- @param directory A directory containing the files which should be verified.
+-- Directory may be false, in which case the digest entry name2check field is
+--           used.
+-- @return True on success, false on error.
+-- @return An error object on failure.
+function digest.verify(dt, directory)
+    assert(type(dt) == "table")
+    assert(type(directory) == "string" or directory == false)
+
+    local e = err.new("checksum verification failed")
+    local rc, re
+
+    for pos, entry in ipairs(dt) do
+        rc, re = compute_checksum_entry(pos, entry, directory, true)
+        if not rc then
+            return false, e:cat(re)
+        end
+    end
+
+    return true
+end
+
+--- Count of digest entries in a digest table.
+--
+-- @param dt A digest table.
+-- @return The number of digest entries in the table. This function should not
+--         fail.
+function digest.count(dt)
+    assert(type(dt) == "table")
+
+    return #dt
+end
+
+--- Sanity check a digest table before writing.
+-- There must be at least one digest entry, all relevant fields (checksum,
+-- digest) must be set. In addition, "digest" needs to be set and must be of
+-- one type for all entries, only. All failures of this function indicate
+-- internal inconsistencies. Ideally the errors should not be passed to the user.
+--
+-- @param dt A digest table with at least one digest entry.
+-- @return True on success, false on error.
+-- @return An error object describing the issue on failure.
+function digest.sanity_check(dt)
+    assert(type(dt) == "table")
+
+    if digest.count(dt) == 0 then
+        return false, err.new("digest table is empty")
+    end
+
+    local digest_type
+    for pos, entry in ipairs(dt) do
+        if not digest_type then
+            digest_type = entry.digest
+        end
+
+        if entry.digest ~= digest.MD5 and entry.digest ~= digest.SHA1 then
+            return false,
+                err.new("digest entry %d has unknown digest type", pos)
+        end
+
+        if digest_type ~= entry.digest then
+            return false, err.new("digest table may not contain a mixture of" ..
+                " digest types")
+        end
+
+        local len = string.len(entry.checksum)
+        if len ~= digest.MD5_LEN and len ~= digest.SHA1_LEN then
+            return false, err.new("digest entry %d has unknown checksum "..
+                "length (%d)", pos, len)
+        end
+
+        if string.len(entry.name) == 0 then
+            return false, err.new("digest entry %d has no name", pos)
+        end
+    end
+
+    return true
+end
+
+--- Write out a digest file.
+-- The specified file will be overwritten. In case of an error, it is the
+-- callers responsibility to delete the potentially incomplete output file.
+--
+-- @param dt A digest table with digest table entries.
+-- @param filename Filename to write digest entries into. The file will be
+-- created or overwritten in case it already exists.
+function digest.write(dt, filename)
+    assert(type(dt) == "table")
+    assert(type(filename) == "string")
+
+    local e = err.new("error writing message digest file '%s'", filename)
+
+    local rc, re = digest.sanity_check(dt)
+    if not rc then
+        return false, e:cat(re)
+    end
+
+    local fd, re = io.open(filename, "w")
+    if not fd then
+        return false, e:append("%s", re)
+    end
+
+    local line = ""
+    for pos, entry in ipairs(dt) do
+        line = string.format("%s  %s\n", entry.checksum, entry.name)
+        rc, re = fd:write(line)
+        if not rc then
+            fd:close()
+            return false, e:append("%s", re)
+        end
+    end
+
+    rc, re = fd:close()
+    if not rc then
+        return false, e:append("%s", re)
+    end
+
+    return true
+end
+
+return strict.lock(digest)
+
+-- vim:sw=4:sts=4:et: