[Simple XML 파서 만들기] #2. 준비 작업 그(it) 얘기

1편이 올라간지 한참이나 지나서어 2편을 올리게 되서 죄송합니다. 대략 8개월이나 지났군요. 이제와서 연재(?)를 계속한다고 해도 의미가 있을지는 모르겠지만, 그래도 미완성으로 남겨둘 수는 없기에 늦었지만 계속 이어나가겠습니다.


  관련글: [Simple XML 파서 만들기] #1. 파싱 원리

  이번 건 XML 파싱과는 크게 상관은 없지만, 본격으로 XML 파서를 만들기 전에 먼저 필요한 것들을 점검해 보겠습니다.


  1. Stream 라이브러리

  XML 문서에 대한 파싱을 수월하게 하기 위해선, 전체 데이터로 부터 순서대로 한 문자씩 읽어와주는 읽기전용 Stream이 필요합니다. 그리고 입력 소스를 다양화 할 수 있고, 여러 종류의 Stream을 조합해서 여러가지 디코딩 방식을 지원할 수 있게 됩니다.

  저는 Stream 클래스를 직접 만들어서 사용했습니다만, 관련 라이브러리가 이미 준비되어 있다면 이 부분은 건너 뛰어도 됩니다. 단, 앞으로 보여줄 소스들은 자체 제작한 Stream 라이브러리를 쓰고 있기 때문에, 이 부분은 적절히 수정해서 적용해야 됩니다.

  다음은 제가 간단히 만든 Stream 클래스의 일부입니다.

template<typename T>
class IReadStream
{
public:
virtual ~IReadStream() {}

virtual T Next() = 0;
virtual T Current() const = 0;
virtual bool isEnd() const = 0;
};


template<typename T>
class CFileReader : IReadStream<T>
{
protected:
HANDLE hSourceFile;
T data;

public:
CFileReader(HANDLE aFileHandle) { hSourceFile = aFileHandle; }
virtual ~CFileReader() {}

T Next() {
DWORD readbytes = 0;
BOOL result;
const int data_size = sizeof(T);

result = ::ReadFile(hSourceFile, &data, data_size,
&readbytes, NULL);
return (result && data_size == readbytes) ? data : (data = NULL);
}

T Current() const { return data; }

bool isEnd() const { return Current() == NULL; }
HANDLE GetHandle() const { return hSourceFile; }
};


/*--- 사용 예 ----*

HANDLE hFile = CreateFileW(
L"filename", GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_RANDOM_ACCESS, NULL
);

CFileReader<wchar_t> file_stream(hFile);

while (file_stream.Next());
{
wchar_t data = file_stream.Current();
...
}

*--------------*/


  2. UTF-8 디코딩

  이 파서는 유니코드를 기반으로 하고 있고, UTF-8 형식으로 만들어진 XML 문서도 상당수 존재하기 때문에 UTF-8 디코딩을 위한 스트림을 추가로 작성해 두었습니다. (UTF-8 디코딩에 관해서는 여기를 참조)

typedef IReadStream<BYTE> IByteReadStream;
typedef IReadStream<wchar_t> IWStringReadStream;


class CUTF8DecodeStream: public IWStringReadStream
{
protected:
IByteReadStream* source;
wchar_t data;

public:
CUTF8DecodeStream(IByteReadStream& source_stream)
{
source = &source_stream;
data = NULL;
}

wchar_t Next();
wchar_t Next(int aCount)
{
if (aCount < 1 || isEnd()) return NULL;
for (int i = 1; i <= aCount; i++) Next();
return Current();
}

wchar_t Current() const { return data; }
bool isEnd() const { return Current() == NULL; }
};


//--- CUTF8DecodeStream class ---//

wchar_t CUTF8DecodeStream::Next()
{
BYTE firstByte, secondByte, thirdByte;

if (firstByte = source->Next())
{
const unsigned int count = GetUTF8ByteCount(firstByte);

switch (count)
{
case 1: break;

case 2:
secondByte = source->Next();
break;

case 3:
secondByte = source->Next();
thirdByte = source->Next();
break;

default:
{ for (int i = 0; i < count; ++i) source->Next(); }
return 0xFFFF;
}
}

return DecodeUTF8(firstByte, secondByte, thirdByte);
}

BYTE GetUTF8ByteCount(BYTE first)
{
// U-00000000 - U-0000007F: 0xxxxxxx
// U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
// U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
// U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
// U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
// U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

if (first < 0x80U) return 1;
else if (first < 0xE0U) return 2;
else if (first < 0xF0U) return 3;
else if (first < 0xF8U) return 4;
else if (first < 0xFCU) return 5;
else return 6;
}

wchar_t DecodeUTF8(BYTE first, BYTE second, BYTE third)
{
switch( GetUTF8ByteCount(first) )
{
case 1: return first;

case 2: if (second)
return ((first & 0x1F) << 6) | (second & 0x3F);

case 3: if (second && third)
return ((first & 0x0F) << 12) |
((second & 0x3F) << 6) | third & 0x3F;
}

return NULL;
}


/*--- 사용 예 ----*

HANDLE hFile = CreateFileW(
L"filename", GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_RANDOM_ACCESS, NULL
);

CFileReader<BYTE> file_stream(hFile);
CUTF8DecodeStream utf8_stram(file_stream);

while (utf8_stram.Next());
{
wchar_t data = utf8_stram.Current();
...
}

*--------------*/


  3. String Buffer

  마지막으로 문자열 버퍼 클래스를 준비해야 합니다. 이 클래스의 역할은 단순히 한 문자씩 입력받아서 버퍼에 쌓아두고, 나중에 한번에 문자열로 받아오게 해주는 것 뿐입니다. 사실 std::wstring을 아주 가볍게 캡슐화 시킨 것에 불과하고, std::wstring을 그대로 사용해도 별로 상관은 없습니다.

class CWStringBuffer
{
private:
std::wstring buffer;
unsigned int size;

public:
CWStringBuffer() : size(0) {}
CWStringBuffer(unsigned int aSize);
virtual ~CWStringBuffer();

void Capacity(unsigned int aSize);
inline size_t Capacity() { return size; }
inline size_t Length() { return buffer.length(); }
inline bool isEmpty() { return buffer.empty(); }
inline bool isFull() { return size && ( Length() >= (Capacity()-1) ); }

void Clear() { if (!isEmpty()) buffer.clear(); }

bool Push(wchar_t aWChar);

inline const std::wstring& GetString() { return buffer; }
wchar_t GetLastChar();
};


/*--- CWStringBuffer ---*/

CWStringBuffer::CWStringBuffer(unsigned int aSize) { Capacity(aSize); }
CWStringBuffer::~CWStringBuffer() { Clear(); }


void CWStringBuffer::Capacity(unsigned int aSize)
{
size = aSize;
Clear();
buffer.reserve(aSize);
}


bool CWStringBuffer::Push(wchar_t aWChar)
{
if (isFull()) return false;

buffer.push_back(aWChar);
return true;
}


wchar_t CWStringBuffer::GetLastChar()
{
return (!buffer.empty()) ? buffer[Length()-1] : NULL;
}



핑백