view src/main/kotlin/name/blackcap/passman/ListSubcommand.kt @ 11:c69665ff37d0

Add merge subcommand (untested).
author David Barts <n5jrn@me.com>
date Sat, 21 Jan 2023 15:39:42 -0800
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")
        }
    }
}