From f918d4df86d7516f0f3552bed2ad1c741e644721 Mon Sep 17 00:00:00 2001 From: Tobias Ulmer Date: Thu, 13 Dec 2012 19:26:11 +0100 Subject: [PATCH] Add digest, a message digest handling module. 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 --- local/digest.lua | 363 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 local/digest.lua diff --git a/local/digest.lua b/local/digest.lua new file mode 100644 index 0000000..be8f8e6 --- /dev/null +++ b/local/digest.lua @@ -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 , 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 . +]] + +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: -- 2.39.5