using System; using System.Text; using System.IO; namespace Be.Windows.Forms { /// /// Implements a fully editable byte provider for file data of any size. /// /// /// Only changes to the file are stored in memory with reads from the /// original data occurring as required. /// public sealed class DynamicFileByteProvider : IByteProvider, IDisposable { const int COPY_BLOCK_SIZE = 4096; string _fileName; Stream _stream; DataMap _dataMap; long _totalLength; bool _readOnly; /// /// Constructs a new instance. /// /// The name of the file from which bytes should be provided. public DynamicFileByteProvider(string fileName) : this(fileName, false) { } /// /// Constructs a new instance. /// /// The name of the file from which bytes should be provided. /// True, opens the file in read-only mode. public DynamicFileByteProvider(string fileName, bool readOnly) { _fileName = fileName; if (!readOnly) { _stream = File.Open(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.Read); } else { _stream = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } _readOnly = readOnly; ReInitialize(); } /// /// Constructs a new instance. /// /// the stream containing the data. /// /// The stream must supported seek operations. /// public DynamicFileByteProvider(Stream stream) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanSeek) throw new ArgumentException("stream must supported seek operations(CanSeek)"); _stream = stream; _readOnly = !stream.CanWrite; ReInitialize(); } #region IByteProvider Members /// /// See for more information. /// public event EventHandler LengthChanged; /// /// See for more information. /// public event EventHandler Changed; /// /// See for more information. /// public byte ReadByte(long index) { long blockOffset; DataBlock block = GetDataBlock(index, out blockOffset); FileDataBlock fileBlock = block as FileDataBlock; if (fileBlock != null) { return ReadByteFromFile(fileBlock.FileOffset + index - blockOffset); } else { MemoryDataBlock memoryBlock = (MemoryDataBlock)block; return memoryBlock.Data[index - blockOffset]; } } /// /// See for more information. /// public void WriteByte(long index, byte value) { try { // Find the block affected. long blockOffset; DataBlock block = GetDataBlock(index, out blockOffset); // If the byte is already in a memory block, modify it. MemoryDataBlock memoryBlock = block as MemoryDataBlock; if (memoryBlock != null) { memoryBlock.Data[index - blockOffset] = value; return; } FileDataBlock fileBlock = (FileDataBlock)block; // If the byte changing is the first byte in the block and the previous block is a memory block, extend that. if (blockOffset == index && block.PreviousBlock != null) { MemoryDataBlock previousMemoryBlock = block.PreviousBlock as MemoryDataBlock; if (previousMemoryBlock != null) { previousMemoryBlock.AddByteToEnd(value); fileBlock.RemoveBytesFromStart(1); if (fileBlock.Length == 0) { _dataMap.Remove(fileBlock); } return; } } // If the byte changing is the last byte in the block and the next block is a memory block, extend that. if (blockOffset + fileBlock.Length - 1 == index && block.NextBlock != null) { MemoryDataBlock nextMemoryBlock = block.NextBlock as MemoryDataBlock; if (nextMemoryBlock != null) { nextMemoryBlock.AddByteToStart(value); fileBlock.RemoveBytesFromEnd(1); if (fileBlock.Length == 0) { _dataMap.Remove(fileBlock); } return; } } // Split the block into a prefix and a suffix and place a memory block in-between. FileDataBlock prefixBlock = null; if (index > blockOffset) { prefixBlock = new FileDataBlock(fileBlock.FileOffset, index - blockOffset); } FileDataBlock suffixBlock = null; if (index < blockOffset + fileBlock.Length - 1) { suffixBlock = new FileDataBlock( fileBlock.FileOffset + index - blockOffset + 1, fileBlock.Length - (index - blockOffset + 1)); } block = _dataMap.Replace(block, new MemoryDataBlock(value)); if (prefixBlock != null) { _dataMap.AddBefore(block, prefixBlock); } if (suffixBlock != null) { _dataMap.AddAfter(block, suffixBlock); } } finally { OnChanged(EventArgs.Empty); } } /// /// See for more information. /// public void InsertBytes(long index, byte[] bs) { try { // Find the block affected. long blockOffset; DataBlock block = GetDataBlock(index, out blockOffset); // If the insertion point is in a memory block, just insert it. MemoryDataBlock memoryBlock = block as MemoryDataBlock; if (memoryBlock != null) { memoryBlock.InsertBytes(index - blockOffset, bs); return; } FileDataBlock fileBlock = (FileDataBlock)block; // If the insertion point is at the start of a file block, and the previous block is a memory block, append it to that block. if (blockOffset == index && block.PreviousBlock != null) { MemoryDataBlock previousMemoryBlock = block.PreviousBlock as MemoryDataBlock; if (previousMemoryBlock != null) { previousMemoryBlock.InsertBytes(previousMemoryBlock.Length, bs); return; } } // Split the block into a prefix and a suffix and place a memory block in-between. FileDataBlock prefixBlock = null; if (index > blockOffset) { prefixBlock = new FileDataBlock(fileBlock.FileOffset, index - blockOffset); } FileDataBlock suffixBlock = null; if (index < blockOffset + fileBlock.Length) { suffixBlock = new FileDataBlock( fileBlock.FileOffset + index - blockOffset, fileBlock.Length - (index - blockOffset)); } block = _dataMap.Replace(block, new MemoryDataBlock(bs)); if (prefixBlock != null) { _dataMap.AddBefore(block, prefixBlock); } if (suffixBlock != null) { _dataMap.AddAfter(block, suffixBlock); } } finally { _totalLength += bs.Length; OnLengthChanged(EventArgs.Empty); OnChanged(EventArgs.Empty); } } /// /// See for more information. /// public void DeleteBytes(long index, long length) { try { long bytesToDelete = length; // Find the first block affected. long blockOffset; DataBlock block = GetDataBlock(index, out blockOffset); // Truncate or remove each block as necessary. while ((bytesToDelete > 0) && (block != null)) { long blockLength = block.Length; DataBlock nextBlock = block.NextBlock; // Delete the appropriate section from the block (this may result in two blocks or a zero length block). long count = Math.Min(bytesToDelete, blockLength - (index - blockOffset)); block.RemoveBytes(index - blockOffset, count); if (block.Length == 0) { _dataMap.Remove(block); if (_dataMap.FirstBlock == null) { _dataMap.AddFirst(new MemoryDataBlock(new byte[0])); } } bytesToDelete -= count; blockOffset += block.Length; block = (bytesToDelete > 0) ? nextBlock : null; } } finally { _totalLength -= length; OnLengthChanged(EventArgs.Empty); OnChanged(EventArgs.Empty); } } /// /// See for more information. /// public long Length { get { return _totalLength; } } /// /// See for more information. /// public bool HasChanges() { if (_readOnly) return false; if (_totalLength != _stream.Length) { return true; } long offset = 0; for (DataBlock block = _dataMap.FirstBlock; block != null; block = block.NextBlock) { FileDataBlock fileBlock = block as FileDataBlock; if (fileBlock == null) { return true; } if (fileBlock.FileOffset != offset) { return true; } offset += fileBlock.Length; } return (offset != _stream.Length); } /// /// See for more information. /// public void ApplyChanges() { if (_readOnly) throw new OperationCanceledException("File is in read-only mode"); // This method is implemented to efficiently save the changes to the same file stream opened for reading. // Saving to a separate file would be a much simpler implementation. // Firstly, extend the file length (if necessary) to ensure that there is enough disk space. if (_totalLength > _stream.Length) { _stream.SetLength(_totalLength); } // Secondly, shift around any file sections that have moved. long dataOffset = 0; for (DataBlock block = _dataMap.FirstBlock; block != null; block = block.NextBlock) { FileDataBlock fileBlock = block as FileDataBlock; if (fileBlock != null && fileBlock.FileOffset != dataOffset) { MoveFileBlock(fileBlock, dataOffset); } dataOffset += block.Length; } // Next, write in-memory changes. dataOffset = 0; for (DataBlock block = _dataMap.FirstBlock; block != null; block = block.NextBlock) { MemoryDataBlock memoryBlock = block as MemoryDataBlock; if (memoryBlock != null) { _stream.Position = dataOffset; for (int memoryOffset = 0; memoryOffset < memoryBlock.Length; memoryOffset += COPY_BLOCK_SIZE) { _stream.Write(memoryBlock.Data, memoryOffset, (int)Math.Min(COPY_BLOCK_SIZE, memoryBlock.Length - memoryOffset)); } } dataOffset += block.Length; } // Finally, if the file has shortened, truncate the stream. _stream.SetLength(_totalLength); ReInitialize(); } /// /// See for more information. /// public bool SupportsWriteByte() { return !_readOnly; } /// /// See for more information. /// public bool SupportsInsertBytes() { return !_readOnly; } /// /// See for more information. /// public bool SupportsDeleteBytes() { return !_readOnly; } #endregion #region IDisposable Members /// /// See for more information. /// ~DynamicFileByteProvider() { Dispose(); } /// /// See for more information. /// public void Dispose() { if (_stream != null) { _stream.Close(); _stream = null; } _fileName = null; _dataMap = null; GC.SuppressFinalize(this); } #endregion /// /// Gets a value, if the file is opened in read-only mode. /// public bool ReadOnly { get { return _readOnly; } } void OnLengthChanged(EventArgs e) { if (LengthChanged != null) LengthChanged(this, e); } void OnChanged(EventArgs e) { if (Changed != null) { Changed(this, e); } } DataBlock GetDataBlock(long findOffset, out long blockOffset) { if (findOffset < 0 || findOffset > _totalLength) { throw new ArgumentOutOfRangeException("findOffset"); } // Iterate over the blocks until the block containing the required offset is encountered. blockOffset = 0; for (DataBlock block = _dataMap.FirstBlock; block != null; block = block.NextBlock) { if ((blockOffset <= findOffset && blockOffset + block.Length > findOffset) || block.NextBlock == null) { return block; } blockOffset += block.Length; } return null; } FileDataBlock GetNextFileDataBlock(DataBlock block, long dataOffset, out long nextDataOffset) { // Iterate over the remaining blocks until a file block is encountered. nextDataOffset = dataOffset + block.Length; block = block.NextBlock; while (block != null) { FileDataBlock fileBlock = block as FileDataBlock; if (fileBlock != null) { return fileBlock; } nextDataOffset += block.Length; block = block.NextBlock; } return null; } byte ReadByteFromFile(long fileOffset) { // Move to the correct position and read the byte. if (_stream.Position != fileOffset) { _stream.Position = fileOffset; } return (byte)_stream.ReadByte(); } void MoveFileBlock(FileDataBlock fileBlock, long dataOffset) { // First, determine whether the next file block needs to move before this one. long nextDataOffset; FileDataBlock nextFileBlock = GetNextFileDataBlock(fileBlock, dataOffset, out nextDataOffset); if (nextFileBlock != null && dataOffset + fileBlock.Length > nextFileBlock.FileOffset) { // The next block needs to move first, so do that now. MoveFileBlock(nextFileBlock, nextDataOffset); } // Now, move the block. if (fileBlock.FileOffset > dataOffset) { // Move the section to earlier in the file stream (done in chunks starting at the beginning of the section). byte[] buffer = new byte[COPY_BLOCK_SIZE]; for (long relativeOffset = 0; relativeOffset < fileBlock.Length; relativeOffset += buffer.Length) { long readOffset = fileBlock.FileOffset + relativeOffset; int bytesToRead = (int)Math.Min(buffer.Length, fileBlock.Length - relativeOffset); _stream.Position = readOffset; _stream.Read(buffer, 0, bytesToRead); long writeOffset = dataOffset + relativeOffset; _stream.Position = writeOffset; _stream.Write(buffer, 0, bytesToRead); } } else { // Move the section to later in the file stream (done in chunks starting at the end of the section). byte[] buffer = new byte[COPY_BLOCK_SIZE]; for (long relativeOffset = 0; relativeOffset < fileBlock.Length; relativeOffset += buffer.Length) { int bytesToRead = (int)Math.Min(buffer.Length, fileBlock.Length - relativeOffset); long readOffset = fileBlock.FileOffset + fileBlock.Length - relativeOffset - bytesToRead; _stream.Position = readOffset; _stream.Read(buffer, 0, bytesToRead); long writeOffset = dataOffset + fileBlock.Length - relativeOffset - bytesToRead; _stream.Position = writeOffset; _stream.Write(buffer, 0, bytesToRead); } } // This block now points to a different position in the file. fileBlock.SetFileOffset(dataOffset); } void ReInitialize() { _dataMap = new DataMap(); _dataMap.AddFirst(new FileDataBlock(0, _stream.Length)); _totalLength = _stream.Length; } } }