【问题标题】:Using BinaryWriter to seek and insert data使用 BinaryWriter 查找和插入数据
【发布时间】:2012-05-21 16:30:46
【问题描述】:

背景:

我是 C# 的新手,目前正在从事一个主要用于学习的项目。我正在创建一个库来处理 Quake 和 Quake II 使用的 .PAK 文件。我可以很好地读取和提取它们,也可以编写空白存档,并且我正在将数据写入存档。

基本规范可以参考这里:http://debian.fmi.uni-sofia.bg/~sergei/cgsr/docs/pak.txt

问题:

我想查找二进制数据部分的末尾并插入给定文件中的数据。创建空文件时,我在方法中使用 BinaryWriter,所以这就是我要用于插入的方法。根据我的发现,使用 BinaryWriter 查找和写入将在给定位置写入并覆盖现有数据。

我的目标是简单地在给定点插入数据而不会丢失任何现有数据。如果这不可能,我想我需要提取包含单个文件信息(名称、偏移量、长度)的整个索引,添加二进制数据,然后为我正在编写的文件附加索引和附加信息。

有什么想法或建议吗?

【问题讨论】:

  • 对于它的价值,我已经在我的构造函数中实例化了一个 FileStream,我在我的方法之间共享它。我一直在刷新流以允许在方法调用之间重新访问它。尽管您有其他怀疑,但所有对流的引用实际上都是相关的。 :)
  • 对于以后阅读本文的任何人,我确实想通了。我已经缓冲了索引(在规范中称为“目录”),每个索引条目都包含在一个类中,包含在一个数组中。我只是将新文件写入索引的起始位置,然后从缓冲数据中重写索引。

标签: c# file-io binary-data


【解决方案1】:

您不能直接使用 C# IO 类将数据插入文件。最简单的方法是将其读入缓冲区并操作缓冲区,或者将第一部分写入临时文件,然后插入新数据,然后写入文件的其余部分,然后重命名。 或者,您可以在文件末尾附加一些数据并将所有内容向上移动以创建适量的可用空间来插入数据。

看看http://www.codeproject.com/Articles/17716/Insert-Text-into-Existing-Files-in-C-Without-Temp

【讨论】:

  • 这正是我一直在寻找的东西。谢谢!
【解决方案2】:

直接使用基于流的对象(如 FileStream)是文件级操作的更好选择。

没有办法在中间插入数据到任何文件中,您必须读取整个文件并将其与新插入的部分一起保存或复制到新文件直到插入点,添加新数据,然后复制其余部分(此时您可以删除原始/重命名)。

【讨论】:

  • 感谢您证实我的怀疑,如果不存在插入方法,这是我最初考虑的方法。
【解决方案3】:

很难有效地进行插入。特别是如果您想对同一个流进行多次插入。您可以编写自己的Stream 类来跟踪您所做的任何插入,并在实际上没有插入更改时假装已插入更改。例如,如果您在位置 10 有一个 A,而您在该位置插入一个 X。您的类可以,而不是将插入写入流并移动所有后续字节,而是在您请求位置 11 时从底层流中请求位置 10 和 A 时返回 X(而在底层流中它是仍然在第 10 位)。然后,当您在流上调用 FlushClose 时,整个已更改的流将一次全部刷新到底层流。这需要一些非常仔细的工程才能做到正确,因为您可能在插入中插入,以任何顺序插入等等......

这是我为您编写的一些代码,它将任意字节插入流中并移动所有后续字节,而无需任何巧妙的缓存。不幸的是,它不使用BinaryWriter,而是直接在Stream(必须是可搜索的)上工作,因为这样更容易编码。它在流的当前位置插入,然后该位置就在插入的字节之后。要插入字符串,请使用 Encoding 类将它们转换为字节。

public static class StreamExtensions
{
    /// <summary>
    /// Inserts the specified binary data at the current position in the stream.
    /// </summary>
    /// <param name="stream">The stream.</param>
    /// <param name="array">The array.</param>
    /// <remarks>
    /// <para>Note that this is a very expensive operation, as all the data following the insertion point has to
    /// be moved.</para>
    /// <para>When this method returns, the writer will be positioned at the end of the inserted data.</para>
    /// </remarks>
    public static void Insert(this Stream stream, byte[] array)
    {
        #region Contract
        if (stream == null)
            throw new ArgumentNullException("writer");
        if (array == null)
            throw new ArgumentNullException("array");
        if (!stream.CanRead || !stream.CanWrite || !stream.CanSeek)
            throw new ArgumentException("writer");
        #endregion

        long originalPosition = stream.Position;
        long readingPosition = originalPosition;
        long writingPosition = originalPosition;

        int length = array.Length;
        int bufferSize = 4096;
        while (bufferSize < length)
            bufferSize *= 2;

        int currentBuffer = 1;
        int[] bufferContents = new int[2];
        byte[][] buffers = new [] { new byte[bufferSize], new byte[bufferSize] };
        Array.Copy(array, buffers[1 - currentBuffer], array.Length);
        bufferContents[1 - currentBuffer] = array.Length;

        bool done;

        do
        {
            bufferContents[currentBuffer] = ReadBlock(stream, ref readingPosition, buffers[currentBuffer], out done);

            // Switch buffers.
            currentBuffer = 1 - currentBuffer;

            WriteBlock(stream, ref writingPosition, buffers[currentBuffer], bufferContents[currentBuffer]);

        } while (!done);

        // Switch buffers.
        currentBuffer = 1 - currentBuffer;

        // Write the remaining data.
        WriteBlock(stream, ref writingPosition, buffers[currentBuffer], bufferContents[currentBuffer]);

        stream.Position = originalPosition + length;
    }

    private static void WriteBlock(Stream stream, ref long writingPosition, byte[] buffer, int length)
    {
        stream.Position = writingPosition;
        stream.Write(buffer, 0, length);
        writingPosition += length;
    }

    private static int ReadBlock(Stream stream, ref long readingPosition, byte[] buffer, out bool done)
    {
        stream.Position = readingPosition;
        int bufferContent = 0;
        int read;
        do
        {
            read = stream.Read(buffer, bufferContent, buffer.Length - bufferContent);
            bufferContent += read;
            readingPosition += read;
        } while (read > 0 && read < buffer.Length);
        done = read == 0;
        return bufferContent;
    }
}

要使用此方法,请将代码粘贴到与您的代码相同的命名空间中,然后:

Stream someStream = ...;
byte[] dataToInsert = new byte[]{ 0xDE, 0xAD, 0xBE, 0xEF };
someStream.Position = 5;
someStream.Insert(dataToInsert);

既然你说你有一个BinaryWriter,因为你用它来创建一个空文件,为了能够使用这个类,你需要实际的Stream。您可以改用静态File.Create()File.Open() 方法,它们返回FileStream 对象。如果您稍后决定仍需要BinaryWriter,则可以使用将流提供给BinaryWriter 构造函数。要从作者那里获取流,请使用BinaryWriter.BaseStream。请注意,关闭 BinaryWriter 也会关闭底层流,除非您通过使用 MiscUtil 库中的 NonClosingStreamWrapper 来阻止它。

【讨论】:

  • 感谢您的解释和代码示例。我已经在我的类构造函数中有一个 FileStream 实例化。无论如何,我一直将该流传递给 BinaryReader/BinaryWriter,因此集成上述代码将是微不足道的。由于提供的链接中的理论和解释,我选择了接受的答案,但我非常感谢您的回复。
  • 我很想这样做,但在我的代表 15 岁之前我不能这样做。
【解决方案4】:

是的,正如您所说,您不能在文件中“插入”数据。您应该在插入点之前使用一些读写器组合并写入新数据。

我不知道 .pak 格式,但我认为这个例子可以说明你。

    var sourceStream = System.IO.File.OpenRead("myFile.pak");
    var destStream = System.IO.File.Create("modified.pak");

    using (var reader = new System.IO.BinaryReader(sourceStream))
    {
        using (var writer = new System.IO.BinaryWriter(destStream))
        {
            while (reader.BaseStream.CanRead)
            {
                char data = reader.ReadChar();
                if (data == '?')
                {
                    writer.Write("<Found!>");
                }
                else
                {
                    writer.Write(data);
                }
            }
        }
    }

【讨论】:

  • 尽管没有关闭流,但我明白了。我没有考虑使用两个流(和文件),但这也可能是解决方案的候选者。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-15
相关资源
最近更新 更多