-
Notifications
You must be signed in to change notification settings - Fork 0
/
gen_tokens.rb
282 lines (252 loc) · 10.3 KB
/
gen_tokens.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# frozen_string_literal: true
# Author: Hundter Biede (hbiede.com)
# Version: 1.5
# License: MIT
require 'csv'
require 'optparse'
# how many characters to pad
token_char_count = 7
# Whether or not to generate PDFs
generate_pdfs = true
# :nocov:
OptionParser.new do |opt|
opt.on(
'-cCOUNT',
'--chars=COUNT',
Integer,
'How many characters long should tokens be? (Default: 7)'
) { |o| token_char_count = o }
opt.on('-n', '--no-pdfs', 'Disable PDF generation') { generate_pdfs = false }
end.parse!
# :nocov:
# Regex to match the following alphabet: ^[a-km-zA-HJ-NPRT-Z2-46-9]{7,7}$
# noinspection SpellCheckingInspection
CHARS = 'qwertyuiopasdfghjkzxcvbnmWERTYUPADFGHJKLZXCVBNM2346789'.scan(/\w/)
# Apologies for the obscenities, but have to prevent these from showing up in
# the passwords
SWEAR_PREVENTION_MATCHER = /(fuc?k)|(fag)|(cunt)|(n[i1]g)|(a[s5][s5])|
([s5]h[i1]t)|(b[i1]a?t?ch)|(c[l1][i1]t)|(j[i1]zz)|([s5]ex)|([s5]meg)|
(d[i1]c?k?)|(pen[i1][s5])|(pube)|(p[i1][s5][s5])|(g[o0]d)|(crap)|(b[o0]ne)|
(basta)|(ar[s5])|(ana[l1])|(anu[s5])|(ba[l1][l1])|(b[l1][o0]w)|(b[o0][o0]b)|
([l1]mf?a[o0])/ix.freeze
# Writes tokens to PDFs
class PDFWriter
# Compile a unique PDF for a singular organization with its passwords and
# moves it to the 'pdfs' directory
#
# @param [String] org The name of the organization
# @param [String] org_tex The contents of the Latex to be written
def self.write_latex_to_pdf(org, org_tex)
# noinspection RegExpRedundantEscape
pdf_name = format('%<FileName>s.tex', FileName: org.gsub(/[\s().#!]/, ''))
File.write(pdf_name, org_tex)
output = `lualatex #{pdf_name} 2>&1`
result = $CHILD_STATUS.success?
if result
system(format('lualatex %<File>s > /dev/null', File: pdf_name))
# :nocov:
else
warn output
exit 1
# :nocov:
end
end
# Create a the content for a single organization's password PDF
#
# @param [String] tex_file The contents of the Latex template
# @param [String] org The name of the organization
# @param [Array<String>] org_passwords A collection of passwords for a given
# organization
def self.create_latex_content(tex_file, org, org_passwords)
org_tex = tex_file.clone
org_tex = org_tex.gsub('REPLACESCHOOL', org)
# Has to be triple escaped to account for the un-escaping done by ruby, then regex, then latex
password_text = org_passwords.join(" \\\\\\\\\n")
org_tex.gsub('REPLACEPW', password_text)
end
# Print a progress report for the token report generation
#
# @param [Integer] index The index of the current org
# @param [String] org The name of the org that was just finished
# @param [Integer] number_of_orgs The number of organizations being ran
# @param [Integer] longest_org_name_length The length of the longest org name
def self.print_progress_report(index, org, number_of_orgs, longest_name_length)
percent_done = (index + 1.0) / number_of_orgs
filled_char_count = (14 * percent_done).floor
# rubocop:disable Lint/FormatParameterMismatch
format("\r%.2f%%%% [=%s%s]: PDF generated for %-#{longest_name_length}s",
100 * percent_done, '=' * filled_char_count, ' ' * (14 - filled_char_count), org)
# rubocop:enable Lint/FormatParameterMismatch
end
# Create a unique PDF for each organization with its passwords
#
# @param [Hash<String => Array<String>>] all_tokens a mapping of organization
# names onto their associated passwords
# @param [String] tex_file The file contents to print
# @return [String] The console output for the generation
def self.create_pdfs(all_tokens, tex_file)
longest_org_name = all_tokens.keys.max_by(&:length).length
all_tokens.each_with_index do |(org, org_passwords), i|
write_latex_to_pdf(org, create_latex_content(tex_file, org, org_passwords))
printf(print_progress_report(i, org, all_tokens.size, longest_org_name))
end
# Clear the progress bar
print("\rAll PDFs generated!")
system('mv *.pdf pdfs/')
system('rm *.out *.aux *.log *.tex')
format('%<TokenCount>d PDFs generated', TokenCount: all_tokens.length)
end
end
# Creates a set of tokens
class TokenGenerator
# Determines if sufficient arguments were given to the program
# else, exits
# @param [Array<String>] args The arguments to the program
def self.token_arg_count_validator(args)
# print help if no arguments are given or help is requested
return unless args.length < 2 || args[0] == '--help'
error_message = 'Usage: ruby %s [VoterInputFileName] [TokenOutputFileName]'
error_message += "\n\tOne header must contain \"School\", \"Organization\", "
error_message += 'or "Chapter"'
error_message += "\n\tAnother header must contain \"Delegates\" or \"Votes\""
warn format(error_message, $PROGRAM_NAME)
raise ArgumentError unless args.include?('--help')
exit 0
end
# Prints a warning about the proper formatting of the CSV before exiting
def self.invalid_headers_warning
warn 'Invalid CSV:'
warn "\n\tHeaders should be \"School\" and \"Delegates\" in any order"
exit 1
end
# Write all newly generated tokens to CSVs
#
# @param [Hash<String => Array<String>>] all_tokens a mapping of organization
# names onto their associated passwords
# @param [String] file The file to write the tokens to
def self.write_tokens_to_csv(all_tokens, file)
CSV.open(file, 'w') do |f|
f << %w[Organization Token]
all_tokens.each do |org, org_passwords|
org_passwords.each do |password|
f << [org, password]
end
end
end
end
# Read the contents of the given CSV file
#
# @param [String] file_name The name of the file
# @return [Array<Array<String>>]the contents of the given CSV file
def self.read_delegate_csv(file_name)
begin
# @type [Array<Array<String>>]
csv = CSV.read(file_name)
rescue Errno::ENOENT
warn format('Sorry, the file %<File>s does not exist', File: file_name)
exit 1
end
csv.delete_if { |line| line.join =~ /^\s*$/ } # delete blank lines
csv
end
# @param [Integer] length The length of the string to be generated
# @return [String] The randomized string
# @private
def self.random_string(length)
length.times.map { CHARS.sample }.join
end
# @private
# @param [Hash<String => Array<String>>] all_tokens The tokens already
# generated, used to prevent duplicates
# @param [Numeric] token_length The length of the token
# @return [String] the new token
def self.gen_token(all_tokens, token_length = token_char_count)
new_token = ''
loop do
new_token = random_string(token_length)
break unless all_tokens.values.flatten.include?(new_token) ||
new_token =~ SWEAR_PREVENTION_MATCHER
end
new_token
end
# Processes the number of delegates given to a single chapter
#
# @param [CSV::Row|Enumerator] line The elements from this line to be processed
# @param [Hash<Integer => integer>] column The columns containing pertinent info
# @param [Hash<String => Array<String>>] all_tokens
# @param [Numeric] token_length The length of the token
def self.process_chapter(line, column, all_tokens, token_char_count)
org = line[column[:Org]]
(0...line[column[:Delegates]].to_i).each do
# gen tokens and push to the csv
if all_tokens.include?(org)
all_tokens.fetch(org).push(gen_token(all_tokens, token_char_count))
else
all_tokens.store(org, [gen_token(all_tokens, token_char_count)])
end
end
end
# Determines what columns indices contain the organizations and delegate counts
#
# @param [Hash<Integer => integer>] columns The mapping to put the columns into
# @param [Array<String>] line The header line
def self.determine_header_columns(columns, line)
# find the column with a header containing the keywords - non-case sensitive
columns[:Org] = line.find_index do |token|
token.match(/(schools?)|(organizations?)|(chapters?)|(names?)/i)
end
columns[:Delegates] = line.find_index do |token|
token.match(/(delegates?)|(voter?s?)/i)
end
end
# Parse the org and generate tokens
#
# @param [Hash<String => Array<String>>] all_tokens The mapping into which the tokens will be inserted
# @param [Array<Array<String>>] lines The lines from the delegate count CSV
# @param [Numeric] token_length The length of the token
def self.parse_organizations(all_tokens, lines, token_char_count)
# index of our two key columns (all other columns are ignored)
# @type [Hash<Integer => integer>]
columns = { Org: 0, Delegates: 0 }
# tokenize all strings to a 2D array
lines.each do |line|
if columns[:Org].nil? || columns[:Delegates].nil?
invalid_headers_warning
elsif columns[:Org] == columns[:Delegates]
# header line
determine_header_columns(columns, line)
else
process_chapter(line, columns, all_tokens, token_char_count)
end
end
end
# Creates the token generation report string. Sets for which the count is nil or non-positive are not counted
#
# @param [Hash<String => Array<String>>] all_tokens The mapping into which the tokens were inserted
# @return [String] A report of the number of tokens generated and the number of groups they are associated with
def self.get_token_count_report(all_tokens)
# noinspection RubyNilAnalysis
trimmed_tokens = all_tokens.filter { |_, count| !count.nil? && count.length.positive? }
format("%<TokenSetCount>d token sets generated (%<TokenCount>d total tokens)\n\n",
TokenSetCount: trimmed_tokens.length,
TokenCount: trimmed_tokens.map { |_, count| count.length }.reduce(:+))
end
# :nocov:
# Manage the program
#
# @param [Boolean] generate_pdfs True if the program should generate PDFs with
# the generated passwords
# @param [Numeric] token_length The length of the token
def self.main(generate_pdfs, token_char_count)
# @type [Hash<String => Array<String>>]
all_tokens = {}
token_arg_count_validator ARGV
lines = read_delegate_csv ARGV[0]
parse_organizations(all_tokens, lines, token_char_count)
write_tokens_to_csv(all_tokens, ARGV[1])
puts get_token_count_report all_tokens
puts PDFWriter.create_pdfs(all_tokens, File.read('pdfs/template/voting.tex')) if generate_pdfs
end
end
TokenGenerator.main(generate_pdfs, token_char_count) if __FILE__ == $PROGRAM_NAME
# :nocov: