// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2018 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <darrick.wong@oracle.com>
 */
#include "xfs.h"
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/statvfs.h>
#include "list.h"
#include "path.h"
#include "xfs_scrub.h"
#include "common.h"
#include "progress.h"
#include "scrub.h"
#include "xfs_errortag.h"
#include "repair.h"

/* Online scrub and repair wrappers. */

/* Type info and names for the scrub types. */
enum scrub_type {
	ST_NONE,	/* disabled */
	ST_AGHEADER,	/* per-AG header */
	ST_PERAG,	/* per-AG metadata */
	ST_FS,		/* per-FS metadata */
	ST_INODE,	/* per-inode metadata */
};
struct scrub_descr {
	const char	*name;
	enum scrub_type	type;
};

/* These must correspond to XFS_SCRUB_TYPE_ */
static const struct scrub_descr scrubbers[XFS_SCRUB_TYPE_NR] = {
	[XFS_SCRUB_TYPE_PROBE] =
		{"metadata",				ST_NONE},
	[XFS_SCRUB_TYPE_SB] =
		{"superblock",				ST_AGHEADER},
	[XFS_SCRUB_TYPE_AGF] =
		{"free space header",			ST_AGHEADER},
	[XFS_SCRUB_TYPE_AGFL] =
		{"free list",				ST_AGHEADER},
	[XFS_SCRUB_TYPE_AGI] =
		{"inode header",			ST_AGHEADER},
	[XFS_SCRUB_TYPE_BNOBT] =
		{"freesp by block btree",		ST_PERAG},
	[XFS_SCRUB_TYPE_CNTBT] =
		{"freesp by length btree",		ST_PERAG},
	[XFS_SCRUB_TYPE_INOBT] =
		{"inode btree",				ST_PERAG},
	[XFS_SCRUB_TYPE_FINOBT] =
		{"free inode btree",			ST_PERAG},
	[XFS_SCRUB_TYPE_RMAPBT] =
		{"reverse mapping btree",		ST_PERAG},
	[XFS_SCRUB_TYPE_REFCNTBT] =
		{"reference count btree",		ST_PERAG},
	[XFS_SCRUB_TYPE_INODE] =
		{"inode record",			ST_INODE},
	[XFS_SCRUB_TYPE_BMBTD] =
		{"data block map",			ST_INODE},
	[XFS_SCRUB_TYPE_BMBTA] =
		{"attr block map",			ST_INODE},
	[XFS_SCRUB_TYPE_BMBTC] =
		{"CoW block map",			ST_INODE},
	[XFS_SCRUB_TYPE_DIR] =
		{"directory entries",			ST_INODE},
	[XFS_SCRUB_TYPE_XATTR] =
		{"extended attributes",			ST_INODE},
	[XFS_SCRUB_TYPE_SYMLINK] =
		{"symbolic link",			ST_INODE},
	[XFS_SCRUB_TYPE_PARENT] =
		{"parent pointer",			ST_INODE},
	[XFS_SCRUB_TYPE_RTBITMAP] =
		{"realtime bitmap",			ST_FS},
	[XFS_SCRUB_TYPE_RTSUM] =
		{"realtime summary",			ST_FS},
	[XFS_SCRUB_TYPE_UQUOTA] =
		{"user quotas",				ST_FS},
	[XFS_SCRUB_TYPE_GQUOTA] =
		{"group quotas",			ST_FS},
	[XFS_SCRUB_TYPE_PQUOTA] =
		{"project quotas",			ST_FS},
};

/* Format a scrub description. */
static void
format_scrub_descr(
	char				*buf,
	size_t				buflen,
	struct xfs_scrub_metadata	*meta,
	const struct scrub_descr	*sc)
{
	switch (sc->type) {
	case ST_AGHEADER:
	case ST_PERAG:
		snprintf(buf, buflen, _("AG %u %s"), meta->sm_agno,
				_(sc->name));
		break;
	case ST_INODE:
		snprintf(buf, buflen, _("Inode %"PRIu64" %s"),
				(uint64_t)meta->sm_ino, _(sc->name));
		break;
	case ST_FS:
		snprintf(buf, buflen, _("%s"), _(sc->name));
		break;
	case ST_NONE:
		assert(0);
		break;
	}
}

