/*
 * Copyright (c) 2015, 2016, 2017, 2025 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * disked.c
 * Disk editor.
 */

#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <endian.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <fstab.h>
#include <inttypes.h>
#include <ioleast.h>
#include <libgen.h>
#include <locale.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <wchar.h>

#include <fsmarshall.h>

#include <mount/biosboot.h>
#include <mount/blockdevice.h>
#include <mount/devices.h>
#include <mount/ext2.h>
#include <mount/filesystem.h>
#include <mount/gpt.h>
#include <mount/harddisk.h>
#include <mount/mbr.h>
#include <mount/partition.h>
#include <mount/uuid.h>

__attribute__((format(printf, 1, 2)))
static char* print_string(const char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	char* ret;
	if ( vasprintf(&ret, format, ap) < 0 )
		ret = NULL;
	va_end(ap);
	return ret;
}

static bool verify_mountpoint(const char* mountpoint)
{
	size_t index = 0;
	if ( mountpoint[index++] != '/' )
		return false;
	while ( mountpoint[index] )
	{
		if ( mountpoint[index] == '.' )
		{
			index++;
			if ( mountpoint[index] == '.' )
				index++;
			if ( !mountpoint[index] || mountpoint[index] == '/' )
				return false;
		}
		while ( mountpoint[index] && mountpoint[index] != '/' )
			index++;
		while ( mountpoint[index] == '/' )
			index++;
	}
	return true;
}

static void simplify_mountpoint(char* mountpoint)
{
	bool slash_pending = false;
	size_t out = 0;
	for ( size_t in = 0; mountpoint[in]; in++ )
	{
		if ( mountpoint[in] == '/' )
		{
			if ( out == 0 )
			{
				mountpoint[out++] = '/';
				while ( mountpoint[in] == '/' )
					in++;
				in--;
				continue;
			}
			slash_pending = true;
			continue;
		}
		if ( slash_pending )
		{
			mountpoint[out++] = '/';
			slash_pending = false;
		}
		mountpoint[out++] = mountpoint[in];
	}
	mountpoint[out] = '\0';
}

