TDD-Deciphered.com

Part 7: Handling the Ring Offset

09/01/2010

We noted in the previous installment that a rotor's behaviour depended on its core mapping, and on the rotation of that core with regards to the wheel, specfied either alphabetically or numerically.

We didn't say, however, what that offset actually meant. Where are we offsetting *from*?

On the physical rotor, there's a catch on the core that clips into a location next to any letter on the alphabet ring. That letter gives the alphabetic offset, or alternately a numeric offset where 1 is A, 2 is etc (considered as 'distance from Z').
The core mapping is specified as the mapping through the rotor at the "default" offset of 'A' or '1'; this is standard practice in discussion of Enigma rotors.

To simulate the electrical behaviour of the rotor as an encryptor, we need to be able to determine the output character that corresponds to any input character, whether at the the default position or at another offset.

The rotor's notch is on the alphabetic ring rather than the core, so its behaviour is independent of offset.

We know we can now specify our rotors, so it's time to test them. We've designed a type of rotor that simply maps input to the same output character, so let's check it.
  1. function testCoreIdentityMappingReturnsInputAtDefaultOffset ()
  2. {
  3. $rotor = new Enigma_Rotor();
  4. $coreMapping=array(
  5. 'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G',
  6. 'H'=>'H','I'=>'I','J'=>'J','K'=>'K','L'=>'L','M'=>'M','N'=>'N',
  7. 'O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T','U'=>'U',
  8. 'V'=>'V','W'=>'W','X'=>'X','Y'=>'Y','Z'=>'Z'
  9. );
  10. $offset=1; //default
  11. $rotor->setRingOffset($offset);
  12. $rotor->setCoreMapping($coreMapping);
  13.  
  14. $testCharacter='V';
  15.  
  16. $this->assertSame(
  17. $testCharacter,
  18. $rotor->getOutputCharacterForInputCharacter($testCharacter)
  19. );
  20. }
Tests are red:
Fatal error: Call to undefined method Enigma_Rotor::getOutputCharacterForInputCharacter() in Enigma/RotorTest.php on line 109
This is the function we specified in our Enigma_Encryptor_Interface, but haven't yet implemented because we didn't need it.

We'll add it in Rotor.php now:
  1. public function getOutputCharacterForInputCharacter($inputCharacter)
  2. {
  3. $inputCharacter=strtoupper($inputCharacter);
  4. return($this->_coreMapping[$inputCharacter]);
  5. }
Our first rotor's independent of offset, so we ignore that consideration in our code for now. Let's also simulate a slightly more complex rotor, with the mapping:
'A'=>'N','B'=>'O','C'=>'P','D'=>'Q','E'=>'R','F'=>'S','G'=>'T',
'H'=>'U','I'=>'V','J'=>'W','K'=>'X','L'=>'Y','M'=>'Z','N'=>'A',
'O'=>'B','P'=>'C','Q'=>'D','R'=>'E','S'=>'F','T'=>'G','U'=>'H',
'V'=>'I','W'=>'J','X'=>'K','Y'=>'L','Z'=>'M'
and test that:
  1. function testRot13Mapping ()
  2. {
  3. $rotor = new Enigma_Rotor();
  4. $coreMapping=array(
  5. 'A'=>'N','B'=>'O','C'=>'P','D'=>'Q','E'=>'R','F'=>'S','G'=>'T',
  6. 'H'=>'U','I'=>'V','J'=>'W','K'=>'X','L'=>'Y','M'=>'Z','N'=>'A',
  7. 'O'=>'B','P'=>'C','Q'=>'D','R'=>'E','S'=>'F','T'=>'G','U'=>'H',
  8. 'V'=>'I','W'=>'J','X'=>'K','Y'=>'L','Z'=>'M'
  9. );
  10. $offset=1;
  11. $rotor->setRingOffset($offset);
  12. $rotor->setCoreMapping($coreMapping);
  13.  
  14. $testInputCharacter='V';
  15. $testOutputCharacter='I';
  16.  
  17. $this->assertSame(
  18. $testOutputCharacter,
  19. $rotor->getOutputCharacterForInputCharacter($testInputCharacter)
  20. );
  21. }
It's green; and as noted it's the widely-used ROT-13 cypher.