/* Predicates for scrub flag state. */

static inline bool is_corrupt(struct xfs_scrub_metadata *sm)
{
	return sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT;
}

static inline bool is_unoptimized(struct xfs_scrub_metadata *sm)
{
	return sm->sm_flags & XFS_SCRUB_OFLAG_PREEN;
}

static inline bool xref_failed(struct xfs_scrub_metadata *sm)
{
	return sm->sm_flags & XFS_SCRUB_OFLAG_XFAIL;
}

static inline bool xref_disagrees(struct xfs_scrub_metadata *sm)
{
	return sm->sm_flags & XFS_SCRUB_OFLAG_XCORRUPT;
}

static inline bool is_incomplete(struct xfs_scrub_metadata *sm)
{
	return sm->sm_flags & XFS_SCRUB_OFLAG_INCOMPLETE;
}

static inline bool is_suspicious(struct xfs_scrub_metadata *sm)
{
	return sm->sm_flags & XFS_SCRUB_OFLAG_WARNING;
}

/* Should we fix it? */
static inline bool needs_repair(struct xfs_scrub_metadata *sm)
{
	return is_corrupt(sm) || xref_disagrees(sm);
}

/* Warn about strange circumstances after scrub. */
static inline void
xfs_scrub_warn_incomplete_scrub(
	struct scrub_ctx		*ctx,
	const char			*descr,
	struct xfs_scrub_metadata	*meta)
{
	if (is_incomplete(meta))
		str_info(ctx, descr, _("Check incomplete."));

	if (is_suspicious(meta)) {
		if (debug)
			str_info(ctx, descr, _("Possibly suspect metadata."));
		else
			str_warn(ctx, descr, _("Possibly suspect metadata."));
	}

	if (xref_failed(meta))
		str_info(ctx, descr, _("Cross-referencing failed."));
}

/* Do a read-only check of some metadata. */
static enum check_outcome
xfs_check_metadata(
	struct scrub_ctx		*ctx,
	int				fd,
	struct xfs_scrub_metadata	*meta,
	bool				is_inode)
{
	char				buf[DESCR_BUFSZ];
	unsigned int			tries = 0;
	int				code;
	int				error;

	assert(!debug_tweak_on("XFS_SCRUB_NO_KERNEL"));
	assert(meta->sm_type < XFS_SCRUB_TYPE_NR);
	format_scrub_descr(buf, DESCR_BUFSZ, meta, &scrubbers[meta->sm_type]);

	dbg_printf("check %s flags %xh\n", buf, meta->sm_flags);
retry:
	error = ioctl(fd, XFS_IOC_SCRUB_METADATA, meta);
	if (debug_tweak_on("XFS_SCRUB_FORCE_REPAIR") && !error)
		meta->sm_flags |= XFS_SCRUB_OFLAG_CORRUPT;
	if (error) {
		code = errno;
		switch (code) {
		case ENOENT:
			/* Metadata not present, just skip it. */
			return CHECK_DONE;
		case ESHUTDOWN:
			/* FS already crashed, give up. */
			str_info(ctx, buf,
_("Filesystem is shut down, aborting."));
			return CHECK_ABORT;
		case EIO:
		case ENOMEM:
			/* Abort on I/O errors or insufficient memory. */
			str_errno(ctx, buf);
			return CHECK_ABORT;
		case EDEADLOCK:
		case EBUSY:
		case EFSBADCRC:
		case EFSCORRUPTED:
			/*
			 * The first two should never escape the kernel,
			 * and the other two should be reported via sm_flags.
			 */
			str_info(ctx, buf,
_("Kernel bug!  errno=%d"), code);
			/* fall through */
		default:
			/* Operational error. */
			str_errno(ctx, buf);
			return CHECK_DONE;
		}
	}

	/*
	 * If the kernel says the test was incomplete or that there was
	 * a cross-referencing discrepancy but no obvious corruption,
	 * we'll try the scan again, just in case the fs was busy.
	 * Only retry so many times.
	 */
	if (tries < 10 && (is_incomplete(meta) ||
			   (xref_disagrees(meta) && !is_corrupt(meta)))) {
		tries++;
		goto retry;
	}

	/* Complain about incomplete or suspicious metadata. */
	xfs_scrub_warn_incomplete_scrub(ctx, buf, meta);

	/*
	 * If we need repairs or there were discrepancies, schedule a
	 * repair if desired, otherwise complain.
	 */
	if (is_corrupt(meta) || xref_disagrees(meta)) {
		if (ctx->mode < SCRUB_MODE_REPAIR) {
			str_error(ctx, buf,
_("Repairs are required."));
			return CHECK_DONE;
		}

		return CHECK_REPAIR;
	}

	/*
	 * If we could optimize, schedule a repair if desired,
	 * otherwise complain.
	 */
	if (is_unoptimized(meta)) {
		if (ctx->mode != SCRUB_MODE_REPAIR) {
			if (!is_inode) {
				/* AG or FS metadata, always warn. */
				str_info(ctx, buf,
_("Optimization is possible."));
			} else if (!ctx->preen_triggers[meta->sm_type]) {
				/* File metadata, only warn once per type. */
				pthread_mutex_lock(&ctx->lock);
				if (!ctx->preen_triggers[meta->sm_type])
					ctx->preen_triggers[meta->sm_type] = true;
				pthread_mutex_unlock(&ctx->lock);
			}
			return CHECK_DONE;
		}

		return CHECK_REPAIR;
	}

	/* Everything is ok. */
	return CHECK_DONE;
}

