From fcb418202c6cfffd12c2b21350d964f2d0f46784 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Thu, 10 Sep 2026 13:31:34 +1000 Subject: [PATCH] Add `simple-cipher` exercise --- config.json | 8 + .../simple-cipher/.docs/instructions.md | 40 +++++ .../practice/simple-cipher/.meta/config.json | 19 ++ .../practice/simple-cipher/.meta/example.zig | 51 ++++++ .../practice/simple-cipher/.meta/tests.toml | 46 +++++ .../practice/simple-cipher/simple_cipher.zig | 44 +++++ .../simple-cipher/test_simple_cipher.zig | 169 ++++++++++++++++++ generators/exercises/simple_cipher.py | 99 ++++++++++ 8 files changed, 476 insertions(+) create mode 100644 exercises/practice/simple-cipher/.docs/instructions.md create mode 100644 exercises/practice/simple-cipher/.meta/config.json create mode 100644 exercises/practice/simple-cipher/.meta/example.zig create mode 100644 exercises/practice/simple-cipher/.meta/tests.toml create mode 100644 exercises/practice/simple-cipher/simple_cipher.zig create mode 100644 exercises/practice/simple-cipher/test_simple_cipher.zig create mode 100644 generators/exercises/simple_cipher.py diff --git a/config.json b/config.json index 7e4095a1..76e320ff 100644 --- a/config.json +++ b/config.json @@ -1035,6 +1035,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "simple-cipher", + "name": "Simple Cipher", + "uuid": "3d08afb9-5be4-4195-ae47-3015598de1a6", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "spiral-matrix", "name": "Spiral Matrix", diff --git a/exercises/practice/simple-cipher/.docs/instructions.md b/exercises/practice/simple-cipher/.docs/instructions.md new file mode 100644 index 00000000..afd0b57d --- /dev/null +++ b/exercises/practice/simple-cipher/.docs/instructions.md @@ -0,0 +1,40 @@ +# Instructions + +Create an implementation of the [Vigenère cipher][wiki]. +The Vigenère cipher is a simple substitution cipher. + +## Cipher terminology + +A cipher is an algorithm used to encrypt, or encode, a string. +The unencrypted string is called the _plaintext_ and the encrypted string is called the _ciphertext_. +Converting plaintext to ciphertext is called _encoding_ while the reverse is called _decoding_. + +In a _substitution cipher_, each plaintext letter is replaced with a ciphertext letter which is computed with the help of a _key_. +(Note, it is possible for replacement letter to be the same as the original letter.) + +## Encoding details + +In this cipher, the key is a series of lowercase letters, such as `"abcd"`. +Each letter of the plaintext is _shifted_ or _rotated_ by a distance based on a corresponding letter in the key. +An `"a"` in the key means a shift of 0 (that is, no shift). +A `"b"` in the key means a shift of 1. +A `"c"` in the key means a shift of 2, and so on. + +The first letter of the plaintext uses the first letter of the key, the second letter of the plaintext uses the second letter of the key and so on. +If you run out of letters in the key before you run out of letters in the plaintext, start over from the start of the key again. + +If the key only contains one letter, such as `"dddddd"`, then all letters of the plaintext are shifted by the same amount (three in this example), which would make this the same as a rotational cipher or shift cipher (sometimes called a Caesar cipher). +For example, the plaintext `"iamapandabear"` would become `"ldpdsdqgdehdu"`. + +If the key only contains the letter `"a"` (one or more times), the shift distance is zero and the ciphertext is the same as the plaintext. + +Usually the key is more complicated than that, though! +If the key is `"abcd"` then letters of the plaintext would be shifted by a distance of 0, 1, 2, and 3. +If the plaintext is `"hello"`, we need 5 shifts so the key would wrap around, giving shift distances of 0, 1, 2, 3, and 0. +Applying those shifts to the letters of `"hello"` we get `"hfnoo"`. + +## Random keys + +If no key is provided, generate a key which consists of at least 100 random lowercase letters from the Latin alphabet. + +[wiki]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher diff --git a/exercises/practice/simple-cipher/.meta/config.json b/exercises/practice/simple-cipher/.meta/config.json new file mode 100644 index 00000000..d2ed4811 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "simple_cipher.zig" + ], + "test": [ + "test_simple_cipher.zig" + ], + "example": [ + ".meta/example.zig" + ] + }, + "blurb": "Implement the Vigenère cipher, a simple substitution cipher.", + "source": "Substitution Cipher at Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Substitution_cipher" +} diff --git a/exercises/practice/simple-cipher/.meta/example.zig b/exercises/practice/simple-cipher/.meta/example.zig new file mode 100644 index 00000000..673ae106 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/example.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const mem = std.mem; + +pub const Cipher = struct { + key: []const u8, + + /// Initializes a cipher with a copy of the given key. + pub fn init(allocator: mem.Allocator, key: []const u8) mem.Allocator.Error!Cipher { + return .{ + .key = try allocator.dupe(u8, key), + }; + } + + /// Initializes a cipher with a randomly generated key of at least 100 + /// lowercase letters. + pub fn initRandom(allocator: mem.Allocator, random: std.Random) mem.Allocator.Error!Cipher { + const key = try allocator.alloc(u8, 100); + for (key) |*item| { + item.* = random.intRangeAtMost(u8, 'a', 'z'); + } + + return .{ + .key = key, + }; + } + + /// Frees the key. + pub fn deinit(self: *Cipher, allocator: mem.Allocator) void { + allocator.free(self.key); + } + + /// Encodes `plaintext`. Caller owns the returned memory. + pub fn encode(self: Cipher, allocator: mem.Allocator, plaintext: []const u8) mem.Allocator.Error![]u8 { + return process(self, allocator, plaintext, 1); + } + + /// Decodes `ciphertext`. Caller owns the returned memory. + pub fn decode(self: Cipher, allocator: mem.Allocator, ciphertext: []const u8) mem.Allocator.Error![]u8 { + return process(self, allocator, ciphertext, -1); + } + + fn process(self: Cipher, allocator: mem.Allocator, plaintext: []const u8, direction: i32) mem.Allocator.Error![]u8 { + const result = try allocator.alloc(u8, plaintext.len); + for (plaintext, 0..) |c, i| { + const shift: i32 = self.key[i % self.key.len] - 'a'; + result[i] = @intCast(@mod(c - 'a' + (shift * direction), 26) + 'a'); + } + + return result; + } +}; diff --git a/exercises/practice/simple-cipher/.meta/tests.toml b/exercises/practice/simple-cipher/.meta/tests.toml new file mode 100644 index 00000000..77e6571e --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/tests.toml @@ -0,0 +1,46 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[b8bdfbe1-bea3-41bb-a999-b41403f2b15d] +description = "Random key cipher -> Can encode" + +[3dff7f36-75db-46b4-ab70-644b3f38b81c] +description = "Random key cipher -> Can decode" + +[8143c684-6df6-46ba-bd1f-dea8fcb5d265] +description = "Random key cipher -> Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" + +[defc0050-e87d-4840-85e4-51a1ab9dd6aa] +description = "Random key cipher -> Key is made only of lowercase letters" + +[565e5158-5b3b-41dd-b99d-33b9f413c39f] +description = "Substitution cipher -> Can encode" + +[d44e4f6a-b8af-4e90-9d08-fd407e31e67b] +description = "Substitution cipher -> Can decode" + +[70a16473-7339-43df-902d-93408c69e9d1] +description = "Substitution cipher -> Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" + +[69a1458b-92a6-433a-a02d-7beac3ea91f9] +description = "Substitution cipher -> Can double shift encode" + +[21d207c1-98de-40aa-994f-86197ae230fb] +description = "Substitution cipher -> Can wrap on encode" + +[a3d7a4d7-24a9-4de6-bdc4-a6614ced0cb3] +description = "Substitution cipher -> Can wrap on decode" + +[e31c9b8c-8eb6-45c9-a4b5-8344a36b9641] +description = "Substitution cipher -> Can encode messages longer than the key" + +[93cfaae0-17da-4627-9a04-d6d1e1be52e3] +description = "Substitution cipher -> Can decode messages longer than the key" diff --git a/exercises/practice/simple-cipher/simple_cipher.zig b/exercises/practice/simple-cipher/simple_cipher.zig new file mode 100644 index 00000000..1d109cd8 --- /dev/null +++ b/exercises/practice/simple-cipher/simple_cipher.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const mem = std.mem; + +pub const Cipher = struct { + key: []const u8, + + /// Initializes a cipher with a copy of the given key. + pub fn init(allocator: mem.Allocator, key: []const u8) mem.Allocator.Error!Cipher { + _ = allocator; + _ = key; + @compileError("please implement the init function"); + } + + /// Initializes a cipher with a randomly generated key of at least 100 + /// lowercase letters. + pub fn initRandom(allocator: mem.Allocator, random: std.Random) mem.Allocator.Error!Cipher { + _ = allocator; + _ = random; + @compileError("please implement the initRandom function"); + } + + /// Frees the key. + pub fn deinit(self: *Cipher, allocator: mem.Allocator) void { + _ = self; + _ = allocator; + @compileError("please implement the deinit function"); + } + + /// Encodes `plaintext`. Caller owns the returned memory. + pub fn encode(self: Cipher, allocator: mem.Allocator, plaintext: []const u8) mem.Allocator.Error![]u8 { + _ = self; + _ = allocator; + _ = plaintext; + @compileError("please implement the encode function"); + } + + /// Decodes `ciphertext`. Caller owns the returned memory. + pub fn decode(self: Cipher, allocator: mem.Allocator, ciphertext: []const u8) mem.Allocator.Error![]u8 { + _ = self; + _ = allocator; + _ = ciphertext; + @compileError("please implement the decode function"); + } +}; diff --git a/exercises/practice/simple-cipher/test_simple_cipher.zig b/exercises/practice/simple-cipher/test_simple_cipher.zig new file mode 100644 index 00000000..f849de96 --- /dev/null +++ b/exercises/practice/simple-cipher/test_simple_cipher.zig @@ -0,0 +1,169 @@ +const std = @import("std"); +const testing = std.testing; + +const simple_cipher = @import("simple_cipher.zig"); +const Cipher = simple_cipher.Cipher; + +const Op = enum { encode, decode, round_trip }; + +/// Applies `op` to `phrase` and checks that the result equals `expect`. +/// The cipher uses `key`, or a randomly generated key when `key` is null; +/// a null `phrase` or `expect` stands for a prefix of the random key. +fn testCipher( + allocator: std.mem.Allocator, + key: ?[]const u8, + op: Op, + phrase_opt: ?[]const u8, + expect_opt: ?[]const u8, +) !void { + var prng = std.Random.DefaultPrng.init(testing.random_seed); + var cipher = if (key) |k| + try Cipher.init(allocator, k) + else + try Cipher.initRandom(allocator, prng.random()); + defer cipher.deinit(allocator); + const phrase = phrase_opt orelse cipher.key[0..expect_opt.?.len]; + const expect = expect_opt orelse cipher.key[0..phrase_opt.?.len]; + switch (op) { + .encode => { + const actual = try cipher.encode(allocator, phrase); + defer allocator.free(actual); + try testing.expectEqualStrings(expect, actual); + }, + .decode => { + const actual = try cipher.decode(allocator, phrase); + defer allocator.free(actual); + try testing.expectEqualStrings(expect, actual); + }, + .round_trip => { + const encoded = try cipher.encode(allocator, phrase); + defer allocator.free(encoded); + const actual = try cipher.decode(allocator, encoded); + defer allocator.free(actual); + try testing.expectEqualStrings(expect, actual); + }, + } +} + +/// Checks that a random key is at least 100 lowercase letters. +fn testKey(allocator: std.mem.Allocator) !void { + var prng = std.Random.DefaultPrng.init(testing.random_seed); + var cipher = try Cipher.initRandom(allocator, prng.random()); + defer cipher.deinit(allocator); + try testing.expect(cipher.key.len >= 100); + for (cipher.key) |letter| { + try testing.expect(std.ascii.isLower(letter)); + } +} + +test "Random key cipher-Can encode" { + const phrase: []const u8 = "aaaaaaaaaa"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ null, .encode, phrase, null }, + ); +} + +test "Random key cipher-Can decode" { + const expect: []const u8 = "aaaaaaaaaa"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ null, .decode, null, expect }, + ); +} + +test "Random key cipher-Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" { + const phrase: []const u8 = "abcdefghij"; + const expect: []const u8 = "abcdefghij"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ null, .round_trip, phrase, expect }, + ); +} + +test "Random key cipher-Key is made only of lowercase letters" { + try testing.checkAllAllocationFailures(testing.allocator, testKey, .{}); +} + +test "Substitution cipher-Can encode" { + const phrase: []const u8 = "aaaaaaaaaa"; + const expect: []const u8 = "abcdefghij"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abcdefghij", .encode, phrase, expect }, + ); +} + +test "Substitution cipher-Can decode" { + const phrase: []const u8 = "abcdefghij"; + const expect: []const u8 = "aaaaaaaaaa"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abcdefghij", .decode, phrase, expect }, + ); +} + +test "Substitution cipher-Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" { + const phrase: []const u8 = "abcdefghij"; + const expect: []const u8 = "abcdefghij"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abcdefghij", .round_trip, phrase, expect }, + ); +} + +test "Substitution cipher-Can double shift encode" { + const phrase: []const u8 = "iamapandabear"; + const expect: []const u8 = "qayaeaagaciai"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "iamapandabear", .encode, phrase, expect }, + ); +} + +test "Substitution cipher-Can wrap on encode" { + const phrase: []const u8 = "zzzzzzzzzz"; + const expect: []const u8 = "zabcdefghi"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abcdefghij", .encode, phrase, expect }, + ); +} + +test "Substitution cipher-Can wrap on decode" { + const phrase: []const u8 = "zabcdefghi"; + const expect: []const u8 = "zzzzzzzzzz"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abcdefghij", .decode, phrase, expect }, + ); +} + +test "Substitution cipher-Can encode messages longer than the key" { + const phrase: []const u8 = "iamapandabear"; + const expect: []const u8 = "iboaqcnecbfcr"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abc", .encode, phrase, expect }, + ); +} + +test "Substitution cipher-Can decode messages longer than the key" { + const phrase: []const u8 = "iboaqcnecbfcr"; + const expect: []const u8 = "iamapandabear"; + try testing.checkAllAllocationFailures( + testing.allocator, + testCipher, + .{ "abc", .decode, phrase, expect }, + ); +} diff --git a/generators/exercises/simple_cipher.py b/generators/exercises/simple_cipher.py new file mode 100644 index 00000000..c43f7617 --- /dev/null +++ b/generators/exercises/simple_cipher.py @@ -0,0 +1,99 @@ +from lib import zstr + +HEADER = """const Cipher = simple_cipher.Cipher; + +const Op = enum { encode, decode, round_trip }; + +/// Applies `op` to `phrase` and checks that the result equals `expect`. +/// The cipher uses `key`, or a randomly generated key when `key` is null; +/// a null `phrase` or `expect` stands for a prefix of the random key. +fn testCipher( + allocator: std.mem.Allocator, + key: ?[]const u8, + op: Op, + phrase_opt: ?[]const u8, + expect_opt: ?[]const u8, +) !void { + var prng = std.Random.DefaultPrng.init(testing.random_seed); + var cipher = if (key) |k| + try Cipher.init(allocator, k) + else + try Cipher.initRandom(allocator, prng.random()); + defer cipher.deinit(allocator); + const phrase = phrase_opt orelse cipher.key[0..expect_opt.?.len]; + const expect = expect_opt orelse cipher.key[0..phrase_opt.?.len]; + switch (op) { + .encode => { + const actual = try cipher.encode(allocator, phrase); + defer allocator.free(actual); + try testing.expectEqualStrings(expect, actual); + }, + .decode => { + const actual = try cipher.decode(allocator, phrase); + defer allocator.free(actual); + try testing.expectEqualStrings(expect, actual); + }, + .round_trip => { + const encoded = try cipher.encode(allocator, phrase); + defer allocator.free(encoded); + const actual = try cipher.decode(allocator, encoded); + defer allocator.free(actual); + try testing.expectEqualStrings(expect, actual); + }, + } +} + +/// Checks that a random key is at least 100 lowercase letters. +fn testKey(allocator: std.mem.Allocator) !void { + var prng = std.Random.DefaultPrng.init(testing.random_seed); + var cipher = try Cipher.initRandom(allocator, prng.random()); + defer cipher.deinit(allocator); + try testing.expect(cipher.key.len >= 100); + for (cipher.key) |letter| { + try testing.expect(std.ascii.isLower(letter)); + } +} +""" + + +def gen_case(case): + inp = case["input"] + + if case["property"] == "key": + return " try testing.checkAllAllocationFailures(testing.allocator, testKey, .{});\n" + + key = zstr(inp["key"]) if "key" in inp else "null" + + if inp.get("ciphertext") == "cipher.encode": + # Round trip: decode(encode(phrase)) == phrase. + op = ".round_trip" + phrase = zstr(inp["plaintext"]) + expect = zstr(case["expected"]) + elif case["property"] == "encode": + op = ".encode" + phrase = zstr(inp["plaintext"]) + e = case["expected"] + # A symbolic expected value refers to a prefix of the random key. + expect = "null" if e.startswith("cipher.key") else zstr(e) + else: + op = ".decode" + ciphertext = inp["ciphertext"] + # A symbolic ciphertext refers to a prefix of the random key. + phrase = "null" if ciphertext.startswith("cipher.key") else zstr(ciphertext) + expect = zstr(case["expected"]) + + out = [] + if phrase != "null": + out.append(f" const phrase: []const u8 = {phrase};\n") + phrase = "phrase" + if expect != "null": + out.append(f" const expect: []const u8 = {expect};\n") + expect = "expect" + out.append( + " try testing.checkAllAllocationFailures(\n" + " testing.allocator,\n" + " testCipher,\n" + f" .{{ {key}, {op}, {phrase}, {expect} }},\n" + " );\n" + ) + return "".join(out)