원래 개인적으로 삽질을 잘 하긴 하지만, 다시 최근에 큰 삽을 한번 팠습니다. 일단, 제가 자바를 잘 모르고, 초보다 보니, 남들 다 아는 지식을 모르고 넘어가는 경우가 많은데, 웬지 딴분들은 다 아셔서 이런일이 안생길것 같지만, 그냥 정리 차원에서 적어둡니다.
먼저 발생한 사건은 다음과 같습니다. 제 아이디인 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); } } }
나름 더 정확한 내용을 알 수 있게 되긴 했지만, 제 멍청함으로 인한 삽질한 시간들은 흑흑흑… 역시… 진리는 소스입니다. 흑흑흑