/* Bulk-notify user about things that could be optimized. */
void
xfs_scrub_report_preen_triggers(
	struct scrub_ctx		*ctx)
{
	int				i;

	for (i = 0; i < XFS_SCRUB_TYPE_NR; i++) {
		pthread_mutex_lock(&ctx->lock);
		if (ctx->preen_triggers[i]) {
			ctx->preen_triggers[i] = false;
			pthread_mutex_unlock(&ctx->lock);
			str_info(ctx, ctx->mntpoint,
_("Optimizations of %s are possible."), scrubbers[i].name);
		} else {
			pthread_mutex_unlock(&ctx->lock);
		}
	}
}

/* Save a scrub context for later repairs. */
static bool
xfs_scrub_save_repair(
	struct scrub_ctx		*ctx,
	struct xfs_action_list		*alist,
	struct xfs_scrub_metadata	*meta)
{
	struct action_item		*aitem;

	/* Schedule this item for later repairs. */
	aitem = malloc(sizeof(struct action_item));
	if (!aitem) {
		str_errno(ctx, _("repair list"));
		return false;
	}
	memset(aitem, 0, sizeof(*aitem));
	aitem->type = meta->sm_type;
	aitem->flags = meta->sm_flags;
	switch (scrubbers[meta->sm_type].type) {
	case ST_AGHEADER:
	case ST_PERAG:
		aitem->agno = meta->sm_agno;
		break;
	case ST_INODE:
		aitem->ino = meta->sm_ino;
		aitem->gen = meta->sm_gen;
		break;
	default:
		break;
	}

	xfs_action_list_add(alist, aitem);
	return true;
}

/* Scrub metadata, saving corruption reports for later. */
static bool
xfs_scrub_metadata(
	struct scrub_ctx		*ctx,
	enum scrub_type			scrub_type,
	xfs_agnumber_t			agno,
	struct xfs_action_list		*alist)
{
	struct xfs_scrub_metadata	meta = {0};
	const struct scrub_descr	*sc;
	enum check_outcome		fix;
	int				type;

