2
2
/*
3
3
* @(#) markov-pwgen
4
4
*
5
- * Copyright © 2023, Revolution Robotics, Inc.
5
+ * Copyright © 2023,2024, Revolution Robotics, Inc.
6
6
*
7
7
*/
8
8
import { readFile } from 'node:fs/promises'
9
9
import os from 'node:os'
10
- import { basename , dirname , join , win32 } from 'node:path'
10
+ import path from 'node:path'
11
11
import { fileURLToPath } from 'node:url'
12
12
import { parseArgs } from 'node:util'
13
13
import { Piscina } from 'piscina'
14
14
import random64 from '../lib/random64.js'
15
15
16
- const __dirname = dirname ( fileURLToPath ( import . meta. url ) )
16
+ const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) )
17
+
18
+ const getPkg = async ( ) => {
19
+ const pkgPath = path . resolve ( __dirname , '..' , 'package.json' )
20
+ const pkgStr = await readFile ( pkgPath , {
21
+ encoding : 'utf8' ,
22
+ flag : 'r'
23
+ } )
24
+ return JSON . parse ( pkgStr )
25
+ }
17
26
18
27
const help = ( pgm ) => {
19
28
console . log ( `Usage: ${ pgm } OPTIONS` )
20
29
console . log ( `OPTIONS (defaults are random within the given range):
21
- --attemptsMax=N , -aN
30
+ --attemptsMax, -a N
22
31
Fail after N attempts to generate chain (default: 100)
23
- --count=N , -cN
32
+ --count, -c N
24
33
Generate N hyphen-delimited passwords (default: [3, 4])
25
34
--dictionary, -d
26
35
Allow dictionary-word passwords (default: false)
27
36
--help, -h
28
37
Print this help, then exit.
29
- --lengthMin=N , -nN
38
+ --lengthMin, -n N
30
39
Minimum password length N (default: [4, 6])
31
- --lengthMax=N , -mN
40
+ --lengthMax, -m N
32
41
Maximum password length N (default: [7, 13])
33
- --order=N , -oN
42
+ --order, -o N
34
43
Markov order N (default: [3, 4])
35
- --transliterate=S,T, -tS,T
36
- Replace in password characters of S with corresponding
37
- characters of T.
44
+ --transliterate, -t FROM,TO
45
+ Replace in password characters of FROM with corresponding
46
+ characters of TO.
47
+ --truncate-set1, -s
48
+ By default, transliteration string TO is extended to
49
+ length of FROM by repeating its last character as necessary.
50
+ This option first truncates FROM to length of TO.
38
51
--upperCase, -u
39
52
Capitalize password components.
40
53
--version, -v
41
54
Print version, then exit.
42
55
NB: Lower Markov order yields more random (i.e., less recognizable) words.` )
43
56
}
44
57
45
- // transliterate: Replace in string characters of `s' to corresponding
46
- // characters of `t'.
58
+ // transliterate: Return a new string where characters of string
59
+ // `fromStr' are replaced by corresponding characters of string
60
+ // `toStr'. When option `truncate' is false (the default), `toStr'
61
+ // is extended to the length of `fromStr' by repeating its last
62
+ // character as necessary. When option `truncate' is true, `fromStr'
63
+ // is truncated to the length of `toStr'.
47
64
if ( ! String . prototype . transliterate ) {
48
- String . prototype . transliterate = function ( s , t ) {
49
- if ( s . length > t . length ) {
50
- s = s . slice ( 0 , t . length )
51
- }
52
-
53
- // Initialize object from letters of s and t as properties and
54
- // values, respectively.
55
- const mz = Object . assign ( ...Array . from ( s ) . map ( ( e , i ) => ( { [ e ] : t [ i ] } ) ) )
56
-
57
- return Array . from ( this ) . map ( c => mz [ c ] || c ) . join ( '' )
65
+ String . prototype . transliterate = function ( fromStr = '' , toStr = '' ,
66
+ options = { truncate : false } ) {
67
+ if ( options . truncate )
68
+ fromStr = fromStr . slice ( 0 , toStr . length )
69
+ else
70
+ toStr = toStr . padEnd ( fromStr . length , toStr . slice ( - 1 ) )
71
+
72
+ const xlt =
73
+ Object . assign ( ...Array . from ( fromStr )
74
+ . map ( ( char , i ) => ( { [ char ] : toStr [ i ] } ) ) )
75
+
76
+ return Array . from ( this ) . map ( char => xlt [ char ] || char ) . join ( '' )
58
77
}
59
78
}
60
79
61
- const processArgs = async pgm => {
80
+ const processArgs = async pkg => {
62
81
const options = {
63
82
attemptsMax : {
64
83
type : 'string' ,
@@ -95,6 +114,11 @@ const processArgs = async pgm => {
95
114
short : 'o' ,
96
115
default : `${ Number ( random64 ( 3 , 4 ) ) } `
97
116
} ,
117
+ 'truncate-set1' : {
118
+ type : 'boolean' ,
119
+ short : 's' ,
120
+ default : false
121
+ } ,
98
122
transliterate : {
99
123
type : 'string' ,
100
124
short : 't'
@@ -110,20 +134,16 @@ const processArgs = async pgm => {
110
134
}
111
135
}
112
136
113
- const { values } = parseArgs ( { options } )
137
+ const { values } = parseArgs ( {
138
+ options,
139
+ allowPositionals : false
140
+ } )
114
141
115
142
if ( values . help ) {
116
- help ( pgm )
143
+ help ( pkg . name )
117
144
process . exit ( 0 )
118
145
} else if ( values . version ) {
119
- const pkgPath = join ( __dirname , '..' , 'package.json' )
120
- const pkgStr = await readFile ( pkgPath , {
121
- encoding : 'utf8' ,
122
- flag : 'r'
123
- } )
124
- const pkgObj = JSON . parse ( pkgStr )
125
-
126
- console . log ( `${ pkgObj . name } v${ pkgObj . version } ` )
146
+ console . log ( `${ pkg . name } v${ pkg . version } ` )
127
147
process . exit ( 0 )
128
148
}
129
149
@@ -135,63 +155,60 @@ const processArgs = async pgm => {
135
155
maxLength : parseInt ( values . lengthMax , 10 ) ,
136
156
order : parseInt ( values . order , 10 ) ,
137
157
transliterate : values . transliterate ,
138
- upperCase : values . upperCase
158
+ upperCase : values . upperCase ,
159
+ truncate : values [ 'truncate-set1' ]
139
160
}
140
161
141
162
if ( taskArgs . maxAttempts < 1 ||
142
- taskArgs . count < 1 ||
143
- taskArgs . minLength < 1 ||
144
- taskArgs . maxLength < taskArgs . minLength ||
145
- taskArgs . order < 1 ) {
146
- help ( pgm )
147
- process . exit ( 1 )
148
- }
163
+ taskArgs . count < 1 ||
164
+ taskArgs . minLength < 1 ||
165
+ taskArgs . maxLength < taskArgs . minLength ||
166
+ taskArgs . order < 1 ) {
167
+ help ( pkg . name )
168
+ process . exit ( 1 )
169
+ }
149
170
150
171
return taskArgs
151
172
}
152
173
153
174
const main = async ( ) => {
154
- let pgm = ''
155
-
156
- switch ( process . platform ) {
157
- case 'win32' :
158
- pgm = win32 . basename ( process . argv [ 1 ] )
159
- break
160
- default :
161
- pgm = basename ( process . argv [ 1 ] )
162
- break
175
+ let pkg = null
176
+ let taskArgs = null
177
+ let piscina = null
178
+ let wordList = null
179
+
180
+ try {
181
+ pkg = await getPkg ( )
182
+ taskArgs = await processArgs ( pkg )
183
+ piscina = new Piscina ( {
184
+ filename : path . resolve ( __dirname , '..' , 'index.js' ) ,
185
+ minThreads : Math . min ( taskArgs . count , Math . ceil ( os . availableParallelism / 2 ) ) ,
186
+ maxThreads : Math . min ( taskArgs . count , Math . ceil ( os . availableParallelism * 1.5 ) ) ,
187
+ idleTimeout : 100
188
+ } )
189
+ wordList = await Promise . all ( [ ...Array ( taskArgs . count ) ] . map ( async _ =>
190
+ await piscina . runTask ( taskArgs ) ) . filter ( Boolean ) )
191
+ } catch ( err ) {
192
+ console . error ( err . message )
193
+ process . exit ( 1 )
163
194
}
164
195
165
- const taskArgs = await processArgs ( pgm )
166
- const piscina = new Piscina ( {
167
- filename : join ( __dirname , '..' , 'index.js' ) ,
168
- minThreads : Math . min ( taskArgs . count , Math . ceil ( os . availableParallelism / 2 ) ) ,
169
- maxThreads : Math . min ( taskArgs . count , Math . ceil ( os . availableParallelism * 1.5 ) ) ,
170
- idleTimeout : 100
171
- } )
172
-
173
- const wordList = await Promise . all ( [ ...Array ( taskArgs . count ) ] . map ( async _ =>
174
- await piscina . runTask ( taskArgs ) ) . filter ( Boolean ) )
175
-
176
196
if ( wordList . length < taskArgs . count ) {
177
- console . log ( `${ pgm } : Unable to generate password with given constraints.` )
197
+ console . error ( `${ pkg . name } : Unable to generate password with given constraints.` )
178
198
process . exit ( 1 )
179
199
}
180
200
181
201
let password = wordList . join ( '-' )
182
202
183
203
if ( taskArgs . transliterate ) {
184
- const [ s , t ] = taskArgs . transliterate . split ( / [ , ; ] \s * | \s + / )
204
+ const [ s , t ] = taskArgs . transliterate . split ( / [ , ] \s * | \s + / )
185
205
186
- password = password . transliterate ( s , t )
206
+ password = password . transliterate ( s , t , { truncate : taskArgs . truncate } )
187
207
}
188
208
189
209
if ( taskArgs . upperCase ) {
190
- /*
191
- * Convert, e.g.:
192
- * ëstïnäë_mëlïbër's_crïmïnäblë_äräcödë => Ëstïnäë_Mëlïbër's_Crïmïnäblë_Äräcödë
193
- */
194
- password = password . replace ( / (?: ^ ) \p{ L} { 2 } | (?< = ( [ ^ \p{ L} \p{ N} ] | _ ) ) \p{ L} { 2 } / gv, c => `${ c [ 0 ] . toUpperCase ( ) } ${ c [ 1 ] } ` )
210
+ password = password . replace ( / ^ \p{ L} .| (?< = ( [ \p{ Pd} _ : ; ] ) ) \p{ L} ./ gv,
211
+ c => `${ c [ 0 ] . toUpperCase ( ) } ${ c [ 1 ] } ` )
195
212
}
196
213
197
214
console . log ( password )
0 commit comments