Создание самораспаковывающегося исполняемого файла с помощью C#

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

MAGICNUMBER .... содержимое текстового файла

Далее текстовый файл копируется в конец исполняемого файла:

скопировать programm.exe/b+textfile.txt/b sfx.exe

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

    string my_filename = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
    StreamReader file = new StreamReader(my_filename);
    const int block_size = 1024;
    const string magic = "MAGICNUMBER";
    char[] buffer = new Char[block_size];
    Int64 count = 0;
    Int64 glob_pos = 0;
    bool flag = false;
    while (file.ReadBlock(buffer, 0, block_size) > 0)
    {
        var rel_pos = buffer.ToString().IndexOf(magic);
        if ((rel_pos > -1) & (!flag))
        {
            flag = true;
            continue;
        }

        if ((rel_pos > -1) & (flag == true))
        {
            glob_pos = block_size * count + rel_pos;
            break;
        }
        count++;
    }



    using (FileStream fs = new FileStream(my_filename, FileMode.Open, FileAccess.Read))
    {
        byte[] b = new byte[fs.Length - glob_pos];
        fs.Seek(glob_pos, SeekOrigin.Begin);
        fs.Read(b, 0, (int)(fs.Length - glob_pos));
        File.WriteAllBytes("c:/output.txt", b);

но почему-то я копирую почти весь файл, а не последние несколько килобайт. Это из-за оптимизации компилятора, встраивания магической константы в цикл while чего-то подобного?

Как мне правильно сделать самораспаковывающийся архив?

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

    string my_filename = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
    StreamReader file = new StreamReader(my_filename);
    const int block_size = 1024;
    const string magic = "MAGIC";
    char[] buffer = new Char[block_size];
    Int64 count = 0;
    Int64 glob_pos = 0;
    while (file.ReadBlock(buffer, 0, block_size) > 0)
    {
        var rel_pos = buffer.ToString().IndexOf(magic);
        if (rel_pos > -1)
        {
            glob_pos = block_size * count + rel_pos;
        }
        count++;
    }



    using (FileStream fs = new FileStream(my_filename, FileMode.Open, FileAccess.Read))
    {
        byte[] b = new byte[fs.Length - glob_pos];
        fs.Seek(glob_pos, SeekOrigin.Begin);
        fs.Read(b, 0, (int)(fs.Length - glob_pos));
        File.WriteAllBytes("c:/output.txt", b);
    }

Итак, я просмотрел весь файл один раз, обнаружил, что это будет последнее вхождение магического числа, и скопировал его отсюда до конца. Хотя файл, созданный этой процедурой, кажется меньше, чем в предыдущей попытке, это никоим образом не тот файл, который я прикрепил к моему «самораспаковывающемуся» архиву. Почему?

Я предполагаю, что вычисление позиции начала прикрепленного файла неверно из-за используемого преобразования из двоичного кода в строку. Если да, то как мне изменить расчет позиции, чтобы сделать его правильным?

Также как мне выбрать магический номер, а затем работать с реальными файлами, например, с pdf? Я не смогу легко изменить pdf-файлы, чтобы включить в него предопределенный магический номер.


person Moonwalker    schedule 07.02.2013    source источник
comment
Вы знакомы с сериализацией? Хотя чтение/запись необработанных двоичных файлов работает, обычно гораздо проще (и менее подвержено ошибкам) ​​использовать сериализацию для кодирования данных. Или даже просто прочитайте весь файл в память (при условии, что это реализуемый вариант), внесите изменения, а затем сохраните их на диск.   -  person sircodesalot    schedule 07.02.2013
comment
@sircodesalot Звучит хорошо. Я был бы признателен за ссылку на какой-то пример.   -  person Moonwalker    schedule 07.02.2013
comment
Да, я быстро напишу.   -  person sircodesalot    schedule 07.02.2013
comment
взгляните на этот проект: codeproject.com /Статьи/339768/   -  person Alex    schedule 07.02.2013
comment
Посмотрите на получившиеся файлы. У тебя действительно есть волшебная нить в начале?   -  person Ark-kun    schedule 11.02.2013
comment
@Ark-kun не волшебная строка в начале, а какой-то треш: MZђ яяё @Ђє ґ Н!ёLН!Эта программа не может быть запущена в режиме DOS.   -  person Moonwalker    schedule 11.02.2013
comment
@Moonwalker это означает, что ваша программа неверна, а не волшебное слово. посмотрю в нем.   -  person Ark-kun    schedule 14.02.2013
comment
@Moonwalker В вашем коде есть еще одна проблема. Что делать, если волшебное слово находится на границе блока?   -  person Ark-kun    schedule 14.02.2013


Ответы (4)


Самое простое решение - заменить

const string magic = "MAGICNUMBER";

с участием

static string magic = "magicnumber".ToUpper();

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

Обновление: это должно работать, если ваши файлы не очень большие. (В этом случае вам нужно будет использовать вращающуюся пару буферов (чтобы читать файл небольшими блоками)):

string inputFilename = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
string outputFilename = inputFilename + ".secret";
string magic = "magic".ToUpper();

byte[] data = File.ReadAllBytes(inputFilename);
byte[] magicData = Encoding.ASCII.GetBytes(magic);

for (int idx = magicData.Length - 1; idx < data.Length; idx++) {
    bool found = true;
    for (int magicIdx = 0; magicIdx < magicData.Length; magicIdx++) {
        if (data[idx - magicData.Length + 1 + magicIdx] != magicData[magicIdx]) {
            found = false;
            break;
        }
    }
    if (found) {
        using (FileStream output = new FileStream(outputFilename, FileMode.Create)) {
            output.Write(data, idx + 1, data.Length - idx - 1);
        }
    }
}

Update2: это должно быть намного быстрее, использовать мало памяти и работать с файлами любого размера, но ваша программа должна быть правильно исполняемой (с размером, кратным 512 байтам):

string inputFilename = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
string outputFilename = inputFilename + ".secret";
string marker = "magic".ToUpper();

byte[] data = File.ReadAllBytes(inputFilename);
byte[] markerData = Encoding.ASCII.GetBytes(marker);
int markerLength = markerData.Length;

const int blockSize = 512; //important!

using(FileStream input = File.OpenRead(inputFilename)) {
    long lastPosition = 0;
    byte[] buffer = new byte[blockSize];
    while (input.Read(buffer, 0, blockSize) >= markerLength) {
        bool found = true;
        for (int idx = 0; idx < markerLength; idx++) {
            if (buffer[idx] != markerData[idx]) {
                found = false;
                break;
            }
        }
        if (found) {
            input.Position = lastPosition + markerLength;
            using (FileStream output = File.OpenWrite(outputFilename)) {
                input.CopyTo(output);
            }
        }
        lastPosition = input.Position;
    }
}

О некоторых подходах читайте здесь: http://www.strchr.com/creating_self-extracting_executables

person Ark-kun    schedule 10.02.2013
comment
Насколько я понимаю, нормальные самораспаковывающиеся архивы ищут сигнатуру архива сами по себе, а не прописывают в конец файла количество байтов для чтения. Почему? - person Moonwalker; 11.02.2013

Попробуйте это. Некоторый С# Stream IO 101:

    public static void Main()
    {
        String path = @"c:\here is your path";

        // Method A: Read all information into a Byte Stream
        Byte[] data = System.IO.File.ReadAllBytes(path);
        String[] lines = System.IO.File.ReadAllLines(path);

        // Method B: Use a stream to do essentially the same thing. (More powerful)
        // Using block essentially means 'close when we're done'. See 'using block' or 'IDisposable'.
        using (FileStream stream = File.OpenRead(path))
        using (StreamReader reader = new StreamReader(stream))
        {
            // This will read all the data as a single string
            String allData = reader.ReadToEnd();
        }

        String outputPath = @"C:\where I'm writing to";

        // Copy from one file-stream to another
        using (FileStream inputStream = File.OpenRead(path))
        using (FileStream outputStream = File.Create(outputPath))
        {
            inputStream.CopyTo(outputStream);

            // Again, this will close both streams when done.
        }

        // Copy to an in-memory stream
        using (FileStream inputStream = File.OpenRead(path))
        using (MemoryStream outputStream = new MemoryStream())
        {
            inputStream.CopyTo(outputStream);

            // Again, this will close both streams when done.
            // If you want to hold the data in memory, just don't wrap your 
            // memory stream in a using block.
        }

        // Use serialization to store data.
        var serializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

        // We'll serialize a person to the memory stream.
        MemoryStream memoryStream = new MemoryStream();
        serializer.Serialize(memoryStream, new Person() { Name = "Sam", Age = 20 });

        // Now the person is stored in the memory stream (just as easy to write to disk using a 
        // file stream as well.

        // Now lets reset the stream to the beginning:
        memoryStream.Seek(0, SeekOrigin.Begin);

        // And deserialize the person
        Person deserializedPerson = (Person)serializer.Deserialize(memoryStream);

        Console.WriteLine(deserializedPerson.Name); // Should print Sam

    }

    // Mark Serializable stuff as serializable.
    // This means that C# will automatically format this to be put in a stream
    [Serializable]
    class Person
    {
        public String Name { get; set; }
        public Int32 Age { get; set; }
    }
person sircodesalot    schedule 07.02.2013
comment
Спасибо, я попробую это - person Moonwalker; 07.02.2013
comment
Ну, похоже, настоящая проблема не в сериализации, а в моей неспособности найти магическое число, разделяющее файлы. - person Moonwalker; 10.02.2013

Вы можете добавить сжатый файл в качестве ресурса в сам проект:

Проект > Свойства
введите здесь описание изображения

Задайте для свойства этого ресурса значение Binary.

Затем вы можете получить ресурс с помощью

byte[] resource = Properties.Resources.NameOfYourResource;
person Olivier Jacot-Descombes    schedule 07.02.2013
comment
Это работает только на этапе компиляции, нет? Я хочу иметь возможность создавать самораспаковывающиеся архивы из отдельной программы. - person Moonwalker; 07.02.2013
comment
@Moonwalker: вы можете добавить ресурс в готовый исполняемый файл. Это немного сложнее, чем просто добавить ZIP-файл после exe, но не настолько. И вместо того, чтобы искать исполняемый файл, он индексируется. - person Luaan; 07.01.2014

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

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

В более надежном случае сохраните файл как дополнительный раздел данных в исполняемом файле. Это более сложно без внешних инструментов, поскольку требует знания формата файла PE, используемого для исполняемых файлов NT, например. http://msdn.microsoft.com/en-us/library/ms809762.aspx

person El Zorko    schedule 12.02.2013