Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby pack·unpack 메서드 완벽 가이드: 이진 데이터 읽고 변환하기

이 글에서는 Ruby의 packunpack 메서드를 활용하는 방법을 배워보겠습니다.

그런데 왜 굳이 이 메서드들이 필요할까요?

텍스트를 다루는 일은 이진(binary) 데이터를 다루는 일보다 훨씬 쉽기 때문입니다.

텍스트라면 다음과 같은 도구들을 자유롭게 사용할 수 있습니다.

  • 정규표현식(regular expressions)
  • scan, match 같은 메서드
  • gsub

하지만 이진 데이터를 다루려면 약간의 추가 작업이 필요합니다. 바로 이 지점에서 Array#packString#unpack 메서드가 빛을 발합니다.

먼저 간단한 문자열 예제부터 시작해서, 점점 더 흥미로운 활용 사례까지 살펴보겠습니다.

문자열을 ASCII 값으로 변환하기

다음 코드는 문자열의 모든 문자를 십진수 값으로 변환합니다.

str = "AABBCC"
str.unpack("c*")

# [65, 65, 66, 66, 67, 67]

unpack에 전달된 "c*" 인자에 주목해 주세요.

이것은 포맷 문자열(format string)로, unpack에게 데이터를 어떻게 처리할지 알려주는 역할을 합니다. 여기서 c는 한 글자를 가져와 정수 값으로 변환하라는 의미입니다(String#ord 메서드와 동일한 동작입니다).

그리고 별표 *는 "입력되는 모든 데이터에 대해 이 포맷을 반복 적용하라"는 뜻입니다.

16진수(Hex)를 문자열로 변환하기

pack 메서드와 함께 H 디렉티브를 사용하면 16진수 숫자를 문자열로 변환할 수 있습니다.

예제:

["414243"].pack("H*")
# "ABC"

"ABC".unpack("H*")
# ["414243"]

16진수를 정수로 변환하는 방법

다음 포맷 문자열은 4바이트의 데이터를 받아 하나의 정수를 반환합니다. 한 가지 주의할 점은 이 바이트들이 리틀 엔디안(little-endian) 형식이라는 것입니다.

예제:

"\xff\x00\x00\x00".unpack("l").first
# 255
"\x90\xC0\xDD\x08".unpack("l").first
# 148750480

first를 사용한 이유는 unpack이 항상 배열을 반환하기 때문입니다.

unpack 메서드로 이진 파일 파싱하기

EXE, PNG, GZIP 같은 이진 파일은 어떻게 읽어야 할까요?

이런 파일들을 일반 텍스트처럼 열어보면 무작위 데이터처럼 보이는 내용이 출력됩니다.

Ruby pack·unpack 메서드 완벽 가이드: 이진 데이터 읽고 변환하기

하지만 이것은 무작위 데이터가 아닙니다.

대부분의 파일 형식에는 공개된 구조 명세(documented structure)가 존재하며, unpack 메서드를 사용하면 그 데이터를 읽어 유용한 형태로 변환할 수 있습니다.

예제를 살펴보겠습니다:

binary_data     = "\x05\x00\x68\x65\x6c\x6c\x6f"
length, message = binary_data.unpack("Sa*")

# [5, "hello"]

이 예제에서 이진 데이터(1과 0보다 훨씬 간결한 16진수로 표현됨)는 두 바이트(16비트) 크기의 길이 필드로 시작하며, 이 필드에는 뒤따르는 문자열의 길이가 담겨 있습니다. 그다음에 실제 문자열이 위치합니다.

이진 파일이나 이진 네트워크 프로토콜에 "길이(length)" 필드가 포함되는 경우는 매우 흔합니다.

이 필드는 파서가 정확히 몇 바이트를 읽어야 하는지 알려주는 역할을 합니다.

네, 이 예제에서는 길이와 데이터를 한 번에 읽었습니다. 설명을 단순하게 유지하기 위함일 뿐입니다.

BinData 젬(Gem) 활용법

이진 구조를 파싱하도록 특별히 만들어진 bindata 젬도 있습니다.

예제:

class BinaryString < BinData::Record
  endian :little
  uint16 :len
  string :name, :read_length => :len
end

read_length 파라미터에 주목하세요. 이 옵션은 bindata에게 필드 값으로부터 길이를 자동으로 계산하도록 지시하므로, 수작업을 크게 줄여줍니다. 🙂

만약 어떤 이진 형식이든 파서를 직접 작성하고 싶다면, 다음 단계를 따라가면 됩니다.

  1. 해당 형식의 명세(specification)를 찾습니다. (공개되어 있지 않다면 리버스 엔지니어링을 해야 하는데, 이것만으로도 하나의 거대한 주제입니다.)
  2. 파일의 각 섹션마다 bindata 클래스를 작성합니다. (보통 먼저 메타데이터를 담은 헤더 섹션이 나오고, 그 뒤에 여러 개의 데이터 섹션이 이어집니다.)
  3. 데이터를 읽고 원하는 대로 처리합니다. (예를 들어 PNG라면 이미지의 색상을 변경할 수도 있습니다.)
  4. 성공입니다!

bindata의 실제 동작 예시가 궁금하다면, 제가 GitHub에 공개한 PNG 파서를 참고해 보세요.

Base64 인코딩

"Base64"라는 인코딩 방식이 있습니다. URL에서 한 번쯤은 본 적이 있을 겁니다.

대략 이런 모습입니다:

U2VuZCByZWluZm9yY2VtZW50cw==

끝에 붙은 더블 이퀄(==)이 보통은 Base64를 판별하는 단서가 됩니다. 다만 입력값에 따라 이퀄 기호가 없는 경우도 있습니다(이 기호들은 패딩 용도로 사용됩니다).

그래서 이 얘기를 왜 하는 걸까요?

그 자체로 유용한 지식이라는 것 외에도 이유가 있습니다.

사실 pack 메서드를 사용하면 문자열을 Base64로 변환할 수 있기 때문입니다.

아래 예제를 확인해 보세요:

def encode64(bin)
  [bin].pack("m")
end

encode64 "abcd"

# "YWJjZA==\n"

실제로 이것은 표준 라이브러리의 Base64 모듈 내부에서 사용되는 정확히 동일한 방식입니다.

정리

이번 글에서는 이진 데이터를 다루는 데 도움이 되는 packunpack 메서드에 대해 배웠습니다. 이 메서드들을 활용하면 이진 파일을 파싱하거나, 문자열을 ASCII 값으로 변환하거나, Base64 인코딩을 수행할 수 있습니다.

이런 유용한 글을 계속 받아보려면 공유와 구독을 잊지 마세요! 🙂