Radish alpha
H
rad:z3QDZAW2FAfuLvihrhiyDC9fAD8G9
HardenedBSD Package Manager
Radicle
Git
check: implement verification of file and dir metadata
strssndktn committed 6 months ago
commit 48e6c122d3f5fa6d9ff6526dc799e121e8644ddb
parent 087ece7
10 files changed +567 -20
modified docs/pkg-check.8
@@ -23,24 +23,24 @@
.Sh SYNOPSIS
.Nm
.Sm off
-
.Fl d | Fl s
+
.Fl d | Fl s | Fl m
.Sm on
.Op Fl nqvy
.Fl a
.Nm
.Sm off
-
.Fl d | Fl s
+
.Fl d | Fl s | Fl m
.Sm on
.Op Fl nqvy
.Op Fl Cgix
.Ar pattern
.Pp
.Nm
-
.Fl -{dependencies,checksums}
+
.Fl -{dependencies,checksums,metadata}
.Op Fl -{dry-run,quiet,verbose,yes}
.Fl -all
.Nm
-
.Fl -{dependencies,checksums}
+
.Fl -{dependencies,checksums,metadata}
.Op Fl -{dry-run,quiet,verbose,yes}
.Op Fl -{case-sensitive,glob,case-insensitive,regex}
.Ar pattern
@@ -53,6 +53,13 @@ or
checks for and installs missing dependencies.
.Pp
.Nm
+
.Fl m
+
or
+
.Nm
+
.Fl -metadata
+
checks for metadata discrepancies between the recorded state in pkgdb and the filesystem.
+
.Pp
+
.Nm
.Fl s
or
.Nm
@@ -85,6 +92,8 @@ This is the default unless
.Ev CASE_SENSITIVE_MATCH
has been set to true in
.Pa pkg.conf .
+
.It Fl m , Fl -metadata
+
Checks for metadata discrepancies between the recorded state in pkgdb and the filesystem.
.It Fl n , Fl -dry-run
Only check for missing dependencies, do not install them.
.It Fl s , Fl -checksums
modified libpkg/libpkg.ver
@@ -18,6 +18,7 @@ global:
	pkg_audit_new;
	pkg_audit_process;
	pkg_cache_full_clean;
+
	pkg_check_files;
	pkg_close_devnull;
	pkg_compiled_for_same_os_major;
	pkg_config_dump;
@@ -108,6 +109,7 @@ global:
	pkg_licenses;
	pkg_list_count;
	pkg_load_metadata;
+
	pkg_meta_attribute_tostring;
	pkg_mkdirs;
	pkg_namecmp;
	pkg_new;
modified libpkg/pkg.c
@@ -16,6 +16,8 @@
#include <errno.h>
#include <fcntl.h>
#include <string.h>
+
#include <pwd.h>
+
#include <grp.h>

#include "pkg.h"
#include "private/event.h"
@@ -1356,35 +1358,328 @@ pkg_validate(struct pkg *pkg, struct pkgdb *db)
	return (EPKG_OK);
}

+
enum {
+
	FILE_OK = 0,
+

+
	FILE_MISSING = 1 << 0,
+
	FILE_SUM_MISMATCH = 1 << 1,
+

+
	FILE_META_MISMATCH_TYPE = 1 << 2,
+
	FILE_META_MISMATCH_UNAME = 1 << 3,
+
	FILE_META_MISMATCH_GNAME = 1 << 4,
+
	FILE_META_MISMATCH_MODE = 1 << 5,
+
	FILE_META_MISMATCH_FFLAGS = 1 << 6,
+
	FILE_META_MISMATCH_MTIME = 1 << 7,
+
	FILE_META_MISMATCH_SYMLINK = 1 << 8,
+
};
+

+
static int
+
pkg_stat(const char *path, struct stat *st, char *symlink_target,
+
	 size_t symlink_target_size, unsigned *file_status)
+
{
+
	ssize_t linklen;
+

+
	if (fstatat(ctx.rootfd, RELATIVE_PATH(path), st,
+
		    AT_SYMLINK_NOFOLLOW) == -1) {
+

+
		if (errno == ENOENT)
+
			*file_status |= FILE_MISSING;
+
		else
+
			pkg_emit_errno("fstatat", RELATIVE_PATH(path));
+
		return (EPKG_FATAL);
+
	}
+

+
	symlink_target[0] = '\0';
+
	if (S_ISLNK(st->st_mode)) {
+
		linklen = readlinkat(ctx.rootfd, RELATIVE_PATH(path),
+
				     symlink_target, symlink_target_size - 1);
+
		if (linklen == -1) {
+
			symlink_target[0] = '\0';
+
			pkg_emit_errno("readlinkat", RELATIVE_PATH(path));
+
			return (EPKG_FATAL);
+
		} else {
+
			symlink_target[linklen] = '\0';
+
		}
+
	}
+

+
	return (EPKG_OK);
+
}
+

