Создаем свой архиватор в 200 строчек кода

в 18:57, , рубрики: c++, архивация, Песочница, файлы, метки: , ,

Архивы в современное время играют немаловажную роль в хранении данных. Они позволяют хранить внутри себя набор разнообразных файлов. Это достаточно удобно, например, для передачи данных по сети — легче передавать один файл, чем несколько. Также архивы позволяют хранить информацию в удобной структурированной форме. Вы это, несомненно, осознавали и до этого.

В данной статье мы попробуем разработать собственный кроссплатформенный консольный архиватор с поддержкой как архивации, так и распаковки ранее запакованых файлов. Требуется уложиться в 200 строчек кода. Писать будем на C++, используя при этом лишь его стандартные библиотеки. Поэтому привязанности к определенной платформе нет — работать это будет и в Windows, и в Linux.
Почему C++, а не привычный C? Не то, что бы я имею что-то против СИ, просто в нем достаточно проблематично работать со строками и, к тому же, отсутствует ООП. Следовательно, если бы мы использовали C, то в 200 строк, думаю, вряд ли бы уложились. Ну, да ладно, приступим же!

Intro

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

Так что же требуется знать, чтобы написать архиватор? Думаю, кроме наличия некоторых знаний уровня «выше базового», касающихся C++, лишь то, что файл — это байты. И ничего более.

Codding

