Mercurial > cgi-bin > hgweb.cgi > PassMan
view src/main/kotlin/name/blackcap/passman/ListSubcommand.kt @ 20:4391afcf6bd0
Fix more bugs, correct more bad tests.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sun, 30 Jun 2024 22:28:52 -0700 |
parents | 72619175004e |
children | ea65ab890f66 |
line wrap: on
line source
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]", options) exitProcess(0) } if (commandLine.args.isNotEmpty()) { die("unexpected trailing arguments", 2) } 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 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) ) val passed = matchers .map { x -> x.value.fold(true) { acc, pred -> acc && pred(entry.getField(x.key)) } } .reduceOrNull() { a, b -> a && b } ?: true if (passed) { if (count > 0) { println() } printer(entry, null) count++ } } val s = if (count == 1) "" else "s" if (count > 0) { println() } println("$count record$s found") } } }