Робота з двійковими даними та реєстром Windows на
платформі. NET. h2>
Опис бібліотеки класів AcedUtils.NET. h2>
Андрій Бризки p>
В
статті описується набір класів, які можуть використовуватися для швидкої
роботи з двійковими даними, у тому числі, для запису даних різного типу в
потік, читання з потоку, стискання, шифрування, контролю цілісності даних, а
також для полегшення роботи з реєстром Windows з додатків на платформі. NET.
Вихідний код бібліотеки AcedUtils.NET і демонстраційне додаток додаються
до статті. p>
Передмова h2>
Основний
метою розробки AcedUtils.NET було прагнення створити класи для ефективного
виконання основних операцій з даними, включаючи стиск, шифрування, роботу з
бінарним потоком. Весь код бібліотеки написаний на мові C # і максимально
оптимізовано за швидкодією. p>
Бібліотека
AcedUtils.NET містить наступні класи, що належать простору імен
AcedUtils: p>
AcedBinary
- Містить статичні методи та функції для роботи з двійковими даними, у тому
числі для обчислення контрольної суми Адлера, для копіювання масивів байт і
роботи з масивами чисел типу Int32. p>
AcedRipeMD
- Використовується для обчислення значення однобічної хеш-функції RipeMD-160
масиву байтів або рядка символів. Включає методи для копіювання і порівняння
цифровий сигнатури, перетворення її в значення типу Guid, очищення масиву,
містить цифрову сигнатуру. p>
AcedCast5
- Призначений для шифрування і дешифрування масиву байт методом CAST-128 в
режимі CFB (64 біта). Блочний алгоритм шифрування реалізований відповідно до
RFC 2144. Алгоритм відрізняється високою швидкодією і надійністю. p>
AcedDeflator,
AcedInflator - використовуються для стиснення та розпакування масиву байт з допомогою
алгоритму LZ + Huffman. p>
AcedMemoryReader,
AcedMemoryWriter - призначені для розміщення даних у бінарний потік і читання
з потоку. При використанні цих класів бінарний потік представляється
масивом типу byte [], розмір якого динамічно збільшується в міру
додавання нових даних. При цьому самі дані можуть бути упаковані з
застосуванням оригінального алгоритму стиснення, зашифровані методом CAST-128 і
захищені значенням цифрової сигнатури RipeMD-160. p>
AcedStreamReader,
AcedStreamWriter - призначені для розміщення даних у бінарний потік і читання
даних з потоку. Тут, на відміну від класів AcedMemoryReader і
AcedMemoryWriter, розмір бінарного потоку не обмежується об'ємом оперативної
пам'яті. Дані містяться в потік і читаються з потоку типу System.IO.Stream,
який асоціюється, відповідно, з екземпляром класу AcedStreamWriter
або AcedStreamReader. p>
AcedReaderStream,
AcedWriterStream - класи-оболонки, що дозволяють працювати з перерахованими вище
класами бінарних потоків так, як ніби вони є нащадками класу
System.IO.Stream. p>
AcedRegistry
- Об'єднує методи для збереження в реєстрі Windows значень різного типу,
в тому числі, строк, масивів байт, значень типу Boolean, DateTime, Decimal,
Double і т.д. Крім того, в AcedRegistry знаходяться методи для читання
відповідних значень з реєстру Windows. p>
Розглянемо
докладніше кожний з перерахованих класів. p>
Клас
AcedBinary p>
В
AcedBinary зібрані функції для роботи з двійковими даними, які використовуються
іншими класами у складі AcedUtils.NET. Однак, вони можуть викликатися і з
прикладної програми. Наприклад, функція SwapBytes () звертає порядок проходження
байт в значенні типу System.UInt32, функція ReverseBits () виконує аналогічне
дію з битами у складі подвійного слова. Точніше, розмір початкового значення
може варіюватися від 1 до 32 біт. Функція Adler32 () обчислює значення
контрольної суми Адлера Відповідно до RFC 1950 для масиву байт або його
фрагмента. Даний алгоритм розрахунку контрольної суми відрізняється від CRC32 більшої
продуктивністю. У цьому класі є ще кілька unsafe-методів,
призначених для копіювання масиву байт, швидкого заповнення масиву чисел
типу System.Int32 і копіювання одного такого масиву в інший. p>
Клас
AcedRipeMD p>
Сенс
однобічної хеш-функції полягає в тому, що практично неможливо
підібрати інший набір байт, для якого значення цифровий сигнатури збігалося
б з початковим значенням. Крім того, неможливо відновити стан вихідних
даних за відомим значенням цифрової сигнатури. Клас AcedRipeMD реалізує
алгоритм розрахунку однобічної хеш-функції RipeMD-160 в повній відповідності з
документом: "RIPEMD-160: A Strengthened Version of RIPEMD" (Hans
Dobbertin, Antoon Bosselaers, Bart Preneel), April 18, 1996. Довжина одержуваної
сигнатури складає 20 байт (160 біт). Цифровий сигнатуру зручно представити у
вигляді масиву з 5 елементів типу System.Int32. p>
Щоб
отримати значення однобічної хеш-функції для масиву байтів або рядка
символів, можна скористатися функцією Compute () класу AcedRipeMD. При
передачі в неї масиву байт вказується зміщення і довжина оброблюваного
фрагмента масиву. Є також unsafe-варіант цієї функції, що приймає в
як параметр вказівник на масив байт. Іноді, наприклад, при роботі з потоком
даних, потрібно розрахувати цифрову сигнатуру для масиву байт,
представленого у вигляді декількох фрагментів. У цьому випадку можна застосувати
функції для розрахунку поточного сигнатури RipeMD-160. Для цього спочатку
викликається функція Initialize, яка повертає або заповнює службовий
масив hashData. Потім потрібно послідовно викликати метод Update для кожного
фрагмента даних. У цей метод передається масив hashData, а також посилання на
перший чи наступний фрагмент даних у вигляді масиву байтів або рядка символів,
для якого обчислюється значення сигнатури. Після того, як функція Update
була викликана для кожного фрагмента, можна отримати саме значення цифровий
сигнатури викликом методу Finalize (). p>
Алгоритм
шифрування CAST-128, що використовується при роботі з бінарним потоком класами
Aced (...) Writer/Aced (...) Reader, припускає, що довжина ключа шифру становить 128
біт. Цифрова сигнатура RipeMD-160 як не можна краще підходить для використання
її як ключ для шифрування даних. Однак, вона представляється числом
розміром 160 біт, а не 128. Для вирішення цієї проблеми в клас AcedRipeMD
додана функція ToGuid (). Вона приймає значення 20-байтного цифровий
сигнатури і повертає відповідне йому значення типу System.Guid, розмір
якого становить 128 біт. p>
В
класі AcedRipeMD є ще кілька допоміжних методів, що полегшують
роботу з цифровою сигнатурою, представленої у вигляді масиву з 5 значень типу
System.Int32. Наприклад, функція Copy () дозволяє швидко скопіювати значення
хеш-функції в масив байт або, навпаки, вважати його з масиву байт. Функція
Equals () використовується для перевірки рівності двох значень цифрового сигнатури,
одне з яких може бути представлено масивом байт. Функція Clear () обнуляє
5 елементів масиву типу System.Int32 [], призначеного для зберігання
сигнатури RipeMD-160. p>
Клас
AcedCast5 p>
В
AcedCast5 реалізований алгоритм CAST-128 (CAST5) відповідно до RFC 2144. Це
незапатентовані алгоритм шифрування з ключем розміром 128 біт, що відрізняється
високою швидкодією і стійкістю до різних видів криптоаналізу. При
застосуванні шифру до даних використовується режим зворотного завантаження шіфротекста
(CFB) з розміром блоку вхідних даних 64 біта. Клас AcedCast5 використовується при
шифрування і дешифрування бінарного потоку даних, представленого класами
Aced (...) Writer/Aced (...) Reader. Крім того, він може застосовуватися самостійно
для шифрування довільних даних. p>
Два
основні методи класу AcedCast5, методи Encrypt () і Decrypt (), призначені,
відповідно, для шифрування і дешифрування масиву байт або його фрагменту з
ключем, який задається параметром keyBytes у вигляді 16-байтного масиву. Якщо в
програмі ключ видається значенням типу System.Guid, то відповідний
йому масив байт можна отримати викликом функції Guid.ToByteArray (). Одночасно
з шифруванням в класі AcedCast5 обчислюється значення однобічної хеш-функції
RipeMD-160 для шіфруемих даних. Функція Encrypt () повертає масив з 5
значень типу System.Int32, що представляють собою цифрову сигнатуру фрагмента
даних, розраховану до того, як дані були зашифровані. Функція Decrypt ()
повертає аналогічний масив, який представляє цифрову сигнатуру фрагмента
даних, розраховану після того, як дані були розшифровані. Якщо при
шифрування і дешифрування використаний один і той же ключ і дані в масиві не
були пошкоджені, функції Encrypt () і Decrypt () повинні повернути одне і теж
значення хеш-функції RipeMD-160. Є також unsafe-варіанти цих функцій,
які передається вказівник на масив шіфруемих байт. Крім того, функції
Encrypt () і Decrypt () можуть приймати параметр iv, що задає початковий вектор
для шифрування або дешифрування даних. p>
В
класі AcedCast5 є функцій для шифрування даних, представлених кількома
фрагментами, тобто поточного шифрування. Зокрема, функція ScheduleKey () на
основі ключа шифру keyBytes створює або заповнює спеціальний масив key,
що містить ключову інформацію, що передається потім як ключ в
інші функції, що відносяться до цього розділу. Таким чином, ключовою
масив створюється тільки один раз, а не перед кожним шифруванням наступного
фрагмента даних. Функція GetOrdinaryIV () повертає значення, що може
використовуватися в якості початкового вектора. Це значення виходить
шифруванням нульового вектора за допомогою поточного ключа шифру. Опції Encrypt ()
і Decrypt (), які приймають параметр key, використовуються для безпосереднього
шифрування і дешифрування даних в поточному режимі. Кожна з цих функцій,
крім ключа та посилання на шіфруемие або дешіфруемие дані, приймає параметр
iv, в якому передається значення початкового вектора. Нове значення вектора,
що може використовуватися при наступному виклику функцій Encrypt ()/Decrypt (),
повертається як результат функції. Коли всі дані зашифровані або розшифровані,
потрібно викликати метод ClearKey () для очищення масиву key, що містить ключову
інформацію. А можна замість або після цього передати масив key в метод
ScheduleKey () для заповнення його інформацією про новий ключі для шифрування
інших даних з іншим ключем без пересозданія масиву key. p>
Класи
AcedDeflator і AcedInflator p>
Ці
класи призначені для стиснення та розпакування даних, представлених масивом
байт або його фрагментом. Застосовуваний алгоритм стиснення аналогічний описаному в
RFC 1951 і реалізованого в бібліотеці zlib, але має ряд відмінностей, зокрема,
використовує інший формат блоку даних. Формат описаний у вихідному коді бібліотеки
на початку файлу Compressor.cs. p>
Упаковка
даних проводиться методом Compress () класу AcedDeflator, розпаковування --
методом Decompress () класу AcedInflator. Особливість роботи з цими класами
полягає в тому, що їх примірники не слід створювати безпосередньо викликом
конструктора. Краще замість цього використовувати посилання на єдиний екземпляр
кожного класу, яку можна отримати, звернувшись до статичної властивості
Instance. Це обмеження пов'язане з тим, що при створенні примірників цих
класів, особливо класу AcedDeflator, виділяється значний об'єм пам'яті під
внутрішні масиви. Зазвичай не потрібно використовувати паралельно кілька
примірників архіватора. Крім того, часте перерозподіл пам'яті веде до
зниження продуктивності. При першому зверненні до властивості Instance створюється
один екземпляр відповідного класу. Посилання на нього зберігається в
статичному полі класу і повертається при кожному наступному зверненні до
властивості Instance. Коли виникає необхідність звільнення пам'яті, зайнятої
внутрішніми масивами архіватора, можна викликати статичний метод Release для
обнулення внутрішнього посилання на екземпляр відповідного класу. Тоді, якщо
немає інших посилань на цей екземпляр, при наступному "збирання сміття"
пам'ять буде повернута операційній системі. p>
Для
стиснення даних функцією AcedDeflator.Compress () в неї передається посилання на
масив байт з зазначенням зміщення і довжини стисливого фрагмента даних. Є два
варіанти цієї функції. У першому випадку пам'ять під масив для збереження
упакованих даних розподіляється самою функцією Compress (). Параметри
beforeGap і afterGap цієї функції задають відступ, відповідно, на початку і в
Наприкінці вихідного масиву на випадок, якщо крім упакованих даних у нього повинна
бути поміщена ще якась інформація. У другому випадку у функцію Compress ()
передається посилання на вже існуючий масив, в який мають бути записані
упаковані дані, а також зсув у цьому масиві, з якого починається
запис. Максимальний розмір упакованого фрагмента у випадку, якщо дані
нестисливі, дорівнює довжині початкового фрагмента плюс 4 байти. Таким чином, довжина
приймального масиву має бути достатньою для зберігання початкового фрагмента
даних плюс 4 байти і плюс зсув у цьому масиві. Функція Compress ()
повертає розмір стисненого фрагмента, тобто число байт, збережене у вихідному
масиві. p>
Параметр
типу AcedCompressionMode, що передається у функції Compress (), вибирає режим
стиснення даних. Він приймає одне з наступних значень: NoCompression - дані
не стискуються, а просто копіюються в вхідний масив з додаванням 4-байтного
довжини фрагмента для подальшої його розпакування; Fastest - найшвидший режим
стиснення, який, проте, може бути ефективним для деяких типів
даних; Fast - використовується режим швидкого стиснення, коли максимальне
відстань між повторюваними послідовностями у вхідному потоці
приймається рівним 65535 байтам; Normal - звичайне стиснення, коли максимальне
відстань між послідовностями складає 131071 байт;
MaximumCompression - максимальне стиснення, доступне даному архіватор,
передбачає, що максимальна відстань між повторюваними
послідовностями становить 262143 байти. p>
Стислі
дані розпаковуються методом AcedInflator.Decompress (). Перш ніж викликати
цей метод необхідно підготувати область пам'яті, достатню для зберігання
результату. Дізнатися початковий розмір стиснених даних можна викликом статичної
функції GetDecompressedLength () класу AcedInflator. У неї передається посилання на
масив байт і зсув у цьому масиві, з якого починаються упаковані
дані. Функція повертає довжину фрагмента даних після його розпакування. Потім
можна створити масив байт достатнього розміру і передати його у функцію
Decompress () для заповнення розпакованим даними. Ця функція приймає
посилання на вихідний масив, що містить стислі дані, зсув у цьому масиві, а
також посилання на приймальний масив, в який виконується розпакування, і зміщення у
приймальному масиві. Функція повертає число байт збережене у вихідному масиві.
Є ще інший варіант функції Decompress (), у якому пам'ять під вихідний
масив розподіляється самою функцією. Ця функція приймає параметри beforeGap
і afterGap, які задають число байт, яке треба зарезервувати,
відповідно, на початку і в кінці вихідного масиву. p>
Клас
AcedMemoryWriter p>
Дозволяє
різнотипні зберігати дані в масиві байт, довжина якого динамічно
збільшується в міру додати до нього даних. Потім ці дані представляються
у вигляді масиву типу System.Byte [], який, крім самих даних, що містить їх
контрольну суму і, можливо, значення цифровий сигнатури. Повертає масив
може бути упакований для економії місця і зашифрований для обмеження доступу до
інформації. p>
При
створення екземпляра класу AcedMemoryWriter можна вказати передбачуване число
байт, яке буде поміщено в бінарний потік. Таким чином вдається уникнути
зайвого перерозподілу пам'яті під внутрішній масив. У AcedMemoryWriter є
методи, назви яких починаються з "Write", призначені для
приміщення в потік значень наступних типів: Boolean, Byte, Byte [], Char,
DateTime, Decimal, Single, Double, Guid, Int16, Int32, Int64, SByte, String,
TimeSpan, UInt16, UInt32, UInt64. Крім того, можна додавати відразу фрагменти
масивів з елементами стандартних value-типів за допомогою перевантажених методів
Write (). При цьому вказується індекс першого зберігається елемента масиву і
число записуваних елементів. Загальне число байт, вміщений у бінарний потік,
повертається властивістю Length класу AcedWriter. Метод Reset () обнуляє довжину
потоку, дозволяючи заповнити його новими даними без пересозданія примірника
класу AcedMemoryWriter. p>
Поточна
довжина внутрішнього масиву повертається і встановлюється властивістю Capacity.
Посилання на внутрішній масив можна отримати викликом функції GetBuffer (). Правда,
Це посилання змінюється при кожному перерозподіл пам'яті, тобто при кожному
зміні властивості Capacity. У деяких випадках, наприклад, при читанні даних
з файлу за допомогою FileStream.Read (), зручніше передати посилання на внутрішній
масив безпосередньо в метод FileStream.Read (), замість того, щоб зчитувати
дані в проміжний масив, а потім переписувати їх у потік методом Write ().
Щоб зробити це швидше, потрібно зберегти в тимчасовій змінної поточну
довжину потоку, тобто значення властивості Length, потім викликати метод Skip (),
передаючи в нього число байт, яке буде пр?? читане з файлу. При цьому довжина
потоку збільшиться на вказане число байт без фактичного заповнення їх
даними. Тепер можна отримати посилання на внутрішній масив з функції
GetBuffer (), а потім викликати метод FileStream.Read (), передаючи в нього
отриману посилання на масив і значення, збережене в тимчасовій змінної, в
як зміщення в масиві. p>
Коли
всі необхідні дані записані в бінарний потік, викликається функція ToArray (),
повертає результуючий масив даних. Є декілька варіантів цієї
функції, які відрізняються набором прийнятих параметрів. Найбільш
функціональним є варіант, що приймає два параметри: compressionMode
типу AcedCompressionMode і keyGuid типу System.Guid. Виклик функції ToArray () з
одним параметром еквівалентний передачі значення Guid.Empty в параметрі keyGuid.
Виклик цієї функції без параметрів еквівалентний передачі значення NoCompression
в параметрі compressionMode і значення Guid.Empty в параметрі keyGuid.
Розглянемо докладніше, ніж керують ці параметри і як вони впливають на
зберігається формат даних. p>
Параметр
типу AcedCompressionMode вибирає режим стиснення даних. Його значення
відповідає одній з констант, розглянутих вище при описі класу
AcedDeflator. Якщо цей параметр дорівнює значенню NoCompression, дані бінарного
потоку не стискаються. Параметр keyGuid задає ключ шифрування для вихідного
масиву байт. Якщо цей параметр дорівнює Guid.Empty, шифрування не виконується.
Значення типу System.Guid використовується як ключ шифру з кількох
причин. По-перше, легко згенерувати нове унікальне значення ключа
викликом функції Guid.NewGuid (). По-друге, значення такого типу мають
загальноприйняте строкове подання. По-третє, Guid легко отримати з
значення однобічної хеш-функції RipeMD-160. Якщо ключ шифру вводиться
користувачем з клавіатури у вигляді рядка символів, необхідно перетворити цю
рядок у цифрову сигнатуру викликом AcedRipeMD.Compute (), а потім в значення
типу System.Guid викликом методу ToGuid () класу AcedRipeMD. Шифрування даних
виконується методами класом AcedCast5. Але перш, ніж шифрувати дані, для
них обчислюється значення 20-байтного сигнатури RipeMD-160, що міститься в
вихідний масив разом з даними і використовується при наступному читанні з
потоку для перевірки того, що дані в потоці розшифровані з правильним ключем
і що вони не були пошкоджені. p>
Послідовність
дій при виклику методу ToArray () класу AcedMemoryWriter наступна. Спочатку
виконується упаковка даних класом AcedDeflator. Потім для отриманого масиву
розраховується значення однобічної хеш-функції RipeMD-160 методами класу
AcedRipeMD. Це значення поміщається у вихідний масив перед даними. Потім
дані шифруються методами класу AcedCast5. Значення цифровий сигнатури НЕ
шифрується. На заключному етапі для всього вмісту вихідного масиву
розраховується контрольна сума Адлера викликом методу AcedBinary.Adler32 (),
яка розміщується в перших 4-х байтах вихідного масиву. Заповнений таким
чином масив повертається як результат функції ToArray (). Залежно від
параметрів, можуть опускатися етапи упаковки та/або розрахунку цифрової сигнатури і
шифрування даних. p>
Приклад
використання класу AcedMemoryWriter: p>
private
byte [] PutData () p>
( p>
AcedMemoryWriter w = new
AcedMemoryWriter (); p>
w.WriteByteArray (new byte [] (5, 6,
7, 8, 9 }); p>
w.WriteInt16 (10000); p>
int [] otherValues = new int [120]; p>
for (int i = 0; i <120; i + = 3) p>
( p>
otherValues [i] = 1; p>
otherValues [i + 1] = 2; p>
otherValues [i + 2] = 3; p>
) p>
w.Write (otherValues, 10, 100); p>
w.WriteString ( "Hello
world !"); p>
/////////////////////////////////////////////// /////// p>
//
Варіант 1: дані повертаються як є з p>
//
додаванням контрольної суми Адлера. p>
/////////////////////////////////////////////// /////// p>
return
w.ToArray (); p>
/* p>
/////////////////////////////////////////////// /////// p>
//
Варіант 2: дані стискаються і захищаються p>
//
контрольною сумою Адлера. p>
/////////////////////////////////////////////// /////// p>
return
w.ToArray (AcedCompressionMode.Fast); p>
*/ p>
/* p>
/////////////////////////////////////////////// /////// p>
//
Варіант 3: дані стискаються, шифруються і захищаються p>
//
цифровий сигнатурою RipeMD-160. p>
/////////////////////////////////////////////// /////// p>
return
w.ToArray (AcedCompressionMode.Fast, p>
new
Guid ( "CA761232-ED42-11CE-BACD-00AA0057B223 ")); p>
*/ p>
) p>
В
даному прикладі функція PutData () поміщає в бінарний потік масив байт як
цілий об'єкт, потім значення типу Int16, потім фрагмент масиву елементів типу
Int32, а в кінці - рядок символів. Результатом функції може бути просто
масив байт, що містить дані, записані в потік, захищені контрольної
сумою Адлера. Розмір цього масиву становить 443 байти. Якщо передати в
функцію AcedMemoryWriter.ToArray () параметр compressionMode зі значенням
AcedCompression.Fast, дані бінарного потоку будуть упаковані і розмір отриманого
масиву складе 51 байт. Якщо, крім того, передати якийсь непорожній
значення типу Guid у параметрі keyGuid, стислі дані будуть захищені цифровий
сигнатурою RipeMD-160 і зашифровані методом CAST-128. За рахунок додавання
сигнатури розмір вихідного масиву збільшиться при цьому на 20 байт і складе 71
байт. p>
Клас
AcedMemoryReader p>
Призначений
для читання даних з масиву байт, створеного екземпляром класу
AcedMemoryWriter. У конструктор класу AcedMemoryReader передається посилання на
масив байт з зазначенням фрагмента, що містить дані бінарного потоку. Якщо
дані зашифровані, в останньому параметрі конструктора необхідно передати
значення типу System.Guid, відповідне ключа шифру, який використовувався
при виклику методу ToArray () класу AcedMemoryWriter. Окремі значення можуть
бути прочитані з потоку методами, назви яких складаються з префікса
"Read" і найменування типу читаного значення. Фрагменти масивів,
що складаються з елементів стандартних value-типів, зчитуються методом Read (). Для
повернення поточної позиції на початок потоку, щоб заново прочитати дані,
використовується метод Reset (). Щоб пропустити певну кількість байт у
вхідному потоці викликається метод Skip (). При спробі читання даних за межами
потоку виникає виключення типу AcedReadBeyondTheEndException. p>
Властивість
Position класу AcedMemoryReader повертає індекс наступного зчитує
байти у внутрішньому масиві, посилання на який повертається функцією
GetBuffer (). Розмір внутрішнього масиву визначається властивістю Size. Зсув
у внутрішньому масиві, з якого починаються дані потоку - властивістю Offset.
Якщо вихідний масив байт, переданий в конструктор класу, є
упакованим, в пам'яті створюється новий масив для розпаковані даних. Тоді
функція GetBuffer () повертає посилання на цей тимчасовий масив, а властивість
Offset завжди дорівнює нулю. Якщо ж вихідний масив не є упакованим,
функція GetBuffer () повертає посилання на масив, переданий параметром bytes в
конструктор класу AcedMemoryReader. Якщо дані потоку зашифровані, масив
байт, який передається в конструктор цього класу, розшифровується на місці. Це
означає, що один і той же зашифрований масив байт не можна використовувати для
ініціалізації декількох екземплярів класу AcedMemoryReader. p>
Якщо
при створенні екземпляра класу AcedMemoryReader в конструктор переданий масив
недостатньої довжини або розрахована для нього контрольна сума Адлера не
збігається зі збереженим в потоці значенням контрольної суми, виникає
виняток AcedDataCorruptedException. Якщо після дешифрування даних
виявляється, що розраховане значення цифровий сигнатури RipeMD-160 для даних
потоку не таке саме, як сигнатури, збереженим на початку масиву
даних, виникає виключення AcedWrongDecryptionKeyException, яке є
нащадком від класу AcedDataCorruptedException. p>
Приклад використання класу
AcedMemoryReader: p>
private void GetData (byte []
dataBytes, out byte [] bytes, p>
out short n, out string s, out int []
otherValues) p>
( p>
AcedMemoryReader r = new
AcedMemoryReader (dataBytes, p>
0, dataBytes.Length); p>
/* p>
AcedMemoryReader r = new
AcedMemoryReader (dataBytes, p>
0, dataBytes.Length, p>
new
Guid ( "CA761232-ED42-11CE-BACD-00AA0057B223 ")); p>
*/ p>
bytes = r.ReadByteArray (); p>
n = r.ReadInt16 (); p>
otherValues = new int [120]; p>
r.Read (otherValues, 10, 100); p>
s
= R.ReadString (); p>
) p>
Передбачається,
що масив байт, який передається параметром dataBytes у функцію GetData (), отриманий
як результат функції PutData (), код якої наведено вище в розділі,
описує клас AcedMemoryWriter. Використовуваний тут конструктор класу
AcedMemoryReader припускає, що дані в бінарному потоці не зашифровані.
Закоментовані фрагмент коду містить виклик конструктора з передачею в нього
ключа шифру, відповідного варіанту 3 функції PutData (). p>
Класи
AcedStreamWriter, AcedStreamReader p>
Ці
класи аналогічні описаним вище класами AcedMemoryWriter, AcedMemoryReader.
При їх використанні, однак, дані поміщаються не в масив байт, а в потік
типу System.IO.Stream, асоційований з екземпляром класу AcedStreamWriter, і
читаються не з масиву байт, а з потоку типу System.IO.Stream,
асоційованого з класом AcedStreamReader. p>
При
роботі з класом AcedStreamWriter в пам'яті створюється буфер розміром 2МБ,
який поступово заповнюється даними. При досягненні кінця буфера, виклик
методів Flush () або Close () класу AcedStreamWriter вміст буфера
упаковується методом Compress () класу AcedDeflator. Стислі дані зберігаються
в іншому буфері, розмір якого також становить 2МБ. Для упакованих даних
обчислюється цифрова сигнатура RipeMD-160, після чого дані шифруються методом
CAST-128. Довжина фрагмента даних, контрольна сума Адлера, цифрова сигнатура
RipeMD-160 і самі стислі і зашифровані дані записуються в вихідний потік
типу System.IO.Stream. Після цього вміст буфера очищається і в нього можна
записувати такі дані. При виклику методу Close () класу AcedStreamWriter,
якщо асоційований з ним потік підтримує операцію Seek, потік
позиціонується на початок записаних даних і в потоці зберігається загальна довжина
(в байтах) даних, поміщених у потік класом AcedStreamWriter. Ця величина
представляється значенням типу System.Int64. Якщо операція Seek НЕ
підтримується потоком типу System.IO.Stream, довжина залишається рівною значенню
-1, Записаного в потік при його асоціації з класом AcedStreamWriter. Метод
AssignStream класу AcedStreamWriter використовується, щоб зв'язати даний
екземпляр класу з потоком System.IO.Stream. Крім посилання на потік у цей метод
передається константа, що вибирає режим стиснення даних, а також значення типу
System.Guid, яке, якщо воно відмінно від Guid.Empty, задає ключ для
шифрування даних. Таким чином, залежно від параметрів, переданих в
метод AssignStream, етапи стиснення даних, розрахунку цифрової сигнатури і
шифрування даних можуть опускатися. p>
Щоб
прочитати дані, збережені в потоці System.IO.Stream класом
AcedStreamWriter, потрібно скористатися класом AcedStreamReader. Примірник
цього класу може бути асоційований з потоком типу System.IO.Stream за допомогою
методу AssignStream. Якщо дані, поміщені в потік, зашифровані, при виклику
методу AssignStream слід вказати ключ шифру у вигляді значення типу
System.Guid. У методі AssignStream відразу зчитується довжина фрагмента даних, поміщеного
в потік класом AcedStreamWriter. Це значення повертається властивістю Length
класу AcedStreamReader. Довжина може бути дорівнює значенню -1, якщо не було
можливості зберегти в потоці справжнє значення довжини. У примірнику класу
AcedStreamReader також є два буфера, кожен розміром з 2МБ. Перший
призначений для даних, лічених з потоку System.IO.Stream, другий - для
розпаковані даних. Коли викликається один з методів Read ... класу
AcedStreamReader, спочатку робиться спроба вважати значення з буфера
розпаковані даних. Якщо досягнуто кінець буфера, з потоку System.IO.Stream
зчитується наступний фрагмент даних. Для цього фрагмента перевіряється значення
контрольної суми Адлера. Потім, якщо дані зашифровані, виконується їх дешифрування
і перевірка цифрового сигнатури RipeMD-160. Потім, якщо дані упаковані,
проводиться їх розпакування в другій буфер. Тепер значення може бути
прочитано та повернено функцією Read. ... При читанні з потоку довгих масивів
перевантаженим методом Read () класу AcedStreamReader можлива ситуація, коли
для зчитування всього масиву доводиться кілька разів заповнювати внутрішній
буфер даними з потоку System.IO.Stream. p>
Так
як екземпляри класів AcedStreamWriter і AcedStreamReader займають собою
значний обсяг пам'яті (кожен понад 4МБ), створювати їх при кожному читанні з
потоку нераціонально. Складальник сміття в. NET Framework автоматично відносить
блоки пам'яті понад 85000 байт до другого покоління (про це див у книзі Джеффрі
Ріхтера "Програмування на платформі. NET Framework" - M.:
Видавничо-торговий дім "Русская Редакция", 2003). Такі блоки краще
використовувати для ресурсів з тривалим часом існування. В іншому
випадку, часте пересозданіе великих блоків пам'яті негативно позначається на
загальної продуктивності програми. Для вирішення цієї проблеми у класах
AcedStreamWriter і AcedStreamReader є статичне властивість Instance,
яке при першому зверненні до нього створює екземпляр відповідного класу,
а при наступних зверненнях просто повертає посилання на існуючий екземпляр.
Тоді, замість того, щоб створювати нові екземпляри класів викликом
відповідних конструкторів, краще скористатися єдиним екземпляром,
що повертається властивістю Instance. Цей підхід, аналогічний тому, який
застосовується в класах AcedDeflator і AcedInflator. Щоб звільнити займану
пам'ять можна викликати статичний метод Release (), що звільняє посилання на
екземпляр відповідного класу. p>
Після
приміщення всіх даних у потік AcedStreamWriter, а також після читання
необхідних даних з потоку AcedStreamReader, потрібно викликати метод Close () для
виконання завершальних дій. Якщо в параметрі closeStream методу Close ()
передано значення True, потік типу System.IO.Stream, асоційований з даними
екземпляром класу AcedStreamWriter або AcedStreamReader, закривається викликом
методу Close () потоку. p>
Класи
AcedWriterStream, AcedReaderStream p>
Ці
класи являють собою оболонку над іншими класами, призначеними для
роботи з бінарним потоком. Вони використовуються, коли треба представити екземпляри
інших класів у вигляді об'єктів, похідних від класу System.IO.Stream. p>
Клас
AcedWriterStream є нащадком класу System.IO.Stream і призначений для
запису даних у потоки типу AcedMemoryWriter і AcedStreamWriter. У його
конструктор передається посилання на інтерфейс IAcedWriter, який підтримується
класами AcedMemoryWriter і AcedStreamWriter. Клас AcedWriterStream
використовується тільки для запису даних у потік, тому його властивості CanRead і
CanSeek повертають значення False, а властивість CanWrite - значення True. Виклик
методів Write (), WriteByte (), Flush (), Close () перенаправляється відповідним
методів об'єкта, що реалізує інтерфейс IAcedWriter. При читанні властивостей Length
і Position повертається число байт, вміщений у вихідний бінарний потік.
Однак, присвоєння значення властивості Position або виклик методів Read (),
ReadByte (), Seek (), SetLength () призводить до виникнення виключення типу
System.NotSupportedException. Властивість Writer класу AcedWriterStream
повертає посилання на об'єкт, який реалізує інтерфейс IAcedWriter, яка була
передана в конструктор класу при його створенні. p>
Аналогічним
чином застосовується клас AcedReaderStream, який також є нащадком
класу System.IO.Stream. Цей клас призначений для читання даних з потоків
типу AcedMemoryReader і AcedStreamReader, що реалізують інтерфейс IAcedReader.
Клас AcedReaderStream призначений виключно для читання даних, тому
його властивість CanRead повертає значення True, а властивості CanWrite і CanSeek
повертають значення False. Виклик методів Read (), ReadByte (), Close ()
перенаправляється відповідним методам об'єкта, що реалізує інтерфейс
IAcedReader. При читанні властивості Position повертається поточна позиція в потоці
щодо початку даних. Властивість Length повертає загальне число байт,
яке може бути прочитане з потоку. У деяких випадках кількість байт,
вміщений у потік, невідомо. Тоді властивість Length повертає значення -1.
Спроба присвоєння значення властивості Position або дзвінка одного з наступних
методів: Seek (), SetLength (), Write (), WriteByte () закінчується виникненням
виключення типу System.NotSupportedException. Властивість Reader класу
AcedReaderStream повертає інтерфейс IAcedReader, переданий в конструктор класу.
Докладну інформацію про властивості і методи інтерфейсів IAcedWriter, IAcedReader
можна знайти у файлі Interfaces.cs вихідного коду. p>
Клас
AcedRegistry p>
AcedRegistry
заповнює собою відсутність в. NET Framework класу, подібного класу TRegistry
в Borland Delphi. Його особливістю в порівнянні зі стандартним класом
Microsoft.Win32.Registry є наявність спеціальних методів для приміщення в
реєстр значень різного типу і для читання відповідних значень з
реєстру. Клас AcedRegistry включає методи для роботи з даними, які
представлені значеннями наступних типів: String, Byte [], Int32, Boolean,
DateTime, Decimal, Double, Guid, Int64. p>
Робота
з класом AcedRegistry починається з виклику конструктора, який приймає три
параметри: перший (registryBaseKey) - вибирає гілку реєстру, таку як
HKEY_CURRENT_USER або HKEY_LOCAL_MACHINE; другий параметр (registryKey)
вказує найменування ключа реєстру, з яким передбачається працювати; третій
параметр задає режим роботи: тільки читання або читання/запис. Якщо зазначений
ключ не існує, то при відкритті його в режимі "тільки для читання"
помилка не виникає. Тоді кожне звернення до функцій Get () для читання
значень ключа поверне False, а при виклику GetDef () буде повернутоаться значення
за умовчанням. При відкритті ключа в режимі, допускає запис, якщо він
відсутній, відповідний ключ негайно створюється в реєстрі. Звернутися до
об'єкту типу Microsoft.Win32.RegistryKey, який представляє відкритий ключ
реєстру, можна через властивість RegistryKey класу AcedRegistry. p>
Перевантажений
метод Put () призначений для запису в реєстр значень різного типу. Функція
Get () зчитує значення з вказаним ім'ям і зберігає його в змінної,
що передається як ref-параметр. Якщо запитувана значення присутній у
реєстрі, функція повертає True. Функція GetDef () відрізняється від Get () тим, що
вона повертає прочитане значення як результат функції. Якщо відповідне
значення не міститься в реєстрі, GetDef () повертає значення за замовчуванням,
яке задається другим параметром при виклику цієї функції. p>
В
Наприкінці роботи з екземпляром класу AcedRegistry для нього обов'язково треба
викликати метод Dispose (). При використанні мови C # зручно помістити створення
класу AcedRegistry в блок using для гарантованого звільнення
unmanaged-ресурсів. p>
Приклад використання класу
AcedRegistry: p>
private const string p>
DemoRegistryKey =
"SoftwareAcedUtils.NETDemo", p>
cfgStreamFileName =
"StreamFileName", p>
cfgCompressionMode =
"CompressionMode"; p>
private static string _streamFileName
= String.Empty; p>
private static AcedCompressionMode
_compressionMode; p>
private static void LoadConfig () p>
( p>
using (AcedRegistry config = new
AcedRegistry p>
(AcedBaseKey.CurrentUser,
DemoRegistryKey, false)) p>
( p>
config.Get (cfgStreamFileName, ref p>
_streamFileName); p>
_compressionMode =
(AcedCompressionMode) p>
config.GetDef (cfgCompressionMode,
0); p>
) p>
) p>
private static void SaveConfig () p>
( p>
using (AcedRegistry config = new
AcedRegistry p>
(AcedBaseKey.CurrentUser,
DemoRegistryKey, true)) p>
( p>
config.Put (cfgStreamFileName, _streamFileName); p>
config.Put (cfgCompressionMode, (int) p>
_compressionMode); p>
))
) p>
Даний
приклад взятий з демонстраційного проекту, який додається до статті. Значення
статичних полів _streamFileName і _compressionMode зберігаються в реєстрі
методом SaveConfig () і зчитуються з реєстру методом LoadConfig (). Тип
AcedCompressionMode являє собою перерахування, яке потрібно привести до
типу System.Int32, щоб помістити його до реєстру. Після читання з реєстру з
допомогою GetDef () значення повинне бути перетворено назад до типу
AcedCompressionMode. p>
Опис
демонстраційного проектаа p>
Щоб
проілюструвати різні способи роботи з двійковими даними за допомогою
розглянутих вище класів, розроблено невеликий додаток, яке
являє собою примітивний аналог архіватора файлів. Верхня частина головної
форми використовується для приміщення в бінарний потік даних довільних файлів.
При натисненні на кнопку "Додати файл" користувачеві пропонується
вибрати на диску файл, який буде доданий в потік. Після приміщення в потік
одного або декількох файлів можна зберегти весь потік на диску у вигляді одного
файлу. Причому дані при цьому можуть бути упаковані й зашифровані. Щоб
перевірити механізм контролю цілісності даних, можна трохи пошкодити дані
у вихідному потоці при збереженні його на диску. Для цього потрібно позначити опцію
"Інвертувати третій байт". Нижня частина форми дозволяє завантажити
дані потоку з файлу на диску. Якщо потік зашифрований, перед читанням з диска
треба встановити опці