cli: Add new write-protect CLI

Add a new write-protect CLI that is based on the classic-CLI feature
in flashrom/master. The syntax is slighty different: With the new
CLI wrapper, we can either call it as `flashprog write-protect` or
`flashprog wp`. To keep the CLI code clean, we allow only one write-
protection operation per call.

For instance, the write-protection status can then be queried like
this:

  $ flashprog wp status -p ch341a_spi

Change-Id: I32818b58c9db939719913fc63063c41a27876554
Signed-off-by: Nico Huber <nico.h@gmx.de>
Reviewed-on: https://review.sourcearcade.org/c/flashprog/+/72991
diff --git a/Makefile b/Makefile
index 65b808b..cc6e4d4 100644
--- a/Makefile
+++ b/Makefile
@@ -401,7 +401,7 @@
 ###############################################################################
 # Frontend related stuff.
 
-CLI_OBJS = cli.o cli_config.o cli_classic.o cli_output.o cli_common.o print.o
+CLI_OBJS = cli.o cli_config.o cli_wp.o cli_classic.o cli_output.o cli_common.o print.o
 
 # By default version information will be fetched from Git if available.
 # Otherwise, versioninfo.inc stores the metadata required to build a
@@ -925,7 +925,7 @@
 endif
 
 OBJS = $(CHIP_OBJS) $(PROGRAMMER_OBJS) $(LIB_OBJS)
-MANS = $(PROGRAM).8 $(PROGRAM)-config.8
+MANS = $(PROGRAM).8 $(PROGRAM)-config.8 $(PROGRAM)-write-protect.8
 
 all: $(PROGRAM)$(EXEC_SUFFIX) $(MANS)
 ifeq ($(ARCH), x86)
diff --git a/cli.c b/cli.c
index f1d483a..49e0c60 100644
--- a/cli.c
+++ b/cli.c
@@ -28,6 +28,8 @@
 	{ "prog",		flashprog_classic_main },
 	{ "cfg",		flashprog_config_main },
 	{ "config",		flashprog_config_main },
+	{ "wp",			flashprog_wp_main },
+	{ "write-protect",	flashprog_wp_main },
 };
 
 static void usage(const char *const name)
@@ -37,6 +39,7 @@
 			" prog                     Standard memory operations\n"
 			"                          (read/erase/write/verify)\n"
 			" cfg | config             Status/config register operations\n"
+			" wp | write-protect       Write-protection operations\n"
 			"\n"
 			"The default is 'prog'. See `%s <command> --help`\n"
 			"for further instructions.\n\n", name);
diff --git a/cli_common.c b/cli_common.c
index 8257fcf..1d43ad2 100644
--- a/cli_common.c
+++ b/cli_common.c
@@ -230,7 +230,7 @@
 	return -1;
 }
 
-void print_generic_options(void)
+void print_generic_options(const bool layout_options)
 {
 	fprintf(stderr, "\n"
 		"Where generic <options> are\n"
@@ -241,6 +241,15 @@
 		"    -V | --verbose                      more verbose output\n"
 		"    -o | --output <logfile>             log output to <logfile>\n"
 		"    -h | --help                         print help text\n");
+
+	if (!layout_options)
+		return;
+	fprintf(stderr, "\n"
+		"and layout <options> are\n"
+		"    -l | --layout <layoutfile>          read ROM layout from <layoutfile>\n"
+		"         --fmap-file <fmapfile>         read ROM layout from fmap in <fmapfile>\n"
+		"         --fmap                         read ROM layout from fmap embedded in ROM\n"
+		"         --ifd                          read layout from an Intel Flash Descriptor\n");
 }
 
 void print_chip_support_status(const struct flashchip *chip)
diff --git a/cli_config.c b/cli_config.c
index 3ffa048..7fbd310 100644
--- a/cli_config.c
+++ b/cli_config.c
@@ -15,6 +15,7 @@
 #include <stdio.h>
 #include <stdint.h>
 #include <stdlib.h>
+#include <stdbool.h>
 #include <string.h>
 #include <getopt.h>
 #include <limits.h>
@@ -88,7 +89,7 @@
 			"\t%s [get] <options> <setting>\n"
 			"\t%s  set  <options> [--temporary] <setting> <value>\n",
 			name, name);