Пусть наш консольный архиватор будет называться Zipper`ом. Мне кажется, вполне подходящее название :)

Для работы потребуется несколько стандартных библиотек C++:

#include <iostream>
#include <string>
#include <vector>
#include <clocale>

Да, мы будем также использовать STL. Это удобно, «модно» и лаконично. STL позволит нам в разы сократить код.
Также мы будем использовать стандартное пространство имен:

using namespace std; 

Теперь разберемся с тем, как вообще будет функционировать наш архиватор. Итак, Zipper будет принимать из консоли некоторые параметры:

-pack — архивация(упаковка) файлов
-unpack — разархивация(распаковка) данных из файла-архива
-files — набор файлов для архивации( при наличии -pack ) или один файл для разархивации( при наличии -unpack )
-path — путь для сохранения разархивированных данных( при наличии -unpack ) или путь для сохранения архива ( при наличии -pack )

Распаковка данных из архива тесно связана с тем, что происходит при архивации. Архивация же будет происходить в два этапа:

1) Получение информации об архивируемых файлах
2) Создание единого файла архива, содержащего блок информации о всех файлах внутри себя и все файлы в бинарном представлении.

При архивации сначала будет происходить получении информации о всех файлах и сохраняться в промежуточный текстовый файл в таком виде:

<size_of_string>||<filesize>||<filename>||<filesize>||<filename>|| ... ||<end_of_info>||

Затем текстовый файл побайтно будет переписан в начало файла-архива и удален. Пояснения к параметрам формата информации:

size_of_string — пять байт, содержащие числа, составляющие единое число в 10-ичной системе счисления, которое указывает общий размер(в байтах) последующего блока информации( до <end_of_info> )

filesize — размер определенного файла в байтах
filename — имя этого файла.

Ну а после блока с информацией мы просто переписываем все архивируемые файлы побайтно в наш архив.
Вот, собственно, и весь процесс архивации. Теперь вернемся к коду.

Для наибольшего удобства мы создадим один класс «Zipper», который будет отвечать за все, что происходит внутри архиватора. Итак, архитектура класса " Zipper ":

class Zipper{

private:
    vector<string> files;   // набор файлов (-files)
    string path;            // путь (-path)    
    string real_bin_file;   // имя выходного файла-архива( используется при архивации )
public:
    Zipper(vector<string> &vec, string p)
    {
          if(vec.size()>0) files.assign(vec.begin(),vec.end());
          path = p+"\";
          real_bin_file=path+"binary.zipper";
        }

        }
    void getInfo();   // Метод для получения информации о файлах на этапе архивации
    void InCompress();   // Архивация данных
    void OutCompress(string binary);   // Распаковка данных ( binary - путь до архива )

    // Статический метод для выделения имени файла из полного пути. 
    // Используется для внутренних нужд.
    static string get_file_name(string fn){return fn.substr(fn.find_last_of("\")+1,fn.size());}
};

Нам также потребуется кое-какой простой «метод-отшельник» для подсчета количества разрядов в числе. Это потребуется, например, для записи числа в архив, как динамического буфера символов. Метод:

int digs(double w)
{
    int yield = 0;
    while(w>10) {yield++;w/=10;}
    return yield+1;
} 

Итак, мы уже имеем более-менее качественный интерфейс. Но пока все то, что было задумано, лишь в сознании.
Пора бы воплотить идею :) Реализуем метод архивации данных:

void Zipper::InCompress()
{
    char byte[1];  // единичный буфер для считывания одного байта

    getInfo();  // получаем необходимую информацию о том, что архивируем

    FILE *f;
    FILE *main=fopen((this->real_bin_file).c_str(),"wb");  // файл - архив
    FILE *info = fopen((this->path+"info.txt").c_str(),"rb");  // файл с информацией

    // переписываем информацию в архив
    while(!feof(info))
    {
        if(fread(byte,1,1,info)==1) fwrite(byte,1,1,main);
        }

        fclose(info);
        remove((this->path+"info.txt").c_str());  // прибираемся за собой

    // последовательная запись в архив архивируемых файлов побайтно :
    for(vector<string>::iterator itr=this->files.begin();itr!=this->files.end();++itr)
    {
        f = fopen((*itr).c_str(),"rb");
        if(!f){ cout<<*itr<<" не найден!"<<endl; break;}
        while(!feof(f))
        {
            if(fread(byte,1,1,f)==1) fwrite(byte,1,1,main);
        }
        cout<<*itr<<" добавлен в архив '"<<this->real_bin_file<<"'."<<endl;
        fclose(f);
    }
    fclose(main);
}

Осталось лишь реализовать наш заветный метод для получения информации. Метод " getInfo() ":

void Zipper::getInfo()
{
    char byte[1];  // единичный буфер для считывания одного байта

    basic_string<char> s_info = "";
    remove((this->path+"info.txt").c_str());  // на всякий случай
    FILE *info = fopen((this->path+"info.txt").c_str(),"a+");  // сохраняем информацию в наш текстовый файл
    int bytes_size=0;  // длина информационного блока в байтах
    for(vector<string>::iterator itr=this->files.begin();itr!=this->files.end();++itr)
    {
        FILE *f = fopen((*itr).c_str(),"rb");
        if(!f) break;

         // получаем размер архивируемого файла
        fseek(f,0,SEEK_END);
        int size = ftell(f);             

        string name = Zipper::get_file_name(*itr);  // получаем имя архивируемого файла

        char *m_size = new char[digs(size)];
        itoa(size,m_size,10);
        fclose(f);

        bytes_size+=digs(size);
        bytes_size+=strlen(name.c_str());

        // все, что "нарыли", сохраняем в промежуточный буфер :
        s_info.append(m_size);
        s_info.append("||");
        s_info.append(name);
        s_info.append("||");

        delete [] m_size;

    }
    bytes_size = s_info.size()+2;
    char *b_buff = new char[digs(bytes_size)];
    itoa(bytes_size,b_buff,10);

    // форматируем до 5 байт
    if(digs(bytes_size)<5) fputs(string(5-digs(bytes_size),'0').c_str(),info);

    fputs(b_buff,info);
    fputs("||",info);
    fputs(s_info.c_str(),info);

    fclose(info);
}

На этом с архивацией данных все. Осталось лишь добавить смысла в архивацию, а именно — распаковку того, что внутри архива. Для этого реализуем последний нереализованный метод класса Zipper — метод OutCompress(). Как говорилось в начале статьи, распаковка тесно связана с упаковкой.Эта привязанность связана с информационным блоком. Если возникнет ошибка при архивации, а именно на этапе получения информации( например, где-то просчет на 1 байт ), то весь процесс распаковки с треском рухнет. Но, этого нам, конечно же, не нужно! Мы ведь пишем валидный код. Итак, процесс распаковки состоит всего из одного этапа, содержащего два подэтапа:

1) Разборка блока с информацией о том, что содержится в архиве
2) Чтение «мессива» байт всех файлов внутри архива по правилам, указанным в информационной секции.

Что ж, реализация метода распаковки:

void Zipper::OutCompress(string binary)
{
    FILE *bin = fopen(binary.c_str(),"rb");   // открываем архив в режиме чтения
    char info_block_size[5];   // размер информационного блока 
    fread(info_block_size,1,5,bin);  // получаем размер
    int _sz = atoi(info_block_size);  // преобразуем буфер в число

    char *info_block = new char[_sz];  // информационный блок
    fread(info_block,1,_sz,bin);   // считываем его 

    // Парсинг информационного блока :
    vector<string> tokens;
    char *tok = strtok(info_block,"||");
    int toks = 0;
    while(tok)
    {
        if(strlen(tok)==0) break;
        tokens.push_back(tok);
        tok=strtok(NULL,"||");
        toks++;
    }

    if(toks%2==1) toks--;  // удаляем мусор
    int files=toks/2;  // количество обнаруженных файлов в архиве

    char byte[1];   // единичный буфер для считывания одного байта

    // Процесс распаковки всех файлов( по правилам полученным из блока с информацией ) :
    for(int i=0;i<files;i++)
    {
        const char* size = tokens[i*2].c_str();
        const char* name = tokens[i*2+1].c_str();
        char full_path[255];
        strcpy(full_path,this->path.c_str());
        strcat(full_path,name);
        int _sz = atoi(size);
        cout<<"--  '"<<name<<"' извлечен в '"<<this->path<<"' ."<<endl;
        FILE *curr = fopen(full_path,"wb");
        for(int r=1;r<=_sz;r++)
        {
            if(fread(byte,1,1,bin)==1) fwrite(byte,1,1,curr);
        }
        fclose(curr);

        delete [] size;
        delete [] name;
    }
    fclose(bin);

}

Вот, собственно, и все, что касается нашего архиватора/распаковщика. Но так как это приложение планируется использовать в консольном режиме, то нам придется подстроить наш Zipper для консоли. Нам необходимо реализовать поддержку 4 параметров, речь о которых шла в самом начале. Это делается довольно легко:

int main(int argv, char* argc[])
{
    /*/  Supported args:
    //
    //    -pack, -unpack, -files, -path
    //
    /*/

    setlocale(LC_ALL,"Russian");
    cout<<endl<<"######################## ZIPPER ########################"<<endl<<endl;
    if(argv>1)
    {
        vector<string> files;  // массив файлов, переданных через параметры из консоли
        string path = "";  // путь
        bool flag_fs = false, flag_path = false;  // флаги режима чтения/записи
        char type[6];              // тип: упаковка или распаковка 
        memset(type,0,6);

        for(int i=1;i<argv;i++)
        {
            if(strcmp(argc[i],"-pack")==0) { strcpy(type,"pack"); flag_fs=flag_path=false;}
            if(strcmp(argc[i],"-unpack")==0) { strcpy(type,"unpack"); flag_fs=flag_path=false;}
            if(strcmp(argc[i],"-path")==0) {flag_path=true; flag_fs=false; continue; }
            if(strcmp(argc[i],"-files")==0) {flag_fs=true; flag_path=false; continue; }

            if(flag_path) {path.assign(argc[i]); }
            if(flag_fs) files.push_back(string(argc[i]));

        }
        Zipper *zip = new Zipper(files,path);
        if(strcmp(type,"pack")==0) zip->InCompress();
        if(strcmp(type,"unpack")==0) zip->OutCompress(files[0]);
    }
    else cout<<"Параметры -pack/-unpack , -files, -path обязательны!"<<endl;
    cout<<endl<<"########################################################"<<endl<<endl;

}

Вот, собственно, и все приложение.

Testing

Теперь займемся неотъемлемой частью разработки — тестированием. Для наибольшего удобства я все продемонстрирую в картинках.

1) В корне локального диска «C» я создам папку с именем «test», в которой будут находиться все файлы для архивации. В «C:test» создам еще одну папку для демонстрации разархивации — «unpack»:

Создаем свой архиватор в 200 строчек кода

Далее открываем консоль и архивируем содержимое " C:test ":

Создаем свой архиватор в 200 строчек кода

Смотрим, что новенького в " C:test ":

Создаем свой архиватор в 200 строчек кода

Видим там наш архив — «binary.zipper». Откроем его с помощью какого-нибудь текстового редактора для изучения содержимого. Я это сделаю с помощью NotePad++:

Создаем свой архиватор в 200 строчек кода

Мы видим секцию с информацией и последующим мессивом байт всех файлов. Что ж, класс =)

Распакуем архив в " C:testunpack ":

Создаем свой архиватор в 200 строчек кода

Смотрим, что получилось на выходе, в " C:testunpack ":

Создаем свой архиватор в 200 строчек кода

Как можем наблюдать, все в целости и сохранности.
Ну что ж, архиватор отлично работает. Причем мы уложились в 200 строчек кода — в общем счете 190 строчек.

Outro

Как вы убедились, написать свой архиватор достаточно просто даже с базовыми знаниями C++, STL и ООП в целом. Последнее даже, скорее, лишнее, так как вполне можно обойтись без целевого класса Zipper. В таком случае код сократится еще на 15-25 строчек :)

Для тех, кому интересно посмотреть на весь код целиком, прошу перейти вот сюда.

Удачи вам в коддинге!

Автор: Asen

Источник

  1. Vyacheslav:

    Все бы ничего, только при выполнении программы система выдает ошибку о недопустимой записи в блок памяти. И сколько я не ковырял в коде, причину так и не нашел. Нужна помощь.

  2. Юра:

    При распаковке выдает ошибки, так как не создается файл
    FILE *curr = fopen(full_path,”wb”);
    как решить эту проблему ?

  3. Уруру:

    Вопрос один а где функции ввода команд

  4. Федя:

    Спасибо за код, но такой оформительский недочетик. argc – количество параметров argv сами параметры, таки просто принято.

  5. Afka:

    А как сюда можно добавить рекурсию, то есть папку со всеми файлами?

  6. Дмитрий:

    Спасибо большое за программу!
    Мне вот именно такая и понадобилась для работы.
    Но в программе были некоторые ошибки:
    1) как уже ранее писали char *m_size = new char[digs(size)+1]; надо было так выделять память
    2) в функции распаковки удаляются указатели size и name, а они константные. И вообще память выделять под них не надо, т.к. они указывают на элементы вектора tokens
    А так ещё раз спасибо автору. Очень полезная программа.

  7. ROBERT:

    есть у кого исправленный вариант? А то я исправил некоторые ошибки, но всё равно не выдаёт результата

  8. ROBERT:

    есть у кого исправленный вариант? А то я исправил некоторые ошибки, но всё равно не выдаёт результат:/

  9. Хвастунов Александр:

    Ну что ж…
    Списываем, коллега?
    А самим подумать?

  10. FrancNet:

    А почему в названии присутствует Архиватор, Compressor?
    Ведь по сути компрессии не происходит.
    Проще тогда tar использовать :)
    Там идет реальная компрессия папок с файлами.
    И распаковка их во всех версиях Windows. Т.е. TAR встроен в Windows.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js