设计健壮文件格式写入器:写入流时如何防范流位置被外部篡改
通用Stream场景下防位置篡改的WAV写入器实现方案
核心思路是对外部传入的Stream做一层透明代理包装,隔离外部对原始Stream的直接操作影响,不需要全量缓存数据,也不限制Stream的类型,完美适配通用写入需求。
具体实现方案
- 第一步:实现自定义
WavEnforcedStream代理类,继承Stream抽象类,内部持有传入的原始Stream实例,同时维护两个核心字段:_expectedPosition记录写入器预期的当前流位置,_ownsStream可选标识是否需要在释放时同步释放原始Stream。 - 第二步:重写Stream的所有核心方法,做写入位置的强校验:
- 每次执行写入操作前,先判断原始Stream的当前Position和内部记录的
_expectedPosition是否一致,如果不一致且Stream支持Seek,就主动将Stream定位到_expectedPosition再执行写入。 - 写入完成后同步更新
_expectedPosition为写入后的偏移值。 - 对外暴露的Position设置、Seek方法直接抛出
NotSupportedException,禁止外部通过代理修改写入位置,仅留内部访问的定位方法供写入器更新文件头使用。 - 不需要用到的读取等方法直接抛出不支持异常,仅保留写入、刷新等写入器必要的能力。
- 每次执行写入操作前,先判断原始Stream的当前Position和内部记录的
- 第三步:WavWriter构造函数接收外部传入的Stream后,直接用该代理类包裹,后续写入器的所有内部操作都只针对代理实例执行,完全不直接操作原始Stream。
边界场景适配
- 针对不支持Seek的单向流(如网络流、Pipe流):因为这类流本身无法回退定位,只需要做位置一致性校验即可,如果检测到外部修改了流位置导致当前偏移和预期不一致,直接抛出不可恢复异常即可,不需要额外做位置修正。
- 针对WAV头更新需求:提前记录WAV头中长度字段的固定偏移位置,在Dispose需要更新头部时,调用代理类的内部定点写入方法,写完头后自动回到之前的写入位置即可,不会打乱整体写入流程。
代码示例
代理Stream实现
internal class WavEnforcedStream : Stream { private readonly Stream _underlyingStream; private long _expectedPosition; private readonly bool _ownsStream; public WavEnforcedStream(Stream underlyingStream, bool ownsStream = false) { _underlyingStream = underlyingStream ?? throw new ArgumentNullException(nameof(underlyingStream)); if (_underlyingStream.CanSeek) { _expectedPosition = _underlyingStream.Position; } _ownsStream = ownsStream; } public override void Write(byte[] buffer, int offset, int count) { if (_underlyingStream.CanSeek && _underlyingStream.Position != _expectedPosition) { _underlyingStream.Seek(_expectedPosition, SeekOrigin.Begin); } _underlyingStream.Write(buffer, offset, count); _expectedPosition += count; } // 内部方法,仅供给WavWriter更新头部时定点写入使用 internal void WriteAt(long position, byte[] buffer, int offset, int count) { if (!_underlyingStream.CanSeek) throw new NotSupportedException("非可定位流不支持定点写入操作。"); var originalExpectedPos = _expectedPosition; _underlyingStream.Seek(position, SeekOrigin.Begin); _underlyingStream.Write(buffer, offset, count); // 写入完成后回到之前的预期位置 _underlyingStream.Seek(originalExpectedPos, SeekOrigin.Begin); } // 其余Stream抽象成员实现 public override bool CanRead => false; public override bool CanSeek => _underlyingStream.CanSeek; public override bool CanWrite => _underlyingStream.CanWrite; public override long Length => _underlyingStream.Length; public override long Position { get => _expectedPosition; set => throw new NotSupportedException("不支持直接设置流位置。"); } public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException("该流仅支持写入操作。"); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException("不支持直接调用Seek方法。"); public override void SetLength(long value) => _underlyingStream.SetLength(value); public override void Flush() => _underlyingStream.Flush(); protected override void Dispose(bool disposing) { if (disposing && _ownsStream) { _underlyingStream.Dispose(); } base.Dispose(disposing); } }
WavWriter核心逻辑
public class WavWriter : IDisposable { private readonly WavEnforcedStream _enforcedStream; // WAV RIFF头中总长度字段的固定偏移为4字节 private const long FileLengthFieldOffset = 4; private long _totalPayloadBytes = 0; public WavWriter(Stream outputStream, WavFormat formatParams) { _enforcedStream = new WavEnforcedStream(outputStream); // 写入初始WAV头 WriteInitialHeader(formatParams); } public void WriteSamples(byte[] sampleBuffer, int offset, int count) { _enforcedStream.Write(sampleBuffer, offset, count); _totalPayloadBytes += count; } public void Dispose() { // 计算最终WAV文件总长度,更新头字段 var totalFileLength = _totalPayloadBytes + 36; // 标准WAV长度计算规则 var lengthBytes = BitConverter.GetBytes(totalFileLength); if (BitConverter.IsLittleEndian == false) { Array.Reverse(lengthBytes); } _enforcedStream.WriteAt(FileLengthFieldOffset, lengthBytes, 0, 4); _enforcedStream.Flush(); _enforcedStream.Dispose(); } private void WriteInitialHeader(WavFormat format) { // 省略WAV头写入的具体逻辑,写入后同步更新_totalPayloadBytes的初始偏移即可 } } // WAV格式参数定义示例 public class WavFormat { public int SampleRate { get; set; } public int BitsPerSample { get; set; } public int Channels { get; set; } }
方案优势
- 无额外内存开销:所有写入操作直接落到原始Stream,没有全量缓存数据的环节,不会出现大文件写入内存溢出的问题。
- 适配所有Stream类型:不管是本地文件流、内存流、网络流都可以兼容,不需要写入器持有Stream的所有权,外部依然可以正常操作原始Stream,就算外部修改了Stream位置,下一次写入器写入时会自动修正回预期位置,不会被外部操作影响。
- 兼容性强:对外接口和原来的WavWriter完全一致,使用者不需要修改任何调用代码就可以获得防位置篡改的能力。
内容的提问来源于stack exchange,提问作者aybe
相关产品推荐
相关产品推荐