-	print_generic_options();
+	print_generic_options(/* layout_options =>*/false);
 	fprintf(stderr, "\n<setting> can be\n"
 			"    qe | quad-enable        Quad-Enable (QE) bit\n"
 			"\nand <value> can be `true', `false', or a number.\n"
diff --git a/cli_wp.c b/cli_wp.c
new file mode 100644
index 0000000..cfc7afd
--- /dev/null
+++ b/cli_wp.c
@@ -0,0 +1,458 @@
+/*
+ * This file is part of the flashprog project.
+ *
+ * This program 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 2 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+
+#include "libflashprog.h"
+#include "cli.h"
+
+static void usage(const char *const name, const char *const msg)
+{
+	if (msg)
+		fprintf(stderr, "\nError: %s\n", msg);
+
+	fprintf(stderr, "\nUsage:"
+			"\t%s [status] <options>\n"
+			"\t%s  list    <options>\n"
+			"\t%s  disable <options> [--temporary]\n"
+			"\t%s  enable  <options> [--temporary]\n"
+			"\t%s  range   <options> [--temporary] <start>,<len>\n"
+			"\t%s  region  <options> [--temporary] <region-name>\n",
+			name, name, name, name, name, name);
+	fprintf(stderr, "\n"
+		"A range is specified by two integers, the offset from the start of the flash\n"
+		"and the length in bytes.  A region is specified by name from the layout, see\n"
+		"layout options below.\n");
+	print_generic_options(/* layout_options =>*/true);
+	exit(1);
+}
+
+static int parse_wp_range(size_t *const start, size_t *const len, const char *const arg)
+{
+	size_t processed;
+
+	if (sscanf(arg, "%zi,%zi%zn", start, len, &processed) != 2)
+		return -1;
+
+	if (*start > SIZE_MAX / 2 || *len > SIZE_MAX / 2)
+		return -1;
+
+	if (processed != strlen(arg))
+		return -1;
+
+	return 0;
+}
+
+static void print_wp_range(const char *const prefix,
+			   struct flashprog_flashctx *const flash,
+			   size_t start, size_t len)
+{
+	/* Start address and length */
+	printf("%sstart=0x%08zx length=0x%08zx ", prefix, start, len);
+
+	/* Easily readable description like 'none' or 'lower 1/8' */
+	size_t chip_len = flashprog_flash_getsize(flash);
+
+	if (len == 0) {
+		printf("(none)\n");
+	} else if (len == chip_len) {
+		printf("(all)\n");
+	} else {
+		const char *location = "";
+		if (start == 0)
+			location = "lower ";
+		if (start == chip_len - len)
+			location = "upper ";
+
+		/* Remove common factors of 2 to simplify */
+		/* the (range_len/chip_len) fraction. */
+		while ((chip_len % 2) == 0 && (len % 2) == 0) {
+			chip_len /= 2;
+			len /= 2;
+		}
+
+		printf("(%s%zu/%zu)\n", location, len, chip_len);
+	}
+}
+
+static const char *get_wp_error_str(int err)
+{
+	switch (err) {
+	case FLASHPROG_WP_ERR_CHIP_UNSUPPORTED:
+		return "WP operations are not implemented for this chip";
+	case FLASHPROG_WP_ERR_READ_FAILED:
+		return "failed to read the current WP configuration";
+	case FLASHPROG_WP_ERR_WRITE_FAILED:
+		return "failed to write the new WP configuration";
+	case FLASHPROG_WP_ERR_VERIFY_FAILED:
+		return "unexpected WP configuration read back from chip";
+	case FLASHPROG_WP_ERR_MODE_UNSUPPORTED:
+		return "the requested protection mode is not supported";
+	case FLASHPROG_WP_ERR_RANGE_UNSUPPORTED:
+		return "the requested protection range is not supported";
+	case FLASHPROG_WP_ERR_RANGE_LIST_UNAVAILABLE:
+		return "could not determine what protection ranges are available";
+	case FLASHPROG_WP_ERR_UNSUPPORTED_STATE:
+		return "can't operate on current WP configuration of the chip";
+	}
+	return "unknown WP error";
+}
+
+static int wp_print_status(struct flashprog_flashctx *const flash)
+{
+	size_t start, len;
+	enum flashprog_wp_mode mode;
+	struct flashprog_wp_cfg *cfg = NULL;
+	enum flashprog_wp_result ret;
+
+	ret = flashprog_wp_cfg_new(&cfg);
+	if (ret == FLASHPROG_WP_OK)
+		ret = flashprog_wp_read_cfg(cfg, flash);
+
+	if (ret != FLASHPROG_WP_OK) {
+		fprintf(stderr, "Failed to get WP status: %s\n", get_wp_error_str(ret));
+		flashprog_wp_cfg_release(cfg);
+		return 1;
+	}
+
+	flashprog_wp_get_range(&start, &len, cfg);
+	mode = flashprog_wp_get_mode(cfg);
+	flashprog_wp_cfg_release(cfg);
+
+	print_wp_range("Protection range: ", flash, start, len);
+
+	const char *mode_desc;
+	switch (mode) {
+		case FLASHPROG_WP_MODE_DISABLED:    mode_desc = "disabled";	break;
+		case FLASHPROG_WP_MODE_HARDWARE:    mode_desc = "hardware";	break;
+		case FLASHPROG_WP_MODE_POWER_CYCLE: mode_desc = "power_cycle";	break;
+		case FLASHPROG_WP_MODE_PERMANENT:   mode_desc = "permanent";	break;
+		default:			    mode_desc = "unknown";	break;
+	}
+	printf("Protection mode: %s\n", mode_desc);
+
+	return 0;
+}
+
+static int wp_print_ranges(struct flashprog_flashctx *const flash)
+{
+	struct flashprog_wp_ranges *list;
+	size_t i;
+
+	const enum flashprog_wp_result ret = flashprog_wp_get_available_ranges(&list, flash);
+	if (ret != FLASHPROG_WP_OK) {
+		fprintf(stderr, "Failed to get list of protection ranges: %s\n", get_wp_error_str(ret));
+		return 1;
+	}
+
+	printf("Available protection ranges:\n");
+	const size_t count = flashprog_wp_ranges_get_count(list);
+	for (i = 0; i < count; i++) {
+		size_t start, len;
+
+		flashprog_wp_ranges_get_range(&start, &len, list, i);
+		print_wp_range("\t", flash, start, len);
+	}
+	flashprog_wp_ranges_release(list);
+
+	return 0;
+}
+
+static int wp_apply(struct flashprog_flashctx *const flash,
+		    const bool enable_wp, const bool disable_wp,
+		    const bool set_wp_range, const size_t wp_start,
+		    const size_t wp_len)
+{
+	struct flashprog_wp_cfg *cfg;
+	enum flashprog_wp_result ret;
+
+	ret = flashprog_wp_cfg_new(&cfg);
+	if (ret == FLASHPROG_WP_OK)
+		ret = flashprog_wp_read_cfg(cfg, flash);
+
+	if (ret != FLASHPROG_WP_OK) {
+		fprintf(stderr, "Failed to get WP status: %s\n", get_wp_error_str(ret));
+		flashprog_wp_cfg_release(cfg);
+		return 1;
+	}
+
+	/* Store current WP mode for printing help text if changing the cfg fails. */
+	const enum flashprog_wp_mode old_mode = flashprog_wp_get_mode(cfg);
+
+	if (set_wp_range)
+		flashprog_wp_set_range(cfg, wp_start, wp_len);
+
+	if (disable_wp)
+		flashprog_wp_set_mode(cfg, FLASHPROG_WP_MODE_DISABLED);
+
+	if (enable_wp)
+		flashprog_wp_set_mode(cfg, FLASHPROG_WP_MODE_HARDWARE);
+
+	ret = flashprog_wp_write_cfg(flash, cfg);
+
+	flashprog_wp_cfg_release(cfg);
+
+	if (ret != FLASHPROG_WP_OK) {
+		fprintf(stderr, "Failed to apply new WP settings: %s\n", get_wp_error_str(ret));
+
+		if (ret != FLASHPROG_WP_ERR_VERIFY_FAILED)
+			return 1;
+
+		/* Warn user if active WP is likely to have caused failure */
+		switch (old_mode) {
+		case FLASHPROG_WP_MODE_HARDWARE:
+			fprintf(stderr, "Note: hardware status register protection is enabled. "
+				"The chip's WP# pin must be set to an inactive voltage "
+				"level to be able to change the WP settings.\n");
+			break;
+		case FLASHPROG_WP_MODE_POWER_CYCLE:
+			fprintf(stderr, "Note: power-cycle status register protection is enabled. "
+				"A power-off, power-on cycle is usually required to change "
+				"the chip's WP settings.\n");
+			break;
+		case FLASHPROG_WP_MODE_PERMANENT:
+			fprintf(stderr, "Note: permanent status register protection is enabled. "
+				"The chip's WP settings cannot be modified.\n");
+			break;
+		default:
+			break;
+		}
+		return 1;
+	}
+
+	if (disable_wp)
+		printf("Disabled hardware protection\n");
+
+	if (enable_wp)
+		printf("Enabled hardware protection\n");
+
+	if (set_wp_range)
+		print_wp_range("Configured protection range: ", flash, wp_start, wp_len);
+
+	return 0;
+}
+
+int flashprog_wp_main(int argc, char *argv[])
+{
+	static const char optstring[] = "+p:c:Vo:hl:";
+	static const struct option long_options[] = {
+		{"programmer",		1, NULL, 'p'},
+		{"chip",		1, NULL, 'c'},
+		{"verbose",		0, NULL, 'V'},
+		{"output",		1, NULL, 'o'},
+		{"help",		0, NULL, 'h'},
+		{"layout",		1, NULL, 'l'},
+		{"ifd",			0, NULL, OPTION_IFD},
+		{"fmap",		0, NULL, OPTION_FMAP},
+		{"fmap-file",		1, NULL, OPTION_FMAP_FILE},
+		{"temporary",		0, NULL, OPTION_CONFIG_VOLATILE},
+		{"status",		0, NULL, OPTION_WP_STATUS},
+		{"list",		0, NULL, OPTION_WP_LIST},
+		{"range",		0, NULL, OPTION_WP_SET_RANGE},
+		{"region",		0, NULL, OPTION_WP_SET_REGION},
+		{"enable",		0, NULL, OPTION_WP_ENABLE},
+		{"disable",		0, NULL, OPTION_WP_DISABLE},
+		{NULL,			0, NULL, 0},
+	};
+	static const struct opt_command cmd_options[] = {
+		{"status",		OPTION_WP_STATUS},
+		{"list",		OPTION_WP_LIST},
+		{"range",		OPTION_WP_SET_RANGE},
+		{"region",		OPTION_WP_SET_REGION},
+		{"enable",		OPTION_WP_ENABLE},
+		{"disable",		OPTION_WP_DISABLE},
+		{NULL,			0},
+	};
+
+	unsigned int ops = 0;
+	int ret = 1, opt;
+	struct log_args log_args = { FLASHPROG_MSG_INFO, FLASHPROG_MSG_DEBUG2, NULL };
+	struct flash_args flash_args = { 0 };
+	struct layout_args layout_args = { 0 };
+	bool volat1le = false;
+	bool enable_wp = false, disable_wp = false, print_wp_status = false;
+	bool set_wp_range = false, set_wp_region = false, print_wp_ranges = false;
+	size_t wp_start = 0, wp_len = 0;
+	char *wp_region = NULL;
+
+	if (cli_init()) /* TODO: Can be moved below argument parsing once usage() uses `stderr` directly. */
+		goto free_ret;
+
+	if (argc < 2)
+		usage(argv[0], NULL);
+
+	while ((opt = getopt_long(argc, argv, optstring, long_options, NULL)) != -1 ||
+	       (opt = getopt_command(argc, argv, cmd_options)) != -1) {
+		switch (opt) {
+		case 'V':
+		case 'o':
+			ret = cli_parse_log_args(&log_args, opt, optarg);
+			if (ret == 1)
+				usage(argv[0], NULL);
+			else if (ret)
+				goto free_ret;
+			break;
+		case 'p':
+		case 'c':
+			ret = cli_parse_flash_args(&flash_args, opt, optarg);
+			if (ret == 1)
+				usage(argv[0], NULL);
+			else if (ret)
+				goto free_ret;
+			break;
+		case OPTION_LAYOUT:
+		case OPTION_IFD:
+		case OPTION_FMAP:
+		case OPTION_FMAP_FILE:
+			ret = cli_parse_layout_args(&layout_args, opt, optarg);
+			if (ret == 1)
+				usage(argv[0], NULL);
+			else if (ret)
+				goto free_ret;
+			break;
+		case OPTION_CONFIG_VOLATILE:
+			volat1le = true;
+			break;
+		case OPTION_WP_STATUS:
+			print_wp_status = true;
+			++ops;
+			break;
+		case OPTION_WP_LIST:
+			print_wp_ranges = true;
+			++ops;
+			break;
+		case OPTION_WP_SET_RANGE:
+			set_wp_range = true;
+			++ops;
+			break;
+		case OPTION_WP_SET_REGION:
+			set_wp_region = true;
+			++ops;
+			break;
+		case OPTION_WP_ENABLE:
+			enable_wp = true;
+			++ops;
+			break;
+		case OPTION_WP_DISABLE:
+			disable_wp = true;
+			++ops;
+			break;
+		case '?':
+		case 'h':
+			usage(argv[0], NULL);
+			break;
+		}
+	}
+
+	if (!ops) {
+		print_wp_status = true;
+		++ops;
+	}
+	if (ops > 1)
+		usage(argv[0], "Only one operation may be specified.");
+
+	if (!enable_wp && !disable_wp && !set_wp_range && !set_wp_region && volat1le)
+		usage(argv[0], "`--temporary' may only be specified for write operations.");
+
+	ret = 1;
+	if (set_wp_range) {
+		if (optind != argc - 1)
+			usage(argv[0], "`range' requires exactly one argument.");
+		if (parse_wp_range(&wp_start, &wp_len, argv[optind++]) < 0)
+			usage(argv[0], "Incorrect wp-range arguments provided.");
+	} else if (set_wp_region) {
+		if (optind != argc - 1)
+			usage(argv[0], "`region' requires exactly one argument.");
+		wp_region = strdup(argv[optind++]);
+		if (!wp_region) {
+			fprintf(stderr, "Out of memory!\n");
+			goto free_ret;
+		}
+	} else if (optind < argc) {
+		usage(argv[0], "Extra parameter found.");
+	}
+
+	if (!flash_args.prog_name)
+		usage(argv[0], "No programmer specified.");
+
+	struct flashprog_programmer *prog;
+	struct flashprog_flashctx *flash;
+	struct flashprog_layout *layout = NULL;
+
+	if (log_args.logfile && open_logfile(log_args.logfile))
+		goto free_ret;
+	verbose_screen = log_args.screen_level;
+	verbose_logfile = log_args.logfile_level;
+	start_logging();
+
+	if (flashprog_programmer_init(&prog, flash_args.prog_name, flash_args.prog_args))
+		goto free_ret;
+	ret = flashprog_flash_probe(&flash, prog, flash_args.chip);
+	if (ret == 3) {
+		fprintf(stderr, "Multiple flash chip definitions match the detected chip.\n"
+				"Please specify which chip definition to use with the -c <chipname> option.\n");
+	} else if (ret) {
+		fprintf(stderr, "No EEPROM/flash device found.\n");
+		goto shutdown_ret;
+	}
+
+	flashprog_flag_set(flash, FLASHPROG_FLAG_NON_VOLATILE_WRSR, !volat1le);
+
+	if (print_wp_status)
+		ret = wp_print_status(flash);
+
+	if (print_wp_ranges)
+		ret = wp_print_ranges(flash);
+
+	if (set_wp_region) {
+		ret = 1;
+		if (cli_process_layout_args(&layout, flash, &layout_args)) {
+			fprintf(stderr, "Failed to read layout.\n");
+			goto release_ret;
+		}
+		if (!layout) {
+			fprintf(stderr, "Error: `--region' operation requires a layout.\n");
+			goto release_ret;
+		}
+
+		if (flashprog_layout_get_region_range(layout, wp_region, &wp_start, &wp_len)) {
+			fprintf(stderr, "Cannot find region '%s'.\n", wp_region);
+			goto release_ret;
+		}
+		set_wp_range = true;
+	}
+
+	if (set_wp_range || enable_wp || disable_wp)
+		ret = wp_apply(flash, enable_wp, disable_wp, set_wp_range, wp_start, wp_len);
+
+release_ret:
+	flashprog_layout_release(layout);
+	flashprog_flash_release(flash);
+shutdown_ret:
+	flashprog_programmer_shutdown(prog);
+free_ret:
+	free(wp_region);
+	free(layout_args.fmapfile);
+	free(layout_args.layoutfile);
+	free(flash_args.chip);
+	free(flash_args.prog_args);
+	free(flash_args.prog_name);
+	free(log_args.logfile);
+	close_logfile();
+	return ret;
+}
diff --git a/flashprog-write-protect.8.tmpl b/flashprog-write-protect.8.tmpl
new file mode 100644
index 0000000..d536374
--- /dev/null
+++ b/flashprog-write-protect.8.tmpl
@@ -0,0 +1,241 @@
+.\" Load the www device when using groff; provide a fallback for groff's MTO macro that formats email addresses.
+.ie \n[.g] \
+.  mso www.tmac
+.el \{
+.  de MTO
+     \\$2 \(la\\$1 \(ra\\$3 \
+.  .
+.\}
+.\" Create wrappers for .MTO and .URL that print only text on systems w/o groff or if not outputting to a HTML
+.\" device. To that end we need to distinguish HTML output on groff from other configurations first.
+.nr groffhtml 0
+.if \n[.g] \
+.  if "\*[.T]"html" \
+.    nr groffhtml 1
+.\" For code reuse it would be nice to have a single wrapper that gets its target macro as parameter.
+.\" However, this did not work out with NetBSD's and OpenBSD's groff...
+.de URLB
+.  ie (\n[groffhtml]==1) \{\
+.    URL \\$@
+.  \}
+.  el \{\
+.    ie "\\$2"" \{\
+.      BR "\\$1" "\\$3"
+.    \}
+.    el \{\
+.      RB "\\$2 \(la" "\\$1" "\(ra\\$3"
+.    \}
+.  \}
+..
+.de MTOB
+.  ie (\n[groffhtml]==1) \{\
+.    MTO \\$@
+.  \}
+.  el \{\
+.    ie "\\$2"" \{\
+.      BR "\\$1" "\\$3"
+.    \}
+.    el \{\
+.      RB "\\$2 \(la" "\\$1" "\(ra\\$3"
+.    \}
+.  \}
+..
+.TH FLASHPROG-WRITE-PROTECT 8 "@MAN_DATE@" "flashprog-write-protect-@VERSION@" "@MAN_DATE@"
+
+.SH NAME
+flashprog-write-protect \- control write-protection settings of flash chips
+
+.SH SYNOPSIS
+.I flashprog write-protect \fR[\fIstatus\fR] <options>
+.br
+.I flashprog write-protect \ list \ \ \ \fR<options>
+.br
+.I flashprog write-protect \ disable    \fR<options> [\fB\-\-temporary\fR]
+.br
+.I flashprog write-protect \ enable   \ \fR<options> [\fB\-\-temporary\fR]
+.br
+.I flashprog write-protect \ range  \ \ \fR<options> [\fB\-\-temporary\fR] <start>\fB,\fR<len>
+.br
+.I flashprog write-protect \ region   \ \fR<options> [\fB\-\-temporary\fR] <region-name>
+.sp
+Where generic <options> are:
+.RS 4
+\fB\-p\fR <programmername>[:<parameters>] [\fB\-c\fR <chipname>]
+.br
+[\fB\-V\fR[\fBV\fR[\fBV\fR]]] [\fB-o\fR <logfile>] [\fB\-h\fR]
+.RE
+.sp
+and layout <options> are:
+.RS 4
+[(\fB-l\fR|\fB--layout\fR) <layout-file>|\fB--fmap\fR <fmap-file>|\fB--fmap\fR|\fB--ifd\fR]
+.RE
+
+.SH DESCRIPTION
+.B flashprog-write-protect
+is a utility for reading and writing the write-protection settings
+of flash chips. Currently, it supports only block protection of SPI NOR
+chips.
+
+.SH OPERATIONS
+You can specify one operation per call.
+.B status
+is the default operation.
+.PP
+.B status
+.RS 4
+Shows the write-protection state, including the currently
+programmed protection range.
+.RE
+.PP
+.B list
+.RS 4
+Prints a list of write-protection ranges supported for the
+flash chip.
+.RE
+.PP
+.B disable
+.RS 4
+Disables write protection locks. The configured range usually
+stays as is, but it will be possible to override it.
+.RE
+.PP
+.B enable
+.RS 4
+Enables write protection locks. The write-protection range
+should be set before running the enable operation.
+.RE
+.PP
+.BR range " <start>,<len>"
+.RS 4
+Configures the protected range.
+.BR start " and " length
+specify the range in decimal, octal (\fB0\fR prefix),
+or hexadecimal (\fB0x\fR prefix) numbers of bytes.
+Any zero-length range will unprotect the entire flash
+(e.g. \fBrange 0,0\fR).
+.RE
+.PP
+.BR region " <region-name>"
+.RS 4
+Configures the protected range, matching a region of the loaded
+layout (from a file or flash, see the respective option-descriptions in
+.MR flashprog 8
+for possible layout sources).
+.RE
+
+.SH OPTIONS
+All operations require the
+.B -p/--programmer
+option to be used (please see
+.MR flashprog 8
+for more information on programmer support and parameters).
+.PP
+.BR \-p ", " \-\-programmer " <name>[" : "<parameter>[" , "<parameter>]...]"
+.RS 4
+Specify the programmer device. This is mandatory for all operations.
+Please see the
+.MR flashprog 8
+manual for a list of currently supported programmers and their parameters.
+.RE
+.PP
+.BR \-c ", " \-\-chip " <chipname>"
+.RS 4
+Probe only for the specified flash ROM chip. This option takes the chip name as
+printed by
+.B "flashprog \-L"
+without the vendor name as parameter. Please note that the chip name is
+case sensitive.
+.RE
+.PP
+.BR \-V ", " \-\-verbose
+.RS 4
+More verbose output. This option can be supplied multiple times
+(max. 3 times, i.e.
+.BR \-VVV )
+for even more debug output.
+.RE
+.PP
+.BR \-o ", " \-\-output " <logfile>"
+.RS 4
+Save the full debug log to
+.BR <logfile> .
+If the file already exists, it will be overwritten. This is the recommended
+way to gather logs from flashprog because they will be verbose even if the
+on-screen messages are not verbose and don't require output redirection.
+.RE
+.PP
+.BR \-h ", " \-\-help
+.RS 4
+Show a help text and exit.
+.RE
+.PP
+.RB ( -l | --layout ") <layout-file>, " --fmap-file " <fmap-file>, " --fmap ", " --ifd
+.RS 4
+Please see the
+.MR flashrom 8
+manual for information about layout files and other layout sources.
+.RE
+.PP
+.B \-\-temporary
+.RS 4
+When the
+.B \-\-temporary
+option is provided for any operation that alters the flash chip's
+configuration, flashprog will attempt to write a temporary
+value that is not stored to flash. This requires special support
+by the flash chip for a volatile write status register command.
+The new value will be lost upon reset of the flash chip. Hence,
+it is futile to use this with external programmers that toggle
+power to the flash chip (e.g. Dediprog).
+.RE
+
+.SH EXAMPLES
+To just print the current write-protection state of the internal
+BIOS flash:
+.sp
+.RS 2
+.B flashprog write-protect -p internal
+.sp
+.RE
+or
+.sp
+.RS 2
+.B flashprog write-protect status -p internal
+.sp
+.RE
+To temporarily enable the currently configured range:
+.sp
+.RS 2
+.B flashprog write-protect enable -p internal --temporary
+.RE
+
+.SH EXIT STATUS
+flashprog exits with 0 on success, 1 on most failures but with 3 if a call to mmap() fails.
+
+.SH REQUIREMENTS
+flashprog needs different access permissions for different programmers.
+See this section in the
+.MR flashprog 8
+manual for details.
+
+.SH BUGS
+You can report bugs, ask us questions or send success reports
+via our communication channels listed here:
+.URLB "https://www.flashprog.org/Contact" "" .
+.sp
+
+.SH LICENSE
+.B flashprog
+is covered by the GNU General Public License (GPL), version 2. Some files are
+additionally available under any later version of the GPL.
+
+.SH COPYRIGHT
+.br
+Please see the individual files.
+.PP
+This manual page was written by Nico Huber and is derived from the
+flashprog(8) manual. It is licensed under the terms of the GNU GPL
+(version 2 or later).
+
+.SH SEE ALSO
+.MR flashprog 8
diff --git a/flashprog.8.tmpl b/flashprog.8.tmpl
index d9a59f9..9ebcf60 100644
--- a/flashprog.8.tmpl
+++ b/flashprog.8.tmpl
@@ -46,12 +46,13 @@
 .SH SYNOPSIS
 Flashprog supports multiple command modes:
 .sp
-.B flashprog \fR([\fBprog\fR]|\fBconfig\fR|\fBcfg\fR)
+.BR flashprog " ([" prog ]| config | cfg | write-protect | wp )
 .sp
 With
 .B prog
 being the default and described in this manual. For the other commands, see
