如何计算大文件块的哈希值?

4
我希望能够在C#中计算任意大小文件块的哈希值。
例如:计算4GB文件中第3个Gigabyte的哈希值。
主要问题是我不想将整个文件加载到内存中,因为可能存在多个文件且偏移量可能非常随意。
据我所知,HashAlgorithm.ComputeHash允许我使用字节缓冲区或流。流将允许我高效地计算哈希值,但仅适用于整个文件而不仅仅是特定的块。
我考虑创建一个备用FileStream对象并将其传递给ComputeHash,在其中重载FileStream方法并对文件中的某个块进行只读操作。
是否有比这更好的解决方案,最好使用内置的C#库? 谢谢。

您可以使用TransformBlockTransformFinalBlock - CodesInChaos
但这仅适用于字节数组?我想使用流的原因是它不需要将整个块或文件存储在内存中来计算哈希。 - xander
ComputeHash(stream) 会分块读取流,对每个块调用 TransformBlock 方法,并在最后调用 TransformFinalBlock 方法。你不需要使用 1GB 的数组来计算 1GB 块的哈希值。 - CodesInChaos
是的,但它会从流的开头读取直到结尾吗?我想让它从流中特定位置开始读取,直到另一个位置或达到一定数量后停止。我不希望它一直哈希直到流的结尾。 - xander
5个回答

5
您可以直接使用TransformBlockTransformFinalBlock。这与HashAlgorithm.ComputeHash内部的操作非常相似。
例如:
using(var hashAlgorithm = new SHA256Managed())
using(var fileStream = new File.OpenRead(...))
{     
    fileStream.Position = ...;
    long bytesToHash = ...;

    var buf = new byte[4 * 1024];
    while(bytesToHash > 0)
    {
        var bytesRead = fileStream.Read(buf, 0, (int)Math.Min(bytesToHash, buf.Length));
        hashAlgorithm.TransformBlock(buf, 0, bytesRead, null, 0);
        bytesToHash -= bytesRead;
        if(bytesRead == 0)
            throw new InvalidOperationException("Unexpected end of stream");
    }
    hashAlgorithm.TransformFinalBlock(buf, 0, 0);
    var hash = hashAlgorithm.Hash;
    return hash;
};

4

您应该传入以下之一:

  • 包含要计算哈希值的数据块的字节数组
  • 限制访问要计算哈希值的数据块的流

第二个选项并不那么难,这里是我做的一个快速LINQPad程序。请注意,它缺少相当多的错误处理,例如检查块是否实际可用(即,您正在传递流的位置和长度实际存在且没有超出基础流的末尾)。

不用说,如果这将成为生产代码,我会添加大量的错误处理,并编写大量单元测试以确保正确处理所有边缘情况。

您可以像这样构造文件的PartialStream实例:

const long gb = 1024 * 1024 * 1024;
using (var fileStream = new FileStream(@"d:\temp\too_long_file.bin", FileMode.Open))
using (var chunk = new PartialStream(fileStream, 2 * gb, 1 * gb))
{
    var hash = hashAlgorithm.ComputeHash(chunk);
}

这是 LINQPad 的测试程序:

void Main()
{
    var buffer = Enumerable.Range(0, 256).Select(i => (byte)i).ToArray();
    using (var underlying = new MemoryStream(buffer))
    using (var partialStream = new PartialStream(underlying, 64, 32))
    {
        var temp = new byte[1024]; // too much, ensure we don't read past window end
        partialStream.Read(temp, 0, temp.Length);
        temp.Dump();
        // should output 64-95 and then 0's for the rest (64-95 = 32 bytes)
    }
}

public class PartialStream : Stream
{
    private readonly Stream _UnderlyingStream;
    private readonly long _Position;
    private readonly long _Length;

    public PartialStream(Stream underlyingStream, long position, long length)
    {
        if (!underlyingStream.CanRead || !underlyingStream.CanSeek)
            throw new ArgumentException("underlyingStream");

        _UnderlyingStream = underlyingStream;
        _Position = position;
        _Length = length;
        _UnderlyingStream.Position = position;
    }

    public override bool CanRead
    {
        get
        {
            return _UnderlyingStream.CanRead;
        }
    }

    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }

    public override bool CanSeek
    {
        get
        {
            return true;
        }
    }

    public override long Length
    {
        get
        {
            return _Length;
        }
    }

    public override long Position
    {
        get
        {
            return _UnderlyingStream.Position - _Position;
        }

        set
        {
            _UnderlyingStream.Position = value + _Position;
        }
    }

    public override void Flush()
    {
        throw new NotSupportedException();
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        switch (origin)
        {
            case SeekOrigin.Begin:
                return _UnderlyingStream.Seek(_Position + offset, SeekOrigin.Begin) - _Position;

            case SeekOrigin.End:
                return _UnderlyingStream.Seek(_Length + offset, SeekOrigin.Begin) - _Position;

            case SeekOrigin.Current:
                return _UnderlyingStream.Seek(offset, SeekOrigin.Current) - _Position;

            default:
                throw new ArgumentException("origin");
        }
    }

    public override void SetLength(long length)
    {
        throw new NotSupportedException();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        long left = _Length - Position;
        if (left < count)
            count = (int)left;
        return _UnderlyingStream.Read(buffer, offset, count);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        throw new NotSupportedException();
    }
}

2
您的建议是,通过传递一个受限访问包装器来清理您的FileStream,这是最干净的解决方案。您的包装器应该将所有内容推迟到包装的Stream中,除了 LengthPosition 属性。
如何实现呢?只需要创建一个继承自Stream的类即可。构造函数应该接收以下参数:
  • 您的源Stream(在您的情况下为FileStream
  • 块的起始位置
  • 块的结束位置
作为扩展,这是可用的所有Streams的列表:http://msdn.microsoft.com/en-us/library/system.io.stream%28v=vs.100%29.aspx#inheritanceContinued

实际上,在Read方法中,您还需要添加一些控件,否则您可能会读取超出“子流”的末尾。 - Thomas Levesque

1

要轻松计算较大流的一部分的哈希值,请使用以下两种方法:

这是一个 LINQPad 程序,演示了以下内容:

void Main()
{
    const long gb = 1024 * 1024 * 1024;
    using (var stream = new FileStream(@"d:\temp\largefile.bin", FileMode.Open))
    {
        stream.Position = 2 * gb; // 3rd gb-chunk
        byte[] buffer = new byte[32768];
        long amount = 1 * gb;

        using (var hashAlgorithm = SHA1.Create())
        {
            while (amount > 0)
            {
                int bytesRead = stream.Read(buffer, 0,
                    (int)Math.Min(buffer.Length, amount));

                if (bytesRead > 0)
                {
                    amount -= bytesRead;
                    if (amount > 0)
                        hashAlgorithm.TransformBlock(buffer, 0, bytesRead,
                            buffer, 0);
                    else
                        hashAlgorithm.TransformFinalBlock(buffer, 0, bytesRead);
                }
                else
                    throw new InvalidOperationException();
            }
            hashAlgorithm.Hash.Dump();
        }
    }
}

0
回答你最初的问题(“是否有更好的解决方案…”):
据我所知,没有更好的解决方案。
这似乎是一个非常特殊、非平凡的任务,因此可能需要一些额外的工作。我认为你使用自定义流类的方法是正确的方向,我可能会做同样的事情。
而且Gusdor和xander已经提供了非常有用的实现信息——干得好,伙计们!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接