Mercurial > cgi-bin > hgweb.cgi > PassMan
diff src/main/kotlin/name/blackcap/passman/ListSubcommand.kt @ 6:711cc42e96d7
Got the list subcommand working, but needs efficiency improvements.
author | David Barts <n5jrn@me.com> |
---|---|
date | Tue, 20 Sep 2022 20:52:21 -0700 |
parents | |
children | f245b9a53495 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/name/blackcap/passman/ListSubcommand.kt Tue Sep 20 20:52:21 2022 -0700 @@ -0,0 +1,157 @@ +package name.blackcap.passman + +import org.apache.commons.cli.* +import java.util.regex.PatternSyntaxException +import kotlin.system.exitProcess + +class ListSubcommand(): Subcommand() { + private companion object { + const val CASE = "case" + const val FIXED = "fixed" + const val HELP = "help" + const val LONG = "long" + const val NAME = "name" + const val NOTES = "notes" + const val USERNAME = "username" + const val ACCESSED = "accessed" + const val CREATED = "created" + const val MODIFIED = "modified" + val FLAG_OPTIONS = listOf<OptionDescriptor>( + OptionDescriptor(CASE, "Treat upper and lower case as distinct."), + OptionDescriptor(FIXED, "Match fixed substrings instead of regular expressions."), + OptionDescriptor(HELP, "Print this help message."), + OptionDescriptor(LONG, "Long format listing.") + ) + val STRING_OPTIONS = listOf<OptionDescriptor>( + OptionDescriptor(NAME, "Match site name."), + OptionDescriptor(NOTES, "Match notes."), + OptionDescriptor(USERNAME, "Match username.") + ) + val TIME_OPTIONS = listOf<OptionDescriptor>( + OptionDescriptor(ACCESSED, "Match time password last read."), + OptionDescriptor(CREATED, "Match time created."), + OptionDescriptor(MODIFIED, "Match time last modified.") + ) + val REDACTED = "(redacted)".toCharArray() + } + private lateinit var commandLine: CommandLine + private val matchers = mutableMapOf<String, MutableList<(Any) -> Boolean>>() + + private data class OptionDescriptor(val name: String, val help: String) + + override fun run(args: Array<String>) { + parseArgs(args) + runQuery() + } + + private fun parseArgs(args: Array<String>): Unit { + val options = Options().apply { + FLAG_OPTIONS.forEach { + addOption(it.name.first().toString(), it.name, false, it.help) + } + (STRING_OPTIONS + TIME_OPTIONS).forEach { + addOption(Option(null, it.name, true, it.help).apply + { setArgs(Option.UNLIMITED_VALUES) }) + } + } + try { + commandLine = DefaultParser().parse(options, args) + } catch (e: ParseException) { + die(e.message ?: "syntax error", 2) + } + if (commandLine.hasOption(HELP)) { + HelpFormatter().printHelp("$SHORTNAME list", options) + exitProcess(0) + } + + STRING_OPTIONS.forEach { + commandLine.getOptionValues(it.name)?.forEach { value -> + val regexOptions = mutableSetOf<RegexOption>() + if (commandLine.hasOption(FIXED)) { + regexOptions.add(RegexOption.LITERAL) + } + if (!commandLine.hasOption(CASE)) { + regexOptions.add(RegexOption.IGNORE_CASE) + } + try { + if (it.name !in matchers) { + matchers[it.name] = mutableListOf<(Any) -> Boolean>() + } + matchers[it.name]!! += { x -> x is String && x.contains(Regex(value, regexOptions)) } + } catch (e: PatternSyntaxException) { + die("${see(value)} - invalid regular expression") + } + } + } + + TIME_OPTIONS.forEach { + commandLine.getOptionValues(it.name)?.forEach { rawValue -> + if (rawValue.isEmpty()) { + die("empty string is not a valid time expression") + } + val (op, exp) = when(rawValue.first()) { + '+', '>' -> Pair<Char, String>('>', rawValue.substring(1)) + '=' -> Pair<Char, String>('=', rawValue.substring(1)) + '-', '<' -> Pair<Char, String>('<', rawValue.substring(1)) + else -> Pair<Char, String>('=', rawValue) + } + val value = parseDateTime(exp) + if (value == null) { + die("${see(rawValue)} - invalid time expression") + throw RuntimeException("will never happen") + } + if (it.name !in matchers) { + matchers[it.name] = mutableListOf<(Any) -> Boolean>() + } + when(op) { + '>' -> matchers[it.name]!! += { x -> x is Long && x > value } + '=' -> matchers[it.name]!! += { x -> x is Long && (x/1000L) == (value/1000L) } + '<' -> matchers[it.name]!! += { x -> x is Long && x < value } + else -> throw RuntimeException("should never happen") + } + } + } + } + + private fun runQuery(): Unit { + val db = Database.open() + + db.connection.prepareStatement("select $NAME, $USERNAME, $NOTES, $CREATED, $MODIFIED, $ACCESSED from passwords").use { + val results = it.executeQuery() + val printer = if (commandLine.hasOption(LONG)) Entry::printLong else Entry::print + var count = 0; + while (results.next()) { + val passed = matchers + .map { entry -> entry.value.fold(true) { + acc, pred -> acc && pred(results.getDecryptedString(entry.key, db.encryption)) } } + .reduceOrNull() { a, b -> a && b } + ?: true + if (passed) { + val entry = Entry( + name = results.getDecryptedString(1, db.encryption), + username = results.getDecryptedString(2, db.encryption), + password = REDACTED, + notes = results.getDecryptedString(3, db.encryption), + created = results.getDate(4), + modified = results.getDate(5), + accessed = results.getDate(6) + ) + if (count > 0) { + println() + } + printer(entry, null) + count++ + } + } + val s = if (count == 1) "" else "s" + if (count > 0) { + println() + } + println("$count record$s found") + } + } +} + +private fun Options.addMultiOption(opt: String?, longOpt: String, hasArg: Boolean, description: String): Unit { + addOption(Option(opt, longOpt, hasArg, description).apply { args = Option.UNLIMITED_VALUES }) +} \ No newline at end of file