이미 몇 개월 전에도, 확실하게는 몰랐지만 몇 차례 파일을 업로드하며 테스트하다 보니 대략 이런 문제가 있는 듯 해서
"대략 패치"(?)해서 사용하고 있었지만 이번에 그 증상과 원인을 확실히 알게 돼서 글을 하나 써 본다.
-----
.NET AJAX Control Toolkit에 있는 AjaxFileUpload를 여러 곳에 사용하고 있는데,
이미지 파일을 업로드하면 바로 해당 페이지에서 그 이미지를 보여주는 페이지도 사용하고 있다.
(예제 코드를 거의 그대로 재사용 중)
그런데 이미지 파일(뿐만 아니라 모든 파일이 마찬가지. 이미지가 아니면 즉시 확인 불가)을 업로드해도
정상적으로 해당 이미지 파일을 보여주지 못하고 "배꼽"만 표시되는 경우가 종종 있었다.
내 개발 환경에 무슨 문제가 있는 건가 하고 아무리 뒤져 봐도 문제를 찾지 못했고
심지어 ASP.NET 공식 사이트에서 해봐도 동일한 문제가 있는 것을 보고, 이건 내 문제가 아니다라는 결론을 내렸다.
>> 참조: http://www.asp.net/AjaxLibrary/AjaxControlToolkitSampleSite/AjaxFileUpload/AjaxFileUpload.aspx
즉, 간략히 요약해 보자면,
위 링크 페이지로 들어가서 파일명이 "3월 약제 고시.jpg"라는 이름으로 된 이미지 파일을 업로드 해보면 이미지가 표시되지 않는다.
그런데, 파일명을 "3.jpg" 혹은 "3월 약.jpg"라고 바꾼 다음 다시 업로드 해보면 잘 표시된다.
황당....
문제는 파일명에 "약제"와 같은 특정 한글 조합이 들어가면 AjaxFileUpload 컨트롤 내부적으로
뭔가 정상 인식할 수 없는 패턴으로 파악, 실제 파일 내용 이외에 추가로 쓰레기 정보까지 파일 내용에 포함시켜버리는
"악성 버그" 때문이었다.
위 그림이 원본 파일과 업로드된 파일을 바이너리 비교한 내용이다.
원본 파일의 시작 배열인 "FF D8 FF E0 00"이 업로드된 파일에서는 10번째부터 시작되는 것을 확인할 수 있다.
즉, 파일 내용 앞에 쓰레기 값이 10바이트가 포함된 것이다.
항상 그런 것이 아니라 파일명을 달리 해서 업로드해보면 또 다른 패턴으로 쓰레기 값이 추가된다.
아예 2바이트만 추가되는 경우, 4바이트가 추가되는 경우 등 다양했다.
공통적인 것은, 항상 쓰레기 값의 마지막은 "0D 0A" 한 번 내지 두 번으로 끝난다는 것이었다.
"0D 0A 0D 0A"
이건 캐리지 리턴(줄바꿈), 즉 "\r\n"인데, 보통 웹에서는 HTTP 헤더와 본문, 즉 파일 콘텐츠 내용을 구분하기 위해 사용되는 값이다.
특정 한글 패턴을 만나면 그게 정상적으로 되지 않고 헤더와 본문을 구분하는 판단이 잘못 되어 HTTP 헤더 일부까지 파일 콘텐츠에 포함된다는 얘기다.
유니코드 사용이 일상화된 21세기에, 대체 코드를 어떻게 만들었길래 파일명에 따라 이런 오류가 발생할 수 있을까?
아무튼 미국 영어권 코드들이란... ㅉㅉㅉ
그래서 아예 파일을 업로드할 때 제대로 업로드할 수 있으면 좋겠지만,
그건 AJAX Control Toolkit 내부 코드라 수정이 힘든 관계로
내 코드 중 업로드된 파일을 읽는 부분에서 아래와 같은 함수를 하나 만들고 호출하는 것으로 이런 버그를 패치했다.
// 처음 16바이트를 읽고 \r\n이 포함되어 있는지 체크한다.
// \r\n이 있으면 그 다음부터가 실제 바이너리(유니코드 파일명 관련 버그)
private long checkBytes(Stream stream)
{
int bytes = 0;
while (bytes < 0x10)
{
bytes++;
if (stream.ReadByte() == 0x0d)
{
bytes++;
if (stream.ReadByte() != 0x0a) break;
bytes = 0;
}
}
stream.Position -= bytes;
return stream.Position;
}
이미지 파일 표시하는 부분은 패치를 했는데,
아직 일반 파일 다운로드하는 부분까지 패치하지는 않았다. 코드 방식이 조금 달라서...
추가 패턴이 있는지 조금 더 확인 후 보완 작업을 더 해야 할 듯.
--- 추가 보완 (2014.03.03) ---
위 코드를 쓰니 원래 파일 헤더(16바이트 이내)에 0x0d, 0x0a가 들어가 있는 경우, 잘려 버리는 문제가 있었다.
예를 들면 PNG 파일이나 PDF 파일은 재수없게도 파일 헤더에 0x0d, 0x0a가 한 번 이상 꼭 들어간다.
그 밖에도 확인하지 못한 파일 종류 중에서도 파일 헤더 내에 저 줄바꿈 문자가 들어갈 가능성이 높다고 생각되어 코드를 보완했다.
(다른 DLL로 함수 위치를 옮기면서 public static으로 한정자도 바뀌었다.)
/// <summary>
/// 파일 스트림 체크: 처음 16바이트를 읽고 \r\n\r\n이 포함되어 있는지 체크한다.
/// \r\n\r\n이 있으면 그 다음부터가 실제 바이너리(AjaxFileUpload 유니코드 파일명 관련 버그)
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns></returns>
public static long CheckFileBytes(Stream stream)
{
int pos;
int bytes = 0, chk = -1;
int len = stream.Length > 0x10 ? 0x10 : (int)stream.Length;
while ((pos = (int)stream.Position) < len)
{
bytes++;
if (stream.ReadByte() == 0x0d)
{
bytes++;
if (stream.ReadByte() == 0x0a)
{
bytes -= 2;
if (chk >= 0) break;
chk = pos;
}
}
else
{
if (chk >= 0) break;
}
}
if (chk > 0 && pos != bytes + 4) // 파일 중간에(pos>0) \r\n이 하나만 있는 경우: 정상 파일
stream.Position = 0;
else
stream.Position -= bytes;
return stream.Position;
}
일단은 수정된 코드로 어지간한 파일들은 모두 정상 처리되는데, 문제는 텍스트 파일이다.
텍스트 파일은 파일 헤더가 따로 없는 관계로
HTTP 헤더가 잘려 들어간 것인지, 원래 텍스트 내용이 그런 것인지 구분할 방법이 없는데
(굳이 구분하자면 파일 확장자로 .txt, .ini 등인지 판단할 수는 있겠지만... 그냥 냅뒀다.)
일단은 첫번째 \r\n 줄바꿈까지 자르는 위 로직이 적용되도록 했다. 해결 방법이 생각나면 또...^^;
--- 추가 보완 (2014.03.06) ---
한글로 된 파일명이 긴 경우에 또 문제가 발생했다. 가만히 보니 파일 내에 포함되는 쓰레기값의 개수는 파일명의 한글 개수와 비례하는 관계가 있었다. 말하자면 한글이 10자가 포함된 경우에 쓰레기값 수가 24바이트였다면 한글이 20자가 되면 쓰레기값도 44바이트로 증가하는 식이었다. 그래서 자르는 로직을 추가 보완했다.
public static long CheckFileBytes(string filePath, Stream stream)
{
// 한글 파일인 경우 한글 문자 수 * 2 + 0d0a0d0a만큼 뒤로 밀림 현상 발견
string fileName = Path.GetFileName(filePath) ?? filePath;
int checkLength = (Encoding.Default.GetBytes(fileName).Length - fileName.Length) * 2 + 4;
if (checkLength < 0x10) checkLength = 0x10;
int pos;
int bytes = 0, chk = -1;
int findReturn = 0;
int len = stream.Length > checkLength ? checkLength : (int)stream.Length;
while ((pos = (int)stream.Position) < len)
{
bytes++;
if (stream.ReadByte() == 0x0d)
{
bytes++;
if (stream.ReadByte() == 0x0a)
{
if (++findReturn >= 2) break;
chk = pos;
}
}
else
{
if (chk == 0) break;
findReturn = 0;
}
}
if (chk > 0 && findReturn != 2) // 파일 중간에(pos>0) \r\n이 하나만 있는 경우: 정상 파일
stream.Position = 0;
else if (chk == 0 && findReturn == 1) // 파일 처음이 \r\n으로 시작하는 경우 자르기
stream.Position = 2;
else if (findReturn != 2) // 파일에 \r\n\r\n이 없는 경우 원복
stream.Position -= bytes;
return stream.Position;
}
이번 추가 보완으로 어지간한 파일은 다 될 듯 하다.
한 가지, 텍스트 파일이 문젠데...
'Tech: > .NET·C#' 카테고리의 다른 글
SEED C# 소스 (33) | 2014.03.25 |
---|---|
AJAX Control Toolkit: NoBot (0) | 2014.03.21 |
AjaxFileUpload와 IE 버전 (0) | 2014.02.07 |
IE에서 AjaxToolkit DropShadow 스타일이 제대로 먹지 않을 때 (0) | 2014.01.27 |
2,000 Things You Should Know About C# (0) | 2013.10.27 |