5 - шифрование, обработка символьных строк

Лабораторная работа 5 для студентов курса "Основы программирования" 1 курса кафедры ИУ5 МГТУ им Н.Э. Баумана.

Содержание

Цель работы

Научиться работать с файлами и c-style строками, используя файловые потоки ввода/вывода и строковые функции. Освоить базовые навыки работы с CMake. Более подробно познакомиться со структурами. Понять, как использовать аргументы командной строки.

Начало работы

Зайдите в свою локальную директорию с репозиторием для выполнения лабораторных работ. Заберите ветку с соответствующей лабораторной работой из общего репозитория (в лабораторной работе 0 был отмечен меткой upstream):

git pull upstream

или

git pull upstream lab_5

Переключитесь на ветку с текущей лабораторной работой:

git checkout lab_5

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

git push --set-upstream origin lab_5

Указания по выполнению лабораторной работы

Для данной лабораторной работы действуют все требования, указанные в третьей лабораторной работе (за исключением функций для выбора и запуска задания и обеспечения циклического запроса на продолжение работы приложения, в силу отсутствия их необходимости, при этом функция, отвечающая за запуск самого приложения все равно должна остаться).

Внимание!

В данной лабораторной работе требуется самостоятельно тщательно продумать набор интерфейсных функций, которые пойдут в заголовочный файл.

Начиная с этой лабораторной работы, требуется использовать CMake для сборки проекта. Пока достаточно (и необходимо) использовать ту информацию, что указана в лабораторной работе 0.4.

Обратите внимание: пайплайн автоматической проверки на GitHub изменен с учетом новых требований. Для корректной работы в CMakeLists.txt должна быть указана кастомная цель с названием clang-format (пример приведен в методических указаниях для лабораторной работы 0.4).

Общие советы

  • Для корректной обработки данных следует открывать файлы в бинарном режиме (std::ios::binary);
  • Перед непосредственно самим шифрованием, целесообразно сначала сформировать массив целочисленных ключей на основе кодового блокнота. Так как размер такого массива неизвестен до полной обработки файла, следует использовать динамический массив, реализация которого была показана на лекциях (добавить в проект в виде отдельных файлов). Либо, если на текущий момент это покажется слишком сложным, посчитать размер массива, предварительно пройдясь по файлу и посчитав количество слов;
  • Для простоты разработки зафиксировать максимально возможную длину слова. Если количество символов в текущем слове превышено, то считать следующую часть новым словом. Например, если размер слова установлен в 8 символов (в реальности должно быть намного больше), и обрабатывается слово abcdefghijk, то оно будет состоять из двух слов: abcdefgh и ijk;
  • Статистические данные можно хранить в статическом массиве структур. Размер массива - 128 элементов. Тогда индексами этого массива будут соответствующие символы из таблицы ASCII;
  • При считывании строки учитывайте, что она может прерваться прям посередине конкретного слова. Для простоты разработки можно считать, что при считывании следующей строки оставшаяся часть будет новым словом;
  • Помните, что кодовый блокнот может содержать одну или несколько пустых строчек подряд. Пустая строчка не считается словом.

Шифр Цезаря

Подробнее можно прочитать тут.

Работа с файлами

  • Для работы с файлами используются классы std::ifstream, std::ofstream и std::fstream для чтения, записи и чтения + записи соответственно. Используйте более специфичный класс (std::ifstream или std::ofstream), если собираетесь только читать из файла или только записывать в него;
  • std::ofstream или std::fstream с флагом std::ios::out создадут файл, если он отсутствует;
  • Файлы открываются по пути относительно той директории, из которой запускается приложение;
  • Обязательно проверяйте, успешно ли открылся файл перед работой с ним;
  • Помните про различные режимы работы с файлами;
  • Не забывайте про буферизованный вывод в файлы. Информация не будет записана в файл, пока буфер не будет сброшен либо вручную (std::flush(), запись std::endl), либо автоматически при закрытии файла (std::close());
  • Несмотря на то, что в C++ файлы, открытые через потоковые классы, закрываются автоматически (согласно идиоме RAII), на данном курсе все равно требуется закрывать файлы вручную через метод std::close();
  • В данной лабораторной работе для чтения из файла целесообразно использовать методы std::getline() (не путать с функцией std::getline()) и std::read();
  • std::getline() считывает в символьный буфер (массив) до указанного количества символов count - 1 или до определенного делиметра (разделяющий знак, по умолчанию это \n), или до конца строки. Следующий символ в буфере после последнего успешно записанного будет занулен (с индексом count - 1, если было считано count - 1 элементов), остальная часть останется неизменной. То есть, если подать аргумент count = 10, то в буфер считается максимум 9 символов (с индексами от 0 до 8 включительно), следующий символ (максимум с индексом 9) будет занулен;
    • Узнать количество успешно считанных (но не записанных) символов можно с помощью метода gcount();
    • Понять, был ли достигнут конец файла, можно с помощью метода std::eof();
    • В примере ниже показано, как можно организовать циклическое считывание с помощью std::getline(), пока не будет достигнут конец файла. Учтите, что std::getline() выставляет бит ошибки, если было считано count - 1 символов. Поэтому необходимо вызывать std::clear():
// в реальности стоит сделать размер буфера побольше, в районе 1024
while (f.getline(buf, 50) || !f.eof()) {
    ...
    f.clear();
}
  • std::read() считывает в символьный буфер до указанного количества символов count. Никакие символы буфера не зануляются. Количество успешно считанных (и записанных) символов можно узнать через std::gcount();
  • Для записи в файл в данной лабораторной работе следует использовать метод std::write().

Работа со строками

  • При работе со строковыми функциями учитывайте, что они расчитаны на взаимодействие с корректными c-style строками. Это значит, что конец строки определяется символом \0 (ASCII код 0). Учитывайте это при передаче размера символьного буфера разным функциям (в зависимости от конкретной функции размер может отличаться на 1);
  • Строковые функции перечислены в заголовочном файле cstring;
  • Функция std::strlen() считает количество символов в строке до первого \0 (не включая его). В общем случае это означает, как правило, что размер строки меньше размера соответствующего ей буфера на 1. Если строка не нуль-терминирована (т.е. не является c-style строкой), то поведение функции неопределено;
  • Функция std::strcpy() копирует содержимое одной строки в другую (включая \0). Использование std::strcpy() запрещено, вместо этого следует использовать более безопасную std::strncpy();
  • Функции std::strchr() и std::strstr() находят символ и подстроку в строке соответственно (возвращают указатель на начало этого символа/подстроки в строке);
  • Функция std::strtok() модифицирует исходную строчку, постепенно разбивая ее на токены (лексемы), разделяемые списком делиметров (разделителей), и возвращает указатель на очередной токен в исходной строке:
    • Список делиметров содержит последовательность символов, которые функция будет игнорировать при поиске очередного токена (то есть последовательности символов, не содержащих символов из списка делиметров). Список делиметров можно менять при каждом вызове функции (если есть необходимость);
    • Функция std::strtok() хранит в себе состояние. Строку надо подать только один раз при первом вызове функции. В последующие вызовы в качестве первого аргумента (строки, которая разбивается на токены) подается nullptr;
    • После каждого последовательного вызова возвращается указатель на часть исходной строки - очередной токен, ограниченный следующим делиметром, который был заменен на \0;
    • Исходная строка безвозвратно меняет свое состояние с каждым вызовом функции (если находятся новые токены);
  • Про остальные строковые функции можно прочитать тут.

Пример

TBD

Задание

Провести кодирование и декодирование текста с помощью шифра Цезаря, используя в качестве алфавита стандартную таблицу ASCII. Величина сдвига для каждой позиции в исходном тексте - сумма (по модулю 128) кодов символов слова кодового блокнота, стоящего в блокноте на той же позиции. Если кодовый блокнот имеет количество слов меньше, чем количество символов в исходном тексте, то по исчерпании слов в нем перейти к первому слову и продолжить.