-.MR flashprog-config 8 .
+.MR flashprog-config 8 ", and"
+.MR flashprog-write-protect 8 .
 .sp
 .B flashprog \fR[\fB\-h\fR|\fB\-R\fR|\fB\-L\fR|\fB\-z\fR|
           \fB\-p\fR <programmername>[:<parameters>] [\fB\-c\fR <chipname>]
@@ -1738,4 +1739,5 @@
 It is licensed under the terms of the GNU GPL (version 2 or later).
 
 .SH SEE ALSO
-.MR flashprog-config 8
+.MR flashprog-config 8 ,
+.MR flashprog-write-protect 8
diff --git a/include/cli.h b/include/cli.h
index 69b0146..1fbdef6 100644
--- a/include/cli.h
+++ b/include/cli.h
@@ -15,6 +15,8 @@
 #ifndef FLASHPROG_CLI_H
 #define FLASHPROG_CLI_H
 
+#include <stdbool.h>
+
 #include "libflashprog.h"
 
 enum {
@@ -35,6 +37,12 @@
 	OPTION_CONFIG_GET,
 	OPTION_CONFIG_SET,
 	OPTION_CONFIG_VOLATILE,
+	OPTION_WP_STATUS,
+	OPTION_WP_SET_RANGE,
+	OPTION_WP_SET_REGION,
+	OPTION_WP_ENABLE,
+	OPTION_WP_DISABLE,
+	OPTION_WP_LIST,
 };
 
 struct log_args {
@@ -67,6 +75,7 @@
 
 int flashprog_classic_main(int argc, char *argv[]);
 int flashprog_config_main(int argc, char *argv[]);
+int flashprog_wp_main(int argc, char *argv[]);
 
 extern enum flashprog_log_level verbose_screen;
 extern enum flashprog_log_level verbose_logfile;
@@ -81,6 +90,6 @@
 };
 int getopt_command(int argc, char *const argv[], const struct opt_command *);
 
-void print_generic_options(void);
+void print_generic_options(bool layout_options);
 
 #endif
diff --git a/meson.build b/meson.build
index 7a3d35c..f87bac5 100644
--- a/meson.build
+++ b/meson.build
@@ -593,7 +593,7 @@
 config_manfile = configuration_data()
 config_manfile.set('VERSION', version)
 config_manfile.set('MAN_DATE', run_command('util/getversion.sh', '--man-date', check : true).stdout().strip())
-foreach man : [ 'flashprog.8', 'flashprog-config.8' ]
+foreach man : [ 'flashprog.8', 'flashprog-config.8', 'flashprog-write-protect.8' ]
   configure_file(
     input : man + '.tmpl',
     output : man,
@@ -609,6 +609,7 @@
     files(
       'cli.c',
       'cli_config.c',
+      'cli_wp.c',
       'cli_classic.c',
       'cli_common.c',
       'cli_output.c',