sbin/md5: improve compatibility with coreutils -c option

The -c option expects a digest file in either BSD or coreutils format.

The output for matched and mismatched files is identical to that
of the coreutils version.

The review of these changes included test cases that have already
been committed for the functionality that existed before.
Another test script is added to cover the coreutils compatible
extension implemented by this patch.

This commit contains a tests/Makefile that has been cleaned up
compared to the review version, using an implicit rule to apply the
TESTBASE path at build time (and the scripts have been renamed to
have an extension of .SH instead of .sh to trigger this rule).

Reviewed by:    imp
Differential Revision:  https://reviews.freebsd.org/D30812
This commit is contained in:
Stefan Eßer 2021-06-25 08:54:36 +02:00
parent 884fc5527a
commit c2870e576b
4 changed files with 209 additions and 34 deletions

View file

@ -1,5 +1,5 @@
.\" $FreeBSD$
.Dd June 19, 2021
.Dd June 24, 2021
.Dt MD5 1
.Os
.Sh NAME
@ -14,6 +14,7 @@
.Op Fl c Ar string
.Op Fl s Ar string
.Op Ar
.Pp
.Nm md5sum
.Op Fl pqrtx
.Op Fl c Ar file
@ -80,13 +81,28 @@ Ignored for compatibility with the coreutils
.Nm -sum
programs.
.It Fl c Ar string
Compare the digest of the file against this string.
If the program was called with a name that does not end in
.Nm sum ,
compare the digest of the file against this string.
.Pq Note that this option is not yet useful if multiple files are specified.
This option causes an error in for the
.Nm -sum
programs because it check the checksums listed in a file for the coreutils
.Nm -sum
programs that is not yet implemented.
.It Fl c Ar file
If the program was called with a name that does end in
.Nm sum ,
the file passed as argument must contain digest lines generated by the same digest algorithm
with or without the
.Fl r
option
.Pq i.e. in either classical BSD format or in GNU coreutils format .
A line with file name followed by
.Dq :
and either OK or FAILED is written for each well-formed line in the digest file.
If applicable, the number of failed comparisons and the number of lines that were
skipped since they were not well-formed are printed at the end.
The
.Fl q
option can be used to quiesce the output unless there are mismatched entries in
the digest.
.Pp
.It Fl s Ar string
Print a checksum of the given
.Ar string .
@ -141,8 +157,8 @@ $ echo -n Hello | md5
Calculate the checksum of multiple files reversing the output:
.Bd -literal -offset indent
$ md5 -r /boot/loader.conf /etc/rc.conf
ada5f60f23af88ff95b8091d6d67bef6 /boot/loader.conf
d80bf36c332dc0fdc479366ec3fa44cd /etc/rc.conf
ada5f60f23af88ff95b8091d6d67bef6 /boot/loader.conf
d80bf36c332dc0fdc479366ec3fa44cd /etc/rc.conf
.Ed
.Pp
Write the digest for
@ -165,6 +181,27 @@ which results in a failure.
$ md5 -c randomstring /boot/loader.conf
MD5 (/boot/loader.conf) = ada5f60f23af88ff95b8091d6d67bef6 [ Failed ]
.Ed
.Pp
If invoked with a name ending in
.Nm -sum
the
.Fl c
option does not compare against a hash string passed as parameter.
Instead, it expects a digest file, as created under the name
.Pa digest
for
.Pa /boot/loader.conf
in the example above.
.Bd -literal -offset indent
$ md5 -c digest /boot/loader.conf
/boot/loader.conf: OK
.Ed
.Pp
The digest file may contain any number of lines in the format generated with or without the
.Fl r
option
.Pq i.e. in either classical BSD format or in GNU coreutils format .
If a hash value does not match the file, FAILED is printed instead of OK.
.Sh SEE ALSO
.Xr cksum 1 ,
.Xr md5 3 ,
@ -203,9 +240,7 @@ The RIPEMD-160 page:
All of the utilities that end in
.Sq sum
are intended to be compatible with the GNU coreutils programs.
However, the long arguments and the
.Fl -check
functionality are not provided.
However, the long option functionality is not provided.
.Sh ACKNOWLEDGMENTS
This program is placed in the public domain for free general use by
RSA Data Security.

View file

