[입 개발] org.apache.commons.codec.binary.Base64 decoding 동작에 관해서…

원래 개인적으로 삽질을 잘 하긴 하지만, 다시 최근에 큰 삽을 한번 팠습니다. 일단, 제가 자바를 잘 모르고, 초보다 보니, 남들 다 아는 지식을 모르고 넘어가는 경우가 많은데, 웬지 딴분들은 다 아셔서 이런일이 안생길것 같지만, 그냥 정리 차원에서 적어둡니다.

먼저 발생한 사건은 다음과 같습니다. 제 아이디인 charsyam 을 base64로 인코딩하면 다음과 같은 값이 나옵니다.

“Y2hhcnN5YW0=”

그리고 이걸 다시 decode 하면 “charsyam” 이라는 값이 나올겁니다. 그런데… 이제 보통 c/c++의 구현을 보면…
하다가 이상한 문자가 있으면 오류가 발생합니다. 즉 위의 “Y2hhc###nN5YW0=” 이런 글자가 있으면 일반적인 기대값은 오류라고 생각합니다.(아니라면 T.T)

그런데… apache coomons의 Base64구현은 조금 재미납니다.

“Y2hhc###nN5YW0=”
“Y2hhc#n#N#5YW0=”
“Y2hhc$$$nN5YW0=”
“Y2hhc$#n4N5YW0=”
“###Y2hhcnN5YW0=”
“###Y2hhcnN5YW0#=”
“Y2hhcnN5YW0=”

이런 것들이 모두 동일하게 “charsyam”으로 제대로 디코딩이 됩니다. 이것은 해당 코드가 DECODE_TABLE이라는 것을 가지고 여기에서 사용하지 않는 문자들은 전부 버린 뒤에 사용하기 때문에 일어나는 동작입니다. 다만 저 같은 사람은… 일단 제 코드를 의심하기 때문에, 삽질이 흑흑흑

먼저 다음과 같이 DECODE_TABLE 이 정의되어 있습니다.

    private static final byte[] DECODE_TABLE = {
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54,
            55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
            5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
            24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
            35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
    };

DECODE_TABLE 에서 -1에 지정되는 문자들은 전부 버리는 거죠. 사실 이게 apache commons base64의 문제(?)는 아닙니다. base64 에서 사용하는 table 이 사용하는 도메인에 따라서 조금씩 다르기 때문에, 이걸 전부 만족하도록 하게 구현이 되어 있는것입니다. 사실 더 많이 고려된 것이기도 하죠. 자세한건 여기(http://en.wikipedia.org/wiki/Base64)에서 확인하시면 됩니다.

그럼 이제 실제 구현을 살펴보면, 제가 말한 대로 DECODE_TABLE[value]의 값이 0 보다 클 경우에만 사용하게 되어있습니다.

   @Override
   void decode(final byte[] in, int inPos, final int inAvail, final Context context) {
       if (context.eof) {
           return;
       }
       if (inAvail < 0) {
           context.eof = true;
       }
       for (int i = 0; i < inAvail; i++) {
           final byte[] buffer = ensureBufferSize(decodeSize, context);
           final byte b = in[inPos++];
           if (b == PAD) {
               // We're done.
               context.eof = true;
               break;
           } else {
               if (b >= 0 && b < DECODE_TABLE.length) {
                   final int result = DECODE_TABLE[b];
                   if (result >= 0) {
                       context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK;
                       context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;
                       if (context.modulus == 0) {
                           buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);
                           buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                           buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
                       }
                   }
               }
           }
       }

       // Two forms of EOF as far as base64 decoder is concerned: actual
       // EOF (-1) and first time '=' character is encountered in stream.
       // This approach makes the '=' padding characters completely optional.
       if (context.eof && context.modulus != 0) {
           final byte[] buffer = ensureBufferSize(decodeSize, context);

           // We have some spare bits remaining
           // Output all whole multiples of 8 bits and ignore the rest
           switch (context.modulus) {
               case 0 : // impossible, as excluded above
               case 1 : // 6 bits - ignore entirely
                   // TODO not currently tested; perhaps it is impossible?
                   break;
               case 2 : // 12 bits = 8 + 4
                   context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
                   buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                   break;
               case 3 : // 18 bits = 8 + 8 + 2
                   context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
                   buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                   buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                   break;
               default:
                   throw new IllegalStateException("Impossible modulus "+context.modulus);
           }
       }
   }

나름 더 정확한 내용을 알 수 있게 되긴 했지만, 제 멍청함으로 인한 삽질한 시간들은 흑흑흑… 역시… 진리는 소스입니다. 흑흑흑