Skip to content

Commit dd64fca

Browse files
committed
Update transliterate to match tr.
Add option to truncate transliteration string. Simplify regex for performing uppercase. Improve error handling. Update dependencies. Add standard to devDependencies. Fix tests.
1 parent 149de7b commit dd64fca

File tree

4 files changed

+5737
-172
lines changed

4 files changed

+5737
-172
lines changed

README.md

+39-29
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,46 @@ _/usr/share/dict/web2_ is used if available, otherwise a
2929
[list from Project Gutenberg](https://www.gutenberg.org/files/3201/files/SINGLE.TXT)
3030
is downloaded.
3131

32-
Recent changes:
32+
### Changes in v2.2.0
33+
- Update transliterate option to match tr command when length of
34+
FROM and TO strings differ.
35+
- Add option, `--truncate-set1`, to preserve previous transliterate
36+
behavior.
37+
- Improve regex implementing capitalization (option `--upperCase`).
38+
- Improve error handling.
39+
- Update dependencies.
40+
- Add standard to devDependencies.
41+
### Changes in V2.1.5
3342
- Fix URLs of locale-based word lists.
3443
- Node v20 required for RegExp.prototype.unicodeSets.
35-
- Don't captialize letters with number prefix.
3644
- Introduce locale support.
37-
- Add command-line option to capitalize password components.
3845

3946
## Synopsis
4047

4148
```
4249
Usage: markov-pwgen OPTIONS
4350
OPTIONS (defaults are random within the given range):
44-
--attemptsMax=N, -aN
51+
--attemptsMax, -a N
4552
Fail after N attempts to generate chain (default: 100)
46-
--count=N, -cN
53+
--count, -c N
4754
Generate N hyphen-delimited passwords (default: [3, 4])
4855
--dictionary, -d
4956
Allow dictionary-word passwords (default: false)
5057
--help, -h
5158
Print this help, then exit.
52-
--lengthMin=N, -nN
59+
--lengthMin, -n N
5360
Minimum password length N (default: [4, 6])
54-
--lengthMax=N, -mN
61+
--lengthMax, -m N
5562
Maximum password length N (default: [7, 13])
56-
--order=N, -oN
63+
--order, -o N
5764
Markov order N (default: [3, 4])
58-
--transliterate=S,T, -tS,T
59-
Replace in password characters of S with corresponding
60-
characters of T.
65+
--transliterate, -t FROM,TO
66+
Replace in password characters of FROM with corresponding
67+
characters of TO.
68+
--truncate-set1, -s
69+
By default, transliteration string TO is extended to
70+
length of FROM by repeating its last character as necessary.
71+
This option first truncates FROM to length of TO.
6172
--upperCase, -u
6273
Capitalize password components.
6374
--version, -v
@@ -95,7 +106,7 @@ Otherwise, run:
95106

96107
```bash
97108
npm pack .
98-
npm install -g ./markov-pwgen-2.1.5.tgz
109+
npm install -g ./markov-pwgen-2.2.0.tgz
99110
```
100111

101112
## MS Windows
@@ -182,32 +193,31 @@ Myrlae-firism-kniong-hoodin
182193
spolid-coeod-prodal
183194
```
184195

185-
The command-line option `--transliterate=S,T` replaces in the
186-
password letters from the string S with the corresponding
187-
letters from string T.
188-
The command-line option `--upperCase' capitalizes each "word".
189-
Together, these options can be used to add more characters to the
190-
output. For instance, the command:
196+
Command-line option `--transliterate=FROM,TO` replaces in the output
197+
letters of string FROM with corresponding letters of string TO.
198+
Command-line option `--upperCase` capitalizes each "word" component.
199+
Together, these options add more variation in the output. For
200+
instance, the command:
191201

192202
```bash
193203
for i in {1..10}; do
194-
markov-pwgen -u -t'gt,97'
204+
markov-pwgen -u -t'its,!7$'
195205
done
196206
```
197207

198208
might produce:
199209

200210
```
201-
Knuclesi7e-Dumbuli7y-Ini7rowers-Puzz9lo7hic
202-
Mendisin9-Monoiden-Or9ermouses-Hemisaferome
203-
7rulen7ron7-Proveried-Nonli9h7ies
204-
Evaleyes-Ji7ier-Es7icen7-Bryocy7es
205-
Popias7raph-Helesses-9iaryonius
206-
Sperman-Unexhausea7-Encernized
207-
Moze77er-En7a7ic-Coi9num-An7arily
208-
Foreboard-Nymphala-Fixured-7olery
209-
Depos7-Sociferous-Papac7ic
210-
Ncas7ic-Brachbis7-9ruelldom
211+
Furan7$-Beneface-Unance$
212+
Vaccou$-Alvayne$$-$ub$eral-Boozener
213+
Re7ragoe$-Hel!g!on-Dockey!ng
214+
Ammone$-Malac7er-Encoax!n-Verneb
215+
Hered$-Fluced-Coequee-Vo7e!ng
216+
Azoc7a7e-Baboun7er-Federanx
217+
M!lder-Kal!dae-Arch!n!
218+
Febr!d!ne-Econqu!r!ng-Paper$ed-$udor!f!d
219+
Unwrongyl-Cyclo7hurl-Fam!l!7e
220+
Unf!l!zed-Bu$7he$-Predoneure
211221
```
212222

213223
# Bugs

bin/markov-pwgen.js

+87-70
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,82 @@
22
/*
33
* @(#) markov-pwgen
44
*
5-
* Copyright © 2023, Revolution Robotics, Inc.
5+
* Copyright © 2023,2024, Revolution Robotics, Inc.
66
*
77
*/
88
import { readFile } from 'node:fs/promises'
99
import os from 'node:os'
10-
import { basename, dirname, join, win32 } from 'node:path'
10+
import path from 'node:path'
1111
import { fileURLToPath } from 'node:url'
1212
import { parseArgs } from 'node:util'
1313
import { Piscina } from 'piscina'
1414
import random64 from '../lib/random64.js'
1515

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+
}
1726

1827
const help = (pgm) => {
1928
console.log(`Usage: ${pgm} OPTIONS`)
2029
console.log(`OPTIONS (defaults are random within the given range):
21-
--attemptsMax=N, -aN
30+
--attemptsMax, -a N
2231
Fail after N attempts to generate chain (default: 100)
23-
--count=N, -cN
32+
--count, -c N
2433
Generate N hyphen-delimited passwords (default: [3, 4])
2534
--dictionary, -d
2635
Allow dictionary-word passwords (default: false)
2736
--help, -h
2837
Print this help, then exit.
29-
--lengthMin=N, -nN
38+
--lengthMin, -n N
3039
Minimum password length N (default: [4, 6])
31-
--lengthMax=N, -mN
40+
--lengthMax, -m N
3241
Maximum password length N (default: [7, 13])
33-
--order=N, -oN
42+
--order, -o N
3443
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.
3851
--upperCase, -u
3952
Capitalize password components.
4053
--version, -v
4154
Print version, then exit.
4255
NB: Lower Markov order yields more random (i.e., less recognizable) words.`)
4356
}
4457

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'.
4764
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('')
5877
}
5978
}
6079

61-
const processArgs = async pgm => {
80+
const processArgs = async pkg => {
6281
const options = {
6382
attemptsMax: {
6483
type: 'string',
@@ -95,6 +114,11 @@ const processArgs = async pgm => {
95114
short: 'o',
96115
default: `${Number(random64(3, 4))}`
97116
},
117+
'truncate-set1': {
118+
type: 'boolean',
119+
short: 's',
120+
default: false
121+
},
98122
transliterate: {
99123
type: 'string',
100124
short: 't'
@@ -110,20 +134,16 @@ const processArgs = async pgm => {
110134
}
111135
}
112136

113-
const { values } = parseArgs({ options })
137+
const { values } = parseArgs({
138+
options,
139+
allowPositionals: false
140+
})
114141

115142
if (values.help) {
116-
help(pgm)
143+
help(pkg.name)
117144
process.exit(0)
118145
} 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}`)
127147
process.exit(0)
128148
}
129149

@@ -135,63 +155,60 @@ const processArgs = async pgm => {
135155
maxLength: parseInt(values.lengthMax, 10),
136156
order: parseInt(values.order, 10),
137157
transliterate: values.transliterate,
138-
upperCase: values.upperCase
158+
upperCase: values.upperCase,
159+
truncate: values['truncate-set1']
139160
}
140161

141162
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+
}
149170

150171
return taskArgs
151172
}
152173

153174
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)
163194
}
164195

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-
176196
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.`)
178198
process.exit(1)
179199
}
180200

181201
let password = wordList.join('-')
182202

183203
if (taskArgs.transliterate) {
184-
const [s, t] = taskArgs.transliterate.split(/[,;]\s*|\s+/)
204+
const [s, t] = taskArgs.transliterate.split(/[,]\s*|\s+/)
185205

186-
password = password.transliterate(s, t)
206+
password = password.transliterate(s, t, { truncate: taskArgs.truncate })
187207
}
188208

189209
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]}`)
195212
}
196213

197214
console.log(password)

0 commit comments

Comments
 (0)