Mercurial > cgi-bin > hgweb.cgi > PassMan
comparison 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 |
comparison
equal
deleted
inserted
replaced
5:ad997df1f560 | 6:711cc42e96d7 |
---|---|
1 package name.blackcap.passman | |
2 | |
3 import org.apache.commons.cli.* | |
4 import java.util.regex.PatternSyntaxException | |
5 import kotlin.system.exitProcess | |
6 | |
7 class ListSubcommand(): Subcommand() { | |
8 private companion object { | |
9 const val CASE = "case" | |
10 const val FIXED = "fixed" | |
11 const val HELP = "help" | |
12 const val LONG = "long" | |
13 const val NAME = "name" | |
14 const val NOTES = "notes" | |
15 const val USERNAME = "username" | |
16 const val ACCESSED = "accessed" | |
17 const val CREATED = "created" | |
18 const val MODIFIED = "modified" | |
19 val FLAG_OPTIONS = listOf<OptionDescriptor>( | |
20 OptionDescriptor(CASE, "Treat upper and lower case as distinct."), | |
21 OptionDescriptor(FIXED, "Match fixed substrings instead of regular expressions."), | |
22 OptionDescriptor(HELP, "Print this help message."), | |
23 OptionDescriptor(LONG, "Long format listing.") | |
24 ) | |
25 val STRING_OPTIONS = listOf<OptionDescriptor>( | |
26 OptionDescriptor(NAME, "Match site name."), | |
27 OptionDescriptor(NOTES, "Match notes."), | |
28 OptionDescriptor(USERNAME, "Match username.") | |
29 ) | |
30 val TIME_OPTIONS = listOf<OptionDescriptor>( | |
31 OptionDescriptor(ACCESSED, "Match time password last read."), | |
32 OptionDescriptor(CREATED, "Match time created."), | |
33 OptionDescriptor(MODIFIED, "Match time last modified.") | |
34 ) | |
35 val REDACTED = "(redacted)".toCharArray() | |
36 } | |
37 private lateinit var commandLine: CommandLine | |
38 private val matchers = mutableMapOf<String, MutableList<(Any) -> Boolean>>() | |
39 | |
40 private data class OptionDescriptor(val name: String, val help: String) | |
41 | |
42 override fun run(args: Array<String>) { | |
43 parseArgs(args) | |
44 runQuery() | |
45 } | |
46 | |
47 private fun parseArgs(args: Array<String>): Unit { | |
48 val options = Options().apply { | |
49 FLAG_OPTIONS.forEach { | |
50 addOption(it.name.first().toString(), it.name, false, it.help) | |
51 } | |
52 (STRING_OPTIONS + TIME_OPTIONS).forEach { | |
53 addOption(Option(null, it.name, true, it.help).apply | |
54 { setArgs(Option.UNLIMITED_VALUES) }) | |
55 } | |
56 } | |
57 try { | |
58 commandLine = DefaultParser().parse(options, args) | |
59 } catch (e: ParseException) { | |
60 die(e.message ?: "syntax error", 2) | |
61 } | |
62 if (commandLine.hasOption(HELP)) { | |
63 HelpFormatter().printHelp("$SHORTNAME list", options) | |
64 exitProcess(0) | |
65 } | |
66 | |
67 STRING_OPTIONS.forEach { | |
68 commandLine.getOptionValues(it.name)?.forEach { value -> | |
69 val regexOptions = mutableSetOf<RegexOption>() | |
70 if (commandLine.hasOption(FIXED)) { | |
71 regexOptions.add(RegexOption.LITERAL) | |
72 } | |
73 if (!commandLine.hasOption(CASE)) { | |
74 regexOptions.add(RegexOption.IGNORE_CASE) | |
75 } | |
76 try { | |
77 if (it.name !in matchers) { | |
78 matchers[it.name] = mutableListOf<(Any) -> Boolean>() | |
79 } | |
80 matchers[it.name]!! += { x -> x is String && x.contains(Regex(value, regexOptions)) } | |
81 } catch (e: PatternSyntaxException) { | |
82 die("${see(value)} - invalid regular expression") | |
83 } | |
84 } | |
85 } | |
86 | |
87 TIME_OPTIONS.forEach { | |
88 commandLine.getOptionValues(it.name)?.forEach { rawValue -> | |
89 if (rawValue.isEmpty()) { | |
90 die("empty string is not a valid time expression") | |
91 } | |
92 val (op, exp) = when(rawValue.first()) { | |
93 '+', '>' -> Pair<Char, String>('>', rawValue.substring(1)) | |
94 '=' -> Pair<Char, String>('=', rawValue.substring(1)) | |
95 '-', '<' -> Pair<Char, String>('<', rawValue.substring(1)) | |
96 else -> Pair<Char, String>('=', rawValue) | |
97 } | |
98 val value = parseDateTime(exp) | |
99 if (value == null) { | |
100 die("${see(rawValue)} - invalid time expression") | |
101 throw RuntimeException("will never happen") | |
102 } | |
103 if (it.name !in matchers) { | |
104 matchers[it.name] = mutableListOf<(Any) -> Boolean>() | |
105 } | |
106 when(op) { | |
107 '>' -> matchers[it.name]!! += { x -> x is Long && x > value } | |
108 '=' -> matchers[it.name]!! += { x -> x is Long && (x/1000L) == (value/1000L) } | |
109 '<' -> matchers[it.name]!! += { x -> x is Long && x < value } | |
110 else -> throw RuntimeException("should never happen") | |
111 } | |
112 } | |
113 } | |
114 } | |
115 | |
116 private fun runQuery(): Unit { | |
117 val db = Database.open() | |
118 | |
119 db.connection.prepareStatement("select $NAME, $USERNAME, $NOTES, $CREATED, $MODIFIED, $ACCESSED from passwords").use { | |
120 val results = it.executeQuery() | |
121 val printer = if (commandLine.hasOption(LONG)) Entry::printLong else Entry::print | |
122 var count = 0; | |
123 while (results.next()) { | |
124 val passed = matchers | |
125 .map { entry -> entry.value.fold(true) { | |
126 acc, pred -> acc && pred(results.getDecryptedString(entry.key, db.encryption)) } } | |
127 .reduceOrNull() { a, b -> a && b } | |
128 ?: true | |
129 if (passed) { | |
130 val entry = Entry( | |
131 name = results.getDecryptedString(1, db.encryption), | |
132 username = results.getDecryptedString(2, db.encryption), | |
133 password = REDACTED, | |
134 notes = results.getDecryptedString(3, db.encryption), | |
135 created = results.getDate(4), | |
136 modified = results.getDate(5), | |
137 accessed = results.getDate(6) | |
138 ) | |
139 if (count > 0) { | |
140 println() | |
141 } | |
142 printer(entry, null) | |
143 count++ | |
144 } | |
145 } | |
146 val s = if (count == 1) "" else "s" | |
147 if (count > 0) { | |
148 println() | |
149 } | |
150 println("$count record$s found") | |
151 } | |
152 } | |
153 } | |
154 | |
155 private fun Options.addMultiOption(opt: String?, longOpt: String, hasArg: Boolean, description: String): Unit { | |
156 addOption(Option(opt, longOpt, hasArg, description).apply { args = Option.UNLIMITED_VALUES }) | |
157 } |