static char* format_bytes_amount(uintmax_t num_bytes)
{
	uintmax_t value = num_bytes;
	uintmax_t value_fraction = 0;
	uintmax_t exponent = 1024;
	char suffixes[] = { '\0', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' };
	size_t num_suffixes = sizeof(suffixes) / sizeof(suffixes[0]);
	size_t suffix_index = 0;
	while ( exponent <= value && suffix_index + 1 < num_suffixes)
	{
		value_fraction = value % exponent;
		value /= exponent;
		suffix_index++;
	}
	char suffix_str[] = { suffixes[suffix_index],  'i', 'B', '\0' };
	char value_fraction_char = '0' + (value_fraction / (1024 / 10 + 1)) % 10;
	char* result;
	if ( asprintf(&result, "%ju.%c %s", value, value_fraction_char,
	              suffix_str) < 0 )
		return NULL;
	return result;
}

static size_t string_display_length(const char* str)
{
	size_t display_length = 0;
	mbstate_t ps;
	memset(&ps, 0, sizeof(ps));
	while ( true )
	{
		wchar_t wc;
		size_t amount = mbrtowc(&wc, str, SIZE_MAX, &ps);
		if ( amount == 0 )
			break;
		if ( amount == (size_t) -1 || amount == (size_t) -2 )
		{
			display_length++;
			str++;
			memset(&ps, 0, sizeof(ps));
			continue;
		}
		int width = wcwidth(wc);
		if ( width < 0 )
			width = 0;
		if ( SIZE_MAX - display_length < (size_t) width )
			display_length = SIZE_MAX;
		else
			display_length += (size_t) width;
		str += amount;
	}
	return display_length;
}

static void split_arguments(char* cmd,
                            size_t* argc_ptr,
                            char** argv,
                            size_t argc_max)
{
	size_t argc = 0;
	size_t cmd_offset = 0;
	while ( cmd[cmd_offset] )
	{
		while ( cmd[cmd_offset] && isspace((unsigned char) cmd[cmd_offset]) )
		{
			cmd[cmd_offset] = '\0';
			cmd_offset++;
		}
		if ( !cmd[cmd_offset] )
			break;
		char* out = cmd + cmd_offset;
		size_t out_offset = 0;
		if ( argc < argc_max )
			argv[argc++] = out;
		bool escape = false;
		bool quote_single = false;
		bool quote_double = false;
		while ( cmd[cmd_offset] )
		{
			if ( !escape && !quote_single && cmd[cmd_offset] == '\\' )
			{
				cmd_offset++;
				escape = true;
			}
			else if ( !escape && !quote_double && cmd[cmd_offset] == '\'' )
			{
				cmd_offset++;
				quote_single = !quote_single;
			}
			else if ( !escape && !quote_single && cmd[cmd_offset] == '"' )
			{
				cmd_offset++;
				quote_double = !quote_double;
			}
			else if ( !(escape || quote_single || quote_double) &&
				      isspace((unsigned char) cmd[cmd_offset]) )
			{
				break;
			}
			else
			{
				out[out_offset++] = cmd[cmd_offset++];
				escape = false;
			}
		}
		char last_c = cmd[cmd_offset];
		out[out_offset] = '\0';
		if ( !last_c )
			break;
		cmd[cmd_offset++] = '\0';
	}
	*argc_ptr = argc;
}

static const char* device_name(const char* name)
{
	if ( !strncmp(name, "/dev/", strlen("/dev/")) )
		return name + strlen("/dev/");
	return name;
}

static bool interactive;

static void display_rows_columns(char* (*format)(void*, size_t, size_t),
                                 void* ctx, size_t rows, size_t columns)
{
	size_t* widths = (size_t*) reallocarray(NULL, sizeof(size_t), columns);
	assert(widths);
	for ( size_t c = 0; c < columns; c++ )
		widths[c] = 0;
	for ( size_t r = 0; r < rows; r++ )
	{
		for ( size_t c = 0; c < columns; c++ )
		{
			char* entry = format(ctx, r, c);
			assert(entry);
			size_t width = string_display_length(entry);
			if ( widths[c] < width )
				widths[c] = width;
			free(entry);
		}
	}
	for ( size_t r = 0; r < rows; r++ )
	{
		for ( size_t c = 0; c < columns; c++ )
		{
			char* entry = format(ctx, r, c);
			assert(entry);
			size_t width = string_display_length(entry);
			printf("%s", entry);
			if ( c + 1 != columns )
			{
				for ( size_t i = width; i < widths[c]; i++ )
					putchar(' ');
				printf("  ");
			}
			free(entry);
		}
		printf("\n");
	}
	free(widths);
}

static void text(const char* str)
{
	fflush(stdout);
	struct winsize ws;
	struct wincurpos wcp;
	if ( tcgetwinsize(1, &ws) < 0 || tcgetwincurpos(1, &wcp) < 0 )
	{
		printf("%s", str);
		return;
	}
	size_t columns = ws.ws_col;
	size_t column = wcp.wcp_col;
	bool blank = false;
	while ( str[0] )
	{
		if ( str[0] == '\e' )
		{
			size_t length = 1;
			while ( str[length] == '[' ||
			        str[length] == ';' ||
			        ('0' <= str[length] && str[length] <= '9') )
				length++;
			if ( 64 <= str[length] && str[length] <= 126 )
				length++;
			fwrite(str, 1, length, stdout);
			str += length;
			continue;
		}
		else if ( str[0] == '\n' )
		{
			putchar('\n');
			blank = false;
			column = 0;
			str++;
			continue;
		}
		else if ( isblank((unsigned char) str[0]) )
		{
			blank = true;
			str++;
			continue;
		}
		size_t word_length = 0;
		size_t word_columns = 0;
		mbstate_t ps = { 0 };
		while ( str[word_length] &&
		        str[word_length] != '\n' &&
		        !isblank((unsigned char) str[word_length]) )
		{
			wchar_t wc;
			size_t amount = mbrtowc(&wc, str + word_length, SIZE_MAX, &ps);
			if ( amount == (size_t) -2 )
				 break;
			if ( amount == (size_t) -1 )
			{
				memset(&ps, 0, sizeof(ps));
				amount = 1;
			}
			if ( amount == (size_t) 0 )
				break;
			word_length += amount;
			int width = wcwidth(wc);
			if ( width < 0 )
				continue;
			word_columns += width;
		}
		if ( (column && blank ? 1 : 0) + word_columns <= columns - column )
		{
			if ( column && blank )
			{
				putchar(' ');
				column++;
			}
			blank = false;
			fwrite(str, 1, word_length, stdout);
			column += word_columns;
			if ( column == columns )
				column = 0;
		}
		else
		{
			if ( column != 0 && column != columns )
				putchar('\n');
			column = 0;
			blank = false;
			fwrite(str, 1, word_length, stdout);
			column += word_columns;
			column %= columns;
		}
		str += word_length;
	}
	fflush(stdout);
}

static void textf(const char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	char* str;
	int len = vasprintf(&str, format, ap);
	va_end(ap);
	if ( len < 0 )
	{
		vprintf(format, ap);
		return;
	}
	text(str);
	free(str);
}

static void prompt(char* buffer,
                   size_t buffer_size,
                   const char* question,
                   const char* answer)
{
	if ( !interactive )
		errx(1, "Cannot ask question in non-interactive mode: %s", question);
	while ( true )
	{
		printf("\e[1m");
		fflush(stdout);
		text(question);
		if ( answer )
			printf(" [%s] ", answer);
		else
			printf(" ");
		fflush(stdout);
		fgets(buffer, buffer_size, stdin);
		printf("\e[22m");
		fflush(stdout);
		size_t buffer_length = strlen(buffer);
		if ( buffer_length && buffer[buffer_length-1] == '\n' )
			buffer[--buffer_length] = '\0';
		while ( buffer_length && buffer[buffer_length-1] == ' ' )
			buffer[--buffer_length] = '\0';
		if ( !strcmp(buffer, "") )
		{
			if ( !answer )
				continue;
			strlcpy(buffer, answer, buffer_size);
		}
		break;
	}
}

static bool remove_partition_device(const char* path)
{
	// TODO: Refuse to do this if the partition are currently in use.
	if ( unmount(path, UNMOUNT_NOFOLLOW) < 0 && errno != ENOMOUNT )
	{
		warn("unmount: %s", path);
		return false;
	}
	if ( unlink(path) < 0 )
	{
		warn("unlink: %s", path);
		return false;
	}
	return true;
}

static void remove_partition_devices(const char* path)
{
	const char* name = path;
	for ( size_t i = 0; path[i]; i++ )
		if ( path[i] == '/' )
			name = path + i + 1;
	size_t name_length = strlen(name);
	char* dir_path = strdup(path);
	if ( !dir_path )
	{
		warn("malloc");
		return; // TODO: Error.
	}
	dirname(dir_path);
	DIR* dir = opendir(dir_path);
	struct dirent* entry;
	while ( (errno = 0, entry = readdir(dir)) )
	{
		if ( strncmp(entry->d_name, name, name_length) != 0 )
			continue;
		if ( entry->d_name[name_length] != 'p' )
			continue;
		bool all_digits = true;
		for ( size_t i = name_length + 1; all_digits && entry->d_name[i]; i++ )
			if ( !('0' <= entry->d_name[i] && entry->d_name[i] <= '9') )
				all_digits = false;
		if ( !all_digits )
			continue;
		// TODO: Refuse to do this if the partitions are currently in use.
		if ( unmountat(dirfd(dir), entry->d_name, UNMOUNT_NOFOLLOW) < 0 &&
		     errno != ENOMOUNT )
		{
			warn("unmount: %s/%s", dir_path, entry->d_name);
			// TODO: Warn/error.
		}
		if ( unlinkat(dirfd(dir), entry->d_name, 0) < 0 )
		{
			warn("unlink: %s/%s", dir_path, entry->d_name);
			// TODO: Warn/error.
		}
		rewinddir(dir);
	}
	if ( errno )
	{
		warn("readdir: %s", dir_path);
		// TODO: Error.
	}
	closedir(dir);
	free(dir_path);
}

static int harddisk_compare(const void* a_ptr, const void* b_ptr)
{
	struct harddisk* a = *((struct harddisk**) a_ptr);
	struct harddisk* b = *((struct harddisk**) b_ptr);
	if ( a->writable && !b->writable )
		return -1;
	if ( !a->writable && b->writable )
		return 1;
	return strcmp(a->path, b->path);
}

struct device_area
{
	off_t start;
	off_t length;
	off_t extended_start;
	off_t ebr_off;
	off_t ebr_move_off;
	struct partition* p;
	char* filesystem;
	char* mountpoint;
	size_t ebr_index;
	size_t ebr_move_index;
	bool inside_extended;
};

static int device_area_compare_start(const void* a_ptr, const void* b_ptr)
{
	const struct device_area* a = (const struct device_area*) a_ptr;
	const struct device_area* b = (const struct device_area*) b_ptr;
	if ( a->start < b->start )
		return -1;
	if ( a->start > b->start )
		return 1;
	return 0;
}

static bool match_fstab_device(const char* device, struct blockdevice* bdev)
{
	if ( strncmp(device, "UUID=", strlen("UUID=")) == 0 )
	{
		device += strlen("UUID=");
		if ( !bdev->fs )
			return false;
		if ( !(bdev->fs->flags & FILESYSTEM_FLAG_UUID) )
			return false;
		if ( !uuid_validate(device) )
			return false;
		unsigned char uuid[16];
		uuid_from_string(uuid, device);
		if ( memcmp(bdev->fs->uuid, uuid, 16) != 0 )
			return false;
		return true;
	}
	else if ( bdev->p && !strcmp(device, bdev->p->path) )
		return true;
	else if ( !bdev->p && bdev->hd && !strcmp(device, bdev->hd->path) )
		return true;
	return false;
}

struct rewrite
{
	FILE* in;
	FILE* out;
	const char* in_path;
	char* out_path;
};

bool rewrite_begin(struct rewrite* rewr, const char* in_path)
{
	memset(rewr, 0, sizeof(*rewr));
	rewr->in_path = in_path;
	if ( !(rewr->in = fopen(rewr->in_path, "r")) )
		return false;
	if ( asprintf(&rewr->out_path, "%s.XXXXXX", in_path) < 0 )
		return fclose(rewr->in), false;
	int out_fd = mkstemp(rewr->out_path);
	if ( out_fd < 0 )
		return free(rewr->out_path), fclose(rewr->in), false;
	if ( !(rewr->out = fdopen(out_fd, "w")) )
		return close(out_fd), free(rewr->out_path), fclose(rewr->in), false;
	return true;
}

void rewrite_abort(struct rewrite* rewr)
{
	fclose(rewr->in);
	fclose(rewr->out);
	unlink(rewr->out_path);
	free(rewr->out_path);
}

bool rewrite_finish(struct rewrite* rewr)
{
	struct stat in_st;
	if ( ferror(rewr->out) || fflush(rewr->out) == EOF )
		return rewrite_abort(rewr), false;
	if ( fstat(fileno(rewr->in), &in_st) < 0 )
		return rewrite_abort(rewr), false;
	mode_t mode = in_st.st_mode & 0777;
	if ( in_st.st_uid != getuid() || in_st.st_gid != getgid() )
		mode &= ~getumask();
	if ( fchmod(fileno(rewr->out), mode) < 0 )
		return rewrite_abort(rewr), false;
	if ( rename(rewr->out_path, rewr->in_path) < 0 )
		return rewrite_abort(rewr), false;
	fclose(rewr->in);
	fclose(rewr->out);
	free(rewr->out_path);
	return true;
}

static bool quitting;
static struct harddisk** hds;
static size_t hds_count;
static struct harddisk* current_hd;
static enum partition_table_type current_pt_type;
static struct partition_table* current_pt;
static size_t current_areas_count;
static struct device_area* current_areas;
static const char* fstab_path = "/etc/fstab";

__attribute__((format(printf, 1, 2)))
static void command_error(const char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	if ( !interactive )
		verr(1, format, ap);
	vfprintf(stderr, format, ap);
	fprintf(stderr, ": %s\n", strerror(errno));
	va_end(ap);
}

__attribute__((format(printf, 1, 2)))
static void command_errorx(const char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	if ( !interactive )
		verrx(1, format, ap);
	vfprintf(stderr, format, ap);
	fprintf(stderr, "\n");
	va_end(ap);
}

// TODO: Finish this and add decimal support.
static bool parse_disk_quantity(off_t* out, const char* string, off_t max)
{
	if ( *string && isspace((unsigned char) *string) )
		string++;
	const char* end;
	bool from_end = false;
	errno = 0;
	intmax_t value = strtoimax(string, (char**) &end, 10);
	if ( value == INTMAX_MIN || errno )
	{
		if ( !errno )
			errno = ERANGE;
		command_error("Parsing `%s'", string);
		return false;
	}
	if ( value < 0 )
	{
		value = -value;
		from_end = true;
	}
	string = end;
	if ( *string && isspace((unsigned char) *string) )
		string++;
	if ( *string == '.' )
	{
		string++;
		// TODO: Support this!
		if ( strtoimax(string, (char**) &end, 10) < 0 )
			return false;
		string = end;
	}
	if ( *string == '%' )
	{
		string++;
		if ( 100 < value )
			return false;
		if ( value == 100 )
			value = max;
		else
			value = (max * value) / 100;
	}
	else if ( *string == 'b' || *string == 'B' )
	{
	}
	else if ( *string == 'k' || *string == 'K' )
	{
		string++;
		if ( *string == 'i' )
			string++;
		if ( *string == 'b' || *string == 'B' )
			string++;
		value = value * 1024LL;
	}
	else if ( *string == 'm' || *string == 'M' ||
	          !*string || isspace((unsigned char) *string) )
	{
		if ( *string )
			string++;
		if ( *string == 'i' )
			string++;
		if ( *string == 'b' || *string == 'B' )
			string++;
		value = value * (1024LL * 1024LL);
	}
	else if ( *string == 'g' || *string == 'G' )
	{
		string++;
		if ( *string == 'i' )
			string++;
		if ( *string == 'b' || *string == 'B' )
			string++;
		value = value * (1024LL * 1024LL * 1024LL);
	}
	else if ( *string == 't' || *string == 'T' )
	{
		string++;
		if ( *string == 'i' )
			string++;
		if ( *string == 'b' || *string == 'B' )
			string++;
		value = value * (1024LL * 1024LL * 1024LL * 1024LL);
	}
	else if ( *string == 'p' || *string == 'P' )
	{
		string++;
		if ( *string == 'i' )
			string++;
		if ( *string == 'b' || *string == 'B' )
			string++;
		value = value * (1024LL * 1024LL * 1024LL * 1024LL * 1024LL);
	}
	if ( *string && isspace((unsigned char) *string) )
		string++;
	if ( *string )
		return false;
	if ( max < value )
		return false;
	if ( from_end )
		value = max - value;
	uintmax_t uvalue = value;
	uintmax_t mask = ~(UINTMAX_C(1048576) - 1);
	uvalue = -(-value & mask);
	value = (off_t) uvalue;
	return *out = value, true;
}

static bool lookup_fstab_by_blockdevice(struct fstab* out_fsent,
                                        char** out_storage,
                                        struct blockdevice* bdev)
{
	FILE* fstab_fp = fopen(fstab_path, "r");
	if ( !fstab_fp )
		return false;
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	while ( 0 < (line_length = getline(&line, &line_size, fstab_fp)) )
	{
		if ( line[line_length - 1] == '\n' )
			line[--line_length] = '\0';
		if ( !scanfsent(line, out_fsent) )
			continue;
		if ( match_fstab_device(out_fsent->fs_spec, bdev) )
		{
			*out_storage = line;
			fclose(fstab_fp);
			return true;
		}
	}
	free(line);
	fclose(fstab_fp);
	return false;
}

static bool remove_blockdevice_from_fstab(struct blockdevice* bdev)
{
	struct rewrite rewr;
	if ( !rewrite_begin(&rewr, fstab_path) )
	{
		if ( errno == ENOENT )
			return true;
		return false;
	}
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	while ( 0 < (line_length = getline(&line, &line_size, rewr.in)) )
	{
		if ( line[line_length - 1] == '\n' )
			line[--line_length] = '\0';
		char* dup = strdup(line);
		if ( !dup )
			return rewrite_abort(&rewr), false;
		struct fstab fsent;
		if ( !scanfsent(dup, &fsent) ||
		     !match_fstab_device(fsent.fs_spec, bdev) )
			fprintf(rewr.out, "%s\n", line);
		free(dup);
	}
	free(line);
	if ( ferror(rewr.in) )
		return rewrite_abort(&rewr), false;
	return rewrite_finish(&rewr);
}

static void print_blockdevice_fsent(FILE* fp,
                                    struct blockdevice* bdev,
                                    const char* mountpoint)
{
	char uuid[5 + UUID_STRING_LENGTH + 1];
	const char* spec = bdev->p ? bdev->p->path : bdev->hd->path;
	if ( bdev->fs->flags & FILESYSTEM_FLAG_UUID )
	{
		strcpy(uuid, "UUID=");
		uuid_to_string(bdev->fs->uuid, uuid + 5);
		spec = uuid;
	}
	fprintf(fp, "%s %s %s %s %i %i\n",
	        spec,
	        mountpoint,
	        bdev->fs->fstype_name,
		    "rw",
		    1,
		    !strcmp(mountpoint, "/") ? 1 : 2);
}

static bool add_blockdevice_to_fstab(struct blockdevice* bdev,
                                     const char* mountpoint)
{
	assert(bdev->fs);
	struct rewrite rewr;
	if ( !rewrite_begin(&rewr, fstab_path) )
	{
		if ( errno == ENOENT )
		{
			FILE* fp = fopen(fstab_path, "w");
			if ( !fp )
				return false;
			print_blockdevice_fsent(fp, bdev, mountpoint);
			if ( ferror(fp) || fflush(fp) == EOF )
				return fclose(fp), false;
			fclose(fp);
			return true;
		}
		return false;
	}
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	bool found = false;
	while ( 0 < (line_length = getline(&line, &line_size, rewr.in)) )
	{
		if ( line[line_length - 1] == '\n' )
			line[--line_length] = '\0';
		char* dup = strdup(line);
		if ( !dup )
			return rewrite_abort(&rewr), false;
		struct fstab fsent;
		if ( !scanfsent(dup, &fsent) )
		{
			fprintf(rewr.out, "%s\n", line);
		}
		else if ( match_fstab_device(fsent.fs_spec, bdev) )
		{
			fprintf(rewr.out, "%s %s %s %s %i %i\n",
			        fsent.fs_spec,
			        mountpoint,
			        fsent.fs_vfstype,
			        fsent.fs_mntops,
			        fsent.fs_freq,
			        fsent.fs_passno);
			found = true;
		}
		else if ( !strcmp(fsent.fs_file, mountpoint) )
		{
			// Remove conflicting mountpoint.
		}
		else
		{
			fprintf(rewr.out, "%s\n", line);
		}
		free(dup);
	}
	free(line);
	if ( ferror(rewr.in) )
		return rewrite_abort(&rewr), false;
	if ( !found )
		print_blockdevice_fsent(rewr.out, bdev, mountpoint);
	return rewrite_finish(&rewr);
}

static void unscan_partition(struct partition* p)
{
	struct blockdevice* bdev = &p->bdev;
	filesystem_release(bdev->fs);
	bdev->fs_error = FILESYSTEM_ERROR_NONE;
}

static void unscan_device(void)
{
	if ( !current_hd )
		return;
	for ( size_t i = 0; i < current_areas_count; i++ )
	{
		free(current_areas[i].filesystem);
		free(current_areas[i].mountpoint);
	}
	current_areas_count = 0;
	free(current_areas);
	current_areas = NULL;
	if ( current_pt )
	{
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
			unscan_partition(current_pt->partitions[i]);
		partition_table_release(current_pt);
	}
	current_pt = NULL;
	current_pt_type = PARTITION_TABLE_TYPE_UNKNOWN;
	current_hd->bdev.pt_error = PARTITION_ERROR_NONE;
}

static void scan_partition(struct partition* p)
{
	unscan_partition(p);
	struct blockdevice* bdev = &p->bdev;
	bdev->fs_error = blockdevice_inspect_filesystem(&bdev->fs, bdev);
	if ( bdev->fs_error == FILESYSTEM_ERROR_ABSENT ||
	     bdev->fs_error == FILESYSTEM_ERROR_UNRECOGNIZED )
		return;
	if ( bdev->fs_error != FILESYSTEM_ERROR_NONE )
		return command_errorx("Scanning `%s': %s", device_name(p->path),
		                      filesystem_error_string(bdev->fs_error));
}

static void scan_device(void)
{
	if ( !current_hd )
		return;
	unscan_device();
	struct blockdevice* bdev = &current_hd->bdev;
	if ( !blockdevice_probe_partition_table_type(&current_pt_type, bdev) )
	{
		// TODO: Try probe for a filesystem here to see if one covers the whole
		//       device.
		command_error("Scanning `%s'", device_name(current_hd->path));
		current_hd = NULL;
		current_pt_type = PARTITION_TABLE_TYPE_UNKNOWN;
		return;
	}
	bdev->pt_error = blockdevice_get_partition_table(&current_pt, bdev);
	if ( bdev->pt_error != PARTITION_ERROR_NONE )
	{
		if ( bdev->pt_error != PARTITION_ERROR_ABSENT &&
		     bdev->pt_error != PARTITION_ERROR_UNRECOGNIZED )
			command_errorx("Scanning `%s': %s", device_name(current_hd->path),
			               partition_error_string(bdev->pt_error));
		partition_table_release(current_pt);
		current_pt = NULL;
		return;
	}
	current_pt_type = current_pt->type;
	// TODO: In case of GPT, verify the header is supported for write (version
	//       check, just using it blindly is safe for read compatibility, but
	//       we need to refuse updating the GPT if uses extensions). Then after
	//       deciding this, check this condition in all commands that modify
	//       the partition table.
	for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		scan_partition(current_pt->partitions[i]);
	size_t areas_length;
	size_t partitions_count = current_pt->partitions_count;
	if ( __builtin_mul_overflow(2, partitions_count, &areas_length) ||
	     __builtin_add_overflow(1, areas_length, &areas_length ) )
	{
		errno = EOVERFLOW;
		command_error("Scanning `%s'", device_name(current_hd->path));
		return;
	}
	if ( current_pt_type == PARTITION_TABLE_TYPE_MBR )
	{
		struct mbr_partition_table* mbrpt =
			(struct mbr_partition_table*) current_pt->raw_partition_table;
		areas_length += mbrpt->ebr_chain_count * 2;
	}
	current_areas = (struct device_area*)
		reallocarray(NULL, sizeof(struct device_area), areas_length);
	if ( !current_areas )
	{
		command_error("malloc");
		partition_table_release(current_pt);
		current_pt = NULL;
		return;
	}
	struct device_area* sort_areas =
		current_areas + areas_length - current_pt->partitions_count;
	for ( size_t i = 0; i < current_pt->partitions_count; i++ )
	{
		struct device_area* area = &sort_areas[i];
		struct partition* p = current_pt->partitions[i];
		memset(area, 0, sizeof(*area));
		area->start = p->start;
		area->length = p->length;
		area->p = p;
		area->inside_extended = p->type == PARTITION_TYPE_LOGICAL;
	}
	qsort(sort_areas, current_pt->partitions_count, sizeof(struct device_area),
	      device_area_compare_start);
	off_t last_end = current_pt->usable_start;
	current_areas_count = 0;
	for ( size_t i = 0; i < current_pt->partitions_count; i++ )
	{
		struct device_area* area = &sort_areas[i];
		if ( area->p->type == PARTITION_TYPE_LOGICAL )
			continue;
		if ( last_end < area->start )
		{
			struct device_area* hole = &current_areas[current_areas_count++];
			memset(hole, 0, sizeof(*hole));
			hole->start = last_end;
			hole->length = area->start - last_end;
		}
		current_areas[current_areas_count++] = *area;
		last_end = area->start + area->length;
		if ( area->p->type == PARTITION_TYPE_EXTENDED &&
		     current_pt_type == PARTITION_TABLE_TYPE_MBR )
		{
			off_t extended_start = area->start;
			off_t extended_end = area->start + area->length;
			struct mbr_partition_table* mbrpt =
				(struct mbr_partition_table*) current_pt->raw_partition_table;
			assert(1 <= mbrpt->ebr_chain_count);
			size_t ebr_i = 0;
			size_t larea_i = i + 1;
			size_t new_part_ebr_i = 0;
			off_t offset = extended_start;
			while ( true )
			{
				struct mbr_ebr_link* ebr = NULL;
				if ( ebr_i < mbrpt->ebr_chain_count )
					ebr = &mbrpt->ebr_chain[ebr_i];
				struct device_area* larea = NULL;
				if ( larea_i < current_pt->partitions_count &&
				     sort_areas[larea_i].p->type == PARTITION_TYPE_LOGICAL )
					larea = &sort_areas[larea_i];
				off_t ebr_start = ebr ? ebr->offset : extended_end;
				off_t larea_start = larea ? larea->start : extended_end;
				off_t dist_ebr = ebr_start - offset;
				off_t dist_larea = larea_start - offset;
				off_t dist = dist_larea < dist_ebr ? dist_larea : dist_ebr;
				bool next_is_larea = larea && dist_larea < dist_ebr;
				bool next_is_ebr = ebr && dist_ebr < dist_larea;
				if ( next_is_ebr )
				{
					struct mbr_partition p;
					memcpy(&p, ebr->ebr.partitions[0], sizeof(p));
					mbr_partition_decode(&p);
					new_part_ebr_i = ebr_i;
					ebr_i++;
					continue;
				}
				if ( current_hd->logical_block_size < dist )
				{
					assert(ebr_i);
					struct device_area* hole = &current_areas[current_areas_count];
					memset(hole, 0, sizeof(*hole));
					hole->ebr_index = new_part_ebr_i;
					hole->ebr_off = offset;
					hole->start = offset + current_hd->logical_block_size;
					hole->length = offset + dist - hole->start;
					hole->extended_start = extended_start;
					if ( next_is_larea )
					{
						hole->length -= current_hd->logical_block_size;
						hole->ebr_move_off = hole->start + hole->length;
						assert(ebr_i);
						hole->ebr_move_index = ebr_i - 1;
					}
					hole->inside_extended = true;
					if ( 0 <= hole->length )
						current_areas_count++;
				}
				if ( next_is_larea )
				{
					current_areas[current_areas_count++] = *larea;
					offset = larea->start + larea->length;
					larea_i++;
					new_part_ebr_i++;
				}
				else
					break;
			}
			i = larea_i - 1;
		}
	}
	if ( last_end < current_pt->usable_end )
	{
		struct device_area* hole = &current_areas[current_areas_count++];
		memset(hole, 0, sizeof(*hole));
		hole->start = last_end;
		hole->length = current_pt->usable_end - last_end;
	}
	size_t new_areas_count = 0;
	for ( size_t i = 0; i < current_areas_count; i++ )
	{
		if ( !current_areas[i].p )
		{
			uintmax_t mask = ~(UINTMAX_C(1048576) - 1);
			off_t start = current_areas[i].start;
			uintmax_t aligned = (uintmax_t) start;
			aligned = -(-aligned & mask);
			off_t start_aligned = (off_t) aligned;
			if ( current_areas[i].length < start_aligned - start )
				continue;
			current_areas[i].start = start_aligned;
			current_areas[i].length -= start_aligned - start;
			current_areas[i].length &= mask;
			if ( current_areas[i].length == 0 )
				continue;
		}
		if ( new_areas_count != i )
			current_areas[new_areas_count] = current_areas[i];
		new_areas_count++;
	}
	current_areas_count = new_areas_count;
}

static void switch_device(struct harddisk* hd)
{
	if ( current_hd )
	{
		unscan_device();
		current_hd = NULL;
	}
	if ( !(current_hd = hd) )
		return;
	scan_device();
}

static bool lookup_harddisk_by_string(struct harddisk** out,
                                      const char* argv0,
                                      const char* name)
{
	for ( size_t i = 0; i < hds_count; i++ )
	{
		char buf[sizeof(i) * 3];
		snprintf(buf, sizeof(buf), "%zu", i);
		if ( strcmp(name, hds[i]->path) != 0 &&
		     strcmp(name, device_name(hds[i]->path)) != 0 &&
		     strcmp(name, buf) != 0 )
			continue;
		*out = hds[i];
		return true;
	}
	command_errorx("%s: No such device `%s'", argv0, name);
	return false;
}

static bool lookup_partition_by_string(struct partition** out,
                                       const char* argv0,
                                       const char* numstr)
{
	char* end;
	unsigned long part_index = strtoul(numstr, &end, 10);
	if ( *end )
	{
		command_errorx("%s: Invalid partition number `%s'", argv0, numstr);
		return false;
	}
	struct partition* part = NULL;
	for ( size_t i = 0; i < current_pt->partitions_count; i++ )
	{
		if ( current_pt->partitions[i]->index == part_index )
		{
			part = current_pt->partitions[i];
			break;
		}
	}
	if ( !part)
	{
		command_errorx("%s: No such partition `%sp%lu'", argv0,
		               device_name(current_hd->path), part_index);
		return false;
	}
	return *out = part, true;
}

bool gpt_update(const char* argv0, struct gpt_partition_table* gptpt)
{
	size_t header_size;
	blksize_t logical_block_size = current_hd->logical_block_size;
	struct gpt pri_gpt;
	memcpy(&pri_gpt, &gptpt->gpt, sizeof(pri_gpt));
	gpt_decode(&pri_gpt);
	size_t rpt_size = (size_t) pri_gpt.number_of_partition_entries *
	                  (size_t) pri_gpt.size_of_partition_entry;
	uint32_t rpt_checksum = gpt_crc32(gptpt->rpt, rpt_size);
	uint64_t pri_gpt_lba = 1;
	off_t pri_gpt_off = (off_t) logical_block_size * (off_t) pri_gpt_lba;
	uint64_t alt_gpt_lba = pri_gpt.alternate_lba;
	off_t alt_gpt_off = (off_t) logical_block_size * (off_t) alt_gpt_lba;
	struct gpt alt_gpt;
	if ( preadall(current_hd->fd, &alt_gpt, sizeof(alt_gpt),
	              alt_gpt_off) < sizeof(alt_gpt) )
	{
		command_error("%s: %s: read", argv0, device_name(current_hd->path));
		return false;
	}
	gpt_decode(&alt_gpt);
	// TODO: Validate the alternate gpt.
	uint64_t alt_rpt_lba = alt_gpt.partition_entry_lba;
	off_t alt_rpt_off = (off_t) logical_block_size * (off_t) alt_rpt_lba;
	if ( pwriteall(current_hd->fd, gptpt->rpt, rpt_size,
	               alt_rpt_off) < rpt_size )
	{
		command_error("%s: %s: write", argv0, device_name(current_hd->path));
		scan_device();
		return false;
	}
	memcpy(&alt_gpt, &gptpt->gpt, sizeof(alt_gpt));
	gpt_decode(&alt_gpt);
	alt_gpt.header_crc32 = 0;
	alt_gpt.my_lba = alt_gpt_lba;
	alt_gpt.alternate_lba = pri_gpt_lba;
	alt_gpt.partition_entry_lba = alt_rpt_lba;
	alt_gpt.partition_entry_array_crc32 = rpt_checksum;
	header_size = alt_gpt.header_size;
	gpt_encode(&alt_gpt);
	alt_gpt.header_crc32 = htole32(gpt_crc32(&alt_gpt, header_size));
	if ( pwriteall(current_hd->fd, &alt_gpt, sizeof(alt_gpt),
	               alt_gpt_off) < sizeof(alt_gpt) )
	{
		command_error("%s: %s: write", argv0, device_name(current_hd->path));
		scan_device();
		return false;
	}
	if ( fsync(current_hd->fd) < 0 )
	{
		command_error("%s: %s: sync", argv0, device_name(current_hd->path));
		scan_device();
		return false;
	}
	uint64_t pri_rpt_lba = pri_gpt.partition_entry_lba;
	off_t pri_rpt_off = (off_t) logical_block_size * (off_t) pri_rpt_lba;
	if ( pwriteall(current_hd->fd, gptpt->rpt, rpt_size,
	               pri_rpt_off) < rpt_size )
	{
		command_error("%s: %s: write", argv0, device_name(current_hd->path));
		scan_device();
		return false;
	}
	memcpy(&pri_gpt, &gptpt->gpt, sizeof(pri_gpt));
	gpt_decode(&pri_gpt);
	pri_gpt.header_crc32 = 0;
	pri_gpt.my_lba = pri_gpt_lba;
	pri_gpt.alternate_lba = alt_gpt_lba;
	pri_gpt.partition_entry_lba = pri_rpt_lba;
	pri_gpt.partition_entry_array_crc32 = rpt_checksum;
	header_size = pri_gpt.header_size;
	gpt_encode(&pri_gpt);
	pri_gpt.header_crc32 = htole32(gpt_crc32(&pri_gpt, header_size));
	if ( pwriteall(current_hd->fd, &pri_gpt, sizeof(pri_gpt),
	               pri_gpt_off) < sizeof(pri_gpt) )
	{
		command_error("%s: %s: write", argv0, device_name(current_hd->path));
		scan_device();
		return false;
	}
	if ( fsync(current_hd->fd) < 0 )
	{
		command_error("%s: %s: sync", argv0, device_name(current_hd->path));
		scan_device();
		return false;
	}
	return true;
}

bool create_partition_device(const char* argv0, const struct partition* p)
{
	int mountfd = open(p->path, O_RDONLY | O_CREAT | O_EXCL);
	if ( mountfd < 0 )
	{
		command_error("%s: %s", argv0, p->path);
		return false;
	}
	int partfd = mkpartition(current_hd->fd, p->start, p->length);
	if ( partfd < 0 )
	{
		close(mountfd);
		command_error("%s: mkpartition: %s", argv0, p->path);
		return false;
	}
	if ( fsm_fsbind(partfd, mountfd, 0) < 0 )
	{
		command_error("%s: fsbind: %s", argv0, p->path);
		return false;
	}
	close(partfd);
	close(mountfd);
	return true;
}

struct command
{
	const char* name;
	void (*function)(size_t argc, char** argv);
	const char* flags;
};

static const struct command commands[];
static const size_t commands_count;

static void on_device(size_t argc, char** argv)
{
	if ( argc < 2 )
	{
		printf("%s\n", current_hd ? device_name(current_hd->path) : "none");
		return;
	}
	const char* name = argv[1];
	if ( 2 < argc )
	{
		command_errorx("%s: extra operand `%s'", argv[0], argv[2]);
		return;
	}
	if ( !strcmp(name, "none") )
	{
		current_hd = NULL;
		return;
	}
	for ( size_t i = 0; i < hds_count; i++ )
	{
		char buf[sizeof(i) * 3];
		snprintf(buf, sizeof(buf), "%zu", i);
		if ( strcmp(name, hds[i]->path) != 0 &&
		     strcmp(name, device_name(hds[i]->path)) != 0 &&
		     strcmp(name, buf) != 0 )
			continue;
		switch_device(hds[i]);
		return;
	}
	command_errorx("%s: No such device `%s'", argv[0], name);
}

static char* display_harddisk_format(void* ctx, size_t row, size_t column)
{
	if ( row == 0 )
	{
		switch ( column )
		{
		case 0: return strdup("#");
		case 1: return strdup("DEVICE");
		case 2: return strdup("SIZE");
		case 3: return strdup("MODEL");
		case 4: return strdup("SERIAL");
		default: return NULL;
		}
	}
	struct harddisk** hds = (struct harddisk**) ctx;
	struct harddisk* hd = hds[row-1];
	switch ( column )
	{
	case 0: return print_string("%zu", row - 1);
	case 1: return strdup(device_name(hd->path));
	case 2: return format_bytes_amount((uintmax_t) hd->st.st_size);
	case 3: return strdup(hd->model);
	case 4: return strdup(hd->serial);
	default: return NULL;
	}
}

static void on_devices(size_t argc, char** argv)
{
	(void) argc;
	(void) argv;
	qsort(hds, hds_count, sizeof(struct harddisk*), harddisk_compare);
	display_rows_columns(display_harddisk_format, hds, 1 + hds_count, 5);
}

static void on_fsck(size_t argc, char** argv)
{
	if ( argc < 2 )
	{
		command_errorx("%s: No partition specified", argv[0]);
		return;
	}
	struct partition* p;
	if ( !lookup_partition_by_string(&p, argv[0], argv[1]) )
		return;
	if ( !p->bdev.fs )
	{
		command_errorx("%s: %s: No filesystem recognized", argv[0],
		               device_name(p->path));
		return;
	}
	struct filesystem* fs = p->bdev.fs;
	if ( !fs->fsck )
	{
		command_errorx("%s: %s: fsck is not supported for %s", argv[0],
		               device_name(p->path), fs->fstype_name);
		return;
	}
	bool interactive_fsck = false;
	// TODO: Run this in its own foreground process group so it can be ^C'd.
	pid_t child_pid;
retry_interactive_fsck:
	if ( (child_pid = fork()) < 0 )
	{
		command_error("%s: fork", argv[0]);
		return;
	}
	if ( child_pid == 0 )
	{
		if ( interactive_fsck )
			execlp(fs->fsck, fs->fsck, "--", p->path, (const char*) NULL);
		else
			execlp(fs->fsck, fs->fsck, "-p", "--", p->path, (const char*) NULL);
		warn("%s: Failed to load filesystem checker: %s", argv[0], fs->fsck);
		_Exit(127);
	}
	int code;
	waitpid(child_pid, &code, 0);
	if ( WIFSIGNALED(code) )
		command_errorx("%s: %s: Filesystem check failed: %s: %s", argv[0],
		               p->path, fs->fsck, strsignal(WTERMSIG(code)));
	else if ( !WIFEXITED(code) )
		command_errorx("%s: %s: Filesystem check failed: %s: %s", argv[0],
		               p->path, fs->fsck, "Unexpected unusual termination");
	else if ( WEXITSTATUS(code) == 127 )
		command_errorx("%s: %s: Filesystem check failed: %s: %s", argv[0],
		               p->path, fs->fsck, "Filesystem checker is not installed");
	else if ( WEXITSTATUS(code) & 4 && !interactive_fsck )
	{
		interactive_fsck = true;
		goto retry_interactive_fsck;
	}
	else if ( WEXITSTATUS(code) != 0 && WEXITSTATUS(code) != 1 )
		command_errorx("%s: %s: Filesystem check failed: %s: %s", argv[0],
		               p->path, fs->fsck, "Filesystem checker was unsuccessful");
}

static void on_help(size_t argc, char** argv)
{
	(void) argc;
	(void) argv;
	// TODO: Show help for a particular command if an argument is given.
	//       Perhaps advertise the man page?
	const char* prefix = "";
	for ( size_t i = 0; i < commands_count; i++ )
	{
		if ( strchr(commands[i].flags, 'a') )
			continue;
		printf("%s%s", prefix, commands[i].name);
		prefix = " ";
	}
	printf("\n");
}

static char* display_area_format(void* ctx, size_t row, size_t column)
{
	if ( row == 0 )
	{
		switch ( column )
		{
		case 0: return strdup("PARTITION");
		case 1: return strdup("SIZE");
		case 2: return strdup("FILESYSTEM");
		case 3: return strdup("MOUNTPOINT");
		// TODO: LABEL
		default: return NULL;
		}
	}
	struct device_area* areas = (struct device_area*) ctx;
	struct device_area* area = &areas[row - 1];
	if ( !area->p )
	{
		switch ( column )
		{
		case 0: return strdup(area->inside_extended ? "  (unused)" : "(unused)");
		case 1: return format_bytes_amount((uintmax_t) area->length);
		case 2: return strdup("-");
		case 3: return strdup("-");
		default: return NULL;
		}
	}
	switch ( column )
	{
	case 0:
		if ( area->inside_extended )
			return print_string("  %s", device_name(area->p->path));
		else
			return strdup(device_name(area->p->path));
	case 1: return format_bytes_amount((uintmax_t) area->length);
	case 2:
		if ( area->p->bdev.fs )
			return strdup(area->p->bdev.fs->fstype_name);
		switch ( area->p->bdev.fs_error )
		{
		case FILESYSTEM_ERROR_NONE: return strdup("(no error)");
		case FILESYSTEM_ERROR_ABSENT: return strdup("none");
		case FILESYSTEM_ERROR_UNRECOGNIZED: return strdup("unrecognized");
		case FILESYSTEM_ERROR_ERRNO: return strdup("(error)");
		}
		return strdup("(unknown error)");
	case 3:
	{
		struct blockdevice* bdev = &area->p->bdev;
		char* storage;
		struct fstab fsent;
		if ( lookup_fstab_by_blockdevice(&fsent, &storage, bdev) )
		{
			char* result = strdup(fsent.fs_file);
			free(storage);
			return result;
		}
		return strdup("-");
	}
	default: return NULL;
	}
}

static void on_ls(size_t argc, char** argv)
{
	(void) argc;
	(void) argv;
	display_rows_columns(display_area_format, current_areas,
	                     1 + current_areas_count, 4);
}

static void on_man(size_t argc, char** argv)
{
	(void) argc;
	sigset_t oldset, sigttou;
	sigemptyset(&sigttou);
	sigaddset(&sigttou, SIGTTOU);
	pid_t child_pid = fork();
	if ( child_pid < 0 )
	{
		command_error("%s: fork", argv[0]);
		return;
	}
	if ( child_pid == 0 )
	{
		setpgid(0, 0);
		sigprocmask(SIG_BLOCK, &sigttou, &oldset);
		tcsetpgrp(0, getpgid(0));
		sigprocmask(SIG_SETMASK, &oldset, NULL);
		const char* defargv[] = { "man", "8", "disked", NULL };
		char** subargv = argc == 1 ? (char**) defargv : argv;
		execvp(subargv[0], (char* const*) subargv);
		warn("%s", subargv[0]);
		_exit(127);
	}
	int code;
	waitpid(child_pid, &code, 0);
	sigprocmask(SIG_BLOCK, &sigttou, &oldset);
	tcsetpgrp(0, getpgid(0));
	sigprocmask(SIG_SETMASK, &oldset, NULL);
}

static char* display_hole_format(void* ctx, size_t row, size_t column)
{
	if ( row == 0 )
	{
		switch ( column )
		{
		case 0: return strdup("HOLE");
		case 1: return strdup("START");
		case 2: return strdup("LENGTH");
		case 3: return strdup("TYPE");
		default: return NULL;
		}
	}
	struct device_area* areas = (struct device_area*) ctx;
	struct device_area* hole = NULL;
	size_t num_hole = 0;
	for ( size_t i = 0; i < current_areas_count; i++ )
	{
		if ( areas[i].p )
			continue;
		if ( num_hole == row - 1 )
		{
			hole = &areas[i];
			break;
		}
		num_hole++;
	}
	switch ( column )
	{
	case 0: return print_string("%zu", row);
	case 1: return format_bytes_amount((uintmax_t) hole->start);
	case 2: return format_bytes_amount((uintmax_t) hole->length);
	case 3: return strdup(hole->inside_extended ? "logical" : "primary");
	default: return NULL;
	}
}

static void on_mkpart(size_t argc, char** argv)
{
	size_t num_holes = 0;
	struct device_area* hole = NULL;
	for ( size_t i = 0; i < current_areas_count; i++ )
	{
		if ( current_areas[i].p )
			continue;
		num_holes++;
		hole = &current_areas[i];
	}
	if ( num_holes == 0 )
	{
		command_errorx("%s: %s: Device has no unused areas left",
		               argv[0], device_name(current_hd->path));
		return;
	}
	else if ( 2 <= num_holes )
	{
		bool type_column = current_pt_type == PARTITION_TABLE_TYPE_MBR;
		display_rows_columns(display_hole_format, current_areas,
		                     1 + num_holes, type_column ? 4 : 3);
		char answer[sizeof(size_t) * 3];
		while ( true )
		{
			if ( 2 <= argc )
				strlcpy(answer, argv[1], sizeof(answer));
			else
				prompt(answer, sizeof(answer),
				       "Which hole to create the partition inside?", "1");
			char* end;
			unsigned long num = strtoul(answer, &end, 10);
			if ( *end || num_holes < num )
			{
				command_errorx("%s: Invalid hole `%s'", argv[0], answer);
				continue;
			}
			for ( size_t i = 0; i < current_areas_count; i++ )
			{
				if ( current_areas[i].p )
					continue;
				if ( --num != 0 )
					continue;
				hole = &current_areas[i];
				break;
			}
			break;
		}
		if ( argc < 2 )
			printf("\n");
	}
	unsigned int slot = 0;
	if ( current_pt_type == PARTITION_TABLE_TYPE_MBR && hole->inside_extended )
	{
		slot = 5 + hole->ebr_index;
	}
	else if ( current_pt_type == PARTITION_TABLE_TYPE_MBR )
	{
		struct mbr_partition_table* mbrpt =
			(struct mbr_partition_table*) current_pt->raw_partition_table;
		for ( unsigned int i = 0; i < 4; i++ )
		{
			struct mbr_partition p;
			memcpy(&p, &mbrpt->mbr.partitions[i], sizeof(p));
			mbr_partition_decode(&p);
			if ( mbr_is_partition_used(&p) )
				continue;
			slot = 1 + i;
			break;
		}
	}
	else if ( current_pt_type == PARTITION_TABLE_TYPE_GPT )
	{
		struct gpt_partition_table* gptpt =
			(struct gpt_partition_table*) current_pt->raw_partition_table;
		struct gpt gpt;
		memcpy(&gpt, &gptpt->gpt, sizeof(gpt));
		gpt_decode(&gpt);
		for ( uint32_t i = 0; i < gpt.number_of_partition_entries; i++ )
		{
			size_t poff = i * (size_t) gpt.size_of_partition_entry;
			struct gpt_partition p;
			memcpy(&p, gptpt->rpt + poff, sizeof(p));
			bool unused = true;
			for ( size_t n = 0; n < 16; n++ )
				if ( p.partition_type_guid[n] )
					unused = false;
			if ( !unused )
				continue;
			slot = 1 + i;
			break;
		}
	}
	else
	{
		command_errorx("%s: %s: Partition scheme not supported", argv[0],
		               device_name(current_hd->path));
		return;
	}
	if ( slot == 0 )
	{
		command_errorx("%s: %s: Cannot add partition because the table is full",
		               argv[0], device_name(current_hd->path));
		return;
	}
	char* start_str = format_bytes_amount((uintmax_t) hole->start);
	assert(start_str); // TODO: Error handling.
	char* length_str = format_bytes_amount((uintmax_t) hole->length);
	assert(length_str); // TODO: Error handling.
	if ( argc < 3 )
		printf("Creating partition inside hole at %s of length %s (100%%)\n",
		       start_str, length_str);
	free(start_str);
	free(length_str);
	off_t start;
	while ( true )
	{
		char answer[256];
		if ( 3 <= argc )
			strlcpy(answer, argv[2], sizeof(answer));
		else
			prompt(answer, sizeof(answer),
			       "Free space before partition? (42%/15G/...)", "0%");
		if ( !parse_disk_quantity(&start, answer, hole->length) )
		{
			command_errorx("%s: %s: Invalid quantity: %s",
			               argv[0], device_name(current_hd->path), answer);
			continue;
		}
		if ( start == hole->length )
		{
			command_errorx("%s: %s: Answer was all free space, but need space "
			               "for the partition itself",
			               argv[0], device_name(current_hd->path));
			continue;
		}
		break;
	}
	if ( argc < 3 )
		printf("\n");
	off_t max_length = hole->length - start;
	off_t length;
	length_str = format_bytes_amount((uintmax_t) max_length);
	assert(length_str);
	if ( argc < 4 )
		printf("Partition size can be at most %s (100%%).\n", length_str);
	free(length_str);
	while ( true )
	{
		char answer[256];
		if ( 4 <= argc )
			strlcpy(answer, argv[3], sizeof(answer));
		else
			prompt(answer, sizeof(answer),
			       "Partition size? (42%/15G/...)", "100%");
		if ( !parse_disk_quantity(&length, answer, max_length) )
		{
			command_errorx("%s: %s: Invalid quantity: %s",
			               argv[0], device_name(current_hd->path), answer);
			continue;
		}
		if ( length == 0 )
		{
			command_errorx("%s: %s: Length was zero (or rounded down to zero)",
			               argv[0], device_name(current_hd->path));
			continue;
		}
		break;
	}
	if ( argc < 4 )
		printf("\n");
	char fstype[256];
	while ( true )
	{
		bool is_mbr = current_pt_type == PARTITION_TABLE_TYPE_MBR;
		bool is_gpt = current_pt_type == PARTITION_TABLE_TYPE_GPT;
		const char* question = "Format a filesystem? (no/ext2)";
		if ( is_mbr )
			question = "Format a filesystem? (no/ext2/extended)";
		else if ( is_gpt )
			question = "Format a filesystem? (no/ext2/biosboot)";
		if ( 5 <= argc )
			strlcpy(fstype, argv[4], sizeof(fstype));
		else
			prompt(fstype, sizeof(fstype), question, "ext2");
		if ( strcmp(fstype, "no") != 0 &&
		     strcmp(fstype, "ext2") != 0 &&
		     (!is_mbr || strcmp(fstype, "extended") != 0) &&
		     (!is_gpt || strcmp(fstype, "biosboot") != 0) )
		{
			command_errorx("%s: %s: Invalid filesystem choice: %s",
			               argv[0], device_name(current_hd->path), fstype);
			continue;
		}
		if ( !strcmp(fstype, "extended") )
		{
			for ( size_t i = 0; i < current_pt->partitions_count; i++ )
			{
				if ( current_pt->partitions[i]->type != PARTITION_TYPE_EXTENDED )
					continue;
				command_errorx("%s: %s: Device already has an extended partition",
		                       argv[0], device_name(current_hd->path));
				return;
			}
		}
		break;
	}
	char mountpoint[256] = "";
	bool mountable = !strcmp(fstype, "ext2");
	while ( mountable )
	{
		if ( 6 <= argc )
			strlcpy(mountpoint, argv[5], sizeof(mountpoint));
		else
			prompt(mountpoint, sizeof(mountpoint),
			       "Where to mount partition? (mountpoint or 'no')", "no");
		if ( !strcmp(mountpoint, "no") )
		{
			mountpoint[0] = '\0';
			break;
		}
		if ( !verify_mountpoint(mountpoint) )
		{
			command_errorx("%s: %s: Invalid mountpoint: %s",
	                       argv[0], device_name(current_hd->path), mountpoint);
			continue;
		}
		simplify_mountpoint(mountpoint);
		break;
	}
	if ( mountable && argc < 6 )
		printf("\n");
	size_t renumbered_partitions = 0;
	if ( current_pt_type == PARTITION_TABLE_TYPE_MBR && hole->inside_extended )
	{
		struct mbr_partition_table* mbrpt =
			(struct mbr_partition_table*) current_pt->raw_partition_table;
		struct mbr ebr;
		struct mbr_partition p;
		off_t next_ebr_off = 0;
		uint32_t next_ebr_full_sectors = 0;
		if ( hole->ebr_index + 1 < mbrpt->ebr_chain_count )
		{
			memcpy(&ebr, &mbrpt->ebr_chain[hole->ebr_index + 1].ebr, sizeof(ebr));
			memcpy(&p, &ebr.partitions[0], sizeof(p));
			mbr_partition_decode(&p);
			next_ebr_full_sectors = p.start_sector + p.total_sectors;
			next_ebr_off = mbrpt->ebr_chain[hole->ebr_index + 1].offset;
		}
		if ( hole->ebr_move_off )
		{
			renumbered_partitions = 5 + hole->ebr_move_index;
			memcpy(&ebr, &mbrpt->ebr_chain[hole->ebr_move_index].ebr, sizeof(ebr));
			memcpy(&p, &ebr.partitions[0], sizeof(p));
			mbr_partition_decode(&p);
			// TODO: Update CHS information?
			p.start_sector = 1;
			next_ebr_full_sectors = p.start_sector + p.total_sectors;
			mbr_partition_encode(&p);
			memcpy(&ebr.partitions[0], &p, sizeof(p));
			memcpy(&p, &ebr.partitions[1], sizeof(p));
			mbr_partition_decode(&p);
			// TODO: Update CHS information?
			p.start_sector = 0;
			if ( next_ebr_off )
			{
				assert(hole->ebr_move_off < next_ebr_off);
				off_t dist = next_ebr_off - hole->extended_start;
				assert(dist);
				p.start_sector = dist / current_hd->logical_block_size;
			}
			mbr_partition_encode(&p);
			memcpy(&ebr.partitions[1], &p, sizeof(p));
			if ( pwriteall(current_hd->fd, &ebr, sizeof(ebr),
				           hole->ebr_move_off) < sizeof(ebr) )
			{
				command_error("%s: %s: write", argv[0], device_name(current_hd->path));
				scan_device();
				return;
			}
			next_ebr_off = hole->ebr_move_off;
		}
		off_t ebr_off = hole->ebr_off;
		memset(&ebr, 0, sizeof(ebr));
		ebr.signature[0] = 0x55;
		ebr.signature[1] = 0xAA;
		memset(&p, 0, sizeof(p));
		off_t p_start = hole->start + start - ebr_off;
		p.flags = 0;
		p.start_head = 1; // TODO: This.
		p.start_sector_cylinder = 1; // TODO: This.
		if ( !strcmp(fstype, "ext2") )
			p.system_id = 0x83;
		else if ( !strcmp(fstype, "extended") )
			p.system_id = 0x05;
		else
			p.system_id = 0x83;
		p.end_head = 2; // TODO: This.
		p.end_sector_cylinder = 2; // TODO: This.
		p.start_sector = p_start / current_hd->logical_block_size;
		p.total_sectors = length / current_hd->logical_block_size;
		mbr_partition_encode(&p);
		memcpy(&ebr.partitions[0], &p, sizeof(p));
		memset(&p, 0, sizeof(p));
		p.system_id = 0x00;
		p.start_sector = 0;
		p.total_sectors = 0;
		if ( next_ebr_off )
		{
			p.system_id = 0x05;
			assert(ebr_off < next_ebr_off);
			off_t dist = next_ebr_off - hole->extended_start;
			assert(dist);
			p.start_sector = dist / current_hd->logical_block_size;
			p.total_sectors = next_ebr_full_sectors;
		}
		mbr_partition_encode(&p);
		memcpy(&ebr.partitions[1], &p, sizeof(p));
		if ( pwriteall(current_hd->fd, &ebr, sizeof(ebr),
		               ebr_off) < sizeof(ebr) )
		{
			command_error("%s: %s: write", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
		if ( 0 < hole->ebr_index )
		{
			size_t prev_ebr_index = hole->ebr_index - 1;
			off_t prev_ebr_off = mbrpt->ebr_chain[prev_ebr_index].offset;
			memcpy(&ebr, &mbrpt->ebr_chain[prev_ebr_index].ebr, sizeof(ebr));
			memcpy(&p, &ebr.partitions[1], sizeof(p));
			mbr_partition_decode(&p);
			// TODO: Update CHS information?
			p.system_id = 0x05;
			assert(prev_ebr_off < ebr_off);
			off_t dist = ebr_off - hole->extended_start;
			assert(dist);
			p.start_sector = dist / current_hd->logical_block_size;
			off_t dist_total = hole->start + start + length - ebr_off;
			assert(dist_total);
			p.total_sectors = dist_total / current_hd->logical_block_size;
			mbr_partition_encode(&p);
			memcpy(&ebr.partitions[1], &p, sizeof(p));
			if ( pwriteall(current_hd->fd, &ebr, sizeof(ebr),
				           prev_ebr_off) < sizeof(ebr) )
			{
				command_error("%s: %s: write", argv[0], device_name(current_hd->path));
				scan_device();
				return;
			}
		}
		if ( fsync(current_hd->fd) < 0 )
		{
			command_error("%s: %s: sync", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
	}
	else if ( current_pt_type == PARTITION_TABLE_TYPE_MBR )
	{
		struct mbr_partition_table* mbrpt =
			(struct mbr_partition_table*) current_pt->raw_partition_table;
		struct mbr mbr;
		memcpy(&mbr, &mbrpt->mbr, sizeof(mbr));
		struct mbr_partition p;
		memset(&p, 0, sizeof(p));
		p.flags = 0;
		p.start_head = 0; // TODO: This.
		p.start_sector_cylinder = 0; // TODO: This.
		if ( !strcmp(fstype, "ext2") )
			p.system_id = 0x83;
		else if ( !strcmp(fstype, "extended") )
			p.system_id = 0x05;
		else
			p.system_id = 0x83;
		p.end_head = 0; // TODO: This.
		p.end_sector_cylinder = 0; // TODO: This.
		p.start_sector = (hole->start + start) / current_hd->logical_block_size;
		p.total_sectors = length / current_hd->logical_block_size;
		mbr_partition_encode(&p);
		memcpy(&mbr.partitions[slot - 1], &p, sizeof(p));
		if ( pwriteall(current_hd->fd, &mbr, sizeof(mbr), 0) < sizeof(mbr) )
		{
			command_error("%s: %s: write", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
		if ( fsync(current_hd->fd) < 0 )
		{
			command_error("%s: %s: sync", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
	}
	else if ( current_pt_type == PARTITION_TABLE_TYPE_GPT )
	{
		struct gpt_partition_table* gptpt =
			(struct gpt_partition_table*) current_pt->raw_partition_table;
		struct gpt gpt;
		memcpy(&gpt, &gptpt->gpt, sizeof(gpt));
		gpt_decode(&gpt);
		size_t poff = (slot - 1) * (size_t) gpt.size_of_partition_entry;
		struct gpt_partition p;
		memset(&p, 0, sizeof(p));
		// TODO: This string might need to have some bytes swapped.
		// TODO: Perhaps just to hell with Linux guids and allocate our own
		//       Sortix values that denote particular filesystems.
		const char* type_uuid_str = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
		if ( !strcmp(fstype, "biosboot") )
			type_uuid_str = BIOSBOOT_GPT_TYPE_UUID;
		uuid_from_string(p.partition_type_guid, type_uuid_str);
		arc4random_buf(p.unique_partition_guid, sizeof(p.unique_partition_guid));
		off_t pstart = hole->start + start;
		off_t pend = hole->start + start + length;
		p.starting_lba = pstart / current_hd->logical_block_size;
		p.ending_lba = pend / current_hd->logical_block_size - 1;
		p.attributes = 0;
		// TODO: Partition name.
		gpt_partition_encode(&p);
		memcpy(gptpt->rpt + poff, &p, sizeof(p));
		if ( !gpt_update(argv[0], gptpt) )
			return;
	}
	else
	{
		command_errorx("%s: %s: Partition scheme not supported", argv[0],
		               device_name(current_hd->path));
		return;
	}
	off_t search_target_offset = hole->start + start;
	if ( current_pt_type == PARTITION_TABLE_TYPE_MBR &&
	     !strcmp(fstype, "extended") )
	{
		struct mbr ebr;
		memset(&ebr, 0, sizeof(ebr));
		ebr.signature[0] = 0x55;
		ebr.signature[1] = 0xAA;
		if ( pwriteall(current_hd->fd, &ebr, sizeof(ebr),
		               search_target_offset) < sizeof(ebr) )
		{
			command_error("%s: %s: write", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
	}
	if ( renumbered_partitions )
	{
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		{
			if ( current_pt->partitions[i]->index < renumbered_partitions )
				continue;
			remove_partition_device(current_pt->partitions[i]->path);
			break;
		}
	}
	scan_device();
	if ( !current_pt ) // TODO: Assumes scan went well.
	{
		command_errorx("%s: %s: Rescan failed",
		               argv[0], device_name(current_hd->path));
		return;
	}
	if ( renumbered_partitions )
	{
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		{
			if ( current_pt->partitions[i]->index < renumbered_partitions )
				continue;
			if ( current_pt->partitions[i]->start == search_target_offset )
				continue;
			struct partition* p = current_pt->partitions[i];
			if ( !create_partition_device(argv[0], p) )
				return;
		}
	}
	struct partition* p = NULL;
	for ( size_t i = 0; i < current_pt->partitions_count; i++ )
	{
		if ( current_pt->partitions[i]->start != search_target_offset )
			continue;
		p = current_pt->partitions[i];
	}
	if ( !p )
	{
		command_errorx("%s: %s: Failed to locate expected %sp%u partition",
		               argv[0], device_name(current_hd->path),
		               device_name(current_hd->path), slot);
		return; // TODO: Something went wrong.
	}
	if ( !create_partition_device(argv[0], p) )
		return;
	printf("(Made %s)\n", device_name(p->path));
	if ( !strcmp(fstype, "ext2") )
	{
		printf("(Formatting %s as ext2...)\n", device_name(p->path));
		struct ext2_superblock zero_sb;
		memset(&zero_sb, 0, sizeof(zero_sb));
		// TODO: Add a blockdevice_pwriteall to libmount and use it.
		if ( pwriteall(current_hd->fd, &zero_sb, sizeof(zero_sb),
		               p->start + 1024) < sizeof(zero_sb) )
		{
			command_error("%s: %s: write", argv[0], device_name(current_hd->path));
			scan_partition(p);
			return;
		}
		if ( fsync(current_hd->fd) < 0 )
		{
			command_error("%s: %s: sync", argv[0], device_name(current_hd->path));
			scan_partition(p);
			return;
		}
		// TODO: Run this in its own foreground process group so ^C works.
		pid_t child_pid = fork();
		if ( child_pid < 0 )
		{
			command_error("%s: fork", argv[0]);
			return;
		}
		const char* mkfs_argv[] =
		{
			"mkfs.ext2",
			"-q",
			mountpoint[0] ? "-M" : p->path,
			mountpoint[0] ? mountpoint : NULL,
			p->path,
			NULL
		};
		if ( child_pid == 0 )
		{
			execvp(mkfs_argv[0], (char* const*) mkfs_argv);
			warn("%s", mkfs_argv[0]);
			_exit(127);
		}
		int status;
		waitpid(child_pid, &status, 0);
		if ( WIFEXITED(status) && WEXITSTATUS(status) == 127 )
		{
			command_errorx("%s: Failed to format filesystem (%s is not installed)",
			               argv[0], mkfs_argv[0]);
			return;
		}
		else if ( WIFEXITED(status) && WEXITSTATUS(status) != 0 )
		{
			command_errorx("%s: Failed to format filesystem", argv[0]);
			return;
		}
		else if ( WIFSIGNALED(status) )
		{
			command_errorx("%s: Failed to format filesystem (%s)",
			               argv[0], strsignal(WTERMSIG(status)));
			return;
		}
		printf("(Formatted %s as ext2)\n", device_name(p->path));
		scan_partition(p);
		if ( !p->bdev.fs || !(p->bdev.fs->flags & FILESYSTEM_FLAG_UUID) )
		{
			command_errorx("%s: %s: Failed to scan expected ext2 filesystem",
			               argv[0], device_name(p->path));
			return;
		}
		if ( mountpoint[0] )
		{
			if ( !add_blockdevice_to_fstab(&p->bdev, mountpoint) )
			{
				command_error("%s: %s: Failed to add partition", argv[0], fstab_path);
				return;
			}
		}
	}
}

static void on_mktable(size_t argc, char** argv)
{
	(void) argc;
	(void) argv;
	if ( current_pt_type != PARTITION_TABLE_TYPE_NONE )
	{
		const char* name = device_name(current_hd->path);
		if ( interactive )
			fprintf(stderr, "Device `%s' already has a partition table.\n", name);
		else
			command_errorx("Device `%s' already has a partition table", name);
		return;
	}
	char type_answer[32];
	const char* type = NULL;
	if ( 2 <= argc )
		type = argv[1];
	if ( !type )
	{
		prompt(type_answer, sizeof(type_answer),
		       "Which partition table type? (mbr/gpt)", "gpt");
		type = type_answer;
	}
	remove_partition_devices(current_hd->path);
	int fd = current_hd->fd;
	const char* name = device_name(current_hd->path);
	size_t logical_block_size = current_hd->logical_block_size;
	if ( !strcasecmp(type, "mbr") )
	{
		struct mbr mbr;
		memset(&mbr, 0, sizeof(mbr));
		mbr.signature[0] = 0x55;
		mbr.signature[1] = 0xAA;
		if ( pwriteall(fd, &mbr, sizeof(mbr), 0) < sizeof(mbr) )
		{
			command_error("%s: %s: write", argv[0], name);
			scan_device();
			return;
		}
	}
	else if ( !strcasecmp(type, "gpt") )
	{
		size_t header_size;
		uint64_t sector_count = current_hd->st.st_size / logical_block_size;
		size_t partition_table_size = 16384;
		if ( partition_table_size < logical_block_size )
			partition_table_size = logical_block_size;
		size_t partition_table_length =
			partition_table_size / sizeof(struct gpt_partition);
		uint64_t partition_table_sectors =
			partition_table_size / logical_block_size;
		uint64_t minimum_leading = 1 + 1 + partition_table_sectors;
		uint64_t minimum_trailing = partition_table_sectors + 1;
		uint64_t minimum_sector_count = minimum_leading + minimum_trailing;
		if ( sector_count <= minimum_sector_count )
		{
			command_errorx("Device `%s' is too small for GPT", name);
			return;
		}
		uint64_t last_lba = sector_count - 1;
		off_t gpt_off = logical_block_size;
		off_t alt_off = logical_block_size * last_lba;
		unsigned char* partition_table =
			(unsigned char*) calloc(1, partition_table_size);
		if ( !partition_table )
		{
			command_error("%s: %s: malloc", argv[0], name);
			return;
		}
		uint32_t partition_table_checksum =
			gpt_crc32(partition_table, partition_table_size);
		uint64_t pt_prim_lba = 2;
		off_t pt_prim_lba_off = pt_prim_lba * logical_block_size;
		if ( pwriteall(fd, partition_table, partition_table_size,
		               pt_prim_lba_off) < partition_table_size )
		{
			command_error("%s: %s: write", argv[0], name);
			free(partition_table);
			scan_device();
			return;
		}
		uint64_t pt_alt_lba = last_lba - partition_table_sectors;
		off_t pt_alt_lba_off = pt_alt_lba * logical_block_size;
		if ( pwriteall(fd, partition_table, partition_table_size,
		               pt_alt_lba_off) < partition_table_size )
		{
			command_error("%s: %s: write", argv[0], name);
			free(partition_table);
			scan_device();
			return;
		}
		free(partition_table);
		struct gpt gpt;
		memset(&gpt, 0, sizeof(gpt));
		memcpy(gpt.signature, "EFI PART", 8);
		gpt.revision = 0x00010000;
		gpt.header_size = sizeof(gpt) - sizeof(gpt.reserved1);
		gpt.header_crc32 = 0;
		gpt.reserved0 = 0;
		gpt.my_lba = 1;
		gpt.alternate_lba = last_lba;
		gpt.first_usable_lba = pt_prim_lba + partition_table_sectors;
		gpt.last_usable_lba = pt_alt_lba - 1;
		arc4random_buf(&gpt.disk_guid, sizeof(gpt.disk_guid));
		gpt.partition_entry_lba = pt_prim_lba;
		gpt.number_of_partition_entries = partition_table_length;
		gpt.size_of_partition_entry = sizeof(struct gpt_partition);
		gpt.partition_entry_array_crc32 = partition_table_checksum;
		header_size = gpt.header_size;
		gpt_encode(&gpt);
		gpt.header_crc32 = htole32(gpt_crc32(&gpt, header_size));
		if ( pwriteall(fd, &gpt, sizeof(gpt), gpt_off) < sizeof(gpt) )
		{
			command_error("%s: %s: write", argv[0], name);
			scan_device();
			return;
		}
		gpt_decode(&gpt);
		gpt.header_crc32 = 0;
		gpt.reserved0 = 0;
		gpt.my_lba = last_lba;
		gpt.alternate_lba = 1;
		gpt.partition_entry_lba = pt_alt_lba;
		header_size = gpt.header_size;
		gpt_encode(&gpt);
		gpt.header_crc32 = htole32(gpt_crc32(&gpt, header_size));
		if ( pwriteall(fd, &gpt, sizeof(gpt), alt_off) < sizeof(gpt) )
		{
			command_error("%s: %s: write", argv[0], name);
			scan_device();
			return;
		}
		if ( UINT32_MAX < sector_count )
			sector_count = UINT32_MAX;
		struct mbr mbr;
		memset(&mbr, 0, sizeof(mbr));
		mbr.signature[0] = 0x55;
		mbr.signature[1] = 0xAA;
		struct mbr_partition p;
		memset(&p, 0, sizeof(p));
		p.flags = 0;
		p.start_head = 0; // TODO: This.
		p.start_sector_cylinder = 0; // TODO: This.
		p.system_id = 0xEE;
		p.end_head = 0; // TODO: This.
		p.end_sector_cylinder = 0; // TODO: This.
		p.start_sector = 1;
		p.total_sectors = sector_count - p.start_sector;
		mbr_partition_encode(&p);
		memcpy(&mbr.partitions[0], &p, sizeof(p));
		if ( pwriteall(fd, &mbr, sizeof(mbr), 0) < sizeof(mbr) )
		{
			command_error("%s: %s: write", argv[0], name);
			scan_device();
			return;
		}
	}
	else
	{
		command_errorx("%s: Unrecognized partition table type `%s'", argv[0], type);
		return;
	}
	if ( fsync(fd) < 0 )
	{
		command_error("%s: %s: sync", argv[0], name);
		scan_device();
		return;
	}
	// TODO: Rescan this device.
	switch_device(current_hd);
}


static void on_mount(size_t argc, char** argv)
{
	if ( argc < 2 )
	{
		command_errorx("%s: No partition specified", argv[0]);
		return;
	}
	struct partition* part;
	if ( !lookup_partition_by_string(&part, argv[0], argv[1]) )
		return;
	if ( argc < 3 )
	{
		command_errorx("%s: Mountpoint or 'no' wasn't specified", argv[0]);
		return;
	}
	char* mountpoint = argv[2];
	if ( !strcmp(mountpoint, "no") )
	{
		if ( !remove_blockdevice_from_fstab(&part->bdev) )
		{
			command_error("%s: %s: Failed to remove partition",
			              argv[0], fstab_path);
			return;
		}
	}
	else
	{
		if ( !verify_mountpoint(mountpoint) )
		{
			command_errorx("%s: Invalid mountpoint `%s'.", argv[0], mountpoint);
			return;
		}
		simplify_mountpoint(mountpoint);
		if ( !part->bdev.fs || !part->bdev.fs->driver )
		{
			const char* name = device_name(part->path);
			printf("Warning: `%s' is not a mountable filesystem.\n", name);
		}
		if ( !add_blockdevice_to_fstab(&part->bdev, mountpoint) )
		{
			command_error("%s: %s: Failed to remove partition",
			              argv[0], fstab_path);
			return;
		}
	}
}

static void on_quit(size_t argc, char** argv)
{
	(void) argc;
	(void) argv;
	quitting = true;
}

static void on_rmpart(size_t argc, char** argv)
{
	if ( argc < 2 )
	{
		command_errorx("%s: No partition specified", argv[0]);
		return;
	}
	struct partition* part;
	if ( !lookup_partition_by_string(&part, argv[0], argv[1]) )
		return;
	bool ok = false;
	if ( part->type == PARTITION_TYPE_EXTENDED )
	{
		bool has_logical = false;
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		{
			if ( current_pt->partitions[i]->type != PARTITION_TYPE_LOGICAL )
				continue;
			has_logical = true;
			break;
		}
		ok = !has_logical;
	}
	if ( interactive && !ok )
	{
		const char* name = device_name(part->path);
		// TODO: Use the name and mount point of the partition if such!
		if ( part->type == PARTITION_TYPE_EXTENDED )
		{
			textf("WARNING: This will \e[91mERASE ALL PARTITIONS\e[m on the "
			      "extended partition \e[93m%s\e[m!\n", name);
			for ( size_t i = 0; i < current_pt->partitions_count; i++ )
			{
				struct partition* logic = current_pt->partitions[i];
				if ( logic->type != PARTITION_TYPE_LOGICAL )
					continue;
				const char* logic_name = device_name(logic->path);
				textf("WARNING: This will \e[91mERASE ALL DATA\e[m on the "
				      "partition \e[93m%s\e[m!\n", logic_name);
			}
		}
		else
			textf("WARNING: This will \e[91mERASE ALL DATA\e[m on the "
			      "partition \e[93m%s\e[m!\n", name);
		// TODO: Warn if GPT require attribute is set.
		while ( true )
		{
			char answer[32];
			prompt(answer, sizeof(answer),
			       "Confirm partition deletion? (yes/no)", "no");
			if ( strcmp(answer, "no") == 0 )
			{
				printf("(Aborted partition deletion)\n");
				return;
			}
			if ( strcmp(answer, "yes") == 0 )
				break;
		}
	}
	// TODO: Ensure the partition is not in use!
	if ( part->type == PARTITION_TYPE_EXTENDED )
	{
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		{
			struct partition* logic = current_pt->partitions[i];
			if ( logic->type != PARTITION_TYPE_LOGICAL )
				continue;
			if ( !remove_partition_device(logic->path) )
			{
				command_error("%s: Failed to remove partition device: %s",
						      argv[0], logic->path);
				return;
			}
			if ( !remove_blockdevice_from_fstab(&logic->bdev) )
			{
				command_error("%s: %s: Failed to remove partition", argv[0],
				              fstab_path);
				return;
			}
		}
	}
	if ( !remove_partition_device(part->path) )
	{
		command_error("%s: Failed to remove partition device: %s",
		              argv[0], part->path);
		return;
	}
	if ( !remove_blockdevice_from_fstab(&part->bdev) )
	{
		command_error("%s: %s: Failed to remove partition", argv[0], fstab_path);
		// TODO: Recreate the partition.
		return;
	}
	unsigned int part_index = part->index;
	size_t renumbered_partitions = 0;
	if ( current_pt_type == PARTITION_TABLE_TYPE_MBR &&
	     part->type == PARTITION_TYPE_LOGICAL )
	{
		assert(5 <= part_index);
		renumbered_partitions = part_index;
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		{
			if ( current_pt->partitions[i]->index <= part_index )
				continue;
			remove_partition_device(current_pt->partitions[i]->path);
		}
		struct mbr_partition_table* mbrpt =
			(struct mbr_partition_table*) current_pt->raw_partition_table;
		size_t ebr_i = part_index - 5;
		off_t ebr_off = 0;
		struct mbr ebr;
		struct mbr_partition p;
		if ( ebr_i == 0 && mbrpt->ebr_chain_count == 1 )
		{
			ebr_off = mbrpt->ebr_chain[0].offset;
			memset(&ebr, 0, sizeof(ebr));
			ebr.signature[0] = 0x55;
			ebr.signature[1] = 0xAA;
		}
		else if ( ebr_i == 0 )
		{
			ebr_off = mbrpt->ebr_chain[0].offset;
			off_t dist = mbrpt->ebr_chain[1].offset - mbrpt->ebr_chain[0].offset;
			memcpy(&ebr, &mbrpt->ebr_chain[1].ebr, sizeof(ebr));
			memcpy(&p, ebr.partitions[0], sizeof(p));
			mbr_partition_decode(&p);
			p.start_sector += dist / current_hd->logical_block_size;
			mbr_partition_encode(&p);
			memcpy(&ebr.partitions[0], &p, sizeof(p));
		}
		else
		{
			memcpy(&ebr, &mbrpt->ebr_chain[ebr_i].ebr, sizeof(ebr));
			memcpy(&p, ebr.partitions[1], sizeof(p));
			ebr_off = mbrpt->ebr_chain[ebr_i - 1].offset;
			memcpy(&ebr, &mbrpt->ebr_chain[ebr_i - 1].ebr, sizeof(ebr));
			memcpy(&ebr.partitions[1], &p, sizeof(p));
		}
		// TODO: Partitions may be reordered.
		if ( pwriteall(current_hd->fd, &ebr, sizeof(ebr), ebr_off) < sizeof(ebr) )
		{
			command_error("%s: %s: write", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
		if ( fsync(current_hd->fd) < 0 )
		{
			command_error("%s: %s: sync", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
	}
	else if ( current_pt_type == PARTITION_TABLE_TYPE_MBR )
	{
		assert(0 < part_index);
		assert(part_index <= 4);
		struct mbr_partition_table* mbrpt =
			(struct mbr_partition_table*) current_pt->raw_partition_table;
		struct mbr mbr;
		memcpy(&mbr, &mbrpt->mbr, sizeof(mbr));
		memset(&mbr.partitions[part_index - 1], 0, sizeof(struct mbr_partition));
		if ( pwriteall(current_hd->fd, &mbr, sizeof(mbr), 0) < sizeof(mbr) )
		{
			command_error("%s: %s: write", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
		if ( fsync(current_hd->fd) < 0 )
		{
			command_error("%s: %s: sync", argv[0], device_name(current_hd->path));
			scan_device();
			return;
		}
	}
	else if ( current_pt_type == PARTITION_TABLE_TYPE_GPT )
	{
		struct gpt_partition_table* gptpt =
			(struct gpt_partition_table*) current_pt->raw_partition_table;
		struct gpt gpt;
		memcpy(&gpt, &gptpt->gpt, sizeof(gpt));
		gpt_decode(&gpt);
		size_t poff = (part_index - 1) * (size_t) gpt.size_of_partition_entry;
		memset(gptpt->rpt + poff, 0, sizeof(struct gpt_partition));
		if ( !gpt_update(argv[0], gptpt) )
			return;
	}
	else
	{
		command_errorx("%s: %s: Partition scheme not supported", argv[0],
		               device_name(current_hd->path));
		return;
	}
	scan_device();
	printf("(Deleted %sp%u)\n", device_name(current_hd->path), part_index);
	if ( current_pt && renumbered_partitions )
	{
		for ( size_t i = 0; i < current_pt->partitions_count; i++ )
		{
			if ( current_pt->partitions[i]->index < renumbered_partitions )
				continue;
			struct partition* p = current_pt->partitions[i];
			if ( !create_partition_device(argv[0], p) )
				return;
		}
	}
}

static void on_rmtable(size_t argc, char** argv)
{
	(void) argc;
	(void) argv;
	// TODO: We shouldn't do this either if we recognize a filesystem on the
	//       raw device itself.
	if ( current_pt_type != PARTITION_TABLE_TYPE_NONE && interactive )
	{
		const char* name = device_name(current_hd->path);
		textf("WARNING: This will \e[91mERASE ALL DATA\e[m on the device "
		      "\e[93m%s\e[m!\n", name);
		// TODO: List all the partitions?
		// TODO: Use the name and mount point of the partition if such!
		if ( current_pt && 0 < current_pt->partitions_count )
			textf("WARNING: Device \e[93m%s\e[m \e[91mHAS PARTITIONS\e[m!\n",
			      name);
		while ( true )
		{
			char answer[32];
			prompt(answer, sizeof(answer),
			       "Confirm erase partition table? (yes/no)", "no");
			if ( strcmp(answer, "no") == 0 )
			{
				printf("(Aborted partition table erase)\n");
				return;
			}
			if ( strcmp(answer, "yes") == 0 )
				break;
		}
	}
	for ( size_t i = 0; current_pt && i < current_pt->partitions_count; i++ )
	{
		struct partition* part = current_pt->partitions[i];
		if ( !remove_partition_device(part->path) )
		{
			command_error("%s: Failed to remove partition device: %s",
					      argv[0], part->path);
			return;
		}
		if ( !remove_blockdevice_from_fstab(&part->bdev) )
		{
			command_error("%s: %s: Failed to remove partitions",
			              argv[0], fstab_path);
			// TODO: Recreate the partition.
			return;
		}
	}
	remove_partition_devices(current_hd->path);
	// TODO: Assert logical_block_size fits in size_t.
	size_t block_size = current_hd->logical_block_size;
	unsigned char* zeroes = (unsigned char*) calloc(1, block_size);
	if ( !zeroes )
	{
		command_error("malloc");
		return;
	}
	off_t sector_0 = 0; // MBR & GPT
	off_t sector_1 = block_size; // GPT
	off_t sector_m1;
	if ( __builtin_sub_overflow(current_hd->st.st_size, block_size, &sector_m1) )
	{
		errno = EOVERFLOW;
		command_error("Removing partition table");
		return;
	}
	if ( pwriteall(current_hd->fd, zeroes, block_size, sector_0) < block_size ||
	     pwriteall(current_hd->fd, zeroes, block_size, sector_1) < block_size ||
	     pwriteall(current_hd->fd, zeroes, block_size, sector_m1) < block_size )
	{
		command_error("%s: %s: write", argv[0], device_name(current_hd->path));
		free(zeroes);
		return;
	}
	free(zeroes);
	if ( fsync(current_hd->fd) < 0 )
	{
		command_error("%s: %s: sync", argv[0], device_name(current_hd->path));
		return;
	}
	scan_device();
}

static void on_sh(size_t argc, char** argv)
{
	(void) argc;
	sigset_t oldset, sigttou;
	sigemptyset(&sigttou);
	sigaddset(&sigttou, SIGTTOU);
	pid_t child_pid = fork();
	if ( child_pid < 0 )
	{
		command_error("%s: fork", argv[0]);
		return;
	}
	if ( child_pid == 0 )
	{
		setpgid(0, 0);
		sigprocmask(SIG_BLOCK, &sigttou, &oldset);
		tcsetpgrp(0, getpgid(0));
		sigprocmask(SIG_SETMASK, &oldset, NULL);
		const char* subargv[] = { "sh", NULL };
		execvp(subargv[0], (char* const*) subargv);
		warn("%s", subargv[0]);
		_exit(127);
	}
	int code;
	waitpid(child_pid, &code, 0);
	sigprocmask(SIG_BLOCK, &sigttou, &oldset);
	tcsetpgrp(0, getpgid(0));
	sigprocmask(SIG_SETMASK, &oldset, NULL);
	scan_device();
}

// TODO: mkfs command.
static const struct command commands[] =
{
	{ "!", on_sh, "as" },
	{ "!man", on_man, "as" },
	{ "?", on_help, "a" },
	{ "device", on_device, "" },
	{ "devices", on_devices, "" },
	{ "d", on_device, "a" },
	{ "ds", on_devices, "a" },
	{ "e", on_quit, "a" },
	{ "exit", on_quit, "" },
	{ "fsck", on_fsck, "dtp" },
	{ "help", on_help, "" },
	{ "ls", on_ls, "dt" },
	{ "man", on_man, "s" },
	{ "mkpart", on_mkpart, "dt" },
	{ "mktable", on_mktable, "d" },
	{ "mount", on_mount, "dtp" },
	{ "q", on_quit, "a" },
	{ "quit", on_quit, "a" },
	{ "rmpart", on_rmpart, "dtp" },
	{ "rmtable", on_rmtable, "d" },
	{ "sh", on_sh, "s" },
};
static const size_t commands_count = sizeof(commands) / sizeof(commands[0]);

static void execute_argv(int argc, char** argv)
{
	for ( size_t i = 0; i < commands_count; i++ )
	{
		if ( strcmp(argv[0], commands[i].name) != 0 )
			continue;
		if ( strchr(commands[i].flags, 'd') && !current_hd )
		{
			if ( !interactive )
				errx(1, "No device specified");
			fprintf(stderr, "Use the `device' command first to specify a device.\n");
			fprintf(stderr, "You can list devices with the `devices' command.\n");
			return;
		}
		if ( strchr(commands[i].flags, 't') && !current_pt )
		{
			if ( current_pt_type == PARTITION_TABLE_TYPE_NONE )
				printf("%s: No partition table found\n",
					   device_name(current_hd->path));
			else if ( current_pt_type == PARTITION_TABLE_TYPE_UNKNOWN )
				printf("%s: No partition table recognized\n",
					   device_name(current_hd->path));
			else
				command_errorx("%s: %s: Partition table not loaded",
					           argv[0], device_name(current_hd->path));
			return;
		}
		commands[i].function(argc, argv);
		return;
	}
	if ( !interactive )
		errx(1, "unrecognized command `%s'", argv[0]);
	fprintf(stderr, "Unrecognized command `%s'. "
	                "Try `help' for more information.\n", argv[0]);
}

static void execute(char* cmd)
{
	size_t argc = 0;
	char* argv[256];
	split_arguments(cmd, &argc, argv, 255);
	if ( argc < 1 )
		return;
	argv[argc] = NULL;
	execute_argv(argc, argv);
}

static void compact_arguments(int* argc, char*** argv)
{
	for ( int i = 0; i < *argc; i++ )
	{
		while ( i < *argc && !(*argv)[i] )
		{
			for ( int n = i; n < *argc; n++ )
				(*argv)[n] = (*argv)[n+1];
			(*argc)--;
		}
	}
}

static void help(FILE* fp, const char* argv0)
{
	fprintf(fp, "Usage: %s [OPTION]...\n", argv0);
	fprintf(fp, "Edit disk partition tables\n");
}

static void version(FILE* fp, const char* argv0)
{
	fprintf(fp, "%s (Sortix) %s\n", argv0, VERSIONSTR);
}

int main(int argc, char* argv[])
{
	setlocale(LC_ALL, "");

	bool set_interactive = false;

	const char* argv0 = argv[0];
	for ( int i = 1; i < argc; i++ )
	{
		const char* arg = argv[i];
		if ( arg[0] != '-' || !arg[1] )
			continue;
		argv[i] = NULL;
		if ( !strcmp(arg, "--") )
			break;
		if ( arg[1] != '-' )
		{
			char c;
			while ( (c = *++arg) ) switch ( c )
			{
			case 'i': interactive = true; set_interactive = true; break;
			case 'n': interactive = false; set_interactive = true; break;
			default:
				fprintf(stderr, "%s: unknown option -- '%c'\n", argv0, c);
				help(stderr, argv0);
				exit(1);
			}
		}
		else if ( !strcmp(arg, "--help") )
			help(stdout, argv0), exit(0);
		else if ( !strcmp(arg, "--version") )
			version(stdout, argv0), exit(0);
		else if ( !strncmp(arg, "--fstab=", strlen("--fstab=")) )
			fstab_path = arg + strlen("--fstab=");
		else if ( !strcmp(arg, "--fstab") )
		{
			if ( i + 1 == argc )
			{
				warn( "option '--fstab' requires an argument");
				fprintf(stderr, "Try `%s --help' for more information.\n", argv[0]);
				exit(125);
			}
			fstab_path = argv[i+1];
			argv[++i] = NULL;
		}
		else
		{
			fprintf(stderr, "%s: unknown option: %s\n", argv0, arg);
			help(stderr, argv0);
			exit(1);
		}
	}

	compact_arguments(&argc, &argv);

	if ( !set_interactive )
		interactive = isatty(0) && isatty(1) && argc < 3;

	if ( !devices_open_all(&hds, &hds_count) )
		err(1, "iterating devices");

	qsort(hds, hds_count, sizeof(struct harddisk*), harddisk_compare);

	if ( 2 <= argc )
	{
		struct harddisk* hd;
		if ( !lookup_harddisk_by_string(&hd, argv[0], argv[1]) )
			exit(1);
		switch_device(hd);
	}
	else if ( 1 <= hds_count )
		switch_device(hds[0]);

	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	if ( 3 <= argc )
		execute_argv(argc - 2, argv + 2);
	else while ( !quitting )
	{
		if ( interactive )
		{
			printf("\e[93m(%s)\e[m ",
			       current_hd ? device_name(current_hd->path) : "disked");
			fflush(stdout);
		}

		if ( (line_length = getline(&line, &line_size, stdin)) < 0 )
		{
			if ( interactive )
				printf("\n");
			break;
		}

		if ( line[line_length-1] == '\n' )
			line[--line_length] = '\0';

		execute(line);
	}
	free(line);

	if ( ferror(stdin) )
		err(1, "getline");

	for ( size_t i = 0; i < hds_count; i++ )
		harddisk_close(hds[i]);
	free(hds);

	return 0;
}