	sc = scrubbers;
	for (type = 0; type < XFS_SCRUB_TYPE_NR; type++, sc++) {
		if (sc->type != scrub_type)
			continue;

		meta.sm_type = type;
		meta.sm_flags = 0;
		meta.sm_agno = agno;
		background_sleep();

		/* Check the item. */
		fix = xfs_check_metadata(ctx, ctx->mnt_fd, &meta, false);
		progress_add(1);
		switch (fix) {
		case CHECK_ABORT:
			return false;
		case CHECK_REPAIR:
			if (!xfs_scrub_save_repair(ctx, alist, &meta))
				return false;
			/* fall through */
		case CHECK_DONE:
			continue;
		case CHECK_RETRY:
			abort();
			break;
		}
	}

	return true;
}

/*
 * Scrub primary superblock.  This will be useful if we ever need to hook
 * a filesystem-wide pre-scrub activity off of the sb 0 scrubber (which
 * currently does nothing).
 */
bool
xfs_scrub_primary_super(
	struct scrub_ctx		*ctx,
	struct xfs_action_list		*alist)
{
	struct xfs_scrub_metadata	meta = {
		.sm_type = XFS_SCRUB_TYPE_SB,
	};
	enum check_outcome		fix;

	/* Check the item. */
	fix = xfs_check_metadata(ctx, ctx->mnt_fd, &meta, false);
	switch (fix) {
	case CHECK_ABORT:
		return false;
	case CHECK_REPAIR:
		if (!xfs_scrub_save_repair(ctx, alist, &meta))
			return false;
		/* fall through */
	case CHECK_DONE:
		return true;
	case CHECK_RETRY:
		abort();
		break;
	}

	return true;
}

/* Scrub each AG's header blocks. */
bool
xfs_scrub_ag_headers(
	struct scrub_ctx		*ctx,
	xfs_agnumber_t			agno,
	struct xfs_action_list		*alist)
{
	return xfs_scrub_metadata(ctx, ST_AGHEADER, agno, alist);
}

/* Scrub each AG's metadata btrees. */
bool
xfs_scrub_ag_metadata(
	struct scrub_ctx		*ctx,
	xfs_agnumber_t			agno,
	struct xfs_action_list		*alist)
{
	return xfs_scrub_metadata(ctx, ST_PERAG, agno, alist);
}

/* Scrub whole-FS metadata btrees. */
bool
xfs_scrub_fs_metadata(
	struct scrub_ctx		*ctx,
	struct xfs_action_list		*alist)
{
	return xfs_scrub_metadata(ctx, ST_FS, 0, alist);
}

/* How many items do we have to check? */
unsigned int
xfs_scrub_estimate_ag_work(
	struct scrub_ctx		*ctx)
{
	const struct scrub_descr	*sc;
	int				type;
	unsigned int			estimate = 0;

	sc = scrubbers;
	for (type = 0; type < XFS_SCRUB_TYPE_NR; type++, sc++) {
		switch (sc->type) {
		case ST_AGHEADER:
		case ST_PERAG:
			estimate += ctx->geo.agcount;
			break;
		case ST_FS:
			estimate++;
			break;
		default:
			break;
		}
	}
	return estimate;
}

/* Scrub inode metadata. */
static bool
__xfs_scrub_file(
	struct scrub_ctx		*ctx,
	uint64_t			ino,
	uint32_t			gen,
	int				fd,
	unsigned int			type,
	struct xfs_action_list		*alist)
{
	struct xfs_scrub_metadata	meta = {0};
	enum check_outcome		fix;

	assert(type < XFS_SCRUB_TYPE_NR);
	assert(scrubbers[type].type == ST_INODE);

	meta.sm_type = type;
	meta.sm_ino = ino;
	meta.sm_gen = gen;

	/* Scrub the piece of metadata. */
	fix = xfs_check_metadata(ctx, fd, &meta, true);
	if (fix == CHECK_ABORT)
		return false;
	if (fix == CHECK_DONE)
		return true;

	return xfs_scrub_save_repair(ctx, alist, &meta);
}

bool
xfs_scrub_inode_fields(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_INODE, alist);
}

bool
xfs_scrub_data_fork(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_BMBTD, alist);
}

bool
xfs_scrub_attr_fork(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_BMBTA, alist);
}

