Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions exercises/practice/simple-cipher/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions exercises/practice/simple-cipher/.meta/config.json
Original file line number Diff line number Diff line change
@@ -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"
}
51 changes: 51 additions & 0 deletions exercises/practice/simple-cipher/.meta/example.zig
Original file line number Diff line number Diff line change
@@ -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;
}
};
46 changes: 46 additions & 0 deletions exercises/practice/simple-cipher/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
44 changes: 44 additions & 0 deletions exercises/practice/simple-cipher/simple_cipher.zig
Original file line number Diff line number Diff line change
@@ -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");
}
};
169 changes: 169 additions & 0 deletions exercises/practice/simple-cipher/test_simple_cipher.zig
Original file line number Diff line number Diff line change
@@ -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 },
);
}
Loading
Loading