Як працювати з бінарними даними для створення власного формату файлів?


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

У цій статті ми поговоримо про те, як можна прочитувати і записувати двійкові дані. У керівництві ми використовуватимемо псевдокод, але Ви можете писати зручною для Вас мовою програмування, яка підтримує базові операції введення/виведення.

Бітові операції

Якщо Ви не знаєте про бітові операції, то в коді зустрінете незрозумілі символи, зокрема: &, |, << і >>. Це стандартні бітові операції для роботи з двійковим представленням чисел, доступні у більшості мов програмування.

Порядок байтів і потоки

До того як розпочати, давайте розберемо два важливих визначення: порядок байтів (endiannes) і потоки.

Порядок байтів визначає – як це не дивно – порядок байтів (вибачте за тавтологію). Припустимо, що у нас є 16-бітове число зі значенням 0x1020. У двійковому виді число може бути представлене по-різному: байт 0x20, а слідом за ним байт зі значенням 0x10 (це зворотний порядок байтів) або 0x10, після якого стоїть байт 0x20 (це прямий порядок байтів).

Потоки – це подібні до масивів об’єкти, які містять послідовність байтів (у деяких випадках біт). Двійкові дані прочитуються і записуються в ці потоки.

Прочитування двійкових даних

Давайте розпочнемо з визначення деяких полів. В ідеалі всі вони мають бути в секції private:

__stream // об'єкт, подібний до масиву і що містить байти__endian // порядок даних у потоці
__length // кількість байтів у потоці__position // положення наступного байта для читання з потоку

Ось так може виглядати конструктор нашого класу:

class DataInput ( stream, endian ) { __stream = stream __endian = endian __length = stream.length __position = 0}

Наступні функції читатимуть з потоку цілі беззнакові числа:

// читання 8-бітового беззнакового цілого числаfunction readU8 (){ // виведемо виняток, якщо більше немає байтів для прочитування if ( __position >= __length ) { throw new Exception ( ".". ) } // повертаємо значення байта і збільшуємо положення наступного байта для його коректного прочитування return __stream[ __position ++]} // читання 16-бітового беззнакового цілого числа
function readU16 (){ value = 0 // оскільки число складається з декількох байтів, то обробляємо 2 випадки if ( __endian == BIG_ENDIAN ) { //прямий порядок байтів value |= readU8 () << 8 value |= readU8 () << 0 } else { // зворотний порядок байтів value |= readU8 () << 0 value |= readU8 () << 8 } return value} // читання 24-бітового беззнакового цілого числаfunction readU24 (){ value = 0 if ( __endian == BIG_ENDIAN ) { value |= readU8 () << 16 value |= readU8 () << 8 value |= readU8 () << 0 } else { value |= readU8 () << 0 value |= readU8 () << 8 value |= readU8 () << 16 } return value} // читання 32-бітового беззнакового цілого числаfunction readU32 (){ value = 0 if ( __endian == BIG_ENDIAN ) { value |= readU8 () << 24 value |= readU8 () << 16 value |= readU8 () << 8
value |= readU8 () << 0 } else { value |= readU8 () << 0 value |= readU8 () << 8 value |= readU8 () << 16 value |= readU8 () << 24 } return value}

Ці функції прочитуватимуть знакові числа:

// читання 8-бітового знакового цілого числа function readS8 (){ // прочитуємо беззнакове число value = readU8 () // дивимося старший біт (знак числа, що означає) if ( value >> 7 == 1 ) { // використовуємо додатковий код для конвертації значення value = ~ ( value ^ 0xff ) } return value} // читання 16-бітового знакового цілого числаfunction readS16 (){ value = readU16 () if ( value >> 15 == 1 ) { value = ~ ( value ^ 0xffff ) } return value} // читання 24-бітового знакового цілого числаfunction readS24 (){ value = readU24 () if ( value >> 23 == 1 ) {
value = ~ ( value ^ 0xffffff ) } return value} // читання 32-бітового знакового цілого числаfunction readS32 (){ value = readU32 () if ( value >> 31 == 1 ) { value = ~ ( value ^ 0xffffffff ) } return value}

Запис двійкових даних

Як і в прикладі, наведеному вище, розпочнемо з визначення полів нашого класу. Відмінностей буде небагато, але вони все ж будуть. Як зазначалося вище, в ідеалі всі представлені нижче поля мають бути в секції private:

__stream // об'єкт, подібний до масиву і що містить байти__endian // порядок даних в потоці__position // положення наступного байта для запису в потік

Ось так виглядатиме конструктор класу :

class DataOutput ( stream, endian ) { __stream = stream __endian = endian
__position = 0}

Наступні функції записуватимуть у потік цілі беззнакові числа:

// запис 8-бітового беззнакового цілого числаfunction writeU8 ( value ) { // наступний рядок забезпечує беззнаковость value &= 0xff // додаємо значення в потік і збільшуємо положення наступного байта __stream[ __position ++] = value} // запис 16-бітового беззнакового цілого числаfunction writeU16 ( value ) { value &= 0xffff // скоректуємо число залежно від порядку байтів if ( __endian == BIG_ENDIAN ) { //прямий порядок writeU8 ( value >> 8 ) writeU8 ( value >> 0 ) } else { // зворотний порядок writeU8 ( value >> 0 ) writeU8 ( value >> 8 )  }} // запис 24-бітового беззнакового цілого числаfunction writeU24 ( value ) { value &= 0xffffff
if ( __endian == BIG_ENDIAN ) { writeU8 ( value >> 16 ) writeU8 ( value >> 8 ) writeU8 ( value >> 0 ) } else { writeU8 ( value >> 0 ) writeU8 ( value >> 8 ) writeU8 ( value >> 16 )  }} // запис 32-бітового беззнакового цілого числаfunction writeU32 ( value ) { value &= 0xffffffff if ( __endian == BIG_ENDIAN ) { writeU8 ( value >> 24 ) writeU8 ( value >> 16 ) writeU8 ( value >> 8 ) writeU8 ( value >> 0 ) } else { writeU8 ( value >> 0 ) writeU8 ( value >> 8 ) writeU8 ( value >> 16 ) writeU8 ( value >> 24 )  }}

А тепер залишилося реалізувати декілька функцій, що записують знакові числа, проте можна використати аналогічні методи, які працюють з беззнаковими числами. Однак для повноти API краще за все визначити ці методи:

// запис 8-бітового беззнакового цілого числаfunction writeS8 ( value ) { writeU8 ( value )} // запис 16-бітового беззнакового цілого числаfunction writeS16 ( value ) { writeU16 ( value )} // запис 24-бітового беззнакового цілого числаfunction writeS24 ( value ) { writeU24 ( value )} // запис 32-бітового беззнакового цілого числаfunction writeS32 ( value ) { writeU32 ( value )}

Висновок

І на цьому все! Тепер Ви знаєте, як можна здійснити читання і запис двійкових даних.

Переклад статті “How to Read and Write Binary Data for Your Custom File Formats”

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *