Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

bootstripper.java

Raw
import static java.text.MessageFormat.format;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.stream.Collectors;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

class bootstripper {
	private final static String operating_system = System.getProperty("os.name");
	private final static boolean win32 = operating_system.toLowerCase().contains("win");
	private final static String filter_name = "dotfiles-aes";
	private final static String key_file = "dotfiles-aes.key";
	private final static String aes_cache_file = "dotfiles-aes.java";
	private final static String salt_file = ".dotfiles-aes-salt";
	private final static String secrets_file = ".secrets";
	private final static String attributes_begin = "# BEGIN bootstripper secrets";
	private final static String attributes_end = "# END bootstripper secrets";
	private final static String hook_marker = "# Managed by dorkfiles bootstripper.java";
	private final static byte[] aes_magic = new byte[] { 'D', 'F', 'A', 'E', 'S', '1' };
	private final static int salt_bytes = 32;
	private final static int derived_key_bits = 512;
	private final static int pbkdf2_iterations = 310_000;
	private final static List<String> warnings = new ArrayList<>();
	private final static List<String> errors = new ArrayList<>();

	public static void main(String[] arguments) throws Throwable {
		var command = "bootstrap";
		var dorkfile_path_input = ".";

		if (arguments.length == 1) {
			if (is_command(arguments[0]))
				command = arguments[0];
			else
				dorkfile_path_input = arguments[0];
		} else if (arguments.length == 2) {
			if (!is_command(arguments[0])) {
				usage();
				throw fail();
			}
			command = arguments[0];
			dorkfile_path_input = arguments[1];
		} else if (arguments.length > 2) {
			usage();
			throw fail();
		}

		var root = Path.of(dorkfile_path_input).toRealPath();
		if ("init-crypto".equals(command)) {
			init_crypto(root);
			return;
		}
		if ("check".equals(command)) {
			setup_crypto(root);
			install_repo_settings(root);
			check(root);
			return;
		}
		if ("pre-commit".equals(command)) {
			pre_commit(root);
			return;
		}
		if ("pre-push".equals(command)) {
			pre_push(root);
			return;
		}
		if ("install-hooks".equals(command)) {
			require_git_repo(root);
			install_hooks(root);
			return;
		}

		log("Assuming dorkfiles root repo in {0}.", emphasize_local(root.toString()));
		if ("clean".equals(command)) {
			var symlinks_file = symlinks_file(root);
			delete_symlinks(root, symlinks_file);
			return;
		}

		if ("link".equals(command)) {
			var symlinks_file = symlinks_file(root);
			check_symlink_sources(root);
			converge_crypto(root);
			install_repo_settings(root);
			install_hooks(root);
			create_symlinks(root, symlinks_file);
			print_attention(false);
			return;
		}

		if ("setup-crypto".equals(command)) {
			converge_crypto(root);
			return;
		}
		var symlinks_file = symlinks_file(root);
		check_symlink_sources(root);
		converge_crypto(root);
		install_repo_settings(root);
		install_hooks(root);
		check(root);
		create_symlinks(root, symlinks_file);
		print_attention(false);
	}

	static boolean is_command(String argument) {
		return "bootstrap".equals(argument) || "link".equals(argument) || "clean".equals(argument) || "check".equals(argument) || "pre-commit".equals(argument) || "pre-push".equals(argument) || "install-hooks".equals(argument) || "init-crypto".equals(argument) || "setup-crypto".equals(argument);
	}

	static void init_crypto(Path root) throws Throwable {
		require_git_repo(root);
		var salt = root.resolve(salt_file);
		if (Files.exists(salt)) {
			log("Crypto salt already exists: {0}", emphasize_local(salt_file));
			return;
		}
		var bytes = new byte[salt_bytes];
		new SecureRandom().nextBytes(bytes);
		Files.writeString(salt, Base64.getEncoder().encodeToString(bytes) + "\n", StandardCharsets.UTF_8);
		log("Created crypto salt: {0}", emphasize_local(salt_file));
	}

	static List<String> setup_crypto(Path root) throws Throwable {
		require_git_repo(root);
		var secrets = read_secrets(root);
		if (secrets.isEmpty())
			return secrets;
		validate_secret_paths(root, secrets);
		install_aes_tool(root);
		ensure_key(root, secrets);
		install_filter(root);
		write_attributes(root, secrets);
		remove_local_attributes(root);
		apply_secret_permissions(root, secrets);
		return secrets;
	}

	static void converge_crypto(Path root) throws Throwable {
		var secrets = setup_crypto(root);
		if (!secrets.isEmpty())
			restore_tracked_secrets(root, secrets);
	}

	static void require_git_repo(Path root) throws Throwable {
		run(root, List.of("git", "rev-parse", "--is-inside-work-tree"), null, false);
	}

	static Path git_common_path(Path root, String path) throws Throwable {
		var common = run(root, List.of("git", "rev-parse", "--git-common-dir"), null, false).text().strip();
		var common_path = Path.of(common);
		if (!common_path.isAbsolute())
			common_path = root.resolve(common_path).normalize();
		return common_path.resolve(path).toAbsolutePath();
	}

	static Path key_path(Path root) throws Throwable {
		return git_common_path(root, key_file);
	}

	static String key_argument(Path root) throws Throwable {
		return key_path(root).toString();
	}

	static String aes_tool_argument(Path root) throws Throwable {
		return git_common_path(root, aes_cache_file).toString();
	}

	static String clean_filter_command(Path root) throws Throwable {
		return "java " + quote_shell(aes_tool_argument(root)) + " encrypt --key-file " + quote_shell(key_argument(root));
	}

	static String smudge_filter_command(Path root) throws Throwable {
		return "java " + quote_shell(aes_tool_argument(root)) + " decrypt --key-file " + quote_shell(key_argument(root));
	}

	static String merge_filter_command(Path root) throws Throwable {
		return "java " + quote_shell(aes_tool_argument(root)) + " merge --key-file " + quote_shell(key_argument(root))
				+ " " + quote_shell("%O") + " " + quote_shell("%A") + " " + quote_shell("%B");
	}

	static List<String> read_secrets(Path root) throws IOException {
		var path = root.resolve(secrets_file);
		if (!Files.exists(path))
			return List.of();
		var secrets = new ArrayList<String>();
		for (var raw : Files.readAllLines(path)) {
			var line = raw.strip();
			if (line.isEmpty() || line.startsWith("#"))
				continue;
			if (line.contains(" ") || line.contains("\t") || line.startsWith("/") || line.contains("\\")
					|| line.startsWith(":") || line.contains("*") || line.contains("?") || line.contains("["))
				throw new IllegalArgumentException("invalid secret path: " + line);
			secrets.add(line);
		}
		return secrets;
	}