+
static unsigned
+
pkg_check_meta(struct stat *st, const char *uname, const char *gname,
+
	       mode_t perm, u_long fflags, time_t mtime_sec,
+
	       char *db_symlink_target, char *fs_symlink_target)
+
{
+
	unsigned file_status = FILE_OK;
+
	uid_t fs_uid;
+
	gid_t fs_gid;
+

+
	fs_uid = get_uid_from_uname(uname);
+
	if (fs_uid != st->st_uid)
+
		file_status |= FILE_META_MISMATCH_UNAME;
+
	fs_gid = get_gid_from_gname(gname);
+
	if (fs_gid != st->st_gid)
+
		file_status |= FILE_META_MISMATCH_GNAME;
+
	if (perm != (st->st_mode & ~S_IFMT))
+
		file_status |= FILE_META_MISMATCH_MODE;
+
#if defined(HAVE_STRUCT_STAT_ST_FLAGS) && defined(HAVE_FFLAGSTOSTR)
+
	if (fflags != st->st_flags)
+
		file_status |= FILE_META_MISMATCH_FFLAGS;
+
#endif
+
	/* we don't check mtime for directories */
+
	if (!S_ISDIR(st->st_mode)) {
+
		if (mtime_sec != st->st_mtim.tv_sec)
+
			file_status |= FILE_META_MISMATCH_MTIME;
+
	}
+

+
	if (S_ISLNK(st->st_mode) != (fs_symlink_target[0] != '\0'))
+
		file_status |= FILE_META_MISMATCH_SYMLINK;
+
	else if (S_ISLNK(st->st_mode) && strcmp(db_symlink_target, fs_symlink_target) != 0)
+
		file_status |= FILE_META_MISMATCH_SYMLINK;
+

+
	return file_status;
+
}
+

+
static const char *
+
stat_type_tostring(mode_t mode) {
+
	/* adapted from freebsd-src/usr.bin/stat.c */
+
	switch (mode & S_IFMT) {
+
	case S_IFIFO: return "Fifo file";
+
	case S_IFCHR: return "Character Device";
+
	case S_IFDIR: return "Directory";
+
	case S_IFBLK: return "Block Device";
+
	case S_IFREG: return "Regular File";
+
	case S_IFLNK: return "Symbolic Link";
+
	case S_IFSOCK: return "Socket";
+
#ifdef S_IFWHT
+
	case S_IFWHT: return "Whiteout File";
+
#endif
+
	default: return "???";
+
	}
+
}
+

+
static int
+
pkg_check_file(struct pkg *pkg, struct pkg_file *f, bool metadata, unsigned file_status)
+
{
+
	int rc = EPKG_OK;
+
	int ret;
+
	struct stat st;
+
	char symlink_target[MAXPATHLEN];
+
	struct passwd *pwd;
+
	struct group *grp;
+
	char db_str[1024], fs_str[1024];
+
	char *db_fflags, *fs_fflags;
+
	time_t tmp_time;
+
	struct tm *tm;
+

+
	ret = pkg_stat(f->path, &st, symlink_target, sizeof(symlink_target), &file_status);
+
	if (ret != EPKG_OK) {
+
		rc = EPKG_FATAL;
+
		goto emit_status;
+
	}
+

+
	if (metadata) {
+
		file_status |= pkg_check_meta(&st, f->uname, f->gname, f->perm, f->fflags,
+
					      f->time[1].tv_sec, f->symlink_target,
+
					      symlink_target);
+

+
		if (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode))
+
			file_status |= FILE_META_MISMATCH_TYPE;
+
		if (S_ISREG(st.st_mode) && (symlink_target[0] != '\0'))
+
			file_status |= FILE_META_MISMATCH_TYPE;
+
		if (S_ISLNK(st.st_mode) && (symlink_target[0] == '\0'))
+
			file_status |= FILE_META_MISMATCH_TYPE;
+
	}
+

+

+
emit_status:
+
	if (file_status & FILE_MISSING)