bool
xfs_scrub_cow_fork(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_BMBTC, alist);
}

bool
xfs_scrub_dir(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_DIR, alist);
}

bool
xfs_scrub_attr(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_XATTR, alist);
}

bool
xfs_scrub_symlink(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_SYMLINK, alist);
}

bool
xfs_scrub_parent(
	struct scrub_ctx	*ctx,
	uint64_t		ino,
	uint32_t		gen,
	int			fd,
	struct xfs_action_list	*alist)
{
	return __xfs_scrub_file(ctx, ino, gen, fd, XFS_SCRUB_TYPE_PARENT, alist);
}

/* Test the availability of a kernel scrub command. */
static bool
__xfs_scrub_test(
	struct scrub_ctx		*ctx,
	unsigned int			type,
	bool				repair)
{
	struct xfs_scrub_metadata	meta = {0};
	struct xfs_error_injection	inject;
	static bool			injected;
	int				error;

	if (debug_tweak_on("XFS_SCRUB_NO_KERNEL"))
		return false;
	if (debug_tweak_on("XFS_SCRUB_FORCE_REPAIR") && !injected) {
		inject.fd = ctx->mnt_fd;
		inject.errtag = XFS_ERRTAG_FORCE_SCRUB_REPAIR;
		error = ioctl(ctx->mnt_fd, XFS_IOC_ERROR_INJECTION, &inject);
		if (error == 0)
			injected = true;
	}

	meta.sm_type = type;
	if (repair)
		meta.sm_flags |= XFS_SCRUB_IFLAG_REPAIR;
	error = ioctl(ctx->mnt_fd, XFS_IOC_SCRUB_METADATA, &meta);
	if (!error)
		return true;
	switch (errno) {
	case EROFS:
		str_info(ctx, ctx->mntpoint,
_("Filesystem is mounted read-only; cannot proceed."));
		return false;
	case ENOTRECOVERABLE:
		str_info(ctx, ctx->mntpoint,
_("Filesystem is mounted norecovery; cannot proceed."));
		return false;
	case EOPNOTSUPP:
	case ENOTTY:
		if (debug || verbose)
			str_info(ctx, ctx->mntpoint,
_("Kernel %s %s facility not detected."),
					_(scrubbers[type].name),
					repair ? _("repair") : _("scrub"));
		return false;
	case ENOENT:
		/* Scrubber says not present on this fs; that's fine. */
		return true;
	default:
		str_info(ctx, ctx->mntpoint, "%s", strerror(errno));
		return true;
	}
}

bool
xfs_can_scrub_fs_metadata(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_PROBE, false);
}

bool
xfs_can_scrub_inode(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_INODE, false);
}

bool
xfs_can_scrub_bmap(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_BMBTD, false);
}

bool
xfs_can_scrub_dir(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_DIR, false);
}

bool
xfs_can_scrub_attr(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_XATTR, false);
}

bool
xfs_can_scrub_symlink(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_SYMLINK, false);
}

bool
xfs_can_scrub_parent(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_PARENT, false);
}

bool
xfs_can_repair(
	struct scrub_ctx	*ctx)
{
	return __xfs_scrub_test(ctx, XFS_SCRUB_TYPE_PROBE, true);
}

/* General repair routines. */

