在这种方式下使用FileStream.seek是否安全?

3
假设我有一种文件格式,它由一系列对象组成,其中每个对象都有以下格式的标题:
public struct FileObjectHeader {
    //The type of the object (not important for this question, but it exists)
    public byte TypeID;
    //The length of the object's data, which DOES NOT include the size of the header.
    public UInt16 Length;
}

紧随其后是指定长度的数据。

我首先为每个对象和对象头创建位置列表,然后读取这些数据:

struct FileObjectIndex {
    public FileObjectHeader Header;
    public long Location;
}

public List<FileObject> ReadObjects(Stream s) {
    List<FileObjectReference> objectRefs = new List<FileObjectReference>();

    try {
        while (true) {
            FileObjectHeader header = ReadObjectHeader(s); 
            //The above advances the stream by the size of the header as well.
            FileObjectReference reference = new FileObjectReference() { Header = header, Position = stream.Position };
            objectRefs.add(reference);
            //Advance the stream to the next object's header.
            s.Seek(header.Length, SeekOrigin.Current);
        }
    } catch (EndOfStreamException) {
        //Do nothing as this is an expected case
    }

    //Now we'd read all of the objects that we've previously located.
    //This code isn't too important for the question but I'm including it for reference.
    List<FileObject> objects = new List<FileObject>();
    foreach (var reference in objectRefs) {
        s.seek(reference.Location, SeekOrigin.Begin);

        objects.add(ReadObject(reference.Header, s));
    }

    return objects;
}

一些注意事项:
  • 如果ReadObjectHeaderReadObject方法无法读取所有所需数据(即到达流的末尾),它们将抛出EndOfStreamException异常。
  • 我在这里使用Seek方法是因为对象可以引用其他对象,而且还有逻辑来确保父对象在其子对象之前加载(文件格式没有保证父对象位于子对象之前)。 我没有在上面的示例代码中包含它,因为这会使示例变得复杂,但它不会改进示例。
  • 在大多数情况下,这可能是只读的FileStream,但我不能保证。 但是,对于这种情况,我主要担心FileStream。
我的问题是:
由于我正在使用FileStream.seek,是否有可能导致超出流的末尾并无限扩展文件的情况? 根据文档:

您可以搜索超过流的任何位置。 当您搜索超过文件长度时,文件大小会增加。 在Windows NT及更高版本中,添加到文件末尾的数据设置为零。 在Windows 98或早期版本中,添加到文件末尾的数据未设置为零,这意味着之前删除的数据对流是可见的。

这种说法似乎表明它可能会在没有我扩展文件的情况下扩展文件,因为它从头文件中读取3个字节。 实际上,似乎不会发生这种情况,但我想确认它不会发生。

1
我可能漏掉了一些东西,但如果你害怕超过原始长度并且只进行读取,为什么不在开始时捕获最大查找位置,然后进行检查以确保不会超过它?我认为有关查找超过的文档是指如果您返回到位置0,然后保存流或在需要原始大小的地方使用它--它将比您最初读取的要大。 - TyCobb
1
我不相信 Seek 实际上 会改变文件本身... 如果你向文件写入内容,那么它会增长。请注意,无论如何,您都应该以 FileAccess.Read 打开文件,因此没有任何操作会更改文件... 请查看参考源以获取确切的详细信息。 - Alexei Levenkov
2个回答

3
对于FileStream.Read()的文档说明如下:

返回值
类型: System.Int32
已读取到缓冲区中的总字节数。如果当前没有请求的字节数,可能会少于请求的字节数,或者如果达到流的结尾,则为零

因此我强烈怀疑(但您应该自行验证)只有在之后向文件写入时才适用于超出末尾的寻找。这是有道理的-如果您知道需要空间,可以保留空间,而不实际写入任何内容(这会很慢)。
然而,在读取时,我的猜测是您应该得到0的返回值,并且不会读取任何数据。也不会进行文件扩展。

2
简单回答你的问题,以下代码不会使你的文件变大。但是它会抛出新的EndOfStreamException()。只有在超出文件末尾位置写入时,文件才会变大。当文件变大时,当前文件末尾和你要写入的起始位置之间的数据将被填充为零(除非你启用了稀疏标志,否则将被标记为未分配)。
using (var fileStream = new FileStream("f", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
    var buffer = new byte[10];
    fileStream.Seek(10, SeekOrigin.Begin);
    var bytesRead = fileStream.Read(buffer, 0, 10);
    if (bytesRead == 0) {
        throw new EndOfStreamException();
    }
}

由于你正在读写二进制结构化数据,我建议以下三点:

  1. Your binary structured data should have an integral number of elements in a disk block. On most systems this is 4096 MSDN. Doing this will allow the CLR to read data directly from the FileSystem cache into your buffer.
  2. Use MemoryMappedFile, and unsafe pointers to access your data (if your app will run on windows only). You can also use a ViewAccessor, but you may find this to be slower than doing the caching yourself due to the extra copies made by interop. If you go the unsafe route, here is code which will quickly fill your structure:

    internal static class Native
    {
        [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
        private static unsafe extern void CopyMemory(void *dest, void *src, int count);
    
        private static unsafe byte[] Serialize(TestStruct[] index)
        {
            var buffer = new byte[Marshal.SizeOf(typeof(TestStruct)) * index.Length];
            fixed (void* d = &index[0])
            {
                fixed (void* s = &buffer[0])
                {
                    CopyMemory(d, s, buffer.Length);
                }
            }
    
            return buffer;
        }
    }
    

这是非常有用的信息。不过,有一件事我认为你应该更正:在C#中if (bytesRead)是无效的,因为它“无法隐式转换类型'int'为'bool'”,所以可能应该是if (bytesRead != 0)或者if (bytesRead != 10)。我还纠正了第二个代码块的格式——列表格式会影响它,需要再缩进一次。 - Pokechu22

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