+
		pkg_emit_file_missing(pkg, f);
+
	if (file_status & FILE_SUM_MISMATCH)
+
		pkg_emit_file_mismatch(pkg, f, f->sum);
+

+
	if (file_status & FILE_META_MISMATCH_TYPE) {
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_TYPE,
+
					    f->symlink_target[0] == '\0' ? "Regular File" : "Symbolic Link",
+
					    stat_type_tostring(st.st_mode));
+
	}
+

+
	if (file_status & FILE_META_MISMATCH_UNAME) {
+
		pwd = getpwuid(st.st_uid);
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_UNAME,
+
					    f->uname, pwd != NULL ? pwd->pw_name : NULL);
+
	}
+
	if (file_status & FILE_META_MISMATCH_GNAME) {
+
		grp = getgrgid(st.st_gid);
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_GNAME,
+
					    f->gname, grp != NULL ? grp->gr_name : NULL);
+
	}
+
	if (file_status & FILE_META_MISMATCH_MODE) {
+
		strmode(f->perm, db_str);
+
		strmode(st.st_mode, fs_str);
+
		db_str[10] = '\0'; /* trim last space character */
+
		fs_str[10] = '\0';
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_PERM,
+
					    db_str + 1, fs_str + 1);
+
	}
+

+
#if defined(HAVE_STRUCT_STAT_ST_FLAGS) && defined(HAVE_FFLAGSTOSTR)
+
	if (file_status & FILE_META_MISMATCH_FFLAGS) {
+
		db_fflags = fflagstostr(f->fflags);
+
		fs_fflags = fflagstostr(st.st_flags);
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_FFLAGS,
+
					    db_fflags, fs_fflags);
+
		free(db_fflags);
+
		free(fs_fflags);
+
	}
+
#endif
+
	if (file_status & FILE_META_MISMATCH_MTIME) {
+
		tmp_time = f->time[1].tv_sec;
+
		tm = localtime(&tmp_time);
+
		strftime(db_str, sizeof(db_str), "%c", tm);
+
		tmp_time = st.st_mtim.tv_sec;
+
		tm = localtime(&tmp_time);
+
		strftime(fs_str, sizeof(fs_str), "%c", tm);
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_MTIME,
+
					    db_str, fs_str);
+
	}
+
	if (file_status & FILE_META_MISMATCH_SYMLINK) {
+
		pkg_emit_file_meta_mismatch(pkg, f, PKG_META_ATTR_SYMLINK,
+
					    f->symlink_target, symlink_target);
+
	}
+

+
	if (file_status == FILE_OK)
+
		pkg_emit_file_meta_ok(pkg, f);
+
	else
+
		rc = EPKG_FATAL;
+

+
	return rc;
+
}
+

+

+
static int
+
pkg_check_dir(struct pkg *pkg, struct pkg_dir *d, unsigned file_status)
+
{
+
	int rc = EPKG_OK;
+
	int err;
+
	struct stat st;
+
	char *db_symlink_target = "";
+
	char symlink_target[MAXPATHLEN];
+
	char db_str[1024], fs_str[1024];
+
	char *db_fflags, *fs_fflags;
+
	struct passwd *pwd;
+
	struct group *grp;
+

+
	err = pkg_stat(d->path, &st, symlink_target, sizeof(symlink_target), &file_status);
+
	if (err != EPKG_OK) {
+
		rc = EPKG_FATAL;
+
		goto emit_status;
+
	}
+

+
	file_status |= pkg_check_meta(&st, d->uname, d->gname, d->perm, d->fflags,
+
				      d->time[1].tv_sec, db_symlink_target,
+
				      symlink_target);
+

+
	if (!S_ISDIR(st.st_mode))
+
		file_status |= FILE_META_MISMATCH_TYPE;
+

+
emit_status:
+
	if (file_status & FILE_MISSING)
+
		pkg_emit_dir_missing(pkg, d);
+

+
	if (file_status & FILE_META_MISMATCH_TYPE) {
+
		pkg_emit_dir_meta_mismatch(pkg, d, PKG_META_ATTR_TYPE,
+
					   "Directory", stat_type_tostring(st.st_mode));
+
	}
+

+
	if (file_status & FILE_META_MISMATCH_UNAME) {
+
		pwd = getpwuid(st.st_uid);
+
		pkg_emit_dir_meta_mismatch(pkg, d, PKG_META_ATTR_UNAME,
+
					   d->uname, pwd != NULL ? pwd->pw_name : NULL);
+
	}
+
	if (file_status & FILE_META_MISMATCH_GNAME) {
+
		grp = getgrgid(st.st_gid);
+
		pkg_emit_dir_meta_mismatch(pkg, d, PKG_META_ATTR_GNAME,
+
					   d->gname, grp != NULL ? grp->gr_name : NULL);
+
	}
+
	if (file_status & FILE_META_MISMATCH_MODE) {
+
		strmode(d->perm, db_str);
+
		strmode(st.st_mode, fs_str);
+
		db_str[10] = '\0'; /* trim last space character */
+
		db_str[10] = '\0';
+
		pkg_emit_dir_meta_mismatch(pkg, d, PKG_META_ATTR_PERM,
+
					    db_str + 1, fs_str + 1);
+
	}
+
#if defined(HAVE_STRUCT_STAT_ST_FLAGS) && defined(HAVE_FFLAGSTOSTR)
+
	if (file_status & FILE_META_MISMATCH_FFLAGS) {
+
		db_fflags = fflagstostr(d->fflags);
+
		fs_fflags = fflagstostr(st.st_flags);
+
		pkg_emit_dir_meta_mismatch(pkg, d, PKG_META_ATTR_FFLAGS,
+
					    db_fflags, fs_fflags);
+
		free(db_fflags);
+
		free(fs_fflags);
+
	}
+
#endif
+
	if (file_status == FILE_OK)
+
		pkg_emit_dir_meta_ok(pkg, d);
+
	else
+
		rc = EPKG_FATAL;
+

+
	return (rc);
+
}
+

int
-
pkg_test_filesum(struct pkg *pkg)
+
pkg_check_files(struct pkg *pkg, bool checksum, bool metadata)
{
	struct pkg_file *f = NULL;
+
	struct pkg_dir *d = NULL;
	int rc = EPKG_OK;
	int ret;
+
	unsigned file_status;

	assert(pkg != NULL);

	while (pkg_files(pkg, &f) == EPKG_OK) {
-
		if (f->sum != NULL &&
+
		file_status = FILE_OK;
+

+
		if (checksum && f->sum != NULL &&
		    /* skip config files as they can be modified */
		    pkghash_get_value(pkg->config_files_hash, f->path) == NULL) {
			ret = pkg_checksum_validate_fileat(ctx.rootfd,
			    RELATIVE_PATH(f->path), f->sum);
			if (ret != 0) {
				if (ret == ENOENT)
-
					pkg_emit_file_missing(pkg, f);
+
					file_status |= FILE_MISSING;
				else
-
					pkg_emit_file_mismatch(pkg, f, f->sum);
+
					file_status |= FILE_SUM_MISMATCH;
				rc = EPKG_FATAL;
			}
		}
+

+
		ret = pkg_check_file(pkg, f, metadata, file_status);
+
		if (ret != EPKG_OK)
+
			rc = EPKG_FATAL;
+
	}
+

+
	if (metadata) {
+
		while (pkg_dirs(pkg, &d) == EPKG_OK) {
+
			file_status = FILE_OK;
+
			ret = pkg_check_dir(pkg, d, file_status);
+
			if (ret != EPKG_OK)
+
				rc = EPKG_FATAL;
+
		}
	}

	return (rc);
}

int
+
pkg_test_filesum(struct pkg *pkg)
+
{
+
	return pkg_check_files(pkg, true, false);
+
}
+

+
int
pkg_try_installed(struct pkgdb *db, const char *name,
		struct pkg **pkg, unsigned flags) {
	struct pkgdb_it *it = NULL;
modified libpkg/pkg.h.in
@@ -1308,6 +1308,8 @@ typedef enum {
	PKG_EVENT_PROGRESS_TICK,
	PKG_EVENT_BACKUP,
	PKG_EVENT_RESTORE,
+
	PKG_EVENT_FILE_META_OK, // TODO: do we need same event for sum ok?
+
	PKG_EVENT_DIR_META_OK,
	/* errors */
	PKG_EVENT_ERROR,
	PKG_EVENT_ERRNO,
@@ -1328,16 +1330,29 @@ typedef enum {
	PKG_EVENT_NOT_FOUND,
	PKG_EVENT_NEW_ACTION,
	PKG_EVENT_MESSAGE,
-
	PKG_EVENT_FILE_MISSING,
+
	PKG_EVENT_DIR_MISSING,
+
	PKG_EVENT_FILE_MISSING, // TODO: add formatter to pipeevent
	PKG_EVENT_CLEANUP_CALLBACK_REGISTER,
	PKG_EVENT_CLEANUP_CALLBACK_UNREGISTER,
	PKG_EVENT_CONFLICTS,
	PKG_EVENT_TRIGGERS_BEGIN,
	PKG_EVENT_TRIGGER,
	PKG_EVENT_TRIGGERS_FINISHED,
-
	PKG_EVENT_PKG_ERRNO
+
	PKG_EVENT_PKG_ERRNO,
+
	PKG_EVENT_FILE_META_MISMATCH,
+
	PKG_EVENT_DIR_META_MISMATCH,
} pkg_event_t;

+
enum pkg_meta_attribute {
+
	PKG_META_ATTR_TYPE,
+
	PKG_META_ATTR_UNAME,
+
	PKG_META_ATTR_GNAME,
+
	PKG_META_ATTR_PERM,
+
	PKG_META_ATTR_FFLAGS,
+
	PKG_META_ATTR_MTIME,
+
	PKG_META_ATTR_SYMLINK,
+
};
+

struct pkg_event {
	pkg_event_t type;
	union {
@@ -1490,6 +1505,10 @@ struct pkg_event {
		} e_pkg_message;
		struct {
			struct pkg *pkg;
+
			struct pkg_dir *dir;
+
		} e_dir_missing;
+
		struct {
+
			struct pkg *pkg;
			struct pkg_file *file;
		} e_file_missing;
		struct {
@@ -1509,6 +1528,28 @@ struct pkg_event {
			size_t total;
			size_t current;
		} e_action;
+
		struct {
+
			struct pkg *pkg;
+
			struct pkg_file *file;
+
			enum pkg_meta_attribute attrib;
+
			const char *db_val;
+
			const char *fs_val;
+
		} e_file_meta_mismatch;
+
		struct {
+
			struct pkg *pkg;
+
			struct pkg_dir *dir;
+
			enum pkg_meta_attribute attrib;
+
			const char *db_val;
+
			const char *fs_val;
+
		} e_dir_meta_mismatch;
+
		struct {
+
			struct pkg *pkg;
+
			struct pkg_file *file;
+
		} e_file_ok;
+
		struct {
+
			struct pkg *pkg;
+
			struct pkg_dir *dir;
+
		} e_dir_ok;
	};
};

@@ -1519,6 +1560,7 @@ struct pkg_event {
typedef int(*pkg_event_cb)(void *, struct pkg_event *);

void pkg_event_register(pkg_event_cb cb, void *data);
+
const char *pkg_meta_attribute_tostring(enum pkg_meta_attribute);

bool pkg_compiled_for_same_os_major(void);
int pkg_ini(const char *, const char *, pkg_init_flags);
@@ -1527,6 +1569,7 @@ const char *pkg_libversion(void);
int pkg_initialized(void);
void pkg_shutdown(void);

+
int pkg_check_files(struct pkg *, bool checksums, bool metadata);
int pkg_test_filesum(struct pkg *);

void pkgdb_cmd(int argc, char **argv);
modified libpkg/pkg_event.c
@@ -830,6 +830,76 @@ pkg_emit_file_missing(struct pkg *pkg, struct pkg_file *f)
}

void
+
pkg_emit_dir_missing(struct pkg *pkg, struct pkg_dir *d)
+
{
+
	struct pkg_event ev;
+
	ev.type = PKG_EVENT_DIR_MISSING;
+

+
	ev.e_dir_missing.pkg = pkg;
+
	ev.e_dir_missing.dir = d;
+

+
	pkg_emit_event(&ev);
+
}
+

+
void
+
pkg_emit_file_meta_mismatch(struct pkg *pkg, struct pkg_file *file,
+
			    enum pkg_meta_attribute attrib,
+
			    const char *db_val, const char *fs_val)
+
{
+
	struct pkg_event ev;
+
	ev.type = PKG_EVENT_FILE_META_MISMATCH;
+

+
	ev.e_file_meta_mismatch.pkg = pkg;
+
	ev.e_file_meta_mismatch.file = file;
+
	ev.e_file_meta_mismatch.attrib = attrib;
+
	ev.e_file_meta_mismatch.db_val = db_val;
+
	ev.e_file_meta_mismatch.fs_val = fs_val;
+

+
	pkg_emit_event(&ev);
+
}
+

+
void
+
pkg_emit_dir_meta_mismatch(struct pkg *pkg, struct pkg_dir *dir,
+
			   enum pkg_meta_attribute attrib,
+
			   const char *db_val, const char *fs_val)
+
{
+
	struct pkg_event ev;
+
	ev.type = PKG_EVENT_DIR_META_MISMATCH;
+

+
	ev.e_dir_meta_mismatch.pkg = pkg;
+
	ev.e_dir_meta_mismatch.dir = dir;
+
	ev.e_dir_meta_mismatch.attrib = attrib;
+
	ev.e_dir_meta_mismatch.db_val = db_val;
+
	ev.e_dir_meta_mismatch.fs_val = fs_val;
+

+
	pkg_emit_event(&ev);
+
}
+

+
void
+
pkg_emit_file_meta_ok(struct pkg *pkg, struct pkg_file *file)
+
{
+
	struct pkg_event ev;
+
	ev.type = PKG_EVENT_FILE_META_OK;
+

+
	ev.e_file_ok.pkg = pkg;
+
	ev.e_file_ok.file = file;
+

+
	pkg_emit_event(&ev);
+
}
+

+
void
+
pkg_emit_dir_meta_ok(struct pkg *pkg, struct pkg_dir *dir)
+
{
+
	struct pkg_event ev;
+
	ev.type = PKG_EVENT_DIR_META_OK;
+

+
	ev.e_dir_ok.pkg = pkg;
+
	ev.e_dir_ok.dir = dir;
+

+
	pkg_emit_event(&ev);
+
}
+

+
void
pkg_plugin_errno(struct pkg_plugin *p, const char *func, const char *arg)
{
	struct pkg_event ev;
modified libpkg/private/event.h
@@ -136,6 +136,7 @@ void pkg_emit_delete_files_begin(struct pkg *p);
void pkg_emit_delete_files_finished(struct pkg *p);
void pkg_emit_new_action(size_t current, size_t total);
void pkg_emit_message(const char *msg);
+
void pkg_emit_dir_missing(struct pkg *p, struct pkg_dir *d);
void pkg_emit_file_missing(struct pkg *p, struct pkg_file *f);
void pkg_register_cleanup_callback(void (*cleanup_cb)(void *data), void *data);
void pkg_unregister_cleanup_callback(void (*cleanup_cb)(void *data), void *data);
@@ -143,5 +144,14 @@ void pkg_emit_conflicts(struct pkg *p1, struct pkg *p2, const char *path);
void pkg_emit_triggers_begin(void);
void pkg_emit_trigger(const char *name, bool cleanup);
void pkg_emit_triggers_finished(void);
+
void pkg_emit_file_meta_mismatch(struct pkg *pkg, struct pkg_file *file,
+
				 enum pkg_meta_attribute attrib,
+
				 const char *db_val, const char *fs_val);
+
void pkg_emit_dir_meta_mismatch(struct pkg *pkg, struct pkg_dir *dir,
+
				enum pkg_meta_attribute attrib,
+
				const char *db_val, const char *fs_val);
+

+
void pkg_emit_file_meta_ok(struct pkg *pkg, struct pkg_file *file);
+
void pkg_emit_dir_meta_ok(struct pkg *pkg, struct pkg_dir *file);

#endif
modified libpkg/utils.c
@@ -1184,3 +1184,19 @@ get_gid_from_gname(const char *gname)
out:
	return (grent.gr_gid);
}
+

+
const char *
+
pkg_meta_attribute_tostring(enum pkg_meta_attribute attrib)
+
{
+
	switch (attrib) {
+
	case PKG_META_ATTR_TYPE: return "type";
+
	case PKG_META_ATTR_UNAME: return "uname";
+
	case PKG_META_ATTR_GNAME: return "gname";
+
	case PKG_META_ATTR_PERM: return "perm";
+
	case PKG_META_ATTR_FFLAGS: return "fflags";
+
	case PKG_META_ATTR_MTIME: return "mtime";
+
	case PKG_META_ATTR_SYMLINK: return "symlink";
+
	}
+

+
	return "???";
+
}
modified src/check.c
@@ -239,6 +239,7 @@ exec_check(int argc, char **argv)
	int ch;
	bool dcheck = false;
	bool checksums = false;
+
	bool metadata = false;
	bool noinstall = false;
	int nbpkgs = 0;
	int i, processed, total = 0;
@@ -253,6 +254,7 @@ exec_check(int argc, char **argv)
		{ "dependencies",	no_argument,	NULL,	'd' },
		{ "glob",		no_argument,	NULL,	'g' },
		{ "case-insensitive",	no_argument,	NULL,	'i' },
+
		{ "metadata",		no_argument,	NULL,	'm' },
		{ "dry-run",		no_argument,	NULL,	'n' },
		{ "recompute",		no_argument,	NULL,	'r' },
		{ "checksums",		no_argument,	NULL,	's' },
@@ -265,7 +267,7 @@ exec_check(int argc, char **argv)

	processed = 0;

-
	while ((ch = getopt_long(argc, argv, "+aBCdginqrsvxy", longopts, NULL)) != -1) {
+
	while ((ch = getopt_long(argc, argv, "+aBCdgimnqrsvxy", longopts, NULL)) != -1) {
		switch (ch) {
		case 'a':
			match = MATCH_ALL;
@@ -286,6 +288,10 @@ exec_check(int argc, char **argv)
		case 'i':
			pkgdb_set_case_sensitivity(false);
			break;
+
		case 'm':
+
			metadata = true;
+
			flags |= PKG_LOAD_FILES|PKG_LOAD_DIRS;
+
			break;
		case 'n':
			noinstall = true;
			break;
@@ -316,14 +322,14 @@ exec_check(int argc, char **argv)
	argc -= optind;
	argv += optind;

-
	if (!(dcheck || checksums)) {
+
	if (!(dcheck || checksums || metadata)) {
		checksums = true;
		flags |= PKG_LOAD_FILES;
	}
	/* Default to all packages if no pkg provided */
-
	if (argc == 0 && (dcheck || checksums)) {
+
	if (argc == 0 && (dcheck || checksums || metadata)) {
		match = MATCH_ALL;
-
	} else if ((argc == 0 && match != MATCH_ALL) || !(dcheck || checksums)) {
+
	} else if ((argc == 0 && match != MATCH_ALL) || !(dcheck || checksums || metadata)) {
		usage_check();
		return (EXIT_FAILURE);
	}
@@ -403,10 +409,11 @@ exec_check(int argc, char **argv)
					rc = EXIT_FAILURE;
				}
			}
-
			if (checksums) {
+
			if (checksums || metadata) {
				if (!quiet && verbose)
-
					printf(" checksums...");
-
				if (pkg_test_filesum(pkg) != EPKG_OK) {
+
					printf("%s%s", checksums ? " checksums..." : "",
+
					       metadata ? " metadata...": "");
+
				if (pkg_check_files(pkg, checksums, metadata) != EPKG_OK) {
					rc = EXIT_FAILURE;
				}
			}
modified src/event.c
@@ -367,10 +367,20 @@ draw_progressbar(int64_t current, int64_t total)
		progressbar_stop();
}

+
static const char *
+
str_or_unknown(const char *str)
+
{
+
	if (str == NULL || str[0] == '\0')
+
		return "???";
+
	return str;
+
}
+

int
event_callback(void *data, struct pkg_event *ev)
{
	struct pkg *pkg = NULL, *pkg_new, *pkg_old;
+
	struct pkg_file *file;
+
	struct pkg_dir *dir;
	struct cleanup *evtmp;
	int *debug = data;
	struct pkg_event_conflict *cur_conflict;
@@ -613,6 +623,22 @@ event_callback(void *data, struct pkg_event *ev)
		pkg_fprintf(stderr, "%n-%v: missing file %Fn\n", pkg, pkg,
		    ev->e_file_missing.file);
		break;
+
	case PKG_EVENT_DIR_META_MISMATCH:
+
		pkg = ev->e_file_meta_mismatch.pkg;
+
		dir = ev->e_dir_meta_mismatch.dir;
+
		pkg_fprintf(stderr, "%n-%v: %Dn [%S] %S -> %S\n", pkg, pkg, dir,
+
			    pkg_meta_attribute_tostring(ev->e_dir_meta_mismatch.attrib),
+
			    str_or_unknown(ev->e_dir_meta_mismatch.db_val),
+
			    str_or_unknown(ev->e_dir_meta_mismatch.fs_val));
+
		break;
+
	case PKG_EVENT_FILE_META_MISMATCH:
+
		pkg = ev->e_file_meta_mismatch.pkg;
+
		file = ev->e_file_meta_mismatch.file;
+
		pkg_fprintf(stderr, "%n-%v: %Fn [%S] %S -> %S\n", pkg, pkg, file,
+
			    pkg_meta_attribute_tostring(ev->e_file_meta_mismatch.attrib),
+
			    str_or_unknown(ev->e_file_meta_mismatch.db_val),
+
			    str_or_unknown(ev->e_file_meta_mismatch.fs_val));
+
		break;
	case PKG_EVENT_PLUGIN_ERRNO:
		warnx("%s: %s(%s): %s",
		    pkg_plugin_get(ev->e_plugin_errno.plugin, PKG_PLUGIN_NAME),
modified tests/frontend/check.sh
@@ -3,14 +3,25 @@
. $(atf_get_srcdir)/test_environment.sh

tests_init \
-
	basic
+
	basic \
+
	dirs

basic_body() {
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1"

+
	user=$(id -un)
+
	if [ "${OS}" = "FreeBSD" ]; then
+
	    group=$(stat -f "%Sg" .)
+
	else
+
	    group=$(id -gn)
+
	fi
	cat <<__EOF__ >> test.ucl
files: {
-
    ${TMPDIR}/a: "",
+
    ${TMPDIR}/a: {
+
		 uname: "${user}"
+
		 gname: "${group}"
+
		 perm: 0644
+
    },
}
__EOF__

@@ -19,7 +30,65 @@ __EOF__
	atf_check mkdir -p target
	atf_check pkg -o REPOS_DIR=/dev/null -r target install -qfy ${TMPDIR}/test-1.pkg
	atf_check pkg -r target check -q
+
	atf_check -s exit:0 pkg -r target check -mq
	echo b > ${TMPDIR}/target/${TMPDIR}/a
+
	touch -r ./a ${TMPDIR}/target/${TMPDIR}/a
	atf_check -s not-exit:0 -e inline:"test-1: checksum mismatch for ${TMPDIR}/a\n" \
-
	    pkg -r target check -q
+
		  pkg -r target check -q
+
	atf_check -s exit:0 pkg -r target check -mq
+
	touch -t 197001010000.01 ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:1 -e match:"\[mtime\]" pkg -r target check -mq
+
	touch -r ./a ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:0 pkg -r target check -mq
+
	ln -sf b ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:1 -e match:"\[symlink\]" -e match:"\[perm\]" pkg -r target check -mq
+
	chmod -h 0644 ${TMPDIR}/target/${TMPDIR}/a
+
	touch -hr ./a ${TMPDIR}/target/${TMPDIR}/a
+
	# linux mangles perms on symlinks
+
	if [ "${OS}" = "FreeBSD" ]; then
+
	    atf_check -s exit:1 -e match:"\[symlink\]" -e not-match:"\[perm\]" -e not-match:"\[mtime\]" pkg -r target check -mq
+
	else
+
	    atf_check -s exit:1 -e match:"\[symlink\]" -e match:"\[perm\]" -e not-match:"\[mtime\]" pkg -r target check -mq
+
	fi
+
	rm ${TMPDIR}/target/${TMPDIR}/a
+
	mkdir ${TMPDIR}/target/${TMPDIR}/a
+
	touch -r a ${TMPDIR}/target/${TMPDIR}/a
+
	chmod 0644 ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:1 -e match:"\[type\]" pkg -r target check -mq
+
}
+

+
dirs_body() {
+
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1"
+

+
	user=$(id -un)
+
	if [ "${OS}" = "FreeBSD" ]; then
+
	    group=$(stat -f "%Sg" .)
+
	else
+
	    group=$(id -gn)
+
	fi
+
	cat <<__EOF__ >> test.ucl
+
directories: {
+
    ${TMPDIR}/a: {
+
		 uname: "${user}"
+
		 gname: "${group}"
+
		 perm: 0644
+
    },
+
}
+
__EOF__
+

+
	mkdir a
+
	atf_check pkg create -M test.ucl
+
	atf_check mkdir -p target
+
	atf_check pkg -o REPOS_DIR=/dev/null -r target install -qfy ${TMPDIR}/test-1.pkg
+
	atf_check pkg -r target check -mq
+
	# mtime is not checked for directories
+
	touch -t 197001010000.01 ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check pkg -r target check -mq
+
	chmod 0600 ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:1 -e match:"\[perm\]" pkg -r target check -mq
+
	chmod 0644 ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:0 pkg -r target check -mq
+
	rmdir ${TMPDIR}/target/${TMPDIR}/a
+
	touch ${TMPDIR}/target/${TMPDIR}/a
+
	atf_check -s exit:1 -e match:"\[type\]" pkg -r target check -mq
}