Archive for April, 2019

More Problems with Object Designs

Wednesday, April 3rd, 2019

xcode.jpg

This morning I was playing around with the CryptoQuip solver, and realizing that the mapping code I had could be made a little simpler if I used the fact that ASCII character representations are all numerically ascending, and that makes character math something to use - as opposed to simple look-ups. For instance, if we look at the existing code for +createPatternText::

  1. + (NSString*) createPatternText:(NSString*)text
  2. {
  3. const char *src = [[text lowercaseString] UTF8String];
  4. NSUInteger len = [text length];
  5. const char *ascii = "abcdefghijklmnopqrstuvwxyz";
  6. char pattern[255];
  7. for (NSUInteger i = 0; i < len; ++i) {
  8. pattern[i] = ascii[strchr(src, src[i]) - src];
  9. }
  10. pattern[len] = '\0';
  11. return [NSString stringWithUTF8String:pattern];
  12. }

the look-up on line 8, using the const char array defined on line 5, is really just an offset calculation, and can be replaced with:

  1. + (NSString*) createPatternText:(NSString*)text
  2. {
  3. const char *src = [[text lowercaseString] UTF8String];
  4. NSUInteger len = MIN([text length], 255);
  5. char pattern[255];
  6. for (NSUInteger i = 0; i < len; ++i) {
  7. pattern[i] = 'a' + (strchr(src, src[i]) - src);
  8. }
  9. pattern[len] = '\0';
  10. return [NSString stringWithUTF8String:pattern];
  11. }

and the result is exactly the same. But now, we're not creating the const char array on the stack, on each call, and the offset into the array is the same addition as we are doing here. Simpler.

But when we go to Swift, we see that the beauty and elegance of the underlying data is being clouded with the all-too-common object oriented design of Character.

  1. var pattern: String {
  2. get {
  3. let ascii: [Character] = ["a","b","c","d","e","f","g","h","i","j","k",
  4. "l","m","n","o","p","q","r","s","t","u","v",
  5. "w","x","y","z"]
  6. var ans = ""
  7. let src = Array(self.lowercased().utf8)
  8. for c in src {
  9. ans.append(ascii[src.firstIndex(of: c)!])
  10. }
  11. return ans
  12. }
  13. }

Sadly, we can't simplify line 9 because the Character object doesn't allow for construction based on an ASCII value. Sure, it has all kinds of nice interrogation methods to test and see if it's ASCII, or uppercase, or a number... but you can't actually do the simple math of adding 1 to 'a' and getting 'b'. That is a shame, and it's not something you can easily add because the Character class would need a new -init: method.

So while I really like parts of Swift 5, I think they didn't really think about all the uses that ObjC fits into when making the new classes. It's a process, I get that, but it's causing folks to write code around these issues, and that's a mistake. No one will go back and fix their code when, and if, they add a way to do this.

Adding Pattern Generation to CryptoQuip

Tuesday, April 2nd, 2019

xcode.jpg

This morning I took a little time to bring the pattern matching code from the Clojure and Swift codebases of the Quip solver to the ObjC version because I noticed the other day that the ideas of the pattern matching were added after the ObjC version was working, and I hadn't back-filled them to the older code. I know it won't make a difference in the execution time, as this is all done before the attack is started, but the old code really was a mess, and the new code had to be cleaner.

The original code had the following method on the CypherWord:

- (BOOL) matchesPattern:(NSString*)plaintext
{
    BOOL  match = YES;
 
    // make sure that we have something to work with
    if (match && (([self getCypherText] == nil) || (plaintext == nil))) {
        match = NO;
    }
 
    // check the lengths - gotta be the same here for sure
    if (match && ([[self getCypherText] length] != [plaintext length])) {
        match = NO;
    }
 
    /*
     * Assume that each pair of characters is a new map, and then test that
     * mapping against all other cyphertext/plaintext pairs that SHOULD match
     * in the word. If we get a miss on any one of them, then we need to fail.
     */
    if (match) {
        unichar    cypher, plain, c, p;
        NSUInteger len = [plaintext length];
        for (NSUInteger i = 0; (i < len) && match; ++i) {
            // get the next possible pair in the two words
            cypher = [[self getCypherText] characterAtIndex:i];
            plain = [plaintext characterAtIndex:i];
            // check all the remaining character pairs
            for (NSUInteger j = (i+1); (j < len) && match; ++j) {
                c = [[self getCypherText] characterAtIndex:j];
                p = [plaintext characterAtIndex:j];
                if (((cypher == c) && (plain != p)) ||
                    ((cypher != c) && (plain == p))){
                    match = NO;
                    break;
                }
            }
        }
    }
 
    return match;
}

And it worked because it checked each pair of characters in the plaintext and cypher text, and looked for mismatches... but it wasn't very clean, and the same scanning had to be done on the same word over and over. So it wasn't all that efficient.

Then I added a class method to CypherWord to generate the pattern:

+ (NSString*) createPatternText:(NSString*)text
{
    const char *src = [[text lowercaseString] UTF8String];
    NSUInteger  len = [text length];
    const char *ascii = "abcdefghijklmnopqrstuvwxyz";
    char        pattern[255];
    for (NSUInteger i = 0; i < len; ++i) {
        pattern[i] = ascii[strchr(src, src[i]) - src];
    }
    pattern[len] = '\0';
    return [NSString stringWithUTF8String:pattern];
}

In the setter we then added:

- (void) setCypherText:(NSString*)text
{
    _cyphertext = text;
    _cypherSize = [text length];
    _cypherPattern = [CypherWord createPatternText:text];
}

So that setting the CypherText set the length and the pattern as well. Not bad at all, and the overhead is minimal for the puzzles we're dealing with.

At this point, we can then re-write the -matchesPattern: to be:

- (BOOL) matchesPattern:(NSString*)plaintext
{
    return ([self getCypherText] != nil) && (plaintext != nil) &&
           ([self length] == [plaintext length]) &&
           [[self getCypherPattern] isEqualToString:[CypherWord
             createPatternText:plaintext]];
}

and leverage the fact that we have instance variables for the length and pattern, and still use all the short-circuit techniques to make sure that we don't make a bad call, or sloppy comparison.

Nothing amazing, but it's nice to get this version up to the same design as the others.