{-# LANGUAGE Safe, CPP #-} module Main where import Options.Applicative -- http://hackage.haskell.org/package/optparse-applicative import Options.Applicative.Help.Pretty (string) -- Necessary to format help text -- https://github.com/pcapriotti/optparse-applicative/issues/90#issuecomment-49868254 import System.Console.ANSI import Data.Monoid ((<>)) -- REV: For GHC 8.0 through 8.2 import Control.Concurrent (threadDelay) import Control.Monad (replicateM_) import Crypto.Enigma import Crypto.Enigma.Display -- Definitions ============================================================== -- Functions and constants --------------------------------------------------- cliName_ = "Enigma Machine (Haskell crypto-enigma) CLI" #if __GLASGOW_HASKELL__ < 800 cliError e = error (e ++ " (see help)") #else cliError e = errorWithoutStackTrace (e ++ " (see help)") #endif stepInterval_ = 125000 -- Command and option definitions -------------------------------------------- data Subcommand = Encode { config :: String, message :: String } | Show { config :: String, letterO :: Maybe String, formatO :: Maybe String, highlightO :: Maybe String, encodingO :: Maybe Bool } | Run { config :: String, messageO :: Maybe String, formatO :: Maybe String, highlightO :: Maybe String, encodingO :: Maybe Bool, showstepsO :: Maybe Bool, numstepsO :: Int, speedO :: Int, overwriteO :: Maybe Bool, noinitialO :: Maybe Bool } deriving Show data Options = Options { subcommand :: Subcommand } deriving Show commandO = Options <$> subcommandO subcommandO :: Parser Subcommand subcommandO = subparser ( command "encode" (info (Encode <$> configArg "encode" <*> messageArg <**> helper) (helpText encodeCmdDesc "ENCODE" encodeCmdArgsFoot encodeCmdExamples)) <> command "show" (info (Show <$> configArg "show" <*> letterOpt <*> formatOpt <*> highlightOpt <*> encodingOpt <**> helper) (helpText showCmdDesc "SHOW" showCmdArgsFoot showCmdExamples)) <> command "run" (info (Run <$> configArg "run" <*> messageOpt <*> formatOpt <*> highlightOpt <*> encodingOpt <*> showstepOpt <*> stepsOpt <*> speedOpt <*> overwriteOpt <*> noinitialOpt <**> helper) (helpText runCmdDesc "RUN" runCmdArgsFoot runCmdExamples)) ) where configArg cmd = strArgument $ metavar "CONFIG" <> help (configArgHelp cmd) messageArg = strArgument $ metavar "MESSAGE" <> help messageArgHelp messageOpt = optional $ strOption ( long "message" <> short 'm' <> metavar "MESSAGE" <> value " " <> help messageOptHelp) letterOpt = optional $ strOption ( long "letter" <> short 'l' <> metavar "LETTER" <> value " " <> help letterOptHelp) formatOpt = optional $ strOption ( long "format" <> short 'f' <> metavar "FORMAT" <> value "single" <> help formatOptHelp) highlightOpt = optional $ strOption ( long "highlight" <> short 'H' <> metavar "HH|SPEC" <> value "bars" <> help highlightOptHelp) encodingOpt = optional $ switch ( long "showencoding" <> short 'e' <> help showencodingOptHelp) showstepOpt = optional $ switch ( long "showstep" <> short 't' <> help "Show step numbers") stepsOpt :: Parser Int stepsOpt = option auto (long "steps" <> short 's' <> metavar "STEPS" <> value (-1) <> help stepsOptHelp) speedOpt :: Parser Int speedOpt = option auto (long "speed" <> short 'S' <> metavar "SPEED" <> value 0 <> help speedOptHelp) overwriteOpt = optional $ switch ( long "overwrite" <> short 'o' <> help overwriteOptHelp) noinitialOpt = optional $ switch ( long "noinitial" <> short 'n' <> help noinitialOptHelp) helpText desc cmd argsFoot examplesFoot = (progDesc desc <> header (cliName_ ++ ": "++ cmd ++" command") <> footerDoc (Just $ string $ unlines ["Argument notes:\n", argsFoot, "Examples:\n", examplesFoot])) -- Command line script ======================================================= main :: IO () main = do opts <- execParser optsParser case subcommand opts of Encode config message -> putStr $ displayEnigmaEncoding (configEnigmaFromString config) message Show config (Just (letter:_)) (Just format) (Just highlight) (Just showenc) -> putStrLn $ displayEnigmaConfig (configEnigmaFromString config) letter (displayOpts{format=format,showencoding=showenc,markerspec=highlight}) Run config (Just message) (Just format) (Just highlight) (Just showenc) (Just showstps) stps speed (Just overwrite) (Just noinitial) -> mapM_ (printConfig (max speed (if overwrite then 1 else 0)) overwrite) ((if noinitial then tail else id) $ listEnigmaOperation (configEnigmaFromString config) message (displayOpts{format=format,showencoding=showenc,markerspec=highlight, showsteps=showstps,steps=stps})) cmd -> putStrLn $ "Unmatched command: " ++ (show cmd) where optsParser :: ParserInfo Options optsParser = info (helper <*> commandO) (fullDesc <> progDesc topDesc <> header cliName_ <> footerDoc (Just $ string topFoot)) -- Like 'configEnigma' but without stack trace and with check for 4 words in a single string configEnigmaFromString :: String -> EnigmaConfig configEnigmaFromString i = if ((length $ words i) /= 4) then cliError ("Enigma machine configuration has the format 'rotors windows plugboard rings'") else either (cliError.show) id (configEnigma' c w s r) where [c, w, s, r] = words i printConfig s True c = printConfig s False c >> replicateM_ ((length $ lines c) + (if (length $ lines c) > 1 then 1 else 0)) (cursorUpLine 1 >> clearLine) printConfig s False c = putStrLn c >> (threadDelay (s * stepInterval_)) -- Help text ================================================================= topDesc = "A simple Enigma machine simulator with rich display of machine configurations." encodeCmdDesc = "Show the encoding of a message." showCmdDesc = "Show an Enigma machine configuration in the specified format, " ++ "optionally indicating the encoding of a specified character." runCmdDesc = "Show the operation of the Enigma machine as a series of configurations, " ++ "as it encodes a message and/or for a specified number of steps. " configArgHelp cmd = case cmd of "encode" -> unlines ["The machine configuration at the start of encoding (see below)"] "show" -> unlines ["The machine configuration to show (see below)"] "run" -> unlines ["The machine setup at the start of operation (see below)"] -- Should not happen: optparse-applicative should have caught this _ -> error "ERROR: Unrecognized command: '" ++ cmd ++ "''!" messageArgHelp = messageOptHelp messageOptHelp = unlines [ "A message to encode; characters that are not letters" , "will be replaced with standard Naval substitutions or", "be removed"] letterOptHelp = unlines [ "An optional input letter to highlight as it is" , "processed by the configuration; defaults to nothing"] formatOptHelp = unlines [ "The format used to display machine configuration(s)" , "(see below)"] highlightOptHelp = unlines [ "Either a pair or characters to use to highlight encoded" , "characters in a machine configuration's encoding or a" , "specification of a color or style (see below)"] showencodingOptHelp = unlines [ "Show the encoding if not normally shown for the" , "specified FORMAT"] stepsOptHelp = unlines [ "A number of steps to run; if omitted when a message is" , "provided, will default to the length of the message;" , "otherwise defaults to 1"] speedOptHelp = unlines [ "Pause between display of each step for SPEED*" ++ show ((fromIntegral stepInterval_) / 1000000) ++ "s;", "1 is the minimum and default speed with --overwrite; ", "defaults to 0 otherwise"] overwriteOptHelp = unlines [ "Overwrite each step after a pause (may result in" , "garbled output on some systems)"] noinitialOptHelp = unlines [ "Don't show the initial starting step"] encodeCmdArgsFoot = init $ unlines [configArgFoot, omitArgFoot "encode"] showCmdArgsFoot = init $ unlines [configArgFoot, formatArgFoot "show", highlightArgFoot, omitArgFoot "show"] runCmdArgsFoot = init $ unlines [configArgFoot, formatArgFoot "run", highlightArgFoot, omitArgFoot "run"] configArgFoot = unlines [ "CONFIG specifies an Enigma machine configuration as a string based on common", "historical conventions and consists of four elements, separated by spaces:", " + names for components, in physical order (starting with the reflector, on the", " left, and ending with the 'first' rotor, on the right), separated by '-'s;", " + letters visible at the machine windows (in physical order);", " + a plugboard specification, consisting of exchanged (i.e. wired-together)", " letter paris, separated by '.'s; and", " + the locations of ring letter A on the rotor for each rotor", " (in physical order)"] formatArgFoot cmd = unlines $ [ "FORMAT will determine how a configuration is represented; possible values", "include:", " + 'single' (the default) which will show a single line representing the", " mapping (a string in which the letter at each position indicates the letter", " encoded to by letter at that position in the alphabet) preformed by the", " machine as a whole, followed by window letters (as 'windows') and", " positions, and indicating a letter and its encoding, if provided;", " + 'internal', which will show a detailed schematic of each processing stage", " (proceeding from top to bottom), in which", " - each line indicates the mapping (see 'single') preformed by the", " component at that stage;", " - each line begins with an indication of the stage (rotor number, \"P\" for", " plugboard, or \"R\" for reflector), and ends with the specification of the", " component at that stage;", " - rotors also indicate their window letter, and position;", " - the letter being encoded it is indicated as input and its encoding at", " each stage is marked;", " the schematic is followed by the mapping for the machine as a whole (as", " 'single'), and preceded by a (trivial, no-op) keyboard 'mapping'", " for reference;", " + 'windows', which shows just the letters visible at the windows;", " and", " + 'config', which simply shows the specification of the", " configuration (in the same format as CONFIG).", "For formats that indicate a letter and its encoding, these will correspond to"] ++ letterSoruce ++ ["The program is forgiving about forgotten format values and will accept a", "range of reasonable substitutes (e.g., 'detailed' or 'schematic' for", "'internal')."] where letterSoruce | cmd == "run" = ["the the letter at that step of the entry of the (symbol substituted) MESSAGE ", "provided as an argument."] | otherwise = ["the LETTER provided as an argument."] highlightArgFoot = unlines [ "HH can be used to determine how any encoded-to characters in mappings", "(see 'single' in the note on FORMAT) are highlighted. By default", "this highlighting is done with combining Unicode characters, which may not", "work on all systems, and as an alternative, any two characters provided as", "HH will be used to 'bracket' the highlighted character. To avoid errors,", "these characters should be enclosed in quotes. Additionally certain", "predefined SPEC values, such as 'red' or 'highlight' are also supported."] omitArgFoot cmd = unlines [ "Note that providing no value, a value of '', or just spaces or invalid", "characters for " ++ omittedArg ++ " is the same as omitting it."] where omittedArg | cmd == "show" = "LETTER" | otherwise = "MESSAGE" topFoot = "Examples:\n" ++ topExamples ++ "\nThis command line interface is part of the Haskell crypto-enigma package." topExamples = unlines [ " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"TESTINGXTESTINGUD\"", " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"TESTINGXTESTINGUD\" -f", " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"TESTING! testing?\" -f", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X'", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X' -H '()'", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X' -H '()' -f internal", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -s 10 -t", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -H '()'", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -H '()' -f internal", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -H '()' -f internal -o -SS", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -f config -e", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -f internal -e", " $ enigma run \"c-β-VIII-VII-VI QMLI UX.MO.AY 01.13.04.11\" -s 500 -t -f internal -o", "", "More information about each of these examples is available in the help for the respective", "commands."] encodeCmdExamples = init $ unlines [ " Encode a message:", " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"TESTINGXTESTINGUD\"", " OZQKPFLPYZRPYTFVU", " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"OZQKPFLPYZRPYTFVU\"", " TESTINGXTESTINGUD", "", " Encode a message and break the output into blocks of 4:", " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"TESTINGXTESTINGUD\" -f", " OZQK PFLP YZRP YTFV U", "", " Standard Naval subistitutions for non-letter characters are performed", " before encoding:", " $ enigma encode \"B-I-III-I EMO UX.MO.AY 13.04.11\" \"TESTING! testing?\" -f", " OZQK PFLP YZRP YTFV U"] showCmdExamples = init $ unlines [ " Show an Enigma machine configuration as its mapping (see 'single'", " in the note on FORMAT), followed by the window letters and ring settings,", " and indicate how a letter is encoded; here X is encoded to T (A would be", " encoded to C, B to N ... Y to W, Z to O):", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X'", " X > CNAUJVQSLEMIKBZRGPHXDFYT̲̅WO EMO 19 10 05", "", " Use an alternate method for highlighting the encoded-to letter:", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X' -H '()'", " X > CNAUJVQSLEMIKBZRGPHXDFY(T)WO EMO 19 10 05", "", " Show a detailed stage-by-stage schematic (see 'internal' in the note", " on FORMAT) of the mappings preformed by a configuration:", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X' -H '()' -f internal", " X > ABCDEFGHIJKLMNOPQRSTUVW(X)YZ", " P YBCDEFGHIJKLONMPQRSTXVW(U)AZ UX.MO.AY", " 1 HCZMRVJPKSUDTQOLWEXN(Y)FAGIB O 05 I", " 2 KOMQEPVZNXRBDLJHFSUWYACT(G)I M 10 III", " 3 AXIQJZ(K)RMSUNTOLYDHVBWEGPFC E 19 I", " R YRUHQSLDPX(N)GOKMIEBFZCWVJAT B", " 3 ATZQVYWRCEGOI(L)NXDHJMKSUBPF I", " 2 VLWMEQYPZOA(N)CIBFDKRXSGTJUH III", " 1 WZBLRVXAYGIPD(T)OHNEJMKFQSUC I", " P YBCDEFGHIJKLONMPQRS(T)XVWUAZ UX.MO.AY", " T < CNAUJVQSLEMIKBZRGPHXDFY(T)WO", "", " Just show the configuration in conventional format (as used in CONFIG):", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X' -f config", " B-I-III-I EMO UX.MO.AY 13.04.11", "", " As above, but show the encoding too (not shown my default for 'config'):", " $ enigma show \"B-I-III-I EMO UX.MO.AY 13.04.11\" -l 'X' -f config -e", " B-I-III-I EMO UX.MO.AY 13.04.11 X > T"] runCmdExamples = init $ unlines [ "(For details on differences among formats used for displaying each step, see the", "examples in the examples for the 'show' command.)", "", " Show the operation of a machine for 10 steps, indicating step numbers (see", " 'single' in the note on FORMAT):", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -s 10 -t", " 0000 CNAUJVQSLEMIKBZRGPHXDFYTWO EMO 19 10 05", " 0001 UNXKGVERLYDIQBTWMHZOAFPCJS EMP 19 10 06", " 0002 QTYJZXUPKDIMLSWHAVNBGROFCE EMQ 19 10 07", " 0003 DMXAPTRWKYINBLUESGQFOZHCJV ENR 19 11 08", " 0004 IUSMHRPEAQTVDYWGJFCKBLOZNX ENS 19 11 09", " 0005 WMVXQRLSPYOGBTKIEFHNZCADJU ENT 19 11 10", " 0006 WKIQXNRSCVBOYFLUDGHZPJAEMT ENU 19 11 11", " 0007 RVPTWSLKYXHGNMQCOAFDZBEJIU ENV 19 11 12", " 0008 IYTKRVSMALDJHZWXUEGCQFOPBN ENW 19 11 13", " 0009 PSWGMODULZVIERFAXNBYHKCQTJ ENX 19 11 14", " 0010 IVOWZKHGARFSPUCMXJLYNBDQTE ENY 19 11 15", "", " Show the operation of a machine as it encodes a message, with step numbers:", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -H '()'", " 0000 CNAUJVQSLEMIKBZRGPHXDFYTWO EMO 19 10 05", " 0001 T > UNXKGVERLYDIQBTWMHZ(O)AFPCJS EMP 19 10 06", " 0002 E > QTYJ(Z)XUPKDIMLSWHAVNBGROFCE EMQ 19 10 07", " 0003 S > DMXAPTRWKYINBLUESG(Q)FOZHCJV ENR 19 11 08", " 0004 T > IUSMHRPEAQTVDYWGJFC(K)BLOZNX ENS 19 11 09", " 0005 I > WMVXQRLS(P)YOGBTKIEFHNZCADJU ENT 19 11 10", " 0006 N > WKIQXNRSCVBOY(F)LUDGHZPJAEMT ENU 19 11 11", " 0007 G > RVPTWS(L)KYXHGNMQCOAFDZBEJIU ENV 19 11 12", "", " Show the operation of a machine as it encodes a message in more detail (see", " 'internal' in the note on FORMAT), with step numbers (only some", " steps shown here):", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -H '()' -f internal", " 0000", " ...", " 0001", " T > ABCDEFGHIJKLMNOPQRS(T)UVWXYZ", " P YBCDEFGHIJKLONMPQRS(T)XVWUAZ UX.MO.AY", " 1 BYLQUIOJRTCSPNKVDWM(X)EZFHAG P 06 I", " 2 KOMQEPVZNXRBDLJHFSUWYAC(T)GI M 10 III", " 3 AXIQJZKRMSUNTOLYDHV(B)WEGPFC E 19 I", " R Y(R)UHQSLDPXNGOKMIEBFZCWVJAT B", " 3 ATZQVYWRCEGOILNXD(H)JMKSUBPF I", " 2 VLWMEQY(P)ZOANCIBFDKRXSGTJUH III", " 1 YAKQUWZXFHOCSNG(M)DILJEPRTBV I", " P YBCDEFGHIJKL(O)NMPQRSTXVWUAZ UX.MO.AY", " O < UNXKGVERLYDIQBTWMHZ(O)AFPCJS", " ...", " 0007", " G > ABCDEF(G)HIJKLMNOPQRSTUVWXYZ", " P YBCDEF(G)HIJKLONMPQRSTXVWUAZ UX.MO.AY", " 1 IDLNWM(J)HEPXQGRYTZBUAVSFKOC V 12 I", " 2 NLPDOUYMW(Q)ACKIGERTVXZBSFHJ N 11 III", " 3 AXIQJZKRMSUNTOLY(D)HVBWEGPFC E 19 I", " R YRU(H)QSLDPXNGOKMIEBFZCWVJAT B", " 3 ATZQVYW(R)CEGOILNXDHJMKSUBPF I", " 2 KVLDPXOYNZMBHAECJ(Q)WRFSITGU III", " 1 TRZBIWMHAGXCFDYJ(L)NVPSUEKOQ I", " P YBCDEFGHIJK(L)ONMPQRSTXVWUAZ UX.MO.AY", " L < RVPTWS(L)KYXHGNMQCOAFDZBEJIU", "", " Show the steps as above, but (slowly) in place (if the platform supports it)", " rather than on a new line for each; only the last step is visible on", " completion (as shown here):", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -H '()' -f internal -o -SS", " 0007", " G > ABCDEF(G)HIJKLMNOPQRSTUVWXYZ", " P YBCDEF(G)HIJKLONMPQRSTXVWUAZ UX.MO.AY", " 1 IDLNWM(J)HEPXQGRYTZBUAVSFKOC V 12 I", " 2 NLPDOUYMW(Q)ACKIGERTVXZBSFHJ N 11 III", " 3 AXIQJZKRMSUNTOLY(D)HVBWEGPFC E 19 I", " R YRU(H)QSLDPXNGOKMIEBFZCWVJAT B", " 3 ATZQVYW(R)CEGOILNXDHJMKSUBPF I", " 2 KVLDPXOYNZMBHAECJ(Q)WRFSITGU III", " 1 TRZBIWMHAGXCFDYJ(L)NVPSUEKOQ I", " P YBCDEFGHIJK(L)ONMPQRSTXVWUAZ UX.MO.AY", " L < RVPTWS(L)KYXHGNMQCOAFDZBEJIU", "", " Stepping a configuration only changes the window letters:", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -f config -e", " 0000 B-I-III-I EMO UX.MO.AY 13.04.11", " 0001 B-I-III-I EMP UX.MO.AY 13.04.11 T > O", " 0002 B-I-III-I EMQ UX.MO.AY 13.04.11 E > Z", " 0003 B-I-III-I ENR UX.MO.AY 13.04.11 S > Q", " 0004 B-I-III-I ENS UX.MO.AY 13.04.11 T > K", " 0005 B-I-III-I ENT UX.MO.AY 13.04.11 I > P", " 0006 B-I-III-I ENU UX.MO.AY 13.04.11 N > F", " 0007 B-I-III-I ENV UX.MO.AY 13.04.11 G > L", " $ enigma run \"B-I-III-I EMO UX.MO.AY 13.04.11\" -m \"TESTING\" -t -f windows -e", " 0000 EMO", " 0001 EMP T > O", " 0002 EMQ E > Z", " 0003 ENR S > Q", " 0004 ENS T > K", " 0005 ENT I > P", " 0006 ENU N > F", " 0007 ENV G > L", "", " Watch the machine run for 500 steps:", " $ enigma run \"c-β-VIII-VII-VI QMLI UX.MO.AY 01.13.04.11\" -s 500 -t -f internal -o"]