RepCHK. Восстановление JPG из CHK файлов.

Страница проекта

На днях ко мне обратились с такой проблемой: жесткий диск на ПК начал глючить, в результате чего при очередном запуске Windows сработала утилита ScanDisk и убила папку со свадебными фотографиями. Естественно, всё, что убито, было помещено в скрытую папку «found» в файлы CHK. Не было бы печали, если бы это было обычное переименовывание файлов в другой формат, так ведь нет — это же последовательная цепочка «битых» кластеров.

Пошарился в интернете и среди бесплатных приложений для Windows смог найти лишь несколько профильных приложений, но, увы, они смогли восстановить лишь 192 фото, и то большинство коррумпированными.

Делать нечего, решил попробовать решить проблему написанием собственной утилиты. Скопировал файлы к себе на Ubuntu и начал решать задачу.

Задача получилась следующей:

  • создать буфер объёмом достаточным для хранения полного фото с учётом «холостого» хода и загружать в него «до краёв» файлы CHK, которые имеют разные размеры;
  • искать среди «мусора» JPEG-данные по какому-либо признаку;
  • сохранять данные в файлы с расширением JPG.

CHK-файлы я поместил в папку «in», а для файлов на вывод создал папку «out».

Код я начал, как положено, с подключения необходимых для работы с файлами и памятью заголовочных файлов:

#include <stdio.h>
#include <memory.h>

Определил указатель на буфер и его размер, после чего в основном коде приложения (int main(…)) выделил память для буфера:

unsigned char *buf = NULL;
unsigned long M64 = 67108864;
int main( int argc, char *argv[] )
{
    if( NULL == (buf = new unsigned char[M64] ) )
    {
        printf( "Memory allocate FAILED!\n" );
        return 0;
    }
    return 0;
}

Теперь, когда буфер готов к работе, я написал основной цикл программы:

    do
    {
        FillBuffer();
        bufptr++;
        if( bufptr >= SearchLen )
            ShiftBuf();
        SearchJPEG();
    } while( lastptr > 1024 );
    delete buf;
    printf( "Done\nTotal files restored: %i\n", scnt );

Здесь цикл do { … } while( lastptr > 1024 ); выполняется до тех пор, пока дынных в буфере достаточно для поиска — 1024, именно столько я определил, как минимальный порог. За количеством данных в буфере следит указатель lastptr, объявленный как:

unsigned long lastptr = 0L;

Тело цикла начинается с вызова процедуры FillBuffer(), которая будет заполнять буфер данными из CHK-файлов.

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

        bufptr++;
        if( bufptr >= SearchLen )
            ShiftBuf();

Указатель поиска:

unsigned long bufptr = 0L;

Порог поиска 1 Мбайт:

#define SearchLen 1048576

После всего этого можно искать данные. В моём случае это только JPEG-файлы:

        SearchJPEG();

Когда данных для заполнения буфера больше не остаётся и объём буфера падает до критического уровня, работа цикла прекращается. После этого остаётся лишь освободить буфер:

delete buf;

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

int scnt = 0;

Как было сказано, наполнением буфера занимается процедура bool FillBuffer().

