commit bfbb35d091d986579edafe7b01ce0ce12dc1a27d
parent 0cae73baf7706b733eec0fadee2e11c7eb25b998
Author: lumidify <nobody@lumidify.org>
Date:   Fri, 17 Dec 2021 09:11:55 +0100
Add Makefile; improve documentation
Diffstat:
| A | .gitignore |  |  | 1 | + | 
| A | CHANGELOG |  |  | 4 | ++++ | 
| A | Makefile |  |  | 37 | +++++++++++++++++++++++++++++++++++++ | 
| M | README |  |  | 11 | +++++++++-- | 
| A | lumia |  |  | 1403 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| D | lumia.pl |  |  | 1403 | ------------------------------------------------------------------------------- | 
| M | tests/README |  |  | 3 | +++ | 
7 files changed, 1457 insertions(+), 1405 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+lumia.1
diff --git a/CHANGELOG b/CHANGELOG
@@ -0,0 +1,4 @@
+1.0 -> 1.1
+* Made it work with cksum implementation that don't have '-q'
+* Added Makefile
+* Improved documentation
diff --git a/Makefile b/Makefile
@@ -0,0 +1,37 @@
+.POSIX:
+
+NAME = lumia
+VERSION = 1.1
+
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/man
+
+MAN1 = ${NAME:=.1}
+MISCFILES = Makefile README LICENSE CHANGELOG tests
+
+${MAN1}: ${NAME}
+	pod2man ${NAME} ${MAN1}
+
+install: ${MAN1}
+	mkdir -p "${DESTDIR}${PREFIX}/bin"
+	cp -f ${NAME} "${DESTDIR}${PREFIX}/bin"
+	chmod 755 "${DESTDIR}${PREFIX}/bin/${NAME}"
+	mkdir -p "${DESTDIR}${MANPREFIX}/man1"
+	cp -f ${MAN1} "${DESTDIR}${MANPREFIX}/man1"
+	chmod 644 "${DESTDIR}${MANPREFIX}/man1/${MAN1}"
+
+uninstall:
+	rm -f "${DESTDIR}${PREFIX}/bin/${NAME}"
+	rm -f "${DESTDIR}${MANPREFIX}/man1/${MAN1}"
+
+clean:
+	rm -f ${MAN1}
+
+dist:
+	rm -rf "${NAME}-${VERSION}"
+	mkdir -p "${NAME}-${VERSION}"
+	cp -rf ${NAME} ${MISCFILES} "${NAME}-${VERSION}"
+	tar cf - "${NAME}-${VERSION}" | gzip -c > "${NAME}-${VERSION}.tar.gz"
+	rm -rf "${NAME}-${VERSION}"
+
+.PHONY: all clean install uninstall dist
diff --git a/README b/README
@@ -1,3 +1,10 @@
-REQUIREMENTS: String::ShellQuote
+REQUIREMENTS: Perl 5, String::ShellQuote
 