Слово определяется как непрерывная последовательность букв и/или цифр, разделяемых знаком пунктуации/пробелом/признаком концом строки. Знаком пунктуации считается любой символ, для которого функция std::ispunct возвращает не 0.

$$ \sigma(s_i) = [\gamma(s_i) + \sum_{j = 0}^{|\lambda_i| - 1} \gamma(l_j)] \mod 128, l \in \lambda_i $$

где:

  • $s_i$ - i-й символ;
  • $\sigma(s_i)$ - ASCII код закодированного i-го символа;
  • $\gamma(s_i)$ - исходный ASCII код i-го символа;
  • $\lambda_i$ - i-е слово в кодовом блокноте;
  • $|\lambda_i|$ - длина i-го слова в кодовом блокноте;
  • $l_j$ - j-й символ i-го слова в кодовом блокноте;
  • $\gamma(l_j)$ - ASCII код j-го символа i-го слова в кодовом блокноте.

Приложение должно работать с четырьмя файлами:

  • Исходный текст;
  • Кодовый блокнот;
  • Закодированный текст;
  • Расшифрованный текст.

Названия всех четырех файлов подаются пользователем при запуске приложения через аргументы командной строки. Приложение должно убедиться, что первые два файла существуют. Последние два приложение должно либо создать самостоятельно (если они отсутствуют), либо полностью удалить их текущее содержимое перед записью.

Допустимо подавать аргументы в заданном порядке. В идеале (но не обязательно), должна быть предусмотрена дополнительная возможность подать файлы в случайном порядке, используя именованные аргументы (например, ./app --some-arg-name=filename.txt --second-arg-name=filename2.txt). Парсинг аргументов осуществляется вручную.

В репозитории этих файлов быть не должно.

Составить и вывести в виде таблицы статистику в следующем виде:

  • Символ из исходного текста;
  • Код символа исходного текста в таблице ASCII;
  • Сколько раз встречается этот символ в исходном тексте;
  • Сколько получилось различных вариантов шифрования этого символа;
  • Размер блокнота (количество слов);
  • Длина исходного текста;
  • Дополнительные данные на усмотрение студента.

Таблица не должна сразу выводиться целиком. Предусмотреть последовательный вывод таблицы (например, по 5 строк за раз) с возможностью переключаться вперед и назад (предыдущие и следующие строки). Неиспользуемые символы в таблицу попасть не должны.

Опционально: обеспечить возможность вывода отдельной строки таблицы для конкретного символа (как по самому символу, так и по его коду). Продумать поведение приложение при чередовании режимов последовательного вывода и поиска по символу.

Дать возможность нативно прервать приложение (т.е. не только подавая соответствующий сигнал через Ctrl+C).

Перед демонстрацией работы убедиться, что исходный (кодируемый) и расшифрованный файлы не отличаются друг от друга. Сделать это можно утилитой diff:

diff file1.txt file2.txt

Если они одинаковые, то команда не выведет ничего, а "echo $?" выведет 0. Подробнее можно узнать в справке (для этого надо запустить утилиту с соответствующим флагом).

Для адекватной работы для кодируемого файла и кодового блокнота необходимо использовать текстовые файлы, состоящие только из символов, принадлежащих стандартной таблице ASCII (символы английского алфавита и знаки препинания, различные служебные символы). Кодировка таких файлов должна быть UTF-8.

Кодировка текста - довольно сложная тема, которую нецелесообразно полноценно поднимать в рамках данного курса. Особенно, когда это касается C++. Подробнее про UTF-8 можно прочитать тут.

Отдельно также стоит упомянуть проблему признака переноса строки (\n, \n\r, \r\n, \r\r). В Linux для переноса строки используется \n. Обычно, ввод и вывод корректно обрабатываются в зависимости от операционной системы, даже если использовать только \n, но при работе с файлами в бинарном режиме это может повлиять на результат. Поэтому стоит убедиться, что все переносы строк состоят только из \n (например, вручную скопировать текст в файл).

Размеры исходных файлов с текстом должны быть не меньше нескольких килобайт (желательно как минимум несколько десятков килобайт).