Архивы в современное время играют немаловажную роль в хранении данных. Они позволяют хранить внутри себя набор разнообразных файлов. Это достаточно удобно, например, для передачи данных по сети — легче передавать один файл, чем несколько. Также архивы позволяют хранить информацию в удобной структурированной форме. Вы это, несомненно, осознавали и до этого.
В данной статье мы попробуем разработать собственный кроссплатформенный консольный архиватор с поддержкой как архивации, так и распаковки ранее запакованых файлов. Требуется уложиться в 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»:
Далее открываем консоль и архивируем содержимое " C:test ":
Смотрим, что новенького в " C:test ":
Видим там наш архив — «binary.zipper». Откроем его с помощью какого-нибудь текстового редактора для изучения содержимого. Я это сделаю с помощью NotePad++:
Мы видим секцию с информацией и последующим мессивом байт всех файлов. Что ж, класс =)
Распакуем архив в " C:testunpack ":
Смотрим, что получилось на выходе, в " C:testunpack ":
Как можем наблюдать, все в целости и сохранности.
Ну что ж, архиватор отлично работает. Причем мы уложились в 200 строчек кода — в общем счете 190 строчек.
Outro
Как вы убедились, написать свой архиватор достаточно просто даже с базовыми знаниями C++, STL и ООП в целом. Последнее даже, скорее, лишнее, так как вполне можно обойтись без целевого класса Zipper. В таком случае код сократится еще на 15-25 строчек :)
Для тех, кому интересно посмотреть на весь код целиком, прошу перейти вот сюда.
Удачи вам в коддинге!
Автор: Asen
Все бы ничего, только при выполнении программы система выдает ошибку о недопустимой записи в блок памяти. И сколько я не ковырял в коде, причину так и не нашел. Нужна помощь.
там, где память выдел. для m_size надо
char *m_size = new char[digs(size)+1];
т.к. еще
При распаковке выдает ошибки, так как не создается файл
FILE *curr = fopen(full_path,”wb”);
как решить эту проблему ?
Вопрос один а где функции ввода команд
Спасибо за код, но такой оформительский недочетик. argc – количество параметров argv сами параметры, таки просто принято.
А как сюда можно добавить рекурсию, то есть папку со всеми файлами?
Спасибо большое за программу!
Мне вот именно такая и понадобилась для работы.
Но в программе были некоторые ошибки:
1) как уже ранее писали char *m_size = new char[digs(size)+1]; надо было так выделять память
2) в функции распаковки удаляются указатели size и name, а они константные. И вообще память выделять под них не надо, т.к. они указывают на элементы вектора tokens
А так ещё раз спасибо автору. Очень полезная программа.
А можешь отправить исправленный код?
Можно полный скрипт, с доработками увидеть!?
есть у кого исправленный вариант? А то я исправил некоторые ошибки, но всё равно не выдаёт результата
есть у кого исправленный вариант? А то я исправил некоторые ошибки, но всё равно не выдаёт результат:/
Ну что ж…
Списываем, коллега?
А самим подумать?
А почему в названии присутствует Архиватор, Compressor?
Ведь по сути компрессии не происходит.
Проще тогда tar использовать :)
Там идет реальная компрессия папок с файлами.
И распаковка их во всех версиях Windows. Т.е. TAR встроен в Windows.