/* Repair some metadata. */
enum check_outcome
xfs_repair_metadata(
	struct scrub_ctx		*ctx,
	int				fd,
	struct action_item		*aitem,
	unsigned int			repair_flags)
{
	char				buf[DESCR_BUFSZ];
	struct xfs_scrub_metadata	meta = { 0 };
	struct xfs_scrub_metadata	oldm;
	int				error;

	assert(aitem->type < XFS_SCRUB_TYPE_NR);
	assert(!debug_tweak_on("XFS_SCRUB_NO_KERNEL"));
	meta.sm_type = aitem->type;
	meta.sm_flags = aitem->flags | XFS_SCRUB_IFLAG_REPAIR;
	switch (scrubbers[aitem->type].type) {
	case ST_AGHEADER:
	case ST_PERAG:
		meta.sm_agno = aitem->agno;
		break;
	case ST_INODE:
		meta.sm_ino = aitem->ino;
		meta.sm_gen = aitem->gen;
		break;
	default:
		break;
	}

	if (!is_corrupt(&meta) && (repair_flags & XRM_REPAIR_ONLY))
		return CHECK_RETRY;

	memcpy(&oldm, &meta, sizeof(oldm));
	format_scrub_descr(buf, DESCR_BUFSZ, &meta, &scrubbers[meta.sm_type]);

	if (needs_repair(&meta))
		str_info(ctx, buf, _("Attempting repair."));
	else if (debug || verbose)
		str_info(ctx, buf, _("Attempting optimization."));

	error = ioctl(fd, XFS_IOC_SCRUB_METADATA, &meta);
	if (error) {
		switch (errno) {
		case EDEADLOCK:
		case EBUSY:
			/* Filesystem is busy, try again later. */
			if (debug || verbose)
				str_info(ctx, buf,
_("Filesystem is busy, deferring repair."));
			return CHECK_RETRY;
		case ESHUTDOWN:
			/* Filesystem is already shut down, abort. */
			str_info(ctx, buf,
_("Filesystem is shut down, aborting."));
			return CHECK_ABORT;
		case ENOTTY:
		case EOPNOTSUPP:
			/*
			 * If we're in no-complain mode, requeue the check for
			 * later.  It's possible that an error in another
			 * component caused us to flag an error in this
			 * component.  Even if the kernel didn't think it
			 * could fix this, it's at least worth trying the scan
			 * again to see if another repair fixed it.
			 */
			if (!(repair_flags & XRM_COMPLAIN_IF_UNFIXED))
				return CHECK_RETRY;
			/*
			 * If we forced repairs or this is a preen, don't
			 * error out if the kernel doesn't know how to fix.
			 */
			if (is_unoptimized(&oldm) ||
			    debug_tweak_on("XFS_SCRUB_FORCE_REPAIR"))
				return CHECK_DONE;
			/* fall through */
		case EINVAL:
			/* Kernel doesn't know how to repair this? */
			str_error(ctx, buf,
_("Don't know how to fix; offline repair required."));
			return CHECK_DONE;
		case EROFS:
			/* Read-only filesystem, can't fix. */
			if (verbose || debug || needs_repair(&oldm))
				str_info(ctx, buf,
_("Read-only filesystem; cannot make changes."));
			return CHECK_DONE;
		case ENOENT:
			/* Metadata not present, just skip it. */
			return CHECK_DONE;
		case ENOMEM:
		case ENOSPC:
			/* Don't care if preen fails due to low resources. */
			if (is_unoptimized(&oldm) && !needs_repair(&oldm))
				return CHECK_DONE;
			/* fall through */
		default:
			/*
			 * Operational error.  If the caller doesn't want us
			 * to complain about repair failures, tell the caller
			 * to requeue the repair for later and don't say a
			 * thing.  Otherwise, print error and bail out.
			 */
			if (!(repair_flags & XRM_COMPLAIN_IF_UNFIXED))
				return CHECK_RETRY;
			str_errno(ctx, buf);
			return CHECK_DONE;
		}
	}
	if (repair_flags & XRM_COMPLAIN_IF_UNFIXED)
		xfs_scrub_warn_incomplete_scrub(ctx, buf, &meta);
	if (needs_repair(&meta)) {
		/*
		 * Still broken; if we've been told not to complain then we
		 * just requeue this and try again later.  Otherwise we
		 * log the error loudly and don't try again.
		 */
		if (!(repair_flags & XRM_COMPLAIN_IF_UNFIXED))
			return CHECK_RETRY;
		str_error(ctx, buf,
_("Repair unsuccessful; offline repair required."));
	} else {
		/* Clean operation, no corruption detected. */
		if (needs_repair(&oldm))
			record_repair(ctx, buf, _("Repairs successful."));
		else
			record_preen(ctx, buf, _("Optimization successful."));
	}
	return CHECK_DONE;
}
