summaryrefslogtreecommitdiff
path: root/tex/generic/enigma/enigma.lua
diff options
context:
space:
mode:
Diffstat (limited to 'tex/generic/enigma/enigma.lua')
-rw-r--r--tex/generic/enigma/enigma.lua1662
1 files changed, 1662 insertions, 0 deletions
diff --git a/tex/generic/enigma/enigma.lua b/tex/generic/enigma/enigma.lua
new file mode 100644
index 0000000..9da4288
--- /dev/null
+++ b/tex/generic/enigma/enigma.lua
@@ -0,0 +1,1662 @@
+#!/usr/bin/env texlua
+-----------------------------------------------------------------------
+-- FILE: enigma.lua
+-- USAGE: Call via interface from within a TeX session.
+-- DESCRIPTION: Enigma logic.
+-- REQUIREMENTS: LuaTeX capable format (Luaplain, ConTeXt).
+-- AUTHOR: Philipp Gesang (Phg), <phg42 dot 2a at gmail dot com>
+-- VERSION: release
+-- CREATED: 2013-03-28 02:12:03+0100
+-----------------------------------------------------------------------
+--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Format Dependent Code]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\startparagraph
+Exported functionality will be collected in the table
+\identifier{enigma}.
+\stopparagraph
+--ichd]]--
+
+local enigma = { machines = { }, callbacks = { } }
+local format_is_context = false
+
+--[[ichd--
+\startparagraph
+Afaict, \LATEX\ for \LUATEX\ still lacks a globally accepted
+namespacing convention. This is more than bad, but we’ll have to cope
+with that. For this reason we brazenly introduce
+\identifier{packagedata} (in analogy to \CONTEXT’s
+\identifier{thirddata}) table as a package namespace proposal. If this
+module is called from a \LATEX\ or plain session, the table
+\identifier{packagedata} will already have been created so we will
+identify the format according to its presence or absence, respectively.
+\stopparagraph
+--ichd]]--
+
+if packagedata then -- latex or plain
+ packagedata.enigma = enigma
+elseif thirddata then -- context
+ format_is_context = true
+ thirddata.enigma = enigma
+else -- external call, mtx-script or whatever
+ _ENV.enigma = enigma
+end
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Prerequisites]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startparagraph
+First of all, we generate local copies of all those library functions
+that are expected to be referenced frequently.
+The format-independent stuff comes first; it consists of functions from
+the
+\identifier{io},
+\identifier{lpeg},
+\identifier{math},
+\identifier{string},
+\identifier{table}, and
+\identifier{unicode}
+libraries.
+\stopparagraph
+--ichd]]--
+
+local get_debug_info = debug.getinfo
+local ioread = io.read
+local iowrite = io.write
+local mathfloor = math.floor
+local mathrandom = math.random
+local next = next
+local nodecopy = node and node.copy
+local nodeid = node and node.id
+local nodeinsert_before = node and node.insert_before
+local nodeinsert_after = node and node.insert_after
+local nodelength = node and node.length
+local nodenew = node and node.new
+local noderemove = node and node.remove
+local nodeslide = node and node.slide
+local nodetraverse = node and node.traverse
+local nodetraverse_id = node and node.traverse_id
+local nodesinstallattributehandler
+local nodestasksappendaction
+local nodestasksdisableaction
+if format_is_context then
+ nodesinstallattributehandler = nodes.installattributehandler
+ nodestasksappendaction = nodes.tasks.appendaction
+ nodestasksdisableaction = nodes.tasks.disableaction
+end
+local stringfind = string.find
+local stringformat = string.format
+local stringlower = string.lower
+local stringsub = string.sub
+local stringupper = string.upper
+local tableconcat = table.concat
+local tonumber = tonumber
+local type = type
+local utf8byte = unicode.utf8.byte
+local utf8char = unicode.utf8.char
+local utf8len = unicode.utf8.len
+local utf8lower = unicode.utf8.lower
+local utf8sub = unicode.utf8.sub
+local utfcharacters = string.utfcharacters
+
+--- debugging tool (careful, this *will* break context!)
+--dofile(kpse.find_file("lualibs-table.lua")) -- archaic version :(
+--table.print = function (...) print(table.serialize(...)) end
+
+local tablecopy
+if format_is_context then
+ tablecopy = table.copy
+else -- could use lualibs instead but not worth the overhead
+ tablecopy = function (t) -- ignores tables as keys
+ local result = { }
+ for k, v in next, t do
+ if type(v) == table then
+ result[k] = tablecopy(v)
+ else
+ result[k] = v
+ end
+ end
+ return result
+ end
+end
+
+local GLYPH_NODE = node and nodeid"glyph"
+local GLUE_NODE = node and nodeid"glue"
+local GLUE_SPEC_NODE = node and nodeid"glue_spec"
+local KERN_NODE = node and nodeid"kern"
+local DISC_NODE = node and nodeid"disc"
+local HLIST_NODE = node and nodeid"hlist"
+local VLIST_NODE = node and nodeid"vlist"
+
+local IGNORE_NODES = node and {
+--[GLUE_NODE] = true,
+ [KERN_NODE] = true,
+--[DISC_NODE] = true,
+} or { }
+
+--[[ichd--
+\startparagraph
+The initialization of the module relies heavily on parsers generated by
+\type{LPEG}.
+\stopparagraph
+--ichd]]--
+
+local lpeg = require "lpeg"
+
+local C, Cb, Cc, Cf, Cg,
+ Cmt, Cp, Cs, Ct
+ = lpeg.C, lpeg.Cb, lpeg.Cc, lpeg.Cf, lpeg.Cg,
+ lpeg.Cmt, lpeg.Cp, lpeg.Cs, lpeg.Ct
+
+local P, R, S, V, lpegmatch
+ = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.match
+
+--local B = lpeg.version() == "0.10" and lpeg.B or nil
+
+--[[ichd--
+\startparagraph
+By default the output to \type{stdout} will be zero. The verbosity
+level can be adjusted in order to alleviate debugging.
+\stopparagraph
+--ichd]]--
+--local verbose_level = 42
+local verbose_level = 0
+
+--[[ichd--
+\startparagraph
+Historically, Enigma-encoded messages were restricted to a size of 250
+characters. With sufficient verbosity we will indicate whether this
+limit has been exceeded during the \TEX\ run.
+\stopparagraph
+--ichd]]--
+local max_msg_length = 250
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Globals]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startparagraph
+The following mappings are used all over the place as we convert back
+and forth between the characters (unicode) and their numerical
+representation.
+\stopparagraph
+--ichd]]--
+
+local value_to_letter -- { [int] -> chr }
+local letter_to_value -- { [chr] -> int }
+local alpha_sorted -- string, length 26
+local raw_rotor_wiring -- { string0, .. string5, }
+local notches -- { [int] -> int } // rotor num -> notch pos
+local reflector_wiring -- { { [int] -> int }, ... } // symmetrical
+do
+ value_to_letter = {
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+ }
+
+ letter_to_value = {
+ a = 01, b = 02, c = 03, d = 04, e = 05, f = 06, g = 07, h = 08,
+ i = 09, j = 10, k = 11, l = 12, m = 13, n = 14, o = 15, p = 16,
+ q = 17, r = 18, s = 19, t = 20, u = 21, v = 22, w = 23, x = 24,
+ y = 25, z = 26,
+ }
+--[[ichd--
+\startparagraph
+The five rotors to simulate.\reference[listing:rotor_wiring]{}
+Their wirings are created from strings at runtime, see below the
+function \luafunction{get_rotors}.
+\stopparagraph
+--ichd]]--
+
+ --[[
+ Nice: http://www.ellsbury.com/ultraenigmawirings.htm
+ ]]--
+ alpha_sorted = "abcdefghijklmnopqrstuvwxyz"
+ raw_rotor_wiring = {
+ [0] = alpha_sorted,
+ "ekmflgdqvzntowyhxuspaibrcj",
+ "ajdksiruxblhwtmcqgznpyfvoe",
+ "bdfhjlcprtxvznyeiwgakmusqo",
+ "esovpzjayquirhxlnftgkdcmwb",
+ "vzbrgityupsdnhlxawmjqofeck",
+ }
+
+--[[ichd--
+\startparagraph
+Notches are assigned to rotors according to the Royal Army
+mnemonic.
+\stopparagraph
+--ichd]]--
+ notches = { }
+ do
+ local raw_notches = "rfwkannnn"
+ --local raw_notches = "qevjz"
+ local n = 1
+ for chr in utfcharacters(raw_notches) do
+ local pos = stringfind(alpha_sorted, chr)
+ notches[n] = pos - 1
+ n = n + 1
+ end
+ end
+
+--[[ichd--
+\placetable[here][listing:reflector]%
+ {The three reflectors and their substitution rules.}{%
+ \starttabulate[|r|l|]
+ \NC UKW a \NC AE BJ CM DZ FL GY HX IV KW NR OQ PU ST \NC \NR
+ \NC UKW b \NC AY BR CU DH EQ FS GL IP JX KN MO TZ VW \NC \NR
+ \NC UKW c \NC AF BV CP DJ EI GO HY KR LZ MX NW QT SU \NC \NR
+ \stoptabulate
+}
+--ichd]]--
+
+ reflector_wiring = { }
+ local raw_ukw = {
+ { a = "e", b = "j", c = "m", d = "z", f = "l", g = "y", h = "x",
+ i = "v", k = "w", n = "r", o = "q", p = "u", s = "t", },
+ { a = "y", b = "r", c = "u", d = "h", e = "q", f = "s", g = "l",
+ i = "p", j = "x", k = "n", m = "o", t = "z", v = "w", },
+ { a = "f", b = "v", c = "p", d = "j", e = "i", g = "o", h = "y",
+ k = "r", l = "z", m = "x", n = "w", q = "t", s = "u", },
+ }
+ for i=1, #raw_ukw do
+ local new_wiring = { }
+ local current_ukw = raw_ukw[i]
+ for from, to in next, current_ukw do
+ from = letter_to_value[from]
+ to = letter_to_value[to]
+ new_wiring[from] = to
+ new_wiring[to] = from
+ end
+ reflector_wiring[i] = new_wiring
+ end
+end
+
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Pretty printing for debug purposes]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startparagraph
+The functions below allow for formatting of the terminal output; they
+have no effect on the workings of the enigma simulator.
+\stopparagraph
+--ichd]]--
+
+local emit
+local pprint_ciphertext
+local pprint_encoding
+local pprint_encoding_scheme
+local pprint_init
+local pprint_machine_step
+local pprint_new_machine
+local pprint_rotor
+local pprint_rotor_scheme
+local pprint_step
+local polite_key_request
+local key_invalid
+do
+ local eol = "\n"
+
+ local colorstring_template = "\027[%d;1m%s\027[0m"
+ local colorize = function (s, color)
+ color = color and color < 38 and color > 29 and color or 31
+ return stringformat(colorstring_template,
+ color,
+ s)
+ end
+
+ local underline = function (s)
+ return stringformat("\027[4;37m%s\027[0m", s)
+ end
+
+ local s_steps = [[Total characters encoded with machine “]]
+ local f_warnsteps = [[ (%d over permitted maximum)]]
+ pprint_machine_step = function (n, name)
+ local sn
+ name = colorize(name, 36)
+ if n > max_msg_length then
+ sn = colorize(n, 31) .. stringformat(f_warnsteps,
+ n - max_msg_length)
+ else
+ sn = colorize(n, 37)
+ end
+ return s_steps .. name .. "”: " .. sn .. "."
+ end
+ local rotorstate = "[s \027[1;37m%s\027[0m n\027[1;37m%2d\027[0m]> "
+ pprint_rotor = function (rotor)
+ local visible = rotor.state % 26 + 1
+ local w, n = rotor.wiring, (rotor.notch - visible) % 26 + 1
+ local tmp = { }
+ for i=1, 26 do
+ local which = (i + rotor.state - 1) % 26 + 1
+ local chr = value_to_letter[rotor.wiring.from[which]]
+ if i == n then -- highlight positions of notches
+ tmp[i] = colorize(stringupper(chr), 32)
+ --elseif chr == value_to_letter[visible] then
+ ---- highlight the character in window
+ -- tmp[i] = colorize(chr, 33)
+ else
+ tmp[i] = chr
+ end
+ end
+ return stringformat(rotorstate,
+ stringupper(value_to_letter[visible]),
+ n)
+ .. tableconcat(tmp)
+ end
+
+ local rotor_scheme = underline"[rot not]"
+ .. " "
+ .. underline(alpha_sorted)
+ pprint_rotor_scheme = function ()
+ return rotor_scheme
+ end
+
+ local s_encoding_scheme = eol
+ .. [[in > 1 => 2 => 3 > UKW > 3 => 2 => 1]]
+ pprint_encoding_scheme = function ()
+ return underline(s_encoding_scheme)
+ end
+ local s_step = " => "
+ local stepcolor = 36
+ local finalcolor = 32
+ pprint_encoding = function (steps)
+ local nsteps, result = #steps, { }
+ for i=0, nsteps-1 do
+ result[i+1] = colorize(value_to_letter[steps[i]], stepcolor)
+ .. s_step
+ end
+ result[nsteps+1] = colorize(value_to_letter[steps[nsteps]],
+ finalcolor)
+ return tableconcat(result)
+ end
+
+ local init_announcement
+ = colorize("\n" .. [[Initial position of rotors: ]],
+ 37)
+ pprint_init = function (init)
+ local result = ""
+ result = value_to_letter[init[1]] .. " "
+ .. value_to_letter[init[2]] .. " "
+ .. value_to_letter[init[3]]
+ return init_announcement .. colorize(stringupper(result), 34)
+ end
+
+ local machine_announcement =
+ [[Enigma machine initialized with the following settings.]] .. eol
+ local s_ukw = colorize(" Reflector:", 37)
+ local s_pb = colorize("Plugboard setting:", 37)
+ local s_ring = colorize(" Ring positions:", 37)
+ local empty_plugboard = colorize(" ——", 34)
+ pprint_new_machine = function (m)
+ local result = { "" }
+ result[#result+1] = underline(machine_announcement)
+ result[#result+1] = s_ukw
+ .. " "
+ .. colorize(
+ stringupper(value_to_letter[m.reflector]),
+ 34
+ )
+ local rings = ""
+ for i=1, 3 do
+ local this = m.ring[i]
+ rings = rings
+ .. " "
+ .. colorize(stringupper(value_to_letter[this + 1]), 34)
+ end
+ result[#result+1] = s_ring .. rings
+ if m.__raw.plugboard then
+ local tpb, pb = m.__raw.plugboard, ""
+ for i=1, #tpb do
+ pb = pb .. " " .. colorize(tpb[i], 34)
+ end
+ result[#result+1] = s_pb .. pb
+ else
+ result[#result+1] = s_pb .. empty_plugboard
+ end
+ result[#result+1] = ""
+ result[#result+1] = pprint_rotor_scheme()
+ for i=1, 3 do
+ result[#result+1] = pprint_rotor(m.rotors[i])
+ end
+ return tableconcat(result, eol) .. eol
+ end
+
+ local step_template = colorize([[Step № ]], 37)
+ local chr_template = colorize([[ —— Input ]], 37)
+ local pbchr_template = colorize([[ → ]], 37)
+ pprint_step = function (n, chr, pb_chr)
+ return eol
+ .. step_template
+ .. colorize(n, 34)
+ .. chr_template
+ .. colorize(stringupper(value_to_letter[chr]), 34)
+ .. pbchr_template
+ .. colorize(stringupper(value_to_letter[pb_chr]), 34)
+ .. eol
+ end
+
+ -- Split the strings into lines, group them in bunches of five etc.
+ local tw = 30
+ local pprint_textblock = function (s)
+ local len = utf8len(s)
+ local position = 1 -- position in string
+ local nline = 5 -- width of current line
+ local out = utf8sub(s, position, position+4)
+ repeat
+ position = position + 5
+ nline = nline + 6
+ if nline > tw then
+ out = out .. eol .. utf8sub(s, position, position+4)
+ nline = 1
+ else
+ out = out .. " " .. utf8sub(s, position, position+4)
+ end
+ until position > len
+ return out
+ end
+
+ local intext = colorize([[Input text:]], 37)
+ local outtext = colorize([[Output text:]], 37)
+ pprint_ciphertext = function (input, output, upper_p)
+ if upper_p then
+ input = stringupper(input)
+ output = stringupper(output)
+ end
+ return eol
+ .. intext
+ .. eol
+ .. pprint_textblock(input)
+ .. eol .. eol
+ .. outtext
+ .. eol
+ .. pprint_textblock(output)
+ end
+
+--[[ichd--
+\startparagraph
+\luafunction{emit} is the main wrapper function for
+\identifier{stdout}. Checks if the global verbosity setting exceeds
+the specified threshold, and only then pushes the output.
+\stopparagraph
+--ichd]]--
+ emit = function (v, f, ...)
+ if f and v and verbose_level >= v then
+ iowrite(f(...) .. eol)
+ end
+ return 0
+ end
+--[[ichd--
+\startparagraph
+The \luafunction{polite_key_request} will be called in case the
+\identifier{day_key} field of the machine setup is empty at the time of
+initialization.
+\stopparagraph
+--ichd]]--
+ local s_request = "\n\n "
+ .. underline"This is an encrypted document." .. [[
+
+
+ Please enter the document key for enigma machine
+ “%s”.
+
+ Key Format:
+
+Ref R1 R2 R3 I1 I2 I3 [P1 ..] Ref: reflector A/B/C
+ Rn: rotor, I through V
+ In: ring position, 01 through 26
+ Pn: optional plugboard wiring, upto 32
+
+>]]
+ polite_key_request = function (name)
+ return stringformat(s_request, colorize(name, 33))
+ end
+
+ local s_invalid_key = colorize"Warning!"
+ .. " The specified key is invalid."
+ key_invalid = function ()
+ return s_invalid_key
+ end
+end
+
+--[[ichd--
+\startparagraph
+The functions \luafunction{new} and \luafunction{ask_for_day_key} are
+used outside their scope, so we declare them beforehand.
+\stopparagraph
+--ichd]]--
+local new
+local ask_for_day_key
+do
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Rotation]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startparagraph
+The following function \luafunction{do_rotate} increments the
+rotational state of a single rotor. There are two tests for notches:
+\startitemize[n]
+ \item whether it’s at the current character, and
+ \item whether it’s at the next character.
+\stopitemize
+The latter is an essential prerequisite for double-stepping.
+\stopparagraph
+--ichd]]--
+ local do_rotate = function (rotor)
+ rotor.state = rotor.state % 26 + 1
+ return rotor,
+ rotor.state == rotor.notch,
+ rotor.state + 1 == rotor.notch
+ end
+
+--[[ichd--
+\startparagraph
+The \luafunction{rotate} function takes care of rotor (\emph{Walze})
+movement. This entails incrementing the next rotor whenever the notch
+has been reached and covers the corner case \emph{double stepping}.
+\stopparagraph
+--ichd]]--
+ local rotate = function (machine)
+ local rotors = machine.rotors
+ local rc, rb, ra = rotors[1], rotors[2], rotors[3]
+
+ ra, nxt = do_rotate(ra)
+ if nxt or machine.double_step then
+ rb, nxt, nxxt = do_rotate(rb)
+ if nxt then
+ rc = do_rotate(rc)
+ end
+ if nxxt then
+ --- weird: home.comcast.net/~dhhamer/downloads/rotors1.pdf
+ machine.double_step = true
+ else
+ machine.double_step = false
+ end
+ end
+ machine.rotors = { rc, rb, ra }
+ end
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Input Preprocessing]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startparagraph
+Internally, we will use lowercase strings as they are a lot more
+readable than uppercase. Lowercasing happens prior to any further
+dealings with input. After the encoding or decoding has been
+accomplished, there will be an optional (re-)uppercasing.
+\stopparagraph
+
+\startparagraph
+Substitutions \reference[listing:preproc]{}are applied onto the
+lowercased input. You might want to avoid some of these, above all the
+rules for numbers, because they translate single digits only. The
+solution is to write out numbers above ten.
+\stopparagraph
+--ichd]]--
+
+ local pp_substitutions = {
+ -- Umlauts are resolved.
+ ["ö"] = "oe",
+ ["ä"] = "ae",
+ ["ü"] = "ue",
+ ["ß"] = "ss",
+ -- WTF?
+ ["ch"] = "q",
+ ["ck"] = "q",
+ -- Punctuation -> “x”
+ [","] = "x",
+ ["."] = "x",
+ [";"] = "x",
+ [":"] = "x",
+ ["/"] = "x",
+ ["’"] = "x",
+ ["‘"] = "x",
+ ["„"] = "x",
+ ["“"] = "x",
+ ["“"] = "x",
+ ["-"] = "x",
+ ["–"] = "x",
+ ["—"] = "x",
+ ["!"] = "x",
+ ["?"] = "x",
+ ["‽"] = "x",
+ ["("] = "x",
+ [")"] = "x",
+ ["["] = "x",
+ ["]"] = "x",
+ ["<"] = "x",
+ [">"] = "x",
+ -- Spaces are omitted.
+ [" "] = "",
+ ["\n"] = "",
+ ["\t"] = "",
+ ["\v"] = "",
+ ["\\"] = "",
+ -- Numbers are resolved.
+ ["0"] = "null",
+ ["1"] = "eins",
+ ["2"] = "zwei",
+ ["3"] = "drei",
+ ["4"] = "vier",
+ ["5"] = "fuenf",
+ ["6"] = "sechs",
+ ["7"] = "sieben",
+ ["8"] = "acht",
+ ["9"] = "neun",
+ }
+
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[
+ title={Main function chain to be applied to single characters},
+]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\startparagraph
+As far as the Enigma is concerned, there is no difference between
+encoding and decoding. Thus, we need only one function
+(\luafunction{encode_char}) to achieve the complete functionality.
+However, within every encoding step, characters will be wired
+differently in at least one of the rotors according to its rotational
+state. Rotation is simulated by adding the \identifier{state} field of
+each rotor to the letter value (its position on the ingoing end).
+\stopparagraph
+\placetable[here][table:dirs]{Directional terminology}{%
+ \starttabulate[|l|r|l|]
+ \NC boolean \NC direction \NC meaning \NC \AR
+ \NC true \NC “from” \NC right to left \NC \AR
+ \NC false \NC “to” \NC left to right \NC \AR
+ \stoptabulate%
+}
+\startparagraph
+The function \luafunction{do_do_encode_char} returns the character
+substitution for one rotor. As a letter passes through each rotor
+twice, the argument \identifier{direction} determines which way the
+substitution is applied.
+\stopparagraph
+--ichd]]--
+ local do_do_encode_char = function (char, rotor, direction)
+ local rw = rotor.wiring
+ local rs = rotor.state
+ local result = char
+ if direction then -- from
+ result = (result + rs - 1) % 26 + 1
+ result = rw.from[result]
+ result = (result - rs - 1) % 26 + 1
+ else -- to
+ result = (result + rs - 1) % 26 + 1
+ result = rw.to[result]
+ result = (result - rs - 1) % 26 + 1
+ end
+ return result
+ end
+
+--[[ichd--
+\startparagraph
+Behind the plugboard, every character undergoes seven substitutions:
+two for each rotor plus the central one through the reflector. The
+function \luafunction{do_encode_char}, although it returns the final
+result only, keeps every intermediary step inside a table for debugging
+purposes. This may look inefficient but is actually a great advantage
+whenever something goes wrong.
+\stopparagraph
+--ichd]]--
+ --- ra -> rb -> rc -> ukw -> rc -> rb -> ra
+ local do_encode_char = function (rotors, reflector, char)
+ local rc, rb, ra = rotors[1], rotors[2], rotors[3]
+ local steps = { [0] = char }
+ --
+ steps[1] = do_do_encode_char(steps[0], ra, true)
+ steps[2] = do_do_encode_char(steps[1], rb, true)
+ steps[3] = do_do_encode_char(steps[2], rc, true)
+ steps[4] = reflector_wiring[reflector][steps[3]]
+ steps[5] = do_do_encode_char(steps[4], rc, false)
+ steps[6] = do_do_encode_char(steps[5], rb, false)
+ steps[7] = do_do_encode_char(steps[6], ra, false)
+ emit(2, pprint_encoding_scheme)
+ emit(2, pprint_encoding, steps)
+ return steps[7]
+ end
+
+--[[ichd--
+\startparagraph
+Before an input character is passed on to the actual encoding routing,
+the function \luafunction{encode_char} matches it agains the latin
+alphabet.
+Characters failing this test are either passed through or ignored,
+depending on the machine option \identifier{other_chars}.
+Also, the counter of encoded characters is incremented at this stage
+and some pretty printer hooks reside here.
+\stopparagraph
+
+\startparagraph
+\luafunction{encode_char} contributes only one element of the encoding
+procedure: the plugboard (\emph{Steckerbrett}).
+Like the rotors described above, a character passed through this
+device twice; the plugboard marks the beginning and end of every step.
+For debugging purposes, the first substitution is stored in a separate
+local variable, \identifier{pb_char}.
+\stopparagraph
+--ichd]]--
+
+ local encode_char = function (machine, char)
+ machine.step = machine.step + 1
+ machine:rotate()
+ local pb = machine.plugboard
+ char = letter_to_value[char]
+ local pb_char = pb[char] -- first plugboard substitution
+ emit(2, pprint_step, machine.step, char, pb_char)
+ emit(3, pprint_rotor_scheme)
+ emit(3, pprint_rotor, machine.rotors[1])
+ emit(3, pprint_rotor, machine.rotors[2])
+ emit(3, pprint_rotor, machine.rotors[3])
+ char = do_encode_char(machine.rotors,
+ machine.reflector,
+ pb_char)
+ return value_to_letter[pb[char]] -- second plugboard substitution
+ end
+
+ local get_random_pattern = function ()
+ local a, b, c
+ = mathrandom(1,26), mathrandom(1,26), mathrandom(1,26)
+ return value_to_letter[a]
+ .. value_to_letter[b]
+ .. value_to_letter[c]
+ end
+
+ local pattern_to_state = function (pat)
+ return {
+ letter_to_value[stringsub(pat, 1, 1)],
+ letter_to_value[stringsub(pat, 2, 2)],
+ letter_to_value[stringsub(pat, 3, 3)],
+ }
+ end
+
+ local set_state = function (machine, state)
+ local rotors = machine.rotors
+ for i=1, 3 do
+ rotors[i].state = state[i] - 1
+ end
+ end
+
+--[[ichd--
+\startparagraph
+When \modulename{Enigma} is called from \TEX, the encoding
+proceeds character by character as we iterate one node at a time.
+\luafunction{encode_string} is a wrapper for use with strings, e.~g. in
+the mtx-script (\at{page}[sec:fun]).
+It handles iteration and extraction of successive characters from the
+sequence.
+\stopparagraph
+--ichd]]--
+ local encode_string = function (machine, str) --, pattern)
+ local result = { }
+ for char in utfcharacters(str) do
+ local tmp = machine:encode(char)
+ if tmp ~= false then
+ if type(tmp) == "table" then
+ for i=1, #tmp do
+ result[#result+1] = tmp[i]
+ end
+ else
+ result[#result+1] = tmp
+ end
+ end
+ end
+ machine:processed_chars()
+ return tableconcat(result)
+ end
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Initialization string parser]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\placetable[here][]{Initialization strings}{%
+ \bTABLE
+ \bTR
+ \bTD Reflector \eTD
+ \bTD[nc=3] Rotor \eTD
+ \bTD[nc=3] Initial \eTD
+ \bTD[nc=10] Plugboard wiring \eTD
+ \eTR
+ \eTR
+ \bTR
+ \bTD in slot \eTD
+ \bTD[nc=3] setting \eTD
+ \bTD[nc=3] rotor \eTD
+ \eTR
+ \bTR
+ \bTD \eTD
+ \bTD 1 \eTD\bTD 2 \eTD\bTD 3 \eTD
+ \bTD 1 \eTD\bTD 2 \eTD\bTD 3 \eTD
+ \bTD 1 \eTD\bTD 2 \eTD\bTD 3 \eTD\bTD 4 \eTD\bTD 5 \eTD
+ \bTD 6 \eTD\bTD 7 \eTD\bTD 8 \eTD\bTD 9 \eTD\bTD 10 \eTD
+ \eTR
+ \bTR
+ \bTD B \eTD
+ \bTD I \eTD\bTD IV \eTD\bTD III \eTD
+ \bTD 16 \eTD\bTD 26 \eTD\bTD 08 \eTD
+ \bTD AD \eTD\bTD CN \eTD\bTD ET \eTD
+ \bTD FL \eTD\bTD GI \eTD\bTD JV \eTD
+ \bTD KZ \eTD\bTD PU \eTD\bTD QY \eTD
+ \bTD WX \eTD
+ \eTR
+ \eTABLE
+}
+--ichd]]--
+ local roman_digits = {
+ i = 1, I = 1,
+ ii = 2, II = 2,
+ iii = 3, III = 3,
+ iv = 4, IV = 4,
+ v = 5, V = 5,
+ }
+
+ local p_init = P{
+ "init",
+ init = V"whitespace"^-1 * Ct(V"do_init"),
+ do_init = (V"reflector" * V"whitespace")^-1
+ * V"rotors" * V"whitespace"
+ * V"ring"
+ * (V"whitespace" * V"plugboard")^-1
+ ,
+ reflector = Cg(C(R("ac","AC")) / stringlower, "reflector")
+ ,
+
+ rotors = Cg(Ct(V"rotor" * V"whitespace"
+ * V"rotor" * V"whitespace"
+ * V"rotor"),
+ "rotors")
+ ,
+ rotor = Cs(V"roman_five" / roman_digits
+ + V"roman_four" / roman_digits
+ + V"roman_three" / roman_digits
+ + V"roman_two" / roman_digits
+ + V"roman_one" / roman_digits)
+ ,
+ roman_one = P"I" + P"i",
+ roman_two = P"II" + P"ii",
+ roman_three = P"III" + P"iii",
+ roman_four = P"IV" + P"iv",
+ roman_five = P"V" + P"v",
+
+ ring = Cg(Ct(V"double_digit" * V"whitespace"
+ * V"double_digit" * V"whitespace"
+ * V"double_digit"),
+ "ring")
+ ,
+ double_digit = C(R"02" * R"09"),
+
+ plugboard = Cg(V"do_plugboard", "plugboard"),
+ --- no need to enforce exactly ten substitutions
+ --do_plugboard = Ct(V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination" * V"whitespace"
+ -- * V"letter_combination")
+ do_plugboard = Ct(V"letter_combination"
+ * (V"whitespace" * V"letter_combination")^0)
+ ,
+ letter_combination = C(R("az", "AZ") * R("az", "AZ")),
+
+ whitespace = S" \n\t\v"^1,
+ }
+
+
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Initialization routines]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\startparagraph
+The plugboard is implemented as a pair of hash tables.
+\stopparagraph
+--ichd]]--
+ local get_plugboard_substitution = function (p)
+ --- Plugboard wirings are symmetrical, thus we have one table for
+ --- each direction.
+ local tmp, result = { }, { }
+ for _, str in next, p do
+ local one, two = stringlower(stringsub(str, 1, 1)),
+ stringlower(stringsub(str, 2))
+ tmp[one] = two
+ tmp[two] = one
+ end
+ local n_letters = 26
+
+ local lv = letter_to_value
+ for n=1, n_letters do
+ local letter = value_to_letter[n]
+ local sub = tmp[letter] or letter
+ -- Map each char either to the plugboard substitution or itself.
+ result[lv[letter]] = lv[sub or letter]
+ end
+ return result
+ end
+
+--[[ichd--
+\startparagraph
+Initialization of the rotors requires some precautions to be taken.
+The most obvious of which is adjusting the displacement of its wiring
+by the ring setting.
+\stopparagraph
+\startparagraph
+Another important task is to store the notch position in order for it
+to be retrievable by the rotation subroutine at a later point.
+\stopparagraph
+\startparagraph
+The actual bidirectional mapping is implemented as a pair of tables.
+The initial order of letters, before the ring shift is applied, is
+alphabetical on the input (right, “from”) side and, on the output
+(left, “to”) side taken by the hard wired correspondence as specified
+in the rotor wirings above.
+NB the descriptions in terms of “output” and “input” directions is
+misleading in so far as during any encoding step the electricity will
+pass through every rotor in both ways.
+Hence, the “input” (right, from) direction literally applies only to
+the first half of the encoding process between plugboard and reflector.
+\stopparagraph
+\startparagraph
+The function \luafunction{do_get_rotor} creates a single rotor instance
+and populates it with character mappings. The \identifier{from} and
+\identifier{to} subfields of its \identifier{wiring} field represent
+the wiring in the respective directions.
+This initital wiring was specified in the corresponding
+\identifier{raw_rotor_wiring} table; the ringshift is added modulo the
+alphabet size in order to get the correctly initialized rotor.
+\stopparagraph
+--ichd]]--
+ local do_get_rotor = function (raw, notch, ringshift)
+ local rotor = {
+ wiring = {
+ from = { },
+ to = { },
+ },
+ state = 0,
+ notch = notch,
+ }
+ local w = rotor.wiring
+ for from=1, 26 do
+ local to = letter_to_value[stringsub(raw, from, from)]
+ --- The shift needs to be added in both directions.
+ to = (to + ringshift - 1) % 26 + 1
+ from = (from + ringshift - 1) % 26 + 1
+ rotor.wiring.from[from] = to
+ rotor.wiring.to [to ] = from
+ end
+ --table.print(rotor, "rotor")
+ return rotor
+ end
+
+--[[ichd--
+\startparagraph
+Rotors are initialized sequentially accordings to the initialization
+request.
+The function \luafunction{get_rotors} walks over the list of
+initialization instructions and calls \luafunction{do_get_rotor} for
+the actual generation of the rotor table. Each rotor generation request
+consists of three elements:
+\stopparagraph
+\startitemize[n]
+ \item the choice of rotor (one of five),
+ \item the notch position of said rotor, and
+ \item the ring shift.
+\stopitemize
+--ichd]]--
+ local get_rotors = function (rotors, ring)
+ local s, r = { }, { }
+ for n=1, 3 do
+ local nr = tonumber(rotors[n])
+ local ni = tonumber(ring[n]) - 1 -- “1” means shift of zero
+ r[n] = do_get_rotor(raw_rotor_wiring[nr], notches[nr], ni)
+ s[n] = ni
+ end
+ return r, s
+ end
+
+ local decode_char = encode_char -- hooray for involutory ciphers
+
+--[[ichd--
+\startparagraph
+The function \luafunction{encode_general} is an intermediate step for
+the actual single-character encoding / decoding routine
+\luafunction{enchode_char}.
+Its purpose is to ensure encodability of a given character before
+passing it along.
+Characters are first checked against the replacement table
+\identifier{pp_substitutions} (see \at{page}[listing:preproc]).
+For single-character replacements the function returns the encoded
+character (string).
+However, should the replacement turn out to consist of more than one
+character, each one will be encoded successively, yielding a list.
+\stopparagraph
+--ichd]]--
+ local encode_general = function (machine, chr)
+ local chr = utf8lower(chr)
+ local replacement
+ = pp_substitutions[chr] or letter_to_value[chr] and chr
+ if not replacement then
+ if machine.other_chars then
+ return chr
+ else
+ return false
+ end
+ end
+
+ if utf8len(replacement) == 1 then
+ return encode_char(machine, replacement)
+ end
+ local result = { }
+ for new_chr in utfcharacters(replacement) do
+ result[#result+1] = encode_char(machine, new_chr)
+ end
+ return result
+ end
+
+ local process_message_key
+ local alpha = R"az"
+ local alpha_dec = alpha / letter_to_value
+ local whitespace = S" \n\t\v"
+ local mkeypattern = Ct(alpha_dec * alpha_dec * alpha_dec)
+ * whitespace^0
+ * C(alpha * alpha *alpha)
+ process_message_key = function (machine, message_key)
+ message_key = stringlower(message_key)
+ local init, three = lpegmatch(mkeypattern, message_key)
+ -- to be implemented
+ end
+
+ local decode_string = function (machine, str, message_key)
+ machine.kenngruppe, str = stringsub(str, 3, 5), stringsub(str, 6)
+ machine:process_message_key(message_key)
+ local decoded = encode_string(machine, str)
+ return decoded
+ end
+
+ local testoptions = {
+ size = 42,
+
+ }
+ local generate_header = function (options)
+ end
+
+ local processed_chars = function (machine)
+ emit(1, pprint_machine_step, machine.step, machine.name)
+ end
+
+--[[ichd--
+\startparagraph
+The day key is entrusted to the function \luafunction{handle_day_key}.
+If the day key is the empty string or \type{nil}, it will ask for a key
+on the terminal. (Cf. below, \at{page}[listing:ask_for_day_key].)
+Lesson: don’t forget providing day keys in your setups when running in
+batch mode.
+\stopparagraph
+--ichd]]--
+ local handle_day_key handle_day_key = function (dk, name, old)
+ local result
+ if not dk or dk == "" then
+ dk = ask_for_day_key(name, old)
+ end
+ result = lpegmatch(p_init, dk)
+ result.reflector = result.reflector or "b"
+ -- If we don’t like the key we’re going to ask again. And again....
+ return result or handle_day_key(nil, name, dk)
+ end
+
+--[[ichd--
+\startparagraph
+The enigma encoding is restricted to an input -- and, naturally, output
+-- alphabet of exactly twenty-seven characters. Obviously, this would
+severely limit the set of encryptable documents. For this reason the
+plain text would be \emph{preprocessed} prior to encoding, removing
+spaces and substituting a range of characters, e.\,g. punctuation, with
+placeholders (“X”) from the encodable spectrum. See above
+\at{page}[listing:preproc] for a comprehensive list of substitutions.
+\stopparagraph
+
+\startparagraph
+The above mentioned preprocessing, however, does not even nearly extend
+to the whole unicode range that modern day typesetting is expected to
+handle. Thus, sooner or later an Enigma machine will encounter
+non-preprocessable characters and it will have to decide what to do
+with them. The Enigma module offers two ways to handle this kind of
+situation: \emph{drop} those characters, possibly distorting the
+deciphered plain text, or to leave them in, leaving hints behind as to
+the structure of the encrypted text. None of these is optional, so it
+is nevertheless advisable to not include non-latin characters in the
+plain text in the first place. The settings key
+\identifier{other_chars} (type boolean) determines whether we will keep
+or drop offending characters.
+\stopparagraph
+--ichd]]--
+
+ new = function (name, args)
+ local setup_string, pattern = args.day_key, args.rotor_setting
+ local raw_settings = handle_day_key(setup_string, name)
+ local rotors, ring =
+ get_rotors(raw_settings.rotors, raw_settings.ring)
+ local plugboard
+ = raw_settings.plugboard
+ and get_plugboard_substitution(raw_settings.plugboard)
+ or get_plugboard_substitution{ }
+ local machine = {
+ name = name,
+ step = 0, -- n characters encoded
+ init = {
+ rotors = raw_settings.rotors,
+ ring = raw_settings.ring
+ },
+ rotors = rotors,
+ ring = ring,
+ state = init_state,
+ other_chars = args.other_chars,
+ spacing = args.spacing,
+ ---> a>1, b>2, c>3
+ reflector = letter_to_value[raw_settings.reflector],
+ plugboard = plugboard,
+ --- functionality
+ rotate = rotate,
+ --process_message_key = process_message_key,
+ encode_string = encode_string,
+ encode_char = encode_char,
+ encode = encode_general,
+ decode_string = decode_string,
+ decode_char = decode_char,
+ set_state = set_state,
+ processed_chars = processed_chars,
+ --- <badcodingstyle> -- hackish but occasionally useful
+ __raw = raw_settings
+ --- </badcodingstyle>
+ } --- machine
+ local init_state
+ = pattern_to_state(pattern or get_random_pattern())
+ emit(1, pprint_init, init_state)
+ machine:set_state(init_state)
+
+ --table.print(machine.rotors)
+ emit(1, pprint_new_machine, machine)
+ return machine
+ end
+
+end
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+
+--[[ichd--
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+\startdocsection[title=Setup Argument Handling]
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+--ichd]]--
+do
+--[[ichd--
+\startparagraph
+As the module is intended to work both with the Plain and \LATEX\
+formats as well as \CONTEXT, we can’t rely on format dependent setups.
+Hence the need for an argument parser. Should be more efficient anyways
+as all the functionality resides in Lua.
+\stopparagraph
+--ichd]]--
+
+ local p_args = P{
+ "args",
+ args = Cf(Ct"" * (V"kv_pair" + V"emptyline")^0, rawset),
+ kv_pair = Cg(V"key"
+ * V"separator"
+ * (V"value" * V"final"
+ + V"empty"))
+ * V"rest_of_line"^-1
+ ,
+ key = V"whitespace"^0 * C(V"key_char"^1),
+ key_char = (1 - V"whitespace" - V"eol" - V"equals")^1,
+ separator = V"whitespace"^0 * V"equals" * V"whitespace"^0,
+ empty = V"whitespace"^0 * V"comma" * V"rest_of_line"^-1
+ * Cc(false)
+ ,
+ value = C((V"balanced" + (1 - V"final"))^1),
+ final = V"whitespace"^0 * V"comma" + V"rest_of_string",
+ rest_of_string = V"whitespace"^0
+ * V"eol_comment"^-1
+ * V"eol"^0
+ * V"eof"
+ ,
+ rest_of_line = V"whitespace"^0 * V"eol_comment"^-1 * V"eol",
+ eol_comment = V"comment_string" * (1 - (V"eol" + V"eof"))^0,
+ comment_string = V"lua_comment" + V"TeX_comment",
+ TeX_comment = V"percent",
+ lua_comment = V"double_dash",
+ emptyline = V"rest_of_line",
+
+ balanced = V"balanced_brk" + V"balanced_brc",
+ balanced_brk = V"lbrk"
+ * (V"balanced" + (1 - V"rbrk"))^0
+ * V"rbrk"
+ ,
+ balanced_brc = V"lbrc"
+ * (V"balanced" + (1 - V"rbrc"))^0
+ * V"rbrc"
+ ,
+ -- Terminals
+ eol = P"\n\r" + P"\r\n" + P"\n" + P"\r",
+ eof = -P(1),
+ whitespace = S" \t\v",
+ equals = P"=",
+ dot = P".",
+ comma = P",",
+ dash = P"-", double_dash = V"dash" * V"dash",
+ percent = P"%",
+ lbrk = P"[", rbrk = P"]",
+ lbrc = P"{", rbrc = P"}",
+ }
+
+
+--[[ichd--
+\startparagraph
+In the next step we process the arguments, check the input for sanity
+etc. The function \luafunction{parse_args} will test whether a value
+has a sanitizer routine and, if so, apply it to its value.
+\stopparagraph
+--ichd]]--
+
+ local boolean_synonyms = {
+ ["1"] = true,
+ doit = true,
+ indeed = true,
+ ok = true,
+ ["⊤"] = true,
+ ["true"] = true,
+ yes = true,
+ }
+ local toboolean
+ = function (value) return boolean_synonyms[value] or false end
+ local alpha = R("az", "AZ")
+ local digit = R"09"
+ local space = S" \t\v"
+ local ans = alpha + digit + space
+ local p_ans = Cs((ans + (1 - ans / ""))^1)
+ local alphanum_or_space = function (str)
+ if type(str) ~= "string" then return nil end
+ return lpegmatch(p_ans, str)
+ end
+ local ensure_int = function (n)
+ n = tonumber(n)
+ if not n then return 0 end
+ return mathfloor(n + 0.5)
+ end
+ p_alpha = Cs((alpha + (1 - alpha / ""))^1)
+ local ensure_alpha = function (s)
+ s = tostring(s)
+ return lpegmatch(p_alpha, s)
+ end
+
+ local sanitizers = {
+ other_chars = toboolean, -- true = keep, false = drop
+ spacing = toboolean,
+ day_key = alphanum_or_space,
+ rotor_setting = ensure_alpha,
+ verbose = ensure_int,
+ }
+ enigma.parse_args = function (raw)
+ local args = lpegmatch(p_args, raw)
+ for k, v in next, args do
+ local f = sanitizers[k]
+ if f then
+ args[k] = f(v)
+ else
+ -- OPTIONAL be fascist and permit only predefined args
+ args[k] = v
+ end
+ end
+ return args
+ end
+--[[ichd--
+\startparagraph
+If the machine setting lacks key settings then we’ll go ahead and ask
+\reference[listing:ask_for_day_key]{}%
+the user directly, hence the function \luafunction{ask_for_day_key}.
+We abort after three misses lest we annoy the user \dots
+\stopparagraph
+--ichd]]--
+ local max_tries = 3
+ ask_for_day_key = function (name, old, try)
+ if try == max_tries then
+ iowrite[[
+Aborting. Entered invalid key three times.
+]]
+ os.exit()
+ end
+ if old then
+ emit(0, key_invalid)
+ end
+ emit(0, polite_key_request, name)
+ local result = ioread()
+ iowrite("\n")
+ return alphanum_or_space(result) or
+ ask_for_day_key(name, (try and try + 1 or 1))
+ end
+end
+
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+--[[ichd--
+\startdocsection[title=Callback]
+\startparagraph
+This is the interface to \TEX. We generate a new callback handler for
+each defined Enigma machine. \CONTEXT\ delivers the head as third
+argument of a callback only (...‽), so we’ll have to do some variable
+shuffling on the function side.
+\stopparagraph
+
+\startparagraph
+When grouping output into the traditional blocks of five letters we
+insert space nodes. As their properties depend on the font we need to
+recreate the space item for every paragraph. Also, as \CONTEXT\ does
+not preload a font we lack access to font metrics before
+\type{\starttext}. Thus creating the space earlier will result in an
+error.
+The function \luafunction{generate_space} will be called inside the
+callback in order to get an appropriate space glue.
+\stopparagraph
+--ichd]]--
+
+local generate_space = function ( )
+ local current_fontparms = font.getfont(font.current()).parameters
+ local space_node = nodenew(GLUE_NODE)
+ space_node.spec = nodenew(GLUE_SPEC_NODE)
+ space_node.spec.width = current_fontparms.space
+ space_node.spec.shrink = current_fontparms.space_shrink
+ space_node.spec.stretch = current_fontparms.space_stretch
+ return space_node
+end
+
+--[[ichd--
+\startparagraph
+\useURL[khaled_hosny_texsx] [http://tex.stackexchange.com/a/11970]
+ [] [tex.sx]
+Registering a callback (“node attribute”?, “node task”?, “task
+action”?) in \CONTEXT\ is not straightforward, let alone documented.
+The trick is to create, install and register a handler first in order
+to use it later on \dots\ many thanks to Khaled Hosny, who posted an
+answer to \from[khaled_hosny_texsx].
+\stopparagraph
+--ichd]]--
+
+local new_callback = function (machine, name)
+ enigma.machines [name] = machine
+ local format_is_context = format_is_context
+ local current_space_node
+ local mod_5 = 0
+
+ --- First we need to choose an insertion method. If autospacing is
+ --- requested, a space will have to be inserted every five
+ --- characters. The rationale behind using differend functions to
+ --- implement each method is that it should be faster than branching
+ --- for each character.
+ local insert_encoded
+
+ if machine.spacing then -- auto-group output
+ insert_encoded = function (head, n, replacement)
+ local insert_glyph = nodecopy(n)
+ if replacement then -- inefficient but bulletproof
+ insert_glyph.char = utf8byte(replacement)
+ --print(utf8char(n.char), "=>", utf8char(insertion.char))
+ end
+ --- if we insert a space we need to return the
+ --- glyph node in order to track positions when
+ --- replacing multiple nodes at once (e.g. ligatures)
+ local insertion = insert_glyph
+ mod_5 = mod_5 + 1
+ if mod_5 > 5 then
+ mod_5 = 1
+ insertion = nodecopy(current_space_node)
+ insertion.next, insert_glyph.prev = insert_glyph, insertion
+ end
+ if head == n then --> replace head
+ local succ = head.next
+ if succ then
+ insert_glyph.next, succ.prev = succ, insert_glyph
+ end
+ head = insertion
+ else --> replace n
+ local pred, succ = n.prev, n.next
+ pred.next, insertion.prev = insertion, pred
+ if succ then
+ insert_glyph.next, succ.prev = succ, insert_glyph
+ end
+ end
+
+ --- insertion becomes the new head
+ return head, insert_glyph -- so we know where to insert
+ end
+ else
+
+ insert_encoded = function (head, n, replacement)
+ local insertion = nodecopy(n)
+ if replacement then
+ insertion.char = utf8byte(replacement)
+ end
+ if head == n then
+ local succ = head.next
+ if succ then
+ insertion.next, succ.prev = succ, insertion
+ end
+ head = insertion
+ else
+ nodeinsert_before(head, n, insertion)
+ noderemove(head, n)
+ end
+ return head, insertion
+ end
+ end
+
+ --- The callback proper starts here.
+ local aux aux = function (head, recurse)
+ if recurse == nil then recurse = 0 end
+ for n in nodetraverse(head) do
+ local nid = n.id
+ --print(utf8char(n.char), n)
+ if nid == GLYPH_NODE then
+ local chr = utf8char(n.char)
+ --print(chr, n)
+ local replacement = machine:encode(chr)
+ --print(chr, replacement, n)
+ local treplacement = replacement and type(replacement)
+ --if replacement == false then
+ if not replacement then
+ noderemove(head, n)
+ elseif treplacement == "string" then
+ --print(head, n, replacement)
+ head, _ = insert_encoded(head, n, replacement)
+ elseif treplacement == "table" then
+ local current = n
+ for i=1, #replacement do
+ head, current = insert_encoded(head, current, replacement[i])
+ end
+ end
+ elseif nid == GLUE_NODE then
+ if n.subtype ~= 15 then -- keeping the parfillskip
+ noderemove(head, n)
+ end
+ elseif IGNORE_NODES[nid] then
+ -- drop spaces and kerns
+ noderemove(head, n)
+ elseif nid == DISC_NODE then
+ --- ligatures need to be resolved if they are characters
+ local npre, npost = n.pre, n.post
+ if nodeid(npre) == GLYPH_NODE and
+ nodeid(npost) == GLYPH_NODE then
+ if npre.char and npost.char then -- ligature between glyphs
+ local replacement_pre = machine:encode(utf8char(npre.char))
+ local replacement_post = machine:encode(utf8char(npost.char))
+ insert_encoded(head, npre, replacement_pre)
+ insert_encoded(head, npost, replacement_post)
+ else -- hlists or whatever
+ -- pass
+ --noderemove(head, npre)
+ --noderemove(head, npost)
+ end
+ end
+ noderemove(head, n)
+ elseif nid == HLIST_NODE or nid == VLIST_NODE then
+ if nodelength(n.list) > 0 then
+ n.list = aux(n.list, recurse + 1)
+ end
+-- else
+-- -- TODO other node types
+-- print(n)
+ end
+ end
+ nodeslide(head)
+ return head
+ end -- callback auxiliary
+
+ --- Context requires
+ --- × argument shuffling; a properly registered “action” gets the
+ --- head passed as its third argument
+ --- × hacking our way around the coupling of pre_linebreak_filter
+ --- and hpack_filter; background:
+ --- http://www.ntg.nl/pipermail/ntg-context/2012/067779.html
+ local cbk = function (a, _, c)
+ local head
+ current_space_node = generate_space ()
+ mod_5 = 0
+ if format_is_context == true then
+ head = c
+ local cbk_env = get_debug_info(4) -- no getenv in lua 5.2
+ --inspect(cbk_env)
+ if cbk_env.func == nodes.processors.pre_linebreak_filter then
+ -- how weird is that?
+ return aux(head)
+ end
+ return head
+ end
+ head = a
+ return aux(head)
+ end
+
+ if format_is_context then
+ local cbk_id = "enigma_" .. name
+ enigma.callbacks[name] = nodesinstallattributehandler{
+ name = cbk_id,
+ namespace = thirddata.enigma,
+ processor = cbk,
+ }
+ local cbk_location = "thirddata.enigma.callbacks." .. name
+ nodestasksappendaction("processors",
+ --"characters",
+ --"finalizers",
+ --- this one is tagged “for users”
+ --- (cf. node-tsk.lua)
+ "before",
+ cbk_location)
+ nodestasksdisableaction("processors", cbk_location)
+ else
+ enigma.callbacks[name] = cbk
+ end
+end
+
+--[[ichd--
+\startparagraph
+Enigma\reference[listing:retrieve]{} machines can be copied and derived
+from one another at will, cf. the \texmacro{defineenigma} on
+\at{page}[listing:define]. Two helper functions residing inside the
+\identifier{thirddata.enigma} namespace take care of these actions:
+\luafunction{save_raw_args} and \luafunction{retrieve_raw_args}. As
+soon as a machine is defined, we store its parsed options inside the
+table \identifier{configurations} for later reference. For further
+details on the machine derivation mechanism see
+\at{page}[listing:inherit].
+\stopparagraph
+--ichd]]--
+local configurations = { }
+
+local save_raw_args = function (conf, name)
+ local current = configurations[name] or { }
+ for k, v in next, conf do
+ current[k] = v
+ end
+ configurations[name] = current
+end
+
+local retrieve_raw_args = function (name)
+ local cn = configurations[name]
+ return cn and tablecopy(cn) or { }
+end
+
+enigma.save_raw_args = save_raw_args
+enigma.retrieve_raw_args = retrieve_raw_args
+
+
+--[[ichd--
+\startparagraph
+The function \luafunction{new_machine} instantiates a table containing
+the complete specification of a workable \emph{Enigma} machine and
+other metadata. The result is intended to be handed over to the
+callback creation mechanism (\luafunction{new_callback}). However, the
+arguments table is usally stored away in the
+\identifier{thirddata.enigma} namespace anyway
+(\luafunction{save_raw_args}), so that the specification of any machine
+can be inherited by some new setup later on.
+\stopparagraph
+--ichd]]--
+local new_machine = function (name)
+ local args = configurations[name]
+ --table.print(configurations)
+ verbose_level = args and args.verbose or verbose_level
+ local machine = new(name, args)
+ return machine
+end
+
+enigma.new_machine = new_machine
+enigma.new_callback = new_callback
+
+--[[ichd--
+\stopdocsection
+--ichd]]--
+
+-- vim:ft=lua:sw=2:ts=2:tw=71:expandtab