The next step is to test with the Ringstellung in use. For this, we'll actually use a genuine rotor from the World War 2 Enigma, rotor I, which has the following mapping:
'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P',
'U'=>'A','V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
Unlike previous rotors, this one will deliver different results depending on the Ringstellung, so we want to add that as a parameter as well as the ring input and output. We should test a few combinations of input and offset, which we'll need to work out manually to start with.

To work out what happens in the rotor here, we'll use a digram that lets us look at the wheel's input an output; together with the mapping table we can use this to work out visually how a signal will be routed at any ring offset. Being able to check this visually will be essential for us to create and trust our tests.

First, let's look how a 'V' routes at the default offset. This shows us that, with the mapping above, and with the latch at A, a signal entering at 'I' on the tyre (remember, the letters are on the tyre) still maps to the 'Virtual I' on the core. Checking our mapping above tells us this will exit the core at 'V', which in turn maps to 'V' on the wheel.



That's not really a revelation; but if we move the ring in the diagram to a new offset (eg Offset B), the use becomes clearer:



Input at V on the ring routes to U on the core. From our table above, this maps to A at core output, which is against B on the ring.

We'll pick up one more data point for our tests, using a larger offset and a different input:



So at offset 'P' or '16' , G=>R=>U=>J

If we wrap this up in another dataprovider, we can test this wheel at the various offsets:
  1. /**
  2.   * @dataProvider rotorIDataProvider
  3.   */
  4. function testRotorIMappingOffset ($offset,$testInputCharacter,$testOutputCharacter)
  5. {
  6. $rotor = new Enigma_Rotor();
  7. $coreMapping=array(
  8. 'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
  9. 'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
  10. 'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P',
  11. 'U'=>'A','V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
  12. );
  13.  
  14. $rotor->setRingOffset($offset);
  15. $rotor->setCoreMapping($coreMapping);
  16.  
  17.  
  18. $this->assertSame($testOutputCharacter,
  19. $rotor->getOutputCharacterForInputCharacter($testInputCharacter));
  20. }
  21.  
  22. function rotorIDataProvider() {
  23. return array(
  24. array(1,'V','I'),
  25. array(2,'V','A'),
  26. array(16,'G','J')
  27. );
  28. }
Of course, we haven't actually written code to handle offsets yet, so the tests are red:
silverbox:Enigma wechsler$ phpunit RotorTest.php 
PHPUnit 3.4.5 by Sebastian Bergmann.

..............FF

Time: 1 second, Memory: 5.75Mb

There were 2 failures:

1) Enigma_RotorTest::testRotorIMappingOffset with data set #1 (2, 'V', 'B')
--- Expected
+++ Actual
@@ @@
-B
+I

/Users/wechsler/tdd-enigma/tests/Enigma/RotorTest.php:151

2) Enigma_RotorTest::testRotorIMappingOffset with data set #2 (16, 'G', 'J')
--- Expected
+++ Actual
@@ @@
-J
+D

/Users/wechsler/tdd-enigma/tests/Enigma/RotorTest.php:151

FAILURES!
Tests: 16, Assertions: 16, Failures: 2.
We can handle the offsets by revising the getOutputCharacterForInputCharacter function in Rotor.php:
  1. public function getOutputCharacterForInputCharacter ($inputCharacter)
  2. {
  3. $inputCharacter = strtoupper($inputCharacter);
  4.  
  5. $coreInputCharacter = $this->_getCharacterOffsetBy(
  6. $inputCharacter, $this->_ringOffset-1
  7. );
  8.  
  9. $coreOutputCharacter
  10. = $this->_coreMapping[$coreInputCharacter];
  11.  
  12. $outputCharacter = $this->_getCharacterOffsetBy(
  13. $coreOutputCharacter, 0 - ($this->_ringOffset-1)
  14. );
  15.  
  16. // $outputCharacter= $this->_coreMapping[$inputCharacter];
  17.  
  18. return ($outputCharacter);
  19. }
  20.  
  21. private function _getCharacterOffsetBy ($character, $offset)
  22. {
  23. $charAsInt =
  24. $this->_charToAlphabetPosition($character);
  25.  
  26. $newInteger = (26 + $charAsInt - $offset) % 26;
  27.  
  28. if($newInteger==0) {
  29. $newInteger=26;
  30. }
  31.  
  32. $newCharacter =
  33. $this-> _alphabetPositionToCharacter($newInteger);
  34.  
  35. return ($newCharacter);
  36. }
  37.  
  38. private function _charToAlphabetPosition ($char)
  39. {
  40. $position= (ord($char)-64);
  41.  
  42. return $position;
  43. }
  44.  
  45. private function _alphabetPositionToCharacter ($position)
  46. {
  47. $char= (chr($position+64));
  48.  
  49. return $char;
  50. }