-See the perldoc in lumia.pl for documentation (run perldoc -F lumia.pl).
+lumia is meant for managing checksums of files in order to prevent bitrot.
+It does this by storing several special files in each directory to keep
+track of the checksums.
+
+See the perldoc in lumia for the documentation (run perldoc -F lumia.pl).
+Alternatively, install it with 'make install' (as root; this just generates
+a man page and copies lumia and the generated man page to the appropriate
+system directories) and then run 'man lumia'.
diff --git a/lumia b/lumia
@@ -0,0 +1,1403 @@
+#!/usr/bin/env perl
+
+# TODO: some way to avoid writing .lumidify* in dirs but still index them?
+# TODO: store modified date and checksum files with changed date
+# TODO: add option to just check dir structure or maybe check if everything exists
+# TODO: add option to compare cksums of two dirs
+# TODO: exit status!
+
+use strict;
+use warnings;
+use File::Spec::Functions qw(catfile abs2rel);
+use File::Basename qw(basename dirname);
+use String::ShellQuote;
+use Pod::Usage;
+use Getopt::Long;
+
+# the file used to store checksums for files
+my $CKSUM_FILE = ".lumidify_archive_cksums";
+# the file used to store directory names
+my $DIR_FILE = ".lumidify_archive_dirs";
+# the file read to ignore files or directories
+my $IGNORE_FILE = ".lumidify_archive_ignore";
+# the file containing checksums of $CKSUM_FILE and $DIR_FILE
+my $DOUBLE_CKSUM_FILE = ".lumidify_archive_cksums.cksum";
+
+# uncomment this instead of the lines below to use
+# sha256 instead of cksum as the hash algorithm
+# Note: this isn't really tested properly
+#my $CKSUM_CMD = 'sha256 -q';
+#my $CKSUM_NUMFIELDS = 1;
+my $CKSUM_CMD = 'cksum';
+my $CKSUM_NUMFIELDS = 2;
+
+my %SPECIAL_FILES = (
+	$CKSUM_FILE => 1,
+	$DIR_FILE => 1,
+	$IGNORE_FILE => 1,
+	$DOUBLE_CKSUM_FILE => 1
+);
+
+# escape a filename for writing into the checksum files
+sub escape_filename {
+	my $file = shift;
+	$file =~ s/\\/\\\\/g;
+	$file =~ s/"/\\"/g;
+	return $file;
+}
+
+# make a generic file iterator
+# $file_func determines whether a file should be returned by the iterator
+# $dir_func is called for each directory and returns all files that
+# should be added to the queue
+sub make_file_iter {
+	my ($file_func, $dir_func, @queue) = @_;
+	return sub {
+		while (@queue) {
+			my $file = pop @queue;
+			if (-d $file) {
+				my $new_files = $dir_func->($file);
+				next if !defined $new_files;
+				push @queue, @$new_files;
+			}
+			return $file if $file_func->($file);
+		}
+		return;
+	};
+}
+
+# make a basic filename iterator, which simply returns all files
+# for which $file_func returns a true value
+sub make_file_iter_basic {
+	my ($file_func, @files) = @_;
+	make_file_iter $file_func, sub {
+		my $dh;
+		if (!opendir $dh, $_[0]) {
+			warn "WARNING: Unable to open directory \"$_[0]\"!";
+			return [];
+		}
+		my @new_files = map "$_[0]/$_", grep {$_ ne "." && $_ ne ".."} readdir $dh;
+		closedir $dh;
+		return \@new_files;
+	}, @files;
+}
+
+# make an interator that only returns the directories which are present
+# in the $DIR_FILE files, in addition to the files and directories that
+# were originally passed as arguments
+# note: this returns nonexistent directories if those are still
+# specified in the lumia files
+sub make_lumia_iter {
+	my ($quiet, @dirs) = @_;
+	make_file_iter sub {1}, sub {
+		my $path = "$_[0]/$DIR_FILE";
+		return [] if !-f $path;
+		my $dirs = read_file($path, {});
+		return if !defined $dirs;
+		my @new_dirs;
+		foreach my $dir (keys %$dirs) {
+			my $dir_path = "$_[0]/$dir";
+			if (!-d $dir_path) {
+				warn "ERROR: Directory \"$dir_path\" mentioned in " .
+					"\"$path\" does not exist or is not directory.\n" if !$quiet;
+			}
+			# still push it even when it doesn't exist so rmold can work properly
+			push @new_dirs, $dir_path;
+		}
+		return \@new_dirs;
+	}, @dirs;
+}
+
+# remove all special lumia files from the given directory
+sub clean_files {
+	my ($dir, $args) = @_;
+	my $iter = make_file_iter_basic sub {exists $SPECIAL_FILES{basename $_[0]};}, $dir;
+	while (my $file = $iter->()) {
+		if (!unlink $file) {
+			warn "WARNING: Unable to remove file \"$file\"!\n";
+		} else {
+			print "Deleted \"$file\"\n" if !$args->{"q"};
+		}
+	}
+}
+
+# read a file, processing each line with $handle_cksum_func if set
+# and writing the results into $cksums
+# $handle_cksum_func must return two values, the checksum of the
+# argument and the rest of the string (that is then parsed for
+# the filename); if it returns undef, this function also returns undef
+sub read_file {
+	my ($file, $cksums, $handle_cksum_func) = @_;
+	my $fh;
+	if (!open $fh, "<", $file) {
+		warn "ERROR: Unable to open file \"$file\": $!\n";
+		return;
+	}
+	my $in_fn = 0;
+	my $cur_cksum;
+	my $cur_str;
+	my $cur_fn = "";
+	foreach (<$fh>) {
+		next if (!$in_fn && /^$/);
+		if ($handle_cksum_func && !$in_fn) {
+			($cur_cksum, $cur_str) = $handle_cksum_func->($_);
+			return undef if !defined $cur_cksum;
+		} else {
+			$cur_str = $_;
+		}
+		my $bs = 0;
+		foreach my $ch (split(//, $cur_str)) {
+			if ($ch eq "\\") {
+				$bs++;
+				$cur_fn .= "\\" if !($bs %= 2) && $in_fn;
+			} elsif ($bs % 2) {
+				$cur_fn .= $ch  if $in_fn;
+				$bs = 0;
+			} elsif ($ch eq "\"") {
+				if ($in_fn) {
+					$in_fn = 0;
+					$cksums->{$cur_fn} = $cur_cksum;
+					$cur_fn = "";
+					last;
+				}
+				$in_fn = 1;
+			} elsif ($in_fn) {
+				$cur_fn .= $ch;
+			}
+		}
+	}
+	close $fh;
+	if ($in_fn) {
+		warn "ERROR: Unterminated filename in file \"$file\"\n";
+		return undef;
+	}
+	return $cksums;
+}
+
+# read a single checksum file, writing the checksums into the hash $cksums and returning it
+sub read_cksum_file {
+	my ($file, $cksums) = @_;
+	return read_file $file, $cksums, sub {
+		my $line = shift;
+		my @fields = split(/ /, $line, $CKSUM_NUMFIELDS+1);
+		if (@fields != $CKSUM_NUMFIELDS+1) {
+			warn "WARNING: Malformed line \"$line\" in file \"$file\"\n";
+			return;
+		}
+		my $cur_cksum = join(" ", @fields[0..$CKSUM_NUMFIELDS-1]);
+		my $cur_str = $fields[$CKSUM_NUMFIELDS];
+		return ($cur_cksum, $cur_str);
+	};
+}
+
+# read the checksums and directory names in $dir
+sub read_cksums {
+	my $dir = shift;
+	my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {});
+	return undef if !defined $cksums;
+	$cksums = read_file("$dir/$DIR_FILE", $cksums);
+	return undef if !defined $cksums;
+	return $cksums;
+}
+
+# get the checksum output for $path
+# returns undef if $CKSUM_CMD returns an error
+sub get_cksum {
+	my $path = shift;
+	my $path_esc = shell_quote $path;
+	my $cksum_output = `$CKSUM_CMD -- $path_esc 2>&1`;
+	if ($?) {
+		warn "ERROR getting cksum for file \"$path\":\n$cksum_output";
+		return undef;
+	}
+	chomp $cksum_output;
+	my @fields = split(/ /, $cksum_output, $CKSUM_NUMFIELDS+1);
+	return join(" ", @fields[0..$CKSUM_NUMFIELDS-1]);
+}
+
+# check the checksums in $dir/$cksum_file
+# if $quiet is set, only print failed files
+sub check_cksums {
+	my ($dir, $cksum_file, $quiet) = @_;
+	my $cksums = read_cksum_file("$dir/$cksum_file", {});
+	return 0 if !defined $cksums;
+	my $failed = 1;
+	foreach my $file (keys %$cksums) {
+		my $path = "$dir/$file";
+		my $output = get_cksum $path;
+		next if !defined $output;
+		if ($output eq $cksums->{$file}) {
+			print "OK $path\n" if !$quiet;
+		} else {
+			print "FAILED $path\n";
+			$failed = 0;
+		}
+	}
+	return $failed;
+}
+
+# check the checksums of all files and directories in @dirs
+sub check_files {
+	my $args = shift;
+	my @dirs;
+	foreach my $file (@_) {
+		if (-d $file) {
+			push @dirs, $file;
+			next;
+		}
+		my $dir = dirname $file;
+		my $base = basename $file;
+		if (exists $SPECIAL_FILES{$base}) {
+			warn "ERROR: File is reserved for lumia: $file\n";
+			next;
+		}
+		my $cksums = read_cksum_file("$dir/$CKSUM_FILE");
+		next if !defined $cksums;
+		if (!exists $cksums->{$base}) {
+			warn "ERROR: File doesn't exist in checksums: $file\n";
+			next;
+		}
+		my $output = get_cksum "$file";
+		next if !defined $output;
+		if ($output eq $cksums->{$base}) {
+			print "OK $file\n" if !$args->{"q"};
+		} else {
+			print "FAILED $file\n";
+		}
+	}
+	my $iter = make_lumia_iter 0, @dirs;
+	while (my $file = $iter->()) {
+		check_cksums $file, $DOUBLE_CKSUM_FILE, $args->{"q"};
+		check_cksums $file, $CKSUM_FILE, $args->{"q"};
+	}
+}
+
+# write the checksums of the special lumia files given as arguments
+# to $DOUBLE_CKSUM_FILE in $dir
+sub write_special_cksums {
+	my ($dir, @files) = @_;
+	my $cksum_file = "$dir/$DOUBLE_CKSUM_FILE";
+	my $cksums = {};
+	if (-f $cksum_file) {
+		$cksums = read_cksum_file $cksum_file, {};
+	}
+	return if !defined $cksums;
+	foreach my $file (@files) {
+		my $cksum_output = get_cksum("$dir/$file");
+		next if (!defined $cksum_output);
+		$cksums->{$file} = $cksum_output;
+	}
+	write_file($cksum_file, $cksums, 1);
+}
+
+# search for new files that aren't present in the checksum files
+# - if $file_func is set, it is called for each new file
+# - if $before_dir_func is set, it is called before processing the
+#   files in each directory that has new files OR if a directory
+#   is entirely new (well, it only checks if $DOUBLE_CKSUM_FILE exists)
+# - if $after_dir_func is set, it is called after processing the
+#   files in each directory that has new files
+sub check_new_files {
+	my ($top_dir, $file_func, $before_dir_func, $after_dir_func) = @_;
+	my $iter = make_file_iter sub {1}, sub {
+		my $dir = shift;
+		my $dh;
+		if (!opendir $dh, $dir) {
+			warn "ERROR: Unable to open directory \"$dir\"!";
+			return undef;
+		}
+		my $read_file_noerror = sub {
+			if (-f $_[0]) {
+				return $_[1]->($_[0], {}) // {};
+			}
+			return {};
+		};
+		my $ignore = $read_file_noerror->("$dir/$IGNORE_FILE", \&read_file);
+		my $lumia_dirs = $read_file_noerror->("$dir/$DIR_FILE", \&read_file);
+		my $lumia_files = $read_file_noerror->("$dir/$CKSUM_FILE", \&read_cksum_file);
+		my @dirs;
+		my $found = 0;
+		while (my $file = readdir $dh) {
+			next if $file eq "." || $file eq "..";
+			next if exists $ignore->{$file} || exists $SPECIAL_FILES{$file};
+			if (!exists $lumia_dirs->{$file} && !exists $lumia_files->{$file}) {
+				if (!$found && defined $before_dir_func) {
+					last if !$before_dir_func->($dir);
+				}
+				if (defined $file_func) {
+					$file_func->($dir, $file);
+				} else {
+					print "$dir/$file\n";
+				}
+				$found = 1;
+			}
+			push @dirs, "$dir/$file" if -d "$dir/$file";
+		}
+		closedir $dh;
+		# also call $before_dir_func if the directory has not been initialized yet
+		if (!$found && !-f "$dir/$DOUBLE_CKSUM_FILE" && defined $before_dir_func) {
+			$before_dir_func->($dir);
+		}
+		if ($found && defined $after_dir_func) {
+			$after_dir_func->($dir);
+		}
+		return \@dirs;
+	}, $top_dir;
+	# Is this a horrible hack? I dunno, but it sure is sweet...
+	while ($iter->()) {}
+}
+
+# add all new files in $top_dir to the checksum files
+sub check_add_new_files {
+	my ($top_dir, $args) = @_;
+	my $changed_dirs = 0;
+	my $changed_files = 0;
+	check_new_files $top_dir, sub {
+		my ($dir, $file) = @_;
+		my $fullpath = "$dir/$file";
+		if (-d $fullpath) {
+			my $dir_file = "$dir/$DIR_FILE";
+			my $fh;
+			if (!open $fh, ">>", $dir_file) {
+				warn "ERROR: Unable to append to file \"$dir_file\"!";
+				return;
+			}
+			print $fh '"' . escape_filename($file) . '"' . "\n";
+			close $fh;
+			$changed_dirs = 1;
+		} else {
+			my $cksum_output = get_cksum $fullpath;
+			return if !defined $cksum_output;
+			my $cksum_file = "$dir/$CKSUM_FILE";
+			my $fh;
+			if (!open $fh, ">>", $cksum_file) {
+				warn "ERROR: Unable to append to file \"$cksum_file\"!";
+				return;
+			}
+			print $fh $cksum_output . ' "' . escape_filename($file) . '"' . "\n";
+			close $fh;
+			$changed_files = 1;
+		}
+		print "Added \"$fullpath\"\n" if !$args->{"q"};
+	}, sub {
+		if (-f "$_[0]/$DOUBLE_CKSUM_FILE") {
+			if (!check_cksums $_[0], $DOUBLE_CKSUM_FILE, 1) {
+				warn "Checksum files corrupt in \"$_[0]\", not adding new checksums!\n";
+				return 0;
+			}
+
+		} else {
+			write_cksums($_[0], {}, 1, 1);
+		}
+		return 1;
+	}, sub {
+		if ($changed_dirs) {
+			write_special_cksums $_[0], $DIR_FILE;
+			$changed_dirs = 0;
+		}
+		if ($changed_files) {
+			write_special_cksums $_[0], $CKSUM_FILE;
+			$changed_files = 0;
+		}
+	};
+}
+
+# write the "checksums" in $contents to $path
+# if $is_cksum_file is set, the value each of the keys in $contents points
+# to is written before the key
+sub write_file {
+	my ($path, $contents, $is_cksum_file) = @_;
+	my $fh;
+	if (!open $fh, ">", $path) {
+		warn "ERROR: Unable to open \"$path\" for writing!";
+		return;
+	}
+	foreach my $filename (keys %$contents) {
+		if ($is_cksum_file) {
+			print $fh "$contents->{$filename} ";
+		}
+		print $fh '"' . escape_filename($filename) . '"' . "\n";
+	}
+	close $fh;
+}
+
+# write the checksums in $contents to the file at $path
+sub write_cksum_file {
+	my ($path, $contents) = @_;
+	write_file $path, $contents, 1;
+}
+
+# write the checksums in $contents to $dir
+# any keys that point to undef are taken to be directories and vice versa
+# $files_modified and $dirs_modified control which of the special lumia
+# files actually get written
+# note: this doesn't use write_file, etc. in order to (possibly) be a bit more efficient
+sub write_cksums {
+	my ($dir, $contents, $files_modified, $dirs_modified) = @_;
+	# No, this isn't efficient...
+	my @special_files;
+	my $dirs_fh;
+	my $files_fh;
+	if ($files_modified) {
+		my $path = "$dir/$CKSUM_FILE";
+		if (!open $files_fh, ">", $path) {
+			warn "ERROR: Unable to open \"$path\" for writing!";
+			return;
+		}
+		push @special_files, $CKSUM_FILE;
+	}
+	if ($dirs_modified) {
+		my $path = "$dir/$DIR_FILE";
+		if (!open $dirs_fh, ">", $path) {
+			warn "ERROR: Unable to open \"$path\" for writing!";
+			return;
+		}
+		push @special_files, $DIR_FILE;
+	}
+	foreach my $key (keys %$contents) {
+		if ($files_modified && defined $contents->{$key}) {
+			print $files_fh $contents->{$key} . ' "' . escape_filename($key) . '"' . "\n";
+		} elsif ($dirs_modified && !defined $contents->{$key}) {
+			print $dirs_fh '"' . escape_filename($key) . '"' . "\n";
+		}
+	}
+	close $files_fh if defined $files_fh;
+	close $dirs_fh if defined $dirs_fh;
+	if (@special_files) {
+		write_special_cksums $dir, @special_files;
+	}
+}
+
+# show all files that are present in the checksum files but don't exist on the filesystem anymore
+sub check_old_files {
+	my $top_dir = shift;
+	my $iter = make_lumia_iter 1, $top_dir;
+	while (my $dir = $iter->()) {
+		if (-e $dir) {
+			my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {}) // {};
+			foreach my $file (keys %$cksums) {
+				if (!-e "$dir/$file") {
+					warn "Nonexistent file: \"$dir/$file\"!\n";
+				}
+			}
+		} else {
+			warn "Nonexistent directory: \"$dir\"!\n";
+		}
+	}
+}
+
+# clean up the lumia checksum files, removing any files that aren't present
+# on the filesystem anymore
+sub remove_old_files {
+	my ($top_dir, $args) = @_;
+	my $iter = make_lumia_iter 1, $top_dir;
+	while (my $dir = $iter->()) {
+		if (!-e $dir) {
+			my $parent = dirname $dir;
+			my $child = basename $dir;
+			my $lumia_dirs = read_file("$parent/$DIR_FILE", {}) // {};
+			if (exists $lumia_dirs->{$child}) {
+				delete $lumia_dirs->{$child};
+				write_file "$parent/$DIR_FILE", $lumia_dirs;
+				print "Removed \"$dir\" from \"$parent/$DIR_FILE\"\n" if !$args->{"q"};
+				write_special_cksums $parent, $DIR_FILE;
+			}
+		} else {
+			my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {}) // {};
+			my $found = 0;
+			foreach my $file (keys %$cksums) {
+				if (!-e "$dir/$file") {
+					delete $cksums->{$file};
+					print "Removed \"$dir/$file\" from \"$dir/$CKSUM_FILE\"\n" if !$args->{"q"};
+					$found = 1;
+				}
+			}
+			if ($found) {
+				write_cksum_file "$dir/$CKSUM_FILE", $cksums;
+				write_special_cksums $dir, $CKSUM_FILE;
+			}
+		}
+	}
+}
+
+# sort the given paths into hash based on the dirname
+# returns: a hash with the keys being the dirnames of the given paths and
+# each one pointing to an array containing the basenames of all paths
+# that had this dirname
+sub sort_by_dir {
+	my %sorted_files;
+	foreach my $file (@_) {
+		if (!-e $file) {
+			warn "ERROR: Source file \"$file\" doesn't exist.\n";
+			next;
+		}
+		my $dir = dirname($file);
+		if (!exists($sorted_files{$dir})) {
+			$sorted_files{$dir} = [];
+		}
+		push(@{$sorted_files{$dir}}, basename($file));
+	}
+	return \%sorted_files;
+}
+
+# check if $dst exists and prompt the user whether it should be overwritten
+# returns 0 if it can be overwritten or doesn't exist, 1 if it shouldn't be overwritten
+sub prompt_overwrite {
+	my $dst = shift;
+	if (-e $dst) {
+		print STDERR "WARNING: \"$dst\" exists already. Do you want to replace it? (y/n) ";
+		my $choice = "";
+		while ($choice ne "y" && $choice ne "n") {
+			$choice = <STDIN>;
+			chomp $choice;
+		}
+		if ($choice eq "n") {
+			warn "Not overwriting \"$dst\"\n";
+			return 1;
+		} else {
+			return 0;
+		}
+	}
+	return 0;
+}
+
+# copies the $src files to $dst and updates the checksums in $dst
+# $src: list of source paths
+# $dst: destination directory or file (in latter case only one src is allowed)
+sub copy_files {
+	my ($src, $dst, $args) = @_;
+	my $dst_dir = $dst;
+	if (!-d $dst) {
+		$dst_dir = dirname $dst;
+	}
+	my $diff_name = 0;
+	# check if the file/dir is getting a different name or
+	# just being copied into a different directory
+	if (!-d $dst && !-d $src->[0]) {
+		$diff_name = 1;
+	}
+	if (!-e $dst && -d $src->[0]) {
+		$diff_name = 1;
+	}
+	my $dst_cksums = read_cksums $dst_dir;
+	return if !defined $dst_cksums;
+	my $src_sorted = sort_by_dir(@$src);
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	foreach my $src_dir (keys %$src_sorted) {
+		my $src_cksums = read_cksums $src_dir;
+		next if !defined $src_cksums;
+		foreach my $src_file (@{$src_sorted->{$src_dir}}) {
+			my $src_path = "$src_dir/$src_file";
+
+			my $dst_path = $diff_name ? $dst : "$dst_dir/$src_file";
+			if (-d $dst_path && -d $src_path) {
+				warn "ERROR: Cannot copy directory to already existing directory\n";
+				next;
+			}
+			if (exists $SPECIAL_FILES{$src_file} || exists $SPECIAL_FILES{basename $dst_path}) {
+				warn "ERROR: Not copying special file\n";
+				next;
+			}
+			next if !$args->{"f"} && prompt_overwrite($dst_path);
+			my $options = $args->{"v"} ? "-av" : "-a";
+			next if system("cp", $options, "--", $src_path, $dst);
+
+			if (-d $src_path) {
+				$dirs_touched = 1;
+			} else {
+				$files_touched = 1;
+			}
+
+			if (exists $src_cksums->{$src_file}) {
+				if ($diff_name) {
+					$dst_cksums->{basename $dst} = $src_cksums->{$src_file};
+				} else {
+					$dst_cksums->{$src_file} = $src_cksums->{$src_file};
+				}
+			} else {
+				warn "WARNING: \"$src_path\" not in cksum or directory list\n";
+			}
+		}
+	}
+	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
+}
+
+# move a file (or directory) from $src to $dst, prompting for confirmation if $dst already exists;
+# automatically appends the basename of $src to $dst if $dst is a directory
+sub move_file {
+	my ($src, $dst, $args) = @_;
+	if (exists $SPECIAL_FILES{basename $src} || exists $SPECIAL_FILES{basename $dst}) {
+		warn "ERROR: Not moving special file\n";
+		return 1;
+	}
+	if (-d $dst) {
+		$dst .= "/" . basename($src);
+	}
+	return 1 if !$args->{"f"} && prompt_overwrite($dst);
+	my $ret;
+	if ($args->{"v"}) {
+		$ret = system("mv", "-v", "--", $src, $dst);
+	} else {
+		$ret = system("mv", "--", $src, $dst);
+	}
+	return 1 if $ret;
+	if (-e $src) {
+		warn "ERROR: file could not be removed from source but will still be " .
+			"removed from checksum database\n";
+	}
+	return 0;
+}
+
+# move all files/directories in $src_files from $src_dir to $dst_dir ($src_files
+# only contains the basenames of the files), removing them from the checksum files
+# in $src_dir and adding them to $dst_cksums
+sub move_from_same_dir {
+	my ($src_dir, $src_files, $dst_cksums, $dst_dir, $args) = @_;
+	my $src_cksums = read_cksums $src_dir;
+	return if !defined $src_cksums;
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	foreach my $src_file (@$src_files) {
+		my $fullpath = "$src_dir/$src_file";
+		my $tmp_dirs_touched = 0;
+		my $tmp_files_touched = 0;
+		if (-d $fullpath) {
+			$tmp_dirs_touched = 1;
+		} else {
+			$tmp_files_touched = 1;
+		}
+
+		next if move_file($fullpath, $dst_dir, $args);
+
+		# need to be able to check if the path is a directory
+		# before actually moving it
+		$dirs_touched ||= $tmp_dirs_touched;
+		$files_touched ||= $tmp_files_touched;
+		if (exists $src_cksums->{$src_file}) {
+			$dst_cksums->{$src_file} = $src_cksums->{$src_file};
+			delete $src_cksums->{$src_file};
+		} else {
+			warn "WARNING: \"$src_dir/$src_file\" not in cksum or directory list.\n";
+		}
+	}
+	write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
+	return ($files_touched, $dirs_touched);
+}
+
+# rename a single file or directory from $src to $dst
+sub move_rename {
+	my ($src, $dst, $args) = @_;
+	my $src_dir = dirname $src;
+	my $dst_dir = dirname $dst;
+	my $src_file = basename $src;
+	my $dst_file = basename $dst;
+
+	my $src_cksums = read_cksums $src_dir;
+	return if !defined $src_cksums;
+	my $dst_cksums = {};
+	# if a file is simply being renamed in the same dir, the cksums
+	# should only be loaded and written once
+	if ($src_dir eq $dst_dir) {
+		%$dst_cksums = %$src_cksums;
+		delete $dst_cksums->{$src_file};
+	} else {
+		$dst_cksums = read_cksums $dst_dir;
+		return if !defined $dst_cksums;
+	}
+
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	if (-d $src) {
+		$dirs_touched = 1;
+	} else {
+		$files_touched = 1;
+	}
+
+	return if move_file($src, $dst, $args);
+
+	if (exists($src_cksums->{$src_file})) {
+		$dst_cksums->{$dst_file} = $src_cksums->{$src_file};
+		delete $src_cksums->{$src_file};
+	} else {
+		warn "WARNING: \"$src\" not in cksum or directory list.\n";
+	}
+	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
+	if ($src_dir ne $dst_dir) {
+		write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
+	}
+}
+
+# move all files and directories in $src to $dst
+# - if $dst does not exist, $src is only allowed to contain one path, which is
+# renamed to $dst
+# - if $dst is a file, $src is only allowed to contain a single path (which
+# must be a file), which is renamed to $dst
+# - otherwise, all files and directories in $src are moved to $dst
+# $src: list of source paths
+# $dst: destination directory or file (in latter case only one src is allowed)
+sub move_files {
+	my ($src, $dst, $args) = @_;
+	if (!-d $dst && $#$src != 0) {
+		die "move: only one source argument allowed when destination is a file\n";
+	}
+	if (!-d $dst && !-d $src->[0]) {
+		move_rename $src->[0], $dst, $args;
+		return;
+	}
+	if (!-e $dst && -d $src->[0]) {
+		move_rename $src->[0], $dst, $args;
+		return;
+	}
+	if (-e $dst && !-d $dst && -d $src->[0]) {
+		die "move: can't move directory to file\n";
+	}
+	# Separate files by current dir so the cksum and dir files only need to be opened once
+	my $src_files = sort_by_dir(@$src);
+	my $dst_cksums = read_cksums $dst;
+	return if !defined $dst_cksums;
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	foreach my $src_dir (keys %$src_files) {
+		my ($tmp_files_touched, $tmp_dirs_touched) = move_from_same_dir $src_dir, $src_files->{$src_dir}, $dst_cksums, $dst, $args;
+		$files_touched ||= $tmp_files_touched;
+		$dirs_touched ||= $tmp_dirs_touched;
+	}
+	write_cksums $dst, $dst_cksums, $files_touched, $dirs_touched;
+}
+
+# remove a file or directory from the filesystem
+sub remove_file_dir {
+	my ($path, $args) = @_;
+	my $options = $args->{"f"} ? "-rf" : "-r";
+	if (system("rm", $options, "--", $path)) {
+		return 1;
+	}
+	if (-e $path) {
+		warn "ERROR: Unable to remove \"$path\" from filesystem but " .
+			"will still be removed from checksum database\n";
+	}
+	return 0;
+}
+
+# remove all files in one directory, updating the checksum files in the process
+# note: the files are only allowed to be basenames, i.e., they must be the
+# actual filenames present in the checksum files
+sub remove_from_same_dir {
+	my ($args, $dir, @files) = @_;
+	my $cksums = read_cksums $dir;
+	return if !defined $cksums;
+	my $dirs_touched = 0;
+	my $files_touched = 0;
+	foreach my $file (@files) {
+		if (exists $SPECIAL_FILES{$file}) {
+			warn "ERROR: not removing special file $file\n";
+			next;
+		}
+		my $fullpath = "$dir/$file";
+		if (!-e $fullpath) {
+			warn "\"$fullpath\": No such file or directory.\n";
+		}
+		next if remove_file_dir($fullpath, $args);
+		if (exists $cksums->{$file}) {
+			if (defined $cksums->{$file}) {
+				$files_touched = 1;
+			} else {
+				$dirs_touched = 1;
+			}
+			delete $cksums->{$file};
+		} else {
+			warn "WARNING: \"$file\" not in cksum or directory list.\n";
+		}
+	}
+	write_cksums $dir, $cksums, $files_touched, $dirs_touched;
+}
+
+# remove all given files and directories, updating the appropriate checksum
+# files in the process
+sub remove_files {
+	my $args = shift;
+	my $sorted_files = sort_by_dir(@_);
+	foreach my $dir (keys %$sorted_files) {
+		remove_from_same_dir($args, $dir, @{$sorted_files->{$dir}});
+	}
+}
+
+# create the given directories, initializing them with empty checksum files
+# note: does not work like "mkdir -p", i.e., the new directories have to
+# be located inside already existing directories
+sub make_dirs {
+	my @created_dirs;
+	foreach (@_) {
+		if (system("mkdir", "--", $_)) {
+			warn "ERROR creating directory $_\n";
+			next;
+		}
+		push(@created_dirs, $_);
+	}
+	# Separate files by current dir so the cksum and dir files only need to be opened once
+	my %dirs;
+	foreach my $dir (@created_dirs) {
+		write_cksums $dir, {}, 1, 1;
+		my $parent = dirname($dir);
+		if (!exists($dirs{$parent})) {
+			$dirs{$parent} = [];
+		}
+		push(@{$dirs{$parent}}, basename($dir));
+	}
+	foreach my $parent (keys %dirs) {
+		my $parent_dirs = read_file "$parent/$DIR_FILE", {};
+		next if !defined $parent_dirs;
+		foreach my $dir (@{$dirs{$parent}}) {
+			$parent_dirs->{$dir} = "";
+		}
+		write_file "$parent/$DIR_FILE", $parent_dirs;
+		write_special_cksums $parent, $DIR_FILE;
+	}
+}
+
+# extract all special lumia files from $src_dir to $dst_dir, recreating the
+# entire directory structure in the process
+sub extract {
+	my ($src_dir, $dst_dir, $args) = @_;
+	my $iter = make_lumia_iter 0, $src_dir;
+	my $options = $args->{"v"} ? "-av" : "-a";
+	while (my $dir = $iter->()) {
+		my $final_dir = abs2rel $dir, $src_dir;
+		my $fulldir = catfile $dst_dir, $final_dir;
+		system("mkdir", "-p", "--", $fulldir);
+		foreach my $file (keys %SPECIAL_FILES) {
+			my $filepath = catfile $dir, $file;
+			if (-e $filepath) {
+				system("cp", $options, "--", $filepath, catfile($fulldir, $file));
+			}
+		}
+	}
+}
+
+# update the checksums of the given files
+# ignores any directories given as arguments
+sub update {
+	my @files;
+	foreach (@_) {
+		if (-d $_) {
+			warn "Ignoring directory \"$_\"\n";
+		} else {
+			push @files, $_;
+		}
+	}
+	my $sorted_files = sort_by_dir @files;
+	foreach my $dir (keys %$sorted_files) {
+		my $cksums = read_cksum_file "$dir/$CKSUM_FILE", {};
+		next if !defined $cksums;
+		my $changed = 0;
+		foreach my $file (@{$sorted_files->{$dir}}) {
+			my $cksum_output = get_cksum "$dir/$file";
+			next if !defined $cksum_output;
+			$cksums->{$file} = $cksum_output;
+			$changed = 1;
+		}
+		if ($changed) {
+			write_cksum_file "$dir/$CKSUM_FILE", $cksums;
+			write_special_cksums $dir, $CKSUM_FILE;
+		}
+	}
+}
+
+sub update_special {
+	my $dir = shift;
+	write_special_cksums $dir, $CKSUM_FILE, $DIR_FILE;
+}
+
+my %args;
+Getopt::Long::Configure("bundling");
+GetOptions(\%args, "f|force", "q|quiet", "v|verbose", "h|help");
+
+pod2usage(-exitval => 0, -verbose => 2) if $args{"h"};
+pod2usage(-exitval => 1, -verbose => 1) if @ARGV < 1;
+
+my $cmd = shift;
+
+if ($cmd eq "mv") {
+	die "mv requires at least two arguments\n" if @ARGV < 2;
+	my @src = @ARGV[0..$#ARGV-1];
+	move_files \@src, $ARGV[-1], \%args;
+} elsif ($cmd eq "rm") {
+	die "rm requires at least one argument\n" if @ARGV < 1;
+	remove_files \%args, @ARGV;
+} elsif ($cmd eq "addnew") {
+	my $dir = @ARGV ? $ARGV[0] : ".";
+	check_add_new_files $dir, \%args;
+} elsif ($cmd eq "checknew") {
+	my $dir = @ARGV ? $ARGV[0] : ".";
+	check_new_files $dir;
+} elsif ($cmd eq "checkold") {
+	my $dir = @ARGV ? $ARGV[0] : ".";
+	check_old_files $dir;
+} elsif ($cmd eq "rmold") {
+	my $dir = @ARGV ? $ARGV[0] : ".";
+	remove_old_files $dir, \%args;
+} elsif ($cmd eq "check") {
+	if (@ARGV < 1) {
+		check_files \%args, ".";
+	} else {
+		check_files \%args, @ARGV;
+	}
+} elsif ($cmd eq "clean") {
+	my $dir = @ARGV ? $ARGV[0] : ".";
+	clean_files $dir, \%args;
+} elsif ($cmd eq "extract") {
+	my $src_dir = ".";
+	my $dst_dir;
+	if (@ARGV == 2) {
+		$src_dir = $ARGV[0];
+		$dst_dir = $ARGV[1];
+	} elsif (@ARGV == 1) {
+		$dst_dir = $ARGV[0];	
+	} else {
+		die "Invalid number of arguments\n";
+	}
+	if (!-d $src_dir) {
+		die "ERROR: Directory \"$src_dir\" does not exist.\n";
+	}
+	if (!-d $dst_dir) {
+		die "ERROR: Directory \"$dst_dir\" does not exist.\n";
+	}
+	extract $src_dir, $dst_dir;
+} elsif ($cmd eq "cp") {
+	die "cp requires at least two arguments\n" if @ARGV < 2;
+	my @src = @ARGV[0..$#ARGV-1];
+	copy_files \@src, $ARGV[-1], \%args;
+} elsif ($cmd eq "mkdir") {
+	die "mkdir requires at least one argument\n" if @ARGV < 1;
+	make_dirs @ARGV;
+} elsif ($cmd eq "update") {
+	die "update requires at least one argument\n" if @ARGV < 1;
+	update @ARGV;
+} elsif ($cmd eq "updatespecial") {
+	die "Invalid number of arguments\n" if @ARGV > 1;
+	my $dir = @ARGV ? $ARGV[0] : ".";
+	update_special $dir;
+} else {
+	pod2usage(-exitval => 1, -verbose => 1);
+}
+
+__END__
+
+=head1 NAME
+
+lumia - Manage checksums on a filesystem
+
+=head1 SYNOPSIS
+
+B<lumia> command [-hqfv] arguments
+
+=head1 DESCRIPTION
+
+lumia is meant for managing checksums of files in order to prevent bitrot.
+It does this by storing several special files in each directory to keep track
+of the checksums:
+
+=over 8
+
+=item B<.lumidify_archive_cksums>
+
+Contains the checksums of all files in the directory.
+
+=item B<.lumidify_archive_dirs>
+
+Contains a list of all directories in the directory.
+
+=item B<.lumidify_archive_cksums.cksum>
+
+Contains the checksums of B<.lumidify_archive_cksums> and B<.lumidify_archive_dirs>
+just because I'm paranoid.
+
+=item B<.lumidify_archive_ignore>
+
+Contains a list of files and directories that should be ignored by lumia.
+Note that this is only read and never written to, unless the command B<clean>
+is used. It is, however, still copied over by the B<extract> command.
+
+=back
+
+When the documentation for the commands talks about the "checksum database",
+it simply means these files.
+
+All file/directory names are enclosed in quotes, with any backslashes or quotes
+inside the name escaped with another backslash. The names are allowed to have
+newlines in them.
+
+The list files only contain a list of filenames, with a newline between the
+closing quote of one name and the opening quote of the next one.
+
+The checksum files additionally contain the output of the checksum program
+used and a space before the starting quote of the filename.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-h>, B<--help>
+
+Show the full documentation.
+
+=item B<-q>, B<--quiet>
+
+Only output errors.
+
+=item B<-f>, B<--force>
+
+Overwrite files without prompting for confirmation.
+
+=item B<-v>, B<--verbose>
+
+Print each file that is processed by the command.
+
+=back
+
+See the full documentation for details on which commands support which options
+and what they do.
+
+It does not matter if the options are written before or after the command.
+
+If C<--> is written anywhere on the command line, option parsing is stopped,
+so that files starting with a hyphen can still be specified.
+
+Note that C<-q> and C<-v> aren't exactly opposites - C<-q> applies to commands
+like B<check>, where it suppresses printing of the individual files, while
+C<-v> applies to commands like B<cp>, where it is just passed on to the system
+command called in the background.
+
+Note further that this is very inconsistent, like the rest of the program, but
+the author has made too many bad decisions to rectify that problem at the moment.
+
+=head1 COMMANDS
+
+Note that some commands support multiple files/directories as arguments and others,
+for which it would make just as much sense, don't. That's just the way it is.
+
+=over 8
+
+=item B<addnew> [-q] [directory]
+
+Walks through B<directory>, adding all new files to the checksum database.
+B<directory> defaults to the current directory.
+
+C<-q> suppresses the printing of each file or directory as it is added.
+
+=item B<checknew> [directory]
+
+Walks through B<directory>, printing all files that aren't part of the checksum
+database. B<directory> defaults to the current directory.
+
+=item B<checkold> [directory]
+
+Prints all files in the checksum database that do not exist on the filesystem anymore.
+B<directory> defaults to the current directory.
+
+=item B<rmold> [-q] [directory]
+
+Removes all files found by B<checkold> from the database. B<directory> defaults to
+the current directory.
+
+C<-q> suppresses the printing of each file as it is removed.
+
+=item B<check> [-q] file/directory ...
+
+Verifies the checksums of all files given, recursing through any directories. If no
+files or directories are given, the current directory is used.
+
+Note that the checksum database in the corresponding directory will be read again for
+every file given on the command line, even if 1000 files in the same directory are given.
+This problem does not occur when recursing through directories, so it is best to only
+give files directly when checking a few. This problem wouldn't be too difficult to
+fix, but, frankly, I'm too lazy, especially since I only added the feature to check
+files individually as a convenience when I want to quickly check a single file in a
+large directory.
+
+To explain why it is this way: The directory recursion is done using an iterator, which
+has the directories pushed onto its queue in the beginning. The iterator only returns
+directories, which are then checked all in one go, but this means that files given on
+the command line need to be handled specially.
+
+C<-q> suppresses the printing of all good checksums but still allows a message to
+be printed when a checksum failed.
+
+=item B<clean> [-q] [directory]
+
+Removes all lumia special files used to store the checksum database from B<directory>
+recursively. B<directory> defaults to the current directory.
+
+Note that this recurses through the entire directory tree, not just the part that is
+actually linked together by the checksum database.
+
+Warning: This just blindly removes all files with one of the special lumia names,
+even if they weren't actually created by lumia.
+
+C<-q> suppresses the printing of each file as it is deleted.
+
+=item B<extract> [-v] [source] destination
+
+Recreates the entire directory structure from B<source> in B<destination>, but only
+copies the special files used to store the checksum database. B<source> defaults to
+the current directory.
+
+C<-v> prints each file as it is copied.
+
+Note that this overwrites files in the destination directory without confirmation.
+
+=item B<mkdir> directory ...
+
+Creates the given directories, initializing them with empty checksum database files.
+
+=item B<update> file ...
+
+Recalculates the checksums for the given files and replaces them in the database.
+
+Note: Directories given as arguments are ignored.
+
+This is mainly meant to quickly "touch" a file after it was modified (e.g. a
+notes file that is occasionally updated).
+
+=item B<updatespecial> [directory]
+
+Recalculates the checksums for the special files C<.lumidify_archive_dirs> and
+C<.lumidify_archive_cksums> and writes them to C<.lumidify_archive_cksums.cksum>.
+B<directory> defaults to the current directory.
+
+This is only meant to be used if, for some reason, the checksum files had to
+be edited manually and thus don't match the checksums in C<.lumidify_archive_cksums.cksum>
+anymore.
+
+=item B<rm> [-f] file ...
+
+Removes the given files and directories recursively from the filesystem and
+checksum database. The following caveats apply:
+
+If any actual errors occur while deleting the file/directory (i.e. the system
+command C<rm> returns a non-zero exit value), the checksum or directory B<is
+left in the database>. If the system C<rm> does not return a non-zero exit value,
+but the file/directory still exists afterwards (e.g. there was a permission
+error and the user answered "n" when prompted), a warning message is printed,
+but the files B<are removed from the database> (if the database can be
+written to).
+
+It is an error if there are no checksum database files in the directory
+of a file named on the command line.
+
+C<-f> is passed through to the system C<rm> command.
+
+=item B<cp> [-vf] source target
+
+=item B<cp> [-vf] source ... directory
+
+Copies the given source files, updating the checksum database in the process.
+
+If the last argument is a file, there must be only one source argument, also a file,
+which is then copied to the target.
+
+If the last argument is a directory, all source arguments are copied into it.
+
+It is an error if a source or destination directory does not contain any
+checksum database files.
+
+B<cp> will issue a warning and skip to the next argument if it is asked to
+merge a directory with an already existing directory. For instance, attempting
+to run C<cp dir1 dir2>, where C<dir2> already contains a directory named
+C<dir1>, will result in an error. This may change in the future, when the
+program is modified to recursively copy the files manually, instead of simply
+calling the system C<cp> on each of the arguments. If this was supported in
+the current version, none of the checksums inside that directory would be
+updated, so it wouldn't be very useful.
+
+C<-v> is passed through to the system C<cp> command.
+
+C<-f> silently overwrites files without prompting the user, much like the
+C<-f> option in the system C<cp> command. This is handled manually by the
+program, though, in order to actually determine what the user chose. See
+also the caveat mentioned above.
+
+=item B<mv> [-f] source target
+
+=item B<mv> [-f] source ... directory
+
+Moves the given source files, updating the checksum database in the process.
+
+If the last argument is a file or does not exist, there must be only one source
+argument, which is renamed to the target name.
+
+If the last argument is an existing directory, all source arguments are moved
+into it.
+
+It is an error if a source or destination directory does not contain any
+checksum database files.
+
+B<mv> behaves the same as B<rm> with regards to checking if the source file
+is still present after the operation and other error handling.
+
+C<-f> is handled in the same manner as with B<cp>.
+
+=back
+
+=head1 MOTIVATION
+
+There are already several programs that can be used to check for bitrot,
+as listed in L</"SEE ALSO">. However, all programs I tried either were
+much too complicated for my taste or just did everything behind my back.
+I wanted a simple tool that did exactly what I told it to and also allowed
+me to keep the old checksums when reorganizing files, in order to avoid
+regenerating the checksums from corrupt files. Since I couldn't find those
+features in any program I tried, I wrote my own.
+
+=head1 DESIGN DECISIONS
+
+It may strike some readers as a peculiar idea to save the checksum files in
+I<every single directory>, but this choice was made after much deliberation.
+The other option I could think of was to have one big database, but that
+would have made all commands much more difficult to implement and additionally
+necessitated opening the entire database for every operation. With individual
+files in each directory, operations like B<cp> become quite trivial (ignoring
+all the edge cases) since only the toplevel checksums need to be copied to
+the new destination, and any subdirectories already contain the checksums.
+
+This method is not without its drawbacks, however. The most glaring problem
+I have found is that there is no way to store the checksums of read-only
+directories or any special directories that cannot be littered with the
+checksum files because that would clash with other software. Despite these
+drawbacks, however, I decided to stick with it because it works for almost
+all cases and doesn't have any of the serious drawbacks that other options
+would have had.
+
+The names of the special files were chosen to be ".lumidify_archive*" not
+out of vanity, but mainly because I couldn't think of any regular files
+with those names, making them a good choice to avoid clashes.
+
+The name of the program, C<lumia> (for "lumidify archive"), was similarly
+chosen because it did not clash with any programs installed on my system and
+thus allowed for easy tab-completion.
+
+=head1 HASH ALGORITHMS
+
+By default, the simple cksum algorithm is used to get the checksums. This
+is not very secure, but the main purpose of the program is to prevent
+bitrot, for which cksum should be sufficient, especially since it is much
+faster than other algorithms.
+
+There is currently no convenient way to change the algorithm other than
+changing the $CKSUM_CMD and $CKSUM_NUMFIELDS variables at the top of
+lumia. $CKSUM_CMD must be the command that returns the checksum
+when it is given a file, and $CKSUM_NUMFIELDS specifies the number of
+space-separated fields the checksum consists of. This has to be specified
+in order to determine where the checksum ends and the filename begins in
+the output. This would be redundant if all implementations of cksum
+supported '-q' for outputting only the checksum, but that only seems to
+be supported by some implementations.
+
+=head1 USAGE SCENARIOS
+
+=over 8
+
+=item B<Security auditing>
+
+This program is B<NOT> designed to provide any security auditing, as should
+be clear from the fact that the checksums are stored right in the same
+directory as the files. See mtree(8) for that.
+
+If you want to, however, you could set $CKSUM_CMD to a secure hash (not cksum)
+and B<extract> the checksums to a separate directory, which you keep in a
+safe place. You could then use the regular C<cp> command to simply replace
+all the checksums with the ones from your backup, in case an attacker modified
+the checksum database in the directory with the actual files you're trying to
+protect. I don't know if there would be any point in doing that, though.
+
+=item B<Managing archives>
+
+This is the purpose I wrote the program for.
+
+You can simply initialize your archive directory with the B<addnew> command.
+Whenever you add new files, just run B<addnew> again. If you want to reorganize
+the archive, you can use the limited commands available.
+
+I usually just use rsync(1) to copy the entire archive directory over to other
+backup drives and then use the B<check> command again on the new drive.
+
+I also have checksums for the main data directory on my computer (except for
+things like git repositories, which I don't want littered with the database
+files). Here, I use the B<update> command for files that I edit more often
+and occasionally run B<check> on the entire directory.
+
+Since the database files are written in each directory, you can run the
+B<addnew> command in any subdirectory when you've added new files there.
+
+=back
+
+=head1 PERFORMANCE
+
+Due to the extensive use of iterators and the author's bad life choices,
+some functions, such as B<addnew> and B<check>, run more slowly than they
+would if they were programmed more efficiently, especially on many small
+files and folders. Too bad.
+
+=head1 PORTABILITY
+
+This program was written on OpenBSD. It will probably work on most other
+reasonably POSIX-Compliant systems, although I cannot guarantee anything.
+$CKSUM_CMD may need to be modified at the top of lumia. The file
+operation commands are called directly with system(), so those need to
+be available.
+
+It will most certainly not work on Windows, but that shouldn't be a
+problem for anyone important.
+
+=head1 BUGS
+
+All system commands (unless I forgot some) are called with "--" before
+listing the actual files, so files beginning with hyphens should be
+supported. I have tested the commands with filenames starting with spaces
+and hyphens and also containing newlines, but there may very well be issues
+still. Please notify me if you find any filenames that do not work. Handling
+filenames properly is difficult.
+
+There are probably many other edge cases, especially in the B<mv>, B<cp>,
+and B<rm> commands. Please notify me if you find an issue.
+
+Operations on files containing newlines may cause Perl to print a warning
+"Unsuccessful stat on filename containing newline" even though nothing is
+wrong since (as described in B<mv> and B<rm>) existence of the file is
+checked afterwards. I didn't feel like disabling warnings, and no normal
+person should be working with files containing newlines anyways, so that's
+the way it is.
+
+=head1 EXIT STATUS
+
+Always 0, unless the arguments given were invalid. We don't do errors around here.
+
+On a more serious note - I should probably change that at some point.
+For the time being, if you want to run B<check> in a script, you can test
+the output printed when the C<-q> option is used, since this won't output
+anything if there are no errors. Do note, though, that actual errors (file not
+found, etc.) are printed to STDERR, while incorrect checksums are printed
+to STDOUT.
+
+=head1 SEE ALSO
+
+par2(1), mtree(8), aide(1), bitrot(no man page)
+
+=head1 LICENSE
+
+Copyright (c) 2019, 2020, 2021 lumidify <nobody[at]lumidify.org>
+
+Permission to use, copy, modify, and/or 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.
+
+=cut
diff --git a/lumia.pl b/lumia.pl
@@ -1,1403 +0,0 @@
-#!/usr/bin/env perl
-
-# TODO: some way to avoid writing .lumidify* in dirs but still index them?
-# TODO: store modified date and checksum files with changed date
-# TODO: add option to just check dir structure or maybe check if everything exists
-# TODO: add option to compare cksums of two dirs
-# TODO: exit status!
-
-use strict;
-use warnings;
-use File::Spec::Functions qw(catfile abs2rel);
-use File::Basename qw(basename dirname);
-use String::ShellQuote;
-use Pod::Usage;
-use Getopt::Long;
-
-# the file used to store checksums for files
-my $CKSUM_FILE = ".lumidify_archive_cksums";
-# the file used to store directory names
-my $DIR_FILE = ".lumidify_archive_dirs";
-# the file read to ignore files or directories
-my $IGNORE_FILE = ".lumidify_archive_ignore";
-# the file containing checksums of $CKSUM_FILE and $DIR_FILE
-my $DOUBLE_CKSUM_FILE = ".lumidify_archive_cksums.cksum";
-
-# uncomment this instead of the lines below to use
-# sha256 instead of cksum as the hash algorithm
-# Note: this isn't really tested properly
-#my $CKSUM_CMD = 'sha256 -q';
-#my $CKSUM_NUMFIELDS = 1;
-my $CKSUM_CMD = 'cksum';
-my $CKSUM_NUMFIELDS = 2;
-
-my %SPECIAL_FILES = (
-	$CKSUM_FILE => 1,
-	$DIR_FILE => 1,
-	$IGNORE_FILE => 1,
-	$DOUBLE_CKSUM_FILE => 1
-);
-
-# escape a filename for writing into the checksum files
-sub escape_filename {
-	my $file = shift;
-	$file =~ s/\\/\\\\/g;
-	$file =~ s/"/\\"/g;
-	return $file;
-}
-
-# make a generic file iterator
-# $file_func determines whether a file should be returned by the iterator
-# $dir_func is called for each directory and returns all files that
-# should be added to the queue
-sub make_file_iter {
-	my ($file_func, $dir_func, @queue) = @_;
-	return sub {
-		while (@queue) {
-			my $file = pop @queue;
-			if (-d $file) {
-				my $new_files = $dir_func->($file);
-				next if !defined $new_files;
-				push @queue, @$new_files;
-			}
-			return $file if $file_func->($file);
-		}
-		return;
-	};
-}
-
-# make a basic filename iterator, which simply returns all files
-# for which $file_func returns a true value
-sub make_file_iter_basic {
-	my ($file_func, @files) = @_;
-	make_file_iter $file_func, sub {
-		my $dh;
-		if (!opendir $dh, $_[0]) {
-			warn "WARNING: Unable to open directory \"$_[0]\"!";
-			return [];
-		}
-		my @new_files = map "$_[0]/$_", grep {$_ ne "." && $_ ne ".."} readdir $dh;
-		closedir $dh;
-		return \@new_files;
-	}, @files;
-}
-
-# make an interator that only returns the directories which are present
-# in the $DIR_FILE files, in addition to the files and directories that
-# were originally passed as arguments
-# note: this returns nonexistent directories if those are still
-# specified in the lumia files
-sub make_lumia_iter {
-	my ($quiet, @dirs) = @_;
-	make_file_iter sub {1}, sub {
-		my $path = "$_[0]/$DIR_FILE";
-		return [] if !-f $path;
-		my $dirs = read_file($path, {});
-		return if !defined $dirs;
-		my @new_dirs;
-		foreach my $dir (keys %$dirs) {
-			my $dir_path = "$_[0]/$dir";
-			if (!-d $dir_path) {
-				warn "ERROR: Directory \"$dir_path\" mentioned in " .
-					"\"$path\" does not exist or is not directory.\n" if !$quiet;
-			}
-			# still push it even when it doesn't exist so rmold can work properly
-			push @new_dirs, $dir_path;
-		}
-		return \@new_dirs;
-	}, @dirs;
-}
-
-# remove all special lumia files from the given directory
-sub clean_files {
-	my ($dir, $args) = @_;
-	my $iter = make_file_iter_basic sub {exists $SPECIAL_FILES{basename $_[0]};}, $dir;
-	while (my $file = $iter->()) {
-		if (!unlink $file) {
-			warn "WARNING: Unable to remove file \"$file\"!\n";
-		} else {
-			print "Deleted \"$file\"\n" if !$args->{"q"};
-		}
-	}
-}
-
-# read a file, processing each line with $handle_cksum_func if set
-# and writing the results into $cksums
-# $handle_cksum_func must return two values, the checksum of the
-# argument and the rest of the string (that is then parsed for
-# the filename); if it returns undef, this function also returns undef
-sub read_file {
-	my ($file, $cksums, $handle_cksum_func) = @_;
-	my $fh;
-	if (!open $fh, "<", $file) {
-		warn "ERROR: Unable to open file \"$file\": $!\n";
-		return;
-	}
-	my $in_fn = 0;
-	my $cur_cksum;
-	my $cur_str;
-	my $cur_fn = "";
-	foreach (<$fh>) {
-		next if (!$in_fn && /^$/);
-		if ($handle_cksum_func && !$in_fn) {
-			($cur_cksum, $cur_str) = $handle_cksum_func->($_);
-			return undef if !defined $cur_cksum;
-		} else {
-			$cur_str = $_;
-		}
-		my $bs = 0;
-		foreach my $ch (split(//, $cur_str)) {
-			if ($ch eq "\\") {
-				$bs++;
-				$cur_fn .= "\\" if !($bs %= 2) && $in_fn;
-			} elsif ($bs % 2) {
-				$cur_fn .= $ch  if $in_fn;
-				$bs = 0;
-			} elsif ($ch eq "\"") {
-				if ($in_fn) {
-					$in_fn = 0;
-					$cksums->{$cur_fn} = $cur_cksum;
-					$cur_fn = "";
-					last;
-				}
-				$in_fn = 1;
-			} elsif ($in_fn) {
-				$cur_fn .= $ch;
-			}
-		}
-	}
-	close $fh;
-	if ($in_fn) {
-		warn "ERROR: Unterminated filename in file \"$file\"\n";
-		return undef;
-	}
-	return $cksums;
-}
-
-# read a single checksum file, writing the checksums into the hash $cksums and returning it
-sub read_cksum_file {
-	my ($file, $cksums) = @_;
-	return read_file $file, $cksums, sub {
-		my $line = shift;
-		my @fields = split(/ /, $line, $CKSUM_NUMFIELDS+1);
-		if (@fields != $CKSUM_NUMFIELDS+1) {
-			warn "WARNING: Malformed line \"$line\" in file \"$file\"\n";
-			return;
-		}
-		my $cur_cksum = join(" ", @fields[0..$CKSUM_NUMFIELDS-1]);
-		my $cur_str = $fields[$CKSUM_NUMFIELDS];
-		return ($cur_cksum, $cur_str);
-	};
-}
-
-# read the checksums and directory names in $dir
-sub read_cksums {
-	my $dir = shift;
-	my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {});
-	return undef if !defined $cksums;
-	$cksums = read_file("$dir/$DIR_FILE", $cksums);
-	return undef if !defined $cksums;
-	return $cksums;
-}
-
-# get the checksum output for $path
-# returns undef if $CKSUM_CMD returns an error
-sub get_cksum {
-	my $path = shift;
-	my $path_esc = shell_quote $path;
-	my $cksum_output = `$CKSUM_CMD -- $path_esc 2>&1`;
-	if ($?) {
-		warn "ERROR getting cksum for file \"$path\":\n$cksum_output";
-		return undef;
-	}
-	chomp $cksum_output;
-	my @fields = split(/ /, $cksum_output, $CKSUM_NUMFIELDS+1);
-	return join(" ", @fields[0..$CKSUM_NUMFIELDS-1]);
-}
-
-# check the checksums in $dir/$cksum_file
-# if $quiet is set, only print failed files
-sub check_cksums {
-	my ($dir, $cksum_file, $quiet) = @_;
-	my $cksums = read_cksum_file("$dir/$cksum_file", {});
-	return 0 if !defined $cksums;
-	my $failed = 1;
-	foreach my $file (keys %$cksums) {
-		my $path = "$dir/$file";
-		my $output = get_cksum $path;
-		next if !defined $output;
-		if ($output eq $cksums->{$file}) {
-			print "OK $path\n" if !$quiet;
-		} else {
-			print "FAILED $path\n";
-			$failed = 0;
-		}
-	}
-	return $failed;
-}
-
-# check the checksums of all files and directories in @dirs
-sub check_files {
-	my $args = shift;
-	my @dirs;
-	foreach my $file (@_) {
-		if (-d $file) {
-			push @dirs, $file;
-			next;
-		}
-		my $dir = dirname $file;
-		my $base = basename $file;
-		if (exists $SPECIAL_FILES{$base}) {
-			warn "ERROR: File is reserved for lumia.pl: $file\n";
-			next;
-		}
-		my $cksums = read_cksum_file("$dir/$CKSUM_FILE");
-		next if !defined $cksums;
-		if (!exists $cksums->{$base}) {
-			warn "ERROR: File doesn't exist in checksums: $file\n";
-			next;
-		}
-		my $output = get_cksum "$file";
-		next if !defined $output;
-		if ($output eq $cksums->{$base}) {
-			print "OK $file\n" if !$args->{"q"};
-		} else {
-			print "FAILED $file\n";
-		}
-	}
-	my $iter = make_lumia_iter 0, @dirs;
-	while (my $file = $iter->()) {
-		check_cksums $file, $DOUBLE_CKSUM_FILE, $args->{"q"};
-		check_cksums $file, $CKSUM_FILE, $args->{"q"};
-	}
-}
-
-# write the checksums of the special lumia files given as arguments
-# to $DOUBLE_CKSUM_FILE in $dir
-sub write_special_cksums {
-	my ($dir, @files) = @_;
-	my $cksum_file = "$dir/$DOUBLE_CKSUM_FILE";
-	my $cksums = {};
-	if (-f $cksum_file) {
-		$cksums = read_cksum_file $cksum_file, {};
-	}
-	return if !defined $cksums;
-	foreach my $file (@files) {
-		my $cksum_output = get_cksum("$dir/$file");
-		next if (!defined $cksum_output);
-		$cksums->{$file} = $cksum_output;
-	}
-	write_file($cksum_file, $cksums, 1);
-}
-
-# search for new files that aren't present in the checksum files
-# - if $file_func is set, it is called for each new file
-# - if $before_dir_func is set, it is called before processing the
-#   files in each directory that has new files OR if a directory
-#   is entirely new (well, it only checks if $DOUBLE_CKSUM_FILE exists)
-# - if $after_dir_func is set, it is called after processing the
-#   files in each directory that has new files
-sub check_new_files {
-	my ($top_dir, $file_func, $before_dir_func, $after_dir_func) = @_;
-	my $iter = make_file_iter sub {1}, sub {
-		my $dir = shift;
-		my $dh;
-		if (!opendir $dh, $dir) {
-			warn "ERROR: Unable to open directory \"$dir\"!";
-			return undef;
-		}
-		my $read_file_noerror = sub {
-			if (-f $_[0]) {
-				return $_[1]->($_[0], {}) // {};
-			}
-			return {};
-		};
-		my $ignore = $read_file_noerror->("$dir/$IGNORE_FILE", \&read_file);
-		my $lumia_dirs = $read_file_noerror->("$dir/$DIR_FILE", \&read_file);
-		my $lumia_files = $read_file_noerror->("$dir/$CKSUM_FILE", \&read_cksum_file);
-		my @dirs;
-		my $found = 0;
-		while (my $file = readdir $dh) {
-			next if $file eq "." || $file eq "..";
-			next if exists $ignore->{$file} || exists $SPECIAL_FILES{$file};
-			if (!exists $lumia_dirs->{$file} && !exists $lumia_files->{$file}) {
-				if (!$found && defined $before_dir_func) {
-					last if !$before_dir_func->($dir);
-				}
-				if (defined $file_func) {
-					$file_func->($dir, $file);
-				} else {
-					print "$dir/$file\n";
-				}
-				$found = 1;
-			}
-			push @dirs, "$dir/$file" if -d "$dir/$file";
-		}
-		closedir $dh;
-		# also call $before_dir_func if the directory has not been initialized yet
-		if (!$found && !-f "$dir/$DOUBLE_CKSUM_FILE" && defined $before_dir_func) {
-			$before_dir_func->($dir);
-		}
-		if ($found && defined $after_dir_func) {
-			$after_dir_func->($dir);
-		}
-		return \@dirs;
-	}, $top_dir;
-	# Is this a horrible hack? I dunno, but it sure is sweet...
-	while ($iter->()) {}
-}
-
-# add all new files in $top_dir to the checksum files
-sub check_add_new_files {
-	my ($top_dir, $args) = @_;
-	my $changed_dirs = 0;
-	my $changed_files = 0;
-	check_new_files $top_dir, sub {
-		my ($dir, $file) = @_;
-		my $fullpath = "$dir/$file";
-		if (-d $fullpath) {
-			my $dir_file = "$dir/$DIR_FILE";
-			my $fh;
-			if (!open $fh, ">>", $dir_file) {
-				warn "ERROR: Unable to append to file \"$dir_file\"!";
-				return;
-			}
-			print $fh '"' . escape_filename($file) . '"' . "\n";
-			close $fh;
-			$changed_dirs = 1;
-		} else {
-			my $cksum_output = get_cksum $fullpath;
-			return if !defined $cksum_output;
-			my $cksum_file = "$dir/$CKSUM_FILE";
-			my $fh;
-			if (!open $fh, ">>", $cksum_file) {
-				warn "ERROR: Unable to append to file \"$cksum_file\"!";
-				return;
-			}
-			print $fh $cksum_output . ' "' . escape_filename($file) . '"' . "\n";
-			close $fh;
-			$changed_files = 1;
-		}
-		print "Added \"$fullpath\"\n" if !$args->{"q"};
-	}, sub {
-		if (-f "$_[0]/$DOUBLE_CKSUM_FILE") {
-			if (!check_cksums $_[0], $DOUBLE_CKSUM_FILE, 1) {
-				warn "Checksum files corrupt in \"$_[0]\", not adding new checksums!\n";
-				return 0;
-			}
-
-		} else {
-			write_cksums($_[0], {}, 1, 1);
-		}
-		return 1;
-	}, sub {
-		if ($changed_dirs) {
-			write_special_cksums $_[0], $DIR_FILE;
-			$changed_dirs = 0;
-		}
-		if ($changed_files) {
-			write_special_cksums $_[0], $CKSUM_FILE;
-			$changed_files = 0;
-		}
-	};
-}
-
-# write the "checksums" in $contents to $path
-# if $is_cksum_file is set, the value each of the keys in $contents points
-# to is written before the key
-sub write_file {
-	my ($path, $contents, $is_cksum_file) = @_;
-	my $fh;
-	if (!open $fh, ">", $path) {
-		warn "ERROR: Unable to open \"$path\" for writing!";
-		return;
-	}
-	foreach my $filename (keys %$contents) {
-		if ($is_cksum_file) {
-			print $fh "$contents->{$filename} ";
-		}
-		print $fh '"' . escape_filename($filename) . '"' . "\n";
-	}
-	close $fh;
-}
-
-# write the checksums in $contents to the file at $path
-sub write_cksum_file {
-	my ($path, $contents) = @_;
-	write_file $path, $contents, 1;
-}
-
-# write the checksums in $contents to $dir
-# any keys that point to undef are taken to be directories and vice versa
-# $files_modified and $dirs_modified control which of the special lumia
-# files actually get written
-# note: this doesn't use write_file, etc. in order to (possibly) be a bit more efficient
-sub write_cksums {
-	my ($dir, $contents, $files_modified, $dirs_modified) = @_;
-	# No, this isn't efficient...
-	my @special_files;
-	my $dirs_fh;
-	my $files_fh;
-	if ($files_modified) {
-		my $path = "$dir/$CKSUM_FILE";
-		if (!open $files_fh, ">", $path) {
-			warn "ERROR: Unable to open \"$path\" for writing!";
-			return;
-		}
-		push @special_files, $CKSUM_FILE;
-	}
-	if ($dirs_modified) {
-		my $path = "$dir/$DIR_FILE";
-		if (!open $dirs_fh, ">", $path) {
-			warn "ERROR: Unable to open \"$path\" for writing!";
-			return;
-		}
-		push @special_files, $DIR_FILE;
-	}
-	foreach my $key (keys %$contents) {
-		if ($files_modified && defined $contents->{$key}) {
-			print $files_fh $contents->{$key} . ' "' . escape_filename($key) . '"' . "\n";
-		} elsif ($dirs_modified && !defined $contents->{$key}) {
-			print $dirs_fh '"' . escape_filename($key) . '"' . "\n";
-		}
-	}
-	close $files_fh if defined $files_fh;
-	close $dirs_fh if defined $dirs_fh;
-	if (@special_files) {
-		write_special_cksums $dir, @special_files;
-	}
-}
-
-# show all files that are present in the checksum files but don't exist on the filesystem anymore
-sub check_old_files {
-	my $top_dir = shift;
-	my $iter = make_lumia_iter 1, $top_dir;
-	while (my $dir = $iter->()) {
-		if (-e $dir) {
-			my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {}) // {};
-			foreach my $file (keys %$cksums) {
-				if (!-e "$dir/$file") {
-					warn "Nonexistent file: \"$dir/$file\"!\n";
-				}
-			}
-		} else {
-			warn "Nonexistent directory: \"$dir\"!\n";
-		}
-	}
-}
-
-# clean up the lumia checksum files, removing any files that aren't present
-# on the filesystem anymore
-sub remove_old_files {
-	my ($top_dir, $args) = @_;
-	my $iter = make_lumia_iter 1, $top_dir;
-	while (my $dir = $iter->()) {
-		if (!-e $dir) {
-			my $parent = dirname $dir;
-			my $child = basename $dir;
-			my $lumia_dirs = read_file("$parent/$DIR_FILE", {}) // {};
-			if (exists $lumia_dirs->{$child}) {
-				delete $lumia_dirs->{$child};
-				write_file "$parent/$DIR_FILE", $lumia_dirs;
-				print "Removed \"$dir\" from \"$parent/$DIR_FILE\"\n" if !$args->{"q"};
-				write_special_cksums $parent, $DIR_FILE;
-			}
-		} else {
-			my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {}) // {};
-			my $found = 0;
-			foreach my $file (keys %$cksums) {
-				if (!-e "$dir/$file") {
-					delete $cksums->{$file};
-					print "Removed \"$dir/$file\" from \"$dir/$CKSUM_FILE\"\n" if !$args->{"q"};
-					$found = 1;
-				}
-			}
-			if ($found) {
-				write_cksum_file "$dir/$CKSUM_FILE", $cksums;
-				write_special_cksums $dir, $CKSUM_FILE;
-			}
-		}
-	}
-}
-
-# sort the given paths into hash based on the dirname
-# returns: a hash with the keys being the dirnames of the given paths and
-# each one pointing to an array containing the basenames of all paths
-# that had this dirname
-sub sort_by_dir {
-	my %sorted_files;
-	foreach my $file (@_) {
-		if (!-e $file) {
-			warn "ERROR: Source file \"$file\" doesn't exist.\n";
-			next;
-		}
-		my $dir = dirname($file);
-		if (!exists($sorted_files{$dir})) {
-			$sorted_files{$dir} = [];
-		}
-		push(@{$sorted_files{$dir}}, basename($file));
-	}
-	return \%sorted_files;
-}
-
-# check if $dst exists and prompt the user whether it should be overwritten
-# returns 0 if it can be overwritten or doesn't exist, 1 if it shouldn't be overwritten
-sub prompt_overwrite {
-	my $dst = shift;
-	if (-e $dst) {
-		print STDERR "WARNING: \"$dst\" exists already. Do you want to replace it? (y/n) ";
-		my $choice = "";
-		while ($choice ne "y" && $choice ne "n") {
-			$choice = <STDIN>;
-			chomp $choice;
-		}
-		if ($choice eq "n") {
-			warn "Not overwriting \"$dst\"\n";
-			return 1;
-		} else {
-			return 0;
-		}
-	}
-	return 0;
-}
-
-# copies the $src files to $dst and updates the checksums in $dst
-# $src: list of source paths
-# $dst: destination directory or file (in latter case only one src is allowed)
-sub copy_files {
-	my ($src, $dst, $args) = @_;
-	my $dst_dir = $dst;
-	if (!-d $dst) {
-		$dst_dir = dirname $dst;
-	}
-	my $diff_name = 0;
-	# check if the file/dir is getting a different name or
-	# just being copied into a different directory
-	if (!-d $dst && !-d $src->[0]) {
-		$diff_name = 1;
-	}
-	if (!-e $dst && -d $src->[0]) {
-		$diff_name = 1;
-	}
-	my $dst_cksums = read_cksums $dst_dir;
-	return if !defined $dst_cksums;
-	my $src_sorted = sort_by_dir(@$src);
-	my $files_touched = 0;
-	my $dirs_touched = 0;
-	foreach my $src_dir (keys %$src_sorted) {
-		my $src_cksums = read_cksums $src_dir;
-		next if !defined $src_cksums;
-		foreach my $src_file (@{$src_sorted->{$src_dir}}) {
-			my $src_path = "$src_dir/$src_file";
-
-			my $dst_path = $diff_name ? $dst : "$dst_dir/$src_file";
-			if (-d $dst_path && -d $src_path) {
-				warn "ERROR: Cannot copy directory to already existing directory\n";
-				next;
-			}
-			if (exists $SPECIAL_FILES{$src_file} || exists $SPECIAL_FILES{basename $dst_path}) {
-				warn "ERROR: Not copying special file\n";
-				next;
-			}
-			next if !$args->{"f"} && prompt_overwrite($dst_path);
-			my $options = $args->{"v"} ? "-av" : "-a";
-			next if system("cp", $options, "--", $src_path, $dst);
-
-			if (-d $src_path) {
-				$dirs_touched = 1;
-			} else {
-				$files_touched = 1;
-			}
-
-			if (exists $src_cksums->{$src_file}) {
-				if ($diff_name) {
-					$dst_cksums->{basename $dst} = $src_cksums->{$src_file};
-				} else {
-					$dst_cksums->{$src_file} = $src_cksums->{$src_file};
-				}
-			} else {
-				warn "WARNING: \"$src_path\" not in cksum or directory list\n";
-			}
-		}
-	}
-	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
-}
-
-# move a file (or directory) from $src to $dst, prompting for confirmation if $dst already exists;
-# automatically appends the basename of $src to $dst if $dst is a directory
-sub move_file {
-	my ($src, $dst, $args) = @_;
-	if (exists $SPECIAL_FILES{basename $src} || exists $SPECIAL_FILES{basename $dst}) {
-		warn "ERROR: Not moving special file\n";
-		return 1;
-	}
-	if (-d $dst) {
-		$dst .= "/" . basename($src);
-	}
-	return 1 if !$args->{"f"} && prompt_overwrite($dst);
-	my $ret;
-	if ($args->{"v"}) {
-		$ret = system("mv", "-v", "--", $src, $dst);
-	} else {
-		$ret = system("mv", "--", $src, $dst);
-	}
-	return 1 if $ret;
-	if (-e $src) {
-		warn "ERROR: file could not be removed from source but will still be " .
-			"removed from checksum database\n";
-	}
-	return 0;
-}
-
-# move all files/directories in $src_files from $src_dir to $dst_dir ($src_files
-# only contains the basenames of the files), removing them from the checksum files
-# in $src_dir and adding them to $dst_cksums
-sub move_from_same_dir {
-	my ($src_dir, $src_files, $dst_cksums, $dst_dir, $args) = @_;
-	my $src_cksums = read_cksums $src_dir;
-	return if !defined $src_cksums;
-	my $files_touched = 0;
-	my $dirs_touched = 0;
-	foreach my $src_file (@$src_files) {
-		my $fullpath = "$src_dir/$src_file";
-		my $tmp_dirs_touched = 0;
-		my $tmp_files_touched = 0;
-		if (-d $fullpath) {
-			$tmp_dirs_touched = 1;
-		} else {
-			$tmp_files_touched = 1;
-		}
-
-		next if move_file($fullpath, $dst_dir, $args);
-
-		# need to be able to check if the path is a directory
-		# before actually moving it
-		$dirs_touched ||= $tmp_dirs_touched;
-		$files_touched ||= $tmp_files_touched;
-		if (exists $src_cksums->{$src_file}) {
-			$dst_cksums->{$src_file} = $src_cksums->{$src_file};
-			delete $src_cksums->{$src_file};
-		} else {
-			warn "WARNING: \"$src_dir/$src_file\" not in cksum or directory list.\n";
-		}
-	}
-	write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
-	return ($files_touched, $dirs_touched);
-}
-
-# rename a single file or directory from $src to $dst
-sub move_rename {
-	my ($src, $dst, $args) = @_;
-	my $src_dir = dirname $src;
-	my $dst_dir = dirname $dst;
-	my $src_file = basename $src;
-	my $dst_file = basename $dst;
-
-	my $src_cksums = read_cksums $src_dir;
-	return if !defined $src_cksums;
-	my $dst_cksums = {};
-	# if a file is simply being renamed in the same dir, the cksums
-	# should only be loaded and written once
-	if ($src_dir eq $dst_dir) {
-		%$dst_cksums = %$src_cksums;
-		delete $dst_cksums->{$src_file};
-	} else {
-		$dst_cksums = read_cksums $dst_dir;
-		return if !defined $dst_cksums;
-	}
-
-	my $files_touched = 0;
-	my $dirs_touched = 0;
-	if (-d $src) {
-		$dirs_touched = 1;
-	} else {
-		$files_touched = 1;
-	}
-
-	return if move_file($src, $dst, $args);
-
-	if (exists($src_cksums->{$src_file})) {
-		$dst_cksums->{$dst_file} = $src_cksums->{$src_file};
-		delete $src_cksums->{$src_file};
-	} else {
-		warn "WARNING: \"$src\" not in cksum or directory list.\n";
-	}
-	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
-	if ($src_dir ne $dst_dir) {
-		write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
-	}
-}
-
-# move all files and directories in $src to $dst
-# - if $dst does not exist, $src is only allowed to contain one path, which is
-# renamed to $dst
-# - if $dst is a file, $src is only allowed to contain a single path (which
-# must be a file), which is renamed to $dst
-# - otherwise, all files and directories in $src are moved to $dst
-# $src: list of source paths
-# $dst: destination directory or file (in latter case only one src is allowed)
-sub move_files {
-	my ($src, $dst, $args) = @_;
-	if (!-d $dst && $#$src != 0) {
-		die "move: only one source argument allowed when destination is a file\n";
-	}
-	if (!-d $dst && !-d $src->[0]) {
-		move_rename $src->[0], $dst, $args;
-		return;
-	}
-	if (!-e $dst && -d $src->[0]) {
-		move_rename $src->[0], $dst, $args;
-		return;
-	}
-	if (-e $dst && !-d $dst && -d $src->[0]) {
-		die "move: can't move directory to file\n";
-	}
-	# Separate files by current dir so the cksum and dir files only need to be opened once
-	my $src_files = sort_by_dir(@$src);
-	my $dst_cksums = read_cksums $dst;
-	return if !defined $dst_cksums;
-	my $files_touched = 0;
-	my $dirs_touched = 0;
-	foreach my $src_dir (keys %$src_files) {
-		my ($tmp_files_touched, $tmp_dirs_touched) = move_from_same_dir $src_dir, $src_files->{$src_dir}, $dst_cksums, $dst, $args;
-		$files_touched ||= $tmp_files_touched;
-		$dirs_touched ||= $tmp_dirs_touched;
-	}
-	write_cksums $dst, $dst_cksums, $files_touched, $dirs_touched;
-}
-
-# remove a file or directory from the filesystem
-sub remove_file_dir {
-	my ($path, $args) = @_;
-	my $options = $args->{"f"} ? "-rf" : "-r";
-	if (system("rm", $options, "--", $path)) {
-		return 1;
-	}
-	if (-e $path) {
-		warn "ERROR: Unable to remove \"$path\" from filesystem but " .
-			"will still be removed from checksum database\n";
-	}
-	return 0;
-}
-
-# remove all files in one directory, updating the checksum files in the process
-# note: the files are only allowed to be basenames, i.e., they must be the
-# actual filenames present in the checksum files
-sub remove_from_same_dir {
-	my ($args, $dir, @files) = @_;
-	my $cksums = read_cksums $dir;
-	return if !defined $cksums;
-	my $dirs_touched = 0;
-	my $files_touched = 0;
-	foreach my $file (@files) {
-		if (exists $SPECIAL_FILES{$file}) {
-			warn "ERROR: not removing special file $file\n";
-			next;
-		}
-		my $fullpath = "$dir/$file";
-		if (!-e $fullpath) {
-			warn "\"$fullpath\": No such file or directory.\n";
-		}
-		next if remove_file_dir($fullpath, $args);
-		if (exists $cksums->{$file}) {
-			if (defined $cksums->{$file}) {
-				$files_touched = 1;
-			} else {
-				$dirs_touched = 1;
-			}
-			delete $cksums->{$file};
-		} else {
-			warn "WARNING: \"$file\" not in cksum or directory list.\n";
-		}
-	}
-	write_cksums $dir, $cksums, $files_touched, $dirs_touched;
-}
-
-# remove all given files and directories, updating the appropriate checksum
-# files in the process
-sub remove_files {
-	my $args = shift;
-	my $sorted_files = sort_by_dir(@_);
-	foreach my $dir (keys %$sorted_files) {
-		remove_from_same_dir($args, $dir, @{$sorted_files->{$dir}});
-	}
-}
-
-# create the given directories, initializing them with empty checksum files
-# note: does not work like "mkdir -p", i.e., the new directories have to
-# be located inside already existing directories
-sub make_dirs {
-	my @created_dirs;
-	foreach (@_) {
-		if (system("mkdir", "--", $_)) {
-			warn "ERROR creating directory $_\n";
-			next;
-		}
-		push(@created_dirs, $_);
-	}
-	# Separate files by current dir so the cksum and dir files only need to be opened once
-	my %dirs;
-	foreach my $dir (@created_dirs) {
-		write_cksums $dir, {}, 1, 1;
-		my $parent = dirname($dir);
-		if (!exists($dirs{$parent})) {
-			$dirs{$parent} = [];
-		}
-		push(@{$dirs{$parent}}, basename($dir));
-	}
-	foreach my $parent (keys %dirs) {
-		my $parent_dirs = read_file "$parent/$DIR_FILE", {};
-		next if !defined $parent_dirs;
-		foreach my $dir (@{$dirs{$parent}}) {
-			$parent_dirs->{$dir} = "";
-		}
-		write_file "$parent/$DIR_FILE", $parent_dirs;
-		write_special_cksums $parent, $DIR_FILE;
-	}
-}
-
-# extract all special lumia files from $src_dir to $dst_dir, recreating the
-# entire directory structure in the process
-sub extract {
-	my ($src_dir, $dst_dir, $args) = @_;
-	my $iter = make_lumia_iter 0, $src_dir;
-	my $options = $args->{"v"} ? "-av" : "-a";
-	while (my $dir = $iter->()) {
-		my $final_dir = abs2rel $dir, $src_dir;
-		my $fulldir = catfile $dst_dir, $final_dir;
-		system("mkdir", "-p", "--", $fulldir);
-		foreach my $file (keys %SPECIAL_FILES) {
-			my $filepath = catfile $dir, $file;
-			if (-e $filepath) {
-				system("cp", $options, "--", $filepath, catfile($fulldir, $file));
-			}
-		}
-	}
-}
-
-# update the checksums of the given files
-# ignores any directories given as arguments
-sub update {
-	my @files;
-	foreach (@_) {
-		if (-d $_) {
-			warn "Ignoring directory \"$_\"\n";
-		} else {
-			push @files, $_;
-		}
-	}
-	my $sorted_files = sort_by_dir @files;
-	foreach my $dir (keys %$sorted_files) {
-		my $cksums = read_cksum_file "$dir/$CKSUM_FILE", {};
-		next if !defined $cksums;
-		my $changed = 0;
-		foreach my $file (@{$sorted_files->{$dir}}) {
-			my $cksum_output = get_cksum "$dir/$file";
-			next if !defined $cksum_output;
-			$cksums->{$file} = $cksum_output;
-			$changed = 1;
-		}
-		if ($changed) {
-			write_cksum_file "$dir/$CKSUM_FILE", $cksums;
-			write_special_cksums $dir, $CKSUM_FILE;
-		}
-	}
-}
-
-sub update_special {
-	my $dir = shift;
-	write_special_cksums $dir, $CKSUM_FILE, $DIR_FILE;
-}
-
-my %args;
-Getopt::Long::Configure("bundling");
-GetOptions(\%args, "f|force", "q|quiet", "v|verbose", "h|help");
-
-pod2usage(-exitval => 0, -verbose => 2) if $args{"h"};
-pod2usage(-exitval => 1, -verbose => 1) if @ARGV < 1;
-
-my $cmd = shift;
-
-if ($cmd eq "mv") {
-	die "mv requires at least two arguments\n" if @ARGV < 2;
-	my @src = @ARGV[0..$#ARGV-1];
-	move_files \@src, $ARGV[-1], \%args;
-} elsif ($cmd eq "rm") {
-	die "rm requires at least one argument\n" if @ARGV < 1;
-	remove_files \%args, @ARGV;
-} elsif ($cmd eq "addnew") {
-	my $dir = @ARGV ? $ARGV[0] : ".";
-	check_add_new_files $dir, \%args;
-} elsif ($cmd eq "checknew") {
-	my $dir = @ARGV ? $ARGV[0] : ".";
-	check_new_files $dir;
-} elsif ($cmd eq "checkold") {
-	my $dir = @ARGV ? $ARGV[0] : ".";
-	check_old_files $dir;
-} elsif ($cmd eq "rmold") {
-	my $dir = @ARGV ? $ARGV[0] : ".";
-	remove_old_files $dir, \%args;
-} elsif ($cmd eq "check") {
-	if (@ARGV < 1) {
-		check_files \%args, ".";
-	} else {
-		check_files \%args, @ARGV;
-	}
-} elsif ($cmd eq "clean") {
-	my $dir = @ARGV ? $ARGV[0] : ".";
-	clean_files $dir, \%args;
-} elsif ($cmd eq "extract") {
-	my $src_dir = ".";
-	my $dst_dir;
-	if (@ARGV == 2) {
-		$src_dir = $ARGV[0];
-		$dst_dir = $ARGV[1];
-	} elsif (@ARGV == 1) {
-		$dst_dir = $ARGV[0];	
-	} else {
-		die "Invalid number of arguments\n";
-	}
-	if (!-d $src_dir) {
-		die "ERROR: Directory \"$src_dir\" does not exist.\n";
-	}
-	if (!-d $dst_dir) {
-		die "ERROR: Directory \"$dst_dir\" does not exist.\n";
-	}
-	extract $src_dir, $dst_dir;
-} elsif ($cmd eq "cp") {
-	die "cp requires at least two arguments\n" if @ARGV < 2;
-	my @src = @ARGV[0..$#ARGV-1];
-	copy_files \@src, $ARGV[-1], \%args;
-} elsif ($cmd eq "mkdir") {
-	die "mkdir requires at least one argument\n" if @ARGV < 1;
-	make_dirs @ARGV;
-} elsif ($cmd eq "update") {
-	die "update requires at least one argument\n" if @ARGV < 1;
-	update @ARGV;
-} elsif ($cmd eq "updatespecial") {
-	die "Invalid number of arguments\n" if @ARGV > 1;
-	my $dir = @ARGV ? $ARGV[0] : ".";
-	update_special $dir;
-} else {
-	pod2usage(-exitval => 1, -verbose => 1);
-}
-
-__END__
-
-=head1 NAME
-
-lumia.pl - Manage checksums on a filesystem
-
-=head1 SYNOPSIS
-
-B<lumia.pl> command [-hqfv] arguments
-
-=head1 OPTIONS
-
-=over 8
-
-=item B<-h>, B<--help>
-
-Show the full documentation.
-
-=item B<-q>, B<--quiet>
-
-Only output errors.
-
-=item B<-f>, B<--force>
-
-Overwrite files without prompting for confirmation.
-
-=item B<-v>, B<--verbose>
-
-Print each file that is processed by the command.
-
-=back
-
-See the full documentation for details on which commands support which options
-and what they do.
-
-It does not matter if the options are written before or after the command.
-
-If C<--> is written anywhere on the command line, option parsing is stopped,
-so that files starting with a hyphen can still be specified.
-
-Note that C<-q> and C<-v> aren't exactly opposites - C<-q> applies to commands
-like B<check>, where it suppresses printing of the individual files, while
-C<-v> applies to commands like B<cp>, where it is just passed on to the system
-command called in the background.
-
-Note further that this is very inconsistent, like the rest of the program, but
-the author has made too many bad decisions to rectify that problem at the moment.
-
-=head1 COMMANDS
-
-Note that some commands support multiple files/directories as arguments and others,
-for which it would make just as much sense, don't. That's just the way it is.
-
-=over 8
-
-=item B<addnew> [-q] [directory]
-
-Walks through B<directory>, adding all new files to the checksum database.
-B<directory> defaults to the current directory.
-
-C<-q> suppresses the printing of each file or directory as it is added.
-
-=item B<checknew> [directory]
-
-Walks through B<directory>, printing all files that aren't part of the checksum
-database. B<directory> defaults to the current directory.
-
-=item B<checkold> [directory]
-
-Prints all files in the checksum database that do not exist on the filesystem anymore.
-B<directory> defaults to the current directory.
-
-=item B<rmold> [-q] [directory]
-
-Removes all files found by B<checkold> from the database. B<directory> defaults to
-the current directory.
-
-C<-q> suppresses the printing of each file as it is removed.
-
-=item B<check> [-q] file/directory ...
-
-Verifies the checksums of all files given, recursing through any directories. If no
-files or directories are given, the current directory is used.
-
-Note that the checksum database in the corresponding directory will be read again for
-every file given on the command line, even if 1000 files in the same directory are given.
-This problem does not occur when recursing through directories, so it is best to only
-give files directly when checking a few. This problem wouldn't be too difficult to
-fix, but, frankly, I'm too lazy, especially since I only added the feature to check
-files individually as a convenience when I want to quickly check a single file in a
-large directory.
-
-To explain why it is this way: The directory recursion is done using an iterator, which
-has the directories pushed onto its queue in the beginning. The iterator only returns
-directories, which are then checked all in one go, but this means that files given on
-the command line need to be handled specially.
-
-C<-q> suppresses the printing of all good checksums but still allows a message to
-be printed when a checksum failed.
-
-=item B<clean> [-q] [directory]
-
-Removes all lumia special files used to store the checksum database from B<directory>
-recursively. B<directory> defaults to the current directory.
-
-Note that this recurses through the entire directory tree, not just the part that is
-actually linked together by the checksum database.
-
-Warning: This just blindly removes all files with one of the special lumia names,
-even if they weren't actually created by lumia.pl.
-
-C<-q> suppresses the printing of each file as it is deleted.
-
-=item B<extract> [-v] [source] destination
-
-Recreates the entire directory structure from B<source> in B<destination>, but only
-copies the special files used to store the checksum database. B<source> defaults to
-the current directory.
-
-C<-v> prints each file as it is copied.
-
-Note that this overwrites files in the destination directory without confirmation.
-
-=item B<mkdir> directory ...
-
-Creates the given directories, initializing them with empty checksum database files.
-
-=item B<update> file ...
-
-Recalculates the checksums for the given files and replaces them in the database.
-
-Note: Directories given as arguments are ignored.
-
-This is mainly meant to quickly "touch" a file after it was modified (e.g. a
-notes file that is occasionally updated).
-
-=item B<updatespecial> [directory]
-
-Recalculates the checksums for the special files C<.lumidify_archive_dirs> and
-C<.lumidify_archive_cksums> and writes them to C<.lumidify_archive_cksums.cksum>.
-B<directory> defaults to the current directory.
-
-This is only meant to be used if, for some reason, the checksum files had to
-be edited manually and thus don't match the checksums in C<.lumidify_archive_cksums.cksum>
-anymore.
-
-=item B<rm> [-f] file ...
-
-Removes the given files and directories recursively from the filesystem and
-checksum database. The following caveats apply:
-
-If any actual errors occur while deleting the file/directory (i.e. the system
-command C<rm> returns a non-zero exit value), the checksum or directory B<is
-left in the database>. If the system C<rm> does not return a non-zero exit value,
-but the file/directory still exists afterwards (e.g. there was a permission
-error and the user answered "n" when prompted), a warning message is printed,
-but the files B<are removed from the database> (if the database can be
-written to).
-
-It is an error if there are no checksum database files in the directory
-of a file named on the command line.
-
-C<-f> is passed through to the system C<rm> command.
-
-=item B<cp> [-vf] source target
-
-=item B<cp> [-vf] source ... directory
-
-Copies the given source files, updating the checksum database in the process.
-
-If the last argument is a file, there must be only one source argument, also a file,
-which is then copied to the target.
-
-If the last argument is a directory, all source arguments are copied into it.
-
-It is an error if a source or destination directory does not contain any
-checksum database files.
-
-B<cp> will issue a warning and skip to the next argument if it is asked to
-merge a directory with an already existing directory. For instance, attempting
-to run C<cp dir1 dir2>, where C<dir2> already contains a directory named
-C<dir1>, will result in an error. This may change in the future, when the
-program is modified to recursively copy the files manually, instead of simply
-calling the system C<cp> on each of the arguments. If this was supported in
-the current version, none of the checksums inside that directory would be
-updated, so it wouldn't be very useful.
-
-C<-v> is passed through to the system C<cp> command.
-
-C<-f> silently overwrites files without prompting the user, much like the
-C<-f> option in the system C<cp> command. This is handled manually by the
-program, though, in order to actually determine what the user chose. See
-also the caveat mentioned above.
-
-=item B<mv> [-f] source target
-
-=item B<mv> [-f] source ... directory
-
-Moves the given source files, updating the checksum database in the process.
-
-If the last argument is a file or does not exist, there must be only one source
-argument, which is renamed to the target name.
-
-If the last argument is an existing directory, all source arguments are moved
-into it.
-
-It is an error if a source or destination directory does not contain any
-checksum database files.
-
-B<mv> behaves the same as B<rm> with regards to checking if the source file
-is still present after the operation and other error handling.
-
-C<-f> is handled in the same manner as with B<cp>.
-
-=back
-
-=head1 DESCRIPTION
-
-lumia.pl is meant for managing checksums of files in order to prevent bitrot.
-It does this by storing several special files in each directory to keep track
-of the checksums:
-
-=over 8
-
-=item B<.lumidify_archive_cksums>
-
-Contains the checksums of all files in the directory.
-
-=item B<.lumidify_archive_dirs>
-
-Contains a list of all directories in the directory.
-
-=item B<.lumidify_archive_cksums.cksum>
-
-Contains the checksums of B<.lumidify_archive_cksums> and B<.lumidify_archive_dirs>
-in order to provide two-layer protection against bitrot.
-
-=item B<.lumidify_archive_ignore>
-
-Contains a list of files and directories that should be ignored by lumia.pl.
-Note that this is only read and never written to, unless the command B<clean>
-is used. It is, however, still copied over by the B<extract> command.
-
-=back
-
-When the documentation for the commands talks about the "checksum database",
-it simply means these files.
-
-All file/directory names are enclosed in quotes, with any backslashes or quotes
-inside the name escaped with another backslash. The names are allowed to have
-newlines in them.
-
-The list files only contain a list of filenames, with a newline between the
-closing quote of one name and the opening quote of the next one.
-
-The checksum files additionally contain the output of the checksum program
-used and a space before the starting quote of the filename.
-
-=head1 MOTIVATION
-
-There are already several programs that can be used to check for bitrot,
-as listed in L</"SEE ALSO">. However, all programs I tried either were
-much too complicated for my taste or just did everything behind my back.
-I wanted a simple tool that did exactly what I told it to and also allowed
-me to keep the old checksums when reorganizing files, in order to avoid
-regenerating the checksums from corrupt files. Since I couldn't find those
-features in any program I tried, I wrote my own.
-
-=head1 DESIGN DECISIONS
-
-It may strike some readers as a peculiar idea to save the checksum files in
-I<every single directory>, but this choice was made after much deliberation.
-The other option I could think of was to have one big database, but that
-would have made all commands much more difficult to implement and additionally
-necessitated opening the entire database for every operation. With individual
-files in each directory, operations like B<cp> become quite trivial (ignoring
-all the edge cases) since only the toplevel checksums need to be copied to
-the new destination, and any subdirectories already contain the checksums.
-
-This method is not without its drawbacks, however. The most glaring problem
-I have found is that there is no way to store the checksums of read-only
-directories or any special directories that cannot be littered with the
-checksum files because that would clash with other software. Despite these
-drawbacks, however, I decided to stick with it because it works for almost
-all cases and doesn't have any of the serious drawbacks that other options
-would have had.
-
-The names of the special files were chosen to be ".lumidify_archive*" not
-out of vanity, but mainly because I couldn't think of any regular files
-with those names, making them a good choice to avoid clashes.
-
-The name of the program, C<lumia.pl> (for "lumidify archive"), was similarly
-chosen because it did not clash with any programs installed on my system and
-thus allowed for easy tab-completion.
-
-=head1 HASH ALGORITHMS
-
-By default, the simple cksum algorithm is used to get the checksums. This
-is not very secure, but the main purpose of the program is to prevent
-bitrot, for which cksum should be sufficient, especially since it is much
-faster than other algorithms.
-
-There is currently no convenient way to change the algorithm other than
-changing the $CKSUM_CMD and $CKSUM_NUMFIELDS variables at the top of
-lumia.pl. $CKSUM_CMD must be the command that returns the checksum
-when it is given a file, and $CKSUM_NUMFIELDS specifies the number of
-space-separated fields the checksum consists of. This has to be specified
-in order to determine where the checksum ends and the filename begins in
-the output. This would be redundant if all implementations of cksum
-supported '-q' for outputting only the checksum, but that only seems to
-be supported by some implementations.
-
-=head1 USAGE SCENARIOS
-
-=over 8
-
-=item B<Security auditing>
-
-This program is B<NOT> designed to provide any security auditing, as should
-be clear from the fact that the checksums are stored right in the same
-directory as the files. See mtree(8) for that.
-
-If you want to, however, you could set $CKSUM_CMD to a secure hash (not cksum)
-and B<extract> the checksums to a separate directory, which you keep in a
-safe place. You could then use the regular C<cp> command to simply replace
-all the checksums with the ones from your backup, in case an attacker modified
-the checksum database in the directory with the actual files you're trying to
-protect. I don't know if there would be any point in doing that, though.
-
-=item B<Managing archives>
-
-This is the purpose I wrote the program for.
-
-You can simply initialize your archive directory with the B<addnew> command.
-Whenever you add new files, just run B<addnew> again. If you want to reorganize
-the archive, you can use the limited commands available.
-
-I usually just use rsync(1) to copy the entire archive directory over to other
-backup drives and then use the B<check> command again on the new drive.
-
-I also have checksums for the main data directory on my computer (except for
-things like git repositories, which I don't want littered with the database
-files). Here, I use the B<update> command for files that I edit more often
-and occasionally run B<check> on the entire directory.
-
-Since the database files are written in each directory, you can run the
-B<addnew> command in any subdirectory when you've added new files there.
-
-=back
-
-=head1 PERFORMANCE
-
-Due to the extensive use of iterators and the author's bad life choices,
-some functions, such as B<addnew> and B<check>, run more slowly than they
-would if they were programmed more efficiently, especially on many small
-files and folders. Too bad.
-
-=head1 PORTABILITY
-
-This program was written on OpenBSD. It will probably work on most other
-reasonably POSIX-Compliant systems, although I cannot guarantee anything.
-$CKSUM_CMD may need to be modified at the top of lumia.pl. The file
-operation commands are called directly with system(), so those need to
-be available.
-
-It will most certainly not work on Windows, but that shouldn't be a
-problem for anyone important.
-
-=head1 BUGS
-
-All system commands (unless I forgot some) are called with "--" before
-listing the actual files, so files beginning with hyphens should be
-supported. I have tested the commands with filenames starting with spaces
-and hyphens and also containing newlines, but there may very well be issues
-still. Please notify me if you find any filenames that do not work. Handling
-filenames properly is difficult.
-
-There are probably many other edge cases, especially in the B<mv>, B<cp>,
-and B<rm> commands. Please notify me if you find an issue.
-
-Operations on files containing newlines may cause Perl to print a warning
-"Unsuccessful stat on filename containing newline" even though nothing is
-wrong since (as described in B<mv> and B<rm>) existence of the file is
-checked afterwards. I didn't feel like disabling warnings, and no normal
-person should be working with files containing newlines anyways, so that's
-the way it is.
-
-=head1 EXIT STATUS
-
-Always 0, unless the arguments given were invalid. We don't do errors around here.
-
-On a more serious note - I should probably change that at some point.
-For the time being, if you want to run B<check> in a script, you can test
-the output printed when the C<-q> option is used, since this won't output
-anything if there are no errors. Do note, though, that actual errors (file not
-found, etc.) are printed to STDERR, while incorrect checksums are printed
-to STDOUT.
-
-=head1 SEE ALSO
-
-par2(1), mtree(8), aide(1), bitrot(no man page)
-
-=head1 LICENSE
-
-Copyright (c) 2019, 2020, 2021 lumidify <nobody[at]lumidify.org>
-
-Permission to use, copy, modify, and/or 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.
-
-=cut
diff --git a/tests/README b/tests/README
@@ -5,3 +5,6 @@ To run all tests, simply run alltests.sh
 
 There aren't that many tests yet. Maybe I'll get around
 to adding more tests for all the edge cases someday.
+
+These only work properly on OpenBSD.
+Maybe I'll fix that someday.