	static void install_aes_tool(Path root) throws Throwable {
		var source = root.resolve("tools/aes.java");
		if (!Files.isRegularFile(source, LinkOption.NOFOLLOW_LINKS))
			throw fail("Expected a regular AES tool source at: {0}", source);
		var target = git_common_path(root, aes_cache_file);
		if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
			if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS))
				throw fail("Refusing to replace non-file cached AES tool: {0}", target);
			if (Files.mismatch(source, target) == -1L)
				return;
		}
		Files.createDirectories(target.getParent());
		var temporary = Files.createTempFile(target.getParent(), "." + aes_cache_file + ".", ".tmp");
		try {
			Files.copy(source, temporary, StandardCopyOption.REPLACE_EXISTING);
			try {
				Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
			} catch (AtomicMoveNotSupportedException ignored) {
				Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING);
			}
		} finally {
			Files.deleteIfExists(temporary);
		}
	}

	static void validate_secret_paths(Path root, List<String> secrets) throws Throwable {
		for (var secret : secrets) {
			var original = Path.of(secret);
			for (var component : original)
				if (component.toString().equals(".."))
					throw fail("Secret path must not contain parent components: {0}", secret);
			var relative = original.normalize();
			if (relative.isAbsolute() || relative.startsWith(".."))
				throw fail("Secret path must stay inside the repository: {0}", secret);
			var current = root;
			for (var component : relative) {
				current = current.resolve(component);
				if (Files.isSymbolicLink(current))
					throw fail("Secret path must not contain symlinks: {0}", secret);
			}
			if (Files.exists(current, LinkOption.NOFOLLOW_LINKS) && !Files.isRegularFile(current, LinkOption.NOFOLLOW_LINKS))
				throw fail("Secret path must name a regular file: {0}", secret);
		}
	}

	static void install_filter(Path root) throws Throwable {
		set_local_config(root, "filter." + filter_name + ".clean", clean_filter_command(root));
		set_local_config(root, "filter." + filter_name + ".smudge", smudge_filter_command(root));
		set_local_config(root, "filter." + filter_name + ".required", "true");
		set_local_config(root, "merge." + filter_name + ".driver", merge_filter_command(root));
	}

	// Autostash is a trap in this repository: the worktree is nearly always dirty, so
	// every rebase or merge stashes first, and the required crypto filter plus the Git
	// hooks can abort the operation before the stash is restored. The changes then sit
	// in a silent `autostash` stash entry while the files look deleted. Refuse instead.
	static void install_repo_settings(Path root) throws Throwable {
		set_local_config(root, "rebase.autostash", "false");
		set_local_config(root, "merge.autostash", "false");
	}

	static void check_repo_settings(Path root) throws Throwable {
		check_local_config(root, "rebase.autostash", "false");
		check_local_config(root, "merge.autostash", "false");
	}

	static void check_local_config(Path root, String name, String value) throws Throwable {
		var current = run(root, List.of("git", "config", "--local", "--get", name), null, false, false);
		if (current.exit != 0 || !current.text().strip().equals(value))
			throw fail("Local Git setting {0} must be {1}. Run `mise run link`.", name, value);
	}

	static void set_local_config(Path root, String name, String value) throws Throwable {
		var current = run(root, List.of("git", "config", "--local", "--get", name), null, false, false);
		if (current.exit != 0 || !current.text().strip().equals(value))
			run(root, List.of("git", "config", "--local", name, value), null, false);
	}

	static String quote_shell(String argument) {
		return "'" + argument.replace("'", "'\\''") + "'";
	}

	static void write_attributes(Path root, List<String> secrets) throws IOException {
		var attributes = root.resolve(".gitattributes");
		var content = Files.exists(attributes) ? without_managed_attributes(Files.readString(attributes)) : "";
		if (!content.isEmpty() && !content.endsWith("\n"))
			content += "\n";
		var lines = new ArrayList<String>();
		lines.add(attributes_begin);
		for (var secret : secrets)
			lines.add(secret + " -text filter=" + filter_name + " -diff merge=" + filter_name);
		lines.add(attributes_end);
		var desired = content + String.join("\n", lines) + "\n";
		if (!Files.exists(attributes) || !Files.readString(attributes).equals(desired))
			Files.writeString(attributes, desired, StandardCharsets.UTF_8);
	}

	static void remove_local_attributes(Path root) throws Throwable {
		var attributes = git_common_path(root, "info/attributes");
		if (!Files.exists(attributes))
			return;
		var previous = Files.readString(attributes);
		var content = without_managed_attributes(previous);
		if (!content.equals(previous))
			Files.writeString(attributes, content, StandardCharsets.UTF_8);
	}

	static String without_managed_attributes(String content) {
		var lines = new ArrayList<String>();
		var in_block = false;
		for (var line : content.split("\\R", -1)) {
			if (attributes_begin.equals(line)) {
				in_block = true;
				continue;
			}
			if (attributes_end.equals(line)) {
				in_block = false;
				continue;
			}
			if (!in_block)
				lines.add(line);
		}
		return String.join("\n", lines);
	}

	static void ensure_key(Path root, List<String> secrets) throws Throwable {
		var key = key_path(root);
		if (Files.exists(key, LinkOption.NOFOLLOW_LINKS)) {
			validate_key(key);
			ensure_permissions(key, "rw-------");
			return;
		}
		Files.createDirectories(key.getParent());
		var temporary = Files.createTempFile(key.getParent(), key_file + ".", ".tmp");
		char[] passphrase = null;
		try {
			passphrase = passphrase();
			var factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
			var spec = new PBEKeySpec(passphrase, read_salt(root), pbkdf2_iterations, derived_key_bits);
			var encoded = factory.generateSecret(spec).getEncoded();
			Files.writeString(temporary, Base64.getEncoder().encodeToString(encoded) + "\n", StandardCharsets.UTF_8);
			ensure_permissions(temporary, "rw-------");
			if (!authenticate_key(root, secrets, temporary))
				self_test_aes(root, temporary);
			try {
				Files.move(temporary, key, StandardCopyOption.ATOMIC_MOVE);
			} catch (AtomicMoveNotSupportedException ignored) {
				Files.move(temporary, key);
			}
			log("Cached dotfiles AES key in {0}", emphasize_local(key.toString()));
		} finally {
			if (passphrase != null)
				Arrays.fill(passphrase, '\0');
			Files.deleteIfExists(temporary);
		}
	}

	static void validate_key(Path key) throws IOException {
		if (!Files.isRegularFile(key, LinkOption.NOFOLLOW_LINKS) || !Files.isReadable(key))
			throw new IllegalStateException("Dotfiles AES key must be a readable regular file: " + key);
		try {
			if (Base64.getDecoder().decode(Files.readString(key).trim()).length != derived_key_bits / 8)
				throw new IllegalStateException("Dotfiles AES key has the wrong length: " + key);
		} catch (IllegalArgumentException problem) {
			throw new IllegalStateException("Dotfiles AES key is not valid Base64: " + key);
		}
	}

	static boolean authenticate_key(Path root, List<String> secrets, Path key) throws Throwable {
		String encrypted_secret = null;
		for (var secret : secrets) {
			var committed = run(root, List.of("git", "cat-file", "blob", "HEAD:" + secret), null, false, false);
			if (committed.exit != 0 || !starts_with(committed.stdout, aes_magic))
				continue;
			encrypted_secret = secret;
			var decrypted = run(root, List.of("java", aes_tool_argument(root), "decrypt", "--key-file", key.toString()), committed.stdout, false, false);
			if (decrypted.exit == 0)
				return true;
		}
		if (encrypted_secret != null)
			throw new IllegalStateException("Passphrase does not decrypt committed secrets, including: " + encrypted_secret);
		return false;
	}

	static byte[] read_salt(Path root) throws IOException {
		var path = root.resolve(salt_file);
		if (!Files.exists(path))
			throw new IllegalStateException("Missing " + salt_file + ". Run `java bootstripper.java init-crypto` once for this repo.");
		return Base64.getDecoder().decode(Files.readString(path).trim());
	}

	static char[] passphrase() {
		var from_env = System.getenv("DOTFILES_AES_PASSPHRASE");
		if (from_env != null && !from_env.isEmpty())
			return from_env.toCharArray();
		var console = System.console();
		if (console == null)
			throw new IllegalStateException("No console available. Set DOTFILES_AES_PASSPHRASE for non-interactive bootstrap.");
		var first = console.readPassword("[%s]:", emphasize_global("Dotfiles AES passphrase"));
		var second = console.readPassword("[%s]:", emphasize_global("Repeat passphrase"));
		if (!Arrays.equals(first, second))
			throw new IllegalStateException("Passphrases do not match.");
		Arrays.fill(second, '\0');
		return first;
	}

	static void self_test_aes(Path root, Path key) throws Throwable {
		var plain = "bootstripper self-test".getBytes(StandardCharsets.UTF_8);
		var aes = aes_tool_argument(root);
		var encrypted = run(root, List.of("java", aes, "encrypt", "--key-file", key.toString()), plain, false, false);
		if (encrypted.exit != 0 || !starts_with(encrypted.stdout, aes_magic))
			throw new IllegalStateException("AES self-test encryption failed: " + encrypted.error().strip());
		var decrypted = run(root, List.of("java", aes, "decrypt", "--key-file", key.toString()), encrypted.stdout, false, false);
		if (decrypted.exit != 0 || !Arrays.equals(plain, decrypted.stdout))
			throw new IllegalStateException("AES self-test decryption failed: " + decrypted.error().strip());
	}

	static void restore_tracked_secrets(Path root, List<String> secrets) throws Throwable {
		var tree_command = new ArrayList<>(List.of("git", "--literal-pathspecs", "ls-tree", "--name-only", "-z", "HEAD", "--"));
		tree_command.addAll(secrets);
		var tracked_output = run(root, tree_command, null, false, false);
		if (tracked_output.exit != 0)
			return;
		var tracked = new LinkedHashSet<String>();
		for (var path : new String(tracked_output.stdout, StandardCharsets.UTF_8).split("\u0000"))
			if (!path.isEmpty()) tracked.add(path);
		if (tracked.isEmpty())
			return;

		var staged_command = new ArrayList<>(List.of("git", "--literal-pathspecs", "diff", "--cached", "--quiet", "HEAD", "--"));
		staged_command.addAll(tracked);
		if (run(root, staged_command, null, false, false).exit != 0) {
			for (var secret : tracked)
				if (run(root, List.of("git", "--literal-pathspecs", "diff", "--cached", "--quiet", "HEAD", "--", secret), null, false, false).exit != 0)
					throw fail("Refusing to restore locally changed secret: {0}", secret);
		}

		var current = new LinkedHashMap<String, byte[]>();
		var needs_restoration = false;
		for (var secret : tracked) {
			var path = root.resolve(secret);
			if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
				throw fail("Refusing to restore locally changed secret: {0}", secret);
			var bytes = Files.readAllBytes(path);
			current.put(secret, bytes);
			needs_restoration |= starts_with(bytes, aes_magic);
		}
		if (!needs_restoration)
			return;

		var pending = new LinkedHashMap<Path, byte[]>();
		for (var secret : tracked) {
			var committed = run(root, List.of("git", "cat-file", "blob", "HEAD:" + secret), null, false);
			var plain = run(root, List.of("java", aes_tool_argument(root), "decrypt", "--key-file", key_argument(root)), committed.stdout, false).stdout;
			var bytes = current.get(secret);
			if (Arrays.equals(bytes, committed.stdout))
				pending.put(root.resolve(secret), plain);
			else if (!Arrays.equals(bytes, plain))
				throw fail("Refusing to restore locally changed secret: {0}", secret);
		}
		for (var entry : pending.entrySet())
			Files.write(entry.getKey(), entry.getValue());
	}

	static void apply_secret_permissions(Path root, List<String> secrets) throws IOException {
		for (var secret : secrets) {
			var path = root.resolve(secret);
			if (Files.exists(path) && secret.startsWith("ssh/") && secret.endsWith(".key"))
				ensure_permissions(path, "rw-------");
		}
	}

	static void install_hooks(Path root) throws Throwable {
		var hooks = git_hooks_path(root);
		var pre_commit = hooks.resolve("pre-commit");
		var pre_push = hooks.resolve("pre-push");
		var legacy_pre_commit = "#!/bin/sh\ncd \"$(git rev-parse --show-toplevel)\" || exit 1\nexec java bootstripper.java pre-commit\n";
		var pre_commit_content = hook_content("pre-commit");
		var pre_push_content = hook_content("pre-push");
		check_hook_destination(pre_commit, pre_commit_content, legacy_pre_commit);
		check_hook_destination(pre_push, pre_push_content, null);
		install_hook(hooks, pre_commit, pre_commit_content);
		install_hook(hooks, pre_push, pre_push_content);
	}

	static Path git_hooks_path(Path root) throws Throwable {
		var configured = run(root, List.of("git", "config", "--show-scope", "--get", "core.hooksPath"), null, false, false);
		if (configured.exit == 0 && !configured.text().matches("(?s)^(local|worktree)\\s+.*"))
			throw fail("Refusing to modify inherited core.hooksPath. Configure a repository-local hooks path or unset it.");
		var result = run(root, List.of("git", "rev-parse", "--path-format=absolute", "--git-path", "hooks"), null, false).text().strip();
		var hooks = Path.of(result).toAbsolutePath().normalize();
		if (hooks.equals(Path.of("/dev/null")) || Files.exists(hooks, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(hooks, LinkOption.NOFOLLOW_LINKS))
			throw fail("Invalid Git hooks directory: {0}", hooks);
		return hooks;
	}

	static String hook_content(String command) {
		return "#!/bin/sh\n" + hook_marker + "\ncd \"$(git rev-parse --show-toplevel)\" || exit 1\nexec java bootstripper.java " + command + "\n";
	}

	static void check_hook_destination(Path hook, String expected, String legacy) throws Throwable {
		if (!Files.exists(hook, LinkOption.NOFOLLOW_LINKS))
			return;
		if (Files.isSymbolicLink(hook) || !Files.isRegularFile(hook, LinkOption.NOFOLLOW_LINKS))
			throw fail("Refusing to replace non-file Git hook: {0}", hook);
		var content = Files.readString(hook);
		if (content.equals(expected) || content.equals(legacy) || content.contains("\n" + hook_marker + "\n"))
			return;
		throw fail("Refusing to replace unmanaged Git hook: {0}", hook);
	}

	static void install_hook(Path hooks, Path hook, String content) throws Throwable {
		var content_matches = Files.isRegularFile(hook, LinkOption.NOFOLLOW_LINKS) && Files.readString(hook).equals(content);
		var permissions_match = hook_permissions_match(hook);
		if (content_matches && permissions_match)
			return;
		Files.createDirectories(hooks);
		if (!content_matches) {
			var temporary = Files.createTempFile(hooks, "." + hook.getFileName() + ".", ".tmp");
			try {
				Files.writeString(temporary, content, StandardCharsets.UTF_8);
				chmod(temporary, "rwxr-xr-x");
				try {
					Files.move(temporary, hook, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
				} catch (AtomicMoveNotSupportedException ignored) {
					Files.move(temporary, hook, StandardCopyOption.REPLACE_EXISTING);
				}
			} finally {
				Files.deleteIfExists(temporary);
			}
		} else {
			chmod(hook, "rwxr-xr-x");
		}
		log("Installed Git hook: {0}", emphasize_local(hook.toString()));
	}

	static boolean hook_permissions_match(Path hook) throws IOException {
		if (win32)
			return true;
		try {
			return Files.exists(hook) && Files.getPosixFilePermissions(hook).equals(PosixFilePermissions.fromString("rwxr-xr-x"));
		} catch (UnsupportedOperationException ignored) {
			return Files.isExecutable(hook);
		}
	}

	static void pre_commit(Path root) throws Throwable {
		run_gitleaks(root, List.of("--pre-commit", "--staged"));
		check_staged_matches_worktree(root, secrets_file);
		check_staged_matches_worktree(root, ".gitattributes");
		check(root);
	}

	static void pre_push(Path root) throws Throwable {
		for (var line : new String(System.in.readAllBytes(), StandardCharsets.UTF_8).lines().toList()) {
			if (line.isBlank())
				continue;
			var fields = line.strip().split("\\s+");
			if (fields.length != 4)
				throw fail("Invalid pre-push input: expected four fields.");
			var local_oid = fields[1];
			var remote_oid = fields[3];
			if (is_zero_oid(local_oid))
				continue;
			if (!is_oid(local_oid) || !is_oid(remote_oid))
				throw fail("Invalid object ID in pre-push input.");
			var local_commit = resolve_commit(root, local_oid, "local");
			var remote_commit = run(root, List.of("git", "rev-parse", "--verify", remote_oid + "^{commit}"), null, false, false);
			var revisions = is_zero_oid(remote_oid) || remote_commit.exit != 0
					? local_commit
					: remote_commit.text().strip() + ".." + local_commit;
			var log_options = "--full-history --diff-merges=first-parent --no-ext-diff --no-textconv " + revisions;
			run_gitleaks(root, List.of("--platform=none", "--log-opts=" + log_options));
		}
	}

	static boolean is_oid(String value) {
		return value.matches("[0-9a-fA-F]{40}|[0-9a-fA-F]{64}");
	}

	static boolean is_zero_oid(String value) {
		return is_oid(value) && value.chars().allMatch(character -> character == '0');
	}

	static String resolve_commit(Path root, String oid, String label) throws Throwable {
		var result = run(root, List.of("git", "rev-parse", "--verify", oid + "^{commit}"), null, false, false);
		if (result.exit != 0)
			throw fail("Pre-push {0} object is not a commit: {1}", label, oid);
		return result.text().strip();
	}

	static void run_gitleaks(Path root, List<String> scan_arguments) throws Throwable {
		var command = new ArrayList<>(List.of("gitleaks", "git"));
		command.addAll(scan_arguments);
		command.addAll(List.of("--redact=100", "--no-banner"));
		Result result;
		try {
			result = run(root, command, null, true, false);
		} catch (IOException error) {
			throw fail("Gitleaks is required for Git hooks. Install Gitleaks, ensure it is on Git''s PATH, then retry.");
		}
		if (result.exit != 0)
			throw fail("Gitleaks blocked the operation (exit {0}). Review its redacted output.", result.exit);
	}

	static void check_staged_matches_worktree(Path root, String path) throws Throwable {
		var tracked = run(root, List.of("git", "ls-files", "--error-unmatch", "--", path), null, false, false);
		var changed = run(root, List.of("git", "-c", "diff.autoRefreshIndex=false", "diff", "--no-ext-diff", "--no-textconv", "--quiet", "--", path), null, false, false);
		if (tracked.exit != 0 || changed.exit != 0)
			throw fail("{0} must match the staged version.", path);
	}

	static void check(Path root) throws Throwable {
		var secrets = read_secrets(root);
		if (!secrets.isEmpty()) {
			if (!Files.isReadable(key_path(root)))
				throw fail("Dotfiles AES key is unavailable. Run `mise run setup-crypto`, then retry.");
			check_filter(root);
			check_attributes(root, secrets);
			check_secrets_not_ignored(root, secrets);
			check_worktree_secrets_decrypted(root, secrets);
			check_key_not_tracked(root);
			check_staged_secrets(root, secrets);
		}
		check_repo_settings(root);
		check_symlink_sources(root);
		log("OK bootstripper check passed.");
	}

	static void check_filter(Path root) throws Throwable {
		var clean = run(root, List.of("git", "config", "--local", "--get", "filter." + filter_name + ".clean"), null, false).text().strip();
		var smudge = run(root, List.of("git", "config", "--local", "--get", "filter." + filter_name + ".smudge"), null, false).text().strip();
		var required = run(root, List.of("git", "config", "--local", "--get", "filter." + filter_name + ".required"), null, false).text().strip();
		var merge = run(root, List.of("git", "config", "--local", "--get", "merge." + filter_name + ".driver"), null, false).text().strip();
		if (!clean.equals(clean_filter_command(root)))
			throw fail("Unexpected clean filter: {0}", clean);
		if (!smudge.equals(smudge_filter_command(root)))
			throw fail("Unexpected smudge filter: {0}", smudge);
		if (!required.equals("true"))
			throw fail("Filter is not required.");
		if (!merge.equals(merge_filter_command(root)))
			throw fail("Unexpected merge driver: {0}", merge);
	}

	static void check_attributes(Path root, List<String> secrets) throws Throwable {
		for (var secret : secrets) {
			var output = run(root, List.of("git", "--literal-pathspecs", "check-attr", "text", "filter", "diff", "merge", "--", secret), null, false).text();
			if (!output.contains(secret + ": text: unset"))
				throw fail("Missing -text attribute for {0}: {1}", secret, output.strip());
			if (!output.contains(secret + ": filter: " + filter_name))
				throw fail("Missing filter attribute for {0}: {1}", secret, output.strip());
			if (!output.contains(secret + ": diff: unset"))
				throw fail("Missing -diff attribute for {0}: {1}", secret, output.strip());
			if (!output.contains(secret + ": merge: " + filter_name))
				throw fail("Missing merge attribute for {0}: {1}", secret, output.strip());
		}
	}

	static void check_secrets_not_ignored(Path root, List<String> secrets) throws Throwable {
		var command = new ArrayList<String>(List.of("git", "check-ignore", "--no-index", "--verbose", "--"));
		command.addAll(secrets);
		var result = run(root, command, null, false, false);
		if (result.exit == 0)
			throw fail("Secret paths are matched by ignore rules, so they can never be encrypted or staged: {0}", result.text().strip());
		if (result.exit != 1)
			throw fail("Unable to check ignore rules for secrets: {0}", result.text().strip());
	}

	static void check_worktree_secrets_decrypted(Path root, List<String> secrets) throws Throwable {
		for (var secret : secrets) {
			var path = root.resolve(secret);
			if (Files.exists(path) && starts_with(Files.readAllBytes(path), aes_magic))
				throw fail("Worktree secret is encrypted; smudge failed: {0}", secret);
		}
	}

	static void check_key_not_tracked(Path root) throws Throwable {
		var bad = run(root, List.of("git", "ls-files", "--", ".git/" + key_file, key_file), null, false).text().strip();
		if (!bad.isEmpty())
			throw fail("Key file is tracked: {0}", key_file);
		var staged = run(root, List.of("git", "diff", "--cached", "--name-only", "--", ".git/" + key_file, key_file), null, false).text().strip();
		if (!staged.isEmpty())
			throw fail("Key file is staged: {0}", key_file);
	}

	static void check_staged_secrets(Path root, List<String> secrets) throws Throwable {
		for (var secret : secrets) {
			var status = run(root, List.of("git", "--literal-pathspecs", "diff", "--cached", "--name-status", "--", secret), null, false).text().strip();
			if (status.isEmpty() || status.startsWith("D"))
				continue;
			var blob = run(root, List.of("git", "show", ":" + secret), null, false).stdout;
			if (!starts_with(blob, aes_magic))
				throw fail("Staged secret is not encrypted: {0}", secret);
			run(root, List.of("java", aes_tool_argument(root), "decrypt", "--key-file", key_argument(root)), blob, false);
		}
	}

	static void check_symlink_sources(Path root) throws Throwable {
		try (var stream = Files.list(root)) {
			var files = stream.filter(path -> path.getFileName().toString().endsWith(".symlinks")).collect(Collectors.toList());
			for (var file : files) {
				var targets = new HashSet<Path>();
				var lines = Files.readAllLines(file);
				String source = null;
				var source_line = 0;
				var line_number = 0;
				for (var raw : lines) {
					line_number++;
					var line = raw.strip();
					if (line.isEmpty())
						continue;
					if (source == null) {
						source = line;
						source_line = line_number;
						continue;
					}
					var source_path = root.resolve(source);
					if (!Files.exists(source_path, LinkOption.NOFOLLOW_LINKS))
						throw fail("Missing symlink source: {0}:{1}: {2}", file.getFileName(), source_line, source);
					var target_path = expand_target(line).normalize();
					if (!targets.add(target_path))
						throw fail("Duplicate symlink target: {0}:{1}: {2}", file.getFileName(), line_number, line);
					source = null;
				}
				if (source != null)
					throw fail("Missing symlink target after: {0}: {1}", file.getFileName(), source);
			}
		}
	}

	static Path symlinks_file(Path root) throws Throwable {
		var hostname = hostname();
		var manifest = symlinks_manifest(root, hostname);
		log("Hostname: {0}", emphasize_global(hostname));
		log("Symlinks manifest: {0}", emphasize_global(manifest + ".symlinks"));
		var symlinks_file = root.resolve(manifest + ".symlinks");
		if (!Files.exists(symlinks_file))
			throw fail("Expected a file containing symlinks at: {0}. Found nothing!", symlinks_file);
		return symlinks_file;
	}

	static String symlinks_manifest(Path root, String hostname) throws IOException {
		var explicit = System.getenv("DOTFILES_SYMLINKS");
		if (explicit != null && !explicit.isBlank())
			return explicit.replaceFirst("\\.symlinks$", "");
		return hostname;
	}

	static void create_symlinks(Path root, Path symlinks_file) throws Throwable {
		log("Creating symlinks as defined in {0} file.", emphasize_local(symlinks_file.getFileName().toString()));
		var lines = Files.readAllLines(symlinks_file);
		for_each_mapping(lines, (source, target) -> link(root, source, target));
	}

	static void delete_symlinks(Path root, Path symlinks_file) throws Throwable {
		log("Deleting symlinks as defined in {0} file.", emphasize_local(symlinks_file.getFileName().toString()));
		var lines = Files.readAllLines(symlinks_file);
		for_each_mapping(lines, (source, target) -> delete(root, source, target));
	}

	static void delete(Path root, String source, String target) throws Throwable {
		var source_path = root.resolve(source).toAbsolutePath();
		var target_path = expand_target(target);

		if (Files.exists(target_path, LinkOption.NOFOLLOW_LINKS)) {
			if (Files.isSymbolicLink(target_path)) {
				log("\nOK {0} exists and points to {1}, a symlink indeed!\nDeleting {0}.", emphasize_global(target_path.toString()), emphasize_local(source_path.toString()));
				Files.delete(target_path);
			} else {
				log("\nNOPE {0} exists, but it is not a symlink! Clean manually.", target_path.toString());
			}
		}
	}

	static void link(Path root, String source, String target) throws Throwable {
		var source_path = root.resolve(source).toAbsolutePath().normalize();
		if (!Files.exists(source_path)) {
			log("\n{0} does not exist anymore in my dorkfiles. Have I yeeted it?", emphasize_global(source_path.toString()));
			return;
		}
		var expected = source_path.toRealPath();
		require_inside_home(expected, "Symlink source");
		var target_path = expand_target(target);
		if (Files.exists(target_path, LinkOption.NOFOLLOW_LINKS)) {
			if (Files.isSymbolicLink(target_path)) {
				var raw_actual = symlink_target_path(target_path);
				if (!inside_home(raw_actual))
					throw fail("Target symlink points outside HOME: {0} -> {1}. Move it manually.", target_path, raw_actual);
				if (!Files.exists(target_path)) {
					log("\nRELINK {0}: broken symlink.", emphasize_global(target_path.toString()));
					remove_owned_symlink(target_path);
				} else {
					var actual = target_path.toRealPath();
					if (actual.equals(expected)) {
						log("\nOK {0} -> {1}.", emphasize_global(target_path.toString()), emphasize_local(source_path.toString()));
						return;
					}
					if (!inside_home(actual))
						throw fail("Target symlink points outside HOME: {0} -> {1}. Move it manually.", target_path, actual);
					else {
						if (!actual.startsWith(root.toRealPath()))
							throw fail("Target symlink points outside dorkfiles: {0} -> {1}. Move it manually.", target_path, actual);
						log("\nRELINK {0}: {1} -> {2}.", emphasize_global(target_path.toString()), emphasize_local(actual.toString()), emphasize_local(expected.toString()));
						remove_owned_symlink(target_path);
					}
				}
			} else if (Files.exists(target_path)) {
				var actual = target_path.toRealPath();
				if (actual.equals(expected)) {
					log("\nOK {0} resolves to {1}.", emphasize_global(target_path.toString()), emphasize_local(source_path.toString()));
					return;
				}
				var backup = sibling_backup_path(target_path);
				var message = format("BACKUP {0} TO {1}.", target_path, backup);
				log("\n{0}", message);
				warn(message);
				Files.move(target_path, backup);
			} else {
				log("\nNOPE {0} exists, but it points into nirvana. Removing broken link!", target_path.toString());
				Files.delete(target_path);
			}
		}
		log("\nLINK {0} TO {1}.", emphasize_local(source_path.toString()), emphasize_local(target_path.toString()));
		Files.createDirectories(target_path.getParent());
		if (target_path.getParent().endsWith(".ssh"))
			chmod(target_path.getParent(), "rwx------");
		Files.createSymbolicLink(target_path, source_path);
	}

	static void remove_owned_symlink(Path target_path) throws Throwable {
		if (!win32) {
			Files.delete(target_path);
			return;
		}
		var retired = sibling_path(target_path, "replaced");
		Files.move(target_path, retired);
		try {
			Files.delete(retired);
		} catch (IOException error) {
			log("WIN32 kept retired link for later cleanup: {0}", retired);
		}
	}

	static Path symlink_target_path(Path target_path) throws IOException {
		var link = Files.readSymbolicLink(target_path);
		var resolved = link.isAbsolute() ? link : target_path.getParent().resolve(link);
		return resolved.toAbsolutePath().normalize();
	}

	static Path sibling_backup_path(Path target_path) throws Throwable {
		return sibling_path(target_path, "backup");
	}

	static Path sibling_path(Path target_path, String kind) throws Throwable {
		var stamp = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now());
		var base = target_path.resolveSibling(target_path.getFileName() + ".dorkfiles-" + kind + "-" + stamp);
		var candidate = base;
		var suffix = 1;
		while (Files.exists(candidate, LinkOption.NOFOLLOW_LINKS)) {
			candidate = Path.of(base + "." + suffix);
			suffix++;
		}
		return candidate;
	}

	static Path expand_target(String target) throws Throwable {
		var path = switch (target) {
			case "~" -> target_home_path();
			default -> target.startsWith("~/") ? target_home_path().resolve(target.substring(2)) : Path.of(target).toAbsolutePath();
		};
		path = path.normalize();
		require_inside_home(path, "Symlink target");
		return path;
	}

	static Path target_home_path() {
		return Path.of(System.getProperty("user.home")).toAbsolutePath().normalize();
	}

	static Path home_path() {
		var home = System.getenv("HOME");
		if (home == null || home.isBlank())
			home = System.getProperty("user.home");
		return Path.of(home).toAbsolutePath().normalize();
	}

	static boolean inside_home(Path path) {
		return path.toAbsolutePath().normalize().startsWith(home_path());
	}

	static void require_inside_home(Path path, String label) throws Throwable {
		if (!inside_home(path))
			throw fail("{0} outside HOME: {1}", label, path.toAbsolutePath().normalize());
	}

	static String hostname() throws Exception {
		var computername = System.getenv("COMPUTERNAME");
		if (computername != null && !computername.isBlank())
			return computername;
		var wsl_distro = System.getenv("WSL_DISTRO_NAME");
		if (wsl_distro != null && !wsl_distro.isBlank())
			return wsl_distro;
		var hostname = System.getenv("HOSTNAME");
		if (hostname != null && !hostname.isBlank() && !"localhost".equals(hostname))
			return hostname;
		var detected = InetAddress.getLocalHost().getHostName();
		if ("localhost".equals(detected) && System.getenv("PREFIX") != null)
			return "termux";
		return detected;
	}

	static void ensure_permissions(Path path, String permissions) throws IOException {
		if (win32)
			return;
		try {
			var desired = PosixFilePermissions.fromString(permissions);
			if (!Files.getPosixFilePermissions(path).equals(desired))
				Files.setPosixFilePermissions(path, desired);
		} catch (UnsupportedOperationException ignored) {}
	}

	static void chmod(Path path, String permissions) throws IOException {
		if (win32)
			return;
		try {
			Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissions));
		} catch (UnsupportedOperationException ignored) {
		}
	}

	static boolean starts_with(byte[] bytes, byte[] prefix) {
		if (bytes.length < prefix.length)
			return false;
		for (var index = 0; index < prefix.length; index++)
			if (bytes[index] != prefix[index])
				return false;
		return true;
	}

	static Result run(Path directory, List<String> command, byte[] input, boolean inherit_io) throws Throwable {
		return run(directory, command, input, inherit_io, true);
	}

	static Result run(Path directory, List<String> command, byte[] input, boolean inherit_io, boolean fail) throws Throwable {
		var process_builder = new ProcessBuilder(command).directory(directory.toFile());
		if (inherit_io)
			process_builder.inheritIO();
		var process = process_builder.start();
		if (input != null) {
			try (var stdin = process.getOutputStream()) {
				stdin.write(input);
			}
		} else {
			process.getOutputStream().close();
		}
		var stdout = inherit_io ? new byte[0] : process.getInputStream().readAllBytes();
		var stderr = inherit_io ? new byte[0] : process.getErrorStream().readAllBytes();
		var exit = process.waitFor();
		var result = new Result(exit, stdout, stderr);
		if (fail && exit != 0)
			throw fail("Command failed ({0}): {1}\n{2}", exit, String.join(" ", command), result.error().strip());
		return result;
	}

	static void usage() {
		log("Bootstraps bugabingas {0}.", emphasize_local("dorkfiles"));
		log("{0}: java bootstripper.java [bootstrap|link|init-crypto|setup-crypto|check|pre-commit|pre-push|install-hooks|clean] [dotfiles root path]", emphasize_global("Usage"));
	}

	static String emphasize_global(String message) {
		return "\033[4m" + message + "\033[0m";
	}

	static String emphasize_local(String message) {
		return "\033[3m" + message + "\033[0m";
	}

	static void warn(String message) {
		warnings.add(message);
	}

	static void error(String message) {
		errors.add(message);
	}

	static void print_attention(boolean fatal) {
		if (warnings.isEmpty() && errors.isEmpty())
			return;
		var color = fatal || !errors.isEmpty() ? "\033[31m" : "\033[33m";
		System.out.println();
		System.out.println(color + "ATTENTION");
		for (var warning : warnings)
			System.out.println("- " + warning);
		for (var error : errors)
			System.out.println("- " + error);
		System.out.println("\033[0m");
	}

	static Throwable fail(String message, Object... arguments) {
		error(format(message, arguments));
		return fail();
	}

	static Throwable fail() {
		print_attention(true);
		System.exit(-1);
		return new Throwable("use 'throw fail()' for control flow");
	}

	static void log(String message, Object... arguments) {
		System.out.println(format(message, arguments));
	}

	static interface ThrowUp<A, B> {
		default Throwable gurgh(A a, B b) {
			try {
				apply(a, b);
				return null;
			} catch (Throwable vomit) {
				return vomit;
			}
		}

		void apply(A a, B b) throws Throwable;
	}

	static void for_each_mapping(List<String> lines, ThrowUp<String, String> block) throws Throwable {
		String source = null;
		String target = null;
		for (String line : lines) {
			if (line.isBlank())
				continue;
			if (source == null)
				source = line;
			else
				target = line;
			if (target != null) {
				var vomit = block.gurgh(source, target);
				if (vomit != null)
					throw vomit;
				source = null;
				target = null;
			}
		}
	}

	static class Result {
		final int exit;
		final byte[] stdout;
		final byte[] stderr;

		Result(int exit, byte[] stdout, byte[] stderr) {
			this.exit = exit;
			this.stdout = stdout;
			this.stderr = stderr;
		}

		String text() {
			return new String(stdout, StandardCharsets.UTF_8);
		}

		String error() {
			return new String(stderr, StandardCharsets.UTF_8);
		}
	}
}