We've unwrapped this into a few subfunctions for clarity, but essentially the flow is:

- Convert the input character to a number
- Add the offset in modulo-26 maths
- Convert back to character
- Do the wheel encryption
- Convert the output character to a number
- Subtract the offset in modulo-26 maths
- Convert back to character

If we now test, we’re green – we’ve implemented the Ringstellung correctly. This probably makes a good time to commit our code to subversion, too;
silverbox:tdd-enigma wechsler$ svn status
M       tests/Enigma/RotorTest.php
M       library/Enigma/Rotor.php
silverbox:tdd-enigma wechsler$ svn commit -m 'Code can now handle ring offset'
Sending        library/Enigma/Rotor.php
Sending        tests/Enigma/RotorTest.php
Transmitting file data ..
Committed revision 5.
We’ve added more code than in previous steps here, but it’s all been in order to fix some already-existing tests. We’ve added no new functionality beyond that, and all the functions we’ve added are private, so there are no new calls that can be made into the Rotor. We don’t need to test all these functions as they’re encapsulated within the “unit” of functionality we’re already testing, and can never be used as a distinct unit themselves.

Let’s take a look at our alphanumeric converter though:
return (ord($char)-64);

This is obviously very simple, but that simplicity gives a hint of possible problems. This function isn’t just a letter-to-number converter, it’ll convert any ASCII character to an integer, even if it’s not a letter. Is this a good idea? What happens if we give it a numeric input?

As with setting a bad mapping above, we need to throw an exception on bad input data, and we’ll want to test that that exception gets thrown.

We’ll add the following code to the convertor:
  1. if(!preg_match('/^[A-Z]$/',$char)) {
  2. throw new Exception ("Bad Character '$char'");
  3. }
We’ve already seen one way to expect an exception in testing, but we can also use a comment notification as for the dataprovider above. As we want to check a few failure cases, we’ll also use a data provider:
  1. /**
  2.   * Try and encrypt something invalid
  3.   * @dataProvider badCharacterIDataProvider
  4.   * @expectedException Exception
  5.   */
  6. function testEncryptBadCharacter($characters)
  7. {
  8. $rotor = new Enigma_Rotor();
  9. $coreMapping=array(
  10. 'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
  11. 'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
  12. 'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P','U'=>'A',
  13. 'V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
  14. );
  15. $offset=0;
  16. $rotor->setRingOffset($offset);
  17.  
  18. $spaceTranslated=
  19. $rotor->getOutputCharacterForInputCharacter($characters);
  20. }
  21.  
  22.  
  23. function badCharacterIDataProvider()
  24. {
  25. return array(array(' '),array(''),array(2),array('toolong'));
  26. }
Note that we test for invalid data in the offset calculation, but have to send the input to getOutputCharacterForInputCharacter() as the closest external function. It’s not ideal but it still performs the testing we need.

Finally, we can co-opt our _charToAlphabetPosition function to allow us to accept alphabetic ring offsets too:
  1. public function setRingOffset ($offset)
  2. {
  3. if(preg_match('/^[A-Z]$/',$offset)) {
  4. $offset=$this->_charToAlphabetPosition($offset);
  5. } else if(!is_integer($offset) || ($offset<1) || ($offset>26)) {
  6. throw new Exception("Offset must be integer in range 1..26");
  7. }
  8. $this->_ringOffset = $offset;
  9. }
Note that we also need to remove the last line from our ringOffsetBoundariesDataProvider;
  1. function ringOffsetBoundariesDataProvider()
  2. {
  3. return array(
  4. array(0,false),
  5. array(1,true),
  6. array(26,true),
  7. array(27,false),
  8. array(0.99,false),
  9. array(25.99,false),
  10. // array('A',false) // No longer invalid!
  11. );
  12. }
And that’s about it for the Rotor. It’s taken us a while to get here as we’ve examined our code very closely and introduced quite a few new tricks, but we’ve got some code for this system element that we can be very confident in.

We've also written by far the most complex part of the code now; from here on it all gets a little simpler. In the next installment, we'll look at how the rotor sits in the slot, and notice that it's fairly similar logic to what we've seen today.

All content copyright Richard George (richard@phase.org), 2009-2010

Sponsored links to recommended books: