commit 17fd40322042797c15ced8c4e94d2f9b40601df1
Author: Martin Ashby <martin@ashbysoft.com>
Date: Sat, 24 Oct 2026 23:43:13 +0100
Initial commit
Diffstat:
6 files changed, 744 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+*.beam
+*.ez
+/build
+erl_crash.dump
diff --git a/README.md b/README.md
@@ -0,0 +1,15 @@
+# solitare
+
+A gleam implementation of Solitare encryption scheme
+
+https://www.schneier.com/academic/solitaire/
+
+Done as a learning exercise for learning gleam
+
+
+## Development
+
+```sh
+gleam run # Run the project
+gleam test # Run the tests
+```
diff --git a/gleam.toml b/gleam.toml
@@ -0,0 +1,20 @@
+name = "solitare"
+version = "1.0.0"
+
+# Fill out these fields if you intend to generate HTML documentation or publish
+# your project to the Hex package manager.
+#
+# description = ""
+# licences = ["Apache-2.0"]
+# repository = { type = "github", user = "", repo = "" }
+# links = [{ title = "Website", href = "" }]
+#
+# For a full reference of all the available options, you can have a look at
+# https://gleam.run/writing-gleam/gleam-toml/.
+
+[dependencies]
+gleam_stdlib = ">= 1.0.0 and < 2.0.0"
+argv = ">= 1.1.0 and < 2.0.0"
+
+[dev_dependencies]
+gleeunit = ">= 1.0.0 and < 2.0.0"
diff --git a/manifest.toml b/manifest.toml
@@ -0,0 +1,18 @@
+# Do not manually edit this file, it is managed by Gleam.
+#
+# This file locks the dependency versions used, to make your build
+# deterministic and to prevent unexpected versions from being included
+# in your application.
+#
+# You should check this file into your source control repository.
+
+packages = [
+ { name = "argv", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "3277D100448BDB4A29B6D58C0F36F631CBC349E8BDD09766C6309DF202831140" },
+ { name = "gleam_stdlib", version = "1.0.5", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "CEE5B6C076A85B45F60C585F4316C63EC8B7127C119D5738C3958A9C4D50404E" },
+ { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" },
+]
+
+[requirements]
+argv = { version = ">= 1.1.0 and < 2.0.0" }
+gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" }
+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
diff --git a/src/solitare.gleam b/src/solitare.gleam
@@ -0,0 +1,482 @@
+import gleam/string
+import gleam/list
+import gleam/result
+import gleam/option.{type Option,Some,None}
+import gleam/io
+import gleam/int
+import argv
+
+pub type NewCardError {
+ InvalidCard
+}
+pub type Suit {
+ Clubs
+ Diamonds
+ Hearts
+ Spades
+ JokerA
+ JokerB
+}
+pub type Card {
+ Card(suit: Suit, value: Option(Int))
+}
+
+pub fn new_card(suit: Suit, value: Option(Int)) -> Result(Card, NewCardError) {
+ case suit {
+ Clubs | Diamonds | Hearts | Spades -> case value {
+ Some(value) -> case value {
+ x if x < 1 || x > 13 -> Error(InvalidCard)
+ y -> Ok(Card(suit, Some(y)))
+ }
+ None -> Error(InvalidCard)
+ }
+ JokerA | JokerB -> case value {
+ Some(_a) -> Error(InvalidCard)
+ None -> Ok(Card(suit, None))
+ }
+ }
+}
+
+pub fn unkeyed_deck() -> List(Card) {
+ let makesuit = fn (s: Suit) -> List(Result(Card,NewCardError)) {
+ list.reverse(int.range(1, 14, [], fn (lst, value) {
+ [new_card(s, Some(value)), ..lst]
+ }))
+ }
+ let assert Ok(ud) = result.all(list.flatten([
+ makesuit(Clubs),
+ makesuit(Diamonds),
+ makesuit(Hearts),
+ makesuit(Spades),
+ [new_card(JokerA, None), new_card(JokerB, None)]
+ ]))
+ ud
+}
+
+pub fn count_cut(deck: List(Card)) -> Result(List(Card),DeckError) {
+ let len = list.length(deck)
+ case len {
+ 0 -> Error(MalformedDeck)
+ _ -> {
+ let #(rest, b) = list.split(deck, len-1)
+ let assert Ok(bottom_card) = list.first(b)
+ let count = count_cut_value(bottom_card)
+ let #(top, bottom) = list.split(rest, count)
+ let res = list.append(bottom, top)
+ |> list.append([bottom_card])
+ Ok(res)
+ }
+ }
+}
+
+// (Use the bridge order of suits: clubs, diamonds, hearts, and spades.
+// If the card is a club, it is the value shown. If the card is a diamond,
+// it is the value plus 13. If it is a heart, it is the value plus 26.
+// If it is a spade, it is the value plus 39. Either joker is a 53.)
+pub fn count_cut_value(card: Card) -> Int {
+ case card.suit {
+ Clubs | Diamonds | Hearts | Spades -> {
+ let assert Some(value) = card.value
+ case card.suit {
+ Clubs -> value
+ Diamonds -> 13 + value
+ Hearts -> 26 + value
+ Spades -> 39 + value
+ JokerA | JokerB -> panic
+ }
+ }
+ JokerA | JokerB -> 53
+ }
+}
+
+pub type DeckError{
+ MalformedDeck
+}
+
+pub fn triple_cut(deck: List(Card)) -> Result(List(Card), DeckError) {
+ let jokers = list.index_map(deck, fn (card, ix) {
+ case is_joker(card) {
+ True -> [ix]
+ False -> []
+ }
+ }) |> list.flatten
+ case list.length(jokers) {
+ 2 -> {
+ let assert Ok(j1) = list.first(jokers)
+ let assert Ok(j2) = list.last(jokers)
+ let #(top, rest) = list.split(deck, j1)
+ let #(mid, bottom) = list.split(rest, j2-j1+1)
+ Ok(list.append(list.append(bottom, mid), top))
+ }
+ _ -> Error(MalformedDeck)
+ }
+}
+
+fn is_joker(card: Card) -> Bool {
+ card.suit == JokerA || card.suit == JokerB
+}
+
+pub fn move_jokers(deck: List(Card)) -> Result(List(Card),DeckError) {
+ move_joker_a(deck)
+ |> result.map(fn (d) {move_joker_b(d)})
+ |> result.flatten()
+}
+
+// joker A moves back one
+pub fn move_joker_a(deck: List(Card)) -> Result(List(Card),DeckError) {
+ let #(pre, suffix) = list.split_while(deck, fn (card) { card.suit != JokerA })
+ let #(jokera, post) = list.split(suffix, 1)
+ case list.length(jokera) {
+ 1 -> case list.length(post) {
+ 0 -> {
+ let #(a, b) = list.split(pre, 1)
+ let res = list.append(a, [Card(JokerA, None)])
+ |> list.append(b)
+ Ok(res)
+ }
+ _ -> {
+ let #(a, b) = list.split(post, 1)
+ let res = list.append(pre, a)
+ |> list.append([Card(JokerA, None)])
+ |> list.append(b)
+ Ok(res)
+ }
+ }
+ _ -> Error(MalformedDeck)
+ }
+}
+
+// joker B moves back two
+pub fn move_joker_b(deck: List(Card)) -> Result(List(Card),DeckError) {
+ let #(pre, suffix) = list.split_while(deck, fn (card) { card.suit != JokerB})
+ let #(jokerb, post) = list.split(suffix, 1)
+ case list.length(jokerb) {
+ 1 -> case list.length(post) {
+ 0 -> {
+ let #(a, b) = list.split(pre, 2)
+ let res = list.append(list.append(a, [Card(JokerB, None)]), b)
+ Ok(res)
+ }
+ 1 -> {
+ let #(a, b) = list.split(pre, 1)
+ let res = list.append(a, [Card(JokerB, None)])
+ |> list.append(b)
+ |> list.append(post)
+ Ok(res)
+ }
+ _ -> {
+ let #(a, b) = list.split(post, 2)
+ let res = list.append(pre, a)
+ |> list.append([Card(JokerB, None)])
+ |> list.append(b)
+ Ok(res)
+ }
+ }
+ _ -> Error(MalformedDeck)
+ }
+}
+
+// it might return None if the output is a joker card, in which case
+// this output must be skipped
+pub fn read_output(deck: List(Card)) -> Result(Option(Int), DeckError) {
+ case list.first(deck) {
+ Ok(first) -> {
+ let val = count_cut_value(first)
+ case at(deck, val) {
+ Some(out_card) -> Ok(output_value(out_card))
+ None -> Error(MalformedDeck)
+ }
+ }
+ Error(_) -> Error(MalformedDeck)
+ }
+}
+
+fn output_value(card: Card) -> Option(Int) {
+ option.map(card.value, fn (value) {
+ case card.suit {
+ Clubs | Hearts -> value
+ Diamonds | Spades -> 13 + value
+ _ -> panic // unreachable, other suits have no value
+ }
+ })
+}
+
+
+pub fn generate_keystream(deck: List(Card), length: Int) -> Result(#(List(Card), List(Int)),DeckError) {
+ case generate_keystream_internal(deck, length, []) {
+ Ok(#(updated_deck, keystream)) -> Ok(#(updated_deck, list.reverse(keystream)))
+ Error(e) -> Error(e)
+ }
+}
+
+// Keystream is returned _reversed_ because prepending is cheaper right now
+fn generate_keystream_internal(deck: List(Card), length: Int, ks: List(Int)) -> Result(#(List(Card), List(Int)), DeckError) {
+ case length {
+ 0 -> Ok(#(deck, ks))
+ _ -> {
+ let updated_deck = move_jokers(deck)
+ |> flat_map(triple_cut)
+ |> flat_map(count_cut)
+ case updated_deck {
+ Ok(ud) -> {
+ case read_output(ud) {
+ Ok(out) -> case out {
+ Some(value) -> generate_keystream_internal(ud, length-1, [value, ..ks])
+ None -> generate_keystream_internal(ud, length, ks)
+ }
+ Error(e) -> Error(e)
+ }
+ }
+ Error(e) -> Error(e)
+ }
+ }
+ }
+
+}
+
+// TODO use a better data structure than List (which is a singly-linked list)
+fn at(l: List(a), i: Int) -> Option(a) {
+ list.index_fold(l, None, fn (acc, e, ix) {
+ case acc {
+ Some(g) -> Some(g)
+ None -> case ix == i {
+ True -> Some(e)
+ False -> None
+ }
+ }
+ })
+}
+
+
+fn flat_map(r: Result(a, b), f: fn (a) -> Result(c, b)) -> Result(c, b) {
+ case r {
+ Ok(x) -> case f(x) {
+ Ok(y) -> Ok(y)
+ Error(z) -> Error(z)
+ }
+ Error(z) -> Error(z)
+ }
+}
+
+
+pub type ToNumberError {
+ InvalidString
+}
+
+fn encode(s: String) -> Result(Int, ToNumberError) {
+ case string.uppercase(s) {
+ "A" -> Ok(1)
+ "B" -> Ok(2)
+ "C" -> Ok(3)
+ "D" -> Ok(4)
+ "E" -> Ok(5)
+ "F" -> Ok(6)
+ "G" -> Ok(7)
+ "H" -> Ok(8)
+ "I" -> Ok(9)
+ "J" -> Ok(10)
+ "K" -> Ok(11)
+ "L" -> Ok(12)
+ "M" -> Ok(13)
+ "N" -> Ok(14)
+ "O" -> Ok(15)
+ "P" -> Ok(16)
+ "Q" -> Ok(17)
+ "R" -> Ok(18)
+ "S" -> Ok(19)
+ "T" -> Ok(20)
+ "U" -> Ok(21)
+ "V" -> Ok(22)
+ "W" -> Ok(23)
+ "X" -> Ok(24)
+ "Y" -> Ok(25)
+ "Z" -> Ok(26)
+ _ -> Error(InvalidString)
+ }
+}
+fn decode(i: Int) -> Result(String,ToNumberError) {
+ case i {
+ 1 -> Ok("A")
+ 2 -> Ok("B")
+ 3 -> Ok("C")
+ 4 -> Ok("D")
+ 5 -> Ok("E")
+ 6 -> Ok("F")
+ 7 -> Ok("G")
+ 8 -> Ok("H")
+ 9 -> Ok("I")
+ 10 -> Ok("J")
+ 11 -> Ok("K")
+ 12 -> Ok("L")
+ 13 -> Ok("M")
+ 14 -> Ok("N")
+ 15 -> Ok("O")
+ 16 -> Ok("P")
+ 17 -> Ok("Q")
+ 18 -> Ok("R")
+ 19 -> Ok("S")
+ 20 -> Ok("T")
+ 21 -> Ok("U")
+ 22 -> Ok("V")
+ 23 -> Ok("W")
+ 24 -> Ok("X")
+ 25 -> Ok("Y")
+ 26 -> Ok("Z")
+ _ -> Error(InvalidString)
+ }
+}
+
+pub type EncryptError {
+ EEMalformedDeck
+ EEInvalidString
+ EEKeystreamError
+}
+fn map_de(de: DeckError) -> EncryptError {
+ case de {
+ MalformedDeck -> EEMalformedDeck
+ }
+}
+fn map_ne(ne: ToNumberError) -> EncryptError {
+ case ne {
+ InvalidString -> EEInvalidString
+ }
+}
+
+// Encrypt with a pre-keyed deck
+pub fn encrypt(deck: List(Card), plaintext: String) -> Result(String, EncryptError) {
+ crypt(deck, plaintext, enc)
+}
+
+// Decrypt with a pre-keyed deck
+pub fn decrypt(deck: List(Card), ciphertext: String) -> Result(String,EncryptError) {
+ crypt(deck, ciphertext, dec)
+}
+
+fn crypt(deck: List(Card), text: String, f: fn (Int, Int) -> Int) -> Result(String,EncryptError) {
+ case string.to_graphemes(text)
+ |> pad("X", 5)
+ |> list.map(encode)
+ |> result.all() {
+ Ok(encoded) -> {
+ case generate_keystream(deck, list.length(encoded)) {
+ Ok(#(_updated_deck, keystream)) -> {
+ case list.strict_zip(encoded, keystream) {
+ Ok(zipped) -> {
+ case list.map(zipped, fn (zz) {
+ let #(e, k) = zz
+ f(e, k)
+ })
+ |> list.map(decode)
+ |> result.all() {
+ Ok(decode_l) -> Ok(string.join(decode_l, ""))
+ Error(e) -> Error(map_ne(e))
+ }
+ }
+ Error(_) -> Error(EEKeystreamError)
+ }
+ }
+ Error(e) -> Error(map_de(e))
+ }
+ }
+ Error(e) -> Error(map_ne(e))
+ }
+}
+
+fn pad(lst: List(String), p: String, x: Int) -> List(String) {
+ let len = list.length(lst)
+ case {len % x} {
+ 0 -> lst
+ x -> list.append(lst, list.repeat(p, 5 - x))
+ }
+}
+
+fn enc(text: Int, key: Int) -> Int {
+ case text + key {
+ x if x > 26 -> x - 26
+ x -> x
+ }
+}
+fn dec(text: Int, key: Int) -> Int {
+ case text - key {
+ x if x < 1 -> x + 26
+ x -> x
+ }
+}
+
+pub fn passphrase_key(passphrase: String) -> Result(List(Card), EncryptError) {
+ string.to_graphemes(passphrase)
+ |> passphrase_key_internal(unkeyed_deck())
+}
+
+fn passphrase_key_internal(passphrase_l: List(String), deck: List(Card)) -> Result(List(Card), EncryptError) {
+ case list.length(passphrase_l) {
+ 0 -> Ok(deck)
+ _ -> {
+ case list.first(passphrase_l) {
+ Ok(ch) -> {
+ case encode(ch) {
+ Ok(enc) -> {
+ case move_jokers(deck)
+ |> flat_map(triple_cut)
+ |> flat_map(count_cut)
+ |> flat_map(fn (d) {count_cut_x(d, enc)}) {
+ Ok(updated_deck) -> {
+ let #(_, rest) = list.split(passphrase_l, 1)
+ passphrase_key_internal(rest, updated_deck)
+ }
+ Error(e) -> Error(map_de(e))
+ }
+ }
+ Error(e) -> Error(map_ne(e))
+ }
+ }
+ Error(_) -> panic
+ }
+ }
+ }
+}
+
+fn count_cut_x(deck: List(Card), x: Int) -> Result(List(Card),DeckError) {
+ let len = list.length(deck)
+ case len {
+ 0 -> Error(MalformedDeck)
+ _ -> {
+ let #(rest, b) = list.split(deck, len-1)
+ let assert Ok(bottom_card) = list.first(b)
+ let #(top, bottom) = list.split(rest, x)
+ let res = list.append(bottom, top)
+ |> list.append([bottom_card])
+ Ok(res)
+ }
+ }
+}
+
+pub fn encrypt_key(key: String, plaintext: String) -> Result(String,EncryptError) {
+ passphrase_key(key)
+ |> flat_map(fn (deck){ encrypt(deck, plaintext) })
+}
+
+pub fn decrypt_key(key: String, ciphertext: String) -> Result(String,EncryptError) {
+ passphrase_key(key)
+ |> flat_map(fn (deck){ decrypt(deck, ciphertext) })
+}
+
+pub fn main() -> Nil {
+ case argv.load().arguments {
+ ["encrypt", key, plaintext] -> {
+ case encrypt_key(key, plaintext) {
+ Ok(ciphertext) -> io.println(ciphertext)
+ Error(e) -> io.println_error("error encrypting: " <> string.inspect(e))
+ }
+ }
+ ["decrypt", key, ciphertext] -> {
+ case decrypt_key(key, ciphertext) {
+ Ok(plaintext) -> io.println(plaintext)
+ Error(e) -> io.println_error("error decrypting: " <> string.inspect(e))
+ }
+ }
+ _ -> io.println("usage: solitare (encrypt|decrypt) key (cipher|plain)text")
+
+ }
+ Nil
+}
+\ No newline at end of file
diff --git a/test/solitare_test.gleam b/test/solitare_test.gleam
@@ -0,0 +1,204 @@
+import gleam/int
+import gleam/list
+import gleam/option.{Some,None}
+import solitare.{type Card,Card,JokerA,JokerB,Clubs,Hearts,Diamonds,Spades,InvalidCard,new_card,unkeyed_deck,count_cut,count_cut_value,triple_cut,move_joker_a,move_joker_b,move_jokers,read_output,encrypt,decrypt,passphrase_key}
+import gleeunit
+import gleam/string
+
+pub fn main() -> Nil {
+ gleeunit.main()
+}
+
+pub fn new_card_test() {
+ let cases = [
+ #(new_card(JokerA, Some(1)), Error(InvalidCard)),
+ #(new_card(JokerB, Some(1)), Error(InvalidCard)),
+ #(new_card(JokerA, None), Ok(Card(JokerA, None))),
+ #(new_card(JokerB, None), Ok(Card(JokerB, None))),
+ #(new_card(Clubs, Some(1)), Ok(Card(Clubs, Some(1)))),
+ #(new_card(Clubs, Some(13)), Ok(Card(Clubs, Some(13)))),
+ #(new_card(Clubs, None), Error(InvalidCard)),
+ #(new_card(Clubs, Some(0)), Error(InvalidCard)),
+ #(new_card(Clubs, Some(14)), Error(InvalidCard)),
+ #(new_card(Clubs, Some(14)), Error(InvalidCard)),
+ #(new_card(Diamonds, Some(1)), Ok(Card(Diamonds, Some(1)))),
+ #(new_card(Hearts, Some(1)), Ok(Card(Hearts, Some(1)))),
+ #(new_card(Spades, Some(1)), Ok(Card(Spades, Some(1)))),
+ ]
+ list.each(cases, fn (tup) {
+ let actual = tup.0
+ let expected = tup.1
+ assert actual == expected
+ })
+}
+
+pub fn unkeyed_deck_test() {
+ let ud = unkeyed_deck()
+ assert list.first(ud) == Ok(Card(Clubs, Some(1)))
+ assert list.last(ud) == Ok(Card(JokerB, None))
+}
+
+pub fn count_cut_value_test() {
+ let cases = [
+ #(count_cut_value(Card(JokerB,None)), 53),
+ #(count_cut_value(Card(JokerA,None)), 53),
+ #(count_cut_value(Card(Clubs,Some(12))), 12),
+ #(count_cut_value(Card(Diamonds,Some(5))), 18),
+ #(count_cut_value(Card(Hearts,Some(3))), 29),
+ #(count_cut_value(Card(Spades,Some(13))), 52)
+ ]
+ list.each(cases, fn (tup) {
+ assert tup.0 == tup.1
+ })
+}
+
+pub fn count_cut_test() {
+ let cases = [
+ #([1, 4, 2, 7, 6, 3], [7, 6, 1, 4, 2, 3])
+ ]
+ list.each(cases, fn (cas) {
+ let in = make_clubs_deck(cas.0)
+ let exp = make_clubs_deck(cas.1)
+ let act = count_cut(in)
+ assert act == Ok(exp)
+ })
+}
+
+fn make_clubs_deck(lst: List(Int)) -> List(Card) {
+ list.map(lst, fn (i) {
+ Card(Clubs, Some(i))
+ })
+}
+
+pub fn triple_cut_test() {
+ let cases = [
+ #("B 5 8 7 1 A 3 9", "3 9 B 5 8 7 1 A"),
+ #("4 B 5 8 7 1 A 3 9", "3 9 B 5 8 7 1 A 4"),
+ #("B 5 8 7 1 A", "B 5 8 7 1 A"),
+ #("3 9 B 5 8 7 1 A", "B 5 8 7 1 A 3 9"),
+ ]
+ list.each(cases, fn (cas) {
+ let #(in_s, exp_s) = cas
+ let in = make_clubs_deck_s(in_s)
+ let exp = make_clubs_deck_s(exp_s)
+ let assert Ok(act) = triple_cut(in)
+ assert act == exp
+ })
+}
+
+fn make_clubs_deck_s(str: String) -> List(Card) {
+ string.split(str, " ")
+ |> list.map(fn (s) {
+ case int.parse(s) {
+ Ok(value) -> Card(Clubs, Some(value))
+ _ -> {
+ case s {
+ "A" -> Card(JokerA, None)
+ "B" -> Card(JokerB, None)
+ _ -> panic
+ }
+ }
+ }
+ })
+}
+
+pub fn move_joker_a_test() {
+ let cases = [
+ #("B 5 8 7 1 A 3 9", "B 5 8 7 1 3 A 9"),
+ #("B 5 8 7 1 3 A 9", "B 5 8 7 1 3 9 A"),
+ #("B 5 8 7 1 3 9 A", "B A 5 8 7 1 3 9"),
+ ]
+ list.each(cases, fn (cas) {
+ let #(in_s, exp_s) = cas
+ let in = make_clubs_deck_s(in_s)
+ let exp = make_clubs_deck_s(exp_s)
+ let act = move_joker_a(in)
+ assert act == Ok(exp)
+ })
+}
+
+
+pub fn move_joker_b_test() {
+ let cases = [
+ #("B 5 8 7 1 A 3 9", "5 8 B 7 1 A 3 9"),
+ #("2 5 8 7 1 B 3 9", "2 5 8 7 1 3 9 B"),
+ #("2 5 8 7 1 3 B 9", "2 B 5 8 7 1 3 9"),
+ #("2 5 8 7 1 3 9 B", "2 5 B 8 7 1 3 9"),
+ ]
+ list.each(cases, fn (cas) {
+ let #(in_s, exp_s) = cas
+ let in = make_clubs_deck_s(in_s)
+ let exp = make_clubs_deck_s(exp_s)
+ let act = move_joker_b(in)
+ assert act == Ok(exp)
+ })
+}
+
+pub fn move_jokers_test() {
+ let cases = [
+ #("B 5 8 7 1 A 3 9", "5 8 B 7 1 3 A 9"),
+ #("3 A B 8 9 6", "3 A 8 B 9 6"),
+ ]
+ list.each(cases, fn (cas) {
+ let #(in_s, exp_s) = cas
+ let in = make_clubs_deck_s(in_s)
+ let exp = make_clubs_deck_s(exp_s)
+ let act = move_jokers(in)
+ assert act == Ok(exp)
+ })
+}
+
+pub fn read_output_test() {
+ let cases = [
+ #("3 5 8 7 1 A 3 9", Some(7)),
+ #("3 4 A B 9 6", None),
+ ]
+ list.each(cases, fn (cas) {
+ let #(in_s, exp) = cas
+ let in = make_clubs_deck_s(in_s)
+ let act = read_output(in)
+ assert act == Ok(exp)
+ })
+}
+
+// test case in the doc is wrong as it isn't modulo 26
+//pub fn generate_keystream_test() {
+// let deck = unkeyed_deck()
+// let assert Ok(#(_updated_deck, keystream)) = generate_keystream(deck, 10)
+// assert keystream == [4, 49, 10, 24, 8, 51, 44, 6, 4, 33]
+//}
+
+
+pub fn encrypt_test() {
+ let deck = unkeyed_deck()
+ let act = encrypt(deck, "AAAAAAAAAA")
+ assert act == Ok("EXKYIZSGEH")
+}
+
+pub fn decrypt_test() {
+ let deck = unkeyed_deck()
+ let act = decrypt(deck, "EXKYIZSGEH")
+ assert act == Ok("AAAAAAAAAA")
+}
+
+pub fn derive_key_test() {
+ let assert Ok(deck) = passphrase_key("FOO")
+ let act = encrypt(deck, "AAAAAAAAAAAAAAA")
+ assert act == Ok("ITHZUJIWGRFARMW")
+}
+
+pub fn derive_key_2_test() {
+ let assert Ok(deck) = passphrase_key("CRYPTONOMICON")
+ let act = encrypt(deck, "SOLITAIRE")
+ assert act == Ok("KIRAKSFJAN")
+}
+
+
+
+
+
+
+
+
+
+