@ -53,6 +53,7 @@ __FBSDID("$FreeBSD$");
#define TEST_BLOCK_COUNT 100000
#define MDTESTCOUNT 8
static int cflag;
static int pflag;
static int qflag;
static int rflag;
@ -152,12 +153,93 @@ static const struct Algorithm_t Algorithm[] = {
(DIGEST_End*)&SKEIN1024_End, &SKEIN1024_Data, &SKEIN1024_Fd }
};
static unsigned digest;
static unsigned malformed;
static bool gnu_emu = false;
static void
MD5_Update(MD5_CTX *c, const unsigned char *data, size_t len)
{
MD5Update(c, data, len);
}
struct chksumrec {
char *filename;
char *chksum;
struct chksumrec *next;
};
static struct chksumrec *head = NULL;
static struct chksumrec **next = &head;
#define PADDING 7 /* extra padding for "SHA512t256 (...) = ...\n" style */
#define CHKFILELINELEN (HEX_DIGEST_LENGTH + MAXPATHLEN + PADDING)
static int gnu_check(const char *checksumsfile)
{
FILE *inp;
char linebuf[CHKFILELINELEN];
int linelen;
int lineno;
char *filename;
char *hashstr;
struct chksumrec *rec;
const char *digestname;
int digestnamelen;
int hashstrlen;
if ((inp = fopen(checksumsfile, "r")) == NULL)
err(1, "%s", checksumsfile);
digestname = Algorithm[digest].name;
digestnamelen = strlen(digestname);
hashstrlen = strlen(*(Algorithm[digest].TestOutput[0]));
lineno = 1;
while (fgets(linebuf, sizeof(linebuf), inp) != NULL) {
linelen = strlen(linebuf) - 1;
if (linelen <= 0)
break;
if (linebuf[linelen] != '\n')
errx(1, "malformed input line %d (len=%d)", lineno, linelen);
linebuf[linelen] = '\0';
filename = linebuf + digestnamelen + 2;
hashstr = linebuf + linelen - hashstrlen;
/*
* supported formats:
* BSD: <DigestName> (<Filename>): <Digest>
* GNU: <Digest> [ *]<Filename>
*/
if (linelen >= digestnamelen + hashstrlen + 6 &&
strncmp(linebuf, digestname, digestnamelen) == 0 &&
strncmp(filename - 2, " (", 2) == 0 &&
strncmp(hashstr - 4, ") = ", 4) == 0) {
*(hashstr - 4) = '\0';
} else if (linelen >= hashstrlen + 3 &&
linebuf[hashstrlen] == ' ') {
linebuf[hashstrlen] = '\0';
hashstr = linebuf;
filename = linebuf + hashstrlen + 1;
if (*filename == ' ' || *filename == '*')
filename++;
} else {
malformed++;
continue;
}
rec = malloc(sizeof (*rec));
if (rec == NULL)
errx(1, "malloc failed");
rec->chksum = strdup(hashstr);
rec->filename = strdup(filename);
if (rec->chksum == NULL || rec->filename == NULL)
errx(1, "malloc failed");
rec->next = NULL;
*next = rec;
next = &rec->next;
lineno++;
}
fclose(inp);
return (lineno - 1);
}
/* Main driver.
Arguments (may be any combination):
@ -177,9 +259,9 @@ main(int argc, char *argv[])
char *p, *string;
char buf[HEX_DIGEST_LENGTH];
size_t len;
unsigned digest;
char *progname;
bool gnu_emu = false;
struct chksumrec *rec;
int numrecs;
if ((progname = strrchr(argv[0], '/')) == NULL)
progname = argv[0];
@ -199,13 +281,13 @@ main(int argc, char *argv[])
*/
len = strlen(progname);
if (len > 3 && strcmp(progname + len - 3, "sum") == 0) {
progname[len - 3] = '\0';
len -= 3;
rflag = 1;
gnu_emu = true;
}
for (digest = 0; digest < sizeof(Algorithm)/sizeof(*Algorithm); digest++)
if (strcasecmp(Algorithm[digest].progname, progname) == 0)
if (strncasecmp(Algorithm[digest].progname, progname, len) == 0)
break;
if (digest == sizeof(Algorithm)/sizeof(*Algorithm))
@ -220,9 +302,11 @@ main(int argc, char *argv[])
case 'b':
break;
case 'c':
cflag = 1;
if (gnu_emu)
errx(1, "-c check option not supported");
checkAgainst = optarg;
numrecs = gnu_check(optarg);
else
checkAgainst = optarg;
break;
case 'p':
pflag = 1;
@ -258,6 +342,20 @@ main(int argc, char *argv[])
err(1, "unable to limit rights for stdio");
#endif
if (cflag && gnu_emu) {
/*
* Replace argv by an array of filenames from the digest file
*/
argc = 0;
argv = (char**)calloc(sizeof(char *), numrecs + 1);
for (rec = head; rec != NULL; rec = rec->next) {
argv[argc] = rec->filename;
argc++;
}
argv[argc] = NULL;
rec = head;
}
if (*argv) {
do {
if ((fd = open(*argv, O_RDONLY)) < 0) {
@ -279,11 +377,15 @@ main(int argc, char *argv[])
err(1, "capsicum");
#endif
}
if (cflag && gnu_emu) {
checkAgainst = rec->chksum;
rec = rec->next;
}
p = Algorithm[digest].Fd(fd, buf);
(void)close(fd);
MDOutput(&Algorithm[digest], p, argv);
} while (*++argv);
} else if (!sflag && !skip) {
} else if (!cflag && !sflag && !skip) {
#ifdef HAVE_CAPSICUM
if (caph_limit_stdin() < 0 || caph_enter() < 0)
err(1, "capsicum");
@ -295,7 +397,12 @@ main(int argc, char *argv[])
p = Algorithm[digest].Data(string, len, buf);
MDOutput(&Algorithm[digest], p, &string);
}
if (gnu_emu) {
if (malformed > 0)
warnx("WARNING: %d lines are improperly formatted", malformed);
if (checksFailed > 0)
warnx("WARNING: %d computed checksums did NOT match", checksFailed);
}
if (failed != 0)
return (1);
if (checksFailed != 0)
@ -310,6 +417,8 @@ main(int argc, char *argv[])
static void
MDOutput(const Algorithm_t *alg, char *p, char *argv[])
{
bool checkfailed = false;
if (p == NULL) {
warn("%s", *argv);
failed++;
@ -318,21 +427,27 @@ MDOutput(const Algorithm_t *alg, char *p, char *argv[])
* If argv is NULL we are reading from stdin, where the output
* format has always been just the hash.
*/
if (qflag || argv == NULL)
printf("%s", p);
else if (rflag)
printf("%s %s", p, *argv);
else
printf("%s (%s) = %s",
alg->name, *argv, p);
if (checkAgainst && strcasecmp(checkAgainst, p) != 0)
{
checksFailed++;
if (!qflag)
printf(" [ Failed ]");
if (cflag && gnu_emu) {
checkfailed = strcasecmp(checkAgainst, p) != 0;
if (!qflag || checkfailed)
printf("%s: %s\n", *argv, checkfailed ? "FAILED" : "OK");
} else if (qflag || argv == NULL) {
printf("%s\n", p);
} else {
if (rflag)
printf("%s %s", p, *argv);
else
printf("%s (%s) = %s", alg->name, *argv, p);
if (checkAgainst) {
checkfailed = strcasecmp(checkAgainst, p) != 0;
if (!qflag && checkfailed)
printf(" [ Failed ]");
}
printf("\n");
}
printf("\n");
}
if (checkfailed)
checksFailed++;
}
/*
@ -559,6 +674,9 @@ static void
usage(const Algorithm_t *alg)
{
fprintf(stderr, "usage: %s [-pqrtx] [-c string] [-s string] [files ...]\n", alg->progname);
if (gnu_emu)
fprintf(stderr, "usage: %ssum [-pqrtx] [-c file] [-s string] [files ...]\n", alg->progname);
else
fprintf(stderr, "usage: %s [-pqrtx] [-c string] [-s string] [files ...]\n", alg->progname);
exit(1);
}

View file

@ -32,6 +32,7 @@ PLAIN_TESTS_SH+= self-test
PLAIN_TESTS_SH+= bsd-c-test
PLAIN_TESTS_SH+= bsd-p-test
PLAIN_TESTS_SH+= bsd-s-test
PLAIN_TESTS_SH+= coreutils-c-test
.SUFFIXES: .SH

View file

@ -0,0 +1,21 @@
#!/bin/sh
/bin/cp %%TESTSBASE%%/sbin/md5/*.inp . || exit 127
exitcode=0
testloop () {
opt=$1
while read algorithm; do
${algorithm}sum -c %%TESTSBASE%%/sbin/md5/${algorithm}.digest || exitcode=1
${algorithm}sum -c %%TESTSBASE%%/sbin/md5/${algorithm}sum.digest || exitcode=1
done < %%TESTSBASE%%/sbin/md5/algorithms.txt
}
testloop ""
testloop -q
testloop -r
testloop -qr
exit $exitcode