public async Task WriteAsync(UserData userData, byte[] array, int offset, int count) { if (array == null) { throw new ArgumentNullException(nameof(array)); } if (offset < 0 || count < 0 || array.Length - offset < count) { throw new Exception($"Invalid range for array of length {array.Length}: [{offset}, {offset} + {count})"); } if (count > MaxContentLength) { throw new Exception($"Chunk too big: {count}"); } if (_torn) { await WritePadding(); _torn = false; } var meter = new Meter() { ChunkBeginPosition = _writer.Position }; var header = new ChunkHeader() { UserData = userData, ContentLength = count, ContentHash = SipHash.ComputeHash(array, offset, count), }; if (!header.EndPosition(meter.ChunkBeginPosition).HasValue) { throw new Exception($"File too big: {meter.ChunkBeginPosition}"); } meter.WriteTo(_meter); header.WriteTo(_header); try { await WriteMetered(_header, 0, _header.Length); await WriteMetered(array, offset, count); } catch { _torn = true; throw; } }
// Returns true if it's safe to read the chunk after the specified chunk. There are // two cases where blindly reading the next chunk can backfire: // // 1. By reading the next chunk you'll actually skip valid chunks. So the chunk you'll read // won't really be the next. // 2. Even if the next chunk decodes correctly (both its header and content hashes match), // it may be not a real chunk but a part of some larger chunk's content. // // To see these horrors in action, replace the implementation of this method with `return true` // and run tests. TrickyTruncateTest() and TrickyEmbedTest() should fail. They correspond to the // two cases described above. Note that there is no malicious action in these tests. The chunkio // files are produced by ChunkWriter. There are also file truncations, but they can naturally // happen when a processing with ChunkWriter crashes. async Task <bool> IsSkippable(long begin, ChunkHeader header) { long end = header.EndPosition(begin).Value; if (begin / MeterInterval == (end - 1) / MeterInterval) { return(true); } Meter?meter = await ReadMeter(MeterBefore(end - 1)); if (meter.HasValue) { return(meter.Value.ChunkBeginPosition == begin); } var content = new byte[header.ContentLength]; long pos = MeteredPosition(begin, ChunkHeader.Size).Value; return(await ReadMetered(pos, content, 0, content.Length) && SipHash.ComputeHash(content) == header.ContentHash); }
async Task <Meter?> ReadMeter(long pos) { Debug.Assert(pos >= 0 && pos % MeterInterval == 0); if (pos == 0) { return new Meter() { ChunkBeginPosition = 0 } } ; _reader.Seek(pos); if (await _reader.ReadAsync(_meter, 0, _meter.Length) != _meter.Length) { return(null); } var res = new Meter(); if (!res.ReadFrom(_meter)) { return(null); } if (res.ChunkBeginPosition > pos) { return(null); } return(res); } async Task <ChunkHeader?> ReadChunkHeader(long pos) { if (!await ReadMetered(pos, _header, 0, _header.Length)) { return(null); } var res = new ChunkHeader(); if (!res.ReadFrom(_header)) { return(null); } if (!res.EndPosition(pos).HasValue) { return(null); } return(res); } async Task <bool> ReadMetered(long pos, byte[] array, int offset, int count) { Debug.Assert(array != null); Debug.Assert(offset >= 0); Debug.Assert(array.Length - offset >= count); if (!(MeteredPosition(pos, count) <= Length)) { return(false); } while (count > 0) { Debug.Assert(IsValidPosition(pos)); if (pos % MeterInterval == 0) { pos += Meter.Size; } _reader.Seek(pos); int n = Math.Min(count, MeterInterval - (int)(pos % MeterInterval)); if (await _reader.ReadAsync(array, offset, n) != n) { return(false); } pos += n; offset += n; count -= n; } return(true); }