/// <summary> /// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used /// (by <see cref="ApplyAsync"/>) to transform <paramref name="oldData"/> into <paramref name="newData"/>. /// </summary> /// <param name="oldData">The original binary data.</param> /// <param name="newData">The new binary data.</param> /// <param name="output">A <see cref="Stream"/> to which the patch will be written.</param> /// <param name="cancellationToken"></param> public async Task CreateAsync(ReadOnlyMemory <byte> oldData, ReadOnlyMemory <byte> newData, Stream output, CancellationToken cancellationToken) { // check arguments if (oldData.IsEmpty) { throw new ArgumentNullException(nameof(oldData)); } if (newData.IsEmpty) { throw new ArgumentNullException(nameof(newData)); } if (output == null) { throw new ArgumentNullException(nameof(output)); } if (!output.CanSeek) { throw new ArgumentException("Output stream must be seekable.", nameof(output)); } if (!output.CanWrite) { throw new ArgumentException("Output stream must be writable.", nameof(output)); } var header = ArrayPool <byte> .Shared.Rent(CHeaderSize); var db = ArrayPool <byte> .Shared.Rent(newData.Length); var eb = ArrayPool <byte> .Shared.Rent(newData.Length); try { await DiffAsync(); } finally { ArrayPool <byte> .Shared.Return(header); ArrayPool <byte> .Shared.Return(db); ArrayPool <byte> .Shared.Return(eb); } async Task DiffAsync() { /* Header is * 0 8 "BSDIFF40" * 8 8 length of bzip2ed ctrl block * 16 8 length of bzip2ed diff block * 24 8 length of new file */ /* File is * 0 32 Header * 32 ?? Bzip2ed ctrl block * ?? ?? Bzip2ed diff block * ?? ?? Bzip2ed extra block */ WriteInt64(CFileSignature, header, 0); // "BSDIFF40" WriteInt64(0, header, 8); WriteInt64(0, header, 16); WriteInt64(newData.Length, header, 24); var startPosition = output.Position; await output.WriteAsync(header.AsMemory(0, header.Length), cancellationToken); var I = SuffixSort(oldData); var dblen = 0; var eblen = 0; await using (var wrappingStream = new WrappingStream(output, Ownership.None)) { await using var bz2Stream = new BZip2Stream(wrappingStream, CompressionMode.Compress, true); // compute the differences, writing ctrl as we go var scan = 0; var pos = 0; var len = 0; var lastscan = 0; var lastpos = 0; var lastoffset = 0; while (scan < newData.Length) { var oldscore = 0; for (var scsc = scan += len; scan < newData.Length; scan++) { len = Search(I, oldData, newData, scan, 0, oldData.Length, out pos); for (; scsc < scan + len; scsc++) { if (scsc + lastoffset < oldData.Length && oldData.Span[scsc + lastoffset] == newData.Span[scsc]) { oldscore++; } } if (len == oldscore && len != 0 || len > oldscore + 8) { break; } if (scan + lastoffset < oldData.Length && oldData.Span[scan + lastoffset] == newData.Span[scan]) { oldscore--; } } if (len != oldscore || scan == newData.Length) { var s = 0; var sf = 0; var lenf = 0; for (var i = 0; lastscan + i < scan && lastpos + i < oldData.Length;) { if (oldData.Span[lastpos + i] == newData.Span[lastscan + i]) { s++; } i++; if (s * 2 - i > sf * 2 - lenf) { sf = s; lenf = i; } } var lenb = 0; if (scan < newData.Length) { s = 0; var sb = 0; for (var i = 1; scan >= lastscan + i && pos >= i; i++) { if (oldData.Span[pos - i] == newData.Span[scan - i]) { s++; } if (s * 2 - i > sb * 2 - lenb) { sb = s; lenb = i; } } } if (lastscan + lenf > scan - lenb) { var overlap = lastscan + lenf - (scan - lenb); s = 0; var ss = 0; var lens = 0; for (var i = 0; i < overlap; i++) { if (newData.Span[lastscan + lenf - overlap + i] == oldData.Span[lastpos + lenf - overlap + i]) { s++; } if (newData.Span[scan - lenb + i] == oldData.Span[pos - lenb + i]) { s--; } if (s > ss) { ss = s; lens = i + 1; } } lenf += lens - overlap; lenb -= lens; } for (var i = 0; i < lenf; i++) { db[dblen + i] = (byte)(newData.Span[lastscan + i] - oldData.Span[lastpos + i]); } for (var i = 0; i < scan - lenb - (lastscan + lenf); i++) { eb[eblen + i] = newData.Span[lastscan + lenf + i]; } dblen += lenf; eblen += scan - lenb - (lastscan + lenf); var buf = ArrayPool <byte> .Shared.Rent(8); try { WriteInt64(lenf, buf, 0); await bz2Stream.WriteAsync(buf.AsMemory(0, 8), cancellationToken); WriteInt64(scan - lenb - (lastscan + lenf), buf, 0); await bz2Stream.WriteAsync(buf.AsMemory(0, 8), cancellationToken); WriteInt64(pos - lenb - (lastpos + lenf), buf, 0); await bz2Stream.WriteAsync(buf.AsMemory(0, 8), cancellationToken); } finally { ArrayPool <byte> .Shared.Return(buf); } lastscan = scan - lenb; lastpos = pos - lenb; lastoffset = pos - scan; } } } // compute size of compressed ctrl data var controlEndPosition = output.Position; WriteInt64(controlEndPosition - startPosition - CHeaderSize, header, 8); // write compressed diff data await using (var wrappingStream = new WrappingStream(output, Ownership.None)) { await using var bz2Stream = new BZip2Stream(wrappingStream, CompressionMode.Compress, true); await bz2Stream.WriteAsync(db.AsMemory(0, dblen), cancellationToken); } // compute size of compressed diff data var diffEndPosition = output.Position; WriteInt64(diffEndPosition - controlEndPosition, header, 16); // write compressed extra data, if any if (eblen > 0) { await using var wrappingStream = new WrappingStream(output, Ownership.None); await using var bz2Stream = new BZip2Stream(wrappingStream, CompressionMode.Compress, true); await bz2Stream.WriteAsync(eb.AsMemory(0, eblen), cancellationToken); } // seek to the beginning, write the header, then seek back to end var endPosition = output.Position; output.Position = startPosition; await output.WriteAsync(header.AsMemory(0, header.Length), cancellationToken); output.Position = endPosition; } }
static void CreateInternal(byte[] oldData, byte[] newData, Stream output) { // check arguments if (oldData == null) { throw new ArgumentNullException("oldData"); } if (newData == null) { throw new ArgumentNullException("newData"); } if (output == null) { throw new ArgumentNullException("output"); } if (!output.CanSeek) { throw new ArgumentException("Output stream must be seekable.", "output"); } if (!output.CanWrite) { throw new ArgumentException("Output stream must be writable.", "output"); } /* Header is * 0 8 "BSDIFF40" * 8 8 length of bzip2ed ctrl block * 16 8 length of bzip2ed diff block * 24 8 length of new file */ /* File is * 0 32 Header * 32 ?? Bzip2ed ctrl block * ?? ?? Bzip2ed diff block * ?? ?? Bzip2ed extra block */ byte[] header = new byte[c_headerSize]; WriteInt64(c_fileSignature, header, 0); // "BSDIFF40" WriteInt64(0, header, 8); WriteInt64(0, header, 16); WriteInt64(newData.Length, header, 24); long startPosition = output.Position; output.Write(header, 0, header.Length); int[] I = SuffixSort(oldData); byte[] db = new byte[newData.Length]; byte[] eb = new byte[newData.Length]; int dblen = 0; int eblen = 0; using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None)) { using var bz2Stream = new BZip2Stream(wrappingStream, CompressionMode.Compress, true); // compute the differences, writing ctrl as we go int scan = 0; int pos = 0; int len = 0; int lastscan = 0; int lastpos = 0; int lastoffset = 0; while (scan < newData.Length) { int oldscore = 0; for (int scsc = scan += len; scan < newData.Length; scan++) { len = Search(I, oldData, newData, scan, 0, oldData.Length, out pos); for (; scsc < scan + len; scsc++) { if ((scsc + lastoffset < oldData.Length) && (oldData[scsc + lastoffset] == newData[scsc])) { oldscore++; } } if ((len == oldscore && len != 0) || (len > oldscore + 8)) { break; } if ((scan + lastoffset < oldData.Length) && (oldData[scan + lastoffset] == newData[scan])) { oldscore--; } } if (len != oldscore || scan == newData.Length) { int s = 0; int sf = 0; int lenf = 0; for (int i = 0; (lastscan + i < scan) && (lastpos + i < oldData.Length);) { if (oldData[lastpos + i] == newData[lastscan + i]) { s++; } i++; if (s * 2 - i > sf * 2 - lenf) { sf = s; lenf = i; } } int lenb = 0; if (scan < newData.Length) { s = 0; int sb = 0; for (int i = 1; (scan >= lastscan + i) && (pos >= i); i++) { if (oldData[pos - i] == newData[scan - i]) { s++; } if (s * 2 - i > sb * 2 - lenb) { sb = s; lenb = i; } } } if (lastscan + lenf > scan - lenb) { int overlap = (lastscan + lenf) - (scan - lenb); s = 0; int ss = 0; int lens = 0; for (int i = 0; i < overlap; i++) { if (newData[lastscan + lenf - overlap + i] == oldData[lastpos + lenf - overlap + i]) { s++; } if (newData[scan - lenb + i] == oldData[pos - lenb + i]) { s--; } if (s > ss) { ss = s; lens = i + 1; } } lenf += lens - overlap; lenb -= lens; } for (int i = 0; i < lenf; i++) { db[dblen + i] = (byte)(newData[lastscan + i] - oldData[lastpos + i]); } for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++) { eb[eblen + i] = newData[lastscan + lenf + i]; } dblen += lenf; eblen += (scan - lenb) - (lastscan + lenf); byte[] buf = new byte[8]; WriteInt64(lenf, buf, 0); bz2Stream.Write(buf, 0, 8); WriteInt64((scan - lenb) - (lastscan + lenf), buf, 0); bz2Stream.Write(buf, 0, 8); WriteInt64((pos - lenb) - (lastpos + lenf), buf, 0); bz2Stream.Write(buf, 0, 8); lastscan = scan - lenb; lastpos = pos - lenb; lastoffset = pos - scan; } } } // compute size of compressed ctrl data long controlEndPosition = output.Position; WriteInt64(controlEndPosition - startPosition - c_headerSize, header, 8); // write compressed diff data using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None)) { using var bz2Stream = new BZip2Stream(wrappingStream, CompressionMode.Compress, true); bz2Stream.Write(db, 0, dblen); } // compute size of compressed diff data long diffEndPosition = output.Position; WriteInt64(diffEndPosition - controlEndPosition, header, 16); // write compressed extra data, if any if (eblen > 0) { using WrappingStream wrappingStream = new WrappingStream(output, Ownership.None); using var bz2Stream = new BZip2Stream(wrappingStream, CompressionMode.Compress, true); bz2Stream.Write(eb, 0, eblen); } // seek to the beginning, write the header, then seek back to end long endPosition = output.Position; output.Position = startPosition; output.Write(header, 0, header.Length); output.Position = endPosition; }