Условиями для подгрузки данных является наполненность буфера меньше половины + указатель поиска и количество файлов не превышает допустимое значение:

    if( astptr < ( bufptr+M64/2 ) && fidx < 10000 )
    {

Если условия удовлетворяют, то цикл do { … } while( fidx < 10000 ); грузит данные из файлов от «FILE0000.CHK», но не более, чем до «FILE9999.CHK», т. к. для 8-символьного файла это предел. И если их оказывается больше, то, вроде бы, создаётся вторая и последующие папки «found» с растущим индексом. За текущим номером файла следит переменная:

int fidx = 0;

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

                char fn[256];
                sprintf( fn, "in/FILE%04d.CHK", fidx );
                if( NULL == ( f = fopen( fn, "rb" ) ) )
                {
                    if( fidx < 10000 )
                        fidx++;
                }

Здесь имя собирается из папки «in», имени файла + его номер с выравниванием на 4 цифры с ведущими нулями, и расширением файла. Затем попытка открыть файл и, если его нет, инкрементируем индекс в допустимом диапазоне. Файл объявлен глобально:

FILE *f = NULL;

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

                else
                {
                    fseek( f, 0, SEEK_END );
                    unsigned long fsz = ftell( f );
                    fseek( f, 0, SEEK_SET );
                    if( lastptr+fsz >= M64 )
                    {
                        fclose( f );
                        f = NULL;
                        return true;
                    }
                    else
                        fidx++;

Здесь видно, что если размер файла + имеющийся объём данных в буфере превышает размеры буфера, то файл закрывается и остаётся на счету до лучших времён, а иначе индекс файла увеличивается и можно загружать данные в буфер:

                    printf( "Loading '%s' to %i with size %i\n", fn, (int)lastptr, (int)fsz );
                    fread( &buf[lastptr], fsz, 1, f );
                    fclose( f );
                    f = NULL;

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

lastptr+=fsz;

Когда файлы закончились и грузить данные неоткуда, необходимо следить за указателем поиска и границами данных:

    if( bufptr < lastptr )
        return true;
    else
    {
        bufptr=lastptr-6;
        ShiftBuf();
    }

На этом процедура заполнения буфера закончена. Не раз уже упоминалось смещение буфера, которым занимается процедура void ShiftBuf(). Её реализация предельно проста: необходимо скопировать данные от указателя в нулевую позицию, оставив при этом 6 байт в начале (первые 6 байт до сигнатуры — часть файла JPEG!):

    memcpy( &buf[0], &buf[bufptr-6], M64-bufptr-6 );

После чего необходимо пересчитать указатели:

    lastptr = lastptr-bufptr+6;
    bufptr = 6;

Ну а теперь о процедуре поиска JPEG-файлов bool SearchJPEG().

В своём случае я не стал изучать формат JPEG-файлов досконально, чтобы разработать совершенный рецепт определения его дескриптора, т. к. открыв CHK-файл я был рад видеть тег «Exif», который используется в нормальных фотокамерах для хранения дополнительных атрибутов фото. Именно его я и искал в «мусоре»:

    if( memcmp( &buf[bufptr], "Exif", 4 ) == 0 )
    {

Найдя сигнатуру можно сохранять данные:

        char fns[256];
        sprintf( fns, "out/%04d.jpg", (int)fsid++ );
        FILE *fs;
        if( NULL == ( fs = fopen( fns, "wb" ) ) )
        {
            printf( "ERROR to create out file '%s'!\n", fns );
            return false;
        }

Имя файла создаётся аналогично тому, как это сделано в процедуре наполнения буфера. Следит за номером файла переменная:

int fsid = 0;

Поскольку я не использовал реальных сигнатур JPEG, то и заморачиваться с определением размера файла для сохранения я не стал:

        unsigned long ffsz = lastptr-bufptr;
        if( ffsz > 16000000 )
            ffsz = 16000000;

Да. Файлы большие, но их потом можно пакетно обработать и будет «Щастье!». Тем более, что их все всё-равно придётся просматривать, т. к. часть из них всё же, возможно, будет коррумпирована.

Ну и, собственно, сохраняем файл не забыв про 6 байт до тега, увеличиваем счётчик файлов и сдвигаем буфер:

        fwrite( &buf[bufptr-6], ffsz, 1, fs );
        fclose( fs );
        printf( "File write as '%s' with size %d\n", fns, (int)ffsz );
        scnt++;
        ShiftBuf();

Вот такая утилита у меня получилась. Она смогла восстановить 649(!) файлов 79% из которых были «живыми».

Я понимаю, что утилита сырая и мало функциональна. Но, на скорую руку она вполне годна. Главное я смог помочь людям и вытащил потерянные файлы из CHK.

Теперь, имея основу приложения, можно её развивать и добавлять в неё действительные сигнатуры различных форматов: графических файлов, документов, аудио- и видеофайлов и т. п., и получить действительно мощную утилиту, которая сможет пригодиться в самых разных случаях. Именно этим я и займусь постепенно и буду рад помощи.

Компиляция в Linux: g++ repchk.cpp

Запуск: ./a.out

Приложение должно лежать на одном уровне с папками «in» и «out».

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *