1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package main
  6
  7import (
  8	"fmt"
  9	"math/rand"
 10	"os"
 11	"strconv"
 12	"strings"
 13	"time"
 14)
 15
 16var (
 17	length         = 20
 18	version        = ""
 19	consonants     = "bcdfghjklmnpqrstvwxyz"
 20	vowels         = "aeiou"
 21	numbers        = "0123456789"
 22	newBase60Chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"
 23)
 24
 25func main() {
 26	if len(os.Args) > 1 && os.Args[1] == "-h" {
 27		printHelp()
 28		os.Exit(0)
 29	} else if len(os.Args) > 1 && os.Args[1] == "-t" {
 30		switch os.Args[2] {
 31		case "nb60":
 32			fmt.Println(generate(length))
 33		case "friendly":
 34			fmt.Println(generateFriendly())
 35		}
 36	} else if len(os.Args) > 3 {
 37		fmt.Print("Error: too many arguments...\n\n")
 38		printHelp()
 39		os.Exit(1)
 40	} else if len(os.Args) == 2 {
 41		// Convert the string argument to an int
 42		length, err := strconv.Atoi(os.Args[1])
 43		if err != nil {
 44			fmt.Println("Error: length must be an integer")
 45			os.Exit(1)
 46		}
 47		fmt.Println(generate(length))
 48		os.Exit(0)
 49	} else if len(os.Args) == 3 {
 50		length, err := strconv.Atoi(os.Args[1])
 51		if err != nil {
 52			fmt.Println("Error: length must be an integer")
 53			os.Exit(1)
 54		}
 55		count, err := strconv.Atoi(os.Args[2])
 56		if err != nil {
 57			fmt.Println("Error: count must be an integer")
 58			os.Exit(1)
 59		}
 60		for i := 0; i < count; i++ {
 61			fmt.Println(generate(length))
 62		}
 63		os.Exit(0)
 64	} else if len(os.Args) == 1 {
 65		fmt.Println(generate(length))
 66		os.Exit(0)
 67	}
 68}
 69
 70func generate(length int) string {
 71	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
 72
 73	var sb strings.Builder
 74	sb.Grow(length)
 75
 76	for i := 0; i < length; i++ {
 77		sb.WriteByte(newBase60Chars[rng.Intn(len(newBase60Chars))])
 78	}
 79
 80	return sb.String()
 81}
 82
 83func generateFriendly() string {
 84	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
 85
 86	sets := make([]string, 3)
 87	for i := 0; i < 3; i++ {
 88		sets[i] = genFriendlySet(rng)
 89	}
 90
 91	result := strings.Join(sets, "-")
 92
 93	letterPos := rng.Intn(len(result))
 94	// Generate a different position until a non-separator is found
 95	for result[letterPos] == '-' {
 96		letterPos = rng.Intn(len(result))
 97	}
 98	existingLetter := result[letterPos]
 99	var newLetter string
100	if strings.Contains(consonants, string(existingLetter)) {
101		newLetter = string(consonants[rng.Intn(len(consonants))])
102	} else if strings.Contains(vowels, string(existingLetter)) {
103		newLetter = string(vowels[rng.Intn(len(vowels))])
104	}
105	result = result[:letterPos] + strings.ToUpper(newLetter) + result[letterPos+1:]
106
107	// Generate a different position until a non-separator and non-uppercase-letter
108	// is found
109	numberPos := rng.Intn(len(result))
110	for result[numberPos] == '-' || result[numberPos] == result[letterPos] {
111		numberPos = rng.Intn(len(result))
112	}
113	result = result[:numberPos] + string(numbers[rng.Intn(len(numbers))]) + result[numberPos+1:]
114
115	return result
116}
117
118func genFriendlySet(rng *rand.Rand) string {
119	set := make([]byte, 6)
120	set[0] = consonants[rng.Intn(len(consonants))]
121	set[1] = vowels[rng.Intn(len(vowels))]
122	set[2] = consonants[rng.Intn(len(consonants))]
123	set[3] = consonants[rng.Intn(len(consonants))]
124	set[4] = vowels[rng.Intn(len(vowels))]
125	set[5] = consonants[rng.Intn(len(consonants))]
126	return string(set)
127}
128
129func printHelp() {
130	fmt.Println(`Usage: eow [-h|-t] <length> <count>
131
132-h prints this help message
133
134-t <type> prints a single 20-character NewBase60 (nb60) or friendly (friendly)
135  		  password.
136
137Arguments are positional, so length AND count may be omitted OR count may be
138omitted. If specifying count, length must also be specified. When ommitted,
139length defaults to 20 and count defaults